From 70bcf58a9bda5f95a3037fb785858f5d7ce3f930 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sun, 3 Dec 2017 17:57:08 +0100 Subject: [PATCH] syscallcompat: convert Getdents to fd input, add emulation Now that we have Fstatat we can use it in Getdents to get rid of the path name. Also, add an emulated version of getdents for MacOS. This allows to drop the !HaveGetdents special cases from fusefrontend. Modify the getdents test to test both native getdents and the emulated version. --- internal/fusefrontend/fs_dir.go | 22 ++++------- internal/syscallcompat/getdents_linux.go | 29 +++++--------- internal/syscallcompat/getdents_other.go | 50 +++++++++++++++++++----- internal/syscallcompat/getdents_test.go | 22 +++++++++-- internal/syscallcompat/sys_linux.go | 7 ++++ 5 files changed, 84 insertions(+), 46 deletions(-) diff --git a/internal/fusefrontend/fs_dir.go b/internal/fusefrontend/fs_dir.go index ae52412..2102008 100644 --- a/internal/fusefrontend/fs_dir.go +++ b/internal/fusefrontend/fs_dir.go @@ -271,20 +271,14 @@ func (fs *FS) OpenDir(dirName string, context *fuse.Context) ([]fuse.DirEntry, f cDirAbsPath := filepath.Join(fs.args.Cipherdir, cDirName) var cipherEntries []fuse.DirEntry var status fuse.Status - if syscallcompat.HaveGetdents { - // Getdents avoids calling Lstat on each file. - cipherEntries, err = syscallcompat.Getdents(cDirAbsPath) - if err != nil { - return nil, fuse.ToStatus(err) - } - } else { - haveGetdentsWarnOnce.Do(func() { - tlog.Warn.Printf("OpenDir: Getdents not available, falling back to OpenDir") - }) - cipherEntries, status = fs.FileSystem.OpenDir(cDirName, context) - if !status.Ok() { - return nil, status - } + fd, err := syscall.Open(cDirAbsPath, syscall.O_RDONLY|syscall.O_NOFOLLOW, 0) + if err != nil { + return nil, fuse.ToStatus(err) + } + defer syscall.Close(fd) + cipherEntries, err = syscallcompat.Getdents(fd) + if err != nil { + return nil, fuse.ToStatus(err) } // Get DirIV (stays nil if PlaintextNames is used) var cachedIV []byte diff --git a/internal/syscallcompat/getdents_linux.go b/internal/syscallcompat/getdents_linux.go index 55bffff..49f9233 100644 --- a/internal/syscallcompat/getdents_linux.go +++ b/internal/syscallcompat/getdents_linux.go @@ -12,26 +12,17 @@ import ( "syscall" "unsafe" + "golang.org/x/sys/unix" + "github.com/hanwen/go-fuse/fuse" "github.com/rfjakob/gocryptfs/internal/tlog" ) -// HaveGetdents is true if we have a working implementation of Getdents -const HaveGetdents = true - const sizeofDirent = int(unsafe.Sizeof(syscall.Dirent{})) -// Getdents wraps syscall.Getdents and converts the result to []fuse.DirEntry. -// The function takes a path instead of an fd because we need to be able to -// call Lstat on files. Fstatat is not yet available in Go as of v1.9: -// https://github.com/golang/go/issues/14216 -func Getdents(dir string) ([]fuse.DirEntry, error) { - fd, err := syscall.Open(dir, syscall.O_RDONLY, 0) - if err != nil { - return nil, err - } - defer syscall.Close(fd) +// getdents wraps syscall.Getdents and converts the result to []fuse.DirEntry. +func getdents(fd int) ([]fuse.DirEntry, error) { // Collect syscall result in smartBuf. // "bytes.Buffer" is smart about expanding the capacity and avoids the // exponential runtime of simple append(). @@ -84,7 +75,7 @@ func Getdents(dir string) ([]fuse.DirEntry, error) { // os.File.Readdir() drops "." and "..". Let's be compatible. continue } - mode, err := convertDType(s.Type, dir+"/"+name) + mode, err := convertDType(fd, name, s.Type) if err != nil { // The file may have been deleted in the meantime. Just skip it // and go on. @@ -125,17 +116,17 @@ func getdentsName(s syscall.Dirent) (string, error) { var dtUnknownWarnOnce sync.Once // convertDType converts a Dirent.Type to at Stat_t.Mode value. -func convertDType(dtype uint8, file string) (uint32, error) { +func convertDType(dirfd int, name string, dtype uint8) (uint32, error) { if dtype != syscall.DT_UNKNOWN { // Shift up by four octal digits = 12 bits return uint32(dtype) << 12, nil } - // DT_UNKNOWN: we have to call Lstat() + // DT_UNKNOWN: we have to call stat() dtUnknownWarnOnce.Do(func() { - tlog.Warn.Printf("Getdents: convertDType: received DT_UNKNOWN, falling back to Lstat") + tlog.Warn.Printf("Getdents: convertDType: received DT_UNKNOWN, falling back to stat") }) - var st syscall.Stat_t - err := syscall.Lstat(file, &st) + var st unix.Stat_t + err := Fstatat(dirfd, name, &st, unix.AT_SYMLINK_NOFOLLOW) if err != nil { return 0, err } diff --git a/internal/syscallcompat/getdents_other.go b/internal/syscallcompat/getdents_other.go index 4ef5b8f..6d08a9b 100644 --- a/internal/syscallcompat/getdents_other.go +++ b/internal/syscallcompat/getdents_other.go @@ -1,17 +1,49 @@ -// +build !linux - package syscallcompat import ( - "log" + "os" + "syscall" + + "golang.org/x/sys/unix" "github.com/hanwen/go-fuse/fuse" ) -// HaveGetdents is true if we have a working implementation of Getdents -const HaveGetdents = false - -func Getdents(dir string) ([]fuse.DirEntry, error) { - log.Panic("only implemented on Linux") - return nil, nil +// emulateGetdents reads all directory entries from the open directory "fd" +// and returns them in a fuse.DirEntry slice. +func emulateGetdents(fd int) (out []fuse.DirEntry, err error) { + // os.File closes the fd in its finalizer. Duplicate the fd to not affect + // the original fd. + newFd, err := syscall.Dup(fd) + if err != nil { + return nil, err + } + f := os.NewFile(uintptr(newFd), "") + defer f.Close() + // Get all file names in the directory + names, err := f.Readdirnames(0) + if err != nil { + return nil, err + } + // Stat all of them and convert to fuse.DirEntry + out = make([]fuse.DirEntry, 0, len(names)) + for _, name := range names { + var st unix.Stat_t + err = Fstatat(fd, name, &st, unix.AT_SYMLINK_NOFOLLOW) + if err == syscall.ENOENT { + // File disappeared between readdir and stat. Pretend we did not + // see it. + continue + } + if err != nil { + return nil, err + } + newEntry := fuse.DirEntry{ + Name: name, + Mode: uint32(st.Mode) & syscall.S_IFMT, + Ino: st.Ino, + } + out = append(out, newEntry) + } + return out, nil } diff --git a/internal/syscallcompat/getdents_test.go b/internal/syscallcompat/getdents_test.go index 8f2bd09..131ffee 100644 --- a/internal/syscallcompat/getdents_test.go +++ b/internal/syscallcompat/getdents_test.go @@ -12,9 +12,19 @@ import ( "github.com/hanwen/go-fuse/fuse" ) +var getdentsUnderTest = getdents + func TestGetdents(t *testing.T) { + t.Logf("testing native getdents") + testGetdents(t) + t.Logf("testing emulateGetdents") + getdentsUnderTest = emulateGetdents + testGetdents(t) +} + +func testGetdents(t *testing.T) { // Fill a directory with filenames of length 1 ... 255 - testDir, err := ioutil.TempDir("", "TestGetdents") + testDir, err := ioutil.TempDir(tmpDir, "TestGetdents") if err != nil { t.Fatal(err) } @@ -35,17 +45,21 @@ func TestGetdents(t *testing.T) { if err != nil { t.Fatal(err) } + defer fd.Close() readdirEntries, err := fd.Readdir(0) if err != nil { t.Fatal(err) } - fd.Close() readdirMap := make(map[string]*syscall.Stat_t) for _, v := range readdirEntries { readdirMap[v.Name()] = fuse.ToStatT(v) } - // Read using our Getdents() - getdentsEntries, err := Getdents(dir) + // Read using our Getdents() implementation + _, err = fd.Seek(0, 0) // Rewind directory + if err != nil { + t.Fatal(err) + } + getdentsEntries, err := getdentsUnderTest(int(fd.Fd())) if err != nil { t.Fatal(err) } diff --git a/internal/syscallcompat/sys_linux.go b/internal/syscallcompat/sys_linux.go index fb8d8b3..b5b949e 100644 --- a/internal/syscallcompat/sys_linux.go +++ b/internal/syscallcompat/sys_linux.go @@ -7,6 +7,8 @@ import ( "golang.org/x/sys/unix" + "github.com/hanwen/go-fuse/fuse" + "github.com/rfjakob/gocryptfs/internal/tlog" ) @@ -115,3 +117,8 @@ func Fstatat(dirfd int, path string, stat *unix.Stat_t, flags int) (err error) { } return unix.Fstatat(dirfd, path, stat, flags) } + +// Getdents syscall. +func Getdents(fd int) ([]fuse.DirEntry, error) { + return getdents(fd) +}