Skip to content

Commit

Permalink
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 Ubuntu 20.04.2 (Linux 5.15.0)

Change-Id: I3f4f92c2d2aa3f4fc3696b661fa3cb0e3b43282b
  • Loading branch information
dsxack committed Nov 13, 2023
1 parent 5b76a74 commit d305c21
Show file tree
Hide file tree
Showing 13 changed files with 196 additions and 118 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
74 changes: 0 additions & 74 deletions fs/loopback_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,90 +8,16 @@ import (
"bytes"
"io/ioutil"
"os"
"reflect"
"sync"
"syscall"
"testing"
"time"

"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/internal/testutil"
"github.com/kylelemons/godebug/pretty"
"golang.org/x/sys/unix"
)

func TestRenameExchange(t *testing.T) {
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})

if err := os.Mkdir(tc.origDir+"/dir", 0755); err != nil {
t.Fatalf("Mkdir: %v", err)
}
tc.writeOrig("file", "hello", 0644)
tc.writeOrig("dir/file", "x", 0644)

f1, err := syscall.Open(tc.mntDir+"/", syscall.O_DIRECTORY, 0)
if err != nil {
t.Fatalf("open 1: %v", err)
}
defer syscall.Close(f1)
f2, err := syscall.Open(tc.mntDir+"/dir", syscall.O_DIRECTORY, 0)
if err != nil {
t.Fatalf("open 2: %v", err)
}
defer syscall.Close(f2)

var before1, before2 unix.Stat_t
if err := unix.Fstatat(f1, "file", &before1, 0); err != nil {
t.Fatalf("Fstatat: %v", err)
}
if err := unix.Fstatat(f2, "file", &before2, 0); err != nil {
t.Fatalf("Fstatat: %v", err)
}

if err := unix.Renameat2(f1, "file", f2, "file", unix.RENAME_EXCHANGE); err != nil {
t.Errorf("rename EXCHANGE: %v", err)
}

var after1, after2 unix.Stat_t
if err := unix.Fstatat(f1, "file", &after1, 0); err != nil {
t.Fatalf("Fstatat: %v", err)
}
if err := unix.Fstatat(f2, "file", &after2, 0); err != nil {
t.Fatalf("Fstatat: %v", err)
}
clearCtime := func(s *unix.Stat_t) {
s.Ctim.Sec = 0
s.Ctim.Nsec = 0
}

clearCtime(&after1)
clearCtime(&after2)
clearCtime(&before2)
clearCtime(&before1)
if diff := pretty.Compare(after1, before2); diff != "" {
t.Errorf("after1, before2: %s", diff)
}
if !reflect.DeepEqual(after2, before1) {
t.Errorf("after2, before1: %#v, %#v", after2, before1)
}

root := tc.loopback.EmbeddedInode().Root()
ino1 := root.GetChild("file")
if ino1 == nil {
t.Fatalf("root.GetChild(%q): null inode", "file")
}
ino2 := root.GetChild("dir").GetChild("file")
if ino2 == nil {
t.Fatalf("dir.GetChild(%q): null inode", "file")
}
if ino1.StableAttr().Ino != after1.Ino {
t.Errorf("got inode %d for %q, want %d", ino1.StableAttr().Ino, "file", after1.Ino)
}
if ino2.StableAttr().Ino != after2.Ino {
t.Errorf("got inode %d for %q want %d", ino2.StableAttr().Ino, "dir/file", after2.Ino)
}
}

func TestRenameNoOverwrite(t *testing.T) {
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})

Expand Down
83 changes: 83 additions & 0 deletions fs/loopback_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package fs

import (
"os"
"reflect"
"syscall"
"testing"

"github.com/kylelemons/godebug/pretty"
"golang.org/x/sys/unix"
)

func TestRenameExchange(t *testing.T) {
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})

if err := os.Mkdir(tc.origDir+"/dir", 0755); err != nil {
t.Fatalf("Mkdir: %v", err)
}
tc.writeOrig("file", "hello", 0644)
tc.writeOrig("dir/file", "x", 0644)

f1, err := syscall.Open(tc.mntDir+"/", syscall.O_DIRECTORY, 0)
if err != nil {
t.Fatalf("open 1: %v", err)
}
defer syscall.Close(f1)
f2, err := syscall.Open(tc.mntDir+"/dir", syscall.O_DIRECTORY, 0)
if err != nil {
t.Fatalf("open 2: %v", err)
}
defer syscall.Close(f2)

var before1, before2 unix.Stat_t
if err := unix.Fstatat(f1, "file", &before1, 0); err != nil {
t.Fatalf("Fstatat: %v", err)
}
if err := unix.Fstatat(f2, "file", &before2, 0); err != nil {
t.Fatalf("Fstatat: %v", err)
}

