github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/internal/sysfs/sysfs_test.go (about)

     1  package sysfs
     2  
     3  import (
     4  	_ "embed"
     5  	"io"
     6  	"io/fs"
     7  	"os"
     8  	"path"
     9  	"runtime"
    10  	"sort"
    11  	"testing"
    12  
    13  	experimentalsys "github.com/bananabytelabs/wazero/experimental/sys"
    14  	"github.com/bananabytelabs/wazero/internal/platform"
    15  	"github.com/bananabytelabs/wazero/internal/testing/require"
    16  	"github.com/bananabytelabs/wazero/sys"
    17  )
    18  
    19  func testOpen_O_RDWR(t *testing.T, tmpDir string, testFS experimentalsys.FS) {
    20  	file := "file"
    21  	realPath := path.Join(tmpDir, file)
    22  	err := os.WriteFile(realPath, []byte{}, 0o600)
    23  	require.NoError(t, err)
    24  
    25  	f, errno := testFS.OpenFile(file, experimentalsys.O_RDWR, 0)
    26  	require.EqualErrno(t, 0, errno)
    27  	defer f.Close()
    28  
    29  	// If the write flag was honored, we should be able to write!
    30  	fileContents := []byte{1, 2, 3, 4}
    31  	n, errno := f.Write(fileContents)
    32  	require.EqualErrno(t, 0, errno)
    33  	require.Equal(t, len(fileContents), n)
    34  
    35  	// Verify the contents actually wrote.
    36  	b, err := os.ReadFile(realPath)
    37  	require.NoError(t, err)
    38  	require.Equal(t, fileContents, b)
    39  
    40  	require.EqualErrno(t, 0, f.Close())
    41  
    42  	// re-create as read-only, using 0444 to allow read-back on windows.
    43  	require.NoError(t, os.Remove(realPath))
    44  	f, errno = testFS.OpenFile(file, experimentalsys.O_RDONLY|experimentalsys.O_CREAT, 0o444)
    45  	require.EqualErrno(t, 0, errno)
    46  	defer f.Close()
    47  
    48  	if runtime.GOOS != "windows" {
    49  		// If the read-only flag was honored, we should not be able to write!
    50  		_, err = f.Write(fileContents)
    51  		require.EqualErrno(t, experimentalsys.EBADF, experimentalsys.UnwrapOSError(err))
    52  	}
    53  
    54  	// Verify stat on the file
    55  	stat, errno := f.Stat()
    56  	require.EqualErrno(t, 0, errno)
    57  	require.Equal(t, fs.FileMode(0o444), stat.Mode.Perm())
    58  
    59  	// from os.TestDirFSPathsValid
    60  	if runtime.GOOS != "windows" {
    61  		t.Run("strange name", func(t *testing.T) {
    62  			f, errno = testFS.OpenFile(`e:xperi\ment.txt`, experimentalsys.O_WRONLY|experimentalsys.O_CREAT|experimentalsys.O_TRUNC, 0o600)
    63  			require.EqualErrno(t, 0, errno)
    64  			defer f.Close()
    65  
    66  			_, errno = f.Stat()
    67  			require.EqualErrno(t, 0, errno)
    68  		})
    69  	}
    70  
    71  	t.Run("O_TRUNC", func(t *testing.T) {
    72  		tmpDir := t.TempDir()
    73  		testFS := DirFS(tmpDir)
    74  
    75  		name := "truncate"
    76  		realPath := path.Join(tmpDir, name)
    77  		require.NoError(t, os.WriteFile(realPath, []byte("123456"), 0o0666))
    78  
    79  		f, errno = testFS.OpenFile(name, experimentalsys.O_RDWR|experimentalsys.O_TRUNC, 0o444)
    80  		require.EqualErrno(t, 0, errno)
    81  		require.EqualErrno(t, 0, f.Close())
    82  
    83  		actual, err := os.ReadFile(realPath)
    84  		require.NoError(t, err)
    85  		require.Equal(t, 0, len(actual))
    86  	})
    87  }
    88  
    89  func testOpen_Read(t *testing.T, testFS experimentalsys.FS, requireFileIno, expectDirIno bool) {
    90  	t.Helper()
    91  
    92  	t.Run("doesn't exist", func(t *testing.T) {
    93  		_, errno := testFS.OpenFile("nope", experimentalsys.O_RDONLY, 0)
    94  
    95  		// We currently follow os.Open not syscall.Open, so the error is wrapped.
    96  		require.EqualErrno(t, experimentalsys.ENOENT, errno)
    97  	})
    98  
    99  	t.Run("readdir . opens root", func(t *testing.T) {
   100  		f, errno := testFS.OpenFile(".", experimentalsys.O_RDONLY, 0)
   101  		require.EqualErrno(t, 0, errno)
   102  		defer f.Close()
   103  
   104  		dirents := requireReaddir(t, f, -1, expectDirIno)
   105  
   106  		// Scrub inodes so we can compare expectations without them.
   107  		for i := range dirents {
   108  			dirents[i].Ino = 0
   109  		}
   110  
   111  		require.Equal(t, []experimentalsys.Dirent{
   112  			{Name: "animals.txt", Type: 0},
   113  			{Name: "dir", Type: fs.ModeDir},
   114  			{Name: "empty.txt", Type: 0},
   115  			{Name: "emptydir", Type: fs.ModeDir},
   116  			{Name: "sub", Type: fs.ModeDir},
   117  		}, dirents)
   118  	})
   119  
   120  	t.Run("readdir empty", func(t *testing.T) {
   121  		f, errno := testFS.OpenFile("emptydir", experimentalsys.O_RDONLY, 0)
   122  		require.EqualErrno(t, 0, errno)
   123  		defer f.Close()
   124  
   125  		entries := requireReaddir(t, f, -1, expectDirIno)
   126  		require.Zero(t, len(entries))
   127  	})
   128  
   129  	t.Run("readdir partial", func(t *testing.T) {
   130  		dirF, errno := testFS.OpenFile("dir", experimentalsys.O_RDONLY, 0)
   131  		require.EqualErrno(t, 0, errno)
   132  		defer dirF.Close()
   133  
   134  		dirents1, errno := dirF.Readdir(1)
   135  		require.EqualErrno(t, 0, errno)
   136  		require.Equal(t, 1, len(dirents1))
   137  
   138  		dirents2, errno := dirF.Readdir(1)
   139  		require.EqualErrno(t, 0, errno)
   140  		require.Equal(t, 1, len(dirents2))
   141  
   142  		// read exactly the last entry
   143  		dirents3, errno := dirF.Readdir(1)
   144  		require.EqualErrno(t, 0, errno)
   145  		require.Equal(t, 1, len(dirents3))
   146  
   147  		dirents := []experimentalsys.Dirent{dirents1[0], dirents2[0], dirents3[0]}
   148  		sort.Slice(dirents, func(i, j int) bool { return dirents[i].Name < dirents[j].Name })
   149  
   150  		requireIno(t, dirents, expectDirIno)
   151  
   152  		// Scrub inodes so we can compare expectations without them.
   153  		for i := range dirents {
   154  			dirents[i].Ino = 0
   155  		}
   156  
   157  		require.Equal(t, []experimentalsys.Dirent{
   158  			{Name: "-", Type: 0},
   159  			{Name: "a-", Type: fs.ModeDir},
   160  			{Name: "ab-", Type: 0},
   161  		}, dirents)
   162  
   163  		// no error reading an exhausted directory
   164  		_, errno = dirF.Readdir(1)
   165  		require.EqualErrno(t, 0, errno)
   166  	})
   167  
   168  	t.Run("file exists", func(t *testing.T) {
   169  		f, errno := testFS.OpenFile("animals.txt", experimentalsys.O_RDONLY, 0)
   170  		require.EqualErrno(t, 0, errno)
   171  		defer f.Close()
   172  
   173  		fileContents := []byte(`bear
   174  cat
   175  shark
   176  dinosaur
   177  human
   178  `)
   179  		// Ensure it implements Pread
   180  		lenToRead := len(fileContents) - 1
   181  		buf := make([]byte, lenToRead)
   182  		n, errno := f.Pread(buf, 1)
   183  		require.EqualErrno(t, 0, errno)
   184  		require.Equal(t, lenToRead, n)
   185  		require.Equal(t, fileContents[1:], buf)
   186  
   187  		// Ensure it implements Seek
   188  		offset, errno := f.Seek(1, io.SeekStart)
   189  		require.EqualErrno(t, 0, errno)
   190  		require.Equal(t, int64(1), offset)
   191  
   192  		// Read should pick up from position 1
   193  		n, errno = f.Read(buf)
   194  		require.EqualErrno(t, 0, errno)
   195  		require.Equal(t, lenToRead, n)
   196  		require.Equal(t, fileContents[1:], buf)
   197  	})
   198  
   199  	t.Run("file stat includes inode", func(t *testing.T) {
   200  		f, errno := testFS.OpenFile("empty.txt", experimentalsys.O_RDONLY, 0)
   201  		require.EqualErrno(t, 0, errno)
   202  		defer f.Close()
   203  
   204  		st, errno := f.Stat()
   205  		require.EqualErrno(t, 0, errno)
   206  
   207  		// Results are inconsistent, so don't validate the opposite.
   208  		if requireFileIno {
   209  			require.NotEqual(t, uint64(0), st.Ino, "%+v", st)
   210  		}
   211  	})
   212  
   213  	// Make sure O_RDONLY isn't treated bitwise as it is usually zero.
   214  	t.Run("or'd flag", func(t *testing.T) {
   215  		// Example of a flag that can be or'd into O_RDONLY even if not
   216  		// currently supported in WASI or GOOS=js
   217  		const O_NOATIME = experimentalsys.Oflag(0x40000)
   218  
   219  		f, errno := testFS.OpenFile("animals.txt", experimentalsys.O_RDONLY|O_NOATIME, 0)
   220  		require.EqualErrno(t, 0, errno)
   221  		defer f.Close()
   222  	})
   223  
   224  	t.Run("writing to a read-only file is EBADF", func(t *testing.T) {
   225  		f, errno := testFS.OpenFile("animals.txt", experimentalsys.O_RDONLY, 0)
   226  		require.EqualErrno(t, 0, errno)
   227  		defer f.Close()
   228  
   229  		_, errno = f.Write([]byte{1, 2, 3, 4})
   230  		require.EqualErrno(t, experimentalsys.EBADF, errno)
   231  	})
   232  
   233  	t.Run("opening a directory with O_RDWR is EISDIR", func(t *testing.T) {
   234  		_, errno := testFS.OpenFile("sub", experimentalsys.O_DIRECTORY|experimentalsys.O_RDWR, 0)
   235  		require.EqualErrno(t, experimentalsys.EISDIR, errno)
   236  	})
   237  }
   238  
   239  func testLstat(t *testing.T, testFS experimentalsys.FS) {
   240  	_, errno := testFS.Lstat("cat")
   241  	require.EqualErrno(t, experimentalsys.ENOENT, errno)
   242  	_, errno = testFS.Lstat("sub/cat")
   243  	require.EqualErrno(t, experimentalsys.ENOENT, errno)
   244  
   245  	var st sys.Stat_t
   246  
   247  	t.Run("dir", func(t *testing.T) {
   248  		st, errno = testFS.Lstat(".")
   249  		require.EqualErrno(t, 0, errno)
   250  		require.True(t, st.Mode.IsDir())
   251  		require.NotEqual(t, uint64(0), st.Ino)
   252  	})
   253  
   254  	var stFile sys.Stat_t
   255  
   256  	t.Run("file", func(t *testing.T) {
   257  		stFile, errno = testFS.Lstat("animals.txt")
   258  		require.EqualErrno(t, 0, errno)
   259  
   260  		require.Zero(t, stFile.Mode.Type())
   261  		require.Equal(t, int64(30), stFile.Size)
   262  		require.NotEqual(t, uint64(0), st.Ino)
   263  	})
   264  
   265  	t.Run("link to file", func(t *testing.T) {
   266  		requireLinkStat(t, testFS, "animals.txt", stFile)
   267  	})
   268  
   269  	var stSubdir sys.Stat_t
   270  	t.Run("subdir", func(t *testing.T) {
   271  		stSubdir, errno = testFS.Lstat("sub")
   272  		require.EqualErrno(t, 0, errno)
   273  
   274  		require.True(t, stSubdir.Mode.IsDir())
   275  		require.NotEqual(t, uint64(0), st.Ino)
   276  	})
   277  
   278  	t.Run("link to dir", func(t *testing.T) {
   279  		requireLinkStat(t, testFS, "sub", stSubdir)
   280  	})
   281  
   282  	t.Run("link to dir link", func(t *testing.T) {
   283  		pathLink := "sub-link"
   284  		stLink, errno := testFS.Lstat(pathLink)
   285  		require.EqualErrno(t, 0, errno)
   286  
   287  		requireLinkStat(t, testFS, pathLink, stLink)
   288  	})
   289  }
   290  
   291  func requireLinkStat(t *testing.T, testFS experimentalsys.FS, path string, stat sys.Stat_t) {
   292  	link := path + "-link"
   293  	stLink, errno := testFS.Lstat(link)
   294  	require.EqualErrno(t, 0, errno)
   295  
   296  	require.NotEqual(t, stat.Ino, stLink.Ino) // inodes are not equal
   297  	require.Equal(t, fs.ModeSymlink, stLink.Mode.Type())
   298  	// From https://linux.die.net/man/2/lstat:
   299  	// The size of a symbolic link is the length of the pathname it
   300  	// contains, without a terminating null byte.
   301  	if runtime.GOOS == "windows" { // size is zero, not the path length
   302  		require.Zero(t, stLink.Size)
   303  	} else {
   304  		require.Equal(t, int64(len(path)), stLink.Size)
   305  	}
   306  }
   307  
   308  func testStat(t *testing.T, testFS experimentalsys.FS) {
   309  	_, errno := testFS.Stat("cat")
   310  	require.EqualErrno(t, experimentalsys.ENOENT, errno)
   311  	_, errno = testFS.Stat("sub/cat")
   312  	require.EqualErrno(t, experimentalsys.ENOENT, errno)
   313  
   314  	st, errno := testFS.Stat("sub/test.txt")
   315  	require.EqualErrno(t, 0, errno)
   316  
   317  	require.False(t, st.Mode.IsDir())
   318  	require.NotEqual(t, uint64(0), st.Dev)
   319  	require.NotEqual(t, uint64(0), st.Ino)
   320  
   321  	st, errno = testFS.Stat("sub")
   322  	require.EqualErrno(t, 0, errno)
   323  
   324  	require.True(t, st.Mode.IsDir())
   325  	// windows before go 1.20 has trouble reading the inode information on directories.
   326  	if runtime.GOOS != "windows" || platform.IsAtLeastGo120 {
   327  		require.NotEqual(t, uint64(0), st.Dev)
   328  		require.NotEqual(t, uint64(0), st.Ino)
   329  	}
   330  }
   331  
   332  // requireReaddir ensures the input file is a directory, and returns its
   333  // entries.
   334  func requireReaddir(t *testing.T, f experimentalsys.File, n int, expectDirIno bool) []experimentalsys.Dirent {
   335  	entries, errno := f.Readdir(n)
   336  	require.EqualErrno(t, 0, errno)
   337  
   338  	sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })
   339  	requireIno(t, entries, expectDirIno)
   340  	return entries
   341  }
   342  
   343  func testReadlink(t *testing.T, readFS, writeFS experimentalsys.FS) {
   344  	testLinks := []struct {
   345  		old, dst string
   346  	}{
   347  		// Same dir.
   348  		{old: "animals.txt", dst: "symlinked-animals.txt"},
   349  		{old: "sub/test.txt", dst: "sub/symlinked-test.txt"},
   350  		// Parent to sub.
   351  		{old: "animals.txt", dst: "sub/symlinked-animals.txt"},
   352  		// Sub to parent.
   353  		{old: "sub/test.txt", dst: "symlinked-zoo.txt"},
   354  	}
   355  
   356  	for _, tl := range testLinks {
   357  		errno := writeFS.Symlink(tl.old, tl.dst) // not os.Symlink for windows compat
   358  		require.Zero(t, errno, "%v", tl)
   359  
   360  		dst, errno := readFS.Readlink(tl.dst)
   361  		require.EqualErrno(t, 0, errno)
   362  		require.Equal(t, tl.old, dst)
   363  	}
   364  
   365  	t.Run("errors", func(t *testing.T) {
   366  		_, err := readFS.Readlink("sub/test.txt")
   367  		require.Error(t, err)
   368  		_, err = readFS.Readlink("")
   369  		require.Error(t, err)
   370  		_, err = readFS.Readlink("animals.txt")
   371  		require.Error(t, err)
   372  	})
   373  }
   374  
   375  func requireIno(t *testing.T, dirents []experimentalsys.Dirent, expectDirIno bool) {
   376  	for i := range dirents {
   377  		d := dirents[i]
   378  		if expectDirIno {
   379  			require.NotEqual(t, uint64(0), d.Ino, "%+v", d)
   380  			d.Ino = 0
   381  		} else {
   382  			require.Zero(t, d.Ino, "%+v", d)
   383  		}
   384  	}
   385  }
   386  
   387  // joinPath avoids us having to rename fields just to avoid conflict with the
   388  // path package.
   389  func joinPath(dirName, baseName string) string {
   390  	return path.Join(dirName, baseName)
   391  }