Files
rclone/vfs/vfstest/dir_unix.go
T
Janne Beate BakengandNick Craig-Wood 00bd00d83d mount2: fix empty directory listings on re-read
With cmd/mount2, reading a directory more than once returned the correct
entries on the first read but nothing on subsequent reads. Plain `ls`
triggers this: it does lseek(fd, 0, SEEK_SET) to rewind the directory
before a second getdents.

go-fuse v2.9.0 rewinds a directory stream by calling Seekdir on the
FileSeekdirer interface. dirStream did not implement it, so go-fuse
returned ENOTSUP and produced an empty listing on every read after the
first.

This implements Seekdir on dirStream: a rewind to offset 0 resets the
stream to the start, restoring correct listings on re-read. Non-zero
offsets are uncommon for in-memory listings and still return ENOTSUP,
matching go-fuse's own default. A compile-time interface assertion is
added so signature drift on future go-fuse updates is caught at build
time.

Before: second and subsequent reads of a directory returned no entries.
After: directories list correctly on every read.

See: https://github.com/hanwen/go-fuse/issues/549
Co-authored-by: Nick Craig-Wood <nick@craig-wood.com>
2026-06-01 12:06:03 +01:00

76 lines
2.1 KiB
Go

//go:build linux || darwin || freebsd
package vfstest
import (
"syscall"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// countDirFd reads the directory referred to by fd to end-of-stream using
// raw getdents syscalls and returns the number of entries (excluding "." and
// ".." which ParseDirent skips).
//
// This deliberately uses syscall.ReadDirent rather than (*os.File).Readdir so
// that it exercises the kernel readdir path directly on a single file
// descriptor, the same way an in-process rewinddir does.
func countDirFd(t *testing.T, fd int) int {
buf := make([]byte, 8192)
var names []string
for {
n, err := syscall.ReadDirent(fd, buf)
require.NoError(t, err)
if n <= 0 {
break
}
_, _, names = syscall.ParseDirent(buf[:n], -1, names)
}
return len(names)
}
// TestDirRewind checks that re-reading a directory after rewinding the
// directory stream (lseek(fd, 0, SEEK_SET), as rewinddir(3) does) returns the
// same entries as the first read.
//
// This reproduces the bug where the mount2 backend returned an empty listing
// on every read after the first because go-fuse v2.9.0 rewinds a directory by
// calling Seekdir, which dirStream did not implement. See PR #9469 and
// https://github.com/hanwen/go-fuse/issues/549
func TestDirRewind(t *testing.T) {
run.skipIfVFS(t)
run.skipIfNoFUSE(t)
run.mkdir(t, "dir")
run.createFile(t, "dir/f1", "1")
run.createFile(t, "dir/f2", "2")
run.createFile(t, "dir/f3", "3")
run.checkDir(t, "dir/|dir/f1 1|dir/f2 1|dir/f3 1")
fh, err := run.os.Open(run.path("dir"))
require.NoError(t, err)
defer func() {
_ = fh.Close()
}()
fd := int(fh.Fd())
first := countDirFd(t, fd)
assert.Equal(t, 3, first, "first read should see all entries")
// rewinddir == lseek(fd, 0, SEEK_SET)
_, err = syscall.Seek(fd, 0, 0)
require.NoError(t, err)
second := countDirFd(t, fd)
assert.Equal(t, first, second, "re-read after rewind should match first read")
require.NoError(t, fh.Close())
run.rm(t, "dir/f1")
run.rm(t, "dir/f2")
run.rm(t, "dir/f3")
run.rmdir(t, "dir")
run.checkDir(t, "")
}