diff --git a/conversions.go b/conversions.go index adecf7b..78ded6d 100644 --- a/conversions.go +++ b/conversions.go @@ -134,6 +134,27 @@ func convertInMessage( Mode: convertFileMode(in.Mode) | os.ModeDir, } + case fusekernel.OpMknod: + in := (*fusekernel.MknodIn)(inMsg.Consume(fusekernel.MknodInSize(protocol))) + if in == nil { + err = errors.New("Corrupt OpMknod") + return + } + + name := inMsg.ConsumeBytes(inMsg.Len()) + i := bytes.IndexByte(name, '\x00') + if i < 0 { + err = errors.New("Corrupt OpMknod") + return + } + name = name[:i] + + o = &fuseops.MkNodeOp{ + Parent: fuseops.InodeID(inMsg.Header().Nodeid), + Name: string(name), + Mode: convertFileMode(in.Mode), + } + case fusekernel.OpCreate: in := (*fusekernel.CreateIn)(inMsg.Consume(fusekernel.CreateInSize(protocol))) if in == nil { @@ -491,6 +512,11 @@ func (c *Connection) kernelResponseForOp( out := (*fusekernel.EntryOut)(m.Grow(size)) convertChildInodeEntry(&o.Entry, out) + case *fuseops.MkNodeOp: + size := fusekernel.EntryOutSize(c.protocol) + out := (*fusekernel.EntryOut)(m.Grow(size)) + convertChildInodeEntry(&o.Entry, out) + case *fuseops.CreateFileOp: eSize := fusekernel.EntryOutSize(c.protocol) diff --git a/fuseops/ops.go b/fuseops/ops.go index 040fc87..b26d989 100644 --- a/fuseops/ops.go +++ b/fuseops/ops.go @@ -235,6 +235,33 @@ type MkDirOp struct { Entry ChildInodeEntry } +// Create a file inode as a child of an existing directory inode. The kernel +// sends this in response to a mknod(2) call. It may also send it in special +// cases such as an NFS export (cf. https://goo.gl/HiLfnK). It is more typical +// to see CreateFileOp, which is received for an open(2) that creates a file. +// +// The Linux kernel appears to verify the name doesn't already exist (mknod +// calls sys_mknodat calls user_path_create calls filename_create, which +// verifies: http://goo.gl/FZpLu5). But osxfuse may not guarantee this, as with +// mkdir(2). And if names may be created outside of the kernel's control, it +// doesn't matter what the kernel does anyway. +// +// Therefore the file system should return EEXIST if the name already exists. +type MkNodeOp struct { + // The ID of parent directory inode within which to create the child. + Parent InodeID + + // The name of the child to create, and the mode with which to create it. + Name string + Mode os.FileMode + + // Set by the file system: information about the inode that was created. + // + // The lookup count for the inode is implicitly incremented. See notes on + // ForgetInodeOp for more information. + Entry ChildInodeEntry +} + // Create a file inode and open it. // // The kernel sends this when the user asks to open a file with the O_CREAT diff --git a/fuseutil/file_system.go b/fuseutil/file_system.go index 3f7d2b4..2921c1d 100644 --- a/fuseutil/file_system.go +++ b/fuseutil/file_system.go @@ -41,6 +41,7 @@ type FileSystem interface { SetInodeAttributes(context.Context, *fuseops.SetInodeAttributesOp) error ForgetInode(context.Context, *fuseops.ForgetInodeOp) error MkDir(context.Context, *fuseops.MkDirOp) error + MkNode(context.Context, *fuseops.MkNodeOp) error CreateFile(context.Context, *fuseops.CreateFileOp) error CreateSymlink(context.Context, *fuseops.CreateSymlinkOp) error Rename(context.Context, *fuseops.RenameOp) error @@ -138,6 +139,9 @@ func (s *fileSystemServer) handleOp( case *fuseops.MkDirOp: err = s.fs.MkDir(ctx, typed) + case *fuseops.MkNodeOp: + err = s.fs.MkNode(ctx, typed) + case *fuseops.CreateFileOp: err = s.fs.CreateFile(ctx, typed) diff --git a/fuseutil/not_implemented_file_system.go b/fuseutil/not_implemented_file_system.go index 4bd923c..fc082d2 100644 --- a/fuseutil/not_implemented_file_system.go +++ b/fuseutil/not_implemented_file_system.go @@ -71,6 +71,13 @@ func (fs *NotImplementedFileSystem) MkDir( return } +func (fs *NotImplementedFileSystem) MkNode( + ctx context.Context, + op *fuseops.MkNodeOp) (err error) { + err = fuse.ENOSYS + return +} + func (fs *NotImplementedFileSystem) CreateFile( ctx context.Context, op *fuseops.CreateFileOp) (err error) { diff --git a/samples/memfs/memfs.go b/samples/memfs/memfs.go index c9e5dda..9963952 100644 --- a/samples/memfs/memfs.go +++ b/samples/memfs/memfs.go @@ -305,28 +305,37 @@ func (fs *memFS) MkDir( return } -func (fs *memFS) CreateFile( +func (fs *memFS) MkNode( ctx context.Context, - op *fuseops.CreateFileOp) (err error) { + op *fuseops.MkNodeOp) (err error) { fs.mu.Lock() defer fs.mu.Unlock() + op.Entry, err = fs.createFile(op.Parent, op.Name, op.Mode) + return +} + +// LOCKS_REQUIRED(fs.mu) +func (fs *memFS) createFile( + parentID fuseops.InodeID, + name string, + mode os.FileMode) (entry fuseops.ChildInodeEntry, err error) { // Grab the parent, which we will update shortly. - parent := fs.getInodeOrDie(op.Parent) + parent := fs.getInodeOrDie(parentID) // Ensure that the name doesn't already exist, so we don't wind up with a // duplicate. - _, _, exists := parent.LookUpChild(op.Name) + _, _, exists := parent.LookUpChild(name) if exists { err = fuse.EEXIST return } - // Set up attributes from the child. + // Set up attributes for the child. now := time.Now() childAttrs := fuseops.InodeAttributes{ Nlink: 1, - Mode: op.Mode, + Mode: mode, Atime: now, Mtime: now, Ctime: now, @@ -339,19 +348,27 @@ func (fs *memFS) CreateFile( childID, child := fs.allocateInode(childAttrs) // Add an entry in the parent. - parent.AddChild(childID, op.Name, fuseutil.DT_File) + parent.AddChild(childID, name, fuseutil.DT_File) // Fill in the response entry. - op.Entry.Child = childID - op.Entry.Attributes = child.attrs + entry.Child = childID + entry.Attributes = child.attrs // We don't spontaneously mutate, so the kernel can cache as long as it wants // (since it also handles invalidation). - op.Entry.AttributesExpiration = time.Now().Add(365 * 24 * time.Hour) - op.Entry.EntryExpiration = op.Entry.EntryExpiration + entry.AttributesExpiration = time.Now().Add(365 * 24 * time.Hour) + entry.EntryExpiration = entry.AttributesExpiration - // We have nothing interesting to put in the Handle field. + return +} +func (fs *memFS) CreateFile( + ctx context.Context, + op *fuseops.CreateFileOp) (err error) { + fs.mu.Lock() + defer fs.mu.Unlock() + + op.Entry, err = fs.createFile(op.Parent, op.Name, op.Mode) return } diff --git a/samples/memfs/memfs_test.go b/samples/memfs/memfs_test.go index 7d47c4f..e2a65e4 100644 --- a/samples/memfs/memfs_test.go +++ b/samples/memfs/memfs_test.go @@ -21,6 +21,7 @@ import ( "os" "os/user" "path" + "runtime" "strconv" "syscall" "testing" @@ -87,21 +88,25 @@ func applyUmask(m os.FileMode) os.FileMode { // Boilerplate //////////////////////////////////////////////////////////////////////// -type MemFSTest struct { +type memFSTest struct { samples.SampleTest } -func init() { RegisterTestSuite(&MemFSTest{}) } - -func (t *MemFSTest) SetUp(ti *TestInfo) { +func (t *memFSTest) SetUp(ti *TestInfo) { t.Server = memfs.NewMemFS(currentUid(), currentGid()) t.SampleTest.SetUp(ti) } //////////////////////////////////////////////////////////////////////// -// Test functions +// Basics //////////////////////////////////////////////////////////////////////// +type MemFSTest struct { + memFSTest +} + +func init() { RegisterTestSuite(&MemFSTest{}) } + func (t *MemFSTest) ContentsOfEmptyFileSystem() { entries, err := fusetesting.ReadDirPicky(t.Dir) @@ -1614,3 +1619,91 @@ func (t *MemFSTest) RenameNonExistentFile() { err = os.Rename(path.Join(t.Dir, "foo"), path.Join(t.Dir, "bar")) ExpectThat(err, Error(HasSubstr("no such file"))) } + +//////////////////////////////////////////////////////////////////////// +// Mknod +//////////////////////////////////////////////////////////////////////// + +type MknodTest struct { + memFSTest +} + +func init() { RegisterTestSuite(&MknodTest{}) } + +func (t *MknodTest) File() { + // mknod(2) only works for root on OS X. + if runtime.GOOS == "darwin" { + return + } + + var err error + p := path.Join(t.Dir, "foo") + + // Create + err = syscall.Mknod(p, syscall.S_IFREG|0641, 0) + AssertEq(nil, err) + + // Stat + fi, err := os.Stat(p) + AssertEq(nil, err) + + ExpectEq(path.Base(p), fi.Name()) + ExpectEq(0, fi.Size()) + ExpectEq(os.FileMode(0641), fi.Mode()) + + // Read + contents, err := ioutil.ReadFile(p) + AssertEq(nil, err) + ExpectEq("", string(contents)) +} + +func (t *MknodTest) Directory() { + // mknod(2) only works for root on OS X. + if runtime.GOOS == "darwin" { + return + } + + var err error + p := path.Join(t.Dir, "foo") + + // Quoth `man 2 mknod`: "Under Linux, this call cannot be used to create + // directories." + err = syscall.Mknod(p, syscall.S_IFDIR|0700, 0) + ExpectEq(syscall.EPERM, err) +} + +func (t *MknodTest) AlreadyExists() { + // mknod(2) only works for root on OS X. + if runtime.GOOS == "darwin" { + return + } + + var err error + p := path.Join(t.Dir, "foo") + + // Create (first) + err = ioutil.WriteFile(p, []byte("taco"), 0600) + AssertEq(nil, err) + + // Create (second) + err = syscall.Mknod(p, syscall.S_IFREG|0600, 0) + ExpectEq(syscall.EEXIST, err) + + // Read + contents, err := ioutil.ReadFile(p) + AssertEq(nil, err) + ExpectEq("taco", string(contents)) +} + +func (t *MknodTest) NonExistentParent() { + // mknod(2) only works for root on OS X. + if runtime.GOOS == "darwin" { + return + } + + var err error + p := path.Join(t.Dir, "foo/bar") + + err = syscall.Mknod(p, syscall.S_IFREG|0600, 0) + ExpectEq(syscall.ENOENT, err) +}