392 lines
11 KiB
Go
392 lines
11 KiB
Go
package sysfs
|
|
|
|
import (
|
|
_ "embed"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"runtime"
|
|
"sort"
|
|
"testing"
|
|
|
|
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
|
|
"github.com/tetratelabs/wazero/internal/platform"
|
|
"github.com/tetratelabs/wazero/internal/testing/require"
|
|
"github.com/tetratelabs/wazero/sys"
|
|
)
|
|
|
|
func testOpen_O_RDWR(t *testing.T, tmpDir string, testFS experimentalsys.FS) {
|
|
file := "file"
|
|
realPath := path.Join(tmpDir, file)
|
|
err := os.WriteFile(realPath, []byte{}, 0o600)
|
|
require.NoError(t, err)
|
|
|
|
f, errno := testFS.OpenFile(file, experimentalsys.O_RDWR, 0)
|
|
require.EqualErrno(t, 0, errno)
|
|
defer f.Close()
|
|
|
|
// If the write flag was honored, we should be able to write!
|
|
fileContents := []byte{1, 2, 3, 4}
|
|
n, errno := f.Write(fileContents)
|
|
require.EqualErrno(t, 0, errno)
|
|
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.EqualErrno(t, 0, 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, experimentalsys.O_RDONLY|experimentalsys.O_CREAT, 0o444)
|
|
require.EqualErrno(t, 0, errno)
|
|
defer f.Close()
|
|
|
|
if runtime.GOOS != "windows" {
|
|
// If the read-only flag was honored, we should not be able to write!
|
|
_, err = f.Write(fileContents)
|
|
require.EqualErrno(t, experimentalsys.EBADF, experimentalsys.UnwrapOSError(err))
|
|
}
|
|
|
|
// Verify stat on the file
|
|
stat, errno := f.Stat()
|
|
require.EqualErrno(t, 0, errno)
|
|
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`, experimentalsys.O_WRONLY|experimentalsys.O_CREAT|experimentalsys.O_TRUNC, 0o600)
|
|
require.EqualErrno(t, 0, errno)
|
|
defer f.Close()
|
|
|
|
_, errno = f.Stat()
|
|
require.EqualErrno(t, 0, errno)
|
|
})
|
|
}
|
|
|
|
t.Run("O_TRUNC", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
testFS := DirFS(tmpDir)
|
|
|
|
name := "truncate"
|
|
realPath := path.Join(tmpDir, name)
|
|
require.NoError(t, os.WriteFile(realPath, []byte("123456"), 0o0666))
|
|
|
|
f, errno = testFS.OpenFile(name, experimentalsys.O_RDWR|experimentalsys.O_TRUNC, 0o444)
|
|
require.EqualErrno(t, 0, errno)
|
|
require.EqualErrno(t, 0, f.Close())
|
|
|
|
actual, err := os.ReadFile(realPath)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0, len(actual))
|
|
})
|
|
}
|
|
|
|
func testOpen_Read(t *testing.T, testFS experimentalsys.FS, requireFileIno, expectDirIno bool) {
|
|
t.Helper()
|
|
|
|
t.Run("doesn't exist", func(t *testing.T) {
|
|
_, errno := testFS.OpenFile("nope", experimentalsys.O_RDONLY, 0)
|
|
|
|
// We currently follow os.Open not syscall.Open, so the error is wrapped.
|
|
require.EqualErrno(t, experimentalsys.ENOENT, errno)
|
|
})
|
|
|
|
t.Run("readdir . opens root", func(t *testing.T) {
|
|
f, errno := testFS.OpenFile(".", experimentalsys.O_RDONLY, 0)
|
|
require.EqualErrno(t, 0, errno)
|
|
defer f.Close()
|
|
|
|
dirents := requireReaddir(t, f, -1, expectDirIno)
|
|
|
|
// Scrub inodes so we can compare expectations without them.
|
|
for i := range dirents {
|
|
dirents[i].Ino = 0
|
|
}
|
|
|
|
require.Equal(t, []experimentalsys.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("readdir empty", func(t *testing.T) {
|
|
f, errno := testFS.OpenFile("emptydir", experimentalsys.O_RDONLY, 0)
|
|
require.EqualErrno(t, 0, errno)
|
|
defer f.Close()
|
|
|
|
entries := requireReaddir(t, f, -1, expectDirIno)
|
|
require.Zero(t, len(entries))
|
|
})
|
|
|
|
t.Run("readdir partial", func(t *testing.T) {
|
|
dirF, errno := testFS.OpenFile("dir", experimentalsys.O_RDONLY, 0)
|
|
require.EqualErrno(t, 0, errno)
|
|
defer dirF.Close()
|
|
|
|
dirents1, errno := dirF.Readdir(1)
|
|
require.EqualErrno(t, 0, errno)
|
|
require.Equal(t, 1, len(dirents1))
|
|
|
|
dirents2, errno := dirF.Readdir(1)
|
|
require.EqualErrno(t, 0, errno)
|
|
require.Equal(t, 1, len(dirents2))
|
|
|
|
// read exactly the last entry
|
|
dirents3, errno := dirF.Readdir(1)
|
|
require.EqualErrno(t, 0, errno)
|
|
require.Equal(t, 1, len(dirents3))
|
|
|
|
dirents := []experimentalsys.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, expectDirIno)
|
|
|
|
// Scrub inodes so we can compare expectations without them.
|
|
for i := range dirents {
|
|
dirents[i].Ino = 0
|
|
}
|
|
|
|
require.Equal(t, []experimentalsys.Dirent{
|
|
{Name: "-", Type: 0},
|
|
{Name: "a-", Type: fs.ModeDir},
|
|
{Name: "ab-", Type: 0},
|
|
}, dirents)
|
|
|
|
// no error reading an exhausted directory
|
|
_, errno = dirF.Readdir(1)
|
|
require.EqualErrno(t, 0, errno)
|
|
})
|
|
|
|
t.Run("file exists", func(t *testing.T) {
|
|
f, errno := testFS.OpenFile("animals.txt", experimentalsys.O_RDONLY, 0)
|
|
require.EqualErrno(t, 0, errno)
|
|
defer f.Close()
|
|
|
|
fileContents := []byte(`bear
|
|
cat
|
|
shark
|
|
dinosaur
|
|
human
|
|
`)
|
|
// Ensure it implements Pread
|
|
lenToRead := len(fileContents) - 1
|
|
buf := make([]byte, lenToRead)
|
|
n, errno := f.Pread(buf, 1)
|
|
require.EqualErrno(t, 0, errno)
|
|
require.Equal(t, lenToRead, n)
|
|
require.Equal(t, fileContents[1:], buf)
|
|
|
|
// Ensure it implements Seek
|
|
offset, errno := f.Seek(1, io.SeekStart)
|
|
require.EqualErrno(t, 0, errno)
|
|
require.Equal(t, int64(1), offset)
|
|
|
|
// Read should pick up from position 1
|
|
n, errno = f.Read(buf)
|
|
require.EqualErrno(t, 0, errno)
|
|
require.Equal(t, lenToRead, n)
|
|
require.Equal(t, fileContents[1:], buf)
|
|
})
|
|
|
|
t.Run("file stat includes inode", func(t *testing.T) {
|
|
f, errno := testFS.OpenFile("empty.txt", experimentalsys.O_RDONLY, 0)
|
|
require.EqualErrno(t, 0, errno)
|
|
defer f.Close()
|
|
|
|
st, errno := f.Stat()
|
|
require.EqualErrno(t, 0, errno)
|
|
|
|
// Results are inconsistent, so don't validate the opposite.
|
|
if requireFileIno {
|
|
require.NotEqual(t, uint64(0), st.Ino, "%+v", st)
|
|
}
|
|
})
|
|
|
|
// 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 = experimentalsys.Oflag(0x40000)
|
|
|
|
f, errno := testFS.OpenFile("animals.txt", experimentalsys.O_RDONLY|O_NOATIME, 0)
|
|
require.EqualErrno(t, 0, errno)
|
|
defer f.Close()
|
|
})
|
|
|
|
t.Run("writing to a read-only file is EBADF", func(t *testing.T) {
|
|
f, errno := testFS.OpenFile("animals.txt", experimentalsys.O_RDONLY, 0)
|
|
require.EqualErrno(t, 0, errno)
|
|
defer f.Close()
|
|
|
|
_, errno = f.Write([]byte{1, 2, 3, 4})
|
|
require.EqualErrno(t, experimentalsys.EBADF, errno)
|
|
})
|
|
|
|
t.Run("opening a directory with O_RDWR is EISDIR", func(t *testing.T) {
|
|
_, errno := testFS.OpenFile("sub", experimentalsys.O_DIRECTORY|experimentalsys.O_RDWR, 0)
|
|
require.EqualErrno(t, experimentalsys.EISDIR, errno)
|
|
})
|
|
}
|
|
|
|
func testLstat(t *testing.T, testFS experimentalsys.FS) {
|
|
_, errno := testFS.Lstat("cat")
|
|
require.EqualErrno(t, experimentalsys.ENOENT, errno)
|
|
_, errno = testFS.Lstat("sub/cat")
|
|
require.EqualErrno(t, experimentalsys.ENOENT, errno)
|
|
|
|
var st sys.Stat_t
|
|
|
|
t.Run("dir", func(t *testing.T) {
|
|
st, errno = testFS.Lstat(".")
|
|
require.EqualErrno(t, 0, errno)
|
|
require.True(t, st.Mode.IsDir())
|
|
require.NotEqual(t, uint64(0), st.Ino)
|
|
})
|
|
|
|
var stFile sys.Stat_t
|
|
|
|
t.Run("file", func(t *testing.T) {
|
|
stFile, errno = testFS.Lstat("animals.txt")
|
|
require.EqualErrno(t, 0, 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 sys.Stat_t
|
|
t.Run("subdir", func(t *testing.T) {
|
|
stSubdir, errno = testFS.Lstat("sub")
|
|
require.EqualErrno(t, 0, 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.EqualErrno(t, 0, errno)
|
|
|
|
requireLinkStat(t, testFS, pathLink, stLink)
|
|
})
|
|
}
|
|
|
|
func requireLinkStat(t *testing.T, testFS experimentalsys.FS, path string, stat sys.Stat_t) {
|
|
link := path + "-link"
|
|
stLink, errno := testFS.Lstat(link)
|
|
require.EqualErrno(t, 0, 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 experimentalsys.FS) {
|
|
_, errno := testFS.Stat("cat")
|
|
require.EqualErrno(t, experimentalsys.ENOENT, errno)
|
|
_, errno = testFS.Stat("sub/cat")
|
|
require.EqualErrno(t, experimentalsys.ENOENT, errno)
|
|
|
|
st, errno := testFS.Stat("sub/test.txt")
|
|
require.EqualErrno(t, 0, 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.EqualErrno(t, 0, 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.IsAtLeastGo120 {
|
|
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 experimentalsys.File, n int, expectDirIno bool) []experimentalsys.Dirent {
|
|
entries, errno := f.Readdir(n)
|
|
require.EqualErrno(t, 0, errno)
|
|
|
|
sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })
|
|
requireIno(t, entries, expectDirIno)
|
|
return entries
|
|
}
|
|
|
|
func testReadlink(t *testing.T, readFS, writeFS experimentalsys.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.EqualErrno(t, 0, 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)
|
|
})
|
|
}
|
|
|
|
func requireIno(t *testing.T, dirents []experimentalsys.Dirent, expectDirIno bool) {
|
|
for i := range dirents {
|
|
d := dirents[i]
|
|
if expectDirIno {
|
|
require.NotEqual(t, uint64(0), d.Ino, "%+v", d)
|
|
d.Ino = 0
|
|
} else {
|
|
require.Zero(t, d.Ino, "%+v", d)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|