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:
52
config.go
52
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
1
examples/wasi/testdata/sub/test.txt
vendored
Normal file
1
examples/wasi/testdata/sub/test.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
greet sub dir
|
||||
21
experimental/fs.go
Normal file
21
experimental/fs.go
Normal 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
41
experimental/fs_test.go
Normal 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
172
internal/fs/fs.go
Normal 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
49
internal/fs/fs_test.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
55
wasi/wasi.go
55
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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user