Files
wazero/internal/syscallfs/syscallfs_test.go
Crypt Keeper 713e187796 fs: extracts syscallfs.ReaderAtOffset for WASI and gojs (#1037)
This extracts a utility `syscallfs.ReaderAtOffset()` to allow WASI and
gojs to re-use the same logic to implement `syscall.Pread`.

What's different than before is that if WASI passes multiple iovecs an
emulated `ReaderAt` will seek to the read position on each call to
`Read` vs once per loop. This was a design decision to keep the call
sites compatible between files that implement ReaderAt and those that
emulate them with Seeker (e.g. avoid the need for a read-scoped closer/
defer function). The main use case for emulation is `embed.file`, whose
seek function is cheap, so there's little performance impact to this.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
2023-01-15 17:30:45 -08:00

414 lines
10 KiB
Go

package syscallfs
import (
"embed"
_ "embed"
"errors"
"io"
"io/fs"
"os"
"path"
"runtime"
"sort"
"strings"
"syscall"
"testing"
"testing/fstest"
"time"
"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, err := testFS.OpenFile(file, os.O_RDWR, 0)
require.NoError(t, err)
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)
}
func testOpen_Read(t *testing.T, tmpDir string, testFS FS) {
file := "file"
fileContents := []byte{1, 2, 3, 4}
err := os.WriteFile(path.Join(tmpDir, file), fileContents, 0o700)
require.NoError(t, err)
dir := "dir"
dirRealPath := path.Join(tmpDir, dir)
err = os.Mkdir(dirRealPath, 0o700)
require.NoError(t, err)
file1 := "file1"
fileInDir := path.Join(dirRealPath, file1)
require.NoError(t, os.WriteFile(fileInDir, []byte{2}, 0o600))
t.Run("doesn't exist", func(t *testing.T) {
_, err := testFS.OpenFile("nope", os.O_RDONLY, 0)
// We currently follow os.Open not syscall.Open, so the error is wrapped.
requireErrno(t, syscall.ENOENT, err)
})
t.Run(". opens root", func(t *testing.T) {
f, err := testFS.OpenFile(".", os.O_RDONLY, 0)
require.NoError(t, err)
defer f.Close()
entries := requireReadDir(t, f)
require.Equal(t, 2, len(entries))
require.True(t, entries[0].IsDir())
require.Equal(t, dir, entries[0].Name())
require.False(t, entries[1].IsDir())
require.Equal(t, file, entries[1].Name())
})
t.Run("dir exists", func(t *testing.T) {
f, err := testFS.OpenFile(dir, os.O_RDONLY, 0)
require.NoError(t, err)
defer f.Close()
entries := requireReadDir(t, f)
require.Equal(t, 1, len(entries))
require.False(t, entries[0].IsDir())
require.Equal(t, file1, entries[0].Name())
})
t.Run("file exists", func(t *testing.T) {
f, err := testFS.OpenFile(file, os.O_RDONLY, 0)
require.NoError(t, err)
defer f.Close()
// 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)
if w, ok := f.(io.Writer); ok {
_, err := w.Write([]byte("hello"))
if runtime.GOOS == "windows" {
requireErrno(t, syscall.EPERM, err)
} else {
requireErrno(t, syscall.EBADF, err)
}
}
})
// 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, err := testFS.OpenFile(file, os.O_RDONLY|O_NOATIME, 0)
require.NoError(t, err)
defer f.Close()
})
}
// requireReadDir ensures the input file is a directory, and returns its
// entries.
func requireReadDir(t *testing.T, f fs.File) []fs.DirEntry {
if w, ok := f.(io.Writer); ok {
_, err := w.Write([]byte("hello"))
requireErrno(t, syscall.EBADF, err)
}
// Ensure it implements fs.ReadDirFile
dir, ok := f.(fs.ReadDirFile)
require.True(t, ok)
entries, err := dir.ReadDir(-1)
require.NoError(t, err)
sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() })
return entries
}
func testUtimes(t *testing.T, tmpDir string, testFS FS) {
file := "file"
err := os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
require.NoError(t, err)
dir := "dir"
err = os.Mkdir(path.Join(tmpDir, dir), 0o700)
require.NoError(t, err)
t.Run("doesn't exist", func(t *testing.T) {
err := testFS.Utimes("nope",
time.Unix(123, 4*1e3).UnixNano(),
time.Unix(567, 8*1e3).UnixNano())
require.Equal(t, syscall.ENOENT, err)
})
type test struct {
name string
path string
atimeNsec, mtimeNsec int64
}
// Note: This sets microsecond granularity because Windows doesn't support
// nanosecond.
tests := []test{
{
name: "file positive",
path: file,
atimeNsec: time.Unix(123, 4*1e3).UnixNano(),
mtimeNsec: time.Unix(567, 8*1e3).UnixNano(),
},
{
name: "dir positive",
path: dir,
atimeNsec: time.Unix(123, 4*1e3).UnixNano(),
mtimeNsec: time.Unix(567, 8*1e3).UnixNano(),
},
{name: "file zero", path: file},
{name: "dir zero", path: dir},
}
// linux and freebsd report inaccurate results when the input ts is negative.
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
tests = append(tests,
test{
name: "file negative",
path: file,
atimeNsec: time.Unix(-123, -4*1e3).UnixNano(),
mtimeNsec: time.Unix(-567, -8*1e3).UnixNano(),
},
test{
name: "dir negative",
path: dir,
atimeNsec: time.Unix(-123, -4*1e3).UnixNano(),
mtimeNsec: time.Unix(-567, -8*1e3).UnixNano(),
},
)
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
err := testFS.Utimes(tc.path, tc.atimeNsec, tc.mtimeNsec)
require.NoError(t, err)
stat, err := os.Stat(path.Join(tmpDir, tc.path))
require.NoError(t, err)
atimeNsec, mtimeNsec, _ := platform.StatTimes(stat)
if platform.CompilerSupported() {
require.Equal(t, atimeNsec, tc.atimeNsec)
} // else only mtimes will return.
require.Equal(t, mtimeNsec, tc.mtimeNsec)
})
}
}
// testFSAdapter implements fs.FS only to use fstest.TestFS
type testFSAdapter struct {
fs FS
}
// Open implements the same method as documented on fs.FS
func (f *testFSAdapter) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) { // FS.OpenFile has fewer constraints than fs.FS
return nil, os.ErrInvalid
}
// This isn't a production-grade fs.FS implementation. The only special
// cases we address here are to pass testfs.TestFS.
if runtime.GOOS == "windows" {
switch {
case strings.Contains(name, "\\"):
return nil, os.ErrInvalid
}
}
return f.fs.OpenFile(name, os.O_RDONLY, 0)
}
// requireErrno should only be used for functions that wrap the underlying
// syscall.Errno.
func requireErrno(t *testing.T, expected syscall.Errno, actual error) {
require.True(t, errors.Is(actual, expected), "expected %v, but was %v", expected, actual)
}
var (
//go:embed testdata/*.txt
readerAtFS embed.FS
readerAtFile = "wazero.txt"
emptyFile = "empty.txt"
)
func TestReaderAtOffset(t *testing.T) {
embedFS, err := fs.Sub(readerAtFS, "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 := fstest.MapFS{readerAtFile: &fstest.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(readerAtFS, "testdata")
require.NoError(t, err)
d, err := embedFS.Open(readerAtFile)
require.NoError(t, err)
defer d.Close()
mapFS := fstest.MapFS{emptyFile: &fstest.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(readerAtFS, "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)
}