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