Allows wasip1 guests to use arbitrarily nested pre-opens (#1536)

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2023-06-27 10:02:57 +08:00
committed by GitHub
parent 451a1b63a0
commit 53ce5eea83
12 changed files with 218 additions and 1174 deletions

View File

@@ -23,6 +23,7 @@ import (
"github.com/tetratelabs/wazero/experimental/sock"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
"github.com/tetratelabs/wazero/internal/platform"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/version"
"github.com/tetratelabs/wazero/sys"
)
@@ -336,6 +337,14 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer) int {
_, err = rt.InstantiateModule(ctx, code, conf)
}
case modeGo:
// Fail fast on multiple mounts with the deprecated GOOS=js.
// GOOS=js will be removed in favor of GOOS=wasip1 once v1.22 is out.
if count := len(mounts); count > 1 || (count == 1 && rootPath == "") {
fmt.Fprintf(stdErr, "invalid mount: only root mounts supported in GOOS=js: %v\n"+
"Consider switching to GOOS=wasip1.\n", mounts)
return 1
}
gojs.MustInstantiate(ctx, rt)
config := gojs.NewConfig(conf).WithOSUser()
@@ -394,11 +403,6 @@ func validateMounts(mounts sliceFlag, stdErr logging.Writer) (rc int, rootPath s
guestPath = dir
}
// Provide a better experience if duplicates are found later.
if guestPath == "" {
guestPath = "/"
}
// Eagerly validate the mounts as we know they should be on the host.
if abs, err := filepath.Abs(dir); err != nil {
fmt.Fprintf(stdErr, "invalid mount: path %q invalid: %v\n", dir, err)
@@ -420,7 +424,7 @@ func validateMounts(mounts sliceFlag, stdErr logging.Writer) (rc int, rootPath s
config = config.WithDirMount(dir, guestPath)
}
if guestPath == "/" {
if internalsys.StripPrefixesAndTrailingSlash(guestPath) == "" {
rootPath = dir
}
}

View File

@@ -397,8 +397,8 @@ func TestRun(t *testing.T) {
{
name: "GOARCH=wasm GOOS=js hostlogging=proc",
wasm: wasmCatGo,
wazeroOpts: []string{"--hostlogging=proc", fmt.Sprintf("--mount=%s:/animals:ro", bearDir)},
wasmArgs: []string{"/animals/not-bear.txt"},
wazeroOpts: []string{"--hostlogging=proc", fmt.Sprintf("--mount=%s:/:ro", bearDir)},
wasmArgs: []string{"/not-bear.txt"},
expectedStderr: `==> go.runtime.wasmExit(code=1)
<==
`,
@@ -424,6 +424,16 @@ func TestRun(t *testing.T) {
<== (err=<nil>,ok=true)
`, bearMode, bearMtime),
},
{
name: "GOARCH=wasm GOOS=js not root mount",
wasm: wasmCatGo,
wazeroOpts: []string{"--hostlogging=proc", fmt.Sprintf("--mount=%s:/animals:ro", bearDir)},
wasmArgs: []string{"/not-bear.txt"},
expectedStderr: fmt.Sprintf(`invalid mount: only root mounts supported in GOOS=js: [%s:/animals:ro]
Consider switching to GOOS=wasip1.
`, bearDir),
expectedExitCode: 1,
},
{
name: "cachedir existing absolute",
wazeroOpts: []string{"--cachedir=" + existingDir1},

View File

@@ -839,11 +839,10 @@ func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) {
environ = append(environ, result)
}
var fs fsapi.FS
var fs []fsapi.FS
var guestPaths []string
if f, ok := c.fsConfig.(*fsConfig); ok {
if fs, err = f.toFS(); err != nil {
return
}
fs, guestPaths = f.preopens()
}
var listeners []*net.TCPListener
@@ -864,7 +863,7 @@ func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) {
c.walltime, c.walltimeResolution,
c.nanotime, c.nanotimeResolution,
c.nanosleep, c.osyield,
fs,
fs, guestPaths,
listeners,
)
}

View File

@@ -4,6 +4,7 @@ import (
"io/fs"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/sysfs"
)
@@ -30,8 +31,7 @@ import (
//
// 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.
// It only works with root mounts ("").
// - 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.
@@ -167,7 +167,10 @@ func (c *fsConfig) WithFSMount(fs fs.FS, guestPath string) FSConfig {
}
func (c *fsConfig) withMount(fs fsapi.FS, guestPath string) FSConfig {
cleaned := sysfs.StripPrefixesAndTrailingSlash(guestPath)
if _, ok := fs.(fsapi.UnimplementedFS); ok {
return c // don't add fake paths.
}
cleaned := sys.StripPrefixesAndTrailingSlash(guestPath)
ret := c.clone()
if i, ok := ret.guestPathToFS[cleaned]; ok {
ret.fs[i] = fs
@@ -180,6 +183,16 @@ func (c *fsConfig) withMount(fs fsapi.FS, guestPath string) FSConfig {
return ret
}
func (c *fsConfig) toFS() (fsapi.FS, error) {
return sysfs.NewRootFS(c.fs, c.guestPaths)
// preopens returns the possible nil index-correlated preopened filesystems
// with guest paths.
func (c *fsConfig) preopens() ([]fsapi.FS, []string) {
preopenCount := len(c.fs)
if preopenCount == 0 {
return nil, nil
}
fs := make([]fsapi.FS, len(c.fs))
copy(fs, c.fs)
guestPaths := make([]string, len(c.guestPaths))
copy(guestPaths, c.guestPaths)
return fs, guestPaths
}

View File

@@ -9,7 +9,7 @@ import (
"github.com/tetratelabs/wazero/internal/testing/require"
)
// TestFSConfig only tests the cases that change the inputs to sysfs.NewRootFS.
// TestFSConfig only tests the cases that change the inputs to sysfs.ValidatePreopens.
func TestFSConfig(t *testing.T) {
base := NewFSConfig()
@@ -17,46 +17,42 @@ func TestFSConfig(t *testing.T) {
testFS2 := testfs.FS{"/": &testfs.File{}}
tests := []struct {
name string
input FSConfig
expected fsapi.FS
name string
input FSConfig
expectedFS []fsapi.FS
expectedGuestPaths []string
}{
{
name: "empty",
input: base,
expected: fsapi.UnimplementedFS{},
name: "empty",
input: base,
},
{
name: "WithFSMount",
input: base.WithFSMount(testFS, "/"),
expected: sysfs.Adapt(testFS),
name: "WithFSMount",
input: base.WithFSMount(testFS, "/"),
expectedFS: []fsapi.FS{sysfs.Adapt(testFS)},
expectedGuestPaths: []string{"/"},
},
{
name: "WithFSMount overwrites",
input: base.WithFSMount(testFS, "/").WithFSMount(testFS2, "/"),
expected: sysfs.Adapt(testFS2),
name: "WithFSMount overwrites",
input: base.WithFSMount(testFS, "/").WithFSMount(testFS2, "/"),
expectedFS: []fsapi.FS{sysfs.Adapt(testFS2)},
expectedGuestPaths: []string{"/"},
},
{
name: "WithFsMount nil",
input: base.WithFSMount(nil, "/"),
expected: fsapi.UnimplementedFS{},
name: "WithFsMount nil",
input: base.WithFSMount(nil, "/"),
},
{
name: "WithDirMount overwrites",
input: base.WithFSMount(testFS, "/").WithDirMount(".", "/"),
expected: sysfs.NewDirFS("."),
name: "WithDirMount overwrites",
input: base.WithFSMount(testFS, "/").WithDirMount(".", "/"),
expectedFS: []fsapi.FS{sysfs.NewDirFS(".")},
expectedGuestPaths: []string{"/"},
},
{
name: "Composition",
input: base.WithReadOnlyDirMount(".", "/").WithDirMount("/tmp", "/tmp"),
expected: func() fsapi.FS {
f, err := sysfs.NewRootFS(
[]fsapi.FS{sysfs.NewReadFS(sysfs.NewDirFS(".")), sysfs.NewDirFS("/tmp")},
[]string{"/", "/tmp"},
)
require.NoError(t, err)
return f
}(),
name: "multiple",
input: base.WithReadOnlyDirMount(".", "/").WithDirMount("/tmp", "/tmp"),
expectedFS: []fsapi.FS{sysfs.NewReadFS(sysfs.NewDirFS(".")), sysfs.NewDirFS("/tmp")},
expectedGuestPaths: []string{"/", "/tmp"},
},
}
@@ -64,31 +60,9 @@ func TestFSConfig(t *testing.T) {
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)
fs, guestPaths := tc.input.(*fsConfig).preopens()
require.Equal(t, tc.expectedFS, fs)
require.Equal(t, tc.expectedGuestPaths, guestPaths)
})
}
}

View File

@@ -278,14 +278,21 @@ type FSContext struct {
// descriptors to file entries.
type FileTable = descriptor.Table[int32, *FileEntry]
// ReaddirTable is a specialization of the descriptor.Table type used to map file
// descriptors to Readdir structs.
// ReaddirTable is a specialization of the descriptor.Table type used to map
// file descriptors to Readdir structs.
type ReaddirTable = descriptor.Table[int32, *Readdir]
// RootFS returns the underlying filesystem. Any files that should be added to
// the table should be inserted via InsertFile.
// RootFS returns a possibly unimplemented root filesystem. Any files that
// should be added to the table should be inserted via InsertFile.
//
// TODO: This is only used by GOOS=js and tests: Remove when we remove GOOS=js
// (after Go 1.22 is released).
func (c *FSContext) RootFS() fsapi.FS {
return c.rootFS
if rootFS := c.rootFS; rootFS == nil {
return fsapi.UnimplementedFS{}
} else {
return rootFS
}
}
// OpenFile opens the file into the table and returns its file descriptor.
@@ -426,18 +433,14 @@ func (c *FSContext) Close() (err error) {
return
}
// NewFSContext creates a FSContext with stdio streams and an optional
// pre-opened filesystem.
//
// If `preopened` is not UnimplementedFS, it is inserted into
// the file descriptor table as FdPreopen.
func (c *Context) NewFSContext(
// InitFSContext initializes a FSContext with stdio streams and optional
// pre-opened filesystems and TCP listeners.
func (c *Context) InitFSContext(
stdin io.Reader,
stdout, stderr io.Writer,
rootFS fsapi.FS,
fs []fsapi.FS, guestPaths []string,
tcpListeners []*net.TCPListener,
) (err error) {
c.fsc.rootFS = rootFS
inFile, err := stdinFileEntry(stdin)
if err != nil {
return err
@@ -454,24 +457,17 @@ func (c *Context) NewFSContext(
}
c.fsc.openedFiles.Insert(errWriter)
if _, ok := rootFS.(fsapi.UnimplementedFS); ok {
// don't add to the pre-opens
} else if comp, ok := rootFS.(*sysfs.CompositeFS); ok {
preopens := comp.FS()
for i, p := range comp.GuestPaths() {
c.fsc.openedFiles.Insert(&FileEntry{
FS: preopens[i],
Name: p,
IsPreopen: true,
File: &lazyDir{fs: rootFS},
})
for i, fs := range fs {
guestPath := guestPaths[i]
if StripPrefixesAndTrailingSlash(guestPath) == "" {
c.fsc.rootFS = fs
}
} else {
c.fsc.openedFiles.Insert(&FileEntry{
FS: rootFS,
Name: "/",
FS: fs,
Name: guestPath,
IsPreopen: true,
File: &lazyDir{fs: rootFS},
File: &lazyDir{fs: fs},
})
}
@@ -480,3 +476,42 @@ func (c *Context) NewFSContext(
}
return nil
}
// StripPrefixesAndTrailingSlash skips any leading "./" or "/" such that the
// result index begins with another string. A result of "." coerces to the
// empty string "" because the current directory is handled by the guest.
//
// Results are the offset/len pair which is an optimization to avoid re-slicing
// overhead, as this function is called for every path operation.
//
// Note: Relative paths should be handled by the guest, as that's what knows
// what the current directory is. However, paths that escape the current
// directory e.g. "../.." have been found in `tinygo test` and this
// implementation takes care to avoid it.
func StripPrefixesAndTrailingSlash(path string) string {
// strip trailing slashes
pathLen := len(path)
for ; pathLen > 0 && path[pathLen-1] == '/'; pathLen-- {
}
pathI := 0
loop:
for pathI < pathLen {
switch path[pathI] {
case '/':
pathI++
case '.':
nextI := pathI + 1
if nextI < pathLen && path[nextI] == '/' {
pathI = nextI + 1
} else if nextI == pathLen {
pathI = nextI
} else {
break loop
}
default:
break loop
}
}
return path[pathI:pathLen]
}

View File

@@ -54,7 +54,7 @@ func TestNewFSContext(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
c := Context{}
err := c.NewFSContext(nil, nil, nil, tc.fs, nil)
err := c.InitFSContext(nil, nil, nil, []fsapi.FS{tc.fs}, []string{"/"}, nil)
require.NoError(t, err)
fsc := c.fsc
defer fsc.Close()
@@ -92,7 +92,7 @@ func TestFSContext_CloseFile(t *testing.T) {
testFS := sysfs.Adapt(embedFS)
c := Context{}
err = c.NewFSContext(nil, nil, nil, testFS, nil)
err = c.InitFSContext(nil, nil, nil, []fsapi.FS{testFS}, []string{"/"}, nil)
require.NoError(t, err)
fsc := c.fsc
defer fsc.Close()
@@ -122,14 +122,14 @@ func TestFSContext_CloseFile(t *testing.T) {
})
}
func TestUnimplementedFSContext(t *testing.T) {
func TestFSContext_noPreopens(t *testing.T) {
c := Context{}
err := c.NewFSContext(nil, nil, nil, fsapi.UnimplementedFS{}, nil)
err := c.InitFSContext(nil, nil, nil, nil, nil, nil)
require.NoError(t, err)
testFS := &c.fsc
require.NoError(t, err)
expected := &FSContext{rootFS: fsapi.UnimplementedFS{}}
expected := &FSContext{}
noopStdin, _ := stdinFileEntry(nil)
expected.openedFiles.Insert(noopStdin)
noopStdout, _ := stdioWriterFileEntry("stdout", nil)
@@ -142,39 +142,7 @@ func TestUnimplementedFSContext(t *testing.T) {
require.NoError(t, err)
// Closes opened files
require.Equal(t, &FSContext{rootFS: fsapi.UnimplementedFS{}}, testFS)
})
}
func TestCompositeFSContext(t *testing.T) {
tmpDir1 := t.TempDir()
testFS1 := sysfs.NewDirFS(tmpDir1)
tmpDir2 := t.TempDir()
testFS2 := sysfs.NewDirFS(tmpDir2)
rootFS, err := sysfs.NewRootFS([]fsapi.FS{testFS2, testFS1}, []string{"/tmp", "/"})
require.NoError(t, err)
c := Context{}
err = c.NewFSContext(nil, nil, nil, rootFS, nil)
require.NoError(t, err)
testFS := &c.fsc
// Ensure the pre-opens have exactly the name specified, and are in order.
preopen3, ok := testFS.openedFiles.Lookup(3)
require.True(t, ok)
require.Equal(t, "/tmp", preopen3.Name)
preopen4, ok := testFS.openedFiles.Lookup(4)
require.True(t, ok)
require.Equal(t, "/", preopen4.Name)
t.Run("Close closes", func(t *testing.T) {
err := testFS.Close()
require.NoError(t, err)
// Closes opened files
require.Equal(t, &FSContext{rootFS: rootFS}, testFS)
require.Equal(t, &FSContext{}, testFS)
})
}
@@ -182,7 +150,7 @@ func TestContext_Close(t *testing.T) {
testFS := sysfs.Adapt(testfs.FS{"foo": &testfs.File{}})
c := Context{}
err := c.NewFSContext(nil, nil, nil, testFS, nil)
err := c.InitFSContext(nil, nil, nil, []fsapi.FS{testFS}, []string{"/"}, nil)
require.NoError(t, err)
fsc := c.fsc
@@ -209,7 +177,7 @@ func TestContext_Close_Error(t *testing.T) {
testFS := sysfs.Adapt(testfs.FS{"foo": file})
c := Context{}
err := c.NewFSContext(nil, nil, nil, testFS, nil)
err := c.InitFSContext(nil, nil, nil, []fsapi.FS{testFS}, []string{"/"}, nil)
require.NoError(t, err)
fsc := c.fsc
@@ -226,21 +194,21 @@ func TestContext_Close_Error(t *testing.T) {
func TestFSContext_Renumber(t *testing.T) {
tmpDir := t.TempDir()
dirFs := sysfs.NewDirFS(tmpDir)
dirFS := sysfs.NewDirFS(tmpDir)
const dirName = "dir"
errno := dirFs.Mkdir(dirName, 0o700)
errno := dirFS.Mkdir(dirName, 0o700)
require.EqualErrno(t, 0, errno)
c := Context{}
err := c.NewFSContext(nil, nil, nil, dirFs, nil)
err := c.InitFSContext(nil, nil, nil, []fsapi.FS{dirFS}, []string{"/"}, nil)
require.NoError(t, err)
fsc := c.fsc
defer fsc.Close()
for _, toFd := range []int32{10, 100, 100} {
fromFd, errno := fsc.OpenFile(dirFs, dirName, os.O_RDONLY, 0)
fromFd, errno := fsc.OpenFile(dirFS, dirName, os.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
prevDirFile, ok := fsc.LookupFile(fromFd)
@@ -358,3 +326,63 @@ func TestReaddDir_Rewind(t *testing.T) {
})
}
}
func TestStripPrefixesAndTrailingSlash(t *testing.T) {
tests := []struct {
path, expected string
}{
{
path: ".",
expected: "",
},
{
path: "/",
expected: "",
},
{
path: "./",
expected: "",
},
{
path: "./foo",
expected: "foo",
},
{
path: ".foo",
expected: ".foo",
},
{
path: "././foo",
expected: "foo",
},
{
path: "/foo",
expected: "foo",
},
{
path: "foo/",
expected: "foo",
},
{
path: "//",
expected: "",
},
{
path: "../../",
expected: "../..",
},
{
path: "./../../",
expected: "../..",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.path, func(t *testing.T) {
path := StripPrefixesAndTrailingSlash(tc.path)
require.Equal(t, tc.expected, path)
})
}
}

View File

@@ -114,7 +114,7 @@ func (c *Context) RandSource() io.Reader {
//
// Note: This is only used for testing.
func DefaultContext(fs fsapi.FS) *Context {
if sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil, nil, fs, nil); err != nil {
if sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil, nil, []fsapi.FS{fs}, []string{""}, nil); err != nil {
panic(fmt.Errorf("BUG: DefaultContext should never error: %w", err))
} else {
return sysCtx
@@ -135,7 +135,7 @@ func NewContext(
nanotimeResolution sys.ClockResolution,
nanosleep sys.Nanosleep,
osyield sys.Osyield,
rootFS fsapi.FS,
fs []fsapi.FS, guestPaths []string,
tcpListeners []*net.TCPListener,
) (sysCtx *Context, err error) {
sysCtx = &Context{args: args, environ: environ}
@@ -188,11 +188,7 @@ func NewContext(
sysCtx.osyield = platform.FakeOsyield
}
if rootFS != nil {
err = sysCtx.NewFSContext(stdin, stdout, stderr, rootFS, tcpListeners)
} else {
err = sysCtx.NewFSContext(stdin, stdout, stderr, fsapi.UnimplementedFS{}, tcpListeners)
}
err = sysCtx.InitFSContext(stdin, stdout, stderr, fs, guestPaths, tcpListeners)
return
}

View File

@@ -5,6 +5,7 @@ import (
"testing"
"time"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/internal/fstest"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/sysfs"
@@ -21,7 +22,7 @@ func TestContext_WalltimeNanos(t *testing.T) {
func TestDefaultSysContext(t *testing.T) {
testFS := sysfs.Adapt(fstest.FS)
sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil, nil, testFS, nil)
sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil, nil, []fsapi.FS{testFS}, []string{"/"}, nil)
require.NoError(t, err)
require.Nil(t, sysCtx.Args())
@@ -92,7 +93,7 @@ func TestNewContext_Args(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
sysCtx, err := NewContext(tc.maxSize, tc.args, nil, bytes.NewReader(make([]byte, 0)), nil, nil, nil, nil, 0, nil, 0, nil, nil, nil, nil)
sysCtx, err := NewContext(tc.maxSize, tc.args, nil, bytes.NewReader(make([]byte, 0)), nil, nil, nil, nil, 0, nil, 0, nil, nil, nil, nil, nil)
if tc.expectedErr == "" {
require.Nil(t, err)
require.Equal(t, tc.args, sysCtx.Args())
@@ -142,7 +143,7 @@ func TestNewContext_Environ(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
sysCtx, err := NewContext(tc.maxSize, nil, tc.environ, bytes.NewReader(make([]byte, 0)), nil, nil, nil, nil, 0, nil, 0, nil, nil, nil, nil)
sysCtx, err := NewContext(tc.maxSize, nil, tc.environ, bytes.NewReader(make([]byte, 0)), nil, nil, nil, nil, 0, nil, 0, nil, nil, nil, nil, nil)
if tc.expectedErr == "" {
require.Nil(t, err)
require.Equal(t, tc.environ, sysCtx.Environ())
@@ -178,7 +179,7 @@ func TestNewContext_Walltime(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, tc.time, tc.resolution, nil, 0, nil, nil, nil, nil)
sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, tc.time, tc.resolution, nil, 0, nil, nil, nil, nil, nil)
if tc.expectedErr == "" {
require.Nil(t, err)
require.Equal(t, tc.time, sysCtx.walltime)
@@ -214,7 +215,7 @@ func TestNewContext_Nanotime(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, tc.time, tc.resolution, nil, nil, nil, nil)
sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, tc.time, tc.resolution, nil, nil, nil, nil, nil)
if tc.expectedErr == "" {
require.Nil(t, err)
require.Equal(t, tc.time, sysCtx.nanotime)
@@ -259,14 +260,14 @@ func Test_clockResolutionInvalid(t *testing.T) {
func TestNewContext_Nanosleep(t *testing.T) {
var aNs sys.Nanosleep = func(int64) {}
sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, aNs, nil, nil, nil)
sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, aNs, nil, nil, nil, nil)
require.Nil(t, err)
require.Equal(t, aNs, sysCtx.nanosleep)
}
func TestNewContext_Osyield(t *testing.T) {
var oy sys.Osyield = func() {}
sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil, oy, nil, nil)
sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil, oy, nil, nil, nil)
require.Nil(t, err)
require.Equal(t, oy, sysCtx.osyield)
}

View File

@@ -1,559 +0,0 @@
package sysfs
import (
"fmt"
"io"
"io/fs"
"strings"
"syscall"
"github.com/tetratelabs/wazero/internal/fsapi"
)
func NewRootFS(fs []fsapi.FS, guestPaths []string) (fsapi.FS, error) {
switch len(fs) {
case 0:
return fsapi.UnimplementedFS{}, nil
case 1:
if StripPrefixesAndTrailingSlash(guestPaths[0]) == "" {
return fs[0], nil
}
}
ret := &CompositeFS{
string: stringFS(fs, guestPaths),
fs: make([]fsapi.FS, len(fs)),
guestPaths: make([]string, len(fs)),
cleanedGuestPaths: make([]string, len(fs)),
rootGuestPaths: map[string]int{},
rootIndex: -1,
}
copy(ret.guestPaths, guestPaths)
copy(ret.fs, fs)
for i, guestPath := range guestPaths {
// Clean the prefix in the same way path matches will.
cleaned := StripPrefixesAndTrailingSlash(guestPath)
if cleaned == "" {
if ret.rootIndex != -1 {
return nil, fmt.Errorf("multiple root filesystems are invalid: %s", ret.string)
}
ret.rootIndex = i
} else if strings.HasPrefix(cleaned, "..") {
// ../ mounts are special cased and aren't returned in a directory
// listing, so we can ignore them for now.
} else if strings.Contains(cleaned, "/") {
return nil, fmt.Errorf("only single-level guest paths allowed: %s", ret.string)
} else {
ret.rootGuestPaths[cleaned] = i
}
ret.cleanedGuestPaths[i] = cleaned
}
// Ensure there is always a root match to keep runtime logic simpler.
if ret.rootIndex == -1 {
ret.rootIndex = len(fs)
ret.cleanedGuestPaths = append(ret.cleanedGuestPaths, "")
ret.fs = append(ret.fs, &fakeRootFS{})
}
return ret, nil
}
type CompositeFS struct {
fsapi.UnimplementedFS
// string is cached for convenience.
string string
// fs is index-correlated with cleanedGuestPaths
fs []fsapi.FS
// guestPaths are the original paths supplied by the end user, cleaned as
// cleanedGuestPaths.
guestPaths []string
// cleanedGuestPaths to match in precedence order, ascending.
cleanedGuestPaths []string
// rootGuestPaths are cleanedGuestPaths that exist directly under root, such as
// "tmp".
rootGuestPaths map[string]int
// rootIndex is the index in fs that is the root filesystem
rootIndex int
}
// String implements fmt.Stringer
func (c *CompositeFS) String() string {
return c.string
}
func stringFS(fs []fsapi.FS, guestPaths []string) string {
var ret strings.Builder
ret.WriteString("[")
writeMount(&ret, fs[0], guestPaths[0])
for i, f := range fs[1:] {
ret.WriteString(" ")
writeMount(&ret, f, guestPaths[i+1])
}
ret.WriteString("]")
return ret.String()
}
func writeMount(ret *strings.Builder, f fsapi.FS, guestPath string) {
ret.WriteString(f.String())
ret.WriteString(":")
ret.WriteString(guestPath)
if _, ok := f.(*readFS); ok {
ret.WriteString(":ro")
}
}
// GuestPaths returns the underlying pre-open paths in original order.
func (c *CompositeFS) GuestPaths() (guestPaths []string) {
return c.guestPaths
}
// FS returns the underlying filesystems in original order.
func (c *CompositeFS) FS() (fs []fsapi.FS) {
fs = make([]fsapi.FS, len(c.guestPaths))
copy(fs, c.fs)
return
}
// OpenFile implements the same method as documented on api.FS
func (c *CompositeFS) OpenFile(path string, flag int, perm fs.FileMode) (f fsapi.File, err syscall.Errno) {
matchIndex, relativePath := c.chooseFS(path)
f, err = c.fs[matchIndex].OpenFile(relativePath, flag, perm)
if err != 0 {
return
}
// Ensure the root directory listing includes any prefix mounts.
if matchIndex == c.rootIndex {
switch path {
case ".", "/", "":
if len(c.rootGuestPaths) > 0 {
f = &openRootDir{path: path, c: c, f: f}
}
}
}
return
}
// An openRootDir is a root directory open for reading, which has mounts inside
// of it.
type openRootDir struct {
fsapi.DirFile
path string
c *CompositeFS
f fsapi.File // the directory file itself
dirents []fsapi.Dirent // the directory contents
direntsI int // the read offset, an index into the files slice
}
// Ino implements the same method as documented on fsapi.File
func (d *openRootDir) Ino() (uint64, syscall.Errno) {
return d.f.Ino()
}
// Stat implements the same method as documented on fsapi.File
func (d *openRootDir) Stat() (fsapi.Stat_t, syscall.Errno) {
return d.f.Stat()
}
// Seek implements the same method as documented on fsapi.File
func (d *openRootDir) Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno) {
if offset != 0 || whence != io.SeekStart {
errno = syscall.ENOSYS
return
}
d.dirents = nil
d.direntsI = 0
return d.f.Seek(offset, whence)
}
// Readdir implements the same method as documented on fsapi.File
func (d *openRootDir) Readdir(count int) (dirents []fsapi.Dirent, errno syscall.Errno) {
if d.dirents == nil {
if errno = d.readdir(); errno != 0 {
return
}
}
// logic similar to go:embed
n := len(d.dirents) - d.direntsI
if n == 0 {
return
}
if count > 0 && n > count {
n = count
}
dirents = make([]fsapi.Dirent, n)
for i := range dirents {
dirents[i] = d.dirents[d.direntsI+i]
}
d.direntsI += n
return
}
func (d *openRootDir) readdir() (errno syscall.Errno) {
// readDir reads the directory fully into d.dirents, replacing any entries that
// correspond to prefix matches or appending them to the end.
if d.dirents, errno = d.f.Readdir(-1); errno != 0 {
return
}
remaining := make(map[string]int, len(d.c.rootGuestPaths))
for k, v := range d.c.rootGuestPaths {
remaining[k] = v
}
for i := range d.dirents {
e := d.dirents[i]
if fsI, ok := remaining[e.Name]; ok {
if d.dirents[i], errno = d.rootEntry(e.Name, fsI); errno != 0 {
return
}
delete(remaining, e.Name)
}
}
var di fsapi.Dirent
for n, fsI := range remaining {
if di, errno = d.rootEntry(n, fsI); errno != 0 {
return
}
d.dirents = append(d.dirents, di)
}
return
}
// Sync implements the same method as documented on fsapi.File
func (d *openRootDir) Sync() syscall.Errno {
return d.f.Sync()
}
// Datasync implements the same method as documented on fsapi.File
func (d *openRootDir) Datasync() syscall.Errno {
return d.f.Datasync()
}
// Chmod implements the same method as documented on fsapi.File
func (d *openRootDir) Chmod(fs.FileMode) syscall.Errno {
return syscall.ENOSYS
}
// Chown implements the same method as documented on fsapi.File
func (d *openRootDir) Chown(int, int) syscall.Errno {
return syscall.ENOSYS
}
// Utimens implements the same method as documented on fsapi.File
func (d *openRootDir) Utimens(*[2]syscall.Timespec) syscall.Errno {
return syscall.ENOSYS
}
// Close implements fs.File
func (d *openRootDir) Close() syscall.Errno {
return d.f.Close()
}
func (d *openRootDir) rootEntry(name string, fsI int) (fsapi.Dirent, syscall.Errno) {
if st, errno := d.c.fs[fsI].Stat("."); errno != 0 {
return fsapi.Dirent{}, errno
} else {
return fsapi.Dirent{Name: name, Ino: st.Ino, Type: st.Mode.Type()}, 0
}
}
// Lstat implements the same method as documented on api.FS
func (c *CompositeFS) Lstat(path string) (fsapi.Stat_t, syscall.Errno) {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Lstat(relativePath)
}
// Stat implements the same method as documented on api.FS
func (c *CompositeFS) Stat(path string) (fsapi.Stat_t, syscall.Errno) {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Stat(relativePath)
}
// Mkdir implements the same method as documented on api.FS
func (c *CompositeFS) Mkdir(path string, perm fs.FileMode) syscall.Errno {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Mkdir(relativePath, perm)
}
// Chmod implements the same method as documented on api.FS
func (c *CompositeFS) Chmod(path string, perm fs.FileMode) syscall.Errno {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Chmod(relativePath, perm)
}
// Chown implements the same method as documented on api.FS
func (c *CompositeFS) Chown(path string, uid, gid int) syscall.Errno {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Chown(relativePath, uid, gid)
}
// Lchown implements the same method as documented on api.FS
func (c *CompositeFS) Lchown(path string, uid, gid int) syscall.Errno {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Lchown(relativePath, uid, gid)
}
// Rename implements the same method as documented on api.FS
func (c *CompositeFS) Rename(from, to string) syscall.Errno {
fromFS, fromPath := c.chooseFS(from)
toFS, toPath := c.chooseFS(to)
if fromFS != toFS {
return syscall.ENOSYS // not yet anyway
}
return c.fs[fromFS].Rename(fromPath, toPath)
}
// Readlink implements the same method as documented on api.FS
func (c *CompositeFS) Readlink(path string) (string, syscall.Errno) {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Readlink(relativePath)
}
// Link implements the same method as documented on api.FS
func (c *CompositeFS) Link(oldName, newName string) syscall.Errno {
fromFS, oldNamePath := c.chooseFS(oldName)
toFS, newNamePath := c.chooseFS(newName)
if fromFS != toFS {
return syscall.ENOSYS // not yet anyway
}
return c.fs[fromFS].Link(oldNamePath, newNamePath)
}
// Utimens implements the same method as documented on api.FS
func (c *CompositeFS) Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Utimens(relativePath, times, symlinkFollow)
}
// Symlink implements the same method as documented on api.FS
func (c *CompositeFS) Symlink(oldName, link string) (err syscall.Errno) {
fromFS, oldNamePath := c.chooseFS(oldName)
toFS, linkPath := c.chooseFS(link)
if fromFS != toFS {
return syscall.ENOSYS // not yet anyway
}
return c.fs[fromFS].Symlink(oldNamePath, linkPath)
}
// Truncate implements the same method as documented on api.FS
func (c *CompositeFS) Truncate(path string, size int64) syscall.Errno {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Truncate(relativePath, size)
}
// Rmdir implements the same method as documented on api.FS
func (c *CompositeFS) Rmdir(path string) syscall.Errno {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Rmdir(relativePath)
}
// Unlink implements the same method as documented on api.FS
func (c *CompositeFS) Unlink(path string) syscall.Errno {
matchIndex, relativePath := c.chooseFS(path)
return c.fs[matchIndex].Unlink(relativePath)
}
// chooseFS chooses the best fs and the relative path to use for the input.
func (c *CompositeFS) chooseFS(path string) (matchIndex int, relativePath string) {
matchIndex = -1
matchPrefixLen := 0
pathI, pathLen := stripPrefixesAndTrailingSlash(path)
// Last is the highest precedence, so we iterate backwards. The last longest
// match wins. e.g. the pre-open "tmp" wins vs "" regardless of order.
for i := len(c.fs) - 1; i >= 0; i-- {
prefix := c.cleanedGuestPaths[i]
if eq, match := hasPathPrefix(path, pathI, pathLen, prefix); eq {
// When the input equals the prefix, there cannot be a longer match
// later. The relative path is the fsapi.FS root, so return empty
// string.
matchIndex = i
relativePath = ""
return
} else if match {
// Check to see if this is a longer match
prefixLen := len(prefix)
if prefixLen > matchPrefixLen || matchIndex == -1 {
matchIndex = i
matchPrefixLen = prefixLen
}
} // Otherwise, keep looking for a match
}
// Now, we know the path != prefix, but it matched an existing fs, because
// setup ensures there's always a root filesystem.
// If this was a root path match the cleaned path is the relative one to
// pass to the underlying filesystem.
if matchPrefixLen == 0 {
// Avoid re-slicing when the input is already clean
if pathI == 0 && len(path) == pathLen {
relativePath = path
} else {
relativePath = path[pathI:pathLen]
}
return
}
// Otherwise, it is non-root match: the relative path is past "$prefix/"
pathI += matchPrefixLen + 1 // e.g. prefix=foo, path=foo/bar -> bar
relativePath = path[pathI:pathLen]
return
}
// hasPathPrefix compares an input path against a prefix, both cleaned by
// stripPrefixesAndTrailingSlash. This returns a pair of eq, match to allow an
// early short circuit on match.
//
// Note: This is case-sensitive because POSIX paths are compared case
// sensitively.
func hasPathPrefix(path string, pathI, pathLen int, prefix string) (eq, match bool) {
matchLen := pathLen - pathI
if prefix == "" {
return matchLen == 0, true // e.g. prefix=, path=foo
}
prefixLen := len(prefix)
// reset pathLen temporarily to represent the length to match as opposed to
// the length of the string (that may contain leading slashes).
if matchLen == prefixLen {
if pathContainsPrefix(path, pathI, prefixLen, prefix) {
return true, true // e.g. prefix=bar, path=bar
}
return false, false
} else if matchLen < prefixLen {
return false, false // e.g. prefix=fooo, path=foo
}
if path[pathI+prefixLen] != '/' {
return false, false // e.g. prefix=foo, path=fooo
}
// Not equal, but maybe a match. e.g. prefix=foo, path=foo/bar
return false, pathContainsPrefix(path, pathI, prefixLen, prefix)
}
// pathContainsPrefix is faster than strings.HasPrefix even if we didn't cache
// the index,len. See benchmarks.
func pathContainsPrefix(path string, pathI, prefixLen int, prefix string) bool {
for i := 0; i < prefixLen; i++ {
if path[pathI] != prefix[i] {
return false // e.g. prefix=bar, path=foo or foo/bar
}
pathI++
}
return true // e.g. prefix=foo, path=foo or foo/bar
}
func StripPrefixesAndTrailingSlash(path string) string {
pathI, pathLen := stripPrefixesAndTrailingSlash(path)
return path[pathI:pathLen]
}
// stripPrefixesAndTrailingSlash skips any leading "./" or "/" such that the
// result index begins with another string. A result of "." coerces to the
// empty string "" because the current directory is handled by the guest.
//
// Results are the offset/len pair which is an optimization to avoid re-slicing
// overhead, as this function is called for every path operation.
//
// Note: Relative paths should be handled by the guest, as that's what knows
// what the current directory is. However, paths that escape the current
// directory e.g. "../.." have been found in `tinygo test` and this
// implementation takes care to avoid it.
func stripPrefixesAndTrailingSlash(path string) (pathI, pathLen int) {
// strip trailing slashes
pathLen = len(path)
for ; pathLen > 0 && path[pathLen-1] == '/'; pathLen-- {
}
pathI = 0
loop:
for pathI < pathLen {
switch path[pathI] {
case '/':
pathI++
case '.':
nextI := pathI + 1
if nextI < pathLen && path[nextI] == '/' {
pathI = nextI + 1
} else if nextI == pathLen {
pathI = nextI
} else {
break loop
}
default:
break loop
}
}
return
}
type fakeRootFS struct {
fsapi.UnimplementedFS
}
// OpenFile implements the same method as documented on api.FS
func (fakeRootFS) OpenFile(path string, flag int, perm fs.FileMode) (fsapi.File, syscall.Errno) {
switch path {
case ".", "/", "":
return fakeRootDir{}, 0
}
return nil, syscall.ENOENT
}
type fakeRootDir struct {
fsapi.DirFile
}
// Ino implements the same method as documented on fsapi.File
func (fakeRootDir) Ino() (uint64, syscall.Errno) {
return 0, 0
}
// Stat implements the same method as documented on fsapi.File
func (fakeRootDir) Stat() (fsapi.Stat_t, syscall.Errno) {
return fsapi.Stat_t{Mode: fs.ModeDir, Nlink: 1}, 0
}
// Readdir implements the same method as documented on fsapi.File
func (fakeRootDir) Readdir(int) (dirents []fsapi.Dirent, errno syscall.Errno) {
return // empty
}
// Sync implements the same method as documented on fsapi.File
func (fakeRootDir) Sync() syscall.Errno {
return 0
}
// Datasync implements the same method as documented on fsapi.File
func (fakeRootDir) Datasync() syscall.Errno {
return 0
}
// Chmod implements the same method as documented on fsapi.File
func (fakeRootDir) Chmod(fs.FileMode) syscall.Errno {
return syscall.ENOSYS
}
// Chown implements the same method as documented on fsapi.File
func (fakeRootDir) Chown(int, int) syscall.Errno {
return syscall.ENOSYS
}
// Utimens implements the same method as documented on fsapi.File
func (fakeRootDir) Utimens(*[2]syscall.Timespec) syscall.Errno {
return syscall.ENOSYS
}
// Close implements the same method as documented on fsapi.File
func (fakeRootDir) Close() syscall.Errno {
return 0
}

View File

@@ -1,448 +0,0 @@
package sysfs
import (
"errors"
"io/fs"
"os"
"path"
"sort"
"strings"
"syscall"
"testing"
gofstest "testing/fstest"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/internal/fstest"
testfs "github.com/tetratelabs/wazero/internal/testing/fs"
"github.com/tetratelabs/wazero/internal/testing/require"
)
func TestNewRootFS(t *testing.T) {
t.Run("empty", func(t *testing.T) {
rootFS, err := NewRootFS(nil, nil)
require.NoError(t, err)
require.Equal(t, fsapi.UnimplementedFS{}, rootFS)
})
t.Run("only root", func(t *testing.T) {
testFS := NewDirFS(t.TempDir())
rootFS, err := NewRootFS([]fsapi.FS{testFS}, []string{""})
require.NoError(t, err)
// Should not be a composite filesystem
require.Equal(t, testFS, rootFS)
})
t.Run("only non root", func(t *testing.T) {
testFS := NewDirFS(".")
rootFS, err := NewRootFS([]fsapi.FS{testFS}, []string{"/tmp"})
require.NoError(t, err)
// unwrapping returns in original order
require.Equal(t, []fsapi.FS{testFS}, rootFS.(*CompositeFS).FS())
require.Equal(t, []string{"/tmp"}, rootFS.(*CompositeFS).GuestPaths())
// String is human-readable
require.Equal(t, "[.:/tmp]", rootFS.String())
// Guest can look up /tmp
f, errno := rootFS.OpenFile("/tmp", os.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
require.EqualErrno(t, 0, f.Close())
// Guest can look up / and see "/tmp" in it
f, errno = rootFS.OpenFile("/", os.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
dirents, errno := f.Readdir(-1)
require.EqualErrno(t, 0, errno)
require.Equal(t, 1, len(dirents))
require.Equal(t, "tmp", dirents[0].Name)
require.True(t, dirents[0].IsDir())
})
t.Run("multiple roots unsupported", func(t *testing.T) {
testFS := NewDirFS(".")
_, err := NewRootFS([]fsapi.FS{testFS, testFS}, []string{"/", "/"})
require.EqualError(t, err, "multiple root filesystems are invalid: [.:/ .:/]")
})
t.Run("virtual paths unsupported", func(t *testing.T) {
testFS := NewDirFS(".")
_, err := NewRootFS([]fsapi.FS{testFS}, []string{"usr/bin"})
require.EqualError(t, err, "only single-level guest paths allowed: [.:usr/bin]")
})
t.Run("multiple matches", func(t *testing.T) {
tmpDir1 := t.TempDir()
testFS1 := NewDirFS(tmpDir1)
require.NoError(t, os.Mkdir(path.Join(tmpDir1, "tmp"), 0o700))
require.NoError(t, os.WriteFile(path.Join(tmpDir1, "a"), []byte{1}, 0o600))
tmpDir2 := t.TempDir()
testFS2 := NewDirFS(tmpDir2)
require.NoError(t, os.WriteFile(path.Join(tmpDir2, "a"), []byte{2}, 0o600))
rootFS, err := NewRootFS([]fsapi.FS{testFS2, testFS1}, []string{"/tmp", "/"})
require.NoError(t, err)
// unwrapping returns in original order
require.Equal(t, []fsapi.FS{testFS2, testFS1}, rootFS.(*CompositeFS).FS())
require.Equal(t, []string{"/tmp", "/"}, rootFS.(*CompositeFS).GuestPaths())
// Should be a composite filesystem
require.NotEqual(t, testFS1, rootFS)
require.NotEqual(t, testFS2, rootFS)
t.Run("last wins", func(t *testing.T) {
f, errno := rootFS.OpenFile("/tmp/a", os.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer f.Close()
b := readAll(t, f)
require.Equal(t, []byte{2}, b)
})
// This test is covered by fstest.TestFS, but doing again here
t.Run("root includes prefix mount", func(t *testing.T) {
f, errno := rootFS.OpenFile(".", os.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer f.Close()
entries, errno := f.Readdir(-1)
require.EqualErrno(t, 0, errno)
names := make([]string, 0, len(entries))
for _, e := range entries {
names = append(names, e.Name)
}
sort.Strings(names)
require.Equal(t, []string{"a", "tmp"}, names)
})
})
}
func TestRootFS_String(t *testing.T) {
tmpFS := NewDirFS(".")
rootFS := NewDirFS(".")
testFS, err := NewRootFS([]fsapi.FS{rootFS, tmpFS}, []string{"/", "/tmp"})
require.NoError(t, err)
require.Equal(t, "[.:/ .:/tmp]", testFS.String())
}
func TestRootFS_Open(t *testing.T) {
tmpDir := t.TempDir()
// Create a subdirectory, so we can test reads outside the fsapi.FS root.
tmpDir = path.Join(tmpDir, t.Name())
require.NoError(t, os.Mkdir(tmpDir, 0o700))
require.NoError(t, fstest.WriteTestFiles(tmpDir))
testRootFS := NewDirFS(tmpDir)
testDirFS := NewDirFS(t.TempDir())
testFS, err := NewRootFS([]fsapi.FS{testRootFS, testDirFS}, []string{"/", "/emptydir"})
require.NoError(t, err)
testOpen_Read(t, testFS, true)
testOpen_O_RDWR(t, tmpDir, testFS)
t.Run("path outside root valid", func(t *testing.T) {
_, err := testFS.OpenFile("../foo", os.O_RDONLY, 0)
// fsapi.FS allows relative path lookups
require.True(t, errors.Is(err, fs.ErrNotExist))
})
}
func TestRootFS_Stat(t *testing.T) {
tmpDir := t.TempDir()
require.NoError(t, fstest.WriteTestFiles(tmpDir))
tmpFS := NewDirFS(t.TempDir())
testFS, err := NewRootFS([]fsapi.FS{NewDirFS(tmpDir), tmpFS}, []string{"/", "/tmp"})
require.NoError(t, err)
testStat(t, testFS)
}
func TestRootFS_examples(t *testing.T) {
tests := []struct {
name string
fs []fsapi.FS
guestPaths []string
expected, unexpected []string
}{
// e.g. from Go project root:
// $ GOOS=js GOARCH=wasm bin/go test -c -o template.wasm text/template
// $ wazero run -mount=src/text/template:/ template.wasm -test.v
{
name: "go test text/template",
fs: []fsapi.FS{
&adapter{fs: testfs.FS{"go-example-stdout-ExampleTemplate-0.txt": &testfs.File{}}},
&adapter{fs: testfs.FS{"testdata/file1.tmpl": &testfs.File{}}},
},
guestPaths: []string{"/tmp", "/"},
expected: []string{"/tmp/go-example-stdout-ExampleTemplate-0.txt", "testdata/file1.tmpl"},
unexpected: []string{"DOES NOT EXIST"},
},
// e.g. from TinyGo project root:
// $ ./build/tinygo test -target wasi -c -o flate.wasm compress/flate
// $ wazero run -mount=$(go env GOROOT)/src/compress/flate:/ flate.wasm -test.v
{
name: "tinygo test compress/flate",
fs: []fsapi.FS{
&adapter{fs: testfs.FS{}},
&adapter{fs: testfs.FS{"testdata/e.txt": &testfs.File{}}},
&adapter{fs: testfs.FS{"testdata/Isaac.Newton-Opticks.txt": &testfs.File{}}},
},
guestPaths: []string{"/", "../", "../../"},
expected: []string{"../testdata/e.txt", "../../testdata/Isaac.Newton-Opticks.txt"},
unexpected: []string{"../../testdata/e.txt"},
},
// e.g. from Go project root:
// $ GOOS=js GOARCH=wasm bin/go test -c -o net.wasm ne
// $ wazero run -mount=src/net:/ net.wasm -test.v -test.short
{
name: "go test net",
fs: []fsapi.FS{
&adapter{fs: testfs.FS{"services": &testfs.File{}}},
&adapter{fs: testfs.FS{"testdata/aliases": &testfs.File{}}},
},
guestPaths: []string{"/etc", "/"},
expected: []string{"/etc/services", "testdata/aliases"},
unexpected: []string{"services"},
},
// e.g. from wagi-python project root:
// $ GOOS=js GOARCH=wasm bin/go test -c -o net.wasm ne
// $ wazero run -hostlogging=filesystem -mount=.:/ -env=PYTHONHOME=/opt/wasi-python/lib/python3.11 \
// -env=PYTHONPATH=/opt/wasi-python/lib/python3.11 opt/wasi-python/bin/python3.wasm
{
name: "python",
fs: []fsapi.FS{
&adapter{fs: gofstest.MapFS{ // to allow resolution of "."
"pybuilddir.txt": &gofstest.MapFile{},
"opt/wasi-python/lib/python3.11/__phello__/__init__.py": &gofstest.MapFile{},
}},
},
guestPaths: []string{"/"},
expected: []string{
".",
"pybuilddir.txt",
"opt/wasi-python/lib/python3.11/__phello__/__init__.py",
},
},
// e.g. from Zig project root: TODO: verify this once cli works with multiple mounts
// $ zig test --test-cmd wazero --test-cmd run --test-cmd -mount=.:/ -mount=/tmp:/tmp \
// --test-cmd-bin -target wasm32-wasi --zig-lib-dir ./lib ./lib/std/std.zig
{
name: "zig",
fs: []fsapi.FS{
&adapter{fs: testfs.FS{"zig-cache": &testfs.File{}}},
&adapter{fs: testfs.FS{"qSQRrUkgJX9L20mr": &testfs.File{}}},
},
guestPaths: []string{"/", "/tmp"},
expected: []string{"zig-cache", "/tmp/qSQRrUkgJX9L20mr"},
unexpected: []string{"/qSQRrUkgJX9L20mr"},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
root, err := NewRootFS(tc.fs, tc.guestPaths)
require.NoError(t, err)
for _, p := range tc.expected {
f, errno := root.OpenFile(p, os.O_RDONLY, 0)
require.Zero(t, errno, p)
require.EqualErrno(t, 0, f.Close(), p)
}
for _, p := range tc.unexpected {
_, err := root.OpenFile(p, os.O_RDONLY, 0)
require.EqualErrno(t, syscall.ENOENT, err)
}
})
}
}
func Test_stripPrefixesAndTrailingSlash(t *testing.T) {
tests := []struct {
path, expected string
}{
{
path: ".",
expected: "",
},
{
path: "/",
expected: "",
},
{
path: "./",
expected: "",
},
{
path: "./foo",
expected: "foo",
},
{
path: ".foo",
expected: ".foo",
},
{
path: "././foo",
expected: "foo",
},
{
path: "/foo",
expected: "foo",
},
{
path: "foo/",
expected: "foo",
},
{
path: "//",
expected: "",
},
{
path: "../../",
expected: "../..",
},
{
path: "./../../",
expected: "../..",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.path, func(t *testing.T) {
pathI, pathLen := stripPrefixesAndTrailingSlash(tc.path)
require.Equal(t, tc.expected, tc.path[pathI:pathLen])
})
}
}
func Test_hasPathPrefix(t *testing.T) {
tests := []struct {
name string
path, prefix string
expectEq, expectMatch bool
}{
{
name: "empty prefix",
path: "foo",
prefix: "",
expectEq: false,
expectMatch: true,
},
{
name: "equal prefix",
path: "foo",
prefix: "foo",
expectEq: true,
expectMatch: true,
},
{
name: "sub path",
path: "foo/bar",
prefix: "foo",
expectMatch: true,
},
{
name: "different sub path",
path: "foo/bar",
prefix: "bar",
expectMatch: false,
},
{
name: "different path same length",
path: "foo",
prefix: "bar",
expectMatch: false,
},
{
name: "longer path",
path: "foo",
prefix: "foo/bar",
expectMatch: false,
},
{
name: "path shorter",
path: "foo",
prefix: "fooo",
expectMatch: false,
},
{
name: "path longer",
path: "fooo",
prefix: "foo",
expectMatch: false,
},
{
name: "shorter path",
path: "foo",
prefix: "foo/bar",
expectMatch: false,
},
{
name: "wrong and shorter path",
path: "foo",
prefix: "bar/foo",
expectMatch: false,
},
{
name: "same relative",
path: "../..",
prefix: "../..",
expectEq: true,
expectMatch: true,
},
{
name: "longer relative",
path: "..",
prefix: "../..",
expectMatch: false,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
path := "././." + tc.path + "/"
eq, match := hasPathPrefix(path, 5, 5+len(tc.path), tc.prefix)
require.Equal(t, tc.expectEq, eq)
require.Equal(t, tc.expectMatch, match)
})
}
}
// BenchmarkHasPrefixVsIterate shows that iteration is faster than re-slicing
// for a prefix match.
func BenchmarkHasPrefixVsIterate(b *testing.B) {
s := "../../.."
prefix := "../bar"
prefixLen := len(prefix)
b.Run("strings.HasPrefix", func(b *testing.B) {
for i := 0; i < b.N; i++ {
if strings.HasPrefix(s, prefix) { //nolint
}
}
})
b.Run("iterate", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for i := 0; i < prefixLen; i++ {
if s[i] != prefix[i] {
break
}
}
}
})
}

View File

@@ -296,15 +296,6 @@ func testStat(t *testing.T, testFS fsapi.FS) {
}
}
func readAll(t *testing.T, f fsapi.File) []byte {
st, errno := f.Stat()
require.EqualErrno(t, 0, errno)
buf := make([]byte, st.Size)
_, errno = f.Read(buf)
require.EqualErrno(t, 0, errno)
return buf
}
// requireReaddir ensures the input file is a directory, and returns its
// entries.
func requireReaddir(t *testing.T, f fsapi.File, n int, expectIno bool) []fsapi.Dirent {