Repository: tetratelabs/wazero
Pull request #972 (2022-12-29 02:21:32)

Exposes experimental writefs.DirFS, but hides implementation
The type we use to expose write operations is still evolving. It might be a single writefs.FS interface, or similar to go where we have an interface per feature (e.g. writefs.MkdirFS). These choices are all implementation details for DirFS and won't be settled before the end of the month version cutoff. Instead, this only exposes the ability to create a DirFS, not an arbitrary implementation of writefs.FS. This does so by making `writefs.FS` an internal type.

codefromthecrypt: @achille-roussel this will drift you, but it will allow end users to start with write features while we decide on a pluggability API. Those giving just a filepath don't need to wait for api shape to settle. We can finalize these things in january, in other words, but allow folks to begin using write support meanwhile.

Patch:
diff --git a/cmd/wazero/wazero.go b/cmd/wazero/wazero.go
index 42bded22..9d4aac73 100644
--- a/cmd/wazero/wazero.go
+++ b/cmd/wazero/wazero.go
@@ -169,7 +169,7 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod
 		host := mount[0]
 		guest := mount[1]
 		if guest == "" { // guest is root
-			rootFS = writefs.New(host)
+			rootFS = writefs.DirFS(host)
 		} else { // TODO: subfs
 			rootFS = &compositeFS{
 				paths: map[string]fs.FS{guest: os.DirFS(host)},
diff --git a/experimental/writefs/writefs.go b/experimental/writefs/writefs.go
index 6a45dcec..c974b2de 100644
--- a/experimental/writefs/writefs.go
+++ b/experimental/writefs/writefs.go
@@ -1,62 +1,28 @@
+// Package writefs includes wazero-specific fs.FS implementations that allow
+// creation and deletion of files and directories.
+//
+// This is a work-in-progress and a workaround needed because write support is
+// not yet supported in fs.FS. See https://github.com/golang/go/issues/45757
+//
+// Tracking issue: https://github.com/tetratelabs/wazero/issues/390
 package writefs
 
 import (
 	"io/fs"
-	"os"
+
+	"github.com/tetratelabs/wazero/internal/writefs"
 )
 
-// FS is a fs.FS which can also create new files or directories.
+// DirFS creates a writeable filesystem at the given path on the host filesystem.
 //
-// Any unsupported method should return syscall.ENOSYS.
+// This is like os.DirFS, but allows creation and deletion of files and
+// directories, which aren't yet supported in fs.FS.
 //
-// See https://github.com/golang/go/issues/45757
-type FS interface {
-	fs.FS
-
-	// OpenFile is similar to os.OpenFile, except the path is relative to this
-	// file system.
-	OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error)
-
-	// Mkdir is similar to os.Mkdir, except the path is relative to this file
-	// system.
-	Mkdir(name string, perm fs.FileMode) error
-
-	// Remove is similar to os.Remove, except the path is relative to this file
-	// system.
-	Remove(path string) error
-}
-
-func New(absoluteDir string) FS {
-	return writeFS(absoluteDir)
-}
-
-type writeFS string
-
-// Open implements fs.FS
-func (dir writeFS) Open(name string) (fs.File, error) {
-	return dir.OpenFile(name, os.O_RDONLY, 0) // same as os.Open(string)
-}
-
-// OpenFile implements FS.OpenFile
-func (dir writeFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) {
-	if !fs.ValidPath(name) {
-		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
-	}
-	return os.OpenFile(string(dir)+"/"+name, flag, perm)
-}
-
-// Mkdir implements FS.Mkdir
-func (dir writeFS) Mkdir(name string, perm fs.FileMode) error {
-	if !fs.ValidPath(name) {
-		return &fs.PathError{Op: "mkdir", Path: name, Err: fs.ErrInvalid}
-	}
-	return os.Mkdir(string(dir)+"/"+name, perm)
-}
-
-// Remove implements FS.Remove
-func (dir writeFS) Remove(path string) error {
-	if !fs.ValidPath(path) {
-		return &fs.PathError{Op: "remove", Path: path, Err: fs.ErrInvalid}
-	}
-	return os.Remove(string(dir) + "/" + path)
+// # Isolation
+//
+// Symbolic links can escape the root path as files are opened via os.OpenFile
+// which cannot restrict following them.
+func DirFS(dir string) fs.FS {
+	// writefs.FS is intentionally internal as it is still evolving
+	return writefs.DirFS(dir)
 }
diff --git a/internal/sys/fs.go b/internal/sys/fs.go
index 84deb60a..d9d3d695 100644
--- a/internal/sys/fs.go
+++ b/internal/sys/fs.go
@@ -13,8 +13,8 @@ import (
 	"syscall"
 	"time"
 
-	"github.com/tetratelabs/wazero/experimental/writefs"
 	"github.com/tetratelabs/wazero/internal/platform"
+	"github.com/tetratelabs/wazero/internal/writefs"
 )
 
 const (
diff --git a/internal/writefs/writefs.go b/internal/writefs/writefs.go
new file mode 100644
index 00000000..874805d9
--- /dev/null
+++ b/internal/writefs/writefs.go
@@ -0,0 +1,62 @@
+package writefs
+
+import (
+	"io/fs"
+	"os"
+)
+
+// FS is a fs.FS which can also create new files or directories.
+//
+// Any unsupported method should return syscall.ENOSYS.
+//
+// See https://github.com/golang/go/issues/45757
+type FS interface {
+	fs.FS
+
+	// OpenFile is similar to os.OpenFile, except the path is relative to this
+	// file system.
+	OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error)
+
+	// Mkdir is similar to os.Mkdir, except the path is relative to this file
+	// system.
+	Mkdir(name string, perm fs.FileMode) error
+
+	// Remove is similar to os.Remove, except the path is relative to this file
+	// system.
+	Remove(path string) error
+}
+
+func DirFS(dir string) FS {
+	return dirFS(dir)
+}
+
+type dirFS string
+
+// Open implements the same method as documented on fs.FS
+func (dir dirFS) Open(name string) (fs.File, error) {
+	return dir.OpenFile(name, os.O_RDONLY, 0) // same as os.Open(string)
+}
+
+// OpenFile implements FS.OpenFile
+func (dir dirFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) {
+	if !fs.ValidPath(name) {
+		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
+	}
+	return os.OpenFile(string(dir)+"/"+name, flag, perm)
+}
+
+// Mkdir implements FS.Mkdir
+func (dir dirFS) Mkdir(name string, perm fs.FileMode) error {
+	if !fs.ValidPath(name) {
+		return &fs.PathError{Op: "mkdir", Path: name, Err: fs.ErrInvalid}
+	}
+	return os.Mkdir(string(dir)+"/"+name, perm)
+}
+
+// Remove implements FS.Remove
+func (dir dirFS) Remove(path string) error {
+	if !fs.ValidPath(path) {
+		return &fs.PathError{Op: "remove", Path: path, Err: fs.ErrInvalid}
+	}
+	return os.Remove(string(dir) + "/" + path)
+}


Test patch:
diff --git a/experimental/writefs/writefs_example_test.go b/experimental/writefs/writefs_example_test.go
new file mode 100644
index 00000000..79253a22
--- /dev/null
+++ b/experimental/writefs/writefs_example_test.go
@@ -0,0 +1,16 @@
+package writefs_test
+
+import (
+	_ "embed"
+
+	"github.com/tetratelabs/wazero"
+	"github.com/tetratelabs/wazero/experimental/writefs"
+)
+
+var config wazero.ModuleConfig //nolint
+
+// This shows how to use writefs.DirFS to map paths relative to "/work/appA",
+// as "/". Unlike os.DirFS, these paths will be writable.
+func Example_dirFS() {
+	config = wazero.NewModuleConfig().WithFS(writefs.DirFS("/work/appA"))
+}
diff --git a/internal/gojs/fs_test.go b/internal/gojs/fs_test.go
index 938e88a0..22d86b8e 100644
--- a/internal/gojs/fs_test.go
+++ b/internal/gojs/fs_test.go
@@ -33,7 +33,7 @@ func Test_writefs(t *testing.T) {
 	t.Parallel()
 	tmpDir := t.TempDir()
 
-	fs := writefs.New(tmpDir)
+	fs := writefs.DirFS(tmpDir)
 	// test expects to write under /tmp
 	require.NoError(t, os.Mkdir(path.Join(tmpDir, "tmp"), 0o700))
 
diff --git a/experimental/writefs/writefs_test.go b/internal/writefs/writefs_test.go
similarity index 94%
rename from experimental/writefs/writefs_test.go
rename to internal/writefs/writefs_test.go
index 25f5c34c..1ec928bd 100644
--- a/experimental/writefs/writefs_test.go
+++ b/internal/writefs/writefs_test.go
@@ -33,7 +33,7 @@ func TestFS(t *testing.T) {
 		require.NoError(t, os.WriteFile(path.Join(dir, name), []byte(data), 0o600))
 	}
 
-	if err := fstest.TestFS(New(dir), expected...); err != nil {
+	if err := fstest.TestFS(DirFS(dir), expected...); err != nil {
 		t.Fatal(err)
 	}
 }
@@ -41,7 +41,7 @@ func TestFS(t *testing.T) {
 func TestMkDir(t *testing.T) {
 	dir := t.TempDir()
 
-	testFS := New(dir)
+	testFS := DirFS(dir)
 
 	name := "mkdir"
 
@@ -61,7 +61,7 @@ func TestMkDir(t *testing.T) {
 func TestRemove(t *testing.T) {
 	dir := t.TempDir()
 
-	testFS := New(dir)
+	testFS := DirFS(dir)
 
 	name := "remove"
 
diff --git a/namespace_test.go b/namespace_test.go
index d37ede41..6d2d1160 100644
--- a/namespace_test.go
+++ b/namespace_test.go
@@ -7,8 +7,7 @@ import (
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
-// TestRuntime_Namespace ensures namespaces are independent.
-func TestRuntime_Namespace(t *testing.T) {
+func TestRuntime_Namespace_Independent(t *testing.T) {
 	r := NewRuntime(testCtx)
 	defer r.Close(testCtx)