package sysfs import ( "bytes" "embed" _ "embed" "io" "io/fs" "os" "path" "runtime" "sort" "syscall" "testing" gofstest "testing/fstest" "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/testing/require" ) func testOpen_O_RDWR(t *testing.T, tmpDir string, testFS FS) { file := "file" realPath := path.Join(tmpDir, file) err := os.WriteFile(realPath, []byte{}, 0o600) require.NoError(t, err) f, errno := testFS.OpenFile(file, os.O_RDWR, 0) require.Zero(t, errno) defer f.Close() w, ok := f.(io.Writer) require.True(t, ok) // If the write flag was honored, we should be able to write! fileContents := []byte{1, 2, 3, 4} n, err := w.Write(fileContents) require.NoError(t, err) require.Equal(t, len(fileContents), n) // Verify the contents actually wrote. b, err := os.ReadFile(realPath) require.NoError(t, err) require.Equal(t, fileContents, b) require.NoError(t, f.Close()) // re-create as read-only, using 0444 to allow read-back on windows. require.NoError(t, os.Remove(realPath)) f, errno = testFS.OpenFile(file, os.O_RDONLY|os.O_CREATE, 0o444) require.Zero(t, errno) defer f.Close() w, ok = f.(io.Writer) require.True(t, ok) if runtime.GOOS != "windows" { // If the read-only flag was honored, we should not be able to write! _, err = w.Write(fileContents) require.EqualErrno(t, syscall.EBADF, platform.UnwrapOSError(err)) } // Verify stat on the file stat, err := f.Stat() require.NoError(t, err) require.Equal(t, fs.FileMode(0o444), stat.Mode().Perm()) // from os.TestDirFSPathsValid if runtime.GOOS != "windows" { t.Run("strange name", func(t *testing.T) { f, errno = testFS.OpenFile(`e:xperi\ment.txt`, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) require.Zero(t, errno) defer f.Close() _, errno = platform.StatFile(f) require.Zero(t, errno) }) } } func testOpen_Read(t *testing.T, testFS FS, expectIno bool) { t.Run("doesn't exist", func(t *testing.T) { _, errno := testFS.OpenFile("nope", os.O_RDONLY, 0) // We currently follow os.Open not syscall.Open, so the error is wrapped. require.EqualErrno(t, syscall.ENOENT, errno) }) t.Run("readdir . opens root", func(t *testing.T) { f, errno := testFS.OpenFile(".", os.O_RDONLY, 0) require.Zero(t, errno) defer f.Close() dirents := requireReaddir(t, f, -1, expectIno) require.Equal(t, []*platform.Dirent{ {Name: "animals.txt", Type: 0}, {Name: "dir", Type: fs.ModeDir}, {Name: "empty.txt", Type: 0}, {Name: "emptydir", Type: fs.ModeDir}, {Name: "sub", Type: fs.ModeDir}, }, dirents) }) t.Run("readdirnames . opens root", func(t *testing.T) { f, errno := testFS.OpenFile(".", os.O_RDONLY, 0) require.Zero(t, errno) defer f.Close() names := requireReaddirnames(t, f, -1) require.Equal(t, []string{"animals.txt", "dir", "empty.txt", "emptydir", "sub"}, names) }) t.Run("readdir empty", func(t *testing.T) { f, errno := testFS.OpenFile("emptydir", os.O_RDONLY, 0) require.Zero(t, errno) defer f.Close() entries := requireReaddir(t, f, -1, expectIno) require.Zero(t, len(entries)) }) t.Run("readdirnames empty", func(t *testing.T) { f, errno := testFS.OpenFile("emptydir", os.O_RDONLY, 0) require.Zero(t, errno) defer f.Close() names := requireReaddirnames(t, f, -1) require.Zero(t, len(names)) }) t.Run("readdir partial", func(t *testing.T) { dirF, errno := testFS.OpenFile("dir", os.O_RDONLY, 0) require.Zero(t, errno) defer dirF.Close() dirents1, errno := platform.Readdir(dirF, 1) require.Zero(t, errno) require.Equal(t, 1, len(dirents1)) dirents2, errno := platform.Readdir(dirF, 1) require.Zero(t, errno) require.Equal(t, 1, len(dirents2)) // read exactly the last entry dirents3, errno := platform.Readdir(dirF, 1) require.Zero(t, errno) require.Equal(t, 1, len(dirents3)) dirents := []*platform.Dirent{dirents1[0], dirents2[0], dirents3[0]} sort.Slice(dirents, func(i, j int) bool { return dirents[i].Name < dirents[j].Name }) requireIno(t, dirents, expectIno) require.Equal(t, []*platform.Dirent{ {Name: "-", Type: 0}, {Name: "a-", Type: fs.ModeDir}, {Name: "ab-", Type: 0}, }, dirents) // no error reading an exhausted directory _, errno = platform.Readdir(dirF, 1) require.Zero(t, errno) }) // TODO: consolidate duplicated tests from platform once we have our own // file type t.Run("readdirnames partial", func(t *testing.T) { dirF, errno := testFS.OpenFile("dir", os.O_RDONLY, 0) require.Zero(t, errno) defer dirF.Close() names1, errno := platform.Readdirnames(dirF, 1) require.Zero(t, errno) require.Equal(t, 1, len(names1)) names2, errno := platform.Readdirnames(dirF, 1) require.Zero(t, errno) require.Equal(t, 1, len(names2)) // read exactly the last entry names3, errno := platform.Readdirnames(dirF, 1) require.Zero(t, errno) require.Equal(t, 1, len(names3)) names := []string{names1[0], names2[0], names3[0]} sort.Strings(names) require.Equal(t, []string{"-", "a-", "ab-"}, names) // no error reading an exhausted directory _, errno = platform.Readdirnames(dirF, 1) require.Zero(t, errno) }) t.Run("file exists", func(t *testing.T) { f, errno := testFS.OpenFile("animals.txt", os.O_RDONLY, 0) require.Zero(t, errno) defer f.Close() fileContents := []byte(`bear cat shark dinosaur human `) // Ensure it implements io.ReaderAt r, ok := f.(io.ReaderAt) require.True(t, ok) lenToRead := len(fileContents) - 1 buf := make([]byte, lenToRead) n, err := r.ReadAt(buf, 1) require.NoError(t, err) require.Equal(t, lenToRead, n) require.Equal(t, fileContents[1:], buf) // Ensure it implements io.Seeker s, ok := f.(io.Seeker) require.True(t, ok) offset, err := s.Seek(1, io.SeekStart) require.NoError(t, err) require.Equal(t, int64(1), offset) b, err := io.ReadAll(f) require.NoError(t, err) require.Equal(t, fileContents[1:], b) }) // Make sure O_RDONLY isn't treated bitwise as it is usually zero. t.Run("or'd flag", func(t *testing.T) { // Example of a flag that can be or'd into O_RDONLY even if not // currently supported in WASI or GOOS=js const O_NOATIME = 0x40000 f, errno := testFS.OpenFile("animals.txt", os.O_RDONLY|O_NOATIME, 0) require.Zero(t, errno) defer f.Close() }) t.Run("writing to a read-only file is EBADF", func(t *testing.T) { f, errno := testFS.OpenFile("animals.txt", os.O_RDONLY, 0) defer require.NoError(t, f.Close()) require.Zero(t, errno) if w, ok := f.(io.Writer); ok { _, err := w.Write([]byte{1, 2, 3, 4}) require.EqualErrno(t, syscall.EBADF, platform.UnwrapOSError(err)) } else { t.Skip("not an io.Writer") } }) t.Run("writing to a directory is EBADF", func(t *testing.T) { f, errno := testFS.OpenFile("sub", os.O_RDONLY, 0) defer require.NoError(t, f.Close()) require.Zero(t, errno) if w, ok := f.(io.Writer); ok { _, err := w.Write([]byte{1, 2, 3, 4}) require.EqualErrno(t, syscall.EBADF, platform.UnwrapOSError(err)) } else { t.Skip("not an io.Writer") } }) } func testLstat(t *testing.T, testFS FS) { _, errno := testFS.Lstat("cat") require.EqualErrno(t, syscall.ENOENT, errno) _, errno = testFS.Lstat("sub/cat") require.EqualErrno(t, syscall.ENOENT, errno) var st platform.Stat_t t.Run("dir", func(t *testing.T) { st, errno = testFS.Lstat(".") require.Zero(t, errno) require.True(t, st.Mode.IsDir()) require.NotEqual(t, uint64(0), st.Ino) }) var stFile platform.Stat_t t.Run("file", func(t *testing.T) { stFile, errno = testFS.Lstat("animals.txt") require.Zero(t, errno) require.Zero(t, stFile.Mode.Type()) require.Equal(t, int64(30), stFile.Size) require.NotEqual(t, uint64(0), st.Ino) }) t.Run("link to file", func(t *testing.T) { requireLinkStat(t, testFS, "animals.txt", stFile) }) var stSubdir platform.Stat_t t.Run("subdir", func(t *testing.T) { stSubdir, errno = testFS.Lstat("sub") require.Zero(t, errno) require.True(t, stSubdir.Mode.IsDir()) require.NotEqual(t, uint64(0), st.Ino) }) t.Run("link to dir", func(t *testing.T) { requireLinkStat(t, testFS, "sub", stSubdir) }) t.Run("link to dir link", func(t *testing.T) { pathLink := "sub-link" stLink, errno := testFS.Lstat(pathLink) require.Zero(t, errno) requireLinkStat(t, testFS, pathLink, stLink) }) } func requireLinkStat(t *testing.T, testFS FS, path string, stat platform.Stat_t) { link := path + "-link" stLink, errno := testFS.Lstat(link) require.Zero(t, errno) require.NotEqual(t, stat.Ino, stLink.Ino) // inodes are not equal require.Equal(t, fs.ModeSymlink, stLink.Mode.Type()) // From https://linux.die.net/man/2/lstat: // The size of a symbolic link is the length of the pathname it // contains, without a terminating null byte. if runtime.GOOS == "windows" { // size is zero, not the path length require.Zero(t, stLink.Size) } else { require.Equal(t, int64(len(path)), stLink.Size) } } func testStat(t *testing.T, testFS FS) { _, errno := testFS.Stat("cat") require.EqualErrno(t, syscall.ENOENT, errno) _, errno = testFS.Stat("sub/cat") require.EqualErrno(t, syscall.ENOENT, errno) st, errno := testFS.Stat("sub/test.txt") require.Zero(t, errno) require.False(t, st.Mode.IsDir()) require.NotEqual(t, uint64(0), st.Dev) require.NotEqual(t, uint64(0), st.Ino) st, errno = testFS.Stat("sub") require.Zero(t, errno) require.True(t, st.Mode.IsDir()) // windows before go 1.20 has trouble reading the inode information on directories. if runtime.GOOS != "windows" || platform.IsGo120 { require.NotEqual(t, uint64(0), st.Dev) require.NotEqual(t, uint64(0), st.Ino) } } // requireReaddir ensures the input file is a directory, and returns its // entries. func requireReaddir(t *testing.T, f fs.File, n int, expectIno bool) []*platform.Dirent { entries, errno := platform.Readdir(f, n) require.Zero(t, errno) sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name }) if _, ok := f.(*openRootDir); ok { // TODO: get inodes to work on the root directory of a composite FS requireIno(t, entries, false) } else { requireIno(t, entries, expectIno) } return entries } // requireReaddirnames ensures the input file is a directory, and returns its // entries. func requireReaddirnames(t *testing.T, f fs.File, n int) []string { names, errno := platform.Readdirnames(f, n) require.Zero(t, errno) sort.Strings(names) return names } func testReadlink(t *testing.T, readFS, writeFS FS) { testLinks := []struct { old, dst string }{ // Same dir. {old: "animals.txt", dst: "symlinked-animals.txt"}, {old: "sub/test.txt", dst: "sub/symlinked-test.txt"}, // Parent to sub. {old: "animals.txt", dst: "sub/symlinked-animals.txt"}, // Sub to parent. {old: "sub/test.txt", dst: "symlinked-zoo.txt"}, } for _, tl := range testLinks { errno := writeFS.Symlink(tl.old, tl.dst) // not os.Symlink for windows compat require.Zero(t, errno, "%v", tl) dst, errno := readFS.Readlink(tl.dst) require.Zero(t, errno) require.Equal(t, tl.old, dst) } t.Run("errors", func(t *testing.T) { _, err := readFS.Readlink("sub/test.txt") require.Error(t, err) _, err = readFS.Readlink("") require.Error(t, err) _, err = readFS.Readlink("animals.txt") require.Error(t, err) }) } var ( //go:embed testdata testdata embed.FS readerAtFile = "wazero.txt" emptyFile = "empty.txt" ) func TestReaderAtOffset(t *testing.T) { embedFS, err := fs.Sub(testdata, "testdata") require.NoError(t, err) d, err := embedFS.Open(readerAtFile) require.NoError(t, err) defer d.Close() bytes, err := io.ReadAll(d) require.NoError(t, err) mapFS := gofstest.MapFS{readerAtFile: &gofstest.MapFile{Data: bytes}} // Write a file as can't open "testdata" in scratch tests because they // can't read the original filesystem. tmpDir := t.TempDir() require.NoError(t, os.WriteFile(path.Join(tmpDir, readerAtFile), bytes, 0o600)) dirFS := os.DirFS(tmpDir) tests := []struct { name string fs fs.FS }{ {name: "os.DirFS", fs: dirFS}, {name: "embed.FS", fs: embedFS}, {name: "fstest.MapFS", fs: mapFS}, } buf := make([]byte, 3) for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { f, err := tc.fs.Open(readerAtFile) require.NoError(t, err) defer f.Close() var r io.Reader = f ra := ReaderAtOffset(f, 0) requireRead3 := func(r io.Reader, buf []byte) { n, err := r.Read(buf) require.NoError(t, err) require.Equal(t, 3, n) } // The file should work as a reader (base case) requireRead3(r, buf) require.Equal(t, "waz", string(buf)) buf = buf[:] // The readerAt impl should be able to start from zero also requireRead3(ra, buf) require.Equal(t, "waz", string(buf)) buf = buf[:] // If the offset didn't change, we expect the next three chars. requireRead3(r, buf) require.Equal(t, "ero", string(buf)) buf = buf[:] // If state was held between reader-at, we expect the same requireRead3(ra, buf) require.Equal(t, "ero", string(buf)) buf = buf[:] // We should also be able to make another reader-at ra = ReaderAtOffset(f, 3) requireRead3(ra, buf) require.Equal(t, "ero", string(buf)) }) } } func TestReaderAtOffset_empty(t *testing.T) { embedFS, err := fs.Sub(testdata, "testdata") require.NoError(t, err) d, err := embedFS.Open(readerAtFile) require.NoError(t, err) defer d.Close() mapFS := gofstest.MapFS{emptyFile: &gofstest.MapFile{}} // Write a file as can't open "testdata" in scratch tests because they // can't read the original filesystem. tmpDir := t.TempDir() require.NoError(t, os.WriteFile(path.Join(tmpDir, emptyFile), []byte{}, 0o600)) dirFS := os.DirFS(tmpDir) tests := []struct { name string fs fs.FS }{ {name: "os.DirFS", fs: dirFS}, {name: "embed.FS", fs: embedFS}, {name: "fstest.MapFS", fs: mapFS}, } buf := make([]byte, 3) for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { f, err := tc.fs.Open(emptyFile) require.NoError(t, err) defer f.Close() var r io.Reader = f ra := ReaderAtOffset(f, 0) requireRead3 := func(r io.Reader, buf []byte) { n, err := r.Read(buf) require.Equal(t, err, io.EOF) require.Equal(t, 0, n) // file is empty } // The file should work as a reader (base case) requireRead3(r, buf) // The readerAt impl should be able to start from zero also requireRead3(ra, buf) }) } } func TestReaderAtOffset_Unsupported(t *testing.T) { embedFS, err := fs.Sub(testdata, "testdata") require.NoError(t, err) f, err := embedFS.Open(emptyFile) require.NoError(t, err) defer f.Close() // mask both io.ReaderAt and io.Seeker ra := ReaderAtOffset(struct{ fs.File }{f}, 0) buf := make([]byte, 3) _, err = ra.Read(buf) require.Equal(t, syscall.ENOSYS, err) } func TestWriterAtOffset(t *testing.T) { tmpDir := t.TempDir() dirFS := NewDirFS(tmpDir) // fs.FS doesn't support writes, and there is no other built-in // implementation except os.File. tests := []struct { name string fs FS }{ {name: "sysfs.dirFS", fs: dirFS}, } for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { f, errno := tc.fs.OpenFile(readerAtFile, os.O_RDWR|os.O_CREATE, 0o600) require.Zero(t, errno) defer f.Close() w := f.(io.Writer) wa := WriterAtOffset(f, 6) text := "wazero" buf := make([]byte, 3) copy(buf, text[:3]) requireWrite3 := func(r io.Writer, buf []byte) { n, err := r.Write(buf) require.NoError(t, err) require.Equal(t, 3, n) } // The file should work as a writer (base case) requireWrite3(w, buf) // The writerAt impl should be able to start from zero also requireWrite3(wa, buf) copy(buf, text[3:]) // If the offset didn't change, the next chars will write after the // first requireWrite3(w, buf) // If state was held between writer-at, we expect the same requireWrite3(wa, buf) // We should also be able to make another writer-at wa = WriterAtOffset(f, 12) requireWrite3(wa, buf) r := ReaderAtOffset(f, 0) b, err := io.ReadAll(r) require.NoError(t, err) // We expect to have written the text two and a half times: // 1. io.Write: offset 0 // 2. io.WriterAt: offset 6 // 3. second io.WriterAt: offset 12, writing "ero" require.Equal(t, text+text+text[3:], string(b)) }) } } func TestWriterAtOffset_empty(t *testing.T) { tmpDir := t.TempDir() dirFS := NewDirFS(tmpDir) // fs.FS doesn't support writes, and there is no other built-in // implementation except os.File. tests := []struct { name string fs FS }{ {name: "sysfs.dirFS", fs: dirFS}, } for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { f, errno := tc.fs.OpenFile(emptyFile, os.O_RDWR|os.O_CREATE, 0o600) require.Zero(t, errno) defer f.Close() r := f.(io.Writer) ra := WriterAtOffset(f, 0) var emptyBuf []byte requireWrite := func(r io.Writer) { n, err := r.Write(emptyBuf) require.NoError(t, err) require.Equal(t, 0, n) // file is empty } // The file should work as a writer (base case) requireWrite(r) // The writerAt impl should be able to start from zero also requireWrite(ra) }) } } func TestWriterAtOffset_Unsupported(t *testing.T) { tmpDir := t.TempDir() dirFS := NewDirFS(tmpDir) f, errno := dirFS.OpenFile(readerAtFile, os.O_RDWR|os.O_CREATE, 0o600) require.Zero(t, errno) defer f.Close() // mask both io.WriterAt and io.Seeker ra := WriterAtOffset(struct{ fs.File }{f}, 0) buf := make([]byte, 3) _, err := ra.Write(buf) require.Equal(t, syscall.ENOSYS, err) } // Test_FileSync doesn't guarantee sync works because the operating system may // sync anyway. There is no test in Go for os.File Sync, but closest is similar // to below. Effectively, this only tests that things don't error. func Test_FileSync(t *testing.T) { testSync(t, func(f fs.File) syscall.Errno { return platform.UnwrapOSError(f.(interface{ Sync() error }).Sync()) }) } // Test_FileDatasync has same issues as Test_Sync. func Test_FileDatasync(t *testing.T) { testSync(t, FileDatasync) } func testSync(t *testing.T, sync func(fs.File) syscall.Errno) { f, err := os.CreateTemp("", t.Name()) require.NoError(t, err) defer f.Close() expected := "hello world!" // Write the expected data _, err = f.Write([]byte(expected)) require.NoError(t, err) // Sync the data. require.Zero(t, sync(f)) // Rewind while the file is still open. _, err = f.Seek(0, io.SeekStart) require.NoError(t, err) // Read data from the file var buf bytes.Buffer _, err = io.Copy(&buf, f) require.NoError(t, err) // It may be the case that sync worked. require.Equal(t, expected, buf.String()) } func requireIno(t *testing.T, dirents []*platform.Dirent, expectIno bool) { for _, e := range dirents { if expectIno { require.NotEqual(t, uint64(0), e.Ino, "%+v", e) e.Ino = 0 } else { require.Zero(t, e.Ino, "%+v", e) } } } // joinPath avoids us having to rename fields just to avoid conflict with the // path package. func joinPath(dirName, baseName string) string { return path.Join(dirName, baseName) }