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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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...),
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user