fs: adds string for better error experience (#1042)

This prepares for pseudo-root when the CLI doesn't provide one by
improving the error messages in general, as well being consistent about
parameter order.

Signed-off-by: Adrian Cole <adrian@tetrate.io>

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2023-01-17 10:01:51 -06:00
committed by GitHub
parent 3609d74c92
commit 3cf29f9f76
21 changed files with 290 additions and 128 deletions

View File

@@ -45,7 +45,7 @@ func Test_testfs(t *testing.T) {
require.NoError(t, os.Mkdir(testfsDir, 0o700))
require.NoError(t, fstest.WriteTestFiles(testfsDir))
rootFS, err := syscallfs.NewDirFS("/", tmpDir)
rootFS, err := syscallfs.NewDirFS(tmpDir, "/")
require.NoError(t, err)
stdout, stderr, err := compileAndRun(testCtx, "testfs", wazero.NewModuleConfig().WithFS(rootFS))

View File

@@ -31,7 +31,7 @@ func TestNewFSContext(t *testing.T) {
embedFS, err := fs.Sub(testdata, "testdata")
require.NoError(t, err)
dirfs, err := syscallfs.NewDirFS("/", ".")
dirfs, err := syscallfs.NewDirFS(".", "/")
require.NoError(t, err)
// Test various usual configuration for the file system.
@@ -41,7 +41,7 @@ func TestNewFSContext(t *testing.T) {
}{
{
name: "embed.FS",
fs: syscallfs.Adapt("/", embedFS),
fs: syscallfs.Adapt(embedFS, "/"),
},
{
name: "syscallfs.NewDirFS",
@@ -55,7 +55,7 @@ func TestNewFSContext(t *testing.T) {
},
{
name: "fstest.MapFS",
fs: syscallfs.Adapt("/", fstest.MapFS{}),
fs: syscallfs.Adapt(fstest.MapFS{}, "/"),
},
}
@@ -114,7 +114,7 @@ func TestEmptyFSContext(t *testing.T) {
}
func TestContext_Close(t *testing.T) {
testFS := syscallfs.Adapt("/", testfs.FS{"foo": &testfs.File{}})
testFS := syscallfs.Adapt(testfs.FS{"foo": &testfs.File{}}, "/")
fsc, err := NewFSContext(nil, nil, nil, testFS)
require.NoError(t, err)
@@ -139,7 +139,7 @@ func TestContext_Close(t *testing.T) {
func TestContext_Close_Error(t *testing.T) {
file := &testfs.File{CloseErr: errors.New("error closing")}
testFS := syscallfs.Adapt("/", testfs.FS{"foo": file})
testFS := syscallfs.Adapt(testfs.FS{"foo": file}, "/")
fsc, err := NewFSContext(nil, nil, nil, testFS)
require.NoError(t, err)

View File

@@ -182,7 +182,7 @@ func NewContext(
}
if fs != nil {
sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, syscallfs.Adapt("/", fs))
sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, syscallfs.Adapt(fs, "/"))
} else {
sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, syscallfs.EmptyFS)
}

View File

@@ -51,7 +51,7 @@ func TestDefaultSysContext(t *testing.T) {
require.Equal(t, &ns, sysCtx.nanosleep)
require.Equal(t, platform.NewFakeRandSource(), sysCtx.RandSource())
testFS := syscallfs.Adapt("/", testfs.FS{})
testFS := syscallfs.Adapt(testfs.FS{}, "/")
expectedFS, _ := NewFSContext(nil, nil, nil, testFS)
expectedOpenedFiles := FileTable{}

View File

@@ -15,16 +15,21 @@ import (
// flags as there is no parameter to pass them through with. Moreover, fs.FS
// documentation does not require the file to be present. In summary, we can't
// enforce flag behavior.
func Adapt(guestDir string, fs fs.FS) FS {
func Adapt(fs fs.FS, guestDir string) FS {
if sys, ok := fs.(FS); ok {
return sys
}
return &adapter{guestDir, fs}
return &adapter{fs, guestDir}
}
type adapter struct {
guestDir string
fs fs.FS
guestDir string
}
// String implements fmt.Stringer
func (a *adapter) String() string {
return fmt.Sprintf("%v:%s:ro", a.fs, a.guestDir)
}
// Open implements the same method as documented on fs.FS

View File

@@ -13,8 +13,13 @@ import (
"github.com/tetratelabs/wazero/internal/testing/require"
)
func TestAdapt_String(t *testing.T) {
testFS := Adapt(os.DirFS("."), "/tmp")
require.Equal(t, ".:/tmp:ro", testFS.String())
}
func TestAdapt_MkDir(t *testing.T) {
testFS := Adapt("/", os.DirFS(t.TempDir()))
testFS := Adapt(os.DirFS(t.TempDir()), "/")
err := testFS.Mkdir("mkdir", fs.ModeDir)
require.Equal(t, syscall.ENOSYS, err)
@@ -22,7 +27,7 @@ func TestAdapt_MkDir(t *testing.T) {
func TestAdapt_Rename(t *testing.T) {
tmpDir := t.TempDir()
testFS := Adapt("/", os.DirFS(tmpDir))
testFS := Adapt(os.DirFS(tmpDir), "/")
file1 := "file1"
file1Path := pathutil.Join(tmpDir, file1)
@@ -42,7 +47,7 @@ func TestAdapt_Rename(t *testing.T) {
func TestAdapt_Rmdir(t *testing.T) {
tmpDir := t.TempDir()
testFS := Adapt("/", os.DirFS(tmpDir))
testFS := Adapt(os.DirFS(tmpDir), "/")
path := "rmdir"
realPath := pathutil.Join(tmpDir, path)
@@ -54,7 +59,7 @@ func TestAdapt_Rmdir(t *testing.T) {
func TestAdapt_Unlink(t *testing.T) {
tmpDir := t.TempDir()
testFS := Adapt("/", os.DirFS(tmpDir))
testFS := Adapt(os.DirFS(tmpDir), "/")
path := "unlink"
realPath := pathutil.Join(tmpDir, path)
@@ -66,7 +71,7 @@ func TestAdapt_Unlink(t *testing.T) {
func TestAdapt_Utimes(t *testing.T) {
tmpDir := t.TempDir()
testFS := Adapt("/", os.DirFS(tmpDir))
testFS := Adapt(os.DirFS(tmpDir), "/")
path := "utimes"
realPath := pathutil.Join(tmpDir, path)
@@ -81,7 +86,7 @@ func TestAdapt_Open_Read(t *testing.T) {
tmpDir := t.TempDir()
tmpDir = pathutil.Join(tmpDir, t.Name())
require.NoError(t, os.Mkdir(tmpDir, 0o700))
testFS := Adapt("/", os.DirFS(tmpDir))
testFS := Adapt(os.DirFS(tmpDir), "/")
testOpen_Read(t, tmpDir, testFS)
@@ -115,7 +120,7 @@ func (dir hackFS) Open(name string) (fs.File, error) {
// fs.FS contract.
func TestAdapt_HackedWrites(t *testing.T) {
tmpDir := t.TempDir()
testFS := Adapt("/", hackFS(tmpDir))
testFS := Adapt(hackFS(tmpDir), "/")
testOpen_O_RDWR(t, tmpDir, testFS)
}
@@ -151,7 +156,7 @@ func TestAdapt_TestFS(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
// Adapt a normal fs.FS to syscallfs.FS
testFS := Adapt("/", tc.fs)
testFS := Adapt(tc.fs, "/")
// Adapt it back to fs.FS and run the tests
require.NoError(t, fstest.TestFS(&testFSAdapter{testFS}))

View File

@@ -1,21 +1,26 @@
package syscallfs
import (
"errors"
"fmt"
"io/fs"
"os"
"syscall"
)
func NewDirFS(guestDir, hostDir string) (FS, error) {
// For easier OS-specific concatenation later, append the path separator.
hostDir = ensureTrailingPathSeparator(hostDir)
if stat, err := os.Stat(hostDir); err != nil {
return nil, syscall.ENOENT
} else if !stat.IsDir() {
return nil, syscall.ENOTDIR
func NewDirFS(hostDir, guestDir string) (FS, error) {
ret := &dirFS{
hostDir: hostDir,
guestDir: guestDir,
cleanedHostDir: ensureTrailingPathSeparator(hostDir),
}
return &dirFS{guestDir, hostDir}, nil
if stat, err := os.Stat(hostDir); err != nil {
return nil, fmt.Errorf("%s invalid: %v", ret, errors.Unwrap(err))
} else if !stat.IsDir() {
return nil, fmt.Errorf("%s invalid: %s is not a directory", ret, hostDir)
}
return ret, nil
}
func ensureTrailingPathSeparator(dir string) string {
@@ -26,7 +31,15 @@ func ensureTrailingPathSeparator(dir string) string {
}
type dirFS struct {
guestDir, hostDir string
hostDir, guestDir string
// cleanedHostDir is for easier OS-specific concatenation, as it always has
// a trailing path separator.
cleanedHostDir string
}
// String implements fmt.Stringer
func (d *dirFS) String() string {
return d.hostDir + ":" + d.guestDir
}
// Open implements the same method as documented on fs.FS
@@ -84,9 +97,9 @@ func (d *dirFS) Utimes(name string, atimeNsec, mtimeNsec int64) error {
func (d *dirFS) join(name string) string {
if name == "." {
return d.hostDir
return d.cleanedHostDir
}
// TODO: Enforce similar to safefilepath.FromFS(name), but be careful as
// relative path inputs are allowed. e.g. dir or name == ../
return d.hostDir + name
return d.cleanedHostDir + name
}

View File

@@ -2,6 +2,7 @@ package syscallfs
import (
"errors"
"fmt"
"io/fs"
"os"
pathutil "path"
@@ -13,9 +14,45 @@ import (
"github.com/tetratelabs/wazero/internal/testing/require"
)
func TestNewDirFS(t *testing.T) {
testFS, err := NewDirFS(".", "/tmp")
require.NoError(t, err)
// String doesn't include the fake name
require.Equal(t, ".:/tmp", testFS.String())
// Guest can look up /
f, err := testFS.OpenFile("/", os.O_RDONLY, 0)
require.NoError(t, err)
require.NoError(t, f.Close())
t.Run("not found", func(t *testing.T) {
_, err := NewDirFS("a", "/tmp")
// get the os-dependent error string. This is needed for windows, e.g.
// "no such file or directory" vs "The system cannot find the file specified."
_, statErr := os.Stat("a")
expectedErrString := errors.Unwrap(statErr).Error()
require.EqualError(t, err, "a:/tmp invalid: "+expectedErrString)
})
t.Run("not a directory", func(t *testing.T) {
f := os.Args[0] // should be safe in scratch tests which don't have the source mounted.
_, err := NewDirFS(f, "/tmp")
require.EqualError(t, err, fmt.Sprintf("%[1]s:/tmp invalid: %[1]s is not a directory", f))
})
}
func TestDirFS_String(t *testing.T) {
testFS, err := NewDirFS(".", "/tmp")
require.NoError(t, err)
require.Equal(t, ".:/tmp", testFS.String())
}
func TestDirFS_MkDir(t *testing.T) {
tmpDir := t.TempDir()
testFS, err := NewDirFS("/", tmpDir)
testFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
name := "mkdir"
@@ -46,7 +83,7 @@ func TestDirFS_MkDir(t *testing.T) {
func TestDirFS_Rename(t *testing.T) {
t.Run("from doesn't exist", func(t *testing.T) {
tmpDir := t.TempDir()
testFS, err := NewDirFS("/", tmpDir)
testFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
file1 := "file1"
@@ -59,7 +96,7 @@ func TestDirFS_Rename(t *testing.T) {
})
t.Run("file to non-exist", func(t *testing.T) {
tmpDir := t.TempDir()
testFS, err := NewDirFS("/", tmpDir)
testFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
file1 := "file1"
@@ -83,7 +120,7 @@ func TestDirFS_Rename(t *testing.T) {
})
t.Run("dir to non-exist", func(t *testing.T) {
tmpDir := t.TempDir()
testFS, err := NewDirFS("/", tmpDir)
testFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
dir1 := "dir1"
@@ -105,7 +142,7 @@ func TestDirFS_Rename(t *testing.T) {
})
t.Run("dir to file", func(t *testing.T) {
tmpDir := t.TempDir()
testFS, err := NewDirFS("/", tmpDir)
testFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
dir1 := "dir1"
@@ -133,7 +170,7 @@ func TestDirFS_Rename(t *testing.T) {
})
t.Run("file to dir", func(t *testing.T) {
tmpDir := t.TempDir()
testFS, err := NewDirFS("/", tmpDir)
testFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
file1 := "file1"
@@ -151,7 +188,7 @@ func TestDirFS_Rename(t *testing.T) {
})
t.Run("dir to dir", func(t *testing.T) {
tmpDir := t.TempDir()
testFS, err := NewDirFS("/", tmpDir)
testFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
dir1 := "dir1"
@@ -188,7 +225,7 @@ func TestDirFS_Rename(t *testing.T) {
})
t.Run("file to file", func(t *testing.T) {
tmpDir := t.TempDir()
testFS, err := NewDirFS("/", tmpDir)
testFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
file1 := "file1"
@@ -217,7 +254,7 @@ func TestDirFS_Rename(t *testing.T) {
})
t.Run("dir to itself", func(t *testing.T) {
tmpDir := t.TempDir()
testFS, err := NewDirFS("/", tmpDir)
testFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
dir1 := "dir1"
@@ -233,7 +270,7 @@ func TestDirFS_Rename(t *testing.T) {
})
t.Run("file to itself", func(t *testing.T) {
tmpDir := t.TempDir()
testFS, err := NewDirFS("/", tmpDir)
testFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
file1 := "file1"
@@ -254,7 +291,7 @@ func TestDirFS_Rename(t *testing.T) {
func TestDirFS_Rmdir(t *testing.T) {
tmpDir := t.TempDir()
testFS, err := NewDirFS("/", tmpDir)
testFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
name := "rmdir"
@@ -295,7 +332,7 @@ func TestDirFS_Rmdir(t *testing.T) {
func TestDirFS_Unlink(t *testing.T) {
tmpDir := t.TempDir()
testFS, err := NewDirFS("/", tmpDir)
testFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
name := "unlink"
@@ -327,7 +364,7 @@ func TestDirFS_Unlink(t *testing.T) {
func TestDirFS_Utimes(t *testing.T) {
tmpDir := t.TempDir()
testFS, err := NewDirFS("/", tmpDir)
testFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
testUtimes(t, tmpDir, testFS)
@@ -340,7 +377,7 @@ func TestDirFS_Open(t *testing.T) {
tmpDir = pathutil.Join(tmpDir, t.Name())
require.NoError(t, os.Mkdir(tmpDir, 0o700))
testFS, err := NewDirFS("/", tmpDir)
testFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
testOpen_Read(t, tmpDir, testFS)
@@ -363,7 +400,7 @@ func TestDirFS_TestFS(t *testing.T) {
require.NoError(t, fstest.WriteTestFiles(tmpDir))
// Create a writeable filesystem
testFS, err := NewDirFS("/", tmpDir)
testFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
// Run TestFS via the adapter

View File

@@ -12,6 +12,11 @@ var EmptyFS FS = empty{}
type empty struct{}
// String implements fmt.Stringer
func (empty) String() string {
return "empty:/:ro"
}
// Open implements the same method as documented on fs.FS
func (empty) Open(name string) (fs.File, error) {
panic(fmt.Errorf("unexpected to call fs.FS.Open(%s)", name))

View File

@@ -0,0 +1,11 @@
package syscallfs
import (
"testing"
"github.com/tetratelabs/wazero/internal/testing/require"
)
func TestEmptyFS_String(t *testing.T) {
require.Equal(t, "empty:/:ro", EmptyFS.String())
}

View File

@@ -15,12 +15,21 @@ import (
func NewReadFS(fs FS) FS {
if _, ok := fs.(*readFS); ok {
return fs
} else if _, ok = fs.(*adapter); ok {
return fs // fs.FS is always read-only
} else if _, ok = fs.(empty); ok {
return fs // empty is always read-only
}
return &readFS{fs}
}
type readFS struct{ fs FS }
// String implements fmt.Stringer
func (r *readFS) String() string {
return r.fs.String() + ":ro"
}
// Open implements the same method as documented on fs.FS
func (r *readFS) Open(name string) (fs.File, error) {
panic(fmt.Errorf("unexpected to call fs.FS.Open(%s)", name))
@@ -28,7 +37,7 @@ func (r *readFS) Open(name string) (fs.File, error) {
// GuestDir implements FS.GuestDir
func (r *readFS) GuestDir() string {
return "/"
return r.fs.GuestDir()
}
// OpenFile implements FS.OpenFile

View File

@@ -11,22 +11,50 @@ import (
"github.com/tetratelabs/wazero/internal/testing/require"
)
func TestReadFS_MkDir(t *testing.T) {
func TestNewReadFS(t *testing.T) {
tmpDir := t.TempDir()
testFS := NewReadFS(Adapt("/", hackFS(tmpDir)))
err := testFS.Mkdir("mkdir", fs.ModeDir)
// Doesn't double-wrap file systems that are already read-only
adapted := Adapt(os.DirFS(tmpDir), "/")
require.Equal(t, adapted, NewReadFS(adapted))
require.Equal(t, EmptyFS, NewReadFS(EmptyFS))
// Wraps a writeable file system
writeable, err := NewDirFS(tmpDir, "/tmp")
require.NoError(t, err)
readFS := NewReadFS(writeable)
require.NotEqual(t, writeable, readFS)
require.Equal(t, writeable.GuestDir(), readFS.GuestDir())
}
func TestReadFS_String(t *testing.T) {
writeable, err := NewDirFS(".", "/tmp")
require.NoError(t, err)
readFS := NewReadFS(writeable)
require.NotEqual(t, writeable, readFS)
require.Equal(t, ".:/tmp:ro", readFS.String())
}
func TestReadFS_MkDir(t *testing.T) {
writeable, err := NewDirFS(t.TempDir(), "/")
require.NoError(t, err)
testFS := NewReadFS(writeable)
err = testFS.Mkdir("mkdir", fs.ModeDir)
require.Equal(t, syscall.ENOSYS, err)
}
func TestReadFS_Rename(t *testing.T) {
tmpDir := t.TempDir()
testFS := NewReadFS(Adapt("/", hackFS(tmpDir)))
writeable, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
testFS := NewReadFS(writeable)
file1 := "file1"
file1Path := pathutil.Join(tmpDir, file1)
file1Contents := []byte{1}
err := os.WriteFile(file1Path, file1Contents, 0o600)
err = os.WriteFile(file1Path, file1Contents, 0o600)
require.NoError(t, err)
file2 := "file2"
@@ -41,43 +69,51 @@ func TestReadFS_Rename(t *testing.T) {
func TestReadFS_Rmdir(t *testing.T) {
tmpDir := t.TempDir()
testFS := NewReadFS(Adapt("/", hackFS(tmpDir)))
writeable, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
testFS := NewReadFS(writeable)
path := "rmdir"
realPath := pathutil.Join(tmpDir, path)
require.NoError(t, os.Mkdir(realPath, 0o700))
err := testFS.Rmdir(path)
err = testFS.Rmdir(path)
require.Equal(t, syscall.ENOSYS, err)
}
func TestReadFS_Unlink(t *testing.T) {
tmpDir := t.TempDir()
testFS := NewReadFS(Adapt("/", hackFS(tmpDir)))
writeable, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
testFS := NewReadFS(writeable)
path := "unlink"
realPath := pathutil.Join(tmpDir, path)
require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
err := testFS.Unlink(path)
err = testFS.Unlink(path)
require.Equal(t, syscall.ENOSYS, err)
}
func TestReadFS_Utimes(t *testing.T) {
tmpDir := t.TempDir()
testFS := NewReadFS(Adapt("/", hackFS(tmpDir)))
writeable, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
testFS := NewReadFS(writeable)
path := "utimes"
realPath := pathutil.Join(tmpDir, path)
require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
err := testFS.Utimes(path, 1, 1)
err = testFS.Utimes(path, 1, 1)
require.Equal(t, syscall.ENOSYS, err)
}
func TestReadFS_Open_Read(t *testing.T) {
tmpDir := t.TempDir()
testFS := NewReadFS(Adapt("/", hackFS(tmpDir)))
writeable, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
testFS := NewReadFS(writeable)
testOpen_Read(t, tmpDir, testFS)
}
@@ -90,7 +126,7 @@ func TestReadFS_TestFS(t *testing.T) {
require.NoError(t, fstest.WriteTestFiles(tmpDir))
// Create a writeable filesystem
testFS, err := NewDirFS("/", tmpDir)
testFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
// Wrap it as read-only

View File

@@ -1,7 +1,6 @@
package syscallfs
import (
"errors"
"fmt"
"io"
"io/fs"
@@ -36,14 +35,14 @@ func NewRootFS(fs ...FS) (FS, error) {
prefix := path[pathI:pathLen]
if prefix == "" {
if ret.rootIndex != -1 {
return nil, errors.New("multiple root filesystems are invalid")
return nil, fmt.Errorf("multiple root filesystems are invalid: %s", fsString(fs))
}
ret.rootIndex = j
} else if strings.HasPrefix(prefix, "..") {
// ../ mounts are special cased and aren't returned in a directory
// listing, so we can ignore them for now.
} else if strings.Contains(prefix, "/") {
return nil, fmt.Errorf("unsupported GuestDir %s, only root level allowed", path)
return nil, fmt.Errorf("only single-level guest paths allowed: %s", fsString(fs))
} else {
ret.rootPrefixes[prefix] = j
}
@@ -54,7 +53,10 @@ func NewRootFS(fs ...FS) (FS, error) {
// Ensure there is always a root match to keep runtime logic simpler.
if ret.rootIndex == -1 {
return nil, errors.New("you must supply a root filesystem")
// TODO: Make a fake root filesystem that can do a directory listing of
// any existing prefixes. We can't use EmptyFS as the pre-open for root
// must work.
return nil, fmt.Errorf("you must supply a root filesystem: %s", fsString(fs))
}
return ret, nil
}
@@ -70,6 +72,19 @@ type CompositeFS struct {
rootIndex int
}
// String implements fmt.Stringer
func (c *CompositeFS) String() string {
// return the string in its initial order
return fsString(c.Unwrap())
}
func fsString(fs []FS) string {
if len(fs) == 1 {
return fmt.Sprintf("%v", fs[0])
}
return fmt.Sprintf("%v", fs)
}
// Unwrap returns the underlying filesystems in original order.
func (c *CompositeFS) Unwrap() []FS {
result := make([]FS, 0, len(c.fs))

View File

@@ -25,7 +25,7 @@ func TestNewRootFS(t *testing.T) {
require.Equal(t, EmptyFS, rootFS)
})
t.Run("only root", func(t *testing.T) {
testFS, err := NewDirFS("/", t.TempDir())
testFS, err := NewDirFS(t.TempDir(), "/")
require.NoError(t, err)
rootFS, err := NewRootFS(testFS)
@@ -35,35 +35,35 @@ func TestNewRootFS(t *testing.T) {
require.Equal(t, testFS, rootFS)
})
t.Run("only non root unsupported", func(t *testing.T) {
testFS, err := NewDirFS("/tmp", t.TempDir())
require.NoError(t, err)
_, err = NewRootFS(testFS, testFS)
require.EqualError(t, err, "you must supply a root filesystem")
})
t.Run("multiple roots unsupported", func(t *testing.T) {
testFS, err := NewDirFS("/", t.TempDir())
require.NoError(t, err)
_, err = NewRootFS(testFS, testFS)
require.EqualError(t, err, "multiple root filesystems are invalid")
})
t.Run("virtual paths unsupported", func(t *testing.T) {
testFS, err := NewDirFS("/usr/bin", t.TempDir())
testFS, err := NewDirFS(".", "/tmp")
require.NoError(t, err)
_, err = NewRootFS(testFS)
require.EqualError(t, err, "unsupported GuestDir /usr/bin, only root level allowed")
require.EqualError(t, err, "you must supply a root filesystem: .:/tmp")
})
t.Run("multiple roots unsupported", func(t *testing.T) {
testFS, err := NewDirFS(".", "/")
require.NoError(t, err)
_, err = NewRootFS(testFS, testFS)
require.EqualError(t, err, "multiple root filesystems are invalid: [.:/ .:/]")
})
t.Run("virtual paths unsupported", func(t *testing.T) {
testFS, err := NewDirFS(".", "/usr/bin")
require.NoError(t, err)
_, err = NewRootFS(testFS)
require.EqualError(t, err, "only single-level guest paths allowed: .:/usr/bin")
})
t.Run("multiple matches", func(t *testing.T) {
tmpDir1 := t.TempDir()
testFS1, err := NewDirFS("/", tmpDir1)
testFS1, err := NewDirFS(tmpDir1, "/")
require.NoError(t, err)
require.NoError(t, os.Mkdir(pathutil.Join(tmpDir1, "tmp"), 0o700))
require.NoError(t, os.WriteFile(pathutil.Join(tmpDir1, "a"), []byte{1}, 0o600))
tmpDir2 := t.TempDir()
testFS2, err := NewDirFS("/tmp", tmpDir2)
testFS2, err := NewDirFS(tmpDir2, "/tmp")
require.NoError(t, err)
require.NoError(t, os.WriteFile(pathutil.Join(tmpDir2, "a"), []byte{2}, 0o600))
@@ -110,6 +110,19 @@ func readDirNames(t *testing.T, f fs.File) []string {
return names
}
func TestRootFS_String(t *testing.T) {
tmpFS, err := NewDirFS(".", "/tmp")
require.NoError(t, err)
rootFS, err := NewDirFS(".", "/")
require.NoError(t, err)
testFS, err := NewRootFS(rootFS, tmpFS)
require.NoError(t, err)
require.Equal(t, "[.:/ .:/tmp]", testFS.String())
}
func TestRootFS_Open(t *testing.T) {
tmpDir := t.TempDir()
@@ -117,7 +130,7 @@ func TestRootFS_Open(t *testing.T) {
tmpDir = pathutil.Join(tmpDir, t.Name())
require.NoError(t, os.Mkdir(tmpDir, 0o700))
testFS, err := NewDirFS("/", tmpDir)
testFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
testOpen_Read(t, tmpDir, testFS)
@@ -144,11 +157,11 @@ func TestRootFS_TestFS(t *testing.T) {
require.NoError(t, os.Rename(pathutil.Join(tmpDir1, "dir"), pathutil.Join(tmpDir2, "dir")))
// Create a root mount
testFS1, err := NewDirFS("/", tmpDir1)
testFS1, err := NewDirFS(tmpDir1, "/")
require.NoError(t, err)
// Create a dir mount
testFS2, err := NewDirFS("/dir", pathutil.Join(tmpDir2, "dir"))
testFS2, err := NewDirFS(pathutil.Join(tmpDir2, "dir"), "/dir")
require.NoError(t, err)
testFS, err := NewRootFS(testFS1, testFS2)
@@ -170,8 +183,8 @@ func TestRootFS_examples(t *testing.T) {
{
name: "go test text/template",
fs: []FS{
&adapter{"/tmp", testfs.FS{"go-example-stdout-ExampleTemplate-0.txt": &testfs.File{}}},
&adapter{".", testfs.FS{"testdata/file1.tmpl": &testfs.File{}}},
&adapter{testfs.FS{"go-example-stdout-ExampleTemplate-0.txt": &testfs.File{}}, "/tmp"},
&adapter{testfs.FS{"testdata/file1.tmpl": &testfs.File{}}, "."},
},
expected: []string{"/tmp/go-example-stdout-ExampleTemplate-0.txt", "testdata/file1.tmpl"},
unexpected: []string{"DOES NOT EXIST"},
@@ -182,9 +195,9 @@ func TestRootFS_examples(t *testing.T) {
{
name: "tinygo test compress/flate",
fs: []FS{
&adapter{"/", testfs.FS{}},
&adapter{"../", testfs.FS{"testdata/e.txt": &testfs.File{}}},
&adapter{"../../", testfs.FS{"testdata/Isaac.Newton-Opticks.txt": &testfs.File{}}},
&adapter{testfs.FS{}, "/"},
&adapter{testfs.FS{"testdata/e.txt": &testfs.File{}}, "../"},
&adapter{testfs.FS{"testdata/Isaac.Newton-Opticks.txt": &testfs.File{}}, "../../"},
},
expected: []string{"../testdata/e.txt", "../../testdata/Isaac.Newton-Opticks.txt"},
unexpected: []string{"../../testdata/e.txt"},
@@ -195,8 +208,8 @@ func TestRootFS_examples(t *testing.T) {
{
name: "go test net",
fs: []FS{
&adapter{"/etc", testfs.FS{"services": &testfs.File{}}},
&adapter{"/", testfs.FS{"testdata/aliases": &testfs.File{}}},
&adapter{testfs.FS{"services": &testfs.File{}}, "/etc"},
&adapter{testfs.FS{"testdata/aliases": &testfs.File{}}, "/"},
},
expected: []string{"/etc/services", "testdata/aliases"},
unexpected: []string{"services"},
@@ -208,10 +221,10 @@ func TestRootFS_examples(t *testing.T) {
{
name: "python",
fs: []FS{
&adapter{"/", gofstest.MapFS{ // to allow resolution of "."
&adapter{gofstest.MapFS{ // to allow resolution of "."
"pybuilddir.txt": &gofstest.MapFile{},
"opt/wasi-python/lib/python3.11/__phello__/__init__.py": &gofstest.MapFile{},
}},
}, "/"},
},
expected: []string{
".",
@@ -225,8 +238,8 @@ func TestRootFS_examples(t *testing.T) {
{
name: "zig",
fs: []FS{
&adapter{"/", testfs.FS{"zig-cache": &testfs.File{}}},
&adapter{"/tmp", testfs.FS{"qSQRrUkgJX9L20mr": &testfs.File{}}},
&adapter{testfs.FS{"zig-cache": &testfs.File{}}, "/"},
&adapter{testfs.FS{"qSQRrUkgJX9L20mr": &testfs.File{}}, "/tmp"},
},
expected: []string{"zig-cache", "/tmp/qSQRrUkgJX9L20mr"},
unexpected: []string{"/qSQRrUkgJX9L20mr"},

View File

@@ -14,6 +14,18 @@ import (
//
// See https://github.com/golang/go/issues/45757
type FS interface {
// String should return a human-readable format of the filesystem:
// - If read-only, $host:$guestDir:ro
// - If read-write, $host:$guestDir
//
// For example, if this filesystem is backed by the real directory
// "/tmp/wasm" and the GuestDir is "/", the expected value is
// "/var/tmp:/tmp".
//
// When the host filesystem isn't a real filesystem, substitute a symbolic,
// human-readable name. e.g. "virtual:/"
String() string
// GuestDir is the name of the path the guest should use this filesystem
// for, or root ("/") for any files.
//

View File

@@ -414,7 +414,7 @@ func TestReaderAtOffset_Unsupported(t *testing.T) {
func TestWriterAtOffset(t *testing.T) {
tmpDir := t.TempDir()
dirFS, err := NewDirFS("/", tmpDir)
dirFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
// fs.FS doesn't support writes, and there is no other built-in
@@ -481,7 +481,7 @@ func TestWriterAtOffset(t *testing.T) {
func TestWriterAtOffset_empty(t *testing.T) {
tmpDir := t.TempDir()
dirFS, err := NewDirFS("/", tmpDir)
dirFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
// fs.FS doesn't support writes, and there is no other built-in
@@ -523,7 +523,7 @@ func TestWriterAtOffset_empty(t *testing.T) {
func TestWriterAtOffset_Unsupported(t *testing.T) {
tmpDir := t.TempDir()
dirFS, err := NewDirFS("/", tmpDir)
dirFS, err := NewDirFS(tmpDir, "/")
require.NoError(t, err)
f, err := dirFS.OpenFile(readerAtFile, os.O_RDWR|os.O_CREATE, 0o600)