Skip to content

Commit

Permalink
draft: fs: support renameExchange in loopback for darwin
Browse files Browse the repository at this point in the history
Tested on MacOS 14.2 (macfuse 4.5.0) and Alpine 3.18 in Docker (Linux 6.4)

Change-Id: I3f4f92c2d2aa3f4fc3696b661fa3cb0e3b43282b
  • Loading branch information
dsxack committed Nov 11, 2023
1 parent 5b76a74 commit abe7ee0
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 45 deletions.
36 changes: 36 additions & 0 deletions fs/loopback.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"syscall"

"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/internal/renameat"
)

// LoopbackRoot holds the parameters for creating a new loopback
Expand Down Expand Up @@ -218,6 +219,41 @@ func (n *LoopbackNode) Create(ctx context.Context, name string, flags uint32, mo
return ch, lf, 0, 0
}

func (n *LoopbackNode) renameExchange(name string, newparent InodeEmbedder, newName string) syscall.Errno {
fd1, err := syscall.Open(n.path(), syscall.O_DIRECTORY, 0)
if err != nil {
return ToErrno(err)
}
defer syscall.Close(fd1)
p2 := filepath.Join(n.RootData.Path, newparent.EmbeddedInode().Path(nil))
fd2, err := syscall.Open(p2, syscall.O_DIRECTORY, 0)
defer syscall.Close(fd2)
if err != nil {
return ToErrno(err)
}

var st syscall.Stat_t
if err := syscall.Fstat(fd1, &st); err != nil {
return ToErrno(err)
}

// Double check that nodes didn't change from under us.
inode := &n.Inode
if inode.Root() != inode && inode.StableAttr().Ino != n.RootData.idFromStat(&st).Ino {
return syscall.EBUSY
}
if err := syscall.Fstat(fd2, &st); err != nil {
return ToErrno(err)
}

newinode := newparent.EmbeddedInode()
if newinode.Root() != newinode && newinode.StableAttr().Ino != n.RootData.idFromStat(&st).Ino {
return syscall.EBUSY
}

return ToErrno(renameat.Renameat(fd1, name, fd2, newName, renameat.RENAME_EXCHANGE))
}

var _ = (NodeSymlinker)((*LoopbackNode)(nil))

