Allows wasip1 guests to use arbitrarily nested pre-opens (#1536)
Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
23
fsconfig.go
23
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user