if err := unix.Renameat2(f1, "file", f2, "file", unix.RENAME_EXCHANGE); err != nil {
t.Errorf("rename EXCHANGE: %v", err)
}

var after1, after2 unix.Stat_t
if err := unix.Fstatat(f1, "file", &after1, 0); err != nil {
t.Fatalf("Fstatat: %v", err)
}
if err := unix.Fstatat(f2, "file", &after2, 0); err != nil {
t.Fatalf("Fstatat: %v", err)
}
clearCtime := func(s *unix.Stat_t) {
s.Ctim.Sec = 0
s.Ctim.Nsec = 0
}

clearCtime(&after1)
clearCtime(&after2)
clearCtime(&before2)
clearCtime(&before1)
if diff := pretty.Compare(after1, before2); diff != "" {
t.Errorf("after1, before2: %s", diff)
}
if !reflect.DeepEqual(after2, before1) {
t.Errorf("after2, before1: %#v, %#v", after2, before1)
}

root := tc.loopback.EmbeddedInode().Root()
ino1 := root.GetChild("file")
if ino1 == nil {
t.Fatalf("root.GetChild(%q): null inode", "file")
}
ino2 := root.GetChild("dir").GetChild("file")
if ino2 == nil {
t.Fatalf("dir.GetChild(%q): null inode", "file")
}
if ino1.StableAttr().Ino != after1.Ino {
t.Errorf("got inode %d for %q, want %d", ino1.StableAttr().Ino, "file", after1.Ino)
}
if ino2.StableAttr().Ino != after2.Ino {
t.Errorf("got inode %d for %q want %d", ino2.StableAttr().Ino, "dir/file", after2.Ino)
}
}
6 changes: 5 additions & 1 deletion fuse/opcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func doInit(server *Server, req *request) {
server.reqMu.Lock()
server.kernelSettings = *input
server.kernelSettings.Flags = input.Flags & (CAP_ASYNC_READ | CAP_BIG_WRITES | CAP_FILE_OPS |
CAP_READDIRPLUS | CAP_NO_OPEN_SUPPORT | CAP_PARALLEL_DIROPS | CAP_MAX_PAGES)
CAP_READDIRPLUS | CAP_NO_OPEN_SUPPORT | CAP_PARALLEL_DIROPS | CAP_MAX_PAGES | CAP_RENAME_SWAP)

if server.opts.EnableLocks {
server.kernelSettings.Flags |= CAP_FLOCK_LOCKS | CAP_POSIX_LOCKS
Expand Down Expand Up @@ -443,6 +443,10 @@ func doSymlink(server *Server, req *request) {
}

func doRename(server *Server, req *request) {
if server.kernelSettings.supportsRenameSwap() {
doRename2(server, req)
return
}
in1 := (*Rename1In)(req.inData)
in := RenameIn{
InHeader: in1.InHeader,
Expand Down
5 changes: 4 additions & 1 deletion fuse/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ func (r *request) parseHeader() Status {
return OK
}

func (r *request) parse() {
func (r *request) parse(kernelSettings InitIn) {
r.arg = r.inputBuf[:]
r.handler = getHandler(r.inHeader.Opcode)
if r.handler == nil {
Expand All @@ -182,6 +182,9 @@ func (r *request) parse() {
}

inSz := int(r.handler.InputSize)
if r.inHeader.Opcode == _OP_RENAME && kernelSettings.supportsRenameSwap() {
inSz = int(unsafe.Sizeof(RenameIn{}))
}
if r.inHeader.Opcode == _OP_INIT && inSz > len(r.arg) {
// Minor version 36 extended the size of InitIn struct
inSz = len(r.arg)
Expand Down
2 changes: 1 addition & 1 deletion fuse/request_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ const outputHeaderSize = 200
const (
_FUSE_KERNEL_VERSION = 7
_MINIMUM_MINOR_VERSION = 12
_OUR_MINOR_VERSION = 12
_OUR_MINOR_VERSION = 19
)
8 changes: 7 additions & 1 deletion fuse/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ func (ms *Server) handleRequest(req *request) Status {
defer ms.requestProcessingMu.Unlock()
}

req.parse()
req.parse(ms.kernelSettings)
if req.handler == nil {
req.status = ENOSYS
}
Expand Down Expand Up @@ -906,6 +906,12 @@ func (in *InitIn) SupportsNotify(notifyType int) bool {
return false
}

// supportsRenameSwap returns whether the kernel supports the
// renamex_np(2) syscall. This is only supported on OS X.
func (in *InitIn) supportsRenameSwap() bool {
return in.Flags&CAP_RENAME_SWAP != 0
}

// WaitMount waits for the first request to be served. Use this to
// avoid racing between accessing the (empty or not yet mounted)
// mountpoint, and the OS trying to setup the user-space mount.
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 only exists on OSX.
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)
}
Loading

0 comments on commit d305c21

Please sign in to comment.