Allow passing fs.FS when calling functions (#571)

Fixes #563 

Signed-off-by: knqyf263 <knqyf263@gmail.com>
Co-authored-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Teppei Fukuda
2022-05-20 04:51:17 +03:00
committed by GitHub
parent b3fc76ed6e
commit 7794530d01
13 changed files with 453 additions and 325 deletions

View File

@@ -3,7 +3,6 @@ package wazero
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"io/fs" "io/fs"
"math" "math"
@@ -11,6 +10,7 @@ import (
"github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/engine/compiler" "github.com/tetratelabs/wazero/internal/engine/compiler"
"github.com/tetratelabs/wazero/internal/engine/interpreter" "github.com/tetratelabs/wazero/internal/engine/interpreter"
fs2 "github.com/tetratelabs/wazero/internal/fs"
"github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/internal/wasm"
) )
@@ -452,21 +452,15 @@ type moduleConfig struct {
// environKeys allow overwriting of existing values. // environKeys allow overwriting of existing values.
environKeys map[string]int environKeys map[string]int
// preopenFD has the next FD number to use fs *fs2.FSConfig
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
} }
func NewModuleConfig() ModuleConfig { func NewModuleConfig() ModuleConfig {
return &moduleConfig{ return &moduleConfig{
startFunctions: []string{"_start"}, startFunctions: []string{"_start"},
environKeys: map[string]int{}, environKeys: map[string]int{},
preopenFD: uint32(3), // after stdin/stdout/stderr
preopens: map[uint32]*wasm.FileEntry{}, fs: fs2.NewFSConfig(),
preopenPaths: map[string]uint32{},
} }
} }
@@ -493,7 +487,7 @@ func (c *moduleConfig) WithEnv(key, value string) ModuleConfig {
// WithFS implements ModuleConfig.WithFS // WithFS implements ModuleConfig.WithFS
func (c *moduleConfig) WithFS(fs fs.FS) ModuleConfig { func (c *moduleConfig) WithFS(fs fs.FS) ModuleConfig {
ret := *c // copy ret := *c // copy
ret.setFS("/", fs) ret.fs = ret.fs.WithFS(fs)
return &ret return &ret
} }
@@ -542,23 +536,10 @@ func (c *moduleConfig) WithRandSource(source io.Reader) ModuleConfig {
// WithWorkDirFS implements ModuleConfig.WithWorkDirFS // WithWorkDirFS implements ModuleConfig.WithWorkDirFS
func (c *moduleConfig) WithWorkDirFS(fs fs.FS) ModuleConfig { func (c *moduleConfig) WithWorkDirFS(fs fs.FS) ModuleConfig {
ret := *c // copy ret := *c // copy
ret.setFS(".", fs) ret.fs = ret.fs.WithWorkDirFS(fs)
return &ret 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. // toSysContext creates a baseline wasm.SysContext configured by ModuleConfig.
func (c *moduleConfig) toSysContext() (sys *wasm.SysContext, err error) { func (c *moduleConfig) toSysContext() (sys *wasm.SysContext, err error) {
var environ []string // Intentionally doesn't pre-allocate to reduce logic to default to nil. 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) 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. preopens, err := c.fs.Preopens()
rootFD := uint32(0) // zero is invalid if err != nil {
setWorkDirFS := false return nil, err
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}
} }
return wasm.NewSysContext(math.MaxUint32, c.args, environ, c.stdin, c.stdout, c.stderr, c.randSource, preopens) return wasm.NewSysContext(math.MaxUint32, c.args, environ, c.stdin, c.stdout, c.stderr, c.randSource, preopens)

View File

@@ -9,6 +9,7 @@ import (
"testing/fstest" "testing/fstest"
"github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/fs"
"github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/internal/wasm"
) )
@@ -438,7 +439,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout nil, // stdout
nil, // stderr nil, // stderr
nil, // randSource nil, // randSource
map[uint32]*wasm.FileEntry{ // openedFiles map[uint32]*fs.FileEntry{ // openedFiles
3: {Path: "/", FS: testFS}, 3: {Path: "/", FS: testFS},
4: {Path: ".", FS: testFS}, 4: {Path: ".", FS: testFS},
}, },
@@ -455,7 +456,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout nil, // stdout
nil, // stderr nil, // stderr
nil, // randSource nil, // randSource
map[uint32]*wasm.FileEntry{ // openedFiles map[uint32]*fs.FileEntry{ // openedFiles
3: {Path: "/", FS: testFS2}, 3: {Path: "/", FS: testFS2},
4: {Path: ".", FS: testFS2}, 4: {Path: ".", FS: testFS2},
}, },
@@ -472,7 +473,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout nil, // stdout
nil, // stderr nil, // stderr
nil, // randSource nil, // randSource
map[uint32]*wasm.FileEntry{ // openedFiles map[uint32]*fs.FileEntry{ // openedFiles
3: {Path: ".", FS: testFS}, 3: {Path: ".", FS: testFS},
}, },
), ),
@@ -488,7 +489,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout nil, // stdout
nil, // stderr nil, // stderr
nil, // randSource nil, // randSource
map[uint32]*wasm.FileEntry{ // openedFiles map[uint32]*fs.FileEntry{ // openedFiles
3: {Path: "/", FS: testFS}, 3: {Path: "/", FS: testFS},
4: {Path: ".", FS: testFS2}, 4: {Path: ".", FS: testFS2},
}, },
@@ -505,7 +506,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout nil, // stdout
nil, // stderr nil, // stderr
nil, // randSource nil, // randSource
map[uint32]*wasm.FileEntry{ // openedFiles map[uint32]*fs.FileEntry{ // openedFiles
3: {Path: ".", FS: testFS}, 3: {Path: ".", FS: testFS},
4: {Path: "/", FS: testFS2}, 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. // 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) sys, err := wasm.NewSysContext(max, args, environ, stdin, stdout, stderr, randsource, openedFiles)
require.NoError(t, err) require.NoError(t, err)
return sys return sys

1
examples/wasi/testdata/sub/test.txt vendored Normal file
View File

@@ -0,0 +1 @@
greet sub dir

21
experimental/fs.go Normal file
View File

@@ -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
}

