fs: adds FSConfig to replace experimental writefs (#1061)

This adds a new top-level type FSConfig, which is configured via
`ModuleConfig.WithFSConfig(fcfg)`. This implements read-only and
read-write directory mounts, something not formally supported before. It
also implements `WithFS` which adapts a normal `fs.FS`. For convenience,
we retain the old `ModuleConfig.WithFS` signature so as to not affect
existing users much. A new configuration for our emerging raw
filesystem, `FSConfig.WithSysfs()` will happen later without breaking
this API.

Here's an example:
```
moduleConfig = wazero.NewModuleConfig().
	// Make the current directory read-only accessible to the guest.
	WithReadOnlyDirMount(".", "/")
	// Make "/tmp/wasm" accessible to the guest as "/tmp".
	WithDirMount("/tmp/wasm", "/tmp")
```

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2023-01-25 10:09:40 -10:00
committed by GitHub
parent affcf6ca80
commit cc68f8ee12
25 changed files with 458 additions and 231 deletions

View File

@@ -16,7 +16,6 @@ import (
"github.com/tetratelabs/wazero/experimental/logging"
gojs "github.com/tetratelabs/wazero/imports/go"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
"github.com/tetratelabs/wazero/internal/sysfs"
"github.com/tetratelabs/wazero/internal/version"
"github.com/tetratelabs/wazero/sys"
)
@@ -161,7 +160,7 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod
env = append(env, fields[0], fields[1])
}
rootFS := validateMounts(mounts, stdErr, exit)
fsConfig := validateMounts(mounts, stdErr, exit)
wasm, err := os.ReadFile(wasmPath)
if err != nil {
@@ -188,6 +187,7 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod
WithStderr(stdErr).
WithStdin(os.Stdin).
WithRandSource(rand.Reader).
WithFSConfig(fsConfig).
WithSysNanosleep().
WithSysNanotime().
WithSysWalltime().
@@ -195,9 +195,6 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod
for i := 0; i < len(env); i += 2 {
conf = conf.WithEnv(env[i], env[i+1])
}
if rootFS != nil {
conf = conf.WithFS(&sysfs.FSHolder{FS: rootFS})
}
code, err := rt.CompileModule(ctx, wasm)
if err != nil {
@@ -227,9 +224,8 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod
exit(0)
}
func validateMounts(mounts sliceFlag, stdErr logging.Writer, exit func(code int)) sysfs.FS {
fs := make([]sysfs.FS, 0, len(mounts))
guestPaths := make([]string, 0, len(mounts))
func validateMounts(mounts sliceFlag, stdErr logging.Writer, exit func(code int)) (config wazero.FSConfig) {
config = wazero.NewFSConfig()
for _, mount := range mounts {
if len(mount) == 0 {
fmt.Fprintln(stdErr, "invalid mount: empty string")
@@ -243,48 +239,41 @@ func validateMounts(mounts sliceFlag, stdErr logging.Writer, exit func(code int)
}
// TODO(anuraaga): Support wasm paths with colon in them.
var host, guest string
var dir, guestPath string
if clnIdx := strings.LastIndexByte(mount, ':'); clnIdx != -1 {
host, guest = mount[:clnIdx], mount[clnIdx+1:]
dir, guestPath = mount[:clnIdx], mount[clnIdx+1:]
} else {
host = mount
guest = host
dir = mount
guestPath = dir
}
// Provide a better experience if duplicates are found later.
if guest == "" {
guest = "/"
if guestPath == "" {
guestPath = "/"
}
// Eagerly validate the mounts as we know they should be on the host.
if abs, err := filepath.Abs(host); err != nil {
fmt.Fprintf(stdErr, "invalid mount: path %q invalid: %v\n", host, err)
if abs, err := filepath.Abs(dir); err != nil {
fmt.Fprintf(stdErr, "invalid mount: path %q invalid: %v\n", dir, err)
exit(1)
} else {
host = abs
dir = abs
}
if stat, err := os.Stat(host); err != nil {
fmt.Fprintf(stdErr, "invalid mount: path %q error: %v\n", host, err)
if stat, err := os.Stat(dir); err != nil {
fmt.Fprintf(stdErr, "invalid mount: path %q error: %v\n", dir, err)
exit(1)
} else if !stat.IsDir() {
fmt.Fprintf(stdErr, "invalid mount: path %q is not a directory\n", host)
fmt.Fprintf(stdErr, "invalid mount: path %q is not a directory\n", dir)
}
next := sysfs.NewDirFS(host)
if readOnly {
next = sysfs.NewReadFS(next)
}
fs = append(fs, next)
guestPaths = append(guestPaths, guest)
}
if fs, err := sysfs.NewRootFS(fs, guestPaths); err != nil {
fmt.Fprintf(stdErr, "invalid mounts %v: %v\n", fs, err)
exit(1)
return nil
config = config.WithReadOnlyDirMount(dir, guestPath)
} else {
return fs
config = config.WithDirMount(dir, guestPath)
}
}
return
}
func detectImports(imports []api.FunctionDefinition) (needsWASI, needsGo bool) {

View File

@@ -15,6 +15,7 @@ import (
"github.com/tetratelabs/wazero/internal/filecache"
"github.com/tetratelabs/wazero/internal/platform"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/sysfs"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/sys"
)
@@ -413,38 +414,18 @@ 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 return fs.ErrNotExist.
//
// This example sets a read-only, embedded file-system:
//
// //go:embed testdata/index.html
// var testdataIndex embed.FS
//
// rooted, err := fs.Sub(testdataIndex, "testdata")
// require.NoError(t, err)
//
// // "index.html" is accessible as "/index.html".
// config := wazero.NewModuleConfig().WithFS(rooted)
//
// This example 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 is a convenience that calls WithFSConfig with an FSConfig of the
// input for the root ("/") guest path.
WithFS(fs.FS) ModuleConfig
// WithName configures the module name. Defaults to what was decoded from the name section.
// WithFSConfig configures the filesystem available to each guest
// instantiated with this configuration. By default, no file access is
// allowed, so functions like `path_open` result in unsupported errors
// (e.g. syscall.ENOSYS).
WithFSConfig(FSConfig) ModuleConfig
// WithName configures the module name. Defaults to what was decoded from
// the name section.
WithName(string) ModuleConfig
// WithStartFunctions configures the functions to call after the module is
@@ -593,8 +574,8 @@ type moduleConfig struct {
environ [][]byte
// environKeys allow overwriting of existing values.
environKeys map[string]int
// fs is the file system to open files with
fs fs.FS
// fsConfig is the file system configuration for ABI like WASI.
fsConfig FSConfig
}
// NewModuleConfig returns a ModuleConfig that can be used for configuring module instantiation.
@@ -648,8 +629,13 @@ func (c *moduleConfig) WithEnv(key, value string) ModuleConfig {
// WithFS implements ModuleConfig.WithFS
func (c *moduleConfig) WithFS(fs fs.FS) ModuleConfig {
return c.WithFSConfig(NewFSConfig().WithFSMount(fs, ""))
}
// WithFSConfig implements ModuleConfig.WithFSConfig
func (c *moduleConfig) WithFSConfig(config FSConfig) ModuleConfig {
ret := c.clone()
ret.fs = fs
ret.fsConfig = config
return ret
}
@@ -764,6 +750,13 @@ func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) {
environ = append(environ, result)
}
var fs sysfs.FS
if f, ok := c.fsConfig.(*fsConfig); ok {
if fs, err = f.toFS(); err != nil {
return
}
}
return internalsys.NewContext(
math.MaxUint32,
c.args,
@@ -775,6 +768,6 @@ func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) {
c.walltime, c.walltimeResolution,
c.nanotime, c.nanotimeResolution,
c.nanosleep,
c.fs,
fs,
)
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"crypto/rand"
"io"
"io/fs"
"math"
"testing"
@@ -12,6 +11,7 @@ import (
"github.com/tetratelabs/wazero/internal/fstest"
"github.com/tetratelabs/wazero/internal/platform"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/sysfs"
testfs "github.com/tetratelabs/wazero/internal/testing/fs"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
@@ -323,7 +323,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
testFS,
sysfs.Adapt(testFS),
),
},
{
@@ -340,7 +340,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
testFS2, // fs
sysfs.Adapt(testFS2), // fs
),
},
{
@@ -563,7 +563,7 @@ func TestModuleConfig_clone(t *testing.T) {
cloned := mc.clone()
// Make post-clone changes
mc.fs = fstest.FS
mc.fsConfig = NewFSConfig().WithFSMount(fstest.FS, "/")
mc.environKeys["2"] = 2
cloned.environKeys["1"] = 1
@@ -573,7 +573,7 @@ func TestModuleConfig_clone(t *testing.T) {
require.Equal(t, map[string]int{"1": 1}, cloned.environKeys)
// Ensure the fs is not shared
require.Nil(t, cloned.fs)
require.Nil(t, cloned.fsConfig)
}
func Test_compiledModule_Name(t *testing.T) {
@@ -697,7 +697,7 @@ func requireSysContext(
walltime *sys.Walltime, walltimeResolution sys.ClockResolution,
nanotime *sys.Nanotime, nanotimeResolution sys.ClockResolution,
nanosleep *sys.Nanosleep,
fs fs.FS,
fs sysfs.FS,
) *internalsys.Context {
sysCtx, err := internalsys.NewContext(
max,

View File

@@ -1,40 +0,0 @@
// Package writefs includes wazero-specific fs.FS implementations that allow
// creation and deletion of files and directories.
//
// This is a work-in-progress and a workaround needed because write support is
// not yet supported in fs.FS. See https://github.com/golang/go/issues/45757
//
// Tracking issue: https://github.com/tetratelabs/wazero/issues/390
package writefs
import (
"io/fs"
"github.com/tetratelabs/wazero/internal/sysfs"
)
// NewDirFS creates a writeable filesystem at the given path on the host
// filesystem.
//
// This is like os.DirFS, but allows creation and deletion of files and
// directories, as well as timestamp modifications. None of which are supported
// in fs.FS.
//
// The following errors are expected:
// - syscall.EINVAL: `dir` is invalid.
// - syscall.ENOENT: `dir` doesn't exist.
// - syscall.ENOTDIR: `dir` exists, but is not a directory.
//
// # Isolation
//
// Symbolic links can escape the root path as files are opened via os.OpenFile
// which cannot restrict following them.
//
// # This is wazero-only
//
// Do not attempt to use the result as a fs.FS, as it will panic. This is a
// bridge to a future filesystem abstraction made for wazero.
func NewDirFS(dir string) fs.FS {
// sysfs.DirFS is intentionally internal as it is still evolving
return &sysfs.FSHolder{FS: sysfs.NewDirFS(dir)}
}

View File

@@ -1,17 +0,0 @@
package writefs_test
import (
_ "embed"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/experimental/writefs"
)
var config wazero.ModuleConfig //nolint
// This shows how to use writefs.NewDirFS to map paths relative to "/work/appA",
// as "/". Unlike os.DirFS, these paths will be writable.
func Example_dirFS() {
fs := writefs.NewDirFS("/work/appA")
config = wazero.NewModuleConfig().WithFS(fs)
}

165
fsconfig.go Normal file
View File

@@ -0,0 +1,165 @@
package wazero
import (
"io/fs"
"github.com/tetratelabs/wazero/internal/sysfs"
)
// FSConfig configures filesystem paths the embedding host allows the wasm
// guest to access. Unconfigured paths are not allowed, so functions like
// `path_open` result in unsupported errors (e.g. syscall.ENOSYS).
//
// # Guest Path
//
// `guestPath` is the name of the path the guest should use a filesystem for, or
// empty for any files.
//
// All `guestPath` paths are normalized, specifically removing any leading or
// trailing slashes. This means "/", "./" or "." all coerce to empty "".
//
// Multiple `guestPath` values can be configured, but the last longest match
// wins. For example, if "tmp", then "" were added, a request to open
// "tmp/foo.txt" use the filesystem associated with "tmp" even though a wider
// path, "" (all files), was added later.
//
// A `guestPath` of "." coerces to the empty string "" because the current
// directory is handled by the guest. In other words, the guest resolves ites
// current directory prior to requesting files.
//
// More notes on `guestPath`
// - Go compiled with runtime.GOOS=js do not pay attention to this value.
// Hence, you need to normalize the filesystem with NewRootFS to ensure
// paths requested resolve as expected.
// - Working directories are typically tracked in wasm, though possible some
// relative paths are requested. For example, TinyGo may attempt to resolve
// a path "../.." in unit tests.
// - Zig uses the first path name it sees as the initial working directory of
// the process.
//
// # Scope
//
// Configuration here is module instance scoped. This means you can use the
// same configuration for multiple calls to Runtime.InstantiateModule. Each
// module will have a different file descriptor table. Any errors accessing
// resources allowed here are deferred to instantiation time of each module.
//
// Any host resources present at the time of configuration, but deleted before
// Runtime.InstantiateModule will trap/panic when the guest wasm initializes or
// calls functions like `fd_read`.
//
// # Windows
//
// While wazero supports Windows as a platform, all known compilers use POSIX
// conventions at runtime. For example, even when running on Windows, paths
// used by wasm are separated by forward slash (/), not backslash (\).
//
// # Notes
//
// - FSConfig is immutable. Each WithXXX function returns a new instance
// including the corresponding change.
// - RATIONALE.md includes design background and relationship to WebAssembly
// System Interfaces (WASI).
type FSConfig interface {
// WithDirMount assigns a directory at `dir` to any paths beginning at
// `guestPath`.
//
// If the same `guestPath` was assigned before, this overrides its value,
// retaining the original precedence. See the documentation of FSConfig for
// more details on `guestPath`.
//
// # Isolation
//
// The guest will have full access to this directory including escaping it
// via relative path lookups like "../../". Full access includes operations
// such as creating or deleting files, limited to any host level access
// controls.
WithDirMount(dir, guestPath string) FSConfig
// WithReadOnlyDirMount assigns a directory at `dir` to any paths
// beginning at `guestPath`.
//
// This is the same as WithDirMount except only read operations are
// permitted. However, escaping the directory via relative path lookups
// like "../../" is still allowed.
WithReadOnlyDirMount(dir, guestPath string) FSConfig
// WithFSMount assigns a fs.FS file system for any paths beginning at
// `guestPath`.
//
// If the same `guestPath` was assigned before, this overrides its value,
// retaining the original precedence. See the documentation of FSConfig for
// more details on `guestPath`.
//
// # Isolation
//
// fs.FS does not restrict the ability to overwrite returned files via
// io.Writer. Moreover, 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
WithFSMount(fs fs.FS, guestPath string) FSConfig
}
type fsConfig struct {
// fs are the currently configured filesystems.
fs []sysfs.FS
// guestPaths are the user-supplied names of the filesystems, retained for
// error messages and fmt.Stringer.
guestPaths []string
// guestPathToFS are the normalized paths to the currently configured
// filesystems, used for de-duplicating.
guestPathToFS map[string]int
}
// NewFSConfig returns a FSConfig that can be used for configuring module instantiation.
func NewFSConfig() FSConfig {
return &fsConfig{guestPathToFS: map[string]int{}}
}
// clone makes a deep copy of this module config.
func (c *fsConfig) clone() *fsConfig {
ret := *c // copy except slice and maps which share a ref
ret.fs = make([]sysfs.FS, 0, len(c.fs))
ret.fs = append(ret.fs, c.fs...)
ret.guestPaths = make([]string, 0, len(c.guestPaths))
ret.guestPaths = append(ret.guestPaths, c.guestPaths...)
ret.guestPathToFS = make(map[string]int, len(c.guestPathToFS))
for key, value := range c.guestPathToFS {
ret.guestPathToFS[key] = value
}
return &ret
}
// WithDirMount implements FSConfig.WithDirMount
func (c *fsConfig) WithDirMount(dir, guestPath string) FSConfig {
return c.withMount(sysfs.NewDirFS(dir), guestPath)
}
// WithReadOnlyDirMount implements FSConfig.WithReadOnlyDirMount
func (c *fsConfig) WithReadOnlyDirMount(dir, guestPath string) FSConfig {
return c.withMount(sysfs.NewReadFS(sysfs.NewDirFS(dir)), guestPath)
}
// WithFSMount implements FSConfig.WithFSMount
func (c *fsConfig) WithFSMount(fs fs.FS, guestPath string) FSConfig {
return c.withMount(sysfs.Adapt(fs), guestPath)
}
func (c *fsConfig) withMount(fs sysfs.FS, guestPath string) FSConfig {
cleaned := sysfs.StripPrefixesAndTrailingSlash(guestPath)
ret := c.clone()
if i, ok := ret.guestPathToFS[cleaned]; ok {
ret.fs[i] = fs
ret.guestPaths[i] = guestPath
} else {
ret.guestPathToFS[cleaned] = len(ret.fs)
ret.fs = append(ret.fs, fs)
ret.guestPaths = append(ret.guestPaths, guestPath)
}
return ret
}
func (c *fsConfig) toFS() (sysfs.FS, error) {
return sysfs.NewRootFS(c.fs, c.guestPaths)
}

27
fsconfig_example_test.go Normal file
View File

@@ -0,0 +1,27 @@
package wazero_test
import (
"embed"
"io/fs"
"log"
"github.com/tetratelabs/wazero"
)
//go:embed testdata/index.html
var testdataIndex embed.FS
var moduleConfig wazero.ModuleConfig
// This example shows how to configure an embed.FS.
func Example_withFSConfig_embedFS() {
// Strip the embedded path testdata/
rooted, err := fs.Sub(testdataIndex, "testdata")
if err != nil {
log.Panicln(err)
}
moduleConfig = wazero.NewModuleConfig().
// Make "index.html" accessible to the guest as "/index.html".
WithFSConfig(wazero.NewFSConfig().WithFSMount(rooted, "/"))
}

106
fsconfig_test.go Normal file
View File

@@ -0,0 +1,106 @@
package wazero
import (
"testing"
"github.com/tetratelabs/wazero/internal/sysfs"
testfs "github.com/tetratelabs/wazero/internal/testing/fs"
"github.com/tetratelabs/wazero/internal/testing/require"
)
// TestFSConfig only tests the cases that change the inputs to sysfs.NewRootFS.
func TestFSConfig(t *testing.T) {
base := NewFSConfig()
testFS := testfs.FS{}
testFS2 := testfs.FS{"/": &testfs.File{}}
tests := []struct {
name string
input FSConfig
expected sysfs.FS
}{
{
name: "empty",
input: base,
expected: sysfs.UnimplementedFS{},
},
{
name: "WithFSMount",
input: base.WithFSMount(testFS, "/"),
expected: sysfs.Adapt(testFS),
},
{
name: "WithFSMount overwrites",
input: base.WithFSMount(testFS, "/").WithFSMount(testFS2, "/"),
expected: sysfs.Adapt(testFS2),
},
{
name: "WithDirMount overwrites",
input: base.WithFSMount(testFS, "/").WithDirMount(".", "/"),
expected: sysfs.NewDirFS("."),
},
{
name: "Composition",
input: base.WithReadOnlyDirMount(".", "/").WithDirMount("/tmp", "/tmp"),
expected: func() sysfs.FS {
f, err := sysfs.NewRootFS(
[]sysfs.FS{sysfs.NewReadFS(sysfs.NewDirFS(".")), sysfs.NewDirFS("/tmp")},
[]string{"/", "/tmp"},
)
require.NoError(t, err)
return f
}(),
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
sysCtx, err := tc.input.(*fsConfig).toFS()
require.NoError(t, err)
require.Equal(t, tc.expected, sysCtx)
})
}
}
func TestFSConfig_Errors(t *testing.T) {
tests := []struct {
name string
input FSConfig
expectedErr string
}{
{
name: "multi-level path not yet supported",
input: NewFSConfig().WithDirMount(".", "/usr/bin"),
expectedErr: "only single-level guest paths allowed: [.:/usr/bin]",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
_, err := tc.input.(*fsConfig).toFS()
require.EqualError(t, err, tc.expectedErr)
})
}
}
func TestFSConfig_clone(t *testing.T) {
fc := NewFSConfig().(*fsConfig)
fc.guestPathToFS["/"] = 0
cloned := fc.clone()
// Make post-clone changes
fc.guestPaths = []string{"/"}
fc.guestPathToFS["/"] = 1
// Ensure the guestPathToFS map is not shared
require.Equal(t, map[string]int{"/": 1}, fc.guestPathToFS)
require.Equal(t, map[string]int{"/": 0}, cloned.guestPathToFS)
// Ensure the guestPaths slice is not shared
require.Zero(t, len(cloned.guestPaths))
}

View File

@@ -14,7 +14,6 @@ import (
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental/writefs"
"github.com/tetratelabs/wazero/internal/fstest"
"github.com/tetratelabs/wazero/internal/leb128"
"github.com/tetratelabs/wazero/internal/sys"
@@ -720,9 +719,8 @@ func Test_fdPread_Errors(t *testing.T) {
}
func Test_fdPrestatGet(t *testing.T) {
testFS := writefs.NewDirFS(t.TempDir())
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
fsConfig := wazero.NewFSConfig().WithDirMount(t.TempDir(), "/")
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFSConfig(fsConfig))
defer r.Close(testCtx)
dirFD := sys.FdPreopen
@@ -806,9 +804,8 @@ func Test_fdPrestatGet_Errors(t *testing.T) {
}
func Test_fdPrestatDirName(t *testing.T) {
testFS := writefs.NewDirFS(t.TempDir())
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
fsConfig := wazero.NewFSConfig().WithDirMount(t.TempDir(), "/")
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFSConfig(fsConfig))
defer r.Close(testCtx)
dirFD := sys.FdPreopen
@@ -2282,9 +2279,8 @@ func Test_fdWrite_Errors(t *testing.T) {
func Test_pathCreateDirectory(t *testing.T) {
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
testFS := writefs.NewDirFS(tmpDir)
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
fsConfig := wazero.NewFSConfig().WithDirMount(tmpDir, "/")
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFSConfig(fsConfig))
defer r.Close(testCtx)
// set up the initial memory to include the path name starting at an offset.
@@ -2312,9 +2308,8 @@ func Test_pathCreateDirectory(t *testing.T) {
func Test_pathCreateDirectory_Errors(t *testing.T) {
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
testFS := writefs.NewDirFS(tmpDir)
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
fsConfig := wazero.NewFSConfig().WithDirMount(tmpDir, "/")
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFSConfig(fsConfig))
defer r.Close(testCtx)
file := "file"
@@ -2820,7 +2815,7 @@ func Test_pathOpen(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().
WithFS(&sysfs.FSHolder{FS: tc.fs}))
WithFS(tc.fs.(fs.FS))) // built-in impls reverse-implement fs.FS
defer r.Close(testCtx)
pathName := tc.path(t)
mod.Memory().Write(0, []byte(pathName))
@@ -2890,9 +2885,8 @@ func writeFile(t *testing.T, tmpDir, file string, contents []byte) {
func Test_pathOpen_Errors(t *testing.T) {
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
testFS := writefs.NewDirFS(tmpDir)
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
fsConfig := wazero.NewFSConfig().WithDirMount(tmpDir, "/")
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFSConfig(fsConfig))
defer r.Close(testCtx)
preopenedFD := sys.FdPreopen
@@ -3033,9 +3027,8 @@ func Test_pathReadlink(t *testing.T) {
func Test_pathRemoveDirectory(t *testing.T) {
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
testFS := writefs.NewDirFS(tmpDir)
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
fsConfig := wazero.NewFSConfig().WithDirMount(tmpDir, "/")
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFSConfig(fsConfig))
defer r.Close(testCtx)
// set up the initial memory to include the path name starting at an offset.
@@ -3065,9 +3058,8 @@ func Test_pathRemoveDirectory(t *testing.T) {
func Test_pathRemoveDirectory_Errors(t *testing.T) {
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
testFS := writefs.NewDirFS(tmpDir)
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
fsConfig := wazero.NewFSConfig().WithDirMount(tmpDir, "/")
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFSConfig(fsConfig))
defer r.Close(testCtx)
file := "file"
@@ -3202,9 +3194,8 @@ func Test_pathSymlink(t *testing.T) {
func Test_pathRename(t *testing.T) {
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
testFS := writefs.NewDirFS(tmpDir)
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
fsConfig := wazero.NewFSConfig().WithDirMount(tmpDir, "/")
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFSConfig(fsConfig))
defer r.Close(testCtx)
// set up the initial memory to include the old path name starting at an offset.
@@ -3245,9 +3236,8 @@ func Test_pathRename(t *testing.T) {
func Test_pathRename_Errors(t *testing.T) {
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
testFS := writefs.NewDirFS(tmpDir)
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
fsConfig := wazero.NewFSConfig().WithDirMount(tmpDir, "/")
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFSConfig(fsConfig))
defer r.Close(testCtx)
file := "file"
@@ -3420,9 +3410,8 @@ func Test_pathRename_Errors(t *testing.T) {
func Test_pathUnlinkFile(t *testing.T) {
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
testFS := writefs.NewDirFS(tmpDir)
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
fsConfig := wazero.NewFSConfig().WithDirMount(tmpDir, "/")
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFSConfig(fsConfig))
defer r.Close(testCtx)
// set up the initial memory to include the path name starting at an offset.
@@ -3452,9 +3441,8 @@ func Test_pathUnlinkFile(t *testing.T) {
func Test_pathUnlinkFile_Errors(t *testing.T) {
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
testFS := writefs.NewDirFS(tmpDir)
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
fsConfig := wazero.NewFSConfig().WithDirMount(tmpDir, "/")
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFSConfig(fsConfig))
defer r.Close(testCtx)
file := "file"
@@ -3562,15 +3550,16 @@ func requireOpenFile(t *testing.T, tmpDir string, pathName string, data []byte,
require.NoError(t, os.WriteFile(realPath, data, 0o600))
}
testFS := sysfs.NewDirFS(tmpDir)
fsConfig := wazero.NewFSConfig()
if readOnly {
oflags = os.O_RDONLY
testFS = sysfs.NewReadFS(testFS)
fsConfig = fsConfig.WithReadOnlyDirMount(tmpDir, "/")
} else {
fsConfig = fsConfig.WithDirMount(tmpDir, "/")
}
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().
WithFS(&sysfs.FSHolder{FS: testFS}))
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFSConfig(fsConfig))
fsc := mod.(*wasm.CallContext).Sys.FS()
fd, err := fsc.OpenFile(pathName, oflags, 0)

View File

@@ -6,7 +6,6 @@ import (
"testing"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/experimental/writefs"
"github.com/tetratelabs/wazero/internal/fstest"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/testing/require"
@@ -44,9 +43,8 @@ func Test_testfs(t *testing.T) {
require.NoError(t, os.Mkdir(testfsDir, 0o700))
require.NoError(t, fstest.WriteTestFiles(testfsDir))
testFS := writefs.NewDirFS(tmpDir)
stdout, stderr, err := compileAndRun(testCtx, "testfs", wazero.NewModuleConfig().WithFS(testFS))
fsConfig := wazero.NewFSConfig().WithDirMount(tmpDir, "/")
stdout, stderr, err := compileAndRun(testCtx, "testfs", wazero.NewModuleConfig().WithFSConfig(fsConfig))
require.Zero(t, stderr)
require.EqualError(t, err, `module "" closed with exit_code(0)`)
@@ -56,12 +54,12 @@ func Test_testfs(t *testing.T) {
func Test_writefs(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
testFS := writefs.NewDirFS(tmpDir)
fsConfig := wazero.NewFSConfig().WithDirMount(tmpDir, "/")
// test expects to write under /tmp
require.NoError(t, os.Mkdir(path.Join(tmpDir, "tmp"), 0o700))
stdout, stderr, err := compileAndRun(testCtx, "writefs", wazero.NewModuleConfig().WithFS(testFS))
stdout, stderr, err := compileAndRun(testCtx, "writefs", wazero.NewModuleConfig().WithFSConfig(fsConfig))
require.Zero(t, stderr)
require.EqualError(t, err, `module "" closed with exit_code(0)`)

View File

@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"time"
"github.com/tetratelabs/wazero/internal/platform"
@@ -109,7 +108,7 @@ func (eofReader) Read([]byte) (int, error) {
}
// DefaultContext returns Context with no values set except a possible nil fs.FS
func DefaultContext(fs fs.FS) *Context {
func DefaultContext(fs sysfs.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 {
@@ -135,7 +134,7 @@ func NewContext(
nanotime *sys.Nanotime,
nanotimeResolution sys.ClockResolution,
nanosleep *sys.Nanosleep,
fs fs.FS,
fs sysfs.FS,
) (sysCtx *Context, err error) {
sysCtx = &Context{args: args, environ: environ}
@@ -182,7 +181,7 @@ func NewContext(
}
if fs != nil {
sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, sysfs.Adapt(fs))
sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, fs)
} else {
sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, sysfs.UnimplementedFS{})
}

View File

@@ -22,6 +22,8 @@ func TestContext_FS(t *testing.T) {
}
func TestDefaultSysContext(t *testing.T) {
testFS := sysfs.Adapt(testfs.FS{})
sysCtx, err := NewContext(
0, // max
nil, // args
@@ -33,7 +35,7 @@ func TestDefaultSysContext(t *testing.T) {
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // nanosleep
testfs.FS{}, // fs
testFS, // fs
)
require.NoError(t, err)
@@ -51,7 +53,6 @@ func TestDefaultSysContext(t *testing.T) {
require.Equal(t, &ns, sysCtx.nanosleep)
require.Equal(t, platform.NewFakeRandSource(), sysCtx.RandSource())
testFS := sysfs.Adapt(testfs.FS{})
expectedFS, _ := NewFSContext(nil, nil, nil, testFS)
expectedOpenedFiles := FileTable{}

View File

@@ -5,6 +5,8 @@ import (
"io/fs"
"os"
pathutil "path"
"runtime"
"strings"
)
// Adapt adapts the input to FS unless it is already one. NewDirFS should be
@@ -15,8 +17,8 @@ import (
// documentation does not require the file to be present. In summary, we can't
// enforce flag behavior.
func Adapt(fs fs.FS) FS {
if sys, ok := fs.(*FSHolder); ok {
return sys.FS
if sys, ok := fs.(FS); ok {
return sys
}
return &adapter{fs: fs}
}
@@ -31,6 +33,11 @@ func (a *adapter) String() string {
return fmt.Sprintf("%v", a.fs)
}
// Open implements the same method as documented on fs.FS
func (a *adapter) Open(name string) (fs.File, error) {
return a.fs.Open(name)
}
// OpenFile implements FS.OpenFile
func (a *adapter) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) {
path = cleanPath(path)
@@ -57,3 +64,26 @@ func cleanPath(name string) string {
cleaned = pathutil.Clean(cleaned) // e.g. "sub/." -> "sub"
return cleaned
}
// fsOpen implements the Open method as documented on fs.FS
func fsOpen(f FS, name string) (fs.File, error) {
if !fs.ValidPath(name) { // FS.OpenFile has fewer constraints than fs.FS
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
}
// This isn't a production-grade fs.FS implementation. The only special
// cases we address here are to pass testfs.TestFS.
if runtime.GOOS == "windows" {
switch {
case strings.Contains(name, "\\"):
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
}
}
if f, err := f.OpenFile(name, os.O_RDONLY, 0); err != nil {
return nil, &fs.PathError{Op: "open", Path: name, Err: err}
} else {
return f, nil
}
}

View File

@@ -159,7 +159,7 @@ func TestAdapt_TestFS(t *testing.T) {
testFS := Adapt(tc.fs)
// Adapt it back to fs.FS and run the tests
require.NoError(t, fstest.TestFS(&testFSAdapter{testFS}))
require.NoError(t, fstest.TestFS(testFS.(fs.FS)))
})
}
}

View File

@@ -33,6 +33,11 @@ func (d *dirFS) String() string {
return d.dir
}
// Open implements the same method as documented on fs.FS
func (d *dirFS) Open(name string) (fs.File, error) {
return fsOpen(d, name)
}
// OpenFile implements FS.OpenFile
func (d *dirFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) {
f, err := os.OpenFile(d.join(name), flag, perm)

View File

@@ -380,5 +380,5 @@ func TestDirFS_TestFS(t *testing.T) {
testFS := NewDirFS(tmpDir)
// Run TestFS via the adapter
require.NoError(t, fstest.TestFS(&testFSAdapter{testFS}))
require.NoError(t, fstest.TestFS(testFS.(fs.FS)))
}

View File

@@ -32,6 +32,11 @@ func (r *readFS) String() string {
return r.fs.String()
}
// Open implements the same method as documented on fs.FS
func (r *readFS) Open(name string) (fs.File, error) {
return fsOpen(r, name)
}
// OpenFile implements FS.OpenFile
func (r *readFS) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) {
// TODO: Once the real implementation is complete, move the below to

View File

@@ -123,5 +123,5 @@ func TestReadFS_TestFS(t *testing.T) {
testFS = NewReadFS(testFS)
// Run TestFS via the adapter
require.NoError(t, fstest.TestFS(&testFSAdapter{testFS}))
require.NoError(t, fstest.TestFS(testFS.(fs.FS)))
}

View File

@@ -56,7 +56,7 @@ func NewRootFS(fs []FS, guestPaths []string) (FS, error) {
if ret.rootIndex == -1 {
ret.rootIndex = len(fs)
ret.guestPaths = append(ret.guestPaths, "")
ret.fs = append(ret.fs, fakeRootFS{})
ret.fs = append(ret.fs, &fakeRootFS{})
}
return ret, nil
}
@@ -106,13 +106,19 @@ func writeMount(ret *strings.Builder, f FS, guestPath string) {
func (c *CompositeFS) Unwrap() []FS {
result := make([]FS, 0, len(c.fs))
for i := len(c.fs) - 1; i >= 0; i-- {
if fs := c.fs[i]; fs != (fakeRootFS{}) {
fs := c.fs[i]
if _, ok := fs.(*fakeRootFS); !ok {
result = append(result, fs)
}
}
return result
}
// Open implements the same method as documented on fs.FS
func (c *CompositeFS) Open(name string) (fs.File, error) {
return fsOpen(c, name)
}
// OpenFile implements FS.OpenFile
func (c *CompositeFS) OpenFile(path string, flag int, perm fs.FileMode) (f fs.File, err error) {
matchIndex, relativePath := c.chooseFS(path)
@@ -401,7 +407,7 @@ loop:
type fakeRootFS struct{ UnimplementedFS }
// OpenFile implements FS.OpenFile
func (fakeRootFS) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) {
func (*fakeRootFS) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) {
switch path {
case ".", "/", "":
return fakeRootDir{}, nil

View File

@@ -173,7 +173,7 @@ func TestRootFS_TestFS(t *testing.T) {
require.NoError(t, err)
// Run TestFS via the adapter
require.NoError(t, fstest.TestFS(&testFSAdapter{testFS}))
require.NoError(t, fstest.TestFS(testFS.(fs.FS)))
}
func TestRootFS_examples(t *testing.T) {

View File

@@ -6,24 +6,12 @@
package sysfs
import (
"fmt"
"io"
"io/fs"
"os"
"syscall"
)
// FSHolder implements fs.FS in order to pass an FS until configuration
// supports it natively.
type FSHolder struct {
FS FS
}
// Open implements the same method as documented on fs.FS
func (*FSHolder) Open(name string) (fs.File, error) {
panic(fmt.Errorf("unexpected to call fs.FS.Open(%s)", name))
}
// FS is a writeable fs.FS bridge backed by syscall functions needed for ABI
// including WASI and runtime.GOOS=js.
//

View File

@@ -10,7 +10,6 @@ import (
"path"
"runtime"
"sort"
"strings"
"syscall"
"testing"
"testing/fstest"
@@ -231,30 +230,6 @@ func testUtimes(t *testing.T, tmpDir string, testFS FS) {
}
}
// testFSAdapter implements fs.FS only to use fstest.TestFS
type testFSAdapter struct {
fs FS
}
// Open implements the same method as documented on fs.FS
func (f *testFSAdapter) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) { // FS.OpenFile has fewer constraints than fs.FS
return nil, os.ErrInvalid
}
// This isn't a production-grade fs.FS implementation. The only special
// cases we address here are to pass testfs.TestFS.
if runtime.GOOS == "windows" {
switch {
case strings.Contains(name, "\\"):
return nil, os.ErrInvalid
}
}
return f.fs.OpenFile(name, os.O_RDONLY, 0)
}
// requireErrno should only be used for functions that wrap the underlying
// syscall.Errno.
func requireErrno(t *testing.T, expected syscall.Errno, actual error) {

View File

@@ -14,6 +14,11 @@ func (UnimplementedFS) String() string {
return "Unimplemented:/"
}
// Open implements the same method as documented on fs.FS
func (UnimplementedFS) Open(name string) (fs.File, error) {
return nil, &fs.PathError{Op: "open", Path: name, Err: syscall.ENOSYS}
}
// OpenFile implements FS.OpenFile
func (UnimplementedFS) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) {
return nil, syscall.ENOSYS

View File

@@ -8,6 +8,7 @@ import (
"testing"
"github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/sysfs"
testfs "github.com/tetratelabs/wazero/internal/testing/fs"
"github.com/tetratelabs/wazero/internal/testing/require"
)
@@ -144,7 +145,7 @@ func TestCallContext_Close(t *testing.T) {
}
t.Run("calls Context.Close()", func(t *testing.T) {
sysCtx := sys.DefaultContext(testfs.FS{"foo": &testfs.File{}})
sysCtx := sys.DefaultContext(sysfs.Adapt(testfs.FS{"foo": &testfs.File{}}))
fsCtx := sysCtx.FS()
_, err := fsCtx.OpenFile("/foo", os.O_RDONLY, 0)
@@ -172,7 +173,7 @@ 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.
testFS := testfs.FS{"foo": &testfs.File{CloseErr: errors.New("error closing")}}
sysCtx := sys.DefaultContext(testFS)
sysCtx := sys.DefaultContext(sysfs.Adapt(testFS))
fsCtx := sysCtx.FS()
_, err := fsCtx.OpenFile("/foo", os.O_RDONLY, 0)
@@ -240,7 +241,7 @@ func TestCallContext_CallDynamic(t *testing.T) {
}
t.Run("calls Context.Close()", func(t *testing.T) {
sysCtx := sys.DefaultContext(testfs.FS{"foo": &testfs.File{}})
sysCtx := sys.DefaultContext(sysfs.Adapt(testfs.FS{"foo": &testfs.File{}}))
fsCtx := sysCtx.FS()
_, err := fsCtx.OpenFile("/foo", os.O_RDONLY, 0)
@@ -268,7 +269,7 @@ func TestCallContext_CallDynamic(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.
testFS := testfs.FS{"foo": &testfs.File{CloseErr: errors.New("error closing")}}
sysCtx := sys.DefaultContext(testFS)
sysCtx := sys.DefaultContext(sysfs.Adapt(testFS))
fsCtx := sysCtx.FS()
path := "/foo"

2
testdata/index.html vendored Normal file
View File

@@ -0,0 +1,2 @@
<!DOCTYPE html>
<html></html>