func (n *LoopbackNode) Symlink(ctx context.Context, target, name string, out *fuse.EntryOut) (*Inode, syscall.Errno) {
Expand Down
4 changes: 0 additions & 4 deletions fs/loopback_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@ func (n *LoopbackNode) Listxattr(ctx context.Context, dest []byte) (uint32, sysc
return 0, syscall.ENOSYS
}

func (n *LoopbackNode) renameExchange(name string, newparent InodeEmbedder, newName string) syscall.Errno {
return syscall.ENOSYS
}

func (f *loopbackFile) Allocate(ctx context.Context, off uint64, sz uint64, mode uint32) syscall.Errno {
// TODO: Handle `mode` parameter.

Expand Down
36 changes: 0 additions & 36 deletions fs/loopback_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ package fs

import (
"context"
"path/filepath"
"syscall"

"golang.org/x/sys/unix"
Expand Down Expand Up @@ -43,41 +42,6 @@ func (n *LoopbackNode) Listxattr(ctx context.Context, dest []byte) (uint32, sysc
return uint32(sz), ToErrno(err)
}

func (n *LoopbackNode) renameExchange(name string, newparent InodeEmbedder, newName string) syscall.Errno {
fd1, err := syscall.Open(n.path(), syscall.O_DIRECTORY, 0)
if err != nil {
return ToErrno(err)
}
defer syscall.Close(fd1)
p2 := filepath.Join(n.RootData.Path, newparent.EmbeddedInode().Path(nil))
fd2, err := syscall.Open(p2, syscall.O_DIRECTORY, 0)
defer syscall.Close(fd2)
if err != nil {
return ToErrno(err)
}

var st syscall.Stat_t
if err := syscall.Fstat(fd1, &st); err != nil {
return ToErrno(err)
}

// Double check that nodes didn't change from under us.
inode := &n.Inode
if inode.Root() != inode && inode.StableAttr().Ino != n.RootData.idFromStat(&st).Ino {
return syscall.EBUSY
}
if err := syscall.Fstat(fd2, &st); err != nil {
return ToErrno(err)
}

newinode := newparent.EmbeddedInode()
if newinode.Root() != newinode && newinode.StableAttr().Ino != n.RootData.idFromStat(&st).Ino {
return syscall.EBUSY
}

return ToErrno(unix.Renameat2(fd1, name, fd2, newName, unix.RENAME_EXCHANGE))
}

var _ = (NodeCopyFileRanger)((*LoopbackNode)(nil))

func (n *LoopbackNode) CopyFileRange(ctx context.Context, fhIn FileHandle,
Expand Down
19 changes: 19 additions & 0 deletions fuse/opcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,19 @@ func doInit(server *Server, req *request) {
server.setSplice()
}

renameSwap := input.Flags & uint32(CAP_RENAME_SWAP)
if renameSwap != 0 {
server.canRenameSwap = true

// closed-source macfuse 4.x has broken compatibility with osxfuse 3.x:
// it passes an additional 64-bit field (flags) as in RenameIn, not Rename1In
// macfuse doesn't want change the behaviour back which is motivated by
// not breaking compatibility the second time, look here for details:
// https://github.com/osxfuse/osxfuse/issues/839
// https://github.com/macfuse/library/blob/eee4f806272fcfba3c8ee662647068f8e3abab72/lib/fuse_lowlevel.c#L1299-L1305
getHandler(_OP_RENAME).InputSize = unsafe.Sizeof(RenameIn{})
}

// maxPages is the maximum request size we want the kernel to use, in units of
// memory pages (usually 4kiB). Linux v4.19 and older ignore this and always use
// 128kiB.
Expand Down Expand Up @@ -443,6 +456,12 @@ func doSymlink(server *Server, req *request) {
}

func doRename(server *Server, req *request) {
// see for details:
// https://github.com/macfuse/library/blob/eee4f806272fcfba3c8ee662647068f8e3abab72/lib/fuse_lowlevel.c#L1299-L1305
if server.canRenameSwap {
doRename2(server, req)
return
}
in1 := (*Rename1In)(req.inData)
in := RenameIn{
InHeader: in1.InHeader,
Expand Down
4 changes: 2 additions & 2 deletions fuse/request_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ const outputHeaderSize = 200

const (
_FUSE_KERNEL_VERSION = 7
_MINIMUM_MINOR_VERSION = 12
_OUR_MINOR_VERSION = 12
_MINIMUM_MINOR_VERSION = 19
_OUR_MINOR_VERSION = 19
)
7 changes: 4 additions & 3 deletions fuse/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ type Server struct {
retrieveNext uint64
retrieveTab map[uint64]*retrieveCacheRequest // notifyUnique -> retrieve request

singleReader bool
canSplice bool
loops sync.WaitGroup
singleReader bool
canSplice bool
canRenameSwap bool
loops sync.WaitGroup

ready chan error

Expand Down
3 changes: 3 additions & 0 deletions fuse/types_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ const (
CAP_SETXATTR_EXT = (1 << 29)
CAP_INIT_EXT = (1 << 30)
CAP_INIT_RESERVED = (1 << 31)

// CAP_RENAME_SWAP is not supported on Linux.
CAP_RENAME_SWAP = 0x0
)

type Attr struct {
Expand Down
8 changes: 8 additions & 0 deletions internal/renameat/renameat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package renameat

// Renameat is a wrapper around renameat syscall.
// On Linux, it is a wrapper around renameat2(2).
// On Darwin, it is a wrapper around renameatx_np(2).
func Renameat(olddirfd int, oldpath string, newdirfd int, newpath string, flags uint) (err error) {
return renameat(olddirfd, oldpath, newdirfd, newpath, flags)
}
38 changes: 38 additions & 0 deletions internal/renameat/renameat_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package renameat

import (
"syscall"
"unsafe"
)

const (
SYS_RENAMEATX_NP = 488
RENAME_SWAP = 0x2
RENAME_EXCHANGE = RENAME_SWAP
)

func renameat(olddirfd int, oldpath string, newdirfd int, newpath string, flags uint) error {
oldpathCString, err := syscall.BytePtrFromString(oldpath)
if err != nil {
return err
}
newpathCString, err := syscall.BytePtrFromString(newpath)
if err != nil {
return err
}

_, _, errno := syscall.Syscall6(
SYS_RENAMEATX_NP,
uintptr(olddirfd),
uintptr(unsafe.Pointer(oldpathCString)),
uintptr(newdirfd),
uintptr(unsafe.Pointer(newpathCString)),
uintptr(flags),
0,
)

if errno != 0 {
return errno
}
return nil
}
11 changes: 11 additions & 0 deletions internal/renameat/renameat_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package renameat

import "golang.org/x/sys/unix"

const (
RENAME_EXCHANGE = unix.RENAME_EXCHANGE
)

func renameat(olddirfd int, oldpath string, newdirfd int, newpath string, flags uint) (err error) {
return unix.Renameat2(olddirfd, oldpath, newdirfd, newpath, flags)
}

0 comments on commit abe7ee0

Please sign in to comment.