From 53ce5eea830b3100af7ec8caa13bb38310c0afbf Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Tue, 27 Jun 2023 10:02:57 +0800 Subject: [PATCH] Allows wasip1 guests to use arbitrarily nested pre-opens (#1536) Signed-off-by: Adrian Cole --- cmd/wazero/wazero.go | 16 +- cmd/wazero/wazero_test.go | 14 +- config.go | 9 +- fsconfig.go | 23 +- fsconfig_test.go | 82 ++--- internal/sys/fs.go | 91 ++++-- internal/sys/fs_test.go | 116 ++++--- internal/sys/sys.go | 10 +- internal/sys/sys_test.go | 15 +- internal/sysfs/rootfs.go | 559 ---------------------------------- internal/sysfs/rootfs_test.go | 448 --------------------------- internal/sysfs/sysfs_test.go | 9 - 12 files changed, 218 insertions(+), 1174 deletions(-) delete mode 100644 internal/sysfs/rootfs.go delete mode 100644 internal/sysfs/rootfs_test.go diff --git a/cmd/wazero/wazero.go b/cmd/wazero/wazero.go index abb648f9..93c24f67 100644 --- a/cmd/wazero/wazero.go +++ b/cmd/wazero/wazero.go @@ -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 } } diff --git a/cmd/wazero/wazero_test.go b/cmd/wazero/wazero_test.go index 5264130a..67464356 100644 --- a/cmd/wazero/wazero_test.go +++ b/cmd/wazero/wazero_test.go @@ -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=,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}, diff --git a/config.go b/config.go index 56a32fcf..9e6e9fb0 100644 --- a/config.go +++ b/config.go @@ -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, ) } diff --git a/fsconfig.go b/fsconfig.go index 099a7917..b603d1e9 100644 --- a/fsconfig.go +++ b/fsconfig.go @@ -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 } diff --git a/fsconfig_test.go b/fsconfig_test.go index a6c58bbd..2ea3aded 100644 --- a/fsconfig_test.go +++ b/fsconfig_test.go @@ -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) }) } } diff --git a/internal/sys/fs.go b/internal/sys/fs.go index 3f1eb6e5..52be57bc 100644 --- a/internal/sys/fs.go +++ b/internal/sys/fs.go @@ -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] +} diff --git a/internal/sys/fs_test.go b/internal/sys/fs_test.go index 167404fa..987a12ae 100644 --- a/internal/sys/fs_test.go +++ b/internal/sys/fs_test.go @@ -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) + }) + } +} diff --git a/internal/sys/sys.go b/internal/sys/sys.go index f035712e..aedd7c70 100644 --- a/internal/sys/sys.go +++ b/internal/sys/sys.go @@ -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 } diff --git a/internal/sys/sys_test.go b/internal/sys/sys_test.go index 414d5fa3..f8d8dfc5 100644 --- a/internal/sys/sys_test.go +++ b/internal/sys/sys_test.go @@ -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) } diff --git a/internal/sysfs/rootfs.go b/internal/sysfs/rootfs.go deleted file mode 100644 index 75bd839f..00000000 --- a/internal/sysfs/rootfs.go +++ /dev/null @@ -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 -} diff --git a/internal/sysfs/rootfs_test.go b/internal/sysfs/rootfs_test.go deleted file mode 100644 index fc47d535..00000000 --- a/internal/sysfs/rootfs_test.go +++ /dev/null @@ -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 - } - } - } - }) -} diff --git a/internal/sysfs/sysfs_test.go b/internal/sysfs/sysfs_test.go index 2566ed1c..56ff29a2 100644 --- a/internal/sysfs/sysfs_test.go +++ b/internal/sysfs/sysfs_test.go @@ -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 {