diff --git a/README.md b/README.md index ee31afcd04..8656f21503 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,22 @@ on your machine unless you explicitly allow it. System access is defined by an emerging specification called WebAssembly System Interface ([WASI](https://github.com/WebAssembly/WASI)). WASI defines -how WebAssembly programs interact with the host embedding them. For example, -WASI defines functions for reading the time, or a random number. +how WebAssembly programs interact with the host embedding them. -This repository includes several [examples](examples) that expose system -interfaces, via the module `wazero.WASISnapshotPreview1`. These examples are -tested and a good way to learn what's possible with wazero. +For example, here's how you can allow WebAssembly modules to read +"/work/home/a.txt" as "/a.txt" or "./a.txt": +```go +wasi, err := r.InstantiateModule(wazero.WASISnapshotPreview1()) +defer wasi.Close() + +sysConfig := wazero.NewSysConfig().WithFS(os.DirFS("/work/home")) +module, err := wazero.StartWASICommandWithConfig(r, compiled, sysConfig) +defer module.Close() +... +``` + +The best way to learn this and other features you get with wazero is by trying +[examples](examples). ## Runtime diff --git a/config.go b/config.go index f2dacfb9d9..558208843d 100644 --- a/config.go +++ b/config.go @@ -5,12 +5,12 @@ import ( "errors" "fmt" "io" + "io/fs" "math" internalwasm "github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/internal/wasm/interpreter" "github.com/tetratelabs/wazero/internal/wasm/jit" - "github.com/tetratelabs/wazero/wasi" ) // NewRuntimeConfigJIT compiles WebAssembly modules into runtime.GOARCH-specific assembly for optimal performance. @@ -217,20 +217,20 @@ func (c *SysConfig) WithEnv(key, value string) *SysConfig { // WithFS assigns the file system to use for any paths beginning at "/". Defaults to not found. // // Note: This sets WithWorkDirFS to the same file-system unless already set. -func (c *SysConfig) WithFS(fs wasi.FS) *SysConfig { +func (c *SysConfig) WithFS(fs fs.FS) *SysConfig { c.setFS("/", fs) return c } -// WithWorkDirFS indicates the file system to use for any paths beginning at ".". Defaults to the same as WithFS. -func (c *SysConfig) WithWorkDirFS(fs wasi.FS) *SysConfig { +// WithWorkDirFS indicates the file system to use for any paths beginning at "./". Defaults to the same as WithFS. +func (c *SysConfig) WithWorkDirFS(fs fs.FS) *SysConfig { c.setFS(".", fs) return c } // withFS is hidden especially until #394 as existing use cases should be possible by composing file systems. // TODO: in #394 add examples on WithFS to accomplish this. -func (c *SysConfig) setFS(path string, fs wasi.FS) { +func (c *SysConfig) setFS(path string, fs fs.FS) { // Check to see if this key already exists and update it. entry := &internalwasm.FileEntry{Path: path, FS: fs} if fd, ok := c.preopenPaths[path]; ok { @@ -265,13 +265,13 @@ func (c *SysConfig) toSysContext() (sys *internalwasm.SysContext, err error) { rootFD := uint32(0) // zero is invalid setWorkDirFS := false preopens := c.preopens - for fd, fs := range preopens { - if fs.FS == nil { - err = fmt.Errorf("FS for %s is nil", fs.Path) + for fd, entry := range preopens { + if entry.FS == nil { + err = fmt.Errorf("FS for %s is nil", entry.Path) return - } else if fs.Path == "/" { + } else if entry.Path == "/" { rootFD = fd - } else if fs.Path == "." { + } else if entry.Path == "." { setWorkDirFS = true } } diff --git a/config_test.go b/config_test.go index 12c6cbc4ca..7bd7d628fe 100644 --- a/config_test.go +++ b/config_test.go @@ -4,6 +4,7 @@ import ( "io" "math" "testing" + "testing/fstest" "github.com/stretchr/testify/require" @@ -58,8 +59,8 @@ func TestRuntimeConfig_Features(t *testing.T) { } func TestSysConfig_toSysContext(t *testing.T) { - memFS := WASIMemFS() - memFS2 := WASIMemFS() + testFS := fstest.MapFS{} + testFS2 := fstest.MapFS{} tests := []struct { name string @@ -186,7 +187,7 @@ func TestSysConfig_toSysContext(t *testing.T) { }, { name: "WithFS", - input: NewSysConfig().WithFS(memFS), + input: NewSysConfig().WithFS(testFS), expected: requireSysContext(t, math.MaxUint32, // max nil, // args @@ -195,14 +196,14 @@ func TestSysConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr map[uint32]*internalwasm.FileEntry{ // openedFiles - 3: {Path: "/", FS: memFS}, - 4: {Path: ".", FS: memFS}, + 3: {Path: "/", FS: testFS}, + 4: {Path: ".", FS: testFS}, }, ), }, { name: "WithFS - overwrites", - input: NewSysConfig().WithFS(memFS).WithFS(memFS2), + input: NewSysConfig().WithFS(testFS).WithFS(testFS2), expected: requireSysContext(t, math.MaxUint32, // max nil, // args @@ -211,14 +212,14 @@ func TestSysConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr map[uint32]*internalwasm.FileEntry{ // openedFiles - 3: {Path: "/", FS: memFS2}, - 4: {Path: ".", FS: memFS2}, + 3: {Path: "/", FS: testFS2}, + 4: {Path: ".", FS: testFS2}, }, ), }, { name: "WithWorkDirFS", - input: NewSysConfig().WithWorkDirFS(memFS), + input: NewSysConfig().WithWorkDirFS(testFS), expected: requireSysContext(t, math.MaxUint32, // max nil, // args @@ -227,13 +228,13 @@ func TestSysConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr map[uint32]*internalwasm.FileEntry{ // openedFiles - 3: {Path: ".", FS: memFS}, + 3: {Path: ".", FS: testFS}, }, ), }, { name: "WithFS and WithWorkDirFS", - input: NewSysConfig().WithFS(memFS).WithWorkDirFS(memFS2), + input: NewSysConfig().WithFS(testFS).WithWorkDirFS(testFS2), expected: requireSysContext(t, math.MaxUint32, // max nil, // args @@ -242,14 +243,14 @@ func TestSysConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr map[uint32]*internalwasm.FileEntry{ // openedFiles - 3: {Path: "/", FS: memFS}, - 4: {Path: ".", FS: memFS2}, + 3: {Path: "/", FS: testFS}, + 4: {Path: ".", FS: testFS2}, }, ), }, { name: "WithWorkDirFS and WithFS", - input: NewSysConfig().WithWorkDirFS(memFS).WithFS(memFS2), + input: NewSysConfig().WithWorkDirFS(testFS).WithFS(testFS2), expected: requireSysContext(t, math.MaxUint32, // max nil, // args @@ -258,8 +259,8 @@ func TestSysConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr map[uint32]*internalwasm.FileEntry{ // openedFiles - 3: {Path: ".", FS: memFS}, - 4: {Path: "/", FS: memFS2}, + 3: {Path: ".", FS: testFS}, + 4: {Path: "/", FS: testFS2}, }, ), }, diff --git a/examples/file_system_test.go b/examples/file_system_test.go index bc30b842f8..1839815a6d 100644 --- a/examples/file_system_test.go +++ b/examples/file_system_test.go @@ -2,16 +2,20 @@ package examples import ( "bytes" + "embed" _ "embed" - "io" + "io/fs" "testing" "github.com/stretchr/testify/require" "github.com/tetratelabs/wazero" - "github.com/tetratelabs/wazero/wasi" ) +// catFS is an embedded filesystem limited to cat.go +//go:embed testdata/cat.go +var catFS embed.FS + // catGo is the TinyGo source //go:embed testdata/cat.go var catGo []byte @@ -31,16 +35,13 @@ func Test_Cat(t *testing.T) { stdoutBuf := bytes.NewBuffer(nil) sysConfig := wazero.NewSysConfig().WithStdout(stdoutBuf) - // Next, configure a sandboxed filesystem to include one file. - file := "cat.go" // arbitrary file - memFS := wazero.WASIMemFS() - err := writeFile(memFS, file, catGo) + // Since wazero uses fs.FS we can use standard libraries to do things like trim the leading path. + rooted, err := fs.Sub(catFS, "testdata") require.NoError(t, err) - sysConfig.WithWorkDirFS(memFS) // Since this runs a main function (_start in WASI), configure the arguments. // Remember, arg[0] is the program name! - sysConfig.WithArgs("cat", file) + sysConfig = sysConfig.WithFS(rooted).WithArgs("cat", "/cat.go") // Compile the `cat` module. compiled, err := r.CompileModule(catWasm) @@ -53,23 +54,9 @@ func Test_Cat(t *testing.T) { // StartWASICommand runs the "_start" function which is what TinyGo compiles "main" to. cat, err := wazero.StartWASICommandWithConfig(r, compiled, sysConfig) - require.NoError(t, err) defer cat.Close() // To ensure it worked, verify stdout from WebAssembly had what we expected. - require.Equal(t, string(catGo), stdoutBuf.String()) -} - -func writeFile(fs wasi.FS, path string, data []byte) error { - f, err := fs.OpenWASI(0, path, wasi.O_CREATE|wasi.O_TRUNC, wasi.R_FD_WRITE, 0, 0) - if err != nil { - return err - } - - if _, err := io.Copy(f, bytes.NewBuffer(data)); err != nil { - return err - } - - return f.Close() + require.Equal(t, catGo, stdoutBuf.Bytes()) } diff --git a/internal/wasi/fs.go b/internal/wasi/fs.go deleted file mode 100644 index 71e4d45d1d..0000000000 --- a/internal/wasi/fs.go +++ /dev/null @@ -1,111 +0,0 @@ -package internalwasi - -import ( - "fmt" - "io" - "io/fs" - "os" - "runtime" - "strings" - - "github.com/tetratelabs/wazero/wasi" -) - -type DirFS string - -func (dir DirFS) OpenWASI(dirFlags uint32, path string, oFlags uint32, fsRights, fsRightsInheriting uint64, fdFlags uint32) (wasi.File, error) { - // I'm not sure how to use all these passed flags and rights yet - if !fs.ValidPath(path) || runtime.GOOS == "windows" && strings.ContainsAny(path, `\:`) { - return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrInvalid} - } - mode := fs.FileMode(0644) - if oFlags&wasi.O_DIR != 0 { - mode |= fs.ModeDir - } - f, err := os.OpenFile(string(dir)+"/"+path, posixOpenFlags(oFlags, fsRights), mode) - if err != nil { - return nil, err - } - return f, nil -} - -type MemFS struct { - Files map[string][]byte -} - -func (m *MemFS) OpenWASI(dirFlags uint32, path string, oFlags uint32, fsRights, fsRightsInheriting uint64, fdFlags uint32) (wasi.File, error) { - if !fs.ValidPath(path) { - return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrInvalid} - } - - var buf []byte - if oFlags&wasi.O_CREATE == 0 { - bts, ok := m.Files[path] - if !ok { - return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist} - } - - if oFlags&wasi.O_TRUNC == 0 { - buf = append(buf, bts...) - } - } - - ret := &memFile{buf: buf} - - if fsRights&wasi.R_FD_WRITE != 0 { - ret.flush = func(bts []byte) { - m.Files[path] = bts - } - } - - return ret, nil -} - -// memFile implements wasi.File -type memFile struct { - buf []byte - offset int64 - flush func(bts []byte) -} - -// Read implements io.Reader -func (f *memFile) Read(p []byte) (int, error) { - // In memFile, the end of the buffer is the end of the file. - if f.offset == int64(len(f.buf)) { - return 0, io.EOF - } - nread := copy(p, f.buf[f.offset:]) - f.offset += int64(nread) - return nread, nil -} - -// Write implements io.Writer -func (f *memFile) Write(p []byte) (int, error) { - nwritten := copy(f.buf[f.offset:], p) - f.buf = append(f.buf, p[nwritten:]...) - f.offset += int64(len(p)) - return len(p), nil -} - -// Seek implements io.Seeker -func (f *memFile) Seek(offset int64, whence int) (int64, error) { - switch whence { - case io.SeekStart: - f.offset = offset - case io.SeekCurrent: - f.offset += offset - case io.SeekEnd: - f.offset = int64(len(f.buf)) + offset - default: - return 0, fmt.Errorf("invalid whence: %d", whence) - } - return f.offset, nil -} - -// Close implements io.Closer -func (f *memFile) Close() error { - if f.flush != nil { - f.flush(f.buf) - } - return nil -} diff --git a/internal/wasi/fs_test.go b/internal/wasi/fs_test.go deleted file mode 100644 index 53faa887a3..0000000000 --- a/internal/wasi/fs_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package internalwasi - -import ( - "testing" - "testing/iotest" - - "github.com/stretchr/testify/require" -) - -// TestMemFile_Read_Seek tests the behavior of Read and Seek by iotest.TestReader. -// See iotest.TestReader -func TestMemFile_Read_Seek(t *testing.T) { - expectedFileContent := []byte("wazero") // arbitrary contents - memFile := &memFile{ - buf: expectedFileContent, - } - // TestReader tests that io.Reader correctly reads the expected contents. - // It also tests io.Seeker when it's implemented, which memFile does. - err := iotest.TestReader(memFile, expectedFileContent) - require.NoError(t, err) -} diff --git a/internal/wasi/wasi.go b/internal/wasi/wasi.go index b21f00bec1..2b2bbfb664 100644 --- a/internal/wasi/wasi.go +++ b/internal/wasi/wasi.go @@ -6,7 +6,7 @@ import ( "fmt" "io" "io/fs" - "os" + "path" "time" internalwasm "github.com/tetratelabs/wazero/internal/wasm" @@ -1100,16 +1100,12 @@ func (a *wasiAPI) FdDatasync(ctx wasm.Module, fd uint32) wasi.Errno { } // FdFdstatGet implements SnapshotPreview1.FdFdstatGet -// TODO: Currently FdFdstatget implements nothing except returning fake fs_right_inheriting func (a *wasiAPI) FdFdstatGet(ctx wasm.Module, fd uint32, resultStat uint32) wasi.Errno { sys := sysContext(ctx) if _, ok := sys.OpenedFile(fd); !ok { return wasi.ErrnoBadf } - if !ctx.Memory().WriteUint64Le(resultStat+16, rightFDRead|rightFDWrite) { - return wasi.ErrnoFault - } return wasi.ErrnoSuccess } @@ -1118,7 +1114,7 @@ func (a *wasiAPI) FdPrestatGet(ctx wasm.Module, fd uint32, resultPrestat uint32) sys := sysContext(ctx) entry, ok := sys.OpenedFile(fd) - if !ok || entry.Path == "" { + if !ok { return wasi.ErrnoBadf } @@ -1140,6 +1136,7 @@ func (a *wasiAPI) FdFdstatSetFlags(ctx wasm.Module, fd uint32, flags uint32) was } // FdFdstatSetRights implements SnapshotPreview1.FdFdstatSetRights +// Note: This will never be implemented per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844 func (a *wasiAPI) FdFdstatSetRights(ctx wasm.Module, fd uint32, fsRightsBase, fsRightsInheriting uint64) wasi.Errno { return wasi.ErrnoNosys // stubbed for GrainLang per #271 } @@ -1196,13 +1193,11 @@ func (a *wasiAPI) FdRead(ctx wasm.Module, fd, iovs, iovsCount, resultSize uint32 var reader io.Reader - if fd == 0 { + if fd == fdStdin { reader = sys.Stdin() + } else if f, ok := sys.OpenedFile(fd); !ok || f.File == nil { + return wasi.ErrnoBadf } else { - f, ok := sys.OpenedFile(fd) - if !ok || f.File == nil { - return wasi.ErrnoBadf - } reader = f.File } @@ -1249,24 +1244,24 @@ func (a *wasiAPI) FdRenumber(ctx wasm.Module, fd, to uint32) wasi.Errno { func (a *wasiAPI) FdSeek(ctx wasm.Module, fd uint32, offset uint64, whence uint32, resultNewoffset uint32) wasi.Errno { sys := sysContext(ctx) - f, ok := sys.OpenedFile(fd) - if !ok || f.File == nil { + var seeker io.Seeker + // Check to see if the file descriptor is available + if f, ok := sys.OpenedFile(fd); !ok || f.File == nil { return wasi.ErrnoBadf - } - seeker, ok := f.File.(io.Seeker) - if !ok { + // fs.FS doesn't declare io.Seeker, but implementations such as os.File implement it. + } else if seeker, ok = f.File.(io.Seeker); !ok { return wasi.ErrnoBadf } if whence > io.SeekEnd /* exceeds the largest valid whence */ { return wasi.ErrnoInval } - newOffst, err := seeker.Seek(int64(offset), int(whence)) + newOffset, err := seeker.Seek(int64(offset), int(whence)) if err != nil { return wasi.ErrnoIo } - if !ctx.Memory().WriteUint32Le(resultNewoffset, uint32(newOffst)) { + if !ctx.Memory().WriteUint32Le(resultNewoffset, uint32(newOffset)) { return wasi.ErrnoFault } @@ -1290,16 +1285,18 @@ func (a *wasiAPI) FdWrite(ctx wasm.Module, fd, iovs, iovsCount, resultSize uint3 var writer io.Writer switch fd { - case 1: + case fdStdout: writer = sys.Stdout() - case 2: + case fdStderr: writer = sys.Stderr() default: - f, ok := sys.OpenedFile(fd) - if !ok || f.File == nil { + // Check to see if the file descriptor is available + if f, ok := sys.OpenedFile(fd); !ok || f.File == nil { + return wasi.ErrnoBadf + // fs.FS doesn't declare io.Writer, but implementations such as os.File implement it. + } else if writer, ok = f.File.(io.Writer); !ok { return wasi.ErrnoBadf } - writer = f.File } var nwritten uint32 @@ -1349,41 +1346,8 @@ func (a *wasiAPI) PathLink(ctx wasm.Module, oldFd, oldFlags, oldPath, oldPathLen return wasi.ErrnoNosys // stubbed for GrainLang per #271 } -const ( - // WASI open flags - oflagCreate = 1 << iota - // TODO: oflagDir - oflagExclusive - oflagTrunc - - // WASI FS rights - rightFDRead = 1 << iota - rightFDWrite = 0x200 -) - -func posixOpenFlags(oFlags uint32, fsRights uint64) (pFlags int) { - // TODO: handle dirflags, which decides whether to follow symbolic links or not, - // by O_NOFOLLOW. Note O_NOFOLLOW doesn't exist on Windows. - if fsRights&rightFDWrite != 0 { - if fsRights&rightFDRead != 0 { - pFlags |= os.O_RDWR - } else { - pFlags |= os.O_WRONLY - } - } - if oFlags&oflagCreate != 0 { - pFlags |= os.O_CREATE - } - if oFlags&oflagExclusive != 0 { - pFlags |= os.O_EXCL - } - if oFlags&oflagTrunc != 0 { - pFlags |= os.O_TRUNC - } - return -} - // PathOpen implements SnapshotPreview1.PathOpen +// Note: Rights will never be implemented per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844 func (a *wasiAPI) PathOpen(ctx wasm.Module, fd, dirflags, pathPtr, pathLen, oflags uint32, fsRightsBase, fsRightsInheriting uint64, fdflags, resultOpenedFd uint32) (errno wasi.Errno) { sys := sysContext(ctx) @@ -1397,25 +1361,20 @@ func (a *wasiAPI) PathOpen(ctx wasm.Module, fd, dirflags, pathPtr, pathLen, ofla if !ok { return wasi.ErrnoFault } - pathName := string(b) - f, err := dir.FS.OpenWASI(dirflags, pathName, oflags, fsRightsBase, fsRightsInheriting, fdflags) - if err != nil { - switch { - case errors.Is(err, fs.ErrNotExist): - return wasi.ErrnoNoent - case errors.Is(err, fs.ErrExist): - return wasi.ErrnoExist - default: - return wasi.ErrnoIo - } - } - // when ofagDir is set, the opened file must be a directory. - // TODO if oflags&oflagDir != 0 return wasi.ErrnoNotdir if stat != dir + // TODO: Consider dirflags and oflags. Also, allow non-read-only open based on config about the mount. + // Ex. allow os.O_RDONLY, os.O_WRONLY, or os.O_RDWR either by config flag or pattern on filename + // See #390 + entry, errno := openFileEntry(dir.FS, path.Join(dir.Path, string(b))) + if errno != wasi.ErrnoSuccess { + return errno + } - if newFD, ok := sys.OpenFile(&internalwasm.FileEntry{Path: pathName, File: f, FS: dir.FS}); !ok { + if newFD, ok := sys.OpenFile(entry); !ok { + _ = entry.File.Close() return wasi.ErrnoIo } else if !ctx.Memory().WriteUint32Le(resultOpenedFd, newFD) { + _ = entry.File.Close() return wasi.ErrnoFault } return wasi.ErrnoSuccess @@ -1499,6 +1458,12 @@ func (a *wasiAPI) SockShutdown(ctx wasm.Module, fd, how uint32) wasi.Errno { return wasi.ErrnoNosys // stubbed for GrainLang per #271 } +const ( + fdStdin = 0 + fdStdout = 1 + fdStderr = 2 +) + // NewAPI is exported for benchmarks func NewAPI() *wasiAPI { return &wasiAPI{ @@ -1520,6 +1485,25 @@ func sysContext(ctx wasm.Module) *internalwasm.SysContext { } } +func openFileEntry(rootFS fs.FS, pathName string) (*internalwasm.FileEntry, wasi.Errno) { + f, err := rootFS.Open(pathName) + if err != nil { + switch { + case errors.Is(err, fs.ErrNotExist): + return nil, wasi.ErrnoNoent + case errors.Is(err, fs.ErrExist): + return nil, wasi.ErrnoExist + default: + return nil, wasi.ErrnoIo + } + } + + // TODO: verify if oflags is a directory and fail with wasi.ErrnoNotdir if not + // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-oflags-flagsu16 + + return &internalwasm.FileEntry{Path: pathName, FS: rootFS, File: f}, wasi.ErrnoSuccess +} + func ValidateWASICommand(module *internalwasm.Module, moduleName string) error { if start, err := requireExport(module, moduleName, FunctionStart, internalwasm.ExternTypeFunc); err != nil { return err diff --git a/internal/wasi/wasi_test.go b/internal/wasi/wasi_test.go index 41df2b8a90..3cffc5846b 100644 --- a/internal/wasi/wasi_test.go +++ b/internal/wasi/wasi_test.go @@ -7,9 +7,13 @@ import ( "errors" "fmt" "io" + "io/fs" "math" "math/rand" + "os" + "path" "testing" + "testing/fstest" "github.com/stretchr/testify/require" @@ -46,7 +50,7 @@ func TestSnapshotPreview1_ArgsGet(t *testing.T) { // Invoke ArgsGet directly and check the memory side effects. errno := a.ArgsGet(mod, argv, argvBuf) - require.Equal(t, wasi.ErrnoSuccess, errno) + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) @@ -58,7 +62,8 @@ func TestSnapshotPreview1_ArgsGet(t *testing.T) { results, err := fn.Call(mod, uint64(argv), uint64(argvBuf)) require.NoError(t, err) - require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) @@ -112,7 +117,7 @@ func TestSnapshotPreview1_ArgsGet_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { errno := a.ArgsGet(mod, tc.argv, tc.argvBuf) require.NoError(t, err) - require.Equal(t, wasi.ErrnoFault, errno) + require.Equal(t, wasi.ErrnoFault, errno, wasi.ErrnoName(errno)) }) } } @@ -140,7 +145,7 @@ func TestSnapshotPreview1_ArgsSizesGet(t *testing.T) { // Invoke ArgsSizesGet directly and check the memory side effects. errno := a.ArgsSizesGet(mod, resultArgc, resultArgvBufSize) - require.Equal(t, wasi.ErrnoSuccess, errno) + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) @@ -152,7 +157,8 @@ func TestSnapshotPreview1_ArgsSizesGet(t *testing.T) { results, err := fn.Call(mod, uint64(resultArgc), uint64(resultArgvBufSize)) require.NoError(t, err) - require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) @@ -203,8 +209,7 @@ func TestSnapshotPreview1_ArgsSizesGet_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { errno := a.ArgsSizesGet(mod, tc.argc, tc.argvBufSize) - require.NoError(t, err) - require.Equal(t, wasi.ErrnoFault, errno) + require.Equal(t, wasi.ErrnoFault, errno, wasi.ErrnoName(errno)) }) } } @@ -234,7 +239,7 @@ func TestSnapshotPreview1_EnvironGet(t *testing.T) { // Invoke EnvironGet directly and check the memory side effects. errno := a.EnvironGet(mod, resultEnviron, resultEnvironBuf) - require.Equal(t, wasi.ErrnoSuccess, errno) + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) @@ -246,7 +251,8 @@ func TestSnapshotPreview1_EnvironGet(t *testing.T) { results, err := fn.Call(mod, uint64(resultEnviron), uint64(resultEnvironBuf)) require.NoError(t, err) - require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) @@ -299,8 +305,7 @@ func TestSnapshotPreview1_EnvironGet_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { errno := a.EnvironGet(mod, tc.environ, tc.environBuf) - require.NoError(t, err) - require.Equal(t, wasi.ErrnoFault, errno) + require.Equal(t, wasi.ErrnoFault, errno, wasi.ErrnoName(errno)) }) } } @@ -328,7 +333,7 @@ func TestSnapshotPreview1_EnvironSizesGet(t *testing.T) { // Invoke EnvironSizesGet directly and check the memory side effects. errno := a.EnvironSizesGet(mod, resultEnvironc, resultEnvironBufSize) - require.Equal(t, wasi.ErrnoSuccess, errno) + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) @@ -340,7 +345,8 @@ func TestSnapshotPreview1_EnvironSizesGet(t *testing.T) { results, err := fn.Call(mod, uint64(resultEnvironc), uint64(resultEnvironBufSize)) require.NoError(t, err) - require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) @@ -391,8 +397,7 @@ func TestSnapshotPreview1_EnvironSizesGet_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { errno := a.EnvironSizesGet(mod, tc.environc, tc.environBufSize) - require.NoError(t, err) - require.Equal(t, wasi.ErrnoFault, errno) + require.Equal(t, wasi.ErrnoFault, errno, wasi.ErrnoName(errno)) }) } } @@ -413,7 +418,8 @@ func TestSnapshotPreview1_ClockResGet(t *testing.T) { t.Run(FunctionClockResGet, func(t *testing.T) { results, err := fn.Call(mod, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -440,7 +446,7 @@ func TestSnapshotPreview1_ClockTimeGet(t *testing.T) { // invoke ClockTimeGet directly and check the memory side effects! errno := a.ClockTimeGet(mod, 0 /* TODO: id */, 0 /* TODO: precision */, resultTimestamp) - require.Equal(t, wasi.ErrnoSuccess, errno) + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) @@ -452,7 +458,8 @@ func TestSnapshotPreview1_ClockTimeGet(t *testing.T) { results, err := fn.Call(mod, 0 /* TODO: id */, 0 /* TODO: precision */, uint64(resultTimestamp)) require.NoError(t, err) - require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) @@ -496,7 +503,8 @@ func TestSnapshotPreview1_ClockTimeGet_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { results, err := fn.Call(mod, 0 /* TODO: id */, 0 /* TODO: precision */, uint64(tc.resultTimestamp)) require.NoError(t, err) - require.Equal(t, uint64(wasi.ErrnoFault), results[0]) // results[0] is the errno + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoFault, errno, wasi.ErrnoName(errno)) }) } } @@ -511,13 +519,15 @@ func TestSnapshotPreview1_FdAdvise(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.FdAdvise", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.FdAdvise(mod, 0, 0, 0, 0)) + errno := a.FdAdvise(mod, 0, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionFdAdvise, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -531,13 +541,15 @@ func TestSnapshotPreview1_FdAllocate(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.FdAllocate", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.FdAllocate(mod, 0, 0, 0)) + errno := a.FdAllocate(mod, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionFdAllocate, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -547,16 +559,18 @@ func TestSnapshotPreview1_FdClose(t *testing.T) { setupFD := func() (publicwasm.Module, publicwasm.Function, *wasiAPI) { ctx := context.Background() - memFs := &MemFS{} + + // fd_close needs to close an open file descriptor. Open two files so that we can tell which is closed. + path1, path2 := "a", "b" + testFs := fstest.MapFS{path1: {Data: make([]byte, 0)}, path2: {Data: make([]byte, 0)}} + entry1, errno := openFileEntry(testFs, path1) + require.Zero(t, errno, wasi.ErrnoName(errno)) + entry2, errno := openFileEntry(testFs, path2) + require.Zero(t, errno, wasi.ErrnoName(errno)) + sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ - fdToClose: { - Path: "/tmp", - FS: memFs, - }, - fdToKeep: { - Path: "path to keep", - FS: memFs, - }, + fdToClose: entry1, + fdToKeep: entry2, }) require.NoError(t, err) @@ -579,7 +593,7 @@ func TestSnapshotPreview1_FdClose(t *testing.T) { defer mod.Close() errno := api.FdClose(mod, fdToClose) - require.Equal(t, wasi.ErrnoSuccess, errno) + require.Zero(t, errno, wasi.ErrnoName(errno)) verify(mod) }) @@ -587,9 +601,10 @@ func TestSnapshotPreview1_FdClose(t *testing.T) { mod, fn, _ := setupFD() defer mod.Close() - ret, err := fn.Call(mod, uint64(fdToClose)) + results, err := fn.Call(mod, uint64(fdToClose)) require.NoError(t, err) - require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(ret[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Zero(t, errno, wasi.ErrnoName(errno)) verify(mod) }) @@ -612,13 +627,15 @@ func TestSnapshotPreview1_FdDatasync(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.FdDatasync", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.FdDatasync(mod, 0)) + errno := a.FdDatasync(mod, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionFdDatasync, func(t *testing.T) { results, err := fn.Call(mod, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -634,13 +651,15 @@ func TestSnapshotPreview1_FdFdstatSetFlags(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.FdFdstatSetFlags", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.FdFdstatSetFlags(mod, 0, 0)) + errno := a.FdFdstatSetFlags(mod, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionFdFdstatSetFlags, func(t *testing.T) { results, err := fn.Call(mod, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -654,13 +673,15 @@ func TestSnapshotPreview1_FdFdstatSetRights(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.FdFdstatSetRights", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.FdFdstatSetRights(mod, 0, 0, 0)) + errno := a.FdFdstatSetRights(mod, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionFdFdstatSetRights, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -674,13 +695,15 @@ func TestSnapshotPreview1_FdFilestatGet(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.FdFilestatGet", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.FdFilestatGet(mod, 0, 0)) + errno := a.FdFilestatGet(mod, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionFdFilestatGet, func(t *testing.T) { results, err := fn.Call(mod, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -694,13 +717,15 @@ func TestSnapshotPreview1_FdFilestatSetSize(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.FdFilestatSetSize", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.FdFilestatSetSize(mod, 0, 0)) + errno := a.FdFilestatSetSize(mod, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionFdFilestatSetSize, func(t *testing.T) { results, err := fn.Call(mod, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -714,13 +739,15 @@ func TestSnapshotPreview1_FdFilestatSetTimes(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.FdFilestatSetTimes", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.FdFilestatSetTimes(mod, 0, 0, 0, 0)) + errno := a.FdFilestatSetTimes(mod, 0, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionFdFilestatSetTimes, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -734,13 +761,15 @@ func TestSnapshotPreview1_FdPread(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.FdPread", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.FdPread(mod, 0, 0, 0, 0, 0)) + errno := a.FdPread(mod, 0, 0, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionFdPread, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -748,7 +777,8 @@ func TestSnapshotPreview1_FdPrestatGet(t *testing.T) { fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err ctx := context.Background() - sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{fd: {Path: "/tmp"}}) + pathName := "/tmp" + sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{fd: {Path: pathName}}) require.NoError(t, err) a, mod, fn := instantiateModule(t, ctx, FunctionFdPrestatGet, ImportFdPrestatGet, moduleName, sys) @@ -756,11 +786,11 @@ func TestSnapshotPreview1_FdPrestatGet(t *testing.T) { resultPrestat := uint32(1) // arbitrary offset expectedMemory := []byte{ - '?', // resultPrstat after this + '?', // resultPrestat after this 0, // 8-bit tag indicating `prestat_dir`, the only available tag 0, 0, 0, // 3-byte padding // the result path length field after this - 4, 0, 0, 0, // = 4, which is len("/tmp") + byte(len(pathName)), 0, 0, 0, // = in little endian encoding '?', } @@ -768,7 +798,7 @@ func TestSnapshotPreview1_FdPrestatGet(t *testing.T) { maskMemory(t, mod, len(expectedMemory)) errno := a.FdPrestatGet(mod, fd, resultPrestat) - require.Equal(t, wasi.ErrnoSuccess, errno) + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) @@ -778,9 +808,10 @@ func TestSnapshotPreview1_FdPrestatGet(t *testing.T) { t.Run(FunctionFdPrestatDirName, func(t *testing.T) { maskMemory(t, mod, len(expectedMemory)) - ret, err := fn.Call(mod, uint64(fd), uint64(resultPrestat)) + results, err := fn.Call(mod, uint64(fd), uint64(resultPrestat)) require.NoError(t, err) - require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(ret[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) @@ -827,7 +858,7 @@ func TestSnapshotPreview1_FdPrestatGet_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { errno := a.FdPrestatGet(mod, tc.fd, tc.resultPrestat) - require.Equal(t, tc.expectedErrno, errno) + require.Equal(t, tc.expectedErrno, errno, wasi.ErrnoName(errno)) }) } } @@ -854,7 +885,7 @@ func TestSnapshotPreview1_FdPrestatDirName(t *testing.T) { maskMemory(t, mod, len(expectedMemory)) errno := a.FdPrestatDirName(mod, fd, path, pathLen) - require.Equal(t, wasi.ErrnoSuccess, errno) + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) @@ -864,9 +895,10 @@ func TestSnapshotPreview1_FdPrestatDirName(t *testing.T) { t.Run(FunctionFdPrestatDirName, func(t *testing.T) { maskMemory(t, mod, len(expectedMemory)) - ret, err := fn.Call(mod, uint64(fd), uint64(path), uint64(pathLen)) + results, err := fn.Call(mod, uint64(fd), uint64(path), uint64(pathLen)) require.NoError(t, err) - require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(ret[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) @@ -931,7 +963,7 @@ func TestSnapshotPreview1_FdPrestatDirName_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { errno := a.FdPrestatDirName(mod, tc.fd, tc.path, tc.pathLen) - require.Equal(t, tc.expectedErrno, errno) + require.Equal(t, tc.expectedErrno, errno, wasi.ErrnoName(errno)) }) } } @@ -946,13 +978,15 @@ func TestSnapshotPreview1_FdPwrite(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.FdPwrite", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.FdPwrite(mod, 0, 0, 0, 0, 0)) + errno := a.FdPwrite(mod, 0, 0, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionFdPwrite, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -992,9 +1026,9 @@ func TestSnapshotPreview1_FdRead(t *testing.T) { }}, {FunctionFdRead, func(_ *wasiAPI, mod *wasm.ModuleContext, fn publicwasm.Function) fdReadFn { return func(ctx publicwasm.Module, fd, iovs, iovsCount, resultSize uint32) wasi.Errno { - ret, err := fn.Call(mod, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultSize)) + results, err := fn.Call(mod, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultSize)) require.NoError(t, err) - return wasi.Errno(ret[0]) + return wasi.Errno(results[0]) } }}, } @@ -1003,9 +1037,9 @@ func TestSnapshotPreview1_FdRead(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { // Create a fresh file to read the contents from - file, memFS := createFile(t, "test_path", []byte("wazero")) + file, testFS := createFile(t, "test_path", []byte("wazero")) sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ - fd: {Path: "test_path", FS: memFS, File: file}, + fd: {Path: "test_path", FS: testFS, File: file}, }) require.NoError(t, err) @@ -1018,7 +1052,7 @@ func TestSnapshotPreview1_FdRead(t *testing.T) { require.True(t, ok) errno := tc.fdRead(a, mod, fn)(mod, fd, iovs, iovsCount, resultSize) - require.Equal(t, wasi.ErrnoSuccess, errno) + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) @@ -1028,12 +1062,12 @@ func TestSnapshotPreview1_FdRead(t *testing.T) { } func TestSnapshotPreview1_FdRead_Errors(t *testing.T) { - validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err - file, memFS := createFile(t, "test_path", []byte{}) // file with empty contents + validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err + file, testFS := createFile(t, "test_path", []byte{}) // file with empty contents ctx := context.Background() sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ - validFD: {Path: "test_path", FS: memFS, File: file}, + validFD: {Path: "test_path", FS: testFS, File: file}, }) require.NoError(t, err) @@ -1115,7 +1149,7 @@ func TestSnapshotPreview1_FdRead_Errors(t *testing.T) { require.True(t, memoryWriteOK) errno := a.FdRead(mod, tc.fd, tc.iovs+offset, tc.iovsCount+offset, tc.resultSize+offset) - require.Equal(t, tc.expectedErrno, errno) + require.Equal(t, tc.expectedErrno, errno, wasi.ErrnoName(errno)) }) } } @@ -1130,13 +1164,15 @@ func TestSnapshotPreview1_FdReaddir(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.FdReaddir", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.FdReaddir(mod, 0, 0, 0, 0, 0)) + errno := a.FdReaddir(mod, 0, 0, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionFdReaddir, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -1150,24 +1186,26 @@ func TestSnapshotPreview1_FdRenumber(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.FdRenumber", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.FdRenumber(mod, 0, 0)) + errno := a.FdRenumber(mod, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionFdRenumber, func(t *testing.T) { results, err := fn.Call(mod, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } func TestSnapshotPreview1_FdSeek(t *testing.T) { - fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err - resultNewoffset := uint32(1) // arbitrary offset in `ctx.Memory` for the new offset value - file, memFS := createFile(t, "test_path", []byte("wazero")) // arbitrary non-empty contents + fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err + resultNewoffset := uint32(1) // arbitrary offset in `ctx.Memory` for the new offset value + file, testFS := createFile(t, "test_path", []byte("wazero")) // arbitrary non-empty contents ctx := context.Background() sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ - fd: {Path: "test_path", FS: memFS, File: file}, + fd: {Path: "test_path", FS: testFS, File: file}, }) require.NoError(t, err) @@ -1185,9 +1223,9 @@ func TestSnapshotPreview1_FdSeek(t *testing.T) { }}, {FunctionFdSeek, func() fdSeekFn { return func(ctx publicwasm.Module, fd uint32, offset uint64, whence, resultNewoffset uint32) wasi.Errno { - ret, err := fn.Call(mod, uint64(fd), offset, uint64(whence), uint64(resultNewoffset)) + results, err := fn.Call(mod, uint64(fd), offset, uint64(whence), uint64(resultNewoffset)) require.NoError(t, err) - return wasi.Errno(ret[0]) + return wasi.Errno(results[0]) } }}, } @@ -1241,16 +1279,27 @@ func TestSnapshotPreview1_FdSeek(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { maskMemory(t, mod, len(tc.expectedMemory)) - file.offset = 1 // set the initial offset of the file to 1 + + // Since we initialized this file, we know it is a seeker (because it is a MapFile) + f, ok := sys.OpenedFile(fd) + require.True(t, ok) + seeker := f.File.(io.Seeker) + + // set the initial offset of the file to 1 + offset, err := seeker.Seek(1, io.SeekStart) + require.NoError(t, err) + require.Equal(t, int64(1), offset) errno := sf.fdSeek()(mod, fd, uint64(tc.offset), uint32(tc.whence), resultNewoffset) - require.Equal(t, wasi.ErrnoSuccess, errno) + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, uint32(len(tc.expectedMemory))) require.True(t, ok) require.Equal(t, tc.expectedMemory, actual) - require.Equal(t, tc.expectedOffset, file.offset) // test that the offset of file is actually updated. + offset, err = seeker.Seek(0, io.SeekCurrent) + require.NoError(t, err) + require.Equal(t, tc.expectedOffset, offset) // test that the offset of file is actually updated. }) } }) @@ -1258,11 +1307,11 @@ func TestSnapshotPreview1_FdSeek(t *testing.T) { } func TestSnapshotPreview1_FdSeek_Errors(t *testing.T) { - validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err - file, memFS := createFile(t, "test_path", []byte("wazero")) // arbitrary valid file with non-empty contents + validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err + file, testFS := createFile(t, "test_path", []byte("wazero")) // arbitrary valid file with non-empty contents ctx := context.Background() sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ - validFD: {Path: "test_path", FS: memFS, File: file}, + validFD: {Path: "test_path", FS: testFS, File: file}, }) require.NoError(t, err) @@ -1301,7 +1350,7 @@ func TestSnapshotPreview1_FdSeek_Errors(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { errno := a.FdSeek(mod, tc.fd, tc.offset, tc.whence, tc.resultNewoffset) - require.Equal(t, tc.expectedErrno, errno) + require.Equal(t, tc.expectedErrno, errno, wasi.ErrnoName(errno)) }) } @@ -1317,13 +1366,15 @@ func TestSnapshotPreview1_FdSync(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.FdSync", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.FdSync(mod, 0)) + errno := a.FdSync(mod, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionFdSync, func(t *testing.T) { results, err := fn.Call(mod, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -1337,13 +1388,15 @@ func TestSnapshotPreview1_FdTell(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.FdTell", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.FdTell(mod, 0, 0)) + errno := a.FdTell(mod, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionFdTell, func(t *testing.T) { results, err := fn.Call(mod, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -1383,20 +1436,23 @@ func TestSnapshotPreview1_FdWrite(t *testing.T) { }}, {FunctionFdWrite, func(_ *wasiAPI, mod *wasm.ModuleContext, fn publicwasm.Function) fdWriteFn { return func(ctx publicwasm.Module, fd, iovs, iovsCount, resultSize uint32) wasi.Errno { - ret, err := fn.Call(mod, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultSize)) + results, err := fn.Call(mod, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultSize)) require.NoError(t, err) - return wasi.Errno(ret[0]) + return wasi.Errno(results[0]) } }}, } + tmpDir := t.TempDir() // open before loop to ensure no locking problems. + for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { // Create a fresh file to write the contents to - file, memFS := createFile(t, "test_path", []byte{}) + pathName := "test_path" + file, testFS := createWriteableFile(t, tmpDir, pathName, []byte{}) sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ - fd: {Path: "test_path", FS: memFS, File: file}, + fd: {Path: pathName, FS: testFS, File: file}, }) require.NoError(t, err) @@ -1408,22 +1464,31 @@ func TestSnapshotPreview1_FdWrite(t *testing.T) { require.True(t, ok) errno := tc.fdWrite(a, mod, fn)(mod, fd, iovs, iovsCount, resultSize) - require.Equal(t, wasi.ErrnoSuccess, errno) + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) require.Equal(t, expectedMemory, actual) - require.Equal(t, []byte("wazero"), file.buf) // verify the file was actually written + + // Since we initialized this file, we know we can read it by path + buf, err := os.ReadFile(path.Join(tmpDir, pathName)) + require.NoError(t, err) + + require.Equal(t, []byte("wazero"), buf) // verify the file was actually written }) } } func TestSnapshotPreview1_FdWrite_Errors(t *testing.T) { - validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err - file, memFS := createFile(t, "test_path", []byte{}) // file with empty contents + validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err + + tmpDir := t.TempDir() // open before loop to ensure no locking problems. + pathName := "test_path" + file, testFS := createWriteableFile(t, tmpDir, pathName, []byte{}) + ctx := context.Background() sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ - validFD: {Path: "test_path", FS: memFS, File: file}, + validFD: {Path: pathName, FS: testFS, File: file}, }) require.NoError(t, err) @@ -1488,22 +1553,11 @@ func TestSnapshotPreview1_FdWrite_Errors(t *testing.T) { mod.Memory().(*wasm.MemoryInstance).Buffer = tc.memory errno := a.FdWrite(mod, tc.fd, iovs, iovsCount, tc.resultSize) - require.Equal(t, tc.expectedErrno, errno) + require.Equal(t, tc.expectedErrno, errno, wasi.ErrnoName(errno)) }) } } -func createFile(t *testing.T, path string, contents []byte) (*memFile, *MemFS) { - memFS := &MemFS{Files: map[string][]byte{}} - f, err := memFS.OpenWASI(0, path, wasi.O_CREATE|wasi.O_TRUNC, wasi.R_FD_WRITE, 0, 0) - require.NoError(t, err) - - mf := f.(*memFile) - mf.buf = append([]byte{}, contents...) - - return mf, memFS -} - // TestSnapshotPreview1_PathCreateDirectory only tests it is stubbed for GrainLang per #271 func TestSnapshotPreview1_PathCreateDirectory(t *testing.T) { ctx := context.Background() @@ -1514,13 +1568,15 @@ func TestSnapshotPreview1_PathCreateDirectory(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.PathCreateDirectory", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.PathCreateDirectory(mod, 0, 0, 0)) + errno := a.PathCreateDirectory(mod, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionPathCreateDirectory, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -1534,13 +1590,15 @@ func TestSnapshotPreview1_PathFilestatGet(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.PathFilestatGet", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.PathFilestatGet(mod, 0, 0, 0, 0, 0)) + errno := a.PathFilestatGet(mod, 0, 0, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionPathFilestatGet, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -1554,13 +1612,15 @@ func TestSnapshotPreview1_PathFilestatSetTimes(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.PathFilestatSetTimes", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.PathFilestatSetTimes(mod, 0, 0, 0, 0, 0, 0, 0)) + errno := a.PathFilestatSetTimes(mod, 0, 0, 0, 0, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionPathFilestatSetTimes, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0, 0, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -1574,31 +1634,32 @@ func TestSnapshotPreview1_PathLink(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.PathLink", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.PathLink(mod, 0, 0, 0, 0, 0, 0, 0)) + errno := a.PathLink(mod, 0, 0, 0, 0, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionPathLink, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0, 0, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } func TestSnapshotPreview1_PathOpen(t *testing.T) { - workdirFD := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err - dirflags := uint32(0) // arbitrary dirflags - path := uint32(1) // arbitrary offset - pathLen := uint32(6) // The length of path - oflags := uint32(0) // arbitrary oflags - fsRightsBase := uint64(rightFDRead) // arbitrary right - fsRightsInheriting := uint64(rightFDRead) // arbitrary right + workdirFD := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err + dirflags := uint32(0) // arbitrary dirflags + oflags := uint32(0) // arbitrary oflags fdFlags := uint32(0) - resultOpenedFd := uint32(8) - initialMemory := []byte{ - '?', // `path` is after this - 'w', 'a', 'z', 'e', 'r', 'o', // path - } + + // Setup the initial memory to include the path name starting at an offset. + pathName := "wazero" + path := uint32(1) + pathLen := uint32(len(pathName)) + initialMemory := append([]byte{'?'}, pathName...) + expectedFD := byte(workdirFD + 1) + resultOpenedFd := uint32(len(initialMemory) + 1) expectedMemory := append( initialMemory, '?', // `resultOpenedFd` is after this @@ -1608,88 +1669,58 @@ func TestSnapshotPreview1_PathOpen(t *testing.T) { ctx := context.Background() - // TestSnapshotPreview1_PathOpen uses a matrix because setting up test files is complicated and has to be clean each time. - type pathOpenFn func(ctx publicwasm.Module, fd, dirflags, path, pathLen, oflags uint32, - fsRightsBase, fsRightsInheriting uint64, - fdFlags, resultOpenedFd uint32) wasi.Errno - pathOpenFns := []struct { - name string - pathOpen func(*wasiAPI, *wasm.ModuleContext, publicwasm.Function) pathOpenFn - }{ - {"SnapshotPreview1.PathOpen", func(a *wasiAPI, _ *wasm.ModuleContext, _ publicwasm.Function) pathOpenFn { - return a.PathOpen - }}, - {FunctionPathOpen, func(_ *wasiAPI, mod *wasm.ModuleContext, fn publicwasm.Function) pathOpenFn { - return func(ctx publicwasm.Module, fd, dirflags, path, pathLen, oflags uint32, - fsRightsBase, fsRightsInheriting uint64, - fdFlags, resultOpenedFd uint32) wasi.Errno { - ret, err := fn.Call(mod, uint64(fd), uint64(dirflags), uint64(path), uint64(pathLen), uint64(oflags), uint64(fsRightsBase), uint64(fsRightsInheriting), uint64(fdFlags), uint64(resultOpenedFd)) - require.NoError(t, err) - return wasi.Errno(ret[0]) - } - }}, - } + // rights are ignored per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844 + fsRightsBase, fsRightsInheriting := uint64(1), uint64(2) - tests := []struct { - name string - fd uint32 - expectedPath string - }{ - { - name: "simple file open", - fd: workdirFD, - expectedPath: "wazero", - }, + setup := func() (*wasiAPI, *wasm.ModuleContext, publicwasm.Function) { + testFS := fstest.MapFS{pathName: &fstest.MapFile{Mode: os.ModeDir}} + sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ + workdirFD: {Path: ".", FS: testFS}, + }) + require.NoError(t, err) + a, mod, fn := instantiateModule(t, ctx, FunctionPathOpen, ImportPathOpen, moduleName, sys) + maskMemory(t, mod, len(expectedMemory)) + ok := mod.Memory().Write(0, initialMemory) + require.True(t, ok) + return a, mod, fn } - for _, pathOpenFn := range pathOpenFns { - pf := pathOpenFn - t.Run(pf.name, func(t *testing.T) { - for _, tt := range tests { - tc := tt - t.Run(tc.name, func(t *testing.T) { - // Create a memFS for testing that has "./wazero" file. - memFS := &MemFS{Files: map[string][]byte{"wazero": {}}} - sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ - workdirFD: {Path: ".", FS: memFS}, - }) - require.NoError(t, err) - - a, mod, fn := instantiateModule(t, ctx, FunctionPathOpen, ImportPathOpen, moduleName, sys) - defer mod.Close() + verify := func(errno wasi.Errno, mod *wasm.ModuleContext) { + require.Zero(t, errno, wasi.ErrnoName(errno)) - maskMemory(t, mod, len(expectedMemory)) - ok := mod.Memory().Write(0, initialMemory) - require.True(t, ok) + actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) + require.True(t, ok) + require.Equal(t, expectedMemory, actual) - errno := pf.pathOpen(a, mod, fn)(mod, tc.fd, dirflags, path, pathLen, oflags, fsRightsBase, fsRightsInheriting, fdFlags, resultOpenedFd) - require.Equal(t, wasi.ErrnoSuccess, errno) + // verify the file was actually opened + f, ok := sysContext(mod).OpenedFile(uint32(expectedFD)) + require.True(t, ok) + require.Equal(t, pathName, f.Path) + } - actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) + t.Run("SnapshotPreview1.PathOpen", func(t *testing.T) { + a, mod, _ := setup() + errno := a.PathOpen(mod, workdirFD, dirflags, path, pathLen, oflags, fsRightsBase, fsRightsInheriting, fdFlags, resultOpenedFd) + verify(errno, mod) + }) - // verify the file was actually opened - f, ok := sys.OpenedFile(uint32(expectedFD)) - require.True(t, ok) - require.Equal(t, tc.expectedPath, f.Path) - }) - } - }) - } + t.Run(FunctionPathOpen, func(t *testing.T) { + _, mod, fn := setup() + results, err := fn.Call(mod, uint64(workdirFD), uint64(dirflags), uint64(path), uint64(pathLen), uint64(oflags), fsRightsBase, fsRightsInheriting, uint64(fdFlags), uint64(resultOpenedFd)) + require.NoError(t, err) + errno := wasi.Errno(results[0]) + verify(errno, mod) + }) } func TestSnapshotPreview1_PathOpen_Errors(t *testing.T) { validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err - // Create a memFS for testing that has "./wazero" file. - memFS := &MemFS{ - Files: map[string][]byte{ - "wazero": []byte(""), - }, - } + pathName := "wazero" + testFS := fstest.MapFS{pathName: &fstest.MapFile{Mode: os.ModeDir}} + ctx := context.Background() sys, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ - validFD: {Path: ".", FS: memFS}, + validFD: {Path: ".", FS: testFS}, }) require.NoError(t, err) @@ -1698,9 +1729,7 @@ func TestSnapshotPreview1_PathOpen_Errors(t *testing.T) { validPath := uint32(0) // arbitrary offset validPathLen := uint32(6) // the length of "wazero" - mod.Memory().Write(validPath, []byte{ - 'w', 'a', 'z', 'e', 'r', 'o', // write to offset 0 (= validPath) - }) // wazero is the path to the file in the memFS + mod.Memory().Write(validPath, []byte(pathName)) tests := []struct { name string @@ -1747,7 +1776,7 @@ func TestSnapshotPreview1_PathOpen_Errors(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { errno := a.PathOpen(mod, tc.fd, 0, tc.path, tc.pathLen, tc.oflags, 0, 0, 0, tc.resultOpenedFd) - require.Equal(t, tc.expectedErrno, errno) + require.Equal(t, tc.expectedErrno, errno, wasi.ErrnoName(errno)) }) } } @@ -1762,13 +1791,15 @@ func TestSnapshotPreview1_PathReadlink(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.PathLink", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.PathReadlink(mod, 0, 0, 0, 0, 0, 0)) + errno := a.PathReadlink(mod, 0, 0, 0, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionPathReadlink, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -1782,13 +1813,15 @@ func TestSnapshotPreview1_PathRemoveDirectory(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.PathRemoveDirectory", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.PathRemoveDirectory(mod, 0, 0, 0)) + errno := a.PathRemoveDirectory(mod, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionPathRemoveDirectory, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -1802,13 +1835,15 @@ func TestSnapshotPreview1_PathRename(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.PathRename", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.PathRename(mod, 0, 0, 0, 0, 0, 0)) + errno := a.PathRename(mod, 0, 0, 0, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionPathRename, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -1822,13 +1857,15 @@ func TestSnapshotPreview1_PathSymlink(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.PathSymlink", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.PathSymlink(mod, 0, 0, 0, 0, 0)) + errno := a.PathSymlink(mod, 0, 0, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionPathSymlink, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -1842,13 +1879,15 @@ func TestSnapshotPreview1_PathUnlinkFile(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.PathUnlinkFile", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.PathUnlinkFile(mod, 0, 0, 0)) + errno := a.PathUnlinkFile(mod, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionPathUnlinkFile, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -1862,13 +1901,15 @@ func TestSnapshotPreview1_PollOneoff(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.PollOneoff", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.PollOneoff(mod, 0, 0, 0, 0)) + errno := a.PollOneoff(mod, 0, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionPollOneoff, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -1920,13 +1961,15 @@ func TestSnapshotPreview1_ProcRaise(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.ProcRaise", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.ProcRaise(mod, 0)) + errno := a.ProcRaise(mod, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionProcRaise, func(t *testing.T) { results, err := fn.Call(mod, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -1940,13 +1983,15 @@ func TestSnapshotPreview1_SchedYield(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.SchedYield", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.SchedYield(mod)) + errno := a.SchedYield(mod) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionSchedYield, func(t *testing.T) { results, err := fn.Call(mod) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -1980,7 +2025,7 @@ func TestSnapshotPreview1_RandomGet(t *testing.T) { // Invoke RandomGet directly and check the memory side effects! errno := a.RandomGet(mod, offset, length) - require.Equal(t, wasi.ErrnoSuccess, errno) + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, offset+length+1) require.True(t, ok) @@ -1992,7 +2037,8 @@ func TestSnapshotPreview1_RandomGet(t *testing.T) { results, err := fn.Call(mod, uint64(offset), uint64(length)) require.NoError(t, err) - require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Zero(t, errno, wasi.ErrnoName(errno)) actual, ok := mod.Memory().Read(0, offset+length+1) require.True(t, ok) @@ -2035,7 +2081,7 @@ func TestSnapshotPreview1_RandomGet_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { errno := a.RandomGet(mod, tc.offset, tc.length) - require.Equal(t, wasi.ErrnoFault, errno) + require.Equal(t, wasi.ErrnoFault, errno, wasi.ErrnoName(errno)) }) } } @@ -2053,7 +2099,7 @@ func TestSnapshotPreview1_RandomGet_SourceError(t *testing.T) { } errno := a.RandomGet(mod, uint32(1), uint32(5)) // arbitrary offset and length - require.Equal(t, wasi.ErrnoIo, errno) + require.Equal(t, wasi.ErrnoIo, errno, wasi.ErrnoName(errno)) } // TestSnapshotPreview1_SockRecv only tests it is stubbed for GrainLang per #271 @@ -2066,13 +2112,15 @@ func TestSnapshotPreview1_SockRecv(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.SockRecv", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.SockRecv(mod, 0, 0, 0, 0, 0, 0)) + errno := a.SockRecv(mod, 0, 0, 0, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionSockRecv, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -2086,13 +2134,15 @@ func TestSnapshotPreview1_SockSend(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.SockSend", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.SockSend(mod, 0, 0, 0, 0, 0)) + errno := a.SockSend(mod, 0, 0, 0, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionSockSend, func(t *testing.T) { results, err := fn.Call(mod, 0, 0, 0, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } @@ -2106,18 +2156,27 @@ func TestSnapshotPreview1_SockShutdown(t *testing.T) { defer mod.Close() t.Run("SnapshotPreview1.SockShutdown", func(t *testing.T) { - require.Equal(t, wasi.ErrnoNosys, a.SockShutdown(mod, 0, 0)) + errno := a.SockShutdown(mod, 0, 0) + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) t.Run(FunctionSockShutdown, func(t *testing.T) { results, err := fn.Call(mod, 0, 0) require.NoError(t, err) - require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64 + errno := wasi.Errno(results[0]) // results[0] is the errno + require.Equal(t, wasi.ErrnoNosys, errno, wasi.ErrnoName(errno)) }) } const testMemoryPageSize = 1 +// maskMemory sets the first memory in the store to '?' * size, so tests can see what's written. +func maskMemory(t *testing.T, mod publicwasm.Module, size int) { + for i := uint32(0); i < uint32(size); i++ { + require.True(t, mod.Memory().WriteByte(i, '?')) + } +} + func instantiateModule(t *testing.T, ctx context.Context, wasiFunction, wasiImport, moduleName string, sys *wasm.SysContext) (*wasiAPI, *wasm.ModuleContext, publicwasm.Function) { enabledFeatures := wasm.Features20191205 store := wasm.NewStore(interpreter.NewEngine(), enabledFeatures) @@ -2150,13 +2209,29 @@ func instantiateModule(t *testing.T, ctx context.Context, wasiFunction, wasiImpo return a, mod, fn } -// maskMemory sets the first memory in the store to '?' * size, so tests can see what's written. -func maskMemory(t *testing.T, mod publicwasm.Module, size int) { - for i := uint32(0); i < uint32(size); i++ { - require.True(t, mod.Memory().WriteByte(i, '?')) +func newSysContext(args, environ []string, openedFiles map[uint32]*wasm.FileEntry) (sys *wasm.SysContext, err error) { + return wasm.NewSysContext(math.MaxUint32, args, environ, new(bytes.Buffer), nil, nil, openedFiles) +} + +func createFile(t *testing.T, pathName string, data []byte) (fs.File, fs.FS) { + mapFile := &fstest.MapFile{Data: data} + if data == nil { + mapFile.Mode = os.ModeDir } + mapFS := fstest.MapFS{pathName: mapFile} + f, err := mapFS.Open(pathName) + require.NoError(t, err) + return f, mapFS } -func newSysContext(args, environ []string, openedFiles map[uint32]*wasm.FileEntry) (sys *wasm.SysContext, err error) { - return wasm.NewSysContext(math.MaxUint32, args, environ, new(bytes.Buffer), nil, nil, openedFiles) +// createWriteableFile uses real files when io.Writer tests are needed. +func createWriteableFile(t *testing.T, tmpDir string, pathName string, data []byte) (fs.File, fs.FS) { + require.NotNil(t, data) + absolutePath := path.Join(tmpDir, pathName) + require.NoError(t, os.WriteFile(absolutePath, data, 0o600)) + + // open the file for writing in a custom way until #390 + f, err := os.OpenFile(absolutePath, os.O_RDWR, 0o600) + require.NoError(t, err) + return f, os.DirFS(tmpDir) } diff --git a/internal/wasm/sys.go b/internal/wasm/sys.go index d0185c54ee..62637b1ca0 100644 --- a/internal/wasm/sys.go +++ b/internal/wasm/sys.go @@ -4,19 +4,19 @@ import ( "errors" "fmt" "io" + "io/fs" "math" "sync/atomic" - - "github.com/tetratelabs/wazero/wasi" ) -// FileEntry temporarily uses wasi types until #394. +// FileEntry maps a path to an open file in a file system. // // Note: This does not introduce cycles because the types here are in the package "wasi" not "internalwasi". type FileEntry struct { Path string - FS wasi.FS - File wasi.File + FS fs.FS + // File when nil this is a mount like "." or "/". + File fs.File } // SysContext holds module-scoped system resources currently only used by internalwasi. diff --git a/internal/wasm/sys_test.go b/internal/wasm/sys_test.go index 5bd958adfb..f9bd18be08 100644 --- a/internal/wasm/sys_test.go +++ b/internal/wasm/sys_test.go @@ -160,7 +160,7 @@ func TestSysContext_Close(t *testing.T) { t.Run("open files", func(t *testing.T) { tempDir := t.TempDir() pathName := "test" - file, _ := createWriteableFile(t, tempDir, pathName, make([]byte, 0)) + file, testFS := createWriteableFile(t, tempDir, pathName, make([]byte, 0)) sys, err := NewSysContext( 0, // max @@ -170,8 +170,8 @@ func TestSysContext_Close(t *testing.T) { nil, // stdout nil, // stderr map[uint32]*FileEntry{ // openedFiles - 3: {Path: "."}, - 4: {Path: path.Join(".", pathName), File: file}, + 3: {Path: ".", FS: testFS}, + 4: {Path: path.Join(".", pathName), File: file, FS: testFS}, }, ) require.NoError(t, err) @@ -181,7 +181,7 @@ func TestSysContext_Close(t *testing.T) { require.Empty(t, sys.openedFiles) // Verify it was actually closed, by trying to close it again. - err = file.Close() + err = file.(*os.File).Close() require.Contains(t, err.Error(), "file already closed") // No problem closing config again because the descriptors were removed, so they won't be called again. @@ -193,8 +193,7 @@ func TestSysContext_Close(t *testing.T) { } // createWriteableFile uses real files when io.Writer tests are needed. -// TODO: temporarily *os.File until #394 -func createWriteableFile(t *testing.T, tmpDir string, pathName string, data []byte) (*os.File, fs.FS) { +func createWriteableFile(t *testing.T, tmpDir string, pathName string, data []byte) (fs.File, fs.FS) { require.NotNil(t, data) absolutePath := path.Join(tmpDir, pathName) require.NoError(t, os.WriteFile(absolutePath, data, 0o600)) diff --git a/wasi.go b/wasi.go index 9b6fd3c308..f746f7238a 100644 --- a/wasi.go +++ b/wasi.go @@ -9,19 +9,6 @@ import ( "github.com/tetratelabs/wazero/wasm" ) -// WASIDirFS returns a file system (a wasi.FS) for the tree of files rooted at -// the directory dir. It's similar to os.DirFS, except that it implements -// wasi.FS instead of the fs.FS interface. -func WASIDirFS(dir string) wasi.FS { - return internalwasi.DirFS(dir) -} - -func WASIMemFS() wasi.FS { - return &internalwasi.MemFS{ - Files: map[string][]byte{}, - } -} - // WASISnapshotPreview1 are functions importable as the module name wasi.ModuleSnapshotPreview1 func WASISnapshotPreview1() *Module { _, fns := internalwasi.SnapshotPreview1Functions() diff --git a/wasi/wasi.go b/wasi/wasi.go index a72a70e3a3..1fbf0aaea6 100644 --- a/wasi/wasi.go +++ b/wasi/wasi.go @@ -3,7 +3,6 @@ package wasi import ( "fmt" - "io" ) const ( @@ -12,40 +11,6 @@ const ( ModuleSnapshotPreview1 = "wasi_snapshot_preview1" ) -// TODO: rename these according to other naming conventions -const ( - // WASI open flags - - O_CREATE = 1 << iota - O_DIR - O_EXCL - O_TRUNC - - // WASI fs rights - - R_FD_READ = 1 << iota - R_FD_SEEK - R_FD_FDSTAT_SET_FLAGS - R_FD_SYNC - R_FD_TELL - R_FD_WRITE -) - -// File combines file I/O interfaces supported by ModuleSnapshotPreview1. -type File interface { - io.Reader - io.Writer - io.Seeker - io.Closer -} - -// FS is an interface for a preopened directory. -type FS interface { - // OpenWASI is a general method to open a file, similar to - // os.OpenFile, but with WASI flags and rights instead of POSIX. - OpenWASI(dirFlags uint32, path string, oFlags uint32, fsRights, fsRightsInheriting uint64, fdFlags uint32) (File, error) -} - // Errno are the error codes returned by WASI functions. // // Note: This is not always an error, as ErrnoSuccess is a valid code. diff --git a/wasi_test.go b/wasi_test.go index f10b1dedbc..a9a2787240 100644 --- a/wasi_test.go +++ b/wasi_test.go @@ -70,6 +70,8 @@ func TestStartWASICommandWithConfig(t *testing.T) { require.Equal(t, append([]byte(tc), 0), stdout.Bytes()) stdout.Reset() - mod.Close() + require.NoError(t, mod.Close()) + + // TODO: figure out how to test config closed. } }