diff --git a/config.go b/config.go index 21b49572..a31e498e 100644 --- a/config.go +++ b/config.go @@ -3,7 +3,6 @@ package wazero import ( "context" "errors" - "fmt" "io" "io/fs" "math" @@ -11,6 +10,7 @@ import ( "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/internal/engine/compiler" "github.com/tetratelabs/wazero/internal/engine/interpreter" + fs2 "github.com/tetratelabs/wazero/internal/fs" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -452,21 +452,15 @@ type moduleConfig struct { // environKeys allow overwriting of existing values. environKeys map[string]int - // preopenFD has the next FD number to use - preopenFD uint32 - // preopens are keyed on file descriptor and only include the Path and FS fields. - preopens map[uint32]*wasm.FileEntry - // preopenPaths allow overwriting of existing paths. - preopenPaths map[string]uint32 + fs *fs2.FSConfig } func NewModuleConfig() ModuleConfig { return &moduleConfig{ startFunctions: []string{"_start"}, environKeys: map[string]int{}, - preopenFD: uint32(3), // after stdin/stdout/stderr - preopens: map[uint32]*wasm.FileEntry{}, - preopenPaths: map[string]uint32{}, + + fs: fs2.NewFSConfig(), } } @@ -493,7 +487,7 @@ func (c *moduleConfig) WithEnv(key, value string) ModuleConfig { // WithFS implements ModuleConfig.WithFS func (c *moduleConfig) WithFS(fs fs.FS) ModuleConfig { ret := *c // copy - ret.setFS("/", fs) + ret.fs = ret.fs.WithFS(fs) return &ret } @@ -542,23 +536,10 @@ func (c *moduleConfig) WithRandSource(source io.Reader) ModuleConfig { // WithWorkDirFS implements ModuleConfig.WithWorkDirFS func (c *moduleConfig) WithWorkDirFS(fs fs.FS) ModuleConfig { ret := *c // copy - ret.setFS(".", fs) + ret.fs = ret.fs.WithWorkDirFS(fs) return &ret } -// setFS maps a path to a file-system. This is only used for base paths: "/" and ".". -func (c *moduleConfig) setFS(path string, fs fs.FS) { - // Check to see if this key already exists and update it. - entry := &wasm.FileEntry{Path: path, FS: fs} - if fd, ok := c.preopenPaths[path]; ok { - c.preopens[fd] = entry - } else { - c.preopens[c.preopenFD] = entry - c.preopenPaths[path] = c.preopenFD - c.preopenFD++ - } -} - // toSysContext creates a baseline wasm.SysContext configured by ModuleConfig. func (c *moduleConfig) toSysContext() (sys *wasm.SysContext, err error) { var environ []string // Intentionally doesn't pre-allocate to reduce logic to default to nil. @@ -578,24 +559,9 @@ func (c *moduleConfig) toSysContext() (sys *wasm.SysContext, err error) { environ = append(environ, key+"="+value) } - // Ensure no-one set a nil FD. We do this here instead of at the call site to allow chaining as nil is unexpected. - rootFD := uint32(0) // zero is invalid - setWorkDirFS := false - preopens := c.preopens - for fd, entry := range preopens { - if entry.FS == nil { - err = fmt.Errorf("FS for %s is nil", entry.Path) - return - } else if entry.Path == "/" { - rootFD = fd - } else if entry.Path == "." { - setWorkDirFS = true - } - } - - // Default the working directory to the root FS if it exists. - if rootFD != 0 && !setWorkDirFS { - preopens[c.preopenFD] = &wasm.FileEntry{Path: ".", FS: preopens[rootFD].FS} + preopens, err := c.fs.Preopens() + if err != nil { + return nil, err } return wasm.NewSysContext(math.MaxUint32, c.args, environ, c.stdin, c.stdout, c.stderr, c.randSource, preopens) diff --git a/config_test.go b/config_test.go index 7a0c9bcf..8d4600b6 100644 --- a/config_test.go +++ b/config_test.go @@ -9,6 +9,7 @@ import ( "testing/fstest" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/fs" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -438,7 +439,7 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - map[uint32]*wasm.FileEntry{ // openedFiles + map[uint32]*fs.FileEntry{ // openedFiles 3: {Path: "/", FS: testFS}, 4: {Path: ".", FS: testFS}, }, @@ -455,7 +456,7 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - map[uint32]*wasm.FileEntry{ // openedFiles + map[uint32]*fs.FileEntry{ // openedFiles 3: {Path: "/", FS: testFS2}, 4: {Path: ".", FS: testFS2}, }, @@ -472,7 +473,7 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - map[uint32]*wasm.FileEntry{ // openedFiles + map[uint32]*fs.FileEntry{ // openedFiles 3: {Path: ".", FS: testFS}, }, ), @@ -488,7 +489,7 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - map[uint32]*wasm.FileEntry{ // openedFiles + map[uint32]*fs.FileEntry{ // openedFiles 3: {Path: "/", FS: testFS}, 4: {Path: ".", FS: testFS2}, }, @@ -505,7 +506,7 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - map[uint32]*wasm.FileEntry{ // openedFiles + map[uint32]*fs.FileEntry{ // openedFiles 3: {Path: ".", FS: testFS}, 4: {Path: "/", FS: testFS2}, }, @@ -576,7 +577,7 @@ func TestModuleConfig_toSysContext_Errors(t *testing.T) { } // requireSysContext ensures wasm.NewSysContext doesn't return an error, which makes it usable in test matrices. -func requireSysContext(t *testing.T, max uint32, args, environ []string, stdin io.Reader, stdout, stderr io.Writer, randsource io.Reader, openedFiles map[uint32]*wasm.FileEntry) *wasm.SysContext { +func requireSysContext(t *testing.T, max uint32, args, environ []string, stdin io.Reader, stdout, stderr io.Writer, randsource io.Reader, openedFiles map[uint32]*fs.FileEntry) *wasm.SysContext { sys, err := wasm.NewSysContext(max, args, environ, stdin, stdout, stderr, randsource, openedFiles) require.NoError(t, err) return sys diff --git a/examples/wasi/testdata/sub/test.txt b/examples/wasi/testdata/sub/test.txt new file mode 100644 index 00000000..4b45af5e --- /dev/null +++ b/examples/wasi/testdata/sub/test.txt @@ -0,0 +1 @@ +greet sub dir diff --git a/experimental/fs.go b/experimental/fs.go new file mode 100644 index 00000000..b00bd3ee --- /dev/null +++ b/experimental/fs.go @@ -0,0 +1,21 @@ +package experimental + +import ( + "context" + "io/fs" + + "github.com/tetratelabs/wazero/api" + internalfs "github.com/tetratelabs/wazero/internal/fs" +) + +// WithFS overrides fs.FS in the context-based manner. Caller needs to take responsibility for closing the filesystem. +func WithFS(ctx context.Context, fs fs.FS) (context.Context, api.Closer, error) { + fsConfig := internalfs.NewFSConfig().WithFS(fs) + preopens, err := fsConfig.Preopens() + if err != nil { + return nil, nil, err + } + + fsCtx := internalfs.NewContext(preopens) + return context.WithValue(ctx, internalfs.Key{}, fsCtx), fsCtx, nil +} diff --git a/experimental/fs_test.go b/experimental/fs_test.go new file mode 100644 index 00000000..abdef6ae --- /dev/null +++ b/experimental/fs_test.go @@ -0,0 +1,41 @@ +package experimental_test + +import ( + "context" + _ "embed" + "log" + "testing" + "testing/fstest" + + "github.com/tetratelabs/wazero/experimental" + "github.com/tetratelabs/wazero/internal/fs" + "github.com/tetratelabs/wazero/internal/testing/require" +) + +// This is a very basic integration of fs config. The main goal is to show how it is configured. +func TestWithFS(t *testing.T) { + fileName := "animals.txt" + mapfs := fstest.MapFS{fileName: &fstest.MapFile{Data: []byte(`animals`)}} + + // Set context to one that has experimental fs config + ctx, closer, err := experimental.WithFS(context.Background(), mapfs) + if err != nil { + log.Panicln(err) + } + defer closer.Close(ctx) + + v := ctx.Value(fs.Key{}) + require.NotNil(t, v) + fsCtx, ok := v.(*fs.Context) + require.True(t, ok) + + entry, ok := fsCtx.OpenedFile(3) + require.True(t, ok) + require.Equal(t, "/", entry.Path) + require.Equal(t, mapfs, entry.FS) + + entry, ok = fsCtx.OpenedFile(4) + require.True(t, ok) + require.Equal(t, ".", entry.Path) + require.Equal(t, mapfs, entry.FS) +} diff --git a/internal/fs/fs.go b/internal/fs/fs.go new file mode 100644 index 00000000..fb25de87 --- /dev/null +++ b/internal/fs/fs.go @@ -0,0 +1,172 @@ +package fs + +import ( + "context" + "fmt" + "io/fs" + "math" + "sync/atomic" +) + +// Key is a context.Context Value key. It allows overriding fs.FS for WASI. +// +// See https://github.com/tetratelabs/wazero/issues/491 +type Key struct{} + +// 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 fs.FS + // File when nil this is a mount like "." or "/". + File fs.File +} + +type Context struct { + // openedFiles is a map of file descriptor numbers (>=3) to open files (or directories) and defaults to empty. + // TODO: This is unguarded, so not goroutine-safe! + openedFiles map[uint32]*FileEntry + + // lastFD is not meant to be read directly. Rather by nextFD. + lastFD uint32 +} + +func NewContext(openedFiles map[uint32]*FileEntry) *Context { + var fsCtx Context + if openedFiles == nil { + fsCtx.openedFiles = map[uint32]*FileEntry{} + fsCtx.lastFD = 2 // STDERR + } else { + fsCtx.openedFiles = openedFiles + fsCtx.lastFD = 2 // STDERR + for fd := range openedFiles { + if fd > fsCtx.lastFD { + fsCtx.lastFD = fd + } + } + } + return &fsCtx +} + +// nextFD gets the next file descriptor number in a goroutine safe way (monotonically) or zero if we ran out. +// TODO: opendFiles is still not goroutine safe! +// TODO: This can return zero if we ran out of file descriptors. A future change can optimize by re-using an FD pool. +func (c *Context) nextFD() uint32 { + if c.lastFD == math.MaxUint32 { + return 0 + } + return atomic.AddUint32(&c.lastFD, 1) +} + +// Close implements io.Closer +func (c *Context) Close(_ context.Context) (err error) { + // Close any files opened in this context + for fd, entry := range c.openedFiles { + delete(c.openedFiles, fd) + if entry.File != nil { // File is nil for a mount like "." or "/" + if e := entry.File.Close(); e != nil { + err = e // This means the err returned == the last non-nil error. + } + } + } + return +} + +// CloseFile returns true if a file was opened and closed without error, or false if not. +func (c *Context) CloseFile(fd uint32) (bool, error) { + f, ok := c.openedFiles[fd] + if !ok { + return false, nil + } + delete(c.openedFiles, fd) + + if f.File == nil { // TODO: currently, this means it is a pre-opened filesystem, but this may change later. + return true, nil + } + if err := f.File.Close(); err != nil { + return false, err + } + return true, nil +} + +// OpenedFile returns a file and true if it was opened or nil and false, if not. +func (c *Context) OpenedFile(fd uint32) (*FileEntry, bool) { + f, ok := c.openedFiles[fd] + return f, ok +} + +// OpenFile returns the file descriptor of the new file or false if we ran out of file descriptors +func (c *Context) OpenFile(f *FileEntry) (uint32, bool) { + newFD := c.nextFD() + if newFD == 0 { + return 0, false + } + c.openedFiles[newFD] = f + return newFD, true +} + +type FSConfig struct { + // preopenFD has the next FD number to use + preopenFD uint32 + // preopens are keyed on file descriptor and only include the Path and FS fields. + preopens map[uint32]*FileEntry + // preopenPaths allow overwriting of existing paths. + preopenPaths map[string]uint32 +} + +func NewFSConfig() *FSConfig { + return &FSConfig{ + preopenFD: uint32(3), // after stdin/stdout/stderr + preopens: map[uint32]*FileEntry{}, + preopenPaths: map[string]uint32{}, + } +} + +// setFS maps a path to a file-system. This is only used for base paths: "/" and ".". +func (c *FSConfig) setFS(path string, fs fs.FS) { + // Check to see if this key already exists and update it. + entry := &FileEntry{Path: path, FS: fs} + if fd, ok := c.preopenPaths[path]; ok { + c.preopens[fd] = entry + } else { + c.preopens[c.preopenFD] = entry + c.preopenPaths[path] = c.preopenFD + c.preopenFD++ + } +} + +func (c *FSConfig) WithFS(fs fs.FS) *FSConfig { + ret := *c // copy + ret.setFS("/", fs) + return &ret +} + +func (c *FSConfig) WithWorkDirFS(fs fs.FS) *FSConfig { + ret := *c // copy + ret.setFS(".", fs) + return &ret +} + +func (c *FSConfig) Preopens() (map[uint32]*FileEntry, error) { + // Ensure no-one set a nil FD. We do this here instead of at the call site to allow chaining as nil is unexpected. + rootFD := uint32(0) // zero is invalid + setWorkDirFS := false + preopens := c.preopens + for fd, entry := range preopens { + if entry.FS == nil { + return nil, fmt.Errorf("FS for %s is nil", entry.Path) + } else if entry.Path == "/" { + rootFD = fd + } else if entry.Path == "." { + setWorkDirFS = true + } + } + + // Default the working directory to the root FS if it exists. + if rootFD != 0 && !setWorkDirFS { + preopens[c.preopenFD] = &FileEntry{Path: ".", FS: preopens[rootFD].FS} + } + + return preopens, nil +} diff --git a/internal/fs/fs_test.go b/internal/fs/fs_test.go new file mode 100644 index 00000000..4f367dd4 --- /dev/null +++ b/internal/fs/fs_test.go @@ -0,0 +1,49 @@ +package fs + +import ( + "context" + "io/fs" + "os" + "path" + "testing" + + "github.com/tetratelabs/wazero/internal/testing/require" +) + +// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. +var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") + +func TestContext_Close(t *testing.T) { + tempDir := t.TempDir() + pathName := "test" + file, _ := createWriteableFile(t, tempDir, pathName, make([]byte, 0)) + + fsc := NewContext(map[uint32]*FileEntry{ + 3: {Path: "."}, + 4: {Path: path.Join(".", pathName), File: file}, + }) + + // Verify base case + require.True(t, len(fsc.openedFiles) > 0, "fsc.openedFiles was empty") + + // Closing should not err. + require.NoError(t, fsc.Close(testCtx)) + + // Verify our intended side-effect + require.Equal(t, 0, len(fsc.openedFiles), "expected no opened files") + + // Verify no error closing again. + require.NoError(t, fsc.Close(testCtx)) +} + +// 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/call_context.go b/internal/wasm/call_context.go index 6bce5917..32cae816 100644 --- a/internal/wasm/call_context.go +++ b/internal/wasm/call_context.go @@ -90,7 +90,7 @@ func (m *CallContext) CloseWithExitCode(ctx context.Context, exitCode uint32) (e // close marks this CallContext as closed and releases underlying system resources without removing // from the store. -func (m *CallContext) close(_ context.Context, exitCode uint32) (c bool, err error) { +func (m *CallContext) close(ctx context.Context, exitCode uint32) (c bool, err error) { // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! closed := uint64(1) + uint64(exitCode)<<32 // Store exitCode as high-order bits. @@ -98,7 +98,7 @@ func (m *CallContext) close(_ context.Context, exitCode uint32) (c bool, err err return false, nil } if sys := m.Sys; sys != nil { // ex nil if from ModuleBuilder - return true, sys.Close() + return true, sys.FS().Close(ctx) } return true, nil } diff --git a/internal/wasm/call_context_test.go b/internal/wasm/call_context_test.go index 055453a9..71305924 100644 --- a/internal/wasm/call_context_test.go +++ b/internal/wasm/call_context_test.go @@ -3,9 +3,9 @@ package wasm import ( "context" "fmt" - "path" "testing" + "github.com/tetratelabs/wazero/internal/fs" "github.com/tetratelabs/wazero/internal/testing/require" ) @@ -141,10 +141,6 @@ func TestCallContext_Close(t *testing.T) { } t.Run("calls SysContext.Close()", func(t *testing.T) { - tempDir := t.TempDir() - pathName := "test" - file, _ := createWriteableFile(t, tempDir, pathName, make([]byte, 0)) - sys, err := NewSysContext( 0, // max nil, // args @@ -153,26 +149,29 @@ func TestCallContext_Close(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - map[uint32]*FileEntry{ // openedFiles + map[uint32]*fs.FileEntry{ // openedFiles 3: {Path: "."}, - 4: {Path: path.Join(".", pathName), File: file}, }, ) require.NoError(t, err) + fsCtx := sys.FS() + moduleName := t.Name() m, err := s.Instantiate(context.Background(), &Module{}, moduleName, sys, nil) require.NoError(t, err) // We use side effects to determine if Close in fact called SysContext.Close (without repeating sys_test.go). // One side effect of SysContext.Close is that it clears the openedFiles map. Verify our base case. - require.True(t, len(sys.openedFiles) > 0, "sys.openedFiles was empty") + _, ok := fsCtx.OpenedFile(3) + require.True(t, ok, "sys.openedFiles was empty") // Closing should not err. require.NoError(t, m.Close(testCtx)) // Verify our intended side-effect - require.Equal(t, 0, len(sys.openedFiles), "expected no opened files") + _, ok = fsCtx.OpenedFile(3) + require.False(t, ok, "expected no opened files") // Verify no error closing again. require.NoError(t, m.Close(testCtx)) diff --git a/internal/wasm/sys.go b/internal/wasm/sys.go index 8c921502..20977ccb 100644 --- a/internal/wasm/sys.go +++ b/internal/wasm/sys.go @@ -5,20 +5,9 @@ import ( "errors" "fmt" "io" - "io/fs" - "math" - "sync/atomic" -) -// 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 fs.FS - // File when nil this is a mount like "." or "/". - File fs.File -} + "github.com/tetratelabs/wazero/internal/fs" +) // SysContext holds module-scoped system resources currently only used by internalwasi. type SysContext struct { @@ -28,22 +17,7 @@ type SysContext struct { stdout, stderr io.Writer randSource io.Reader - // openedFiles is a map of file descriptor numbers (>=3) to open files (or directories) and defaults to empty. - // TODO: This is unguarded, so not goroutine-safe! - openedFiles map[uint32]*FileEntry - - // lastFD is not meant to be read directly. Rather by nextFD. - lastFD uint32 -} - -// nextFD gets the next file descriptor number in a goroutine safe way (monotonically) or zero if we ran out. -// TODO: opendFiles is still not goroutine safe! -// TODO: This can return zero if we ran out of file descriptors. A future change can optimize by re-using an FD pool. -func (c *SysContext) nextFD() uint32 { - if c.lastFD == math.MaxUint32 { - return 0 - } - return atomic.AddUint32(&c.lastFD, 1) + fs *fs.Context } // Args is like os.Args and defaults to nil. @@ -98,6 +72,13 @@ func (c *SysContext) Stderr() io.Writer { return c.stderr } +func (c *SysContext) FS() *fs.Context { + if c.fs == nil { + return &fs.Context{} + } + return c.fs +} + // RandSource is a source of random bytes and defaults to crypto/rand.Reader. // see wazero.ModuleConfig WithRandSource func (c *SysContext) RandSource() io.Reader { @@ -129,7 +110,7 @@ var _ = DefaultSysContext() // Force panic on bug. // NewSysContext is a factory function which helps avoid needing to know defaults or exporting all fields. // Note: max is exposed for testing. max is only used for env/args validation. -func NewSysContext(max uint32, args, environ []string, stdin io.Reader, stdout, stderr io.Writer, randSource io.Reader, openedFiles map[uint32]*FileEntry) (sys *SysContext, err error) { +func NewSysContext(max uint32, args, environ []string, stdin io.Reader, stdout, stderr io.Writer, randSource io.Reader, openedFiles map[uint32]*fs.FileEntry) (sys *SysContext, err error) { sys = &SysContext{args: args, environ: environ} if sys.argsSize, err = nullTerminatedByteCount(max, args); err != nil { @@ -164,18 +145,8 @@ func NewSysContext(max uint32, args, environ []string, stdin io.Reader, stdout, sys.randSource = randSource } - if openedFiles == nil { - sys.openedFiles = map[uint32]*FileEntry{} - sys.lastFD = 2 // STDERR - } else { - sys.openedFiles = openedFiles - sys.lastFD = 2 // STDERR - for fd := range openedFiles { - if fd > sys.lastFD { - sys.lastFD = fd - } - } - } + sys.fs = fs.NewContext(openedFiles) + return } @@ -207,50 +178,3 @@ func nullTerminatedByteCount(max uint32, elements []string) (uint32, error) { } return uint32(bufSize), nil } - -// Close implements io.Closer -func (c *SysContext) Close() (err error) { - // Close any files opened in this context - for fd, entry := range c.openedFiles { - delete(c.openedFiles, fd) - if entry.File != nil { // File is nil for a mount like "." or "/" - if e := entry.File.Close(); e != nil { - err = e // This means the err returned == the last non-nil error. - } - } - } - return -} - -// CloseFile returns true if a file was opened and closed without error, or false if not. -func (c *SysContext) CloseFile(fd uint32) (bool, error) { - f, ok := c.openedFiles[fd] - if !ok { - return false, nil - } - delete(c.openedFiles, fd) - - if f.File == nil { // TODO: currently, this means it is a pre-opened filesystem, but this may change later. - return true, nil - } - if err := f.File.Close(); err != nil { - return false, err - } - return true, nil -} - -// OpenedFile returns a file and true if it was opened or nil and false, if not. -func (c *SysContext) OpenedFile(fd uint32) (*FileEntry, bool) { - f, ok := c.openedFiles[fd] - return f, ok -} - -// OpenFile returns the file descriptor of the new file or false if we ran out of file descriptors -func (c *SysContext) OpenFile(f *FileEntry) (uint32, bool) { - newFD := c.nextFD() - if newFD == 0 { - return 0, false - } - c.openedFiles[newFD] = f - return newFD, true -} diff --git a/internal/wasm/sys_test.go b/internal/wasm/sys_test.go index 3de8399a..52b08953 100644 --- a/internal/wasm/sys_test.go +++ b/internal/wasm/sys_test.go @@ -3,11 +3,7 @@ package wasm import ( "bytes" "io" - "io/fs" - "os" - "path" "testing" - "testing/fstest" "github.com/tetratelabs/wazero/internal/testing/require" ) @@ -32,8 +28,6 @@ func TestDefaultSysContext(t *testing.T) { require.Equal(t, eofReader{}, sys.Stdin()) require.Equal(t, io.Discard, sys.Stdout()) require.Equal(t, io.Discard, sys.Stderr()) - require.Equal(t, 0, len(sys.openedFiles), "expected no opened files") - require.Equal(t, sys, DefaultSysContext()) } @@ -154,108 +148,3 @@ func TestNewSysContext_Environ(t *testing.T) { }) } } - -func TestSysContext_Close(t *testing.T) { - t.Run("no files", func(t *testing.T) { - sys := DefaultSysContext() - require.NoError(t, sys.Close()) - }) - - t.Run("open files", func(t *testing.T) { - tempDir := t.TempDir() - pathName := "test" - file, testFS := createWriteableFile(t, tempDir, pathName, make([]byte, 0)) - - sys, err := NewSysContext( - 0, // max - nil, // args - nil, // environ - nil, // stdin - nil, // stdout - nil, // stderr - nil, // randSource - map[uint32]*FileEntry{ // openedFiles - 3: {Path: "/", FS: testFS}, - 4: {Path: ".", FS: testFS}, - 5: {Path: path.Join(".", pathName), File: file, FS: testFS}, - }, - ) - require.NoError(t, err) - - // Closing should delete the file descriptors after closing the files. - require.NoError(t, sys.Close()) - require.Equal(t, 0, len(sys.openedFiles), "expected no opened files") - - // Verify it was actually closed, by trying to close it again. - 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. - require.NoError(t, sys.Close()) - }) - - t.Run("FS never used", func(t *testing.T) { - testFS := fstest.MapFS{} - sys, err := NewSysContext( - 0, // max - nil, // args - nil, // environ - nil, // stdin - nil, // stdout - nil, // stderr - nil, // randSource - map[uint32]*FileEntry{ // no openedFiles - 3: {Path: "/", FS: testFS}, - 4: {Path: ".", FS: testFS}, - }, - ) - require.NoError(t, err) - - // Even if there are no open files, the descriptors for the file-system mappings should be removed. - require.NoError(t, sys.Close()) - require.Equal(t, 0, len(sys.openedFiles), "expected no opened files") - }) - - t.Run("open file externally closed", func(t *testing.T) { - tempDir := t.TempDir() - pathName := "test" - file, testFS := createWriteableFile(t, tempDir, pathName, make([]byte, 0)) - - sys, err := NewSysContext( - 0, // max - nil, // args - nil, // environ - nil, // stdin - nil, // stdout - nil, // stderr - nil, // randSource - map[uint32]*FileEntry{ // openedFiles - 3: {Path: "/", FS: testFS}, - 4: {Path: ".", FS: testFS}, - 5: {Path: path.Join(".", pathName), File: file, FS: testFS}, - }, - ) - require.NoError(t, err) - - // Close the file externally - file.Close() - - // Closing should err as it expected to be open - require.Contains(t, sys.Close().Error(), "file already closed") - - // However, cleanup should still occur. - require.Equal(t, 0, len(sys.openedFiles), "expected no opened files") - }) -} - -// 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/wasi/wasi.go b/wasi/wasi.go index 0c758c7e..a6a8cd0f 100644 --- a/wasi/wasi.go +++ b/wasi/wasi.go @@ -16,6 +16,7 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/experimental" + internalfs "github.com/tetratelabs/wazero/internal/fs" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -691,9 +692,9 @@ func (a *snapshotPreview1) FdAllocate(ctx context.Context, m api.Module, fd uint // See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_close // See https://linux.die.net/man/3/close func (a *snapshotPreview1) FdClose(ctx context.Context, m api.Module, fd uint32) Errno { - sys := sysCtx(m) + _, fsc := sysFSCtx(ctx, m) - if ok, err := sys.CloseFile(fd); err != nil { + if ok, err := fsc.CloseFile(fd); err != nil { return ErrnoIo } else if !ok { return ErrnoBadf @@ -740,9 +741,9 @@ func (a *snapshotPreview1) FdDatasync(ctx context.Context, m api.Module, fd uint // See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_fdstat_get // See https://linux.die.net/man/3/fsync func (a *snapshotPreview1) FdFdstatGet(ctx context.Context, m api.Module, fd uint32, resultStat uint32) Errno { - sys := sysCtx(m) + _, fsc := sysFSCtx(ctx, m) - if _, ok := sys.OpenedFile(fd); !ok { + if _, ok := fsc.OpenedFile(fd); !ok { return ErrnoBadf } return ErrnoSuccess @@ -776,9 +777,9 @@ func (a *snapshotPreview1) FdFdstatGet(ctx context.Context, m api.Module, fd uin // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#prestat // See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_prestat_get func (a *snapshotPreview1) FdPrestatGet(ctx context.Context, m api.Module, fd uint32, resultPrestat uint32) Errno { - sys := sysCtx(m) + _, fsc := sysFSCtx(ctx, m) - entry, ok := sys.OpenedFile(fd) + entry, ok := fsc.OpenedFile(fd) if !ok { return ErrnoBadf } @@ -851,9 +852,9 @@ func (a *snapshotPreview1) FdPread(ctx context.Context, m api.Module, fd, iovs, // See FdPrestatGet // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_prestat_dir_name func (a *snapshotPreview1) FdPrestatDirName(ctx context.Context, m api.Module, fd uint32, pathPtr uint32, pathLen uint32) Errno { - sys := sysCtx(m) + _, fsc := sysFSCtx(ctx, m) - f, ok := sys.OpenedFile(fd) + f, ok := fsc.OpenedFile(fd) if !ok { return ErrnoBadf } @@ -919,13 +920,13 @@ func (a *snapshotPreview1) FdPwrite(ctx context.Context, m api.Module, fd, iovs, // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#iovec // See https://linux.die.net/man/3/readv func (a *snapshotPreview1) FdRead(ctx context.Context, m api.Module, fd, iovs, iovsCount, resultSize uint32) Errno { - sys := sysCtx(m) + sys, fsc := sysFSCtx(ctx, m) var reader io.Reader if fd == fdStdin { reader = sys.Stdin() - } else if f, ok := sys.OpenedFile(fd); !ok || f.File == nil { + } else if f, ok := fsc.OpenedFile(fd); !ok || f.File == nil { return ErrnoBadf } else { reader = f.File @@ -1002,11 +1003,11 @@ func (a *snapshotPreview1) FdRenumber(ctx context.Context, m api.Module, fd, to // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_seek // See https://linux.die.net/man/3/lseek func (a *snapshotPreview1) FdSeek(ctx context.Context, m api.Module, fd uint32, offset uint64, whence uint32, resultNewoffset uint32) Errno { - sys := sysCtx(m) + _, fsc := sysFSCtx(ctx, m) var seeker io.Seeker // Check to see if the file descriptor is available - if f, ok := sys.OpenedFile(fd); !ok || f.File == nil { + if f, ok := fsc.OpenedFile(fd); !ok || f.File == nil { return ErrnoBadf // fs.FS doesn't declare io.Seeker, but implementations such as os.File implement it. } else if seeker, ok = f.File.(io.Seeker); !ok { @@ -1088,7 +1089,7 @@ func (a *snapshotPreview1) FdTell(ctx context.Context, m api.Module, fd, resultO // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_write // See https://linux.die.net/man/3/writev func (a *snapshotPreview1) FdWrite(ctx context.Context, m api.Module, fd, iovs, iovsCount, resultSize uint32) Errno { - sys := sysCtx(m) + sys, fsc := sysFSCtx(ctx, m) var writer io.Writer @@ -1099,7 +1100,7 @@ func (a *snapshotPreview1) FdWrite(ctx context.Context, m api.Module, fd, iovs, writer = sys.Stderr() default: // Check to see if the file descriptor is available - if f, ok := sys.OpenedFile(fd); !ok || f.File == nil { + if f, ok := fsc.OpenedFile(fd); !ok || f.File == nil { return 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 { @@ -1201,9 +1202,9 @@ func (a *snapshotPreview1) PathLink(ctx context.Context, m api.Module, oldFd, ol // See https://linux.die.net/man/3/openat func (a *snapshotPreview1) PathOpen(ctx context.Context, m api.Module, fd, dirflags, pathPtr, pathLen, oflags uint32, fsRightsBase, fsRightsInheriting uint64, fdflags, resultOpenedFd uint32) (errno Errno) { - sys := sysCtx(m) + _, fsc := sysFSCtx(ctx, m) - dir, ok := sys.OpenedFile(fd) + dir, ok := fsc.OpenedFile(fd) if !ok || dir.FS == nil { return ErrnoBadf } @@ -1221,7 +1222,7 @@ func (a *snapshotPreview1) PathOpen(ctx context.Context, m api.Module, fd, dirfl return errno } - if newFD, ok := sys.OpenFile(entry); !ok { + if newFD, ok := fsc.OpenFile(entry); !ok { _ = entry.File.Close() return ErrnoIo } else if !m.Memory().WriteUint32Le(ctx, resultOpenedFd, newFD) { @@ -1362,7 +1363,23 @@ func sysCtx(m api.Module) *wasm.SysContext { } } -func openFileEntry(rootFS fs.FS, pathName string) (*wasm.FileEntry, Errno) { +func sysFSCtx(ctx context.Context, m api.Module) (*wasm.SysContext, *internalfs.Context) { + if internal, ok := m.(*wasm.CallContext); !ok { + panic(fmt.Errorf("unsupported wasm.Module implementation: %v", m)) + } else { + // Override Context when it is passed via context + if fsValue := ctx.Value(internalfs.Key{}); fsValue != nil { + fsCtx, ok := fsValue.(*internalfs.Context) + if !ok { + panic(fmt.Errorf("unsupported fs key: %v", fsValue)) + } + return internal.Sys, fsCtx + } + return internal.Sys, internal.Sys.FS() + } +} + +func openFileEntry(rootFS fs.FS, pathName string) (*internalfs.FileEntry, Errno) { f, err := rootFS.Open(pathName) if err != nil { switch { @@ -1378,7 +1395,7 @@ func openFileEntry(rootFS fs.FS, pathName string) (*wasm.FileEntry, Errno) { // 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 &wasm.FileEntry{Path: pathName, FS: rootFS, File: f}, ErrnoSuccess + return &internalfs.FileEntry{Path: pathName, FS: rootFS, File: f}, ErrnoSuccess } func writeOffsetsAndNullTerminatedValues(ctx context.Context, mem api.Memory, values []string, offsets, bytes uint32) Errno { diff --git a/wasi/wasi_test.go b/wasi/wasi_test.go index 88cedc45..2f92a7e4 100644 --- a/wasi/wasi_test.go +++ b/wasi/wasi_test.go @@ -19,6 +19,7 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/experimental" + fs2 "github.com/tetratelabs/wazero/internal/fs" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/sys" @@ -546,7 +547,7 @@ func TestSnapshotPreview1_FdClose(t *testing.T) { entry2, errno := openFileEntry(testFs, path2) require.Zero(t, errno, ErrnoName(errno)) - sysCtx, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ + sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{ fdToClose: entry1, fdToKeep: entry2, }) @@ -558,11 +559,12 @@ func TestSnapshotPreview1_FdClose(t *testing.T) { verify := func(mod api.Module) { // Verify fdToClose is closed and removed from the opened FDs. - _, ok := sysCtx(mod).OpenedFile(fdToClose) + _, fsc := sysFSCtx(testCtx, mod) + _, ok := fsc.OpenedFile(fdToClose) require.False(t, ok) // Verify fdToKeep is not closed - _, ok = sysCtx(mod).OpenedFile(fdToKeep) + _, ok = fsc.OpenedFile(fdToKeep) require.True(t, ok) } @@ -731,7 +733,7 @@ func TestSnapshotPreview1_FdPrestatGet(t *testing.T) { fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err pathName := "/tmp" - sysCtx, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{fd: {Path: pathName}}) + sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{fd: {Path: pathName}}) require.NoError(t, err) a, mod, fn := instantiateModule(testCtx, t, functionFdPrestatGet, importFdPrestatGet, sysCtx) @@ -776,7 +778,7 @@ func TestSnapshotPreview1_FdPrestatGet_Errors(t *testing.T) { fd := uint32(3) // fd 3 will be opened for the "/tmp" directory after 0, 1, and 2, that are stdin/out/err validAddress := uint32(0) // Arbitrary valid address as arguments to fd_prestat_get. We chose 0 here. - sysCtx, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{fd: {Path: "/tmp"}}) + sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{fd: {Path: "/tmp"}}) require.NoError(t, err) a, mod, _ := instantiateModule(testCtx, t, functionFdPrestatGet, importFdPrestatGet, sysCtx) @@ -818,7 +820,7 @@ func TestSnapshotPreview1_FdPrestatGet_Errors(t *testing.T) { func TestSnapshotPreview1_FdPrestatDirName(t *testing.T) { fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err - sysCtx, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{fd: {Path: "/tmp"}}) + sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{fd: {Path: "/tmp"}}) require.NoError(t, err) a, mod, fn := instantiateModule(testCtx, t, functionFdPrestatDirName, importFdPrestatDirName, sysCtx) @@ -859,7 +861,7 @@ func TestSnapshotPreview1_FdPrestatDirName(t *testing.T) { func TestSnapshotPreview1_FdPrestatDirName_Errors(t *testing.T) { fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err - sysCtx, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{fd: {Path: "/tmp"}}) + sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{fd: {Path: "/tmp"}}) require.NoError(t, err) a, mod, _ := instantiateModule(testCtx, t, functionFdPrestatDirName, importFdPrestatDirName, sysCtx) @@ -981,7 +983,7 @@ func TestSnapshotPreview1_FdRead(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Create a fresh file to read the contents from file, testFS := createFile(t, "test_path", []byte("wazero")) - sysCtx, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ + sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{ fd: {Path: "test_path", FS: testFS, File: file}, }) require.NoError(t, err) @@ -1008,7 +1010,7 @@ func TestSnapshotPreview1_FdRead_Errors(t *testing.T) { 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 - sysCtx, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ + sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{ validFD: {Path: "test_path", FS: testFS, File: file}, }) require.NoError(t, err) @@ -1137,11 +1139,13 @@ func TestSnapshotPreview1_FdSeek(t *testing.T) { 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 - sysCtx, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ + sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{ fd: {Path: "test_path", FS: testFS, File: file}, }) require.NoError(t, err) + fsCtx := sysCtx.FS() + a, mod, fn := instantiateModule(testCtx, t, functionFdSeek, importFdSeek, sysCtx) defer mod.Close(testCtx) @@ -1214,7 +1218,7 @@ func TestSnapshotPreview1_FdSeek(t *testing.T) { maskMemory(t, testCtx, mod, len(tc.expectedMemory)) // Since we initialized this file, we know it is a seeker (because it is a MapFile) - f, ok := sysCtx.OpenedFile(fd) + f, ok := fsCtx.OpenedFile(fd) require.True(t, ok) seeker := f.File.(io.Seeker) @@ -1243,7 +1247,7 @@ func TestSnapshotPreview1_FdSeek_Errors(t *testing.T) { 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 - sysCtx, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ + sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{ validFD: {Path: "test_path", FS: testFS, File: file}, }) require.NoError(t, err) @@ -1374,7 +1378,7 @@ func TestSnapshotPreview1_FdWrite(t *testing.T) { // Create a fresh file to write the contents to pathName := "test_path" file, testFS := createWriteableFile(t, tmpDir, pathName, []byte{}) - sysCtx, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ + sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{ fd: {Path: pathName, FS: testFS, File: file}, }) require.NoError(t, err) @@ -1409,7 +1413,7 @@ func TestSnapshotPreview1_FdWrite_Errors(t *testing.T) { pathName := "test_path" file, testFS := createWriteableFile(t, tmpDir, pathName, []byte{}) - sysCtx, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ + sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{ validFD: {Path: pathName, FS: testFS, File: file}, }) require.NoError(t, err) @@ -1553,32 +1557,44 @@ func TestSnapshotPreview1_PathLink(t *testing.T) { } 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 - oflags := uint32(0) // arbitrary oflags - fdFlags := uint32(0) + type pathOpenArgs struct { + fd uint32 + dirflags uint32 + pathPtr uint32 + pathLen uint32 + oflags uint32 + fsRightsBase uint64 + fsRightsInheriting uint64 + fdflags uint32 + resultOpenedFd uint32 + } - // 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...) + setup := func(workdirFD uint32, pathName string) (*snapshotPreview1, api.Module, api.Function, pathOpenArgs, []byte, uint32) { + // Setup the initial memory to include the path name starting at an offset. + initialMemory := append([]byte{'?'}, pathName...) - expectedFD := byte(workdirFD + 1) - resultOpenedFd := uint32(len(initialMemory) + 1) - expectedMemory := append( - initialMemory, - '?', // `resultOpenedFd` is after this - expectedFD, 0, 0, 0, - '?', - ) + expectedFD := workdirFD + 1 + expectedMemory := append( + initialMemory, + '?', // `resultOpenedFd` is after this + byte(expectedFD), 0, 0, 0, + '?', + ) - // rights are ignored per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844 - fsRightsBase, fsRightsInheriting := uint64(1), uint64(2) + args := pathOpenArgs{ + fd: workdirFD, + dirflags: 0, + pathPtr: 1, + pathLen: uint32(len(pathName)), + oflags: 0, + fsRightsBase: 1, // rights are ignored per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844 + fsRightsInheriting: 2, + fdflags: 0, + resultOpenedFd: uint32(len(initialMemory) + 1), + } - setup := func() (*snapshotPreview1, api.Module, api.Function) { testFS := fstest.MapFS{pathName: &fstest.MapFile{Mode: os.ModeDir}} - sysCtx, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ + sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{ workdirFD: {Path: ".", FS: testFS}, }) require.NoError(t, err) @@ -1586,10 +1602,10 @@ func TestSnapshotPreview1_PathOpen(t *testing.T) { maskMemory(t, testCtx, mod, len(expectedMemory)) ok := mod.Memory().Write(testCtx, 0, initialMemory) require.True(t, ok) - return a, mod, fn + return a, mod, fn, args, expectedMemory, expectedFD } - verify := func(errno Errno, mod api.Module) { + verify := func(ctx context.Context, errno Errno, mod api.Module, pathName string, expectedMemory []byte, expectedFD uint32) { require.Zero(t, errno, ErrnoName(errno)) actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) @@ -1597,23 +1613,55 @@ func TestSnapshotPreview1_PathOpen(t *testing.T) { require.Equal(t, expectedMemory, actual) // verify the file was actually opened - f, ok := sysCtx(mod).OpenedFile(uint32(expectedFD)) + _, fsc := sysFSCtx(ctx, mod) + f, ok := fsc.OpenedFile(expectedFD) require.True(t, ok) require.Equal(t, pathName, f.Path) } t.Run("snapshotPreview1.PathOpen", func(t *testing.T) { - a, mod, _ := setup() - errno := a.PathOpen(testCtx, mod, workdirFD, dirflags, path, pathLen, oflags, fsRightsBase, fsRightsInheriting, fdFlags, resultOpenedFd) - verify(errno, mod) + workdirFD := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err + pathName := "wazero" + + a, mod, _, args, expectedMemory, expectedFD := setup(workdirFD, pathName) + errno := a.PathOpen(testCtx, mod, args.fd, args.dirflags, args.pathPtr, args.pathLen, args.oflags, + args.fsRightsBase, args.fsRightsInheriting, args.fdflags, args.resultOpenedFd) + verify(testCtx, errno, mod, pathName, expectedMemory, expectedFD) }) t.Run(functionPathOpen, func(t *testing.T) { - _, mod, fn := setup() - results, err := fn.Call(testCtx, uint64(workdirFD), uint64(dirflags), uint64(path), uint64(pathLen), uint64(oflags), fsRightsBase, fsRightsInheriting, uint64(fdFlags), uint64(resultOpenedFd)) + workdirFD := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err + pathName := "wazero" + + _, mod, fn, args, expectedMemory, expectedFD := setup(workdirFD, pathName) + results, err := fn.Call(testCtx, uint64(args.fd), uint64(args.dirflags), uint64(args.pathPtr), uint64(args.pathLen), + uint64(args.oflags), args.fsRightsBase, args.fsRightsInheriting, uint64(args.fdflags), uint64(args.resultOpenedFd)) require.NoError(t, err) errno := Errno(results[0]) - verify(errno, mod) + verify(testCtx, errno, mod, pathName, expectedMemory, expectedFD) + }) + + t.Run("snapshotPreview1.PathOpen.WithFS", func(t *testing.T) { + workdirFD := uint32(100) // dummy fd as it is not used + pathName := "wazero" + + // The filesystem initialized in setup() is not used as it will be overridden. + a, mod, _, args, expectedMemory, _ := setup(workdirFD, pathName) + + // Override fs.FS through context + workdirFD = uint32(4) // 3 is '/' and 4 is '.' + expectedFD := workdirFD + 1 + expectedMemory[8] = byte(expectedFD) // replace expected memory with expected fd + testFS := fstest.MapFS{pathName: &fstest.MapFile{Mode: os.ModeDir}} + ctx, closer, err := experimental.WithFS(testCtx, testFS) + require.NoError(t, err) + defer closer.Close(ctx) + + errno := a.PathOpen(ctx, mod, workdirFD, args.dirflags, args.pathPtr, args.pathLen, args.oflags, + args.fsRightsBase, args.fsRightsInheriting, args.fdflags, args.resultOpenedFd) + require.Zero(t, errno, ErrnoName(errno)) + + verify(ctx, errno, mod, pathName, expectedMemory, expectedFD) }) } @@ -1622,7 +1670,7 @@ func TestSnapshotPreview1_PathOpen_Errors(t *testing.T) { pathName := "wazero" testFS := fstest.MapFS{pathName: &fstest.MapFile{Mode: os.ModeDir}} - sysCtx, err := newSysContext(nil, nil, map[uint32]*wasm.FileEntry{ + sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{ validFD: {Path: ".", FS: testFS}, }) require.NoError(t, err) @@ -2076,7 +2124,7 @@ func instantiateModule(ctx context.Context, t *testing.T, wasifunction, wasiimpo return a, mod, fn } -func newSysContext(args, environ []string, openedFiles map[uint32]*wasm.FileEntry) (sysCtx *wasm.SysContext, err error) { +func newSysContext(args, environ []string, openedFiles map[uint32]*fs2.FileEntry) (sysCtx *wasm.SysContext, err error) { return wasm.NewSysContext(math.MaxUint32, args, environ, new(bytes.Buffer), nil, nil, nil, openedFiles) }