github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/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/bananabytelabs/wazero/experimental/sys"
    14  	"github.com/bananabytelabs/wazero/internal/fstest"
    15  	"github.com/bananabytelabs/wazero/internal/sysfs"
    16  	testfs "github.com/bananabytelabs/wazero/internal/testing/fs"
    17  	"github.com/bananabytelabs/wazero/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  }