wasi: adds platform.Dirent in preparation of inode fetching (#1154)

wasi_snapshot_preview1 recently requires fd_readdir to return actual
inode values. On zero, wasi-libc will call fdstat to retrieve them.

This introduces our own `platform.Dirent` type and `Readdir` function
which a later change will allow fetching of inodes.

See https://github.com/WebAssembly/wasi-libc/pull/345

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2023-02-23 18:56:27 +08:00
committed by GitHub
parent 3a06ae38e7
commit bb002c862f
7 changed files with 270 additions and 155 deletions

View File

@@ -813,7 +813,7 @@ func fdReaddirFn(_ context.Context, mod api.Module, params []uint64) Errno {
if err != nil {
return ToErrno(err)
}
rd, dir = f.File.(fs.ReadDirFile), f.ReadDir
rd, dir = f.File, f.ReadDir
}
// First, determine the maximum directory entries that can be encoded as
@@ -831,26 +831,26 @@ func fdReaddirFn(_ context.Context, mod api.Module, params []uint64) Errno {
// The host keeps state for any unread entries from the prior call because
// we cannot seek to a previous directory position. Collect these entries.
entries, errno := lastDirEntries(dir, cookie)
dirents, errno := lastDirents(dir, cookie)
if errno != ErrnoSuccess {
return errno
}
// Add entries for dot and dot-dot as wasi-testsuite requires them.
if cookie == 0 && entries == nil {
if cookie == 0 && dirents == nil {
var err error
if f, ok := fsc.LookupFile(fd); !ok {
return ErrnoBadf
} else if entries, err = sys.DotEntries(f.File); err != nil {
} else if dirents, err = dotDirents(f.File); err != nil {
return ToErrno(err)
}
dir.Entries = entries
dir.Dirents = dirents
dir.CountRead = 2 // . and ..
}
// Check if we have maxDirEntries, and read more from the FS as needed.
if entryCount := len(entries); entryCount < maxDirEntries {
l, err := rd.ReadDir(maxDirEntries - entryCount)
if entryCount := len(dirents); entryCount < maxDirEntries {
l, err := platform.Readdir(rd, maxDirEntries-entryCount)
if err == io.EOF { // EOF is not an error
} else if err != nil {
if errno = ToErrno(err); errno == ErrnoNoent {
@@ -864,15 +864,15 @@ func fdReaddirFn(_ context.Context, mod api.Module, params []uint64) Errno {
}
} else {
dir.CountRead += uint64(len(l))
entries = append(entries, l...)
dirents = append(dirents, l...)
// Replace the cache with up to maxDirEntries, starting at cookie.
dir.Entries = entries
dir.Dirents = dirents
}
}
// Determine how many dirents we can write, excluding a potentially
// truncated entry.
bufused, direntCount, writeTruncatedEntry := maxDirents(entries, bufLen)
bufused, direntCount, writeTruncatedEntry := maxDirents(dirents, bufLen)
// Now, write entries to the underlying buffer.
if bufused > 0 {
@@ -883,12 +883,12 @@ func fdReaddirFn(_ context.Context, mod api.Module, params []uint64) Errno {
// ^^ yes this can overflow to negative, which means our implementation
// doesn't support writing greater than max int64 entries.
dirents, ok := mem.Read(buf, bufused)
buf, ok := mem.Read(buf, bufused)
if !ok {
return ErrnoFault
}
writeDirents(entries, direntCount, writeTruncatedEntry, dirents, d_next)
writeDirents(dirents, direntCount, writeTruncatedEntry, buf, d_next)
}
if !mem.WriteUint32Le(resultBufused, bufused) {
@@ -897,16 +897,29 @@ func fdReaddirFn(_ context.Context, mod api.Module, params []uint64) Errno {
return ErrnoSuccess
}
// dotDirents returns "." and "..", where "." has a real stat because
// wasi-testsuite does inode validation.
func dotDirents(f fs.File) ([]*platform.Dirent, error) {
var st platform.Stat_t
if err := platform.StatFile(f, &st); err != nil {
return nil, err
}
return []*platform.Dirent{
{Name: ".", Ino: st.Ino, Type: fs.ModeDir},
{Name: "..", Type: fs.ModeDir},
}, nil
}
const largestDirent = int64(math.MaxUint32 - DirentSize)
// lastDirEntries is broken out from fdReaddirFn for testability.
func lastDirEntries(dir *sys.ReadDir, cookie int64) (entries []fs.DirEntry, errno Errno) {
// lastDirents is broken out from fdReaddirFn for testability.
func lastDirents(dir *sys.ReadDir, cookie int64) (dirents []*platform.Dirent, errno Errno) {
if cookie < 0 {
errno = ErrnoInval // invalid as we will never send a negative cookie.
return
}
entryCount := int64(len(dir.Entries))
entryCount := int64(len(dir.Dirents))
if entryCount == 0 { // there was no prior call
if cookie != 0 {
errno = ErrnoInval // invalid as we haven't sent that cookie
@@ -924,12 +937,12 @@ func lastDirEntries(dir *sys.ReadDir, cookie int64) (entries []fs.DirEntry, errn
case cookiePos > entryCount:
errno = ErrnoInval // invalid as we read that far, yet.
case cookiePos > 0: // truncate so to avoid large lists.
entries = dir.Entries[cookiePos:]
dirents = dir.Dirents[cookiePos:]
default:
entries = dir.Entries
dirents = dir.Dirents
}
if len(entries) == 0 {
entries = nil
if len(dirents) == 0 {
dirents = nil
}
return
}
@@ -943,7 +956,7 @@ func lastDirEntries(dir *sys.ReadDir, cookie int64) (entries []fs.DirEntry, errn
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_readdir
// See https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/cloudlibc/src/libc/dirent/readdir.c#L44
func maxDirents(entries []fs.DirEntry, bufLen uint32) (bufused, direntCount uint32, writeTruncatedEntry bool) {
func maxDirents(entries []*platform.Dirent, bufLen uint32) (bufused, direntCount uint32, writeTruncatedEntry bool) {
lenRemaining := bufLen
for _, e := range entries {
if lenRemaining < DirentSize {
@@ -957,7 +970,7 @@ func maxDirents(entries []fs.DirEntry, bufLen uint32) (bufused, direntCount uint
}
// use int64 to guard against huge filenames
nameLen := int64(len(e.Name()))
nameLen := int64(len(e.Name))
var entryLen uint32
// Check to see if DirentSize + nameLen overflows, or if it would be
@@ -1000,21 +1013,21 @@ func maxDirents(entries []fs.DirEntry, bufLen uint32) (bufused, direntCount uint
// based on maxDirents. truncatedEntryLen means write one past entryCount,
// without its name. See maxDirents for why
func writeDirents(
entries []fs.DirEntry,
entryCount uint32,
dirents []*platform.Dirent,
direntCount uint32,
writeTruncatedEntry bool,
dirents []byte,
buf []byte,
d_next uint64,
) {
pos, i := uint32(0), uint32(0)
for ; i < entryCount; i++ {
e := entries[i]
nameLen := uint32(len(e.Name()))
for ; i < direntCount; i++ {
e := dirents[i]
nameLen := uint32(len(e.Name))
writeDirent(dirents[pos:], d_next, nameLen, e.IsDir())
writeDirent(buf[pos:], d_next, nameLen, e.IsDir())
pos += DirentSize
copy(dirents[pos:], e.Name())
copy(buf[pos:], e.Name)
pos += nameLen
d_next++
}
@@ -1025,11 +1038,11 @@ func writeDirents(
// Write a dirent without its name
dirent := make([]byte, DirentSize)
e := entries[i]
writeDirent(dirent, d_next, uint32(len(e.Name())), e.IsDir())
e := dirents[i]
writeDirent(dirent, d_next, uint32(len(e.Name)), e.IsDir())
// Potentially truncate it
copy(dirents[pos:], dirent)
copy(buf[pos:], dirent)
}
// writeDirent writes DirentSize bytes
@@ -1046,10 +1059,10 @@ func writeDirent(buf []byte, dNext uint64, dNamlen uint32, dType bool) {
}
// openedDir returns the directory and ErrnoSuccess if the fd points to a readable directory.
func openedDir(fsc *sys.FSContext, fd uint32) (fs.ReadDirFile, *sys.ReadDir, Errno) {
func openedDir(fsc *sys.FSContext, fd uint32) (fs.File, *sys.ReadDir, Errno) {
if f, ok := fsc.LookupFile(fd); !ok {
return nil, nil, ErrnoBadf
} else if d, ok := f.File.(fs.ReadDirFile); !ok {
} else if !f.IsDir() {
// fd_readdir docs don't indicate whether to return ErrnoNotdir or
// ErrnoBadf. It has been noticed that rust will crash on ErrnoNotdir,
// and POSIX C ref seems to not return this, so we don't either.
@@ -1061,7 +1074,7 @@ func openedDir(fsc *sys.FSContext, fd uint32) (fs.ReadDirFile, *sys.ReadDir, Err
if f.ReadDir == nil {
f.ReadDir = &sys.ReadDir{}
}
return d, f.ReadDir, ErrnoSuccess
return f.File, f.ReadDir, ErrnoSuccess
}
}

View File

@@ -1801,21 +1801,21 @@ func Test_fdRead_Errors(t *testing.T) {
}
var (
testDirEntries = func() []fs.DirEntry {
entries, err := fstest.FS.ReadDir("dir")
if err != nil {
panic(err)
}
testDirents = func() []*platform.Dirent {
d, err := fstest.FS.Open("dir")
if err != nil {
panic(err)
}
defer d.Close()
dots, err := sys.DotEntries(d)
dirents, err := platform.Readdir(d, -1)
if err != nil {
panic(err)
}
return append(dots, entries...)
dots := []*platform.Dirent{
{Name: ".", Type: fs.ModeDir},
{Name: "..", Type: fs.ModeDir},
}
return append(dots, dirents...)
}()
direntDot = []byte{
@@ -1891,7 +1891,7 @@ func Test_fdReaddir(t *testing.T) {
expectedMem: direntDot,
expectedReadDir: &sys.ReadDir{
CountRead: 2,
Entries: testDirEntries[0:2], // dot and dot-dot
Dirents: testDirents[0:2], // dot and dot-dot
},
},
{
@@ -1908,7 +1908,7 @@ func Test_fdReaddir(t *testing.T) {
expectedMem: dirents,
expectedReadDir: &sys.ReadDir{
CountRead: 5,
Entries: testDirEntries,
Dirents: testDirents,
},
},
{
@@ -1925,7 +1925,7 @@ func Test_fdReaddir(t *testing.T) {
expectedMem: direntDot[:DirentSize], // header without name
expectedReadDir: &sys.ReadDir{
CountRead: 3,
Entries: testDirEntries[0:3],
Dirents: testDirents[0:3],
},
},
{
@@ -1942,7 +1942,7 @@ func Test_fdReaddir(t *testing.T) {
expectedMem: direntDot,
expectedReadDir: &sys.ReadDir{
CountRead: 3,
Entries: testDirEntries[0:3],
Dirents: testDirents[0:3],
},
},
{
@@ -1950,14 +1950,14 @@ func Test_fdReaddir(t *testing.T) {
dir: func() *sys.FileEntry {
dir, err := fstest.FS.Open("dir")
require.NoError(t, err)
entry, err := dir.(fs.ReadDirFile).ReadDir(1)
dirent, err := platform.Readdir(dir, 1)
require.NoError(t, err)
return &sys.FileEntry{
File: dir,
ReadDir: &sys.ReadDir{
CountRead: 3,
Entries: append(testDirEntries[0:2], entry...),
Dirents: append(testDirents[0:2], dirent...),
},
}
},
@@ -1967,7 +1967,7 @@ func Test_fdReaddir(t *testing.T) {
expectedMem: direntDotDot,
expectedReadDir: &sys.ReadDir{
CountRead: 4,
Entries: testDirEntries[1:4],
Dirents: testDirents[1:4],
},
},
{
@@ -1975,14 +1975,14 @@ func Test_fdReaddir(t *testing.T) {
dir: func() *sys.FileEntry {
dir, err := fstest.FS.Open("dir")
require.NoError(t, err)
entry, err := dir.(fs.ReadDirFile).ReadDir(1)
dirent, err := platform.Readdir(dir, 1)
require.NoError(t, err)
return &sys.FileEntry{
File: dir,
ReadDir: &sys.ReadDir{
CountRead: 3,
Entries: append(testDirEntries[0:2], entry...),
Dirents: append(testDirents[0:2], dirent...),
},
}
},
@@ -1993,7 +1993,7 @@ func Test_fdReaddir(t *testing.T) {
expectedMemSize: len(direntDotDot), // we do not want to compare the full buffer since we don't know what the leftover 4 bytes will contain.
expectedReadDir: &sys.ReadDir{
CountRead: 4,
Entries: testDirEntries[1:4],
Dirents: testDirents[1:4],
},
},
{
@@ -2001,14 +2001,14 @@ func Test_fdReaddir(t *testing.T) {
dir: func() *sys.FileEntry {
dir, err := fstest.FS.Open("dir")
require.NoError(t, err)
entry, err := dir.(fs.ReadDirFile).ReadDir(1)
dirent, err := platform.Readdir(dir, 1)
require.NoError(t, err)
return &sys.FileEntry{
File: dir,
ReadDir: &sys.ReadDir{
CountRead: 3,
Entries: append(testDirEntries[0:2], entry...),
Dirents: append(testDirents[0:2], dirent...),
},
}
},
@@ -2018,7 +2018,7 @@ func Test_fdReaddir(t *testing.T) {
expectedMem: append(direntDotDot, dirent1[0:24]...),
expectedReadDir: &sys.ReadDir{
CountRead: 5,
Entries: testDirEntries[1:5],
Dirents: testDirents[1:5],
},
},
{
@@ -2026,14 +2026,14 @@ func Test_fdReaddir(t *testing.T) {
dir: func() *sys.FileEntry {
dir, err := fstest.FS.Open("dir")
require.NoError(t, err)
entry, err := dir.(fs.ReadDirFile).ReadDir(1)
dirent, err := platform.Readdir(dir, 1)
require.NoError(t, err)
return &sys.FileEntry{
File: dir,
ReadDir: &sys.ReadDir{
CountRead: 3,
Entries: append(testDirEntries[0:2], entry...),
Dirents: append(testDirents[0:2], dirent...),
},
}
},
@@ -2043,7 +2043,7 @@ func Test_fdReaddir(t *testing.T) {
expectedMem: append(direntDotDot, dirent1...),
expectedReadDir: &sys.ReadDir{
CountRead: 5,
Entries: testDirEntries[1:5],
Dirents: testDirents[1:5],
},
},
{
@@ -2051,14 +2051,14 @@ func Test_fdReaddir(t *testing.T) {
dir: func() *sys.FileEntry {
dir, err := fstest.FS.Open("dir")
require.NoError(t, err)
two, err := dir.(fs.ReadDirFile).ReadDir(2)
two, err := platform.Readdir(dir, 2)
require.NoError(t, err)
return &sys.FileEntry{
File: dir,
ReadDir: &sys.ReadDir{
CountRead: 4,
Entries: append(testDirEntries[0:2], two[0:]...),
Dirents: append(testDirents[0:2], two[0:]...),
},
}
},
@@ -2068,7 +2068,7 @@ func Test_fdReaddir(t *testing.T) {
expectedMem: dirent1,
expectedReadDir: &sys.ReadDir{
CountRead: 5,
Entries: testDirEntries[2:],
Dirents: testDirents[2:],
},
},
{
@@ -2076,14 +2076,14 @@ func Test_fdReaddir(t *testing.T) {
dir: func() *sys.FileEntry {
dir, err := fstest.FS.Open("dir")
require.NoError(t, err)
two, err := dir.(fs.ReadDirFile).ReadDir(2)
two, err := platform.Readdir(dir, 2)
require.NoError(t, err)
return &sys.FileEntry{
File: dir,
ReadDir: &sys.ReadDir{
CountRead: 4,
Entries: append(testDirEntries[0:2], two[0:]...),
Dirents: append(testDirents[0:2], two[0:]...),
},
}
},
@@ -2093,7 +2093,7 @@ func Test_fdReaddir(t *testing.T) {
expectedMem: append(dirent1, dirent2...),
expectedReadDir: &sys.ReadDir{
CountRead: 5,
Entries: testDirEntries[2:],
Dirents: testDirents[2:],
},
},
}

View File

@@ -2,7 +2,6 @@ package wasi_snapshot_preview1
import (
"io"
"io/fs"
"syscall"
"testing"
@@ -98,12 +97,12 @@ func Test_fdRead_shouldContinueRead(t *testing.T) {
}
}
func Test_lastDirEntries(t *testing.T) {
func Test_lastDirents(t *testing.T) {
tests := []struct {
name string
f *sys.ReadDir
cookie int64
expectedEntries []fs.DirEntry
expectedDirents []*platform.Dirent
expectedErrno Errno
}{
{
@@ -118,7 +117,7 @@ func Test_lastDirEntries(t *testing.T) {
name: "cookie is negative",
f: &sys.ReadDir{
CountRead: 3,
Entries: testDirEntries,
Dirents: testDirents,
},
cookie: -1,
expectedErrno: ErrnoInval,
@@ -127,7 +126,7 @@ func Test_lastDirEntries(t *testing.T) {
name: "cookie is greater than last d_next",
f: &sys.ReadDir{
CountRead: 3,
Entries: testDirEntries,
Dirents: testDirents,
},
cookie: 5,
expectedErrno: ErrnoInval,
@@ -136,25 +135,25 @@ func Test_lastDirEntries(t *testing.T) {
name: "cookie is last pos",
f: &sys.ReadDir{
CountRead: 3,
Entries: testDirEntries,
Dirents: testDirents,
},
cookie: 3,
expectedEntries: nil,
expectedDirents: nil,
},
{
name: "cookie is one before last pos",
f: &sys.ReadDir{
CountRead: 3,
Entries: testDirEntries,
Dirents: testDirents,
},
cookie: 2,
expectedEntries: testDirEntries[2:],
expectedDirents: testDirents[2:],
},
{
name: "cookie is before current entries",
f: &sys.ReadDir{
CountRead: 5,
Entries: testDirEntries,
Dirents: testDirents,
},
cookie: 1,
expectedErrno: ErrnoNosys, // not implemented
@@ -163,10 +162,10 @@ func Test_lastDirEntries(t *testing.T) {
name: "read from the beginning (cookie=0)",
f: &sys.ReadDir{
CountRead: 3,
Entries: testDirEntries,
Dirents: testDirents,
},
cookie: 0,
expectedEntries: testDirEntries,
expectedDirents: testDirents,
},
}
@@ -178,9 +177,9 @@ func Test_lastDirEntries(t *testing.T) {
if f == nil {
f = &sys.ReadDir{}
}
entries, errno := lastDirEntries(f, tc.cookie)
entries, errno := lastDirents(f, tc.cookie)
require.Equal(t, tc.expectedErrno, errno)
require.Equal(t, tc.expectedEntries, entries)
require.Equal(t, tc.expectedDirents, entries)
})
}
}
@@ -188,7 +187,7 @@ func Test_lastDirEntries(t *testing.T) {
func Test_maxDirents(t *testing.T) {
tests := []struct {
name string
entries []fs.DirEntry
dirents []*platform.Dirent
maxLen uint32
expectedCount uint32
expectedwriteTruncatedEntry bool
@@ -199,28 +198,28 @@ func Test_maxDirents(t *testing.T) {
},
{
name: "can't fit one",
entries: testDirEntries,
dirents: testDirents,
maxLen: 23,
expectedBufused: 23,
expectedwriteTruncatedEntry: false,
},
{
name: "only fits header",
entries: testDirEntries,
dirents: testDirents,
maxLen: 24,
expectedBufused: 24,
expectedwriteTruncatedEntry: true,
},
{
name: "one",
entries: testDirEntries,
dirents: testDirents,
maxLen: 25,
expectedCount: 1,
expectedBufused: 25,
},
{
name: "one but not room for two's name",
entries: testDirEntries,
dirents: testDirents,
maxLen: 25 + 25,
expectedCount: 1,
expectedwriteTruncatedEntry: true, // can write DirentSize
@@ -228,14 +227,14 @@ func Test_maxDirents(t *testing.T) {
},
{
name: "two",
entries: testDirEntries,
dirents: testDirents,
maxLen: 25 + 26,
expectedCount: 2,
expectedBufused: 25 + 26,
},
{
name: "two but not three's dirent",
entries: testDirEntries,
dirents: testDirents,
maxLen: 25 + 26 + 20,
expectedCount: 2,
expectedwriteTruncatedEntry: false, // 20 + 4 == DirentSize
@@ -243,7 +242,7 @@ func Test_maxDirents(t *testing.T) {
},
{
name: "two but not three's name",
entries: testDirEntries,
dirents: testDirents,
maxLen: 25 + 26 + 26,
expectedCount: 2,
expectedwriteTruncatedEntry: true, // can write DirentSize
@@ -251,7 +250,7 @@ func Test_maxDirents(t *testing.T) {
},
{
name: "three",
entries: testDirEntries,
dirents: testDirents,
maxLen: 25 + 26 + 27,
expectedCount: 3,
expectedwriteTruncatedEntry: false, // end of dir
@@ -259,7 +258,7 @@ func Test_maxDirents(t *testing.T) {
},
{
name: "max",
entries: testDirEntries,
dirents: testDirents,
maxLen: 100,
expectedCount: 3,
expectedwriteTruncatedEntry: false, // end of dir
@@ -271,7 +270,7 @@ func Test_maxDirents(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
bufused, direntCount, writeTruncatedEntry := maxDirents(tc.entries, tc.maxLen)
bufused, direntCount, writeTruncatedEntry := maxDirents(tc.dirents, tc.maxLen)
require.Equal(t, tc.expectedCount, direntCount)
require.Equal(t, tc.expectedwriteTruncatedEntry, writeTruncatedEntry)
require.Equal(t, tc.expectedBufused, bufused)
@@ -280,12 +279,17 @@ func Test_maxDirents(t *testing.T) {
}
var (
testDirEntries = func() []fs.DirEntry {
entries, err := fstest.FS.ReadDir("dir")
testDirents = func() []*platform.Dirent {
dir, err := fstest.FS.Open("dir")
if err != nil {
panic(err)
}
return entries
defer dir.Close()
dirents, err := platform.Readdir(dir, -1)
if err != nil {
panic(err)
}
return dirents
}()
dirent1 = []byte{
@@ -314,37 +318,37 @@ var (
func Test_writeDirents(t *testing.T) {
tests := []struct {
name string
entries []fs.DirEntry
entries []*platform.Dirent
entryCount uint32
writeTruncatedEntry bool
expectedEntriesBuf []byte
}{
{
name: "none",
entries: testDirEntries,
entries: testDirents,
},
{
name: "one",
entries: testDirEntries,
entries: testDirents,
entryCount: 1,
expectedEntriesBuf: dirent1,
},
{
name: "two",
entries: testDirEntries,
entries: testDirents,
entryCount: 2,
expectedEntriesBuf: append(dirent1, dirent2...),
},
{
name: "two with truncated",
entries: testDirEntries,
entries: testDirents,
entryCount: 2,
writeTruncatedEntry: true,
expectedEntriesBuf: append(append(dirent1, dirent2...), dirent3[0:10]...),
},
{
name: "three",
entries: testDirEntries,
entries: testDirents,
entryCount: 3,
expectedEntriesBuf: append(append(dirent1, dirent2...), dirent3...),
},

View File

@@ -1,6 +1,7 @@
package platform
import (
"io"
"io/fs"
"syscall"
)
@@ -21,11 +22,69 @@ func Readdirnames(f fs.File, n int) (names []string, err error) {
case fs.ReadDirFile:
var entries []fs.DirEntry
entries, err = f.ReadDir(n)
if err == nil {
names = make([]string, 0, len(entries))
for _, e := range entries {
names = append(names, e.Name())
}
if err != nil {
break
}
names = make([]string, 0, len(entries))
for _, e := range entries {
names = append(names, e.Name())
}
default:
err = syscall.ENOTDIR
}
err = UnwrapOSError(err)
return
}
// Dirent is an entry read from a directory.
//
// This is a portable variant of syscall.Dirent containing fields needed for
// WebAssembly ABI including WASI snapshot-01 and wasi-filesystem. Unlike
// fs.DirEntry, this may include the Ino.
type Dirent struct {
// ^^ Dirent name matches syscall.Dirent
// Name is the base name of the directory entry.
Name string
// Ino is the file serial number, or zero if not available.
Ino uint64
// Type is fs.FileMode masked on fs.ModeType. For example, zero is a
// regular file, fs.ModeDir is a directory and fs.ModeIrregular is unknown.
Type fs.FileMode
}
// IsDir returns true if the Type is fs.ModeDir.
func (d *Dirent) IsDir() bool {
return d.Type == fs.ModeDir
}
// Readdir reads the contents of the directory associated with file and returns
// a slice of up to n Dirent values in an arbitrary order. This is a stateful
// function, so subsequent calls return any next values.
//
// If n > 0, Readdir returns at most n entries or an error.
// If n <= 0, Readdir returns all remaining entries or an error.
//
// Note: The error will be nil or a syscall.Errno. No error is returned on EOF.
func Readdir(f fs.File, n int) (dirents []*Dirent, err error) {
// ^^ case format is to match POSIX and similar to os.File.Readdir
switch f := f.(type) {
case fs.ReadDirFile:
var entries []fs.DirEntry
entries, err = f.ReadDir(n)
if err == io.EOF {
err = nil
}
if err != nil {
break
}
dirents = make([]*Dirent, 0, len(entries))
for _, e := range entries {
// By default, we don't attempt to read inode data
dirents = append(dirents, &Dirent{Name: e.Name(), Type: e.Type()})
}
default:
err = syscall.ENOTDIR

View File

@@ -77,3 +77,84 @@ func TestReaddirnames(t *testing.T) {
})
}
}
func TestReaddir(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
require.NoError(t, fstest.WriteTestFiles(tmpDir))
dirFS := os.DirFS(tmpDir)
tests := []struct {
name string
fs fs.FS
}{
{name: "os.DirFS", fs: dirFS}, // To test readdirFile
{name: "fstest.MapFS", fs: fstest.FS}, // To test adaptation of ReadDirFile
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
dirF, err := tc.fs.Open(".")
require.NoError(t, err)
defer dirF.Close()
t.Run("dir", func(t *testing.T) {
dirents, err := platform.Readdir(dirF, -1)
require.NoError(t, err)
sort.Slice(dirents, func(i, j int) bool { return dirents[i].Name < dirents[j].Name })
require.Equal(t, 5, len(dirents))
require.Equal(t, "animals.txt", dirents[0].Name)
require.Zero(t, dirents[0].Type)
require.Equal(t, "dir", dirents[1].Name)
require.Equal(t, fs.ModeDir, dirents[1].Type)
require.Equal(t, "empty.txt", dirents[2].Name)
require.Zero(t, dirents[2].Type)
require.Equal(t, "emptydir", dirents[3].Name)
require.Equal(t, fs.ModeDir, dirents[3].Type)
require.Equal(t, "sub", dirents[4].Name)
require.Equal(t, fs.ModeDir, dirents[4].Type)
// read again even though it is exhausted
dirents, err = platform.Readdir(dirF, 100)
require.NoError(t, err)
require.Zero(t, len(dirents))
})
// windows and fstest.MapFS allow you to read a closed dir
if runtime.GOOS != "windows" && tc.name != "fstest.MapFS" {
t.Run("closed dir", func(t *testing.T) {
require.NoError(t, dirF.Close())
_, err := platform.Readdir(dirF, -1)
require.EqualErrno(t, syscall.EIO, err)
})
}
fileF, err := tc.fs.Open("empty.txt")
require.NoError(t, err)
defer fileF.Close()
t.Run("file", func(t *testing.T) {
_, err := platform.Readdir(fileF, -1)
require.EqualErrno(t, syscall.ENOTDIR, err)
})
subdirF, err := tc.fs.Open("sub")
require.NoError(t, err)
defer subdirF.Close()
t.Run("subdir", func(t *testing.T) {
dirents, err := platform.Readdir(subdirF, -1)
require.NoError(t, err)
sort.Slice(dirents, func(i, j int) bool { return dirents[i].Name < dirents[j].Name })
require.Equal(t, 1, len(dirents))
require.Equal(t, "test.txt", dirents[0].Name)
require.Zero(t, dirents[0].Type)
})
})
}
}

View File

@@ -187,20 +187,20 @@ func (f *FileEntry) IsDir() bool {
}
// Stat returns the underlying stat of this file.
func (f *FileEntry) Stat(stat *platform.Stat_t) (err error) {
err = platform.StatFile(f.File, stat)
if err == nil && stat.Mode.IsDir() {
f.isDirectory = true
func (f *FileEntry) Stat(st *platform.Stat_t) (err error) {
err = platform.StatFile(f.File, st)
if err == nil {
f.isDirectory = st.Mode.IsDir()
}
return
}
// ReadDir is the status of a prior fs.ReadDirFile call.
type ReadDir struct {
// CountRead is the total count of files read including Entries.
// CountRead is the total count of files read including Dirents.
CountRead uint64
// Entries is the contents of the last fs.ReadDirFile call. Notably,
// Dirents is the contents of the last platform.Readdir call. Notably,
// directory listing are not rewindable, so we keep entries around in case
// the caller mis-estimated their buffer and needs a few still cached.
//
@@ -208,7 +208,7 @@ type ReadDir struct {
// In wasi preview1, dot and dot-dot entries are required to exist, but the
// reverse is true for preview2. More importantly, preview2 holds separate
// stateful dir-entry-streams per file.
Entries []fs.DirEntry
Dirents []*platform.Dirent
}
type FSContext struct {
@@ -333,7 +333,7 @@ func (c *FSContext) ReOpenDir(fd uint32) (*FileEntry, error) {
return f, err
}
f.ReadDir.CountRead, f.ReadDir.Entries = 0, nil
f.ReadDir.CountRead, f.ReadDir.Dirents = 0, nil
return f, nil
}
@@ -454,46 +454,3 @@ func WriterForFile(fsc *FSContext, fd uint32) (writer io.Writer) {
}
return
}
// DotEntries returns "." and "..", where "." has a real stat because
// wasi-testsuite does inode validation.
func DotEntries(f fs.File) ([]fs.DirEntry, error) {
var st platform.Stat_t
if err := platform.StatFile(f, &st); err != nil {
return nil, err
}
return []fs.DirEntry{&dotEntry{stat: &st}, dotDotEntry{}}, nil
}
// dotEntry is a fs.DirEntry representing the directory being listed.
type dotEntry struct {
// stat is the stat of the opened directory
stat *platform.Stat_t
}
func (i *dotEntry) Name() string { return "." }
func (i *dotEntry) Type() fs.FileMode { return i.stat.Mode.Type() }
func (i *dotEntry) Info() (fs.FileInfo, error) { return i, nil }
func (i *dotEntry) Size() int64 { return i.stat.Size }
func (i *dotEntry) Mode() fs.FileMode { return i.stat.Mode }
func (i *dotEntry) ModTime() time.Time {
return time.Unix(i.stat.Mtim/1e9, i.stat.Mtim%1e9)
}
func (i *dotEntry) IsDir() bool { return true }
func (i *dotEntry) Sys() interface{} { return nil }
// dotDotEntry is a fake entry for dot-dot (".."), added to satisfy WASI tests.
//
// Note: This is intentionally invalid as WASI decided that it must be present
// on any list, including the root directory: No values are tested apart from
// the name.
type dotDotEntry struct{}
func (dotDotEntry) Name() string { return ".." }
func (dotDotEntry) Type() fs.FileMode { return fs.ModeDir }
func (dotDotEntry) Info() (fs.FileInfo, error) { return dotDotEntry{}, nil }
func (dotDotEntry) Size() int64 { return 0 }
func (dotDotEntry) Mode() fs.FileMode { return fs.ModeDir }
func (dotDotEntry) ModTime() time.Time { return time.Unix(0, 0) }
func (dotDotEntry) IsDir() bool { return true }
func (dotDotEntry) Sys() interface{} { return nil }

View File

@@ -12,6 +12,7 @@ import (
"testing"
"testing/fstest"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/sysfs"
testfs "github.com/tetratelabs/wazero/internal/testing/fs"
"github.com/tetratelabs/wazero/internal/testing/require"
@@ -240,7 +241,7 @@ func TestFSContext_ReOpenDir(t *testing.T) {
require.True(t, ok)
// Set arbitrary state.
ent.ReadDir = &ReadDir{Entries: make([]fs.DirEntry, 10), CountRead: 12345}
ent.ReadDir = &ReadDir{Dirents: make([]*platform.Dirent, 10), CountRead: 12345}
// Then reopen the same file descriptor.
ent, err = fsc.ReOpenDir(dirFd)