libgocryptfs/internal/syscallcompat/getdents_linux.go
Jakob Unterwurzacher 70bcf58a9b 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.
2017-12-03 19:33:26 +01:00

136 lines
3.7 KiB
Go

// +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"
"sync"
"syscall"
"unsafe"
"golang.org/x/sys/unix"
"github.com/hanwen/go-fuse/fuse"
"github.com/rfjakob/gocryptfs/internal/tlog"
)
const sizeofDirent = int(unsafe.Sizeof(syscall.Dirent{}))
// 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().
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(fd, name, s.Type)
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
}
var dtUnknownWarnOnce sync.Once
// convertDType converts a Dirent.Type to at Stat_t.Mode value.
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 stat()
dtUnknownWarnOnce.Do(func() {
tlog.Warn.Printf("Getdents: convertDType: received DT_UNKNOWN, falling back to stat")
})
var st unix.Stat_t
err := Fstatat(dirfd, name, &st, unix.AT_SYMLINK_NOFOLLOW)
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
}