Commit 35021463 authored by Dmitriy Smotrov's avatar Dmitriy Smotrov Committed by Han-Wen Nienhuys

fs: support renameExchange in loopback for darwin

Tested on MacOS 14.2 (macfuse 4.5.0) and Ubuntu 20.04.2 (Linux 5.15.0)

Change-Id: I3f4f92c2d2aa3f4fc3696b661fa3cb0e3b43282b
parent c1a4d6c4
...@@ -11,6 +11,7 @@ import ( ...@@ -11,6 +11,7 @@ import (
"syscall" "syscall"
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/internal/renameat"
) )
// LoopbackRoot holds the parameters for creating a new loopback // LoopbackRoot holds the parameters for creating a new loopback
...@@ -218,6 +219,41 @@ func (n *LoopbackNode) Create(ctx context.Context, name string, flags uint32, mo ...@@ -218,6 +219,41 @@ func (n *LoopbackNode) Create(ctx context.Context, name string, flags uint32, mo
return ch, lf, 0, 0 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)) var _ = (NodeSymlinker)((*LoopbackNode)(nil))
func (n *LoopbackNode) Symlink(ctx context.Context, target, name string, out *fuse.EntryOut) (*Inode, syscall.Errno) { func (n *LoopbackNode) Symlink(ctx context.Context, target, name string, out *fuse.EntryOut) (*Inode, syscall.Errno) {
......
...@@ -41,10 +41,6 @@ func (n *LoopbackNode) Listxattr(ctx context.Context, dest []byte) (uint32, sysc ...@@ -41,10 +41,6 @@ func (n *LoopbackNode) Listxattr(ctx context.Context, dest []byte) (uint32, sysc
return 0, syscall.ENOSYS 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 { func (f *loopbackFile) Allocate(ctx context.Context, off uint64, sz uint64, mode uint32) syscall.Errno {
// TODO: Handle `mode` parameter. // TODO: Handle `mode` parameter.
......
...@@ -9,7 +9,6 @@ package fs ...@@ -9,7 +9,6 @@ package fs
import ( import (
"context" "context"
"path/filepath"
"syscall" "syscall"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
...@@ -43,41 +42,6 @@ func (n *LoopbackNode) Listxattr(ctx context.Context, dest []byte) (uint32, sysc ...@@ -43,41 +42,6 @@ func (n *LoopbackNode) Listxattr(ctx context.Context, dest []byte) (uint32, sysc
return uint32(sz), ToErrno(err) 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)) var _ = (NodeCopyFileRanger)((*LoopbackNode)(nil))
func (n *LoopbackNode) CopyFileRange(ctx context.Context, fhIn FileHandle, func (n *LoopbackNode) CopyFileRange(ctx context.Context, fhIn FileHandle,
......
...@@ -8,7 +8,6 @@ import ( ...@@ -8,7 +8,6 @@ import (
"bytes" "bytes"
"io/ioutil" "io/ioutil"
"os" "os"
"reflect"
"sync" "sync"
"syscall" "syscall"
"testing" "testing"
...@@ -16,82 +15,9 @@ import ( ...@@ -16,82 +15,9 @@ import (
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/internal/testutil" "github.com/hanwen/go-fuse/v2/internal/testutil"
"github.com/kylelemons/godebug/pretty"
"golang.org/x/sys/unix" "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) { func TestRenameNoOverwrite(t *testing.T) {
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true}) tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
......
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)
}
}
...@@ -102,7 +102,7 @@ func doInit(server *Server, req *request) { ...@@ -102,7 +102,7 @@ func doInit(server *Server, req *request) {
server.reqMu.Lock() server.reqMu.Lock()
server.kernelSettings = *input server.kernelSettings = *input
server.kernelSettings.Flags = input.Flags & (CAP_ASYNC_READ | CAP_BIG_WRITES | CAP_FILE_OPS | 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 { if server.opts.EnableLocks {
server.kernelSettings.Flags |= CAP_FLOCK_LOCKS | CAP_POSIX_LOCKS server.kernelSettings.Flags |= CAP_FLOCK_LOCKS | CAP_POSIX_LOCKS
...@@ -443,6 +443,10 @@ func doSymlink(server *Server, req *request) { ...@@ -443,6 +443,10 @@ func doSymlink(server *Server, req *request) {
} }
func doRename(server *Server, req *request) { func doRename(server *Server, req *request) {
if server.kernelSettings.supportsRenameSwap() {
doRename2(server, req)
return
}
in1 := (*Rename1In)(req.inData) in1 := (*Rename1In)(req.inData)
in := RenameIn{ in := RenameIn{
InHeader: in1.InHeader, InHeader: in1.InHeader,
......
...@@ -172,7 +172,7 @@ func (r *request) parseHeader() Status { ...@@ -172,7 +172,7 @@ func (r *request) parseHeader() Status {
return OK return OK
} }
func (r *request) parse() { func (r *request) parse(kernelSettings InitIn) {
r.arg = r.inputBuf[:] r.arg = r.inputBuf[:]
r.handler = getHandler(r.inHeader.Opcode) r.handler = getHandler(r.inHeader.Opcode)
if r.handler == nil { if r.handler == nil {
...@@ -182,6 +182,9 @@ func (r *request) parse() { ...@@ -182,6 +182,9 @@ func (r *request) parse() {
} }
inSz := int(r.handler.InputSize) 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) { if r.inHeader.Opcode == _OP_INIT && inSz > len(r.arg) {
// Minor version 36 extended the size of InitIn struct // Minor version 36 extended the size of InitIn struct
inSz = len(r.arg) inSz = len(r.arg)
......
...@@ -9,5 +9,5 @@ const outputHeaderSize = 200 ...@@ -9,5 +9,5 @@ const outputHeaderSize = 200
const ( const (
_FUSE_KERNEL_VERSION = 7 _FUSE_KERNEL_VERSION = 7
_MINIMUM_MINOR_VERSION = 12 _MINIMUM_MINOR_VERSION = 12
_OUR_MINOR_VERSION = 12 _OUR_MINOR_VERSION = 19
) )
...@@ -508,7 +508,7 @@ func (ms *Server) handleRequest(req *request) Status { ...@@ -508,7 +508,7 @@ func (ms *Server) handleRequest(req *request) Status {
defer ms.requestProcessingMu.Unlock() defer ms.requestProcessingMu.Unlock()
} }
req.parse() req.parse(ms.kernelSettings)
if req.handler == nil { if req.handler == nil {
req.status = ENOSYS req.status = ENOSYS
} }
...@@ -906,6 +906,12 @@ func (in *InitIn) SupportsNotify(notifyType int) bool { ...@@ -906,6 +906,12 @@ func (in *InitIn) SupportsNotify(notifyType int) bool {
return false 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 // WaitMount waits for the first request to be served. Use this to
// avoid racing between accessing the (empty or not yet mounted) // avoid racing between accessing the (empty or not yet mounted)
// mountpoint, and the OS trying to setup the user-space mount. // mountpoint, and the OS trying to setup the user-space mount.
......
...@@ -28,6 +28,9 @@ const ( ...@@ -28,6 +28,9 @@ const (
CAP_SETXATTR_EXT = (1 << 29) CAP_SETXATTR_EXT = (1 << 29)
CAP_INIT_EXT = (1 << 30) CAP_INIT_EXT = (1 << 30)
CAP_INIT_RESERVED = (1 << 31) CAP_INIT_RESERVED = (1 << 31)
// CAP_RENAME_SWAP only exists on OSX.
CAP_RENAME_SWAP = 0x0
) )
type Attr struct { type Attr struct {
......
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)
}
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
}
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)
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment