diff options
author | Jakob Unterwurzacher | 2017-08-13 21:13:44 +0200 |
---|---|---|
committer | Jakob Unterwurzacher | 2017-08-15 19:03:57 +0200 |
commit | e50a6a57e57bc3cc925ba9a6e7f4dc1da4da3c84 (patch) | |
tree | 79df16efd4d2474502a0e5116a4ee9599a33daee /internal/syscallcompat/getdents_linux.go | |
parent | affb1c2f6617d66bdc9fda41b017e0de000c3691 (diff) |
syscallcompat: implement Getdents()
The Readdir function provided by os is inherently slow because
it calls Lstat on all files.
Getdents gives us all the information we need, but does not have
a proper wrapper in the stdlib.
Implement the "Getdents()" wrapper function that calls
syscall.Getdents() and parses the returned byte blob to a
fuse.DirEntry slice.
Diffstat (limited to 'internal/syscallcompat/getdents_linux.go')
-rw-r--r-- | internal/syscallcompat/getdents_linux.go | 138 |
1 files changed, 138 insertions, 0 deletions
diff --git a/internal/syscallcompat/getdents_linux.go b/internal/syscallcompat/getdents_linux.go new file mode 100644 index 0000000..97cd75f --- /dev/null +++ b/internal/syscallcompat/getdents_linux.go @@ -0,0 +1,138 @@ +// +build linux + +package syscallcompat + +// Other implementations of getdents in Go: +// https://github.com/ericlagergren/go-gnulib/blob/cb7a6e136427e242099b2c29d661016c19458801/dirent/getdents_unix.go +// https://github.com/golang/tools/blob/5831d16d18029819d39f99bdc2060b8eff410b6b/imports/fastwalk_unix.go + +import ( + "bytes" + "syscall" + "unsafe" + + "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) + // Collect syscall result in smartBuf. + // "bytes.Buffer" is smart about expanding the capacity and avoids the + // exponential runtime of simple append(). + var smartBuf bytes.Buffer + tmp := make([]byte, 10000) + for { + n, err := syscall.Getdents(fd, tmp) + if err != nil { + return nil, err + } + if n == 0 { + break + } + smartBuf.Write(tmp[:n]) + } + // Make sure we have at least Sizeof(Dirent) of zeros after the last + // entry. This prevents a cast to Dirent from reading past the buffer. + smartBuf.Grow(sizeofDirent) + buf := smartBuf.Bytes() + // Count the number of directory entries in the buffer so we can allocate + // a fuse.DirEntry slice of the correct size at once. + var numEntries, offset int + for offset < len(buf) { + s := *(*syscall.Dirent)(unsafe.Pointer(&buf[offset])) + if s.Reclen == 0 { + tlog.Warn.Printf("Getdents: corrupt entry #%d: Reclen=0 at offset=%d. Returning EBADR", + numEntries, offset) + // EBADR = Invalid request descriptor + return nil, syscall.EBADR + } + if int(s.Reclen) > sizeofDirent { + tlog.Warn.Printf("Getdents: corrupt entry #%d: Reclen=%d > %d. Returning EBADR", + numEntries, sizeofDirent, s.Reclen) + return nil, syscall.EBADR + } + offset += int(s.Reclen) + numEntries++ + } + // Parse the buffer into entries + entries := make([]fuse.DirEntry, 0, numEntries) + offset = 0 + for offset < len(buf) { + s := *(*syscall.Dirent)(unsafe.Pointer(&buf[offset])) + name, err := getdentsName(s) + if err != nil { + return nil, err + } + offset += int(s.Reclen) + if name == "." || name == ".." { + // os.File.Readdir() drops "." and "..". Let's be compatible. + continue + } + mode, err := convertDType(s.Type, dir+"/"+name) + if err != nil { + // The file may have been deleted in the meantime. Just skip it + // and go on. + continue + } + entries = append(entries, fuse.DirEntry{ + Ino: s.Ino, + Mode: mode, + Name: name, + }) + } + return entries, nil +} + +// getdentsName extracts the filename from a Dirent struct and returns it as +// a Go string. +func getdentsName(s syscall.Dirent) (string, error) { + // After the loop, l contains the index of the first '\0'. + l := 0 + for l = range s.Name { + if s.Name[l] == 0 { + break + } + } + if l < 1 { + tlog.Warn.Printf("Getdents: invalid name length l=%d. Returning EBADR", l) + // EBADR = Invalid request descriptor + return "", syscall.EBADR + } + // Copy to byte slice. + name := make([]byte, l) + for i := range name { + name[i] = byte(s.Name[i]) + } + return string(name), nil +} + +// convertDType converts a Dirent.Type to at Stat_t.Mode value. +func convertDType(dtype uint8, file string) (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() + var st syscall.Stat_t + err := syscall.Lstat(file, &st) + if err != nil { + return 0, err + } + // The S_IFMT bit mask extracts the file type from the mode. + return st.Mode & syscall.S_IFMT, nil +} |