Removes WithWorkDirFS and "." resolution (#660)
This removes WithWorkDirFS and any other attempts to resolve the current directory (".") in host functions. This is a reaction to reality of compilers who track this inside wasm (not via host functions). One nice side effect is substantially simpler internal implementation of file-systems.
This also allows experimental.WithFS to block file access via passing nil.
Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
28
RATIONALE.md
28
RATIONALE.md
@@ -379,6 +379,34 @@ file descriptors if resources aren't managed properly. In other words, blind cop
|
||||
violate isolation or endanger the embedding process. In summary, we try to be similar to normal Go code, but often need
|
||||
act differently and document `ModuleConfig` is more about emulating, not necessarily performing real system calls.
|
||||
|
||||
## File systems
|
||||
|
||||
### Why doesn't wazero implement the working directory?
|
||||
|
||||
An early design of wazero's API included a `WithWorkDirFS` which allowed
|
||||
control over which file a relative path such as "./config.yml" resolved to,
|
||||
independent of the root file system. This intended to help separate concerns
|
||||
like mutability of files, but it didn't work and was removed.
|
||||
|
||||
Compilers that target wasm act differently with regard to the working
|
||||
directory. For example, while `GOOS=js` uses host functions to track the
|
||||
working directory, WASI host functions do not. wasi-libc, used by TinyGo,
|
||||
tracks working directory changes in compiled wasm instead: initially "/" until
|
||||
code calls `chdir`.
|
||||
|
||||
The only place wazero can standardize a layered concern is via a host function.
|
||||
Since WASI doesn't use host functions to track the working directory, we can't
|
||||
standardize the storage and initial value of it.
|
||||
|
||||
Meanwhile, code may be able to affect the working directory by compiling
|
||||
`chdir` into their main function, using an argument or ENV for the initial
|
||||
value (possibly `PWD`). Those unable to control the compiled code should only
|
||||
use absolute paths in configuration.
|
||||
|
||||
See
|
||||
* https://github.com/golang/go/blob/go1.19beta1/src/syscall/fs_js.go#L324
|
||||
* https://github.com/WebAssembly/wasi-libc/pull/214#issue-673090117
|
||||
|
||||
### FdPrestatDirName
|
||||
|
||||
`FdPrestatDirName` is a WASI function to return the path of the pre-opened directory of a file descriptor.
|
||||
|
||||
@@ -57,12 +57,12 @@ type Builder interface {
|
||||
|
||||
// Compile compiles the "env" module that can instantiated in any namespace (wazero.Namespace).
|
||||
//
|
||||
// Note: This has the same effect as the same function name on wazero.ModuleBuilder.
|
||||
// Note: This has the same effect as the same function on wazero.ModuleBuilder.
|
||||
Compile(context.Context, wazero.CompileConfig) (wazero.CompiledModule, error)
|
||||
|
||||
// Instantiate instantiates the "env" module into the provided namespace.
|
||||
//
|
||||
// Note: This has the same effect as the same function name on wazero.ModuleBuilder.
|
||||
// Note: This has the same effect as the same function on wazero.ModuleBuilder.
|
||||
Instantiate(context.Context, wazero.Namespace) (api.Closer, error)
|
||||
}
|
||||
|
||||
|
||||
58
config.go
58
config.go
@@ -394,10 +394,10 @@ type ModuleConfig interface {
|
||||
// See https://linux.die.net/man/3/environ and https://en.wikipedia.org/wiki/Null-terminated_string
|
||||
WithEnv(key, value string) ModuleConfig
|
||||
|
||||
// WithFS assigns the file system to use for any paths beginning at "/". Defaults to not found.
|
||||
// Note: This sets WithWorkDirFS to the same file-system unless already set.
|
||||
// WithFS assigns the file system to use for any paths beginning at "/".
|
||||
// Defaults return fs.ErrNotExist.
|
||||
//
|
||||
// Ex. This sets a read-only, embedded file-system to serve files under the root ("/") and working (".") directories:
|
||||
// Ex. This sets a read-only, embedded file-system:
|
||||
//
|
||||
// //go:embed testdata/index.html
|
||||
// var testdataIndex embed.FS
|
||||
@@ -405,9 +405,24 @@ type ModuleConfig interface {
|
||||
// rooted, err := fs.Sub(testdataIndex, "testdata")
|
||||
// require.NoError(t, err)
|
||||
//
|
||||
// // "index.html" is accessible as both "/index.html" and "./index.html" because we didn't use WithWorkDirFS.
|
||||
// // "index.html" is accessible as "/index.html".
|
||||
// config := wazero.NewModuleConfig().WithFS(rooted)
|
||||
//
|
||||
// Ex. This sets a mutable file-system:
|
||||
//
|
||||
// // Files relative to "/work/appA" are accessible as "/".
|
||||
// config := wazero.NewModuleConfig().WithFS(os.DirFS("/work/appA"))
|
||||
//
|
||||
// Isolation
|
||||
//
|
||||
// os.DirFS documentation includes important notes about isolation, which
|
||||
// also applies to fs.Sub. As of Go 1.19, the built-in file-systems are not
|
||||
// jailed (chroot). See https://github.com/golang/go/issues/42322
|
||||
//
|
||||
// Working Directory "."
|
||||
//
|
||||
// Relative path resolution, such as "./config.yml" to "/config.yml" or
|
||||
// otherwise, is compiler-specific. See /RATIONALE.md for notes.
|
||||
WithFS(fs.FS) ModuleConfig
|
||||
|
||||
// WithName configures the module name. Defaults to what was decoded or overridden via CompileConfig.WithModuleName.
|
||||
@@ -531,20 +546,6 @@ type ModuleConfig interface {
|
||||
//
|
||||
// Note: The caller is responsible to close any io.Reader they supply: It is not closed on api.Module Close.
|
||||
WithRandSource(io.Reader) ModuleConfig
|
||||
|
||||
// WithWorkDirFS indicates the file system to use for any paths beginning at "./". Defaults to the same as WithFS.
|
||||
//
|
||||
// Ex. This sets a read-only, embedded file-system as the root ("/"), and a mutable one as the working directory ("."):
|
||||
//
|
||||
// //go:embed appA
|
||||
// var rootFS embed.FS
|
||||
//
|
||||
// // Files relative to this source under appA are available under "/" and files relative to "/work/appA" under ".".
|
||||
// config := wazero.NewModuleConfig().WithFS(rootFS).WithWorkDirFS(os.DirFS("/work/appA"))
|
||||
//
|
||||
// Note: os.DirFS documentation includes important notes about isolation, which also applies to fs.Sub. As of Go 1.18,
|
||||
// the built-in file-systems are not jailed (chroot). See https://github.com/golang/go/issues/42322
|
||||
WithWorkDirFS(fs.FS) ModuleConfig
|
||||
}
|
||||
|
||||
type moduleConfig struct {
|
||||
@@ -564,7 +565,8 @@ type moduleConfig struct {
|
||||
environ []string
|
||||
// environKeys allow overwriting of existing values.
|
||||
environKeys map[string]int
|
||||
fs *internalsys.FSConfig
|
||||
// fs is the file system to open files with
|
||||
fs fs.FS
|
||||
}
|
||||
|
||||
// NewModuleConfig returns a ModuleConfig that can be used for configuring module instantiation.
|
||||
@@ -572,7 +574,6 @@ func NewModuleConfig() ModuleConfig {
|
||||
return &moduleConfig{
|
||||
startFunctions: []string{"_start"},
|
||||
environKeys: map[string]int{},
|
||||
fs: internalsys.NewFSConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,7 +584,6 @@ func (c *moduleConfig) clone() *moduleConfig {
|
||||
for key, value := range c.environKeys {
|
||||
ret.environKeys[key] = value
|
||||
}
|
||||
ret.fs = c.fs.Clone()
|
||||
return &ret
|
||||
}
|
||||
|
||||
@@ -610,7 +610,7 @@ func (c *moduleConfig) WithEnv(key, value string) ModuleConfig {
|
||||
// WithFS implements ModuleConfig.WithFS
|
||||
func (c *moduleConfig) WithFS(fs fs.FS) ModuleConfig {
|
||||
ret := c.clone()
|
||||
ret.fs = ret.fs.WithFS(fs)
|
||||
ret.fs = fs
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -698,13 +698,6 @@ func (c *moduleConfig) WithRandSource(source io.Reader) ModuleConfig {
|
||||
return ret
|
||||
}
|
||||
|
||||
// WithWorkDirFS implements ModuleConfig.WithWorkDirFS
|
||||
func (c *moduleConfig) WithWorkDirFS(fs fs.FS) ModuleConfig {
|
||||
ret := c.clone()
|
||||
ret.fs = ret.fs.WithWorkDirFS(fs)
|
||||
return ret
|
||||
}
|
||||
|
||||
// toSysContext creates a baseline wasm.Context configured by ModuleConfig.
|
||||
func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) {
|
||||
var environ []string // Intentionally doesn't pre-allocate to reduce logic to default to nil.
|
||||
@@ -724,11 +717,6 @@ func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) {
|
||||
environ = append(environ, key+"="+value)
|
||||
}
|
||||
|
||||
preopens, err := c.fs.Preopens()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return internalsys.NewContext(
|
||||
math.MaxUint32,
|
||||
c.args,
|
||||
@@ -740,6 +728,6 @@ func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) {
|
||||
c.walltime, c.walltimeResolution,
|
||||
c.nanotime, c.nanotimeResolution,
|
||||
c.nanosleep,
|
||||
preopens,
|
||||
c.fs,
|
||||
)
|
||||
}
|
||||
|
||||
115
config_test.go
115
config_test.go
@@ -3,6 +3,7 @@ package wazero
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
internalsys "github.com/tetratelabs/wazero/internal/sys"
|
||||
testfs "github.com/tetratelabs/wazero/internal/testing/fs"
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
"github.com/tetratelabs/wazero/internal/wasm"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
@@ -304,8 +306,8 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
base.(*moduleConfig).nanotime = &nt
|
||||
base.(*moduleConfig).nanotimeResolution = 1
|
||||
|
||||
testFS := fstest.MapFS{}
|
||||
testFS2 := fstest.MapFS{}
|
||||
testFS := testfs.FS{}
|
||||
testFS2 := testfs.FS{"/": &testfs.File{}}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -326,7 +328,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
&wt, 1, // walltime, walltimeResolution
|
||||
&nt, 1, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
nil, // openedFiles
|
||||
nil, // fs
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -343,7 +345,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
&wt, 1, // walltime, walltimeResolution
|
||||
&nt, 1, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
nil, // openedFiles
|
||||
nil, // fs
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -360,7 +362,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
&wt, 1, // walltime, walltimeResolution
|
||||
&nt, 1, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
nil, // openedFiles
|
||||
nil, // fs
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -377,7 +379,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
&wt, 1, // walltime, walltimeResolution
|
||||
&nt, 1, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
nil, // openedFiles
|
||||
nil, // fs
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -394,7 +396,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
&wt, 1, // walltime, walltimeResolution
|
||||
&nt, 1, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
nil, // openedFiles
|
||||
nil, // fs
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -411,7 +413,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
&wt, 1, // walltime, walltimeResolution
|
||||
&nt, 1, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
nil, // openedFiles
|
||||
nil, // fs
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -428,7 +430,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
&wt, 1, // walltime, walltimeResolution
|
||||
&nt, 1, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
nil, // openedFiles
|
||||
nil, // fs
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -445,7 +447,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
&wt, 1, // walltime, walltimeResolution
|
||||
&nt, 1, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
nil, // openedFiles
|
||||
nil, // fs
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -462,7 +464,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
&wt, 1, // walltime, walltimeResolution
|
||||
&nt, 1, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
nil, // openedFiles
|
||||
nil, // fs
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -479,10 +481,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
&wt, 1, // walltime, walltimeResolution
|
||||
&nt, 1, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
map[uint32]*internalsys.FileEntry{ // openedFiles
|
||||
3: {Path: "/", FS: testFS},
|
||||
4: {Path: ".", FS: testFS},
|
||||
},
|
||||
testFS,
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -499,69 +498,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
&wt, 1, // walltime, walltimeResolution
|
||||
&nt, 1, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
map[uint32]*internalsys.FileEntry{ // openedFiles
|
||||
3: {Path: "/", FS: testFS2},
|
||||
4: {Path: ".", FS: testFS2},
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "WithWorkDirFS",
|
||||
input: base.WithWorkDirFS(testFS),
|
||||
expected: requireSysContext(t,
|
||||
math.MaxUint32, // max
|
||||
nil, // args
|
||||
nil, // environ
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
&wt, 1, // walltime, walltimeResolution
|
||||
&nt, 1, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
map[uint32]*internalsys.FileEntry{ // openedFiles
|
||||
3: {Path: ".", FS: testFS},
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "WithFS and WithWorkDirFS",
|
||||
input: base.WithFS(testFS).WithWorkDirFS(testFS2),
|
||||
expected: requireSysContext(t,
|
||||
math.MaxUint32, // max
|
||||
nil, // args
|
||||
nil, // environ
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
&wt, 1, // walltime, walltimeResolution
|
||||
&nt, 1, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
map[uint32]*internalsys.FileEntry{ // openedFiles
|
||||
3: {Path: "/", FS: testFS},
|
||||
4: {Path: ".", FS: testFS2},
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "WithWorkDirFS and WithFS",
|
||||
input: base.WithWorkDirFS(testFS).WithFS(testFS2),
|
||||
expected: requireSysContext(t,
|
||||
math.MaxUint32, // max
|
||||
nil, // args
|
||||
nil, // environ
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
&wt, 1, // walltime, walltimeResolution
|
||||
&nt, 1, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
map[uint32]*internalsys.FileEntry{ // openedFiles
|
||||
3: {Path: ".", FS: testFS},
|
||||
4: {Path: "/", FS: testFS2},
|
||||
},
|
||||
testFS2, // fs
|
||||
),
|
||||
},
|
||||
}
|
||||
@@ -764,16 +701,6 @@ func TestModuleConfig_toSysContext_Errors(t *testing.T) {
|
||||
input: NewModuleConfig().WithEnv("", "a"),
|
||||
expectedErr: "environ invalid: empty key",
|
||||
},
|
||||
{
|
||||
name: "WithFS nil",
|
||||
input: NewModuleConfig().WithFS(nil),
|
||||
expectedErr: "FS for / is nil",
|
||||
},
|
||||
{
|
||||
name: "WithWorkDirFS nil",
|
||||
input: NewModuleConfig().WithWorkDirFS(nil),
|
||||
expectedErr: "FS for . is nil",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
@@ -788,8 +715,8 @@ func TestModuleConfig_clone(t *testing.T) {
|
||||
mc := NewModuleConfig().(*moduleConfig)
|
||||
cloned := mc.clone()
|
||||
|
||||
fs1 := fstest.MapFS{}
|
||||
mc.fs.WithWorkDirFS(fs1)
|
||||
// Make post-clone changes
|
||||
mc.fs = fstest.MapFS{}
|
||||
mc.environKeys["2"] = 2
|
||||
|
||||
cloned.environKeys["1"] = 1
|
||||
@@ -799,9 +726,7 @@ func TestModuleConfig_clone(t *testing.T) {
|
||||
require.Equal(t, map[string]int{"1": 1}, cloned.environKeys)
|
||||
|
||||
// Ensure the fs is not shared
|
||||
preopens, err := cloned.fs.Preopens()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[uint32]*internalsys.FileEntry{}, preopens)
|
||||
require.Nil(t, cloned.fs)
|
||||
}
|
||||
|
||||
func TestCompiledCode_Close(t *testing.T) {
|
||||
@@ -839,7 +764,7 @@ func requireSysContext(
|
||||
walltime *sys.Walltime, walltimeResolution sys.ClockResolution,
|
||||
nanotime *sys.Nanotime, nanotimeResolution sys.ClockResolution,
|
||||
nanosleep *sys.Nanosleep,
|
||||
openedFiles map[uint32]*internalsys.FileEntry,
|
||||
fs fs.FS,
|
||||
) *internalsys.Context {
|
||||
sysCtx, err := internalsys.NewContext(
|
||||
max,
|
||||
@@ -852,7 +777,7 @@ func requireSysContext(
|
||||
walltime, walltimeResolution,
|
||||
nanotime, nanotimeResolution,
|
||||
nanosleep,
|
||||
openedFiles,
|
||||
fs,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return sysCtx
|
||||
|
||||
@@ -53,8 +53,7 @@ func main() {
|
||||
}
|
||||
|
||||
// InstantiateModule runs the "_start" function which is what TinyGo compiles "main" to.
|
||||
// * Set the program name (arg[0]) to "wasi" and add args to write "test.txt" to stdout twice.
|
||||
// * We use "/test.txt" or "./test.txt" because WithFS by default maps the workdir "." to "/".
|
||||
// * Set the program name (arg[0]) to "wasi" and add args to write "/test.txt" to stdout twice.
|
||||
if _, err = r.InstantiateModule(ctx, code, config.WithArgs("wasi", os.Args[1])); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
|
||||
// Test_main ensures the following will work:
|
||||
//
|
||||
// go run cat.go ./test.txt
|
||||
// go run cat.go /test.txt
|
||||
func Test_main(t *testing.T) {
|
||||
stdout, _ := maintester.TestMain(t, main, "cat", "./test.txt")
|
||||
stdout, _ := maintester.TestMain(t, main, "cat", "/test.txt")
|
||||
require.Equal(t, "greet filesystem\n", stdout)
|
||||
}
|
||||
|
||||
@@ -8,16 +8,14 @@ import (
|
||||
internalfs "github.com/tetratelabs/wazero/internal/sys"
|
||||
)
|
||||
|
||||
// WithFS overrides fs.FS in the context-based manner. Caller needs to take responsibility for closing the filesystem.
|
||||
// WithFS overrides fs.FS in the context-based manner. Caller needs to take
|
||||
// responsibility for closing the filesystem.
|
||||
//
|
||||
// Note: This has the same effect as the same function name on wazero.ModuleConfig.
|
||||
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
|
||||
// Note: This has the same effect as the same function on wazero.ModuleConfig.
|
||||
func WithFS(ctx context.Context, fs fs.FS) (context.Context, api.Closer) {
|
||||
if fs == nil {
|
||||
fs = internalfs.EmptyFS
|
||||
}
|
||||
|
||||
fsCtx := internalfs.NewFSContext(preopens)
|
||||
return context.WithValue(ctx, internalfs.FSKey{}, fsCtx), fsCtx, nil
|
||||
fsCtx := internalfs.NewFSContext(fs)
|
||||
return context.WithValue(ctx, internalfs.FSKey{}, fsCtx), fsCtx
|
||||
}
|
||||
|
||||
@@ -37,10 +37,7 @@ func Example_withFS() {
|
||||
|
||||
// Setup the filesystem overlay, noting that it can fail if the directory is
|
||||
// invalid and must be closed.
|
||||
ctx, closer, err := experimental.WithFS(ctx, os.DirFS("."))
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
ctx, closer := experimental.WithFS(ctx, os.DirFS("."))
|
||||
defer closer.Close(ctx)
|
||||
|
||||
fdPrestatDirName := mod.ExportedFunction("fd_prestat_dir_name")
|
||||
|
||||
@@ -3,7 +3,6 @@ package experimental_test
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"log"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
@@ -18,10 +17,7 @@ func TestWithFS(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
ctx, closer := experimental.WithFS(context.Background(), mapfs)
|
||||
defer closer.Close(ctx)
|
||||
|
||||
v := ctx.Value(sys.FSKey{})
|
||||
@@ -29,13 +25,19 @@ func TestWithFS(t *testing.T) {
|
||||
fsCtx, ok := v.(*sys.FSContext)
|
||||
require.True(t, ok)
|
||||
|
||||
entry, ok := fsCtx.OpenedFile(3)
|
||||
entry, ok := fsCtx.OpenedFile(ctx, 3)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "/", entry.Path)
|
||||
require.Equal(t, mapfs, entry.FS)
|
||||
|
||||
entry, ok = fsCtx.OpenedFile(4)
|
||||
// Override to nil context, ex to block file access
|
||||
ctx, closer = experimental.WithFS(ctx, nil)
|
||||
defer closer.Close(ctx)
|
||||
|
||||
v = ctx.Value(sys.FSKey{})
|
||||
require.NotNil(t, v)
|
||||
fsCtx, ok = v.(*sys.FSContext)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, ".", entry.Path)
|
||||
require.Equal(t, mapfs, entry.FS)
|
||||
|
||||
_, ok = fsCtx.OpenedFile(ctx, 3)
|
||||
require.False(t, ok)
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ func TestReader(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
realFs := fstest.MapFS{"animals.txt": &fstest.MapFile{Data: animals}}
|
||||
sys := wazero.NewModuleConfig().WithWorkDirFS(realFs)
|
||||
sys := wazero.NewModuleConfig().WithFS(realFs)
|
||||
|
||||
// Create a module that just delegates to wasi functions.
|
||||
compiled, err := r.CompileModule(testCtx, fsWasm, wazero.NewCompileConfig())
|
||||
|
||||
@@ -385,7 +385,7 @@ func addSpectestModule(t *testing.T, s *wasm.Store, ns *wasm.Namespace) {
|
||||
err = s.Engine.CompileModule(testCtx, mod)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = s.Instantiate(testCtx, ns, mod, mod.NameSection.ModuleName, sys.DefaultContext(), nil)
|
||||
_, err = s.Instantiate(testCtx, ns, mod, mod.NameSection.ModuleName, sys.DefaultContext(nil), nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"math"
|
||||
"sync/atomic"
|
||||
@@ -13,15 +13,33 @@ import (
|
||||
// See https://github.com/tetratelabs/wazero/issues/491
|
||||
type FSKey struct{}
|
||||
|
||||
// EmptyFS is exported to special-case an empty file system.
|
||||
var EmptyFS = &emptyFS{}
|
||||
|
||||
type emptyFS struct{}
|
||||
|
||||
// compile-time check to ensure emptyFS implements fs.FS
|
||||
var _ fs.FS = &emptyFS{}
|
||||
|
||||
// Open implements the same method as documented on fs.FS.
|
||||
func (f *emptyFS) Open(name string) (fs.File, error) {
|
||||
if !fs.ValidPath(name) {
|
||||
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
|
||||
}
|
||||
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
||||
}
|
||||
|
||||
// FileEntry maps a path to an open file in a file system.
|
||||
type FileEntry struct {
|
||||
Path string
|
||||
FS fs.FS
|
||||
// File when nil this is a mount like "." or "/".
|
||||
// File when nil this is the root "/" (fd=3)
|
||||
File fs.File
|
||||
}
|
||||
|
||||
type FSContext struct {
|
||||
// fs is the root ("/") mount.
|
||||
fs fs.FS
|
||||
|
||||
// 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
|
||||
@@ -30,25 +48,31 @@ type FSContext struct {
|
||||
lastFD uint32
|
||||
}
|
||||
|
||||
func NewFSContext(openedFiles map[uint32]*FileEntry) *FSContext {
|
||||
var fsCtx FSContext
|
||||
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
|
||||
// emptyFSContext is the context associated with EmptyFS.
|
||||
//
|
||||
// Note: This is not mutable as operations functions do not affect field state.
|
||||
var emptyFSContext = &FSContext{
|
||||
fs: EmptyFS,
|
||||
openedFiles: map[uint32]*FileEntry{},
|
||||
lastFD: 2,
|
||||
}
|
||||
|
||||
// NewFSContext returns a mutable context if the fs is not EmptyFS.
|
||||
func NewFSContext(fs fs.FS) *FSContext {
|
||||
if fs == EmptyFS {
|
||||
return emptyFSContext
|
||||
}
|
||||
return &FSContext{
|
||||
fs: fs,
|
||||
openedFiles: map[uint32]*FileEntry{
|
||||
3: {Path: "/"}, // after STDERR
|
||||
},
|
||||
lastFD: 3,
|
||||
}
|
||||
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: openedFiles 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 *FSContext) nextFD() uint32 {
|
||||
if c.lastFD == math.MaxUint32 {
|
||||
@@ -57,22 +81,40 @@ func (c *FSContext) nextFD() uint32 {
|
||||
return atomic.AddUint32(&c.lastFD, 1)
|
||||
}
|
||||
|
||||
// Close implements io.Closer
|
||||
func (c *FSContext) 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.
|
||||
// OpenedFile returns a file and true if it was opened or nil and false, if not.
|
||||
func (c *FSContext) OpenedFile(_ context.Context, fd uint32) (*FileEntry, bool) {
|
||||
f, ok := c.openedFiles[fd]
|
||||
return f, ok
|
||||
}
|
||||
|
||||
// OpenFile is like syscall.Open and returns the file descriptor of the new file or an error.
|
||||
//
|
||||
// TODO: Consider dirflags and oflags. Also, allow non-read-only open based on config about the mount.
|
||||
// Ex. allow os.O_RDONLY, os.O_WRONLY, or os.O_RDWR either by config flag or pattern on filename
|
||||
// See #390
|
||||
func (c *FSContext) OpenFile(_ context.Context, name string /* TODO: flags int, perm int */) (uint32, error) {
|
||||
// fs.ValidFile cannot start with '/'
|
||||
fsOpenPath := name
|
||||
if name[0] == '/' {
|
||||
fsOpenPath = name[1:]
|
||||
}
|
||||
|
||||
f, err := c.fs.Open(fsOpenPath)
|
||||
if err != nil {
|
||||
return 0, err // Don't wrap the underlying error which is already a PathError!
|
||||
}
|
||||
return
|
||||
|
||||
newFD := c.nextFD()
|
||||
if newFD == 0 {
|
||||
_ = f.Close()
|
||||
return 0, &fs.PathError{Op: "open", Path: name, Err: errors.New("out of file descriptors")}
|
||||
}
|
||||
c.openedFiles[newFD] = &FileEntry{Path: name, File: f}
|
||||
return newFD, nil
|
||||
}
|
||||
|
||||
// CloseFile returns true if a file was opened and closed without error, or false if not.
|
||||
func (c *FSContext) CloseFile(fd uint32) (bool, error) {
|
||||
func (c *FSContext) CloseFile(_ context.Context, fd uint32) (bool, error) {
|
||||
f, ok := c.openedFiles[fd]
|
||||
if !ok {
|
||||
return false, nil
|
||||
@@ -88,97 +130,16 @@ func (c *FSContext) CloseFile(fd uint32) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// OpenedFile returns a file and true if it was opened or nil and false, if not.
|
||||
func (c *FSContext) 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 *FSContext) 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{},
|
||||
// Close implements io.Closer
|
||||
func (c *FSContext) 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 the root filesystem
|
||||
if e := entry.File.Close(); e != nil {
|
||||
err = e // This means the err returned == the last non-nil error.
|
||||
}
|
||||
}
|
||||
|
||||
// Clone makes a deep copy of this FS config.
|
||||
func (c *FSConfig) Clone() *FSConfig {
|
||||
ret := *c // copy except maps which share a ref
|
||||
ret.preopens = make(map[uint32]*FileEntry, len(c.preopens))
|
||||
for key, value := range c.preopens {
|
||||
ret.preopens[key] = value
|
||||
}
|
||||
ret.preopenPaths = make(map[string]uint32, len(c.preopenPaths))
|
||||
for key, value := range c.preopenPaths {
|
||||
ret.preopenPaths[key] = value
|
||||
}
|
||||
return &ret
|
||||
}
|
||||
|
||||
// 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.Clone()
|
||||
ret.setFS("/", fs)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *FSConfig) WithWorkDirFS(fs fs.FS) *FSConfig {
|
||||
ret := c.Clone()
|
||||
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
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,25 +3,62 @@ package sys
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
testfs "github.com/tetratelabs/wazero/internal/testing/fs"
|
||||
"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) {
|
||||
pathName := "test"
|
||||
file := &testFile{}
|
||||
func TestEmptyFS(t *testing.T) {
|
||||
testFS := EmptyFS
|
||||
|
||||
fsc := NewFSContext(map[uint32]*FileEntry{
|
||||
3: {Path: "."},
|
||||
4: {Path: path.Join(".", pathName), File: file},
|
||||
t.Run("validates path", func(t *testing.T) {
|
||||
f, err := testFS.Open("/foo.txt")
|
||||
require.Nil(t, f)
|
||||
require.EqualError(t, err, "open /foo.txt: invalid argument")
|
||||
})
|
||||
|
||||
t.Run("path not found", func(t *testing.T) {
|
||||
f, err := testFS.Open("foo.txt")
|
||||
require.Nil(t, f)
|
||||
require.EqualError(t, err, "open foo.txt: file does not exist")
|
||||
})
|
||||
}
|
||||
|
||||
func TestEmptyFSContext(t *testing.T) {
|
||||
testFS := emptyFSContext
|
||||
expected := &FSContext{
|
||||
fs: EmptyFS,
|
||||
openedFiles: map[uint32]*FileEntry{},
|
||||
lastFD: 2,
|
||||
}
|
||||
|
||||
t.Run("OpenFile doesn't affect state", func(t *testing.T) {
|
||||
fd, err := testFS.OpenFile(testCtx, "foo.txt")
|
||||
require.Zero(t, fd)
|
||||
require.EqualError(t, err, "open foo.txt: file does not exist")
|
||||
|
||||
// Ensure this didn't modify state
|
||||
require.Equal(t, expected, testFS)
|
||||
})
|
||||
|
||||
t.Run("Close doesn't affect state", func(t *testing.T) {
|
||||
err := testFS.Close(testCtx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Ensure this didn't modify state
|
||||
require.Equal(t, expected, testFS)
|
||||
})
|
||||
}
|
||||
|
||||
func TestContext_Close(t *testing.T) {
|
||||
fsc := NewFSContext(testfs.FS{"foo": &testfs.File{}})
|
||||
_, err := fsc.OpenFile(testCtx, "/foo")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify base case
|
||||
require.True(t, len(fsc.openedFiles) > 0, "fsc.openedFiles was empty")
|
||||
|
||||
@@ -36,41 +73,13 @@ func TestContext_Close(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestContext_Close_Error(t *testing.T) {
|
||||
file := &testFile{errors.New("error closing")}
|
||||
file := &testfs.File{CloseErr: errors.New("error closing")}
|
||||
fsc := NewFSContext(testfs.FS{"foo": file})
|
||||
_, err := fsc.OpenFile(testCtx, "/foo")
|
||||
require.NoError(t, err)
|
||||
|
||||
fsc := NewFSContext(map[uint32]*FileEntry{
|
||||
3: {Path: ".", File: file},
|
||||
4: {Path: "/", File: file},
|
||||
})
|
||||
require.EqualError(t, fsc.Close(testCtx), "error closing")
|
||||
|
||||
// Paths should clear even under error
|
||||
require.Zero(t, len(fsc.openedFiles), "expected no opened files")
|
||||
}
|
||||
|
||||
// compile-time check to ensure testFile implements fs.File
|
||||
var _ fs.File = &testFile{}
|
||||
|
||||
type testFile struct{ closeErr error }
|
||||
|
||||
func (f *testFile) Close() error { return f.closeErr }
|
||||
func (f *testFile) Stat() (fs.FileInfo, error) { return nil, nil }
|
||||
func (f *testFile) Read(_ []byte) (int, error) { return 0, nil }
|
||||
func (f *testFile) Seek(_ int64, _ int) (int64, error) { return 0, nil }
|
||||
|
||||
func TestFSConfig_Clone(t *testing.T) {
|
||||
fsc := NewFSConfig()
|
||||
cloned := fsc.Clone()
|
||||
|
||||
fsc.preopens[2] = nil
|
||||
fsc.preopenPaths["2"] = 2
|
||||
|
||||
cloned.preopens[1] = nil
|
||||
cloned.preopenPaths["1"] = 1
|
||||
|
||||
// Ensure the maps are not shared
|
||||
require.Equal(t, map[uint32]*FileEntry{2: nil}, fsc.preopens)
|
||||
require.Equal(t, map[string]uint32{"2": 2}, fsc.preopenPaths)
|
||||
require.Equal(t, map[uint32]*FileEntry{1: nil}, cloned.preopens)
|
||||
require.Equal(t, map[string]uint32{"1": 1}, cloned.preopenPaths)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"time"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
@@ -28,8 +29,7 @@ type Context struct {
|
||||
nanotimeResolution sys.ClockResolution
|
||||
nanosleep *sys.Nanosleep
|
||||
randSource io.Reader
|
||||
|
||||
fs *FSContext
|
||||
fsc *FSContext
|
||||
}
|
||||
|
||||
// Args is like os.Args and defaults to nil.
|
||||
@@ -109,7 +109,7 @@ func (c *Context) Nanosleep(ctx context.Context, ns int64) {
|
||||
(*(c.nanosleep))(ctx, ns)
|
||||
}
|
||||
|
||||
// FS returns the file system context.
|
||||
// FS returns a possibly nil file system context.
|
||||
func (c *Context) FS(ctx context.Context) *FSContext {
|
||||
// Override Context when it is passed via context
|
||||
if fsValue := ctx.Value(FSKey{}); fsValue != nil {
|
||||
@@ -119,7 +119,7 @@ func (c *Context) FS(ctx context.Context) *FSContext {
|
||||
}
|
||||
return fsCtx
|
||||
}
|
||||
return c.fs
|
||||
return c.fsc
|
||||
}
|
||||
|
||||
// RandSource is a source of random bytes and defaults to crypto/rand.Reader.
|
||||
@@ -137,19 +137,16 @@ func (eofReader) Read([]byte) (int, error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
// DefaultContext returns Context with no values set.
|
||||
//
|
||||
// Note: This isn't a constant because Context.openedFiles is currently mutable even when empty.
|
||||
// TODO: Make it an error to open or close files when no FS was assigned.
|
||||
func DefaultContext() *Context {
|
||||
if sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil, nil); err != nil {
|
||||
// DefaultContext returns Context with no values set except a possibly nil fs.FS
|
||||
func DefaultContext(fs fs.FS) *Context {
|
||||
if sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil, fs); err != nil {
|
||||
panic(fmt.Errorf("BUG: DefaultContext should never error: %w", err))
|
||||
} else {
|
||||
return sysCtx
|
||||
}
|
||||
}
|
||||
|
||||
var _ = DefaultContext() // Force panic on bug.
|
||||
var _ = DefaultContext(nil) // Force panic on bug.
|
||||
var ns sys.Nanosleep = platform.FakeNanosleep
|
||||
|
||||
// NewContext is a factory function which helps avoid needing to know defaults or exporting all fields.
|
||||
@@ -163,7 +160,7 @@ func NewContext(
|
||||
walltime *sys.Walltime, walltimeResolution sys.ClockResolution,
|
||||
nanotime *sys.Nanotime, nanotimeResolution sys.ClockResolution,
|
||||
nanosleep *sys.Nanosleep,
|
||||
openedFiles map[uint32]*FileEntry,
|
||||
fs fs.FS,
|
||||
) (sysCtx *Context, err error) {
|
||||
sysCtx = &Context{args: args, environ: environ}
|
||||
|
||||
@@ -227,7 +224,11 @@ func NewContext(
|
||||
sysCtx.nanosleep = &ns
|
||||
}
|
||||
|
||||
sysCtx.fs = NewFSContext(openedFiles)
|
||||
if fs != nil {
|
||||
sysCtx.fsc = NewFSContext(fs)
|
||||
} else {
|
||||
sysCtx.fsc = NewFSContext(EmptyFS)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
testfs "github.com/tetratelabs/wazero/internal/testing/fs"
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
@@ -25,7 +26,7 @@ func TestDefaultSysContext(t *testing.T) {
|
||||
nil, 0, // walltime, walltimeResolution
|
||||
nil, 0, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
nil, // openedFiles
|
||||
testfs.FS{}, // fs
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -45,7 +46,7 @@ func TestDefaultSysContext(t *testing.T) {
|
||||
require.Equal(t, sys.ClockResolution(1), sysCtx.NanotimeResolution())
|
||||
require.Equal(t, &ns, sysCtx.nanosleep)
|
||||
require.Equal(t, rand.Reader, sysCtx.RandSource())
|
||||
require.Equal(t, NewFSContext(map[uint32]*FileEntry{}), sysCtx.FS(testCtx))
|
||||
require.Equal(t, NewFSContext(testfs.FS{}), sysCtx.FS(testCtx))
|
||||
}
|
||||
|
||||
func TestNewContext_Args(t *testing.T) {
|
||||
@@ -97,7 +98,7 @@ func TestNewContext_Args(t *testing.T) {
|
||||
nil, 0, // walltime, walltimeResolution
|
||||
nil, 0, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
nil, // openedFiles
|
||||
nil, // fs
|
||||
)
|
||||
if tc.expectedErr == "" {
|
||||
require.Nil(t, err)
|
||||
@@ -159,7 +160,7 @@ func TestNewContext_Environ(t *testing.T) {
|
||||
nil, 0, // walltime, walltimeResolution
|
||||
nil, 0, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
nil, // openedFiles
|
||||
nil, // fs
|
||||
)
|
||||
if tc.expectedErr == "" {
|
||||
require.Nil(t, err)
|
||||
@@ -207,7 +208,7 @@ func TestNewContext_Walltime(t *testing.T) {
|
||||
tc.time, tc.resolution, // walltime, walltimeResolution
|
||||
nil, 0, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
nil, // openedFiles
|
||||
nil, // fs
|
||||
)
|
||||
if tc.expectedErr == "" {
|
||||
require.Nil(t, err)
|
||||
@@ -255,7 +256,7 @@ func TestNewContext_Nanotime(t *testing.T) {
|
||||
nil, 0, // nanotime, nanotimeResolution
|
||||
tc.time, tc.resolution, // nanotime, nanotimeResolution
|
||||
nil, // nanosleep
|
||||
nil, // openedFiles
|
||||
nil, // fs
|
||||
)
|
||||
if tc.expectedErr == "" {
|
||||
require.Nil(t, err)
|
||||
@@ -313,7 +314,7 @@ func TestNewContext_Nanosleep(t *testing.T) {
|
||||
nil, 0, // Nanosleep, NanosleepResolution
|
||||
nil, 0, // Nanosleep, NanosleepResolution
|
||||
&aNs, // nanosleep
|
||||
nil, // openedFiles
|
||||
nil, // fs
|
||||
)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, &aNs, sysCtx.nanosleep)
|
||||
|
||||
31
internal/testing/fs/fs.go
Normal file
31
internal/testing/fs/fs.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package testfs
|
||||
|
||||
import "io/fs"
|
||||
|
||||
// compile-time check to ensure File implements fs.File
|
||||
var _ fs.File = &File{}
|
||||
|
||||
// compile-time check to ensure FS implements fs.FS
|
||||
var _ fs.FS = &FS{}
|
||||
|
||||
// FS emulates fs.FS. Note: the path (map key) cannot begin with "/"!
|
||||
type FS map[string]*File
|
||||
|
||||
// Open implements the same method as documented on fs.FS.
|
||||
func (f FS) Open(name string) (fs.File, error) {
|
||||
if !fs.ValidPath(name) {
|
||||
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
|
||||
}
|
||||
if file, ok := f[name]; !ok {
|
||||
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
||||
} else {
|
||||
return file, nil
|
||||
}
|
||||
}
|
||||
|
||||
type File struct{ CloseErr error }
|
||||
|
||||
func (f *File) Close() error { return f.CloseErr }
|
||||
func (f *File) Stat() (fs.FileInfo, error) { return nil, nil }
|
||||
func (f *File) Read(_ []byte) (int, error) { return 0, nil }
|
||||
func (f *File) Seek(_ int64, _ int) (int64, error) { return 0, nil }
|
||||
38
internal/testing/fs/fs_test.go
Normal file
38
internal/testing/fs/fs_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package testfs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"testing"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
)
|
||||
|
||||
func TestFS(t *testing.T) {
|
||||
testFS := &FS{}
|
||||
|
||||
t.Run("validates path", func(t *testing.T) {
|
||||
f, err := testFS.Open("/foo.txt")
|
||||
require.Nil(t, f)
|
||||
require.EqualError(t, err, "open /foo.txt: invalid argument")
|
||||
})
|
||||
|
||||
t.Run("path not found", func(t *testing.T) {
|
||||
f, err := testFS.Open("foo.txt")
|
||||
require.Nil(t, f)
|
||||
require.EqualError(t, err, "open foo.txt: file does not exist")
|
||||
})
|
||||
|
||||
(*testFS)["foo.txt"] = &File{}
|
||||
f, err := testFS.Open("foo.txt")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, f, &File{})
|
||||
}
|
||||
|
||||
func TestFile(t *testing.T) {
|
||||
f := &File{CloseErr: fs.ErrClosed}
|
||||
|
||||
t.Run("returns close error", func(t *testing.T) {
|
||||
err := f.Close()
|
||||
require.Equal(t, fs.ErrClosed, err)
|
||||
})
|
||||
}
|
||||
@@ -63,7 +63,8 @@ func Equal(t TestingT, expected, actual interface{}, formatWithArgs ...interface
|
||||
|
||||
// Inline the comparison if the types are likely small:
|
||||
if expectString {
|
||||
fail(t, fmt.Sprintf("expected %q, but was %q", expected, actual), "", formatWithArgs...)
|
||||
// Don't use %q as it escapes newlines!
|
||||
fail(t, fmt.Sprintf("expected \"%s\", but was \"%s\"", expected, actual), "", formatWithArgs...)
|
||||
return
|
||||
} else if et.Kind() < reflect.Array {
|
||||
fail(t, fmt.Sprintf("expected %v, but was %v", expected, actual), "", formatWithArgs...)
|
||||
|
||||
@@ -115,10 +115,11 @@ func (m *CallContext) close(ctx context.Context, exitCode uint32) (c bool, err e
|
||||
if !atomic.CompareAndSwapUint64(m.closed, 0, closed) {
|
||||
return false, nil
|
||||
}
|
||||
if sysCtx := m.Sys; sysCtx != nil { // ex nil if from ModuleBuilder
|
||||
return true, sysCtx.FS(ctx).Close(ctx)
|
||||
c = true
|
||||
if sysCtx := m.Sys; sysCtx != nil { // nil if from ModuleBuilder
|
||||
err = sysCtx.FS(ctx).Close(ctx)
|
||||
}
|
||||
return true, nil
|
||||
return
|
||||
}
|
||||
|
||||
// Memory implements the same method as documented on api.Module.
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"testing"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/sys"
|
||||
testfs "github.com/tetratelabs/wazero/internal/testing/fs"
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
)
|
||||
|
||||
@@ -143,24 +143,25 @@ func TestCallContext_Close(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("calls Context.Close()", func(t *testing.T) {
|
||||
sysCtx := sys.DefaultContext()
|
||||
sysCtx := sys.DefaultContext(testfs.FS{"foo": &testfs.File{}})
|
||||
fsCtx := sysCtx.FS(testCtx)
|
||||
|
||||
fsCtx.OpenFile(&sys.FileEntry{Path: "."})
|
||||
_, err := fsCtx.OpenFile(testCtx, "/foo")
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := s.Instantiate(context.Background(), ns, &Module{}, t.Name(), sysCtx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// We use side effects to determine if Close in fact called Context.Close (without repeating sys_test.go).
|
||||
// One side effect of Context.Close is that it clears the openedFiles map. Verify our base case.
|
||||
_, ok := fsCtx.OpenedFile(3)
|
||||
_, ok := fsCtx.OpenedFile(testCtx, 3)
|
||||
require.True(t, ok, "sysCtx.openedFiles was empty")
|
||||
|
||||
// Closing should not err.
|
||||
require.NoError(t, m.Close(testCtx))
|
||||
|
||||
// Verify our intended side-effect
|
||||
_, ok = fsCtx.OpenedFile(3)
|
||||
_, ok = fsCtx.OpenedFile(testCtx, 3)
|
||||
require.False(t, ok, "expected no opened files")
|
||||
|
||||
// Verify no error closing again.
|
||||
@@ -169,10 +170,12 @@ func TestCallContext_Close(t *testing.T) {
|
||||
|
||||
t.Run("error closing", func(t *testing.T) {
|
||||
// Right now, the only way to err closing the sys context is if a File.Close erred.
|
||||
sysCtx := sys.DefaultContext()
|
||||
testFS := testfs.FS{"foo": &testfs.File{CloseErr: errors.New("error closing")}}
|
||||
sysCtx := sys.DefaultContext(testFS)
|
||||
fsCtx := sysCtx.FS(testCtx)
|
||||
|
||||
fsCtx.OpenFile(&sys.FileEntry{Path: ".", File: &testFile{errors.New("error closing")}})
|
||||
_, err := fsCtx.OpenFile(testCtx, "/foo")
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := s.Instantiate(context.Background(), ns, &Module{}, t.Name(), sysCtx, nil)
|
||||
require.NoError(t, err)
|
||||
@@ -180,17 +183,7 @@ func TestCallContext_Close(t *testing.T) {
|
||||
require.EqualError(t, m.Close(testCtx), "error closing")
|
||||
|
||||
// Verify our intended side-effect
|
||||
_, ok := fsCtx.OpenedFile(3)
|
||||
_, ok := fsCtx.OpenedFile(testCtx, 3)
|
||||
require.False(t, ok, "expected no opened files")
|
||||
})
|
||||
}
|
||||
|
||||
// compile-time check to ensure testFile implements fs.File
|
||||
var _ fs.File = &testFile{}
|
||||
|
||||
type testFile struct{ closeErr error }
|
||||
|
||||
func (f *testFile) Close() error { return f.closeErr }
|
||||
func (f *testFile) Stat() (fs.FileInfo, error) { return nil, nil }
|
||||
func (f *testFile) Read(_ []byte) (int, error) { return 0, nil }
|
||||
func (f *testFile) Seek(_ int64, _ int) (int64, error) { return 0, nil }
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/sys"
|
||||
testfs "github.com/tetratelabs/wazero/internal/testing/fs"
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
)
|
||||
|
||||
@@ -162,15 +163,18 @@ func TestNamespace_CloseWithExitCode(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("error closing", func(t *testing.T) {
|
||||
sysCtx := sys.DefaultContext()
|
||||
// Right now, the only way to err closing the sys context is if a File.Close erred.
|
||||
testFS := testfs.FS{"foo": &testfs.File{CloseErr: errors.New("error closing")}}
|
||||
sysCtx := sys.DefaultContext(testFS)
|
||||
fsCtx := sysCtx.FS(testCtx)
|
||||
|
||||
fsCtx.OpenFile(&sys.FileEntry{Path: ".", File: &testFile{errors.New("error closing")}})
|
||||
_, err := fsCtx.OpenFile(testCtx, "/foo")
|
||||
require.NoError(t, err)
|
||||
|
||||
ns, m1, m2 := newTestNamespace()
|
||||
m1.CallCtx.Sys = sysCtx // This should err, but both should close
|
||||
|
||||
err := ns.CloseWithExitCode(testCtx, 2)
|
||||
err = ns.CloseWithExitCode(testCtx, 2)
|
||||
require.EqualError(t, err, "error closing")
|
||||
|
||||
// Both modules were closed
|
||||
|
||||
@@ -100,7 +100,7 @@ func TestStore_Instantiate(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
sysCtx := sys.DefaultContext()
|
||||
sysCtx := sys.DefaultContext(nil)
|
||||
mod, err := s.Instantiate(testCtx, ns, m, "", sysCtx, nil)
|
||||
require.NoError(t, err)
|
||||
defer mod.Close(testCtx)
|
||||
@@ -211,7 +211,7 @@ func TestStore_hammer(t *testing.T) {
|
||||
N = 100
|
||||
}
|
||||
hammer.NewHammer(t, P, N).Run(func(name string) {
|
||||
mod, instantiateErr := s.Instantiate(testCtx, ns, importingModule, name, sys.DefaultContext(), nil)
|
||||
mod, instantiateErr := s.Instantiate(testCtx, ns, importingModule, name, sys.DefaultContext(nil), nil)
|
||||
require.NoError(t, instantiateErr)
|
||||
require.NoError(t, mod.Close(testCtx))
|
||||
}, nil)
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path"
|
||||
|
||||
"github.com/tetratelabs/wazero"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
@@ -48,12 +47,12 @@ type Builder interface {
|
||||
|
||||
// Compile compiles the ModuleName module that can instantiated in any namespace (wazero.Namespace).
|
||||
//
|
||||
// Note: This has the same effect as the same function name on wazero.ModuleBuilder.
|
||||
// Note: This has the same effect as the same function on wazero.ModuleBuilder.
|
||||
Compile(context.Context, wazero.CompileConfig) (wazero.CompiledModule, error)
|
||||
|
||||
// Instantiate instantiates the ModuleName module into the provided namespace.
|
||||
//
|
||||
// Note: This has the same effect as the same function name on wazero.ModuleBuilder.
|
||||
// Note: This has the same effect as the same function on wazero.ModuleBuilder.
|
||||
Instantiate(context.Context, wazero.Namespace) (api.Closer, error)
|
||||
}
|
||||
|
||||
@@ -677,7 +676,7 @@ func (a *wasi) FdAllocate(ctx context.Context, mod api.Module, fd uint32, offset
|
||||
// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_close
|
||||
// See https://linux.die.net/man/3/close
|
||||
func (a *wasi) FdClose(ctx context.Context, mod api.Module, fd uint32) Errno {
|
||||
if ok, err := getSysCtx(mod).FS(ctx).CloseFile(fd); err != nil {
|
||||
if ok, err := getSysCtx(mod).FS(ctx).CloseFile(ctx, fd); err != nil {
|
||||
return ErrnoIo
|
||||
} else if !ok {
|
||||
return ErrnoBadf
|
||||
@@ -724,7 +723,7 @@ func (a *wasi) FdDatasync(ctx context.Context, mod api.Module, fd uint32) Errno
|
||||
// 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 *wasi) FdFdstatGet(ctx context.Context, mod api.Module, fd uint32, resultStat uint32) Errno {
|
||||
if _, ok := getSysCtx(mod).FS(ctx).OpenedFile(fd); !ok {
|
||||
if _, ok := getSysCtx(mod).FS(ctx).OpenedFile(ctx, fd); !ok {
|
||||
return ErrnoBadf
|
||||
}
|
||||
return ErrnoSuccess
|
||||
@@ -758,7 +757,7 @@ func (a *wasi) FdFdstatGet(ctx context.Context, mod api.Module, fd uint32, resul
|
||||
// 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 *wasi) FdPrestatGet(ctx context.Context, mod api.Module, fd uint32, resultPrestat uint32) Errno {
|
||||
entry, ok := getSysCtx(mod).FS(ctx).OpenedFile(fd)
|
||||
entry, ok := getSysCtx(mod).FS(ctx).OpenedFile(ctx, fd)
|
||||
if !ok {
|
||||
return ErrnoBadf
|
||||
}
|
||||
@@ -831,7 +830,7 @@ func (a *wasi) FdPread(ctx context.Context, mod api.Module, fd, iovs, iovsCount
|
||||
// See FdPrestatGet
|
||||
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_prestat_dir_name
|
||||
func (a *wasi) FdPrestatDirName(ctx context.Context, mod api.Module, fd uint32, pathPtr uint32, pathLen uint32) Errno {
|
||||
f, ok := getSysCtx(mod).FS(ctx).OpenedFile(fd)
|
||||
f, ok := getSysCtx(mod).FS(ctx).OpenedFile(ctx, fd)
|
||||
if !ok {
|
||||
return ErrnoBadf
|
||||
}
|
||||
@@ -975,7 +974,7 @@ func (a *wasi) FdRenumber(ctx context.Context, mod api.Module, fd, to uint32) Er
|
||||
func (a *wasi) FdSeek(ctx context.Context, mod api.Module, fd uint32, offset uint64, whence uint32, resultNewoffset uint32) Errno {
|
||||
var seeker io.Seeker
|
||||
// Check to see if the file descriptor is available
|
||||
if f, ok := getSysCtx(mod).FS(ctx).OpenedFile(fd); !ok || f.File == nil {
|
||||
if f, ok := getSysCtx(mod).FS(ctx).OpenedFile(ctx, 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 {
|
||||
@@ -1094,7 +1093,7 @@ func fdReader(ctx context.Context, mod api.Module, fd uint32) io.Reader {
|
||||
sysCtx := getSysCtx(mod)
|
||||
if fd == fdStdin {
|
||||
return sysCtx.Stdin()
|
||||
} else if f, ok := sysCtx.FS(ctx).OpenedFile(fd); !ok || f.File == nil {
|
||||
} else if f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok {
|
||||
return nil
|
||||
} else {
|
||||
return f.File
|
||||
@@ -1111,9 +1110,10 @@ func fdWriter(ctx context.Context, mod api.Module, fd uint32) io.Writer {
|
||||
return sysCtx.Stderr()
|
||||
default:
|
||||
// Check to see if the file descriptor is available
|
||||
if f, ok := sysCtx.FS(ctx).OpenedFile(fd); !ok || f.File == nil {
|
||||
if f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok || f.File == nil {
|
||||
return nil
|
||||
// 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 {
|
||||
return nil
|
||||
} else {
|
||||
@@ -1189,9 +1189,9 @@ func (a *wasi) PathLink(ctx context.Context, mod api.Module, oldFd, oldFlags, ol
|
||||
// See https://linux.die.net/man/3/openat
|
||||
func (a *wasi) PathOpen(ctx context.Context, mod api.Module, fd, dirflags, pathPtr, pathLen, oflags uint32, fsRightsBase,
|
||||
fsRightsInheriting uint64, fdflags, resultOpenedFd uint32) (errno Errno) {
|
||||
fsc := getSysCtx(mod).FS(ctx)
|
||||
dir, ok := fsc.OpenedFile(fd)
|
||||
if !ok || dir.FS == nil {
|
||||
sysCtx := getSysCtx(mod)
|
||||
fsc := sysCtx.FS(ctx)
|
||||
if _, ok := fsc.OpenedFile(ctx, fd); !ok {
|
||||
return ErrnoBadf
|
||||
}
|
||||
|
||||
@@ -1200,19 +1200,17 @@ func (a *wasi) PathOpen(ctx context.Context, mod api.Module, fd, dirflags, pathP
|
||||
return ErrnoFault
|
||||
}
|
||||
|
||||
// TODO: Consider dirflags and oflags. Also, allow non-read-only open based on config about the mount.
|
||||
// Ex. allow os.O_RDONLY, os.O_WRONLY, or os.O_RDWR either by config flag or pattern on filename
|
||||
// See #390
|
||||
entry, errno := openFileEntry(dir.FS, path.Join(dir.Path, string(b)))
|
||||
if errno != ErrnoSuccess {
|
||||
return errno
|
||||
}
|
||||
|
||||
if newFD, ok := fsc.OpenFile(entry); !ok {
|
||||
_ = entry.File.Close()
|
||||
if newFD, err := fsc.OpenFile(ctx, string(b)); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
return ErrnoNoent
|
||||
case errors.Is(err, fs.ErrExist):
|
||||
return ErrnoExist
|
||||
default:
|
||||
return ErrnoIo
|
||||
}
|
||||
} else if !mod.Memory().WriteUint32Le(ctx, resultOpenedFd, newFD) {
|
||||
_ = entry.File.Close()
|
||||
_, _ = fsc.CloseFile(ctx, newFD)
|
||||
return ErrnoFault
|
||||
}
|
||||
return ErrnoSuccess
|
||||
@@ -1320,25 +1318,6 @@ func getSysCtx(mod api.Module) *sys.Context {
|
||||
}
|
||||
}
|
||||
|
||||
func openFileEntry(rootFS fs.FS, pathName string) (*sys.FileEntry, Errno) {
|
||||
f, err := rootFS.Open(pathName)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
return nil, ErrnoNoent
|
||||
case errors.Is(err, fs.ErrExist):
|
||||
return nil, ErrnoExist
|
||||
default:
|
||||
return nil, ErrnoIo
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: verify if oflags is a directory and fail with ErrnoNotdir if not
|
||||
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-oflags-flagsu16
|
||||
|
||||
return &sys.FileEntry{Path: pathName, FS: rootFS, File: f}, ErrnoSuccess
|
||||
}
|
||||
|
||||
func writeOffsetsAndNullTerminatedValues(ctx context.Context, mem api.Memory, values []string, offsets, bytes uint32) Errno {
|
||||
for _, value := range values {
|
||||
// Write current offset and advance it.
|
||||
|
||||
@@ -442,23 +442,25 @@ func Test_FdAllocate(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_FdClose(t *testing.T) {
|
||||
fdToClose := uint32(3) // arbitrary fd
|
||||
fdToKeep := uint32(4) // another arbitrary fd
|
||||
fdToClose := uint32(4) // arbitrary fd
|
||||
fdToKeep := uint32(5) // another arbitrary fd
|
||||
|
||||
setupFD := func() (api.Module, api.Function, *wasi) {
|
||||
// fd_close needs to close an open file descriptor. Open two files so that we can tell which is closed.
|
||||
path1, path2 := "a", "b"
|
||||
testFs := fstest.MapFS{path1: {Data: make([]byte, 0)}, path2: {Data: make([]byte, 0)}}
|
||||
entry1, errno := openFileEntry(testFs, path1)
|
||||
require.Zero(t, errno, ErrnoName(errno))
|
||||
entry2, errno := openFileEntry(testFs, path2)
|
||||
require.Zero(t, errno, ErrnoName(errno))
|
||||
|
||||
sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{
|
||||
fdToClose: entry1,
|
||||
fdToKeep: entry2,
|
||||
})
|
||||
testFS := fstest.MapFS{path1: {Data: make([]byte, 0)}, path2: {Data: make([]byte, 0)}}
|
||||
sysCtx, err := newSysContext(nil, nil, testFS)
|
||||
require.NoError(t, err)
|
||||
fsc := sysCtx.FS(testCtx)
|
||||
|
||||
fd, err := fsc.OpenFile(testCtx, path1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fdToClose, fd)
|
||||
|
||||
fd, err = fsc.OpenFile(testCtx, path2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fdToKeep, fd)
|
||||
|
||||
mod, fn := instantiateModule(testCtx, t, functionFdClose, importFdClose, sysCtx)
|
||||
return mod, fn, a
|
||||
@@ -467,11 +469,11 @@ func Test_FdClose(t *testing.T) {
|
||||
verify := func(mod api.Module) {
|
||||
// Verify fdToClose is closed and removed from the opened FDs.
|
||||
fsc := getSysCtx(mod).FS(testCtx)
|
||||
_, ok := fsc.OpenedFile(fdToClose)
|
||||
_, ok := fsc.OpenedFile(testCtx, fdToClose)
|
||||
require.False(t, ok)
|
||||
|
||||
// Verify fdToKeep is not closed
|
||||
_, ok = fsc.OpenedFile(fdToKeep)
|
||||
_, ok = fsc.OpenedFile(testCtx, fdToKeep)
|
||||
require.True(t, ok)
|
||||
}
|
||||
|
||||
@@ -633,11 +635,8 @@ func Test_FdPread(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_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]*internalsys.FileEntry{fd: {Path: pathName}})
|
||||
require.NoError(t, err)
|
||||
sysCtx, fd := requireOpenDir(t, pathName)
|
||||
|
||||
mod, fn := instantiateModule(testCtx, t, functionFdPrestatGet, importFdPrestatGet, sysCtx)
|
||||
defer mod.Close(testCtx)
|
||||
@@ -677,12 +676,21 @@ func Test_FdPrestatGet(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func requireOpenDir(t *testing.T, pathName string) (*internalsys.Context, uint32) {
|
||||
testFS := fstest.MapFS{pathName[1:]: {Mode: fs.ModeDir}}
|
||||
sysCtx, err := newSysContext(nil, nil, testFS)
|
||||
require.NoError(t, err)
|
||||
fsc := sysCtx.FS(testCtx)
|
||||
fd, err := fsc.OpenFile(testCtx, pathName)
|
||||
require.NoError(t, err)
|
||||
return sysCtx, fd
|
||||
}
|
||||
|
||||
func Test_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]*internalsys.FileEntry{fd: {Path: "/tmp"}})
|
||||
require.NoError(t, err)
|
||||
pathName := "/tmp"
|
||||
sysCtx, fd := requireOpenDir(t, pathName)
|
||||
|
||||
mod, _ := instantiateModule(testCtx, t, functionFdPrestatGet, importFdPrestatGet, sysCtx)
|
||||
defer mod.Close(testCtx)
|
||||
@@ -721,10 +729,8 @@ func Test_FdPrestatGet_Errors(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_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]*internalsys.FileEntry{fd: {Path: "/tmp"}})
|
||||
require.NoError(t, err)
|
||||
pathName := "/tmp"
|
||||
sysCtx, fd := requireOpenDir(t, pathName)
|
||||
|
||||
mod, fn := instantiateModule(testCtx, t, functionFdPrestatDirName, importFdPrestatDirName, sysCtx)
|
||||
defer mod.Close(testCtx)
|
||||
@@ -763,9 +769,8 @@ func Test_FdPrestatDirName(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_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]*internalsys.FileEntry{fd: {Path: "/tmp"}})
|
||||
require.NoError(t, err)
|
||||
pathName := "/tmp"
|
||||
sysCtx, fd := requireOpenDir(t, pathName)
|
||||
|
||||
mod, _ := instantiateModule(testCtx, t, functionFdPrestatDirName, importFdPrestatDirName, sysCtx)
|
||||
defer mod.Close(testCtx)
|
||||
@@ -841,7 +846,7 @@ func Test_FdPwrite(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_FdRead(t *testing.T) {
|
||||
fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err
|
||||
var fd uint32
|
||||
iovs := uint32(1) // arbitrary offset
|
||||
initialMemory := []byte{
|
||||
'?', // `iovs` is after this
|
||||
@@ -885,10 +890,12 @@ func Test_FdRead(t *testing.T) {
|
||||
tc := tt
|
||||
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]*internalsys.FileEntry{
|
||||
fd: {Path: "test_path", FS: testFS, File: file},
|
||||
})
|
||||
_, testFS := createFile(t, "test_path", []byte("wazero"))
|
||||
sysCtx, err := newSysContext(nil, nil, testFS)
|
||||
require.NoError(t, err)
|
||||
fsc := sysCtx.FS(testCtx)
|
||||
|
||||
fd, err = fsc.OpenFile(testCtx, "test_path")
|
||||
require.NoError(t, err)
|
||||
|
||||
mod, fn := instantiateModule(testCtx, t, functionFdRead, importFdRead, sysCtx)
|
||||
@@ -910,12 +917,12 @@ func Test_FdRead(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_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
|
||||
_, testFS := createFile(t, "test_path", []byte{}) // file with empty contents
|
||||
sysCtx, err := newSysContext(nil, nil, testFS)
|
||||
require.NoError(t, err)
|
||||
fsc := sysCtx.FS(testCtx)
|
||||
|
||||
sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{
|
||||
validFD: {Path: "test_path", FS: testFS, File: file},
|
||||
})
|
||||
validFD, err := fsc.OpenFile(testCtx, "test_path")
|
||||
require.NoError(t, err)
|
||||
|
||||
mod, _ := instantiateModule(testCtx, t, functionFdRead, importFdRead, sysCtx)
|
||||
@@ -1038,16 +1045,14 @@ func Test_FdRenumber(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_FdSeek(t *testing.T) {
|
||||
fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err
|
||||
resultNewoffset := uint32(1) // arbitrary offset in `ctx.Memory` for the new offset value
|
||||
file, testFS := createFile(t, "test_path", []byte("wazero")) // arbitrary non-empty contents
|
||||
|
||||
sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{
|
||||
fd: {Path: "test_path", FS: testFS, File: file},
|
||||
})
|
||||
_, testFS := createFile(t, "test_path", []byte("wazero")) // arbitrary non-empty contents
|
||||
sysCtx, err := newSysContext(nil, nil, testFS)
|
||||
require.NoError(t, err)
|
||||
|
||||
fsCtx := sysCtx.FS(testCtx)
|
||||
fd, err := fsCtx.OpenFile(testCtx, "test_path")
|
||||
require.NoError(t, err)
|
||||
|
||||
mod, fn := instantiateModule(testCtx, t, functionFdSeek, importFdSeek, sysCtx)
|
||||
defer mod.Close(testCtx)
|
||||
@@ -1121,7 +1126,7 @@ func Test_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 := fsCtx.OpenedFile(fd)
|
||||
f, ok := fsCtx.OpenedFile(testCtx, fd)
|
||||
require.True(t, ok)
|
||||
seeker := f.File.(io.Seeker)
|
||||
|
||||
@@ -1147,12 +1152,12 @@ func Test_FdSeek(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_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
|
||||
_, testFS := createFile(t, "test_path", []byte("wazero")) // arbitrary non-empty contents
|
||||
sysCtx, err := newSysContext(nil, nil, testFS)
|
||||
require.NoError(t, err)
|
||||
|
||||
sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{
|
||||
validFD: {Path: "test_path", FS: testFS, File: file},
|
||||
})
|
||||
fsCtx := sysCtx.FS(testCtx)
|
||||
validFD, err := fsCtx.OpenFile(testCtx, "test_path")
|
||||
require.NoError(t, err)
|
||||
|
||||
mod, _ := instantiateModule(testCtx, t, functionFdSeek, importFdSeek, sysCtx)
|
||||
@@ -1193,7 +1198,6 @@ func Test_FdSeek_Errors(t *testing.T) {
|
||||
require.Equal(t, tc.expectedErrno, errno, ErrnoName(errno))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Test_FdSync only tests it is stubbed for GrainLang per #271
|
||||
@@ -1233,7 +1237,7 @@ func Test_FdTell(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_FdWrite(t *testing.T) {
|
||||
fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err
|
||||
fd := uint32(4)
|
||||
iovs := uint32(1) // arbitrary offset
|
||||
initialMemory := []byte{
|
||||
'?', // `iovs` is after this
|
||||
@@ -1280,11 +1284,7 @@ func Test_FdWrite(t *testing.T) {
|
||||
t.Run(tc.name, func(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]*internalsys.FileEntry{
|
||||
fd: {Path: pathName, FS: testFS, File: file},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sysCtx := newContextWithWritableFile(t, tmpDir, pathName)
|
||||
|
||||
mod, fn := instantiateModule(testCtx, t, functionFdWrite, importFdWrite, sysCtx)
|
||||
defer mod.Close(testCtx)
|
||||
@@ -1310,16 +1310,10 @@ func Test_FdWrite(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_FdWrite_Errors(t *testing.T) {
|
||||
validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err
|
||||
|
||||
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
|
||||
pathName := "test_path"
|
||||
file, testFS := createWriteableFile(t, tmpDir, pathName, []byte{})
|
||||
|
||||
sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{
|
||||
validFD: {Path: pathName, FS: testFS, File: file},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
validFD := uint32(4)
|
||||
sysCtx := newContextWithWritableFile(t, tmpDir, pathName)
|
||||
|
||||
mod, _ := instantiateModule(testCtx, t, functionFdWrite, importFdWrite, sysCtx)
|
||||
defer mod.Close(testCtx)
|
||||
@@ -1461,7 +1455,6 @@ func Test_PathLink(t *testing.T) {
|
||||
|
||||
func Test_PathOpen(t *testing.T) {
|
||||
type pathOpenArgs struct {
|
||||
fd uint32
|
||||
dirflags uint32
|
||||
pathPtr uint32
|
||||
pathLen uint32
|
||||
@@ -1472,11 +1465,13 @@ func Test_PathOpen(t *testing.T) {
|
||||
resultOpenedFd uint32
|
||||
}
|
||||
|
||||
setup := func(workdirFD uint32, pathName string) (api.Module, api.Function, pathOpenArgs, []byte, uint32) {
|
||||
rootFD := uint32(3) // after 0, 1, and 2, that are stdin/out/err
|
||||
expectedFD := rootFD + 1
|
||||
|
||||
setup := func(pathName string) (api.Module, api.Function, pathOpenArgs, []byte) {
|
||||
// Setup the initial memory to include the path name starting at an offset.
|
||||
initialMemory := append([]byte{'?'}, pathName...)
|
||||
|
||||
expectedFD := workdirFD + 1
|
||||
expectedMemory := append(
|
||||
initialMemory,
|
||||
'?', // `resultOpenedFd` is after this
|
||||
@@ -1485,7 +1480,6 @@ func Test_PathOpen(t *testing.T) {
|
||||
)
|
||||
|
||||
args := pathOpenArgs{
|
||||
fd: workdirFD,
|
||||
dirflags: 0,
|
||||
pathPtr: 1,
|
||||
pathLen: uint32(len(pathName)),
|
||||
@@ -1497,18 +1491,17 @@ func Test_PathOpen(t *testing.T) {
|
||||
}
|
||||
|
||||
testFS := fstest.MapFS{pathName: &fstest.MapFile{Mode: os.ModeDir}}
|
||||
sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{
|
||||
workdirFD: {Path: ".", FS: testFS},
|
||||
})
|
||||
sysCtx, err := newSysContext(nil, nil, testFS)
|
||||
require.NoError(t, err)
|
||||
|
||||
mod, fn := instantiateModule(testCtx, t, functionPathOpen, importPathOpen, sysCtx)
|
||||
maskMemory(t, testCtx, mod, len(expectedMemory))
|
||||
ok := mod.Memory().Write(testCtx, 0, initialMemory)
|
||||
require.True(t, ok)
|
||||
return mod, fn, args, expectedMemory, expectedFD
|
||||
return mod, fn, args, expectedMemory
|
||||
}
|
||||
|
||||
verify := func(ctx context.Context, errno Errno, mod api.Module, pathName string, expectedMemory []byte, expectedFD uint32) {
|
||||
verify := func(ctx context.Context, errno Errno, mod api.Module, pathName string, expectedMemory []byte) {
|
||||
require.Zero(t, errno, ErrnoName(errno))
|
||||
|
||||
actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory)))
|
||||
@@ -1517,54 +1510,48 @@ func Test_PathOpen(t *testing.T) {
|
||||
|
||||
// verify the file was actually opened
|
||||
fsc := getSysCtx(mod).FS(ctx)
|
||||
f, ok := fsc.OpenedFile(expectedFD)
|
||||
f, ok := fsc.OpenedFile(testCtx, expectedFD)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, pathName, f.Path)
|
||||
}
|
||||
|
||||
t.Run("wasi.PathOpen", func(t *testing.T) {
|
||||
workdirFD := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err
|
||||
pathName := "wazero"
|
||||
|
||||
mod, _, args, expectedMemory, expectedFD := setup(workdirFD, pathName)
|
||||
errno := a.PathOpen(testCtx, mod, args.fd, args.dirflags, args.pathPtr, args.pathLen, args.oflags,
|
||||
mod, _, args, expectedMemory := setup(pathName)
|
||||
errno := a.PathOpen(testCtx, mod, rootFD, args.dirflags, args.pathPtr, args.pathLen, args.oflags,
|
||||
args.fsRightsBase, args.fsRightsInheriting, args.fdflags, args.resultOpenedFd)
|
||||
verify(testCtx, errno, mod, pathName, expectedMemory, expectedFD)
|
||||
verify(testCtx, errno, mod, pathName, expectedMemory)
|
||||
})
|
||||
|
||||
t.Run(functionPathOpen, func(t *testing.T) {
|
||||
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),
|
||||
mod, fn, args, expectedMemory := setup(pathName)
|
||||
results, err := fn.Call(testCtx, uint64(rootFD), 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(testCtx, errno, mod, pathName, expectedMemory, expectedFD)
|
||||
verify(testCtx, errno, mod, pathName, expectedMemory)
|
||||
})
|
||||
|
||||
t.Run("wasi.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.
|
||||
mod, _, args, expectedMemory, _ := setup(workdirFD, pathName)
|
||||
mod, _, args, expectedMemory := setup(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)
|
||||
ctx, closer := experimental.WithFS(testCtx, testFS)
|
||||
defer closer.Close(ctx)
|
||||
|
||||
errno := a.PathOpen(ctx, mod, workdirFD, args.dirflags, args.pathPtr, args.pathLen, args.oflags,
|
||||
errno := a.PathOpen(ctx, mod, rootFD, 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)
|
||||
verify(ctx, errno, mod, pathName, expectedMemory)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1572,10 +1559,7 @@ func Test_PathOpen_Errors(t *testing.T) {
|
||||
validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err
|
||||
pathName := "wazero"
|
||||
testFS := fstest.MapFS{pathName: &fstest.MapFile{Mode: os.ModeDir}}
|
||||
|
||||
sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{
|
||||
validFD: {Path: ".", FS: testFS},
|
||||
})
|
||||
sysCtx, err := newSysContext(nil, nil, testFS)
|
||||
require.NoError(t, err)
|
||||
|
||||
mod, _ := instantiateModule(testCtx, t, functionPathOpen, importPathOpen, sysCtx)
|
||||
@@ -2008,7 +1992,7 @@ func instantiateModule(ctx context.Context, t *testing.T, wasiFunction, wasiImpo
|
||||
return mod, fn
|
||||
}
|
||||
|
||||
func newSysContext(args, environ []string, openedFiles map[uint32]*internalsys.FileEntry) (sysCtx *internalsys.Context, err error) {
|
||||
func newSysContext(args, environ []string, fs fs.FS) (sysCtx *internalsys.Context, err error) {
|
||||
return internalsys.NewContext(
|
||||
math.MaxUint32,
|
||||
args,
|
||||
@@ -2020,7 +2004,7 @@ func newSysContext(args, environ []string, openedFiles map[uint32]*internalsys.F
|
||||
nil, 0,
|
||||
nil, 0,
|
||||
nil, // nanosleep
|
||||
openedFiles,
|
||||
fs,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2035,6 +2019,25 @@ func createFile(t *testing.T, pathName string, data []byte) (fs.File, fs.FS) {
|
||||
return f, mapFS
|
||||
}
|
||||
|
||||
// newContextWithWritableFile is temporary until we add the ability to open files for writing.
|
||||
func newContextWithWritableFile(t *testing.T, tmpDir string, pathName string) *internalsys.Context {
|
||||
writeable, testFS := createWriteableFile(t, tmpDir, pathName, []byte{})
|
||||
sysCtx, err := newSysContext(nil, nil, testFS)
|
||||
require.NoError(t, err)
|
||||
|
||||
fsc := sysCtx.FS(testCtx)
|
||||
fd, err := fsc.OpenFile(testCtx, pathName)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Swap the read-only file with a writeable one until #390
|
||||
f, ok := fsc.OpenedFile(testCtx, fd)
|
||||
require.True(t, ok)
|
||||
f.File.Close()
|
||||
f.File = writeable
|
||||
|
||||
return sysCtx
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
Reference in New Issue
Block a user