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:
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
11
internal/syscallfs/emptyfs_test.go
Normal file
11
internal/syscallfs/emptyfs_test.go
Normal 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())
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user