diff --git a/connection.go b/connection.go index 0f8a13f..d28bf1a 100644 --- a/connection.go +++ b/connection.go @@ -434,6 +434,10 @@ func (c *Connection) shouldLogError( return false } + case *fuseops.GetXattrOp: + if err == syscall.ENODATA || err == syscall.ERANGE { + return false + } case *unknownOp: // Don't bother the user with methods we intentionally don't support. if err == syscall.ENOSYS { @@ -489,7 +493,7 @@ func (c *Connection) Reply(ctx context.Context, opErr error) { if !noResponse { err := c.writeMessage(outMsg.Bytes()) if err != nil && c.errorLogger != nil { - c.errorLogger.Printf("writeMessage: %v", err) + c.errorLogger.Printf("writeMessage: %v %v", err, outMsg.Bytes()) } } } diff --git a/conversions.go b/conversions.go index b30a643..6b0f16e 100644 --- a/conversions.go +++ b/conversions.go @@ -420,6 +420,107 @@ func convertInMessage( Flags: fusekernel.InitFlags(in.Flags), } + case fusekernel.OpRemovexattr: + buf := inMsg.ConsumeBytes(inMsg.Len()) + n := len(buf) + if n == 0 || buf[n-1] != '\x00' { + err = errors.New("Corrupt OpRemovexattr") + return + } + + o = &fuseops.RemoveXattrOp{ + Inode: fuseops.InodeID(inMsg.Header().Nodeid), + Name: string(buf[:n-1]), + } + + case fusekernel.OpGetxattr: + type input fusekernel.GetxattrIn + in := (*input)(inMsg.Consume(unsafe.Sizeof(input{}))) + if in == nil { + err = errors.New("Corrupt OpGetxattr") + return + } + + name := inMsg.ConsumeBytes(inMsg.Len()) + i := bytes.IndexByte(name, '\x00') + if i < 0 { + err = errors.New("Corrupt OpGetxattr") + return + } + name = name[:i] + + to := &fuseops.GetXattrOp{ + Inode: fuseops.InodeID(inMsg.Header().Nodeid), + Name: string(name), + } + o = to + + readSize := int(in.Size) + p := outMsg.GrowNoZero(readSize) + if p == nil { + err = fmt.Errorf("Can't grow for %d-byte read", readSize) + return + } + + sh := (*reflect.SliceHeader)(unsafe.Pointer(&to.Dst)) + sh.Data = uintptr(p) + sh.Len = readSize + sh.Cap = readSize + + case fusekernel.OpListxattr: + type input fusekernel.ListxattrIn + in := (*input)(inMsg.Consume(unsafe.Sizeof(input{}))) + if in == nil { + err = errors.New("Corrupt OpListxattr") + return + } + + to := &fuseops.ListXattrOp{ + Inode: fuseops.InodeID(inMsg.Header().Nodeid), + } + o = to + + readSize := int(in.Size) + if readSize != 0 { + p := outMsg.GrowNoZero(readSize) + if p == nil { + err = fmt.Errorf("Can't grow for %d-byte read", readSize) + return + } + sh := (*reflect.SliceHeader)(unsafe.Pointer(&to.Dst)) + sh.Data = uintptr(p) + sh.Len = readSize + sh.Cap = readSize + } + case fusekernel.OpSetxattr: + type input fusekernel.SetxattrIn + in := (*input)(inMsg.Consume(unsafe.Sizeof(input{}))) + if in == nil { + err = errors.New("Corrupt OpSetxattr") + return + } + + payload := inMsg.ConsumeBytes(inMsg.Len()) + // payload should be "name\x00value" + if len(payload) < 3 { + err = errors.New("Corrupt OpSetxattr") + return + } + i := bytes.IndexByte(payload, '\x00') + if i < 0 { + err = errors.New("Corrupt OpSetxattr") + return + } + + name, value := payload[:i], payload[i+1:len(payload)] + + o = &fuseops.SetXattrOp{ + Inode: fuseops.InodeID(inMsg.Header().Nodeid), + Name: string(name), + Value: value, + Flags: in.Flags, + } + default: o = &unknownOp{ OpCode: inMsg.Header().Opcode, @@ -459,17 +560,32 @@ func (c *Connection) kernelResponse( // If the user returned the error, fill in the error field of the outgoing // message header. if opErr != nil { - m.OutHeader().Error = -int32(syscall.EIO) - if errno, ok := opErr.(syscall.Errno); ok { - m.OutHeader().Error = -int32(errno) + handled := false + + if opErr == syscall.ERANGE { + switch o := op.(type) { + case *fuseops.GetXattrOp: + writeXattrSize(m, uint32(o.BytesRead)) + handled = true + case *fuseops.ListXattrOp: + writeXattrSize(m, uint32(o.BytesRead)) + handled = true + } } - // Special case: for some types, convertInMessage grew the message in order - // to obtain a destination buffer. Make sure that we shrink back to just - // the header, because on OS X the kernel otherwise returns EINVAL when we - // attempt to write an error response with a length that extends beyond the - // header. - m.ShrinkTo(buffer.OutMessageHeaderSize) + if !handled { + m.OutHeader().Error = -int32(syscall.EIO) + if errno, ok := opErr.(syscall.Errno); ok { + m.OutHeader().Error = -int32(errno) + } + + // Special case: for some types, convertInMessage grew the message in order + // to obtain a destination buffer. Make sure that we shrink back to just + // the header, because on OS X the kernel otherwise returns EINVAL when we + // attempt to write an error response with a length that extends beyond the + // header. + m.ShrinkTo(buffer.OutMessageHeaderSize) + } } // Otherwise, fill in the rest of the response. @@ -623,6 +739,29 @@ func (c *Connection) kernelResponseForOp( out.St.Bsize = o.IoSize out.St.Frsize = o.BlockSize + case *fuseops.RemoveXattrOp: + // Empty response + + case *fuseops.GetXattrOp: + // convertInMessage already set up the destination buffer to be at the end + // of the out message. We need only shrink to the right size based on how + // much the user read. + if o.BytesRead == 0 { + writeXattrSize(m, uint32(o.BytesRead)) + } else { + m.ShrinkTo(buffer.OutMessageHeaderSize + o.BytesRead) + } + + case *fuseops.ListXattrOp: + if o.BytesRead == 0 { + writeXattrSize(m, uint32(o.BytesRead)) + } else { + m.ShrinkTo(buffer.OutMessageHeaderSize + o.BytesRead) + } + + case *fuseops.SetXattrOp: + // Empty response + case *initOp: out := (*fusekernel.InitOut)(m.Grow(int(unsafe.Sizeof(fusekernel.InitOut{})))) @@ -744,3 +883,8 @@ func convertFileMode(unixMode uint32) os.FileMode { } return mode } + +func writeXattrSize(m *buffer.OutMessage, size uint32) { + out := (*fusekernel.GetxattrOut)(m.Grow(int(unsafe.Sizeof(fusekernel.GetxattrOut{})))) + out.Size = size +} diff --git a/debug.go b/debug.go index 1cf95bd..d6f9fdc 100644 --- a/debug.go +++ b/debug.go @@ -89,6 +89,15 @@ func describeRequest(op interface{}) (s string) { addComponent("handle %d", typed.Handle) addComponent("offset %d", typed.Offset) addComponent("%d bytes", len(typed.Data)) + + case *fuseops.RemoveXattrOp: + addComponent("name %s", typed.Name) + + case *fuseops.GetXattrOp: + addComponent("name %s", typed.Name) + + case *fuseops.SetXattrOp: + addComponent("name %s", typed.Name) } // Use just the name if there is no extra info. diff --git a/errors.go b/errors.go index 455c79b..dcf5eef 100644 --- a/errors.go +++ b/errors.go @@ -22,6 +22,7 @@ const ( EEXIST = syscall.EEXIST EINVAL = syscall.EINVAL EIO = syscall.EIO + ENOATTR = syscall.ENODATA ENOENT = syscall.ENOENT ENOSYS = syscall.ENOSYS ENOTDIR = syscall.ENOTDIR diff --git a/fuseops/ops.go b/fuseops/ops.go index f8fa739..a939799 100644 --- a/fuseops/ops.go +++ b/fuseops/ops.go @@ -767,3 +767,81 @@ type ReadSymlinkOp struct { // Set by the file system: the target of the symlink. Target string } + +//////////////////////////////////////////////////////////////////////// +// eXtended attributes +//////////////////////////////////////////////////////////////////////// + +// Remove an extended attribute. +// +// This is sent in response to removexattr(2). Return ENOATTR if the +// extended attribute does not exist. +type RemoveXattrOp struct { + // The inode that we are removing an extended attribute from. + Inode InodeID + + // The name of the extended attribute. + Name string +} + +// Get an extended attribute. +// +// This is sent in response to getxattr(2). Return ENOATTR if the +// extended attribute does not exist. +type GetXattrOp struct { + // The inode whose extended attribute we are reading. + Inode InodeID + + // The name of the extended attribute. + Name string + + // The destination buffer. If the size is too small for the + // value, the ERANGE error should be sent. + Dst []byte + + // Set by the file system: the number of bytes read into Dst, or + // the number of bytes that would have been read into Dst if Dst was + // big enough (return ERANGE in this case). + BytesRead int +} + +// List all the extended attributes for a file. +// +// This is sent in response to listxattr(2). +type ListXattrOp struct { + // The inode whose extended attributes we are listing. + Inode InodeID + + // The destination buffer. If the size is too small for the + // value, the ERANGE error should be sent. + // + // The output data should consist of a sequence of NUL-terminated strings, + // one for each xattr. + Dst []byte + + // Set by the file system: the number of bytes read into Dst, or + // the number of bytes that would have been read into Dst if Dst was + // big enough (return ERANGE in this case). + BytesRead int +} + +// Set an extended attribute. +// +// This is sent in response to setxattr(2). Return ENOSPC if there is +// insufficient space remaining to store the extended attribute. +type SetXattrOp struct { + // The inode whose extended attribute we are setting. + Inode InodeID + + // The name of the extended attribute + Name string + + // The value to for the extened attribute. + Value []byte + + // If Flags is 0x1, and the attribute exists already, EEXIST should be returned. + // If Flags is 0x2, and the attribute does not exist, ENOATTR should be returned. + // If Flags is 0x0, the extended attribute will be created if need be, or will + // simply replace the value if the attribute exists. + Flags uint32 +} diff --git a/fuseutil/file_system.go b/fuseutil/file_system.go index 2921c1d..9cc0fa8 100644 --- a/fuseutil/file_system.go +++ b/fuseutil/file_system.go @@ -57,6 +57,10 @@ type FileSystem interface { FlushFile(context.Context, *fuseops.FlushFileOp) error ReleaseFileHandle(context.Context, *fuseops.ReleaseFileHandleOp) error ReadSymlink(context.Context, *fuseops.ReadSymlinkOp) error + RemoveXattr(context.Context, *fuseops.RemoveXattrOp) error + GetXattr(context.Context, *fuseops.GetXattrOp) error + ListXattr(context.Context, *fuseops.ListXattrOp) error + SetXattr(context.Context, *fuseops.SetXattrOp) error // Regard all inodes (including the root inode) as having their lookup counts // decremented to zero, and clean up any resources associated with the file @@ -186,6 +190,18 @@ func (s *fileSystemServer) handleOp( case *fuseops.ReadSymlinkOp: err = s.fs.ReadSymlink(ctx, typed) + + case *fuseops.RemoveXattrOp: + err = s.fs.RemoveXattr(ctx, typed) + + case *fuseops.GetXattrOp: + err = s.fs.GetXattr(ctx, typed) + + case *fuseops.ListXattrOp: + err = s.fs.ListXattr(ctx, typed) + + case *fuseops.SetXattrOp: + err = s.fs.SetXattr(ctx, typed) } c.Reply(ctx, err) diff --git a/fuseutil/not_implemented_file_system.go b/fuseutil/not_implemented_file_system.go index fc082d2..b21e1e3 100644 --- a/fuseutil/not_implemented_file_system.go +++ b/fuseutil/not_implemented_file_system.go @@ -183,5 +183,33 @@ func (fs *NotImplementedFileSystem) ReadSymlink( return } +func (fs *NotImplementedFileSystem) RemoveXattr( + ctx context.Context, + op *fuseops.RemoveXattrOp) (err error) { + err = fuse.ENOSYS + return +} + +func (fs *NotImplementedFileSystem) GetXattr( + ctx context.Context, + op *fuseops.GetXattrOp) (err error) { + err = fuse.ENOSYS + return +} + +func (fs *NotImplementedFileSystem) ListXattr( + ctx context.Context, + op *fuseops.ListXattrOp) (err error) { + err = fuse.ENOSYS + return +} + +func (fs *NotImplementedFileSystem) SetXattr( + ctx context.Context, + op *fuseops.SetXattrOp) (err error) { + err = fuse.ENOSYS + return +} + func (fs *NotImplementedFileSystem) Destroy() { } diff --git a/internal/fusekernel/fuse_kernel.go b/internal/fusekernel/fuse_kernel.go index 778f5fe..ef543cb 100644 --- a/internal/fusekernel/fuse_kernel.go +++ b/internal/fusekernel/fuse_kernel.go @@ -660,6 +660,11 @@ type GetxattrOut struct { Padding uint32 } +type ListxattrIn struct { + Size uint32 + Padding uint32 +} + type LkIn struct { Fh uint64 Owner uint64 diff --git a/samples/memfs/inode.go b/samples/memfs/inode.go index b436a1a..b0471cc 100644 --- a/samples/memfs/inode.go +++ b/samples/memfs/inode.go @@ -61,6 +61,9 @@ type inode struct { // // INVARIANT: If !isSymlink(), len(target) == 0 target string + + // extended attributes and values + xattrs map[string][]byte } //////////////////////////////////////////////////////////////////////// @@ -78,7 +81,8 @@ func newInode( // Create the object. in = &inode{ - attrs: attrs, + attrs: attrs, + xattrs: make(map[string][]byte), } return diff --git a/samples/memfs/memfs.go b/samples/memfs/memfs.go index 9963952..bf7c570 100644 --- a/samples/memfs/memfs.go +++ b/samples/memfs/memfs.go @@ -18,6 +18,7 @@ import ( "fmt" "io" "os" + "syscall" "time" "golang.org/x/net/context" @@ -628,3 +629,88 @@ func (fs *memFS) ReadSymlink( return } + +func (fs *memFS) GetXattr(ctx context.Context, + op *fuseops.GetXattrOp) (err error) { + fs.mu.Lock() + defer fs.mu.Unlock() + + inode := fs.getInodeOrDie(op.Inode) + if value, ok := inode.xattrs[op.Name]; ok { + op.BytesRead = len(value) + if len(op.Dst) >= len(value) { + copy(op.Dst, value) + } else { + err = syscall.ERANGE + } + } else { + err = fuse.ENOATTR + } + + return +} + +func (fs *memFS) ListXattr(ctx context.Context, + op *fuseops.ListXattrOp) (err error) { + fs.mu.Lock() + defer fs.mu.Unlock() + + inode := fs.getInodeOrDie(op.Inode) + + dst := op.Dst[:] + for key := range inode.xattrs { + keyLen := len(key) + 1 + + if err == nil && len(dst) >= keyLen { + copy(dst, key) + dst = dst[keyLen:] + } else { + err = syscall.ERANGE + } + op.BytesRead += keyLen + } + + return +} + +func (fs *memFS) RemoveXattr(ctx context.Context, + op *fuseops.RemoveXattrOp) (err error) { + fs.mu.Lock() + defer fs.mu.Unlock() + inode := fs.getInodeOrDie(op.Inode) + + if _, ok := inode.xattrs[op.Name]; ok { + delete(inode.xattrs, op.Name) + } else { + err = fuse.ENOATTR + } + return +} + +func (fs *memFS) SetXattr(ctx context.Context, + op *fuseops.SetXattrOp) (err error) { + fs.mu.Lock() + defer fs.mu.Unlock() + inode := fs.getInodeOrDie(op.Inode) + + _, ok := inode.xattrs[op.Name] + + switch op.Flags { + case 0x1: + if ok { + err = fuse.EEXIST + } + case 0x2: + if !ok { + err = fuse.ENOATTR + } + } + + if err == nil { + value := make([]byte, len(op.Value)) + copy(value, op.Value) + inode.xattrs[op.Name] = value + } + + return +} diff --git a/samples/memfs/memfs_test.go b/samples/memfs/memfs_test.go index 977dae3..dcdad01 100644 --- a/samples/memfs/memfs_test.go +++ b/samples/memfs/memfs_test.go @@ -27,11 +27,13 @@ import ( "testing" "time" + "github.com/jacobsa/fuse" "github.com/jacobsa/fuse/fusetesting" "github.com/jacobsa/fuse/samples" "github.com/jacobsa/fuse/samples/memfs" . "github.com/jacobsa/oglematchers" . "github.com/jacobsa/ogletest" + "github.com/kahing/go-xattr" ) func TestMemFS(t *testing.T) { RunTests(t) } @@ -1611,6 +1613,85 @@ func (t *MemFSTest) RenameNonExistentFile() { ExpectThat(err, Error(HasSubstr("no such file"))) } +func (t *MemFSTest) NoXattrs() { + var err error + + // Create a file. + filePath := path.Join(t.Dir, "foo") + err = ioutil.WriteFile(filePath, []byte("taco"), 0400) + AssertEq(nil, err) + + // List xattr names. + names, err := xattr.List(filePath) + AssertEq(nil, err) + ExpectThat(names, ElementsAre()) + + // Attempt to read a non-existent xattr. + _, err = xattr.Getxattr(filePath, "foo", nil) + ExpectEq(fuse.ENOATTR, err) +} + +func (t *MemFSTest) SetXAttr() { + var err error + + // Create a file. + filePath := path.Join(t.Dir, "foo") + err = ioutil.WriteFile(filePath, []byte("taco"), 0600) + AssertEq(nil, err) + + err = xattr.Setxattr(filePath, "foo", []byte("bar"), xattr.REPLACE) + AssertEq(fuse.ENOATTR, err) + + err = xattr.Setxattr(filePath, "foo", []byte("bar"), xattr.CREATE) + AssertEq(nil, err) + + value, err := xattr.Get(filePath, "foo") + AssertEq(nil, err) + AssertEq("bar", string(value)) + + err = xattr.Setxattr(filePath, "foo", []byte("hello world"), xattr.REPLACE) + AssertEq(nil, err) + + value, err = xattr.Get(filePath, "foo") + AssertEq(nil, err) + AssertEq("hello world", string(value)) + + names, err := xattr.List(filePath) + AssertEq(nil, err) + AssertEq(1, len(names)) + AssertEq("foo", names[0]) + + err = xattr.Setxattr(filePath, "bar", []byte("hello world"), 0x0) + AssertEq(nil, err) + + names, err = xattr.List(filePath) + AssertEq(nil, err) + AssertEq(2, len(names)) + ExpectThat(names, Contains("foo")) + ExpectThat(names, Contains("bar")) +} + +func (t *MemFSTest) RemoveXAttr() { + var err error + + // Create a file + filePath := path.Join(t.Dir, "foo") + err = ioutil.WriteFile(filePath, []byte("taco"), 0600) + AssertEq(nil, err) + + err = xattr.Removexattr(filePath, "foo") + AssertEq(fuse.ENOATTR, err) + + err = xattr.Setxattr(filePath, "foo", []byte("bar"), xattr.CREATE) + AssertEq(nil, err) + + err = xattr.Removexattr(filePath, "foo") + AssertEq(nil, err) + + _, err = xattr.Getxattr(filePath, "foo", nil) + AssertEq(fuse.ENOATTR, err) +} + //////////////////////////////////////////////////////////////////////// // Mknod ////////////////////////////////////////////////////////////////////////