41
experimental/fs_test.go Normal file
View File

@@ -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)
}

172
internal/fs/fs.go Normal file
View File

@@ -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
}

49
internal/fs/fs_test.go Normal file
View File

@@ -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)
}

View File

@@ -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 // close marks this CallContext as closed and releases underlying system resources without removing
// from the store. // 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()! // 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. 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 return false, nil
} }
if sys := m.Sys; sys != nil { // ex nil if from ModuleBuilder if sys := m.Sys; sys != nil { // ex nil if from ModuleBuilder
return true, sys.Close() return true, sys.FS().Close(ctx)
} }
return true, nil return true, nil
} }

View File

@@ -3,9 +3,9 @@ package wasm
import ( import (
"context" "context"
"fmt" "fmt"
"path"
"testing" "testing"
"github.com/tetratelabs/wazero/internal/fs"
"github.com/tetratelabs/wazero/internal/testing/require" "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) { 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( sys, err := NewSysContext(
0, // max 0, // max
nil, // args nil, // args
@@ -153,26 +149,29 @@ func TestCallContext_Close(t *testing.T) {
nil, // stdout nil, // stdout
nil, // stderr nil, // stderr
nil, // randSource nil, // randSource
map[uint32]*FileEntry{ // openedFiles map[uint32]*fs.FileEntry{ // openedFiles
3: {Path: "."}, 3: {Path: "."},
4: {Path: path.Join(".", pathName), File: file},
}, },
) )
require.NoError(t, err) require.NoError(t, err)
fsCtx := sys.FS()
moduleName := t.Name() moduleName := t.Name()
m, err := s.Instantiate(context.Background(), &Module{}, moduleName, sys, nil) m, err := s.Instantiate(context.Background(), &Module{}, moduleName, sys, nil)
require.NoError(t, err) require.NoError(t, err)
// We use side effects to determine if Close in fact called SysContext.Close (without repeating sys_test.go). // 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. // 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. // Closing should not err.
require.NoError(t, m.Close(testCtx)) require.NoError(t, m.Close(testCtx))
// Verify our intended side-effect // 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. // Verify no error closing again.
require.NoError(t, m.Close(testCtx)) require.NoError(t, m.Close(testCtx))

View File

@@ -5,20 +5,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"math"
"sync/atomic"
)
// FileEntry maps a path to an open file in a file system. "github.com/tetratelabs/wazero/internal/fs"
// )
// 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
}
// SysContext holds module-scoped system resources currently only used by internalwasi. // SysContext holds module-scoped system resources currently only used by internalwasi.
type SysContext struct { type SysContext struct {
@@ -28,22 +17,7 @@ type SysContext struct {
stdout, stderr io.Writer stdout, stderr io.Writer
randSource io.Reader randSource io.Reader
// openedFiles is a map of file descriptor numbers (>=3) to open files (or directories) and defaults to empty. fs *fs.Context
// 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)
} }
// Args is like os.Args and defaults to nil. // Args is like os.Args and defaults to nil.
@@ -98,6 +72,13 @@ func (c *SysContext) Stderr() io.Writer {
return c.stderr 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. // RandSource is a source of random bytes and defaults to crypto/rand.Reader.
// see wazero.ModuleConfig WithRandSource // see wazero.ModuleConfig WithRandSource
func (c *SysContext) RandSource() io.Reader { 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. // 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. // 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} sys = &SysContext{args: args, environ: environ}
if sys.argsSize, err = nullTerminatedByteCount(max, args); err != nil { 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 sys.randSource = randSource
} }
if openedFiles == nil { sys.fs = fs.NewContext(openedFiles)
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
}
}
}
return return
} }
@@ -207,50 +178,3 @@ func nullTerminatedByteCount(max uint32, elements []string) (uint32, error) {
} }
return uint32(bufSize), nil 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
}

View File

@@ -3,11 +3,7 @@ package wasm
import ( import (
"bytes" "bytes"
"io" "io"
"io/fs"
"os"
"path"
"testing" "testing"
"testing/fstest"
"github.com/tetratelabs/wazero/internal/testing/require" "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, eofReader{}, sys.Stdin())
require.Equal(t, io.Discard, sys.Stdout()) require.Equal(t, io.Discard, sys.Stdout())
require.Equal(t, io.Discard, sys.Stderr()) require.Equal(t, io.Discard, sys.Stderr())
require.Equal(t, 0, len(sys.openedFiles), "expected no opened files")
require.Equal(t, sys, DefaultSysContext()) 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)
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/experimental"
internalfs "github.com/tetratelabs/wazero/internal/fs"
"github.com/tetratelabs/wazero/internal/wasm" "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://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_close
// See https://linux.die.net/man/3/close // See https://linux.die.net/man/3/close
func (a *snapshotPreview1) FdClose(ctx context.Context, m api.Module, fd uint32) Errno { 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 return ErrnoIo
} else if !ok { } else if !ok {
return ErrnoBadf 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://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_fdstat_get
// See https://linux.die.net/man/3/fsync // See https://linux.die.net/man/3/fsync
func (a *snapshotPreview1) FdFdstatGet(ctx context.Context, m api.Module, fd uint32, resultStat uint32) Errno { 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 ErrnoBadf
} }
return ErrnoSuccess 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/snapshot-01/phases/snapshot/docs.md#prestat
// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_prestat_get // 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 { 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 { if !ok {
return ErrnoBadf return ErrnoBadf
} }
@@ -851,9 +852,9 @@ func (a *snapshotPreview1) FdPread(ctx context.Context, m api.Module, fd, iovs,
// See FdPrestatGet // See FdPrestatGet
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_prestat_dir_name // 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 { 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 { if !ok {
return ErrnoBadf 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://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#iovec
// See https://linux.die.net/man/3/readv // See https://linux.die.net/man/3/readv
func (a *snapshotPreview1) FdRead(ctx context.Context, m api.Module, fd, iovs, iovsCount, resultSize uint32) Errno { 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 var reader io.Reader
if fd == fdStdin { if fd == fdStdin {
reader = sys.Stdin() 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 return ErrnoBadf
} else { } else {
reader = f.File 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://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_seek
// See https://linux.die.net/man/3/lseek // 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 { 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 var seeker io.Seeker
// Check to see if the file descriptor is available // 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 return ErrnoBadf
// fs.FS doesn't declare io.Seeker, but implementations such as os.File implement it. // fs.FS doesn't declare io.Seeker, but implementations such as os.File implement it.
} else if seeker, ok = f.File.(io.Seeker); !ok { } 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://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_write
// See https://linux.die.net/man/3/writev // See https://linux.die.net/man/3/writev
func (a *snapshotPreview1) FdWrite(ctx context.Context, m api.Module, fd, iovs, iovsCount, resultSize uint32) Errno { 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 var writer io.Writer
@@ -1099,7 +1100,7 @@ func (a *snapshotPreview1) FdWrite(ctx context.Context, m api.Module, fd, iovs,
writer = sys.Stderr() writer = sys.Stderr()
default: default:
// Check to see if the file descriptor is available // 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 return ErrnoBadf
// fs.FS doesn't declare io.Writer, but implementations such as os.File implement it. // fs.FS doesn't declare io.Writer, but implementations such as os.File implement it.
} else if writer, ok = f.File.(io.Writer); !ok { } 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 // 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, func (a *snapshotPreview1) PathOpen(ctx context.Context, m api.Module, fd, dirflags, pathPtr, pathLen, oflags uint32, fsRightsBase,
fsRightsInheriting uint64, fdflags, resultOpenedFd uint32) (errno Errno) { 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 { if !ok || dir.FS == nil {
return ErrnoBadf return ErrnoBadf
} }
@@ -1221,7 +1222,7 @@ func (a *snapshotPreview1) PathOpen(ctx context.Context, m api.Module, fd, dirfl
return errno return errno
} }
if newFD, ok := sys.OpenFile(entry); !ok { if newFD, ok := fsc.OpenFile(entry); !ok {
_ = entry.File.Close() _ = entry.File.Close()
return ErrnoIo return ErrnoIo
} else if !m.Memory().WriteUint32Le(ctx, resultOpenedFd, newFD) { } 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) f, err := rootFS.Open(pathName)
if err != nil { if err != nil {
switch { 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 // 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 // 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 { func writeOffsetsAndNullTerminatedValues(ctx context.Context, mem api.Memory, values []string, offsets, bytes uint32) Errno {

View File

@@ -19,6 +19,7 @@ import (
"github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/experimental"
fs2 "github.com/tetratelabs/wazero/internal/fs"
"github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/sys" "github.com/tetratelabs/wazero/sys"
@@ -546,7 +547,7 @@ func TestSnapshotPreview1_FdClose(t *testing.T) {
entry2, errno := openFileEntry(testFs, path2) entry2, errno := openFileEntry(testFs, path2)
require.Zero(t, errno, ErrnoName(errno)) 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, fdToClose: entry1,
fdToKeep: entry2, fdToKeep: entry2,
}) })
@@ -558,11 +559,12 @@ func TestSnapshotPreview1_FdClose(t *testing.T) {
verify := func(mod api.Module) { verify := func(mod api.Module) {
// Verify fdToClose is closed and removed from the opened FDs. // 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) require.False(t, ok)
// Verify fdToKeep is not closed // Verify fdToKeep is not closed
_, ok = sysCtx(mod).OpenedFile(fdToKeep) _, ok = fsc.OpenedFile(fdToKeep)
require.True(t, ok) 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 fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err
pathName := "/tmp" 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) require.NoError(t, err)
a, mod, fn := instantiateModule(testCtx, t, functionFdPrestatGet, importFdPrestatGet, sysCtx) 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 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. 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) require.NoError(t, err)
a, mod, _ := instantiateModule(testCtx, t, functionFdPrestatGet, importFdPrestatGet, sysCtx) 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) { func TestSnapshotPreview1_FdPrestatDirName(t *testing.T) {
fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err 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) require.NoError(t, err)
a, mod, fn := instantiateModule(testCtx, t, functionFdPrestatDirName, importFdPrestatDirName, sysCtx) 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) { func TestSnapshotPreview1_FdPrestatDirName_Errors(t *testing.T) {
fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err 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) require.NoError(t, err)
a, mod, _ := instantiateModule(testCtx, t, functionFdPrestatDirName, importFdPrestatDirName, sysCtx) 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) { t.Run(tc.name, func(t *testing.T) {
// Create a fresh file to read the contents from // Create a fresh file to read the contents from
file, testFS := createFile(t, "test_path", []byte("wazero")) 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}, fd: {Path: "test_path", FS: testFS, File: file},
}) })
require.NoError(t, err) 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 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 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}, validFD: {Path: "test_path", FS: testFS, File: file},
}) })
require.NoError(t, err) 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 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 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}, fd: {Path: "test_path", FS: testFS, File: file},
}) })
require.NoError(t, err) require.NoError(t, err)
fsCtx := sysCtx.FS()
a, mod, fn := instantiateModule(testCtx, t, functionFdSeek, importFdSeek, sysCtx) a, mod, fn := instantiateModule(testCtx, t, functionFdSeek, importFdSeek, sysCtx)
defer mod.Close(testCtx) defer mod.Close(testCtx)
@@ -1214,7 +1218,7 @@ func TestSnapshotPreview1_FdSeek(t *testing.T) {
maskMemory(t, testCtx, mod, len(tc.expectedMemory)) maskMemory(t, testCtx, mod, len(tc.expectedMemory))
// Since we initialized this file, we know it is a seeker (because it is a MapFile) // 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) require.True(t, ok)
seeker := f.File.(io.Seeker) 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 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 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}, validFD: {Path: "test_path", FS: testFS, File: file},
}) })
require.NoError(t, err) require.NoError(t, err)
@@ -1374,7 +1378,7 @@ func TestSnapshotPreview1_FdWrite(t *testing.T) {
// Create a fresh file to write the contents to // Create a fresh file to write the contents to
pathName := "test_path" pathName := "test_path"
file, testFS := createWriteableFile(t, tmpDir, pathName, []byte{}) 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}, fd: {Path: pathName, FS: testFS, File: file},
}) })
require.NoError(t, err) require.NoError(t, err)
@@ -1409,7 +1413,7 @@ func TestSnapshotPreview1_FdWrite_Errors(t *testing.T) {
pathName := "test_path" pathName := "test_path"
file, testFS := createWriteableFile(t, tmpDir, pathName, []byte{}) 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}, validFD: {Path: pathName, FS: testFS, File: file},
}) })
require.NoError(t, err) require.NoError(t, err)
@@ -1553,32 +1557,44 @@ func TestSnapshotPreview1_PathLink(t *testing.T) {
} }
func TestSnapshotPreview1_PathOpen(t *testing.T) { func TestSnapshotPreview1_PathOpen(t *testing.T) {
workdirFD := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err type pathOpenArgs struct {
dirflags := uint32(0) // arbitrary dirflags fd uint32
oflags := uint32(0) // arbitrary oflags dirflags uint32
fdFlags := uint32(0) 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. setup := func(workdirFD uint32, pathName string) (*snapshotPreview1, api.Module, api.Function, pathOpenArgs, []byte, uint32) {
pathName := "wazero" // Setup the initial memory to include the path name starting at an offset.
path := uint32(1) initialMemory := append([]byte{'?'}, pathName...)
pathLen := uint32(len(pathName))
initialMemory := append([]byte{'?'}, pathName...)
expectedFD := byte(workdirFD + 1) expectedFD := workdirFD + 1
resultOpenedFd := uint32(len(initialMemory) + 1) expectedMemory := append(
expectedMemory := append( initialMemory,
initialMemory, '?', // `resultOpenedFd` is after this
'?', // `resultOpenedFd` is after this byte(expectedFD), 0, 0, 0,
expectedFD, 0, 0, 0, '?',
'?', )
)
// rights are ignored per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844 args := pathOpenArgs{
fsRightsBase, fsRightsInheriting := uint64(1), uint64(2) 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}} 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}, workdirFD: {Path: ".", FS: testFS},
}) })
require.NoError(t, err) require.NoError(t, err)
@@ -1586,10 +1602,10 @@ func TestSnapshotPreview1_PathOpen(t *testing.T) {
maskMemory(t, testCtx, mod, len(expectedMemory)) maskMemory(t, testCtx, mod, len(expectedMemory))
ok := mod.Memory().Write(testCtx, 0, initialMemory) ok := mod.Memory().Write(testCtx, 0, initialMemory)
require.True(t, ok) 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)) require.Zero(t, errno, ErrnoName(errno))
actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) 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) require.Equal(t, expectedMemory, actual)
// verify the file was actually opened // 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.True(t, ok)
require.Equal(t, pathName, f.Path) require.Equal(t, pathName, f.Path)
} }
t.Run("snapshotPreview1.PathOpen", func(t *testing.T) { t.Run("snapshotPreview1.PathOpen", func(t *testing.T) {
a, mod, _ := setup() workdirFD := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err
errno := a.PathOpen(testCtx, mod, workdirFD, dirflags, path, pathLen, oflags, fsRightsBase, fsRightsInheriting, fdFlags, resultOpenedFd) pathName := "wazero"
verify(errno, mod)
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) { t.Run(functionPathOpen, func(t *testing.T) {
_, mod, fn := setup() workdirFD := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err
results, err := fn.Call(testCtx, uint64(workdirFD), uint64(dirflags), uint64(path), uint64(pathLen), uint64(oflags), fsRightsBase, fsRightsInheriting, uint64(fdFlags), uint64(resultOpenedFd)) 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) require.NoError(t, err)
errno := Errno(results[0]) 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" pathName := "wazero"
testFS := fstest.MapFS{pathName: &fstest.MapFile{Mode: os.ModeDir}} 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}, validFD: {Path: ".", FS: testFS},
}) })
require.NoError(t, err) require.NoError(t, err)
@@ -2076,7 +2124,7 @@ func instantiateModule(ctx context.Context, t *testing.T, wasifunction, wasiimpo
return a, mod, fn 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) return wasm.NewSysContext(math.MaxUint32, args, environ, new(bytes.Buffer), nil, nil, nil, openedFiles)
} }