github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/internal/sys/fs_test.go (about) 1 package sys 2 3 import ( 4 "embed" 5 "errors" 6 "fmt" 7 "io/fs" 8 "os" 9 "path" 10 "testing" 11 gofstest "testing/fstest" 12 13 "github.com/wasilibs/wazerox/experimental/sys" 14 "github.com/wasilibs/wazerox/internal/fstest" 15 "github.com/wasilibs/wazerox/internal/sysfs" 16 testfs "github.com/wasilibs/wazerox/internal/testing/fs" 17 "github.com/wasilibs/wazerox/internal/testing/require" 18 ) 19 20 //go:embed testdata 21 var testdata embed.FS 22 23 func TestNewFSContext(t *testing.T) { 24 embedFS, err := fs.Sub(testdata, "testdata") 25 require.NoError(t, err) 26 27 dirfs := sysfs.DirFS(".") 28 29 // Test various usual configuration for the file system. 30 tests := []struct { 31 name string 32 fs sys.FS 33 }{ 34 { 35 name: "embed.FS", 36 fs: &sysfs.AdaptFS{FS: embedFS}, 37 }, 38 { 39 name: "DirFS", 40 // Don't use "testdata" because it may not be present in 41 // cross-architecture (a.k.a. scratch) build containers. 42 fs: dirfs, 43 }, 44 { 45 name: "ReadFS", 46 fs: &sysfs.ReadFS{FS: dirfs}, 47 }, 48 { 49 name: "fstest.MapFS", 50 fs: &sysfs.AdaptFS{FS: gofstest.MapFS{}}, 51 }, 52 } 53 54 for _, tt := range tests { 55 tc := tt 56 57 t.Run(tc.name, func(t *testing.T) { 58 for _, root := range []string{"/", ""} { 59 t.Run(fmt.Sprintf("root = '%s'", root), func(t *testing.T) { 60 c := Context{} 61 err := c.InitFSContext(nil, nil, nil, []sys.FS{tc.fs}, []string{root}, nil) 62 require.NoError(t, err) 63 fsc := c.fsc 64 defer fsc.Close() 65 66 preopenedDir, _ := fsc.openedFiles.Lookup(FdPreopen) 67 require.Equal(t, tc.fs, fsc.rootFS) 68 require.NotNil(t, preopenedDir) 69 require.Equal(t, "/", preopenedDir.Name) 70 71 // Verify that each call to OpenFile returns a different file 72 // descriptor. 73 f1, errno := fsc.OpenFile(preopenedDir.FS, preopenedDir.Name, 0, 0) 74 require.EqualErrno(t, 0, errno) 75 require.NotEqual(t, FdPreopen, f1) 76 77 // Verify that file descriptors are reused. 78 // 79 // Note that this specific behavior is not required by WASI which 80 // only documents that file descriptor numbers will be selected 81 // randomly and applications should not rely on them. We added this 82 // test to ensure that our implementation properly reuses descriptor 83 // numbers but if we were to change the reuse strategy, this test 84 // would likely break and need to be updated. 85 require.EqualErrno(t, 0, fsc.CloseFile(f1)) 86 f2, errno := fsc.OpenFile(preopenedDir.FS, preopenedDir.Name, 0, 0) 87 require.EqualErrno(t, 0, errno) 88 require.Equal(t, f1, f2) 89 }) 90 } 91 }) 92 93 } 94 } 95 96 func TestFSContext_CloseFile(t *testing.T) { 97 embedFS, err := fs.Sub(testdata, "testdata") 98 require.NoError(t, err) 99 testFS := &sysfs.AdaptFS{FS: embedFS} 100 101 c := Context{} 102 err = c.InitFSContext(nil, nil, nil, []sys.FS{testFS}, []string{"/"}, nil) 103 require.NoError(t, err) 104 fsc := c.fsc 105 defer fsc.Close() 106 107 fdToClose, errno := fsc.OpenFile(testFS, "empty.txt", sys.O_RDONLY, 0) 108 require.EqualErrno(t, 0, errno) 109 110 fdToKeep, errno := fsc.OpenFile(testFS, "test.txt", sys.O_RDONLY, 0) 111 require.EqualErrno(t, 0, errno) 112 113 // Close 114 require.EqualErrno(t, 0, fsc.CloseFile(fdToClose)) 115 116 // Verify fdToClose is closed and removed from the opened FDs. 117 _, ok := fsc.LookupFile(fdToClose) 118 require.False(t, ok) 119 120 // Verify fdToKeep is not closed 121 _, ok = fsc.LookupFile(fdToKeep) 122 require.True(t, ok) 123 124 t.Run("EBADF for an invalid FD", func(t *testing.T) { 125 require.EqualErrno(t, sys.EBADF, fsc.CloseFile(42)) // 42 is an arbitrary invalid FD 126 }) 127 t.Run("Can close a pre-open", func(t *testing.T) { 128 require.EqualErrno(t, 0, fsc.CloseFile(FdPreopen)) 129 }) 130 } 131 132 func TestFSContext_noPreopens(t *testing.T) { 133 c := Context{} 134 err := c.InitFSContext(nil, nil, nil, nil, nil, nil) 135 require.NoError(t, err) 136 testFS := &c.fsc 137 require.NoError(t, err) 138 139 expected := &FSContext{} 140 noopStdin, _ := stdinFileEntry(nil) 141 expected.openedFiles.Insert(noopStdin) 142 noopStdout, _ := stdioWriterFileEntry("stdout", nil) 143 expected.openedFiles.Insert(noopStdout) 144 noopStderr, _ := stdioWriterFileEntry("stderr", nil) 145 expected.openedFiles.Insert(noopStderr) 146 147 t.Run("Close closes", func(t *testing.T) { 148 err := testFS.Close() 149 require.NoError(t, err) 150 151 // Closes opened files 152 require.Equal(t, &FSContext{}, testFS) 153 }) 154 } 155 156 func TestContext_Close(t *testing.T) { 157 testFS := &sysfs.AdaptFS{FS: testfs.FS{"foo": &testfs.File{}}} 158 159 c := Context{} 160 err := c.InitFSContext(nil, nil, nil, []sys.FS{testFS}, []string{"/"}, nil) 161 require.NoError(t, err) 162 fsc := c.fsc 163 164 // Verify base case 165 require.Equal(t, 1+FdPreopen, int32(fsc.openedFiles.Len())) 166 167 _, errno := fsc.OpenFile(testFS, "foo", sys.O_RDONLY, 0) 168 require.EqualErrno(t, 0, errno) 169 require.Equal(t, 2+FdPreopen, int32(fsc.openedFiles.Len())) 170 171 // Closing should not err. 172 require.NoError(t, fsc.Close()) 173 174 // Verify our intended side-effect 175 require.Zero(t, fsc.openedFiles.Len()) 176 177 // Verify no error closing again. 178 require.NoError(t, fsc.Close()) 179 } 180 181 func TestContext_Close_Error(t *testing.T) { 182 file := &testfs.File{CloseErr: errors.New("error closing")} 183 184 testFS := &sysfs.AdaptFS{FS: testfs.FS{"foo": file}} 185 186 c := Context{} 187 err := c.InitFSContext(nil, nil, nil, []sys.FS{testFS}, []string{"/"}, nil) 188 require.NoError(t, err) 189 fsc := c.fsc 190 191 // open another file 192 _, errno := fsc.OpenFile(testFS, "foo", sys.O_RDONLY, 0) 193 require.EqualErrno(t, 0, errno) 194 195 // arbitrary errors coerce to EIO 196 require.EqualErrno(t, sys.EIO, fsc.Close()) 197 198 // Paths should clear even under error 199 require.Zero(t, fsc.openedFiles.Len(), "expected no opened files") 200 } 201 202 func TestFSContext_Renumber(t *testing.T) { 203 tmpDir := t.TempDir() 204 dirFS := sysfs.DirFS(tmpDir) 205 206 const dirName = "dir" 207 errno := dirFS.Mkdir(dirName, 0o700) 208 require.EqualErrno(t, 0, errno) 209 210 c := Context{} 211 err := c.InitFSContext(nil, nil, nil, []sys.FS{dirFS}, []string{"/"}, nil) 212 require.NoError(t, err) 213 fsc := c.fsc 214 215 defer fsc.Close() 216 217 for _, toFd := range []int32{10, 100, 100} { 218 fromFd, errno := fsc.OpenFile(dirFS, dirName, sys.O_RDONLY, 0) 219 require.EqualErrno(t, 0, errno) 220 221 prevDirFile, ok := fsc.LookupFile(fromFd) 222 require.True(t, ok) 223 224 require.EqualErrno(t, 0, fsc.Renumber(fromFd, toFd)) 225 226 renumberedDirFile, ok := fsc.LookupFile(toFd) 227 require.True(t, ok) 228 229 require.Equal(t, prevDirFile, renumberedDirFile) 230 231 // Previous file descriptor shouldn't be used. 232 _, ok = fsc.LookupFile(fromFd) 233 require.False(t, ok) 234 } 235 236 t.Run("errors", func(t *testing.T) { 237 // Sanity check for 3 being preopen. 238 preopen, ok := fsc.LookupFile(3) 239 require.True(t, ok) 240 require.True(t, preopen.IsPreopen) 241 242 // From is preopen. 243 require.Equal(t, sys.ENOTSUP, fsc.Renumber(3, 100)) 244 245 // From does not exist. 246 require.Equal(t, sys.EBADF, fsc.Renumber(12345, 3)) 247 248 // Both are preopen. 249 require.Equal(t, sys.ENOTSUP, fsc.Renumber(3, 3)) 250 }) 251 } 252 253 func TestDirentCache_Read(t *testing.T) { 254 c := Context{} 255 err := c.InitFSContext(nil, nil, nil, []sys.FS{&sysfs.AdaptFS{FS: fstest.FS}}, []string{"/"}, nil) 256 require.NoError(t, err) 257 fsc := c.fsc 258 defer fsc.Close() 259 260 d, errno := sysfs.OpenFSFile(fstest.FS, "dir", 0, 0) 261 require.EqualErrno(t, 0, errno) 262 defer d.Close() 263 264 testDirents, errno := d.Readdir(-1) 265 if errno != 0 { 266 panic(errno) 267 } 268 testDirents = append([]sys.Dirent{ 269 {Name: ".", Type: fs.ModeDir}, 270 {Name: "..", Type: fs.ModeDir}, 271 }, testDirents...) 272 273 tests := []struct { 274 name string 275 initialDir string 276 dir func(fd int32) 277 fd int32 278 pos uint64 279 n uint32 280 expectedDirents []sys.Dirent 281 expectedErrno sys.Errno 282 }{ 283 { 284 name: "empty dir has dot entries", 285 initialDir: "emptydir", 286 pos: 0, 287 n: 100, 288 expectedDirents: testDirents[:2], 289 }, 290 { 291 name: "rewind empty directory", 292 initialDir: "emptydir", 293 dir: func(fd int32) { 294 f, _ := fsc.LookupFile(fd) 295 rdd, _ := f.DirentCache() 296 _, _ = rdd.Read(0, 5) 297 }, 298 pos: 0, 299 n: 100, 300 expectedDirents: testDirents[:2], 301 }, 302 { 303 name: "full read", 304 initialDir: "dir", 305 pos: 0, 306 n: 100, 307 expectedDirents: testDirents, 308 }, 309 { 310 name: "read first", 311 initialDir: "dir", 312 pos: 0, 313 n: 1, 314 expectedDirents: testDirents[:1], 315 }, 316 { 317 name: "read second", 318 initialDir: "dir", 319 dir: func(fd int32) { 320 f, _ := fsc.LookupFile(fd) 321 rdd, _ := f.DirentCache() 322 _, _ = rdd.Read(0, 1) 323 }, 324 pos: 1, 325 n: 1, 326 expectedDirents: testDirents[1:2], 327 }, 328 { 329 name: "read second and third", 330 initialDir: "dir", 331 dir: func(fd int32) { 332 f, _ := fsc.LookupFile(fd) 333 rdd, _ := f.DirentCache() 334 _, _ = rdd.Read(0, 1) 335 }, 336 pos: 1, 337 n: 2, 338 expectedDirents: testDirents[1:3], 339 }, 340 { 341 name: "read exactly third", 342 initialDir: "dir", 343 dir: func(fd int32) { 344 f, _ := fsc.LookupFile(fd) 345 rdd, _ := f.DirentCache() 346 _, _ = rdd.Read(0, 2) 347 }, 348 pos: 2, 349 n: 1, 350 expectedDirents: testDirents[2:3], 351 }, 352 { 353 name: "read third and beyond", 354 initialDir: "dir", 355 dir: func(fd int32) { 356 f, _ := fsc.LookupFile(fd) 357 rdd, _ := f.DirentCache() 358 _, _ = rdd.Read(0, 2) 359 }, 360 pos: 2, 361 n: 5, 362 expectedDirents: testDirents[2:], 363 }, 364 { 365 name: "read exhausted directory", 366 initialDir: "dir", 367 dir: func(fd int32) { 368 f, _ := fsc.LookupFile(fd) 369 rdd, _ := f.DirentCache() 370 _, _ = rdd.Read(0, 5) 371 }, 372 pos: 5, 373 n: 5, 374 expectedDirents: nil, 375 }, 376 { 377 name: "rewind directory", 378 initialDir: "dir", 379 dir: func(fd int32) { 380 f, _ := fsc.LookupFile(fd) 381 rdd, _ := f.DirentCache() 382 _, _ = rdd.Read(0, 5) 383 }, 384 pos: 0, 385 n: 5, 386 expectedDirents: testDirents, 387 }, 388 { 389 name: "DirentCache: not a dir", 390 initialDir: "dir/-", 391 pos: 0, 392 n: 1, 393 expectedErrno: sys.ENOTDIR, 394 }, 395 { 396 name: "pos invalid when no prior state", 397 initialDir: "dir", 398 pos: 1, 399 n: 1, 400 expectedErrno: sys.ENOENT, 401 }, 402 } 403 404 for _, tt := range tests { 405 tc := tt 406 t.Run(tc.name, func(t *testing.T) { 407 fd, errno := fsc.OpenFile(fsc.RootFS(), tc.initialDir, sys.O_RDONLY, 0) 408 require.EqualErrno(t, 0, errno) 409 defer fsc.CloseFile(fd) // nolint 410 f, _ := fsc.LookupFile(fd) 411 dir, errno := f.DirentCache() 412 if errno != 0 { 413 require.EqualErrno(t, tc.expectedErrno, errno) 414 return 415 } 416 417 if tc.dir != nil { 418 tc.dir(fd) 419 } 420 421 dirents, errno := dir.Read(tc.pos, tc.n) 422 require.EqualErrno(t, tc.expectedErrno, errno) 423 require.Equal(t, tc.expectedDirents, dirents) 424 }) 425 } 426 } 427 428 // This is similar to https://github.com/WebAssembly/wasi-testsuite/blob/ac32f57400cdcdd0425d3085c24fc7fc40011d1c/tests/rust/src/bin/fd_readdir.rs#L120 429 func TestDirentCache_ReadNewFile(t *testing.T) { 430 tmpDir := t.TempDir() 431 432 c := Context{} 433 err := c.InitFSContext(nil, nil, nil, []sys.FS{sysfs.DirFS(tmpDir)}, []string{"/"}, nil) 434 require.NoError(t, err) 435 fsc := c.fsc 436 defer fsc.Close() 437 438 fd, errno := fsc.OpenFile(fsc.RootFS(), ".", sys.O_RDONLY, 0) 439 require.EqualErrno(t, 0, errno) 440 defer fsc.CloseFile(fd) // nolint 441 f, _ := fsc.LookupFile(fd) 442 443 dir, errno := f.DirentCache() 444 require.EqualErrno(t, 0, errno) 445 446 // Read the empty directory, which should only have the dot entries. 447 dirents, errno := dir.Read(0, 5) 448 require.EqualErrno(t, 0, errno) 449 require.Equal(t, 2, len(dirents)) 450 require.Equal(t, ".", dirents[0].Name) 451 require.Equal(t, "..", dirents[1].Name) 452 453 // Write a new file to the directory 454 require.NoError(t, os.WriteFile(path.Join(tmpDir, "file"), nil, 0o0666)) 455 456 // Read it again, which should see the new file. 457 dirents, errno = dir.Read(0, 5) 458 require.EqualErrno(t, 0, errno) 459 require.Equal(t, 3, len(dirents)) 460 require.Equal(t, ".", dirents[0].Name) 461 require.Equal(t, "..", dirents[1].Name) 462 require.Equal(t, "file", dirents[2].Name) 463 464 // Read it again, using the file position. 465 filePos := uint64(2) 466 dirents, errno = dir.Read(filePos, 3) 467 require.EqualErrno(t, 0, errno) 468 require.Equal(t, 1, len(dirents)) 469 require.Equal(t, "file", dirents[0].Name) 470 } 471 472 func TestStripPrefixesAndTrailingSlash(t *testing.T) { 473 tests := []struct { 474 path, expected string 475 }{ 476 { 477 path: ".", 478 expected: "", 479 }, 480 { 481 path: "/", 482 expected: "", 483 }, 484 { 485 path: "./", 486 expected: "", 487 }, 488 { 489 path: "./foo", 490 expected: "foo", 491 }, 492 { 493 path: ".foo", 494 expected: ".foo", 495 }, 496 { 497 path: "././foo", 498 expected: "foo", 499 }, 500 { 501 path: "/foo", 502 expected: "foo", 503 }, 504 { 505 path: "foo/", 506 expected: "foo", 507 }, 508 { 509 path: "//", 510 expected: "", 511 }, 512 { 513 path: "../../", 514 expected: "../..", 515 }, 516 { 517 path: "./../../", 518 expected: "../..", 519 }, 520 } 521 522 for _, tt := range tests { 523 tc := tt 524 525 t.Run(tc.path, func(t *testing.T) { 526 path := StripPrefixesAndTrailingSlash(tc.path) 527 require.Equal(t, tc.expected, path) 528 }) 529 } 530 }