libgocryptfs/internal/syscallcompat/getdents_linux.go

156 lines
4.1 KiB
Go

//go:build linux
// +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"
)
const sizeofDirent = int(unsafe.Sizeof(unix.Dirent{}))
// maxReclen sanity check: Reclen should never be larger than this.
// Due to padding between entries, it is 280 even on 32-bit architectures.
// See https://github.com/rfjakob/gocryptfs/issues/197 for details.
const maxReclen = 280
type DirEntry struct {
Name string
Mode uint32
}
// getdents wraps unix.Getdents and converts the result to []fuse.DirEntry.
func getdents(fd int) (entries []DirEntry, entriesSpecial []DirEntry, err 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 := unix.Getdents(fd, tmp)
// unix.Getdents has been observed to return EINTR on cifs mounts
if err == unix.EINTR {
if n > 0 {
smartBuf.Write(tmp[:n])
}
continue
} else if err != nil {
if smartBuf.Len() > 0 {
return nil, nil, syscall.EIO
}
return nil, 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 := *(*unix.Dirent)(unsafe.Pointer(&buf[offset]))
if s.Reclen == 0 {
// EBADR = Invalid request descriptor
return nil, nil, syscall.EBADR
}
if int(s.Reclen) > maxReclen {
return nil, nil, syscall.EBADR
}
offset += int(s.Reclen)
numEntries++
}
// Parse the buffer into entries.
// Note: syscall.ParseDirent() only returns the names,
// we want all the data, so we have to implement
// it on our own.
entries = make([]DirEntry, 0, numEntries)
offset = 0
for offset < len(buf) {
s := *(*unix.Dirent)(unsafe.Pointer(&buf[offset]))
name, err := getdentsName(s)
if err != nil {
return nil, nil, err
}
offset += int(s.Reclen)
if name == "." || name == ".." {
// These are always directories, no need to call convertDType.
entriesSpecial = append(entriesSpecial, DirEntry{
Mode: syscall.S_IFDIR,
Name: name,
})
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, DirEntry{
Mode: mode,
Name: name,
})
}
return entries, entriesSpecial, nil
}
// getdentsName extracts the filename from a Dirent struct and returns it as
// a Go string.
func getdentsName(s unix.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 {
// 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
func dtUnknownWarn(dirfd int) {
var buf syscall.Statfs_t
syscall.Fstatfs(dirfd, &buf)
}
// 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() { dtUnknownWarn(dirfd) })
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
}