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 }