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:
@@ -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) {
|
||||
|
||||
61
config.go
61
config.go
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)}
|
||||
}
|
||||
@@ -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
165
fsconfig.go
Normal 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
27
fsconfig_example_test.go
Normal 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
106
fsconfig_test.go
Normal 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))
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)`)
|
||||
|
||||
@@ -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{})
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
2
testdata/index.html
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
<!DOCTYPE html>
|
||||
<html></html>
|
||||
Reference in New Issue
Block a user