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