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.
This commit is contained in:
Jakob Unterwurzacher 2017-12-03 17:57:08 +01:00
parent e33593d30d
commit 70bcf58a9b
5 changed files with 84 additions and 46 deletions

View File

@ -271,20 +271,14 @@ func (fs *FS) OpenDir(dirName string, context *fuse.Context) ([]fuse.DirEntry, f
cDirAbsPath := filepath.Join(fs.args.Cipherdir, cDirName) cDirAbsPath := filepath.Join(fs.args.Cipherdir, cDirName)
var cipherEntries []fuse.DirEntry var cipherEntries []fuse.DirEntry
var status fuse.Status var status fuse.Status
if syscallcompat.HaveGetdents { fd, err := syscall.Open(cDirAbsPath, syscall.O_RDONLY|syscall.O_NOFOLLOW, 0)
// Getdents avoids calling Lstat on each file. if err != nil {
cipherEntries, err = syscallcompat.Getdents(cDirAbsPath) return nil, fuse.ToStatus(err)
if err != nil { }
return nil, fuse.ToStatus(err) defer syscall.Close(fd)
} cipherEntries, err = syscallcompat.Getdents(fd)
} else { if err != nil {
haveGetdentsWarnOnce.Do(func() { return nil, fuse.ToStatus(err)
tlog.Warn.Printf("OpenDir: Getdents not available, falling back to OpenDir")
})
cipherEntries, status = fs.FileSystem.OpenDir(cDirName, context)
if !status.Ok() {
return nil, status
}
} }
// Get DirIV (stays nil if PlaintextNames is used) // Get DirIV (stays nil if PlaintextNames is used)
var cachedIV []byte var cachedIV []byte

View File

@ -12,26 +12,17 @@ import (
"syscall" "syscall"
"unsafe" "unsafe"
"golang.org/x/sys/unix"
"github.com/hanwen/go-fuse/fuse" "github.com/hanwen/go-fuse/fuse"
"github.com/rfjakob/gocryptfs/internal/tlog" "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{})) const sizeofDirent = int(unsafe.Sizeof(syscall.Dirent{}))
// Getdents wraps syscall.Getdents and converts the result to []fuse.DirEntry. // 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 func getdents(fd int) ([]fuse.DirEntry, error) {
// 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)
// Collect syscall result in smartBuf. // Collect syscall result in smartBuf.
// "bytes.Buffer" is smart about expanding the capacity and avoids the // "bytes.Buffer" is smart about expanding the capacity and avoids the
// exponential runtime of simple append(). // 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. // os.File.Readdir() drops "." and "..". Let's be compatible.
continue continue
} }
mode, err := convertDType(s.Type, dir+"/"+name) mode, err := convertDType(fd, name, s.Type)
if err != nil { if err != nil {
// The file may have been deleted in the meantime. Just skip it // The file may have been deleted in the meantime. Just skip it
// and go on. // and go on.
@ -125,17 +116,17 @@ func getdentsName(s syscall.Dirent) (string, error) {
var dtUnknownWarnOnce sync.Once var dtUnknownWarnOnce sync.Once
// convertDType converts a Dirent.Type to at Stat_t.Mode value. // 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 { if dtype != syscall.DT_UNKNOWN {
// Shift up by four octal digits = 12 bits // Shift up by four octal digits = 12 bits
return uint32(dtype) << 12, nil return uint32(dtype) << 12, nil
} }
// DT_UNKNOWN: we have to call Lstat() // DT_UNKNOWN: we have to call stat()
dtUnknownWarnOnce.Do(func() { 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 var st unix.Stat_t
err := syscall.Lstat(file, &st) err := Fstatat(dirfd, name, &st, unix.AT_SYMLINK_NOFOLLOW)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@ -1,17 +1,49 @@
// +build !linux
package syscallcompat package syscallcompat
import ( import (
"log" "os"
"syscall"
"golang.org/x/sys/unix"
"github.com/hanwen/go-fuse/fuse" "github.com/hanwen/go-fuse/fuse"
) )
// HaveGetdents is true if we have a working implementation of Getdents // emulateGetdents reads all directory entries from the open directory "fd"
const HaveGetdents = false // and returns them in a fuse.DirEntry slice.
func emulateGetdents(fd int) (out []fuse.DirEntry, err error) {
func Getdents(dir string) ([]fuse.DirEntry, error) { // os.File closes the fd in its finalizer. Duplicate the fd to not affect
log.Panic("only implemented on Linux") // the original fd.
return nil, nil 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
} }

View File

@ -12,9 +12,19 @@ import (
"github.com/hanwen/go-fuse/fuse" "github.com/hanwen/go-fuse/fuse"
) )
var getdentsUnderTest = getdents
func TestGetdents(t *testing.T) { 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 // Fill a directory with filenames of length 1 ... 255
testDir, err := ioutil.TempDir("", "TestGetdents") testDir, err := ioutil.TempDir(tmpDir, "TestGetdents")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -35,17 +45,21 @@ func TestGetdents(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer fd.Close()
readdirEntries, err := fd.Readdir(0) readdirEntries, err := fd.Readdir(0)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
fd.Close()
readdirMap := make(map[string]*syscall.Stat_t) readdirMap := make(map[string]*syscall.Stat_t)
for _, v := range readdirEntries { for _, v := range readdirEntries {
readdirMap[v.Name()] = fuse.ToStatT(v) readdirMap[v.Name()] = fuse.ToStatT(v)
} }
// Read using our Getdents() // Read using our Getdents() implementation
getdentsEntries, err := Getdents(dir) _, err = fd.Seek(0, 0) // Rewind directory
if err != nil {
t.Fatal(err)
}
getdentsEntries, err := getdentsUnderTest(int(fd.Fd()))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -7,6 +7,8 @@ import (
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"github.com/hanwen/go-fuse/fuse"
"github.com/rfjakob/gocryptfs/internal/tlog" "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) return unix.Fstatat(dirfd, path, stat, flags)
} }
// Getdents syscall.
func Getdents(fd int) ([]fuse.DirEntry, error) {
return getdents(fd)
}