github.com/tetratelabs/wazero@v1.2.1/internal/sysfs/rootfs_test.go (about)

     1  package sysfs
     2  
     3  import (
     4  	"errors"
     5  	"io/fs"
     6  	"os"
     7  	"path"
     8  	"sort"
     9  	"strings"
    10  	"syscall"
    11  	"testing"
    12  	gofstest "testing/fstest"
    13  
    14  	"github.com/tetratelabs/wazero/internal/fsapi"
    15  	"github.com/tetratelabs/wazero/internal/fstest"
    16  	testfs "github.com/tetratelabs/wazero/internal/testing/fs"
    17  	"github.com/tetratelabs/wazero/internal/testing/require"
    18  )
    19  
    20  func TestNewRootFS(t *testing.T) {
    21  	t.Run("empty", func(t *testing.T) {
    22  		rootFS, err := NewRootFS(nil, nil)
    23  		require.NoError(t, err)
    24  
    25  		require.Equal(t, fsapi.UnimplementedFS{}, rootFS)
    26  	})
    27  	t.Run("only root", func(t *testing.T) {
    28  		testFS := NewDirFS(t.TempDir())
    29  
    30  		rootFS, err := NewRootFS([]fsapi.FS{testFS}, []string{""})
    31  		require.NoError(t, err)
    32  
    33  		// Should not be a composite filesystem
    34  		require.Equal(t, testFS, rootFS)
    35  	})
    36  	t.Run("only non root", func(t *testing.T) {
    37  		testFS := NewDirFS(".")
    38  
    39  		rootFS, err := NewRootFS([]fsapi.FS{testFS}, []string{"/tmp"})
    40  		require.NoError(t, err)
    41  
    42  		// unwrapping returns in original order
    43  		require.Equal(t, []fsapi.FS{testFS}, rootFS.(*CompositeFS).FS())
    44  		require.Equal(t, []string{"/tmp"}, rootFS.(*CompositeFS).GuestPaths())
    45  
    46  		// String is human-readable
    47  		require.Equal(t, "[.:/tmp]", rootFS.String())
    48  
    49  		// Guest can look up /tmp
    50  		f, errno := rootFS.OpenFile("/tmp", os.O_RDONLY, 0)
    51  		require.EqualErrno(t, 0, errno)
    52  		require.EqualErrno(t, 0, f.Close())
    53  
    54  		// Guest can look up / and see "/tmp" in it
    55  		f, errno = rootFS.OpenFile("/", os.O_RDONLY, 0)
    56  		require.EqualErrno(t, 0, errno)
    57  
    58  		dirents, errno := f.Readdir(-1)
    59  		require.EqualErrno(t, 0, errno)
    60  		require.Equal(t, 1, len(dirents))
    61  		require.Equal(t, "tmp", dirents[0].Name)
    62  		require.True(t, dirents[0].IsDir())
    63  	})
    64  	t.Run("multiple roots unsupported", func(t *testing.T) {
    65  		testFS := NewDirFS(".")
    66  
    67  		_, err := NewRootFS([]fsapi.FS{testFS, testFS}, []string{"/", "/"})
    68  		require.EqualError(t, err, "multiple root filesystems are invalid: [.:/ .:/]")
    69  	})
    70  	t.Run("virtual paths unsupported", func(t *testing.T) {
    71  		testFS := NewDirFS(".")
    72  
    73  		_, err := NewRootFS([]fsapi.FS{testFS}, []string{"usr/bin"})
    74  		require.EqualError(t, err, "only single-level guest paths allowed: [.:usr/bin]")
    75  	})
    76  	t.Run("multiple matches", func(t *testing.T) {
    77  		tmpDir1 := t.TempDir()
    78  		testFS1 := NewDirFS(tmpDir1)
    79  		require.NoError(t, os.Mkdir(path.Join(tmpDir1, "tmp"), 0o700))
    80  		require.NoError(t, os.WriteFile(path.Join(tmpDir1, "a"), []byte{1}, 0o600))
    81  
    82  		tmpDir2 := t.TempDir()
    83  		testFS2 := NewDirFS(tmpDir2)
    84  		require.NoError(t, os.WriteFile(path.Join(tmpDir2, "a"), []byte{2}, 0o600))
    85  
    86  		rootFS, err := NewRootFS([]fsapi.FS{testFS2, testFS1}, []string{"/tmp", "/"})
    87  		require.NoError(t, err)
    88  
    89  		// unwrapping returns in original order
    90  		require.Equal(t, []fsapi.FS{testFS2, testFS1}, rootFS.(*CompositeFS).FS())
    91  		require.Equal(t, []string{"/tmp", "/"}, rootFS.(*CompositeFS).GuestPaths())
    92  
    93  		// Should be a composite filesystem
    94  		require.NotEqual(t, testFS1, rootFS)
    95  		require.NotEqual(t, testFS2, rootFS)
    96  
    97  		t.Run("last wins", func(t *testing.T) {
    98  			f, errno := rootFS.OpenFile("/tmp/a", os.O_RDONLY, 0)
    99  			require.EqualErrno(t, 0, errno)
   100  			defer f.Close()
   101  
   102  			b := readAll(t, f)
   103  			require.Equal(t, []byte{2}, b)
   104  		})
   105  
   106  		// This test is covered by fstest.TestFS, but doing again here
   107  		t.Run("root includes prefix mount", func(t *testing.T) {
   108  			f, errno := rootFS.OpenFile(".", os.O_RDONLY, 0)
   109  			require.EqualErrno(t, 0, errno)
   110  			defer f.Close()
   111  
   112  			entries, errno := f.Readdir(-1)
   113  			require.EqualErrno(t, 0, errno)
   114  			names := make([]string, 0, len(entries))
   115  			for _, e := range entries {
   116  				names = append(names, e.Name)
   117  			}
   118  			sort.Strings(names)
   119  
   120  			require.Equal(t, []string{"a", "tmp"}, names)
   121  		})
   122  	})
   123  }
   124  
   125  func TestRootFS_String(t *testing.T) {
   126  	tmpFS := NewDirFS(".")
   127  	rootFS := NewDirFS(".")
   128  
   129  	testFS, err := NewRootFS([]fsapi.FS{rootFS, tmpFS}, []string{"/", "/tmp"})
   130  	require.NoError(t, err)
   131  
   132  	require.Equal(t, "[.:/ .:/tmp]", testFS.String())
   133  }
   134  
   135  func TestRootFS_Open(t *testing.T) {
   136  	tmpDir := t.TempDir()
   137  
   138  	// Create a subdirectory, so we can test reads outside the fsapi.FS root.
   139  	tmpDir = path.Join(tmpDir, t.Name())
   140  	require.NoError(t, os.Mkdir(tmpDir, 0o700))
   141  	require.NoError(t, fstest.WriteTestFiles(tmpDir))
   142  
   143  	testRootFS := NewDirFS(tmpDir)
   144  	testDirFS := NewDirFS(t.TempDir())
   145  	testFS, err := NewRootFS([]fsapi.FS{testRootFS, testDirFS}, []string{"/", "/emptydir"})
   146  	require.NoError(t, err)
   147  
   148  	testOpen_Read(t, testFS, true)
   149  
   150  	testOpen_O_RDWR(t, tmpDir, testFS)
   151  
   152  	t.Run("path outside root valid", func(t *testing.T) {
   153  		_, err := testFS.OpenFile("../foo", os.O_RDONLY, 0)
   154  
   155  		// fsapi.FS allows relative path lookups
   156  		require.True(t, errors.Is(err, fs.ErrNotExist))
   157  	})
   158  }
   159  
   160  func TestRootFS_Stat(t *testing.T) {
   161  	tmpDir := t.TempDir()
   162  	require.NoError(t, fstest.WriteTestFiles(tmpDir))
   163  
   164  	tmpFS := NewDirFS(t.TempDir())
   165  	testFS, err := NewRootFS([]fsapi.FS{NewDirFS(tmpDir), tmpFS}, []string{"/", "/tmp"})
   166  	require.NoError(t, err)
   167  	testStat(t, testFS)
   168  }
   169  
   170  func TestRootFS_examples(t *testing.T) {
   171  	tests := []struct {
   172  		name                 string
   173  		fs                   []fsapi.FS
   174  		guestPaths           []string
   175  		expected, unexpected []string
   176  	}{
   177  		// e.g. from Go project root:
   178  		//	$ GOOS=js GOARCH=wasm bin/go test -c -o template.wasm text/template
   179  		//	$ wazero run -mount=src/text/template:/ template.wasm -test.v
   180  		{
   181  			name: "go test text/template",
   182  			fs: []fsapi.FS{
   183  				&adapter{fs: testfs.FS{"go-example-stdout-ExampleTemplate-0.txt": &testfs.File{}}},
   184  				&adapter{fs: testfs.FS{"testdata/file1.tmpl": &testfs.File{}}},
   185  			},
   186  			guestPaths: []string{"/tmp", "/"},
   187  			expected:   []string{"/tmp/go-example-stdout-ExampleTemplate-0.txt", "testdata/file1.tmpl"},
   188  			unexpected: []string{"DOES NOT EXIST"},
   189  		},
   190  		// e.g. from TinyGo project root:
   191  		//	$ ./build/tinygo test -target wasi -c -o flate.wasm compress/flate
   192  		//	$ wazero run -mount=$(go env GOROOT)/src/compress/flate:/ flate.wasm -test.v
   193  		{
   194  			name: "tinygo test compress/flate",
   195  			fs: []fsapi.FS{
   196  				&adapter{fs: testfs.FS{}},
   197  				&adapter{fs: testfs.FS{"testdata/e.txt": &testfs.File{}}},
   198  				&adapter{fs: testfs.FS{"testdata/Isaac.Newton-Opticks.txt": &testfs.File{}}},
   199  			},
   200  			guestPaths: []string{"/", "../", "../../"},
   201  			expected:   []string{"../testdata/e.txt", "../../testdata/Isaac.Newton-Opticks.txt"},
   202  			unexpected: []string{"../../testdata/e.txt"},
   203  		},
   204  		// e.g. from Go project root:
   205  		//	$ GOOS=js GOARCH=wasm bin/go test -c -o net.wasm ne
   206  		//	$ wazero run -mount=src/net:/ net.wasm -test.v -test.short
   207  		{
   208  			name: "go test net",
   209  			fs: []fsapi.FS{
   210  				&adapter{fs: testfs.FS{"services": &testfs.File{}}},
   211  				&adapter{fs: testfs.FS{"testdata/aliases": &testfs.File{}}},
   212  			},
   213  			guestPaths: []string{"/etc", "/"},
   214  			expected:   []string{"/etc/services", "testdata/aliases"},
   215  			unexpected: []string{"services"},
   216  		},
   217  		// e.g. from wagi-python project root:
   218  		//	$ GOOS=js GOARCH=wasm bin/go test -c -o net.wasm ne
   219  		//	$ wazero run -hostlogging=filesystem -mount=.:/ -env=PYTHONHOME=/opt/wasi-python/lib/python3.11 \
   220  		//	  -env=PYTHONPATH=/opt/wasi-python/lib/python3.11 opt/wasi-python/bin/python3.wasm
   221  		{
   222  			name: "python",
   223  			fs: []fsapi.FS{
   224  				&adapter{fs: gofstest.MapFS{ // to allow resolution of "."
   225  					"pybuilddir.txt": &gofstest.MapFile{},
   226  					"opt/wasi-python/lib/python3.11/__phello__/__init__.py": &gofstest.MapFile{},
   227  				}},
   228  			},
   229  			guestPaths: []string{"/"},
   230  			expected: []string{
   231  				".",
   232  				"pybuilddir.txt",
   233  				"opt/wasi-python/lib/python3.11/__phello__/__init__.py",
   234  			},
   235  		},
   236  		// e.g. from Zig project root: TODO: verify this once cli works with multiple mounts
   237  		//	$ zig test --test-cmd wazero --test-cmd run --test-cmd -mount=.:/ -mount=/tmp:/tmp \
   238  		//	  --test-cmd-bin -target wasm32-wasi --zig-lib-dir ./lib ./lib/std/std.zig
   239  		{
   240  			name: "zig",
   241  			fs: []fsapi.FS{
   242  				&adapter{fs: testfs.FS{"zig-cache": &testfs.File{}}},
   243  				&adapter{fs: testfs.FS{"qSQRrUkgJX9L20mr": &testfs.File{}}},
   244  			},
   245  			guestPaths: []string{"/", "/tmp"},
   246  			expected:   []string{"zig-cache", "/tmp/qSQRrUkgJX9L20mr"},
   247  			unexpected: []string{"/qSQRrUkgJX9L20mr"},
   248  		},
   249  	}
   250  
   251  	for _, tt := range tests {
   252  		tc := tt
   253  
   254  		t.Run(tc.name, func(t *testing.T) {
   255  			root, err := NewRootFS(tc.fs, tc.guestPaths)
   256  			require.NoError(t, err)
   257  
   258  			for _, p := range tc.expected {
   259  				f, errno := root.OpenFile(p, os.O_RDONLY, 0)
   260  				require.Zero(t, errno, p)
   261  				require.EqualErrno(t, 0, f.Close(), p)
   262  			}
   263  
   264  			for _, p := range tc.unexpected {
   265  				_, err := root.OpenFile(p, os.O_RDONLY, 0)
   266  				require.EqualErrno(t, syscall.ENOENT, err)
   267  			}
   268  		})
   269  	}
   270  }
   271  
   272  func Test_stripPrefixesAndTrailingSlash(t *testing.T) {
   273  	tests := []struct {
   274  		path, expected string
   275  	}{
   276  		{
   277  			path:     ".",
   278  			expected: "",
   279  		},
   280  		{
   281  			path:     "/",
   282  			expected: "",
   283  		},
   284  		{
   285  			path:     "./",
   286  			expected: "",
   287  		},
   288  		{
   289  			path:     "./foo",
   290  			expected: "foo",
   291  		},
   292  		{
   293  			path:     ".foo",
   294  			expected: ".foo",
   295  		},
   296  		{
   297  			path:     "././foo",
   298  			expected: "foo",
   299  		},
   300  		{
   301  			path:     "/foo",
   302  			expected: "foo",
   303  		},
   304  		{
   305  			path:     "foo/",
   306  			expected: "foo",
   307  		},
   308  		{
   309  			path:     "//",
   310  			expected: "",
   311  		},
   312  		{
   313  			path:     "../../",
   314  			expected: "../..",
   315  		},
   316  		{
   317  			path:     "./../../",
   318  			expected: "../..",
   319  		},
   320  	}
   321  
   322  	for _, tt := range tests {
   323  		tc := tt
   324  
   325  		t.Run(tc.path, func(t *testing.T) {
   326  			pathI, pathLen := stripPrefixesAndTrailingSlash(tc.path)
   327  			require.Equal(t, tc.expected, tc.path[pathI:pathLen])
   328  		})
   329  	}
   330  }
   331  
   332  func Test_hasPathPrefix(t *testing.T) {
   333  	tests := []struct {
   334  		name                  string
   335  		path, prefix          string
   336  		expectEq, expectMatch bool
   337  	}{
   338  		{
   339  			name:        "empty prefix",
   340  			path:        "foo",
   341  			prefix:      "",
   342  			expectEq:    false,
   343  			expectMatch: true,
   344  		},
   345  		{
   346  			name:        "equal prefix",
   347  			path:        "foo",
   348  			prefix:      "foo",
   349  			expectEq:    true,
   350  			expectMatch: true,
   351  		},
   352  		{
   353  			name:        "sub path",
   354  			path:        "foo/bar",
   355  			prefix:      "foo",
   356  			expectMatch: true,
   357  		},
   358  		{
   359  			name:        "different sub path",
   360  			path:        "foo/bar",
   361  			prefix:      "bar",
   362  			expectMatch: false,
   363  		},
   364  		{
   365  			name:        "different path same length",
   366  			path:        "foo",
   367  			prefix:      "bar",
   368  			expectMatch: false,
   369  		},
   370  		{
   371  			name:        "longer path",
   372  			path:        "foo",
   373  			prefix:      "foo/bar",
   374  			expectMatch: false,
   375  		},
   376  		{
   377  			name:        "path shorter",
   378  			path:        "foo",
   379  			prefix:      "fooo",
   380  			expectMatch: false,
   381  		},
   382  		{
   383  			name:        "path longer",
   384  			path:        "fooo",
   385  			prefix:      "foo",
   386  			expectMatch: false,
   387  		},
   388  		{
   389  			name:        "shorter path",
   390  			path:        "foo",
   391  			prefix:      "foo/bar",
   392  			expectMatch: false,
   393  		},
   394  		{
   395  			name:        "wrong and shorter path",
   396  			path:        "foo",
   397  			prefix:      "bar/foo",
   398  			expectMatch: false,
   399  		},
   400  		{
   401  			name:        "same relative",
   402  			path:        "../..",
   403  			prefix:      "../..",
   404  			expectEq:    true,
   405  			expectMatch: true,
   406  		},
   407  		{
   408  			name:        "longer relative",
   409  			path:        "..",
   410  			prefix:      "../..",
   411  			expectMatch: false,
   412  		},
   413  	}
   414  
   415  	for _, tt := range tests {
   416  		tc := tt
   417  
   418  		t.Run(tc.name, func(t *testing.T) {
   419  			path := "././." + tc.path + "/"
   420  			eq, match := hasPathPrefix(path, 5, 5+len(tc.path), tc.prefix)
   421  			require.Equal(t, tc.expectEq, eq)
   422  			require.Equal(t, tc.expectMatch, match)
   423  		})
   424  	}
   425  }
   426  
   427  // BenchmarkHasPrefixVsIterate shows that iteration is faster than re-slicing
   428  // for a prefix match.
   429  func BenchmarkHasPrefixVsIterate(b *testing.B) {
   430  	s := "../../.."
   431  	prefix := "../bar"
   432  	prefixLen := len(prefix)
   433  	b.Run("strings.HasPrefix", func(b *testing.B) {
   434  		for i := 0; i < b.N; i++ {
   435  			if strings.HasPrefix(s, prefix) { //nolint
   436  			}
   437  		}
   438  	})
   439  	b.Run("iterate", func(b *testing.B) {
   440  		for i := 0; i < b.N; i++ {
   441  			for i := 0; i < prefixLen; i++ {
   442  				if s[i] != prefix[i] {
   443  					break
   444  				}
   445  			}
   446  		}
   447  	})
   448  }