Consolidates internal code to syscallfs (#1003)

This consolidates internal code to syscallfs, which removes the fs.FS
specific path rules, except when adapting one to syscallfs. For example,
this allows the underlying filesystem to decide if relative paths are
supported or not, as well any EINVAL related concerns.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2023-01-04 13:53:53 +08:00
committed by GitHub
parent 846575d0fa
commit 83e4b66659
21 changed files with 563 additions and 676 deletions

View File

@@ -165,8 +165,8 @@ func abortWithMessage(ctx context.Context, mod api.Module, stack []uint64) {
columnNumber := uint32(stack[3])
// Don't panic if there was a problem reading the message
stderr := fsc.FdWriter(internalsys.FdStderr)
if msg, msgOk := readAssemblyScriptString(mem, message); msgOk {
stderr := internalsys.WriterForFile(fsc, internalsys.FdStderr)
if msg, msgOk := readAssemblyScriptString(mem, message); msgOk && stderr != nil {
if fn, fnOk := readAssemblyScriptString(mem, fileName); fnOk {
_, _ = fmt.Fprintf(stderr, "%s at %s:%d:%d\n", msg, fn, lineNumber, columnNumber)
}
@@ -200,8 +200,7 @@ var traceStdout = &wasm.HostFunc{
IsHostFunction: true,
GoFunc: api.GoModuleFunc(func(_ context.Context, mod api.Module, stack []uint64) {
fsc := mod.(*wasm.CallContext).Sys.FS()
stdout := fsc.FdWriter(internalsys.FdStdout)
traceTo(mod, stack, stdout)
traceTo(mod, stack, internalsys.WriterForFile(fsc, internalsys.FdStdout))
}),
},
}
@@ -209,8 +208,7 @@ var traceStdout = &wasm.HostFunc{
// traceStderr implements trace to the configured Stderr.
var traceStderr = traceStdout.WithGoModuleFunc(func(_ context.Context, mod api.Module, stack []uint64) {
fsc := mod.(*wasm.CallContext).Sys.FS()
stderr := fsc.FdWriter(internalsys.FdStderr)
traceTo(mod, stack, stderr)
traceTo(mod, stack, internalsys.WriterForFile(fsc, internalsys.FdStderr))
})
// traceTo implements the function "trace" in AssemblyScript. e.g.
@@ -224,6 +222,9 @@ var traceStderr = traceStdout.WithGoModuleFunc(func(_ context.Context, mod api.M
//
// See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L61
func traceTo(mod api.Module, params []uint64, writer io.Writer) {
if writer == nil {
return // closed
}
message := uint32(params[0])
nArgs := uint32(params[1])
arg0 := api.DecodeF64(params[2])

View File

@@ -12,6 +12,7 @@ import (
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/syscallfs"
. "github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -57,8 +58,8 @@ func fdCloseFn(_ context.Context, mod api.Module, params []uint64) Errno {
fsc := mod.(*wasm.CallContext).Sys.FS()
fd := uint32(params[0])
if ok := fsc.CloseFile(fd); !ok {
return ErrnoBadf
if err := fsc.CloseFile(fd); err != nil {
return ToErrno(err)
}
return ErrnoSuccess
}
@@ -121,20 +122,19 @@ func fdFdstatGetFn(_ context.Context, mod api.Module, params []uint64) Errno {
return ErrnoFault
}
stat, err := fsc.StatFile(fd)
if err != nil {
return ToErrno(err)
}
filetype := getWasiFiletype(stat.Mode())
var fdflags uint16
// Determine if it is writeable
if w := fsc.FdWriter(fd); w != nil {
var stat fs.FileInfo
var err error
if f, ok := fsc.LookupFile(fd); !ok {
return ErrnoBadf
} else if stat, err = f.File.Stat(); err != nil {
return ToErrno(err)
} else if _, ok := f.File.(io.Writer); ok {
// TODO: maybe cache flags to open instead
fdflags = FD_APPEND
}
filetype := getWasiFiletype(stat.Mode())
writeFdstat(buf, filetype, fdflags)
return ErrnoSuccess
@@ -233,7 +233,7 @@ func fdFilestatGetFunc(mod api.Module, fd, resultBuf uint32) Errno {
return ErrnoFault
}
stat, err := fsc.StatFile(fd)
stat, err := sys.StatFile(fsc, fd)
if err != nil {
return ToErrno(err)
}
@@ -358,14 +358,16 @@ func fdPrestatGetFn(_ context.Context, mod api.Module, params []uint64) Errno {
return ErrnoBadf
}
entry, ok := fsc.OpenedFile(fd)
if !ok {
return ErrnoBadf
var name string
if f, ok := fsc.LookupFile(fd); !ok {
return ErrnoBadf // closed
} else {
name = f.Name
}
// Upper 32-bits are zero because...
// * Zero-value 8-bit tag, and 3-byte zero-value padding
prestat := uint64(len(entry.Name) << 32)
prestat := uint64(len(name) << 32)
if !mod.Memory().WriteUint64Le(resultPrestat, prestat) {
return ErrnoFault
}
@@ -417,17 +419,19 @@ func fdPrestatDirNameFn(_ context.Context, mod api.Module, params []uint64) Errn
return ErrnoBadf
}
f, ok := fsc.OpenedFile(fd)
if !ok {
return ErrnoBadf
var name string
if f, ok := fsc.LookupFile(fd); !ok {
return ErrnoBadf // closed
} else {
name = f.Name
}
// Some runtimes may have another semantics. See /RATIONALE.md
if uint32(len(f.Name)) < pathLen {
if uint32(len(name)) < pathLen {
return ErrnoNametoolong
}
if !mod.Memory().Write(path, []byte(f.Name)[:pathLen]) {
if !mod.Memory().Write(path, []byte(name)[:pathLen]) {
return ErrnoFault
}
return ErrnoSuccess
@@ -519,21 +523,21 @@ func fdReadOrPread(mod api.Module, params []uint64, isPread bool) Errno {
resultNread = uint32(params[3])
}
r := fsc.FdReader(fd)
if r == nil {
r, ok := fsc.LookupFile(fd)
if !ok {
return ErrnoBadf
}
read := r.Read
read := r.File.Read
if isPread {
if ra, ok := r.(io.ReaderAt); ok {
if ra, ok := r.File.(io.ReaderAt); ok {
// ReadAt is the Go equivalent to pread.
read = func(p []byte) (int, error) {
n, err := ra.ReadAt(p, offset)
offset += int64(n)
return n, err
}
} else if s, ok := r.(io.Seeker); ok {
} else if s, ok := r.File.(io.Seeker); ok {
// Unfortunately, it is often not supported.
// See /RATIONALE.md "fd_pread: io.Seeker fallback when io.ReaderAt is not supported"
initialOffset, err := s.Seek(0, io.SeekCurrent)
@@ -853,7 +857,7 @@ func writeDirent(buf []byte, dNext uint64, dNamlen uint32, dType bool) {
// openedDir returns the directory and ErrnoSuccess if the fd points to a readable directory.
func openedDir(fsc *sys.FSContext, fd uint32) (fs.ReadDirFile, *sys.ReadDir, Errno) {
if f, ok := fsc.OpenedFile(fd); !ok {
if f, ok := fsc.LookupFile(fd); !ok {
return nil, nil, ErrnoBadf
} else if d, ok := f.File.(fs.ReadDirFile); !ok {
// fd_readdir docs don't indicate whether to return ErrnoNotdir or
@@ -933,7 +937,7 @@ func fdSeekFn(_ context.Context, mod api.Module, params []uint64) Errno {
var seeker io.Seeker
// Check to see if the file descriptor is available
if f, ok := fsc.OpenedFile(fd); !ok {
if f, ok := fsc.LookupFile(fd); !ok {
return ErrnoBadf
// fs.FS doesn't declare io.Seeker, but implementations such as os.File implement it.
} else if seeker, ok = f.File.(io.Seeker); !ok {
@@ -1040,7 +1044,7 @@ func fdWriteFn(_ context.Context, mod api.Module, params []uint64) Errno {
iovsCount := uint32(params[2])
resultNwritten := uint32(params[3])
writer := fsc.FdWriter(fd)
writer := sys.WriterForFile(fsc, fd)
if writer == nil {
return ErrnoBadf
}
@@ -1118,10 +1122,8 @@ func pathCreateDirectoryFn(_ context.Context, mod api.Module, params []uint64) E
return errno
}
if fd, err := fsc.Mkdir(pathName, 0o700); err != nil {
if err := fsc.FS().Mkdir(pathName, 0o700); err != nil {
return ToErrno(err)
} else {
_ = fsc.CloseFile(fd)
}
return ErrnoSuccess
@@ -1183,20 +1185,23 @@ func pathFilestatGetFn(_ context.Context, mod api.Module, params []uint64) Errno
}
pathName := string(b)
// Prepend the path if necessary.
if dir, ok := fsc.OpenedFile(dirfd); !ok {
return ErrnoBadf
} else if _, ok := dir.File.(fs.ReadDirFile); !ok {
return ErrnoNotdir // TODO: cache filetype instead of poking.
} else {
// os.File implements ReadDirFile, so we have to check with stat.
stat, err := sys.StatFile(fsc, dirfd)
if err != nil {
return ToErrno(err)
}
if !stat.IsDir() {
return ErrnoNotdir
} else { // prepend the name
// TODO: consolidate "at" logic with path_open as same issues occur.
pathName = path.Join(dir.Name, pathName)
pathName = path.Join(stat.Name(), pathName)
}
// Stat the file without allocating a file descriptor
stat, errnoResult := statFile(fsc, pathName)
if errnoResult != ErrnoSuccess {
return errnoResult
stat, err = syscallfs.StatPath(fsc.FS(), pathName)
if err != nil {
return ToErrno(err)
}
// Write the stat result to memory
@@ -1315,22 +1320,20 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) Errno {
fileOpenFlags, isDir := openFlags(oflags, fdflags)
var newFD uint32
var err error
if isDir && oflags&O_CREAT != 0 {
return ErrnoInval // use pathCreateDirectory!
} else {
newFD, err = fsc.OpenFile(pathName, fileOpenFlags, 0o600)
}
newFD, err := fsc.OpenFile(pathName, fileOpenFlags, 0o600)
if err != nil {
return ToErrno(err)
}
// Check any flags that require the file to evaluate.
if isDir {
if errno := failIfNotDirectory(fsc, newFD); errno != ErrnoSuccess {
return errno
if stat, err := sys.StatFile(fsc, newFD); err != nil || !stat.IsDir() {
_ = fsc.CloseFile(newFD)
return ErrnoNotdir
}
}
@@ -1358,8 +1361,8 @@ func atPath(fsc *sys.FSContext, mem api.Memory, dirFd, path, pathLen uint32) (st
// "/tmp/foo/bar" not "/bar".
}
if _, ok := fsc.OpenedFile(dirFd); !ok {
return "", ErrnoBadf
if _, ok := fsc.LookupFile(dirFd); !ok {
return "", ErrnoBadf // closed
}
b, ok := mem.Read(path, pathLen)
@@ -1392,17 +1395,6 @@ func openFlags(oflags, fdflags uint16) (openFlags int, isDir bool) {
return
}
func failIfNotDirectory(fsc *sys.FSContext, fd uint32) Errno {
// Lookup the previous file
if f, ok := fsc.OpenedFile(fd); !ok {
return ErrnoBadf
} else if _, ok := f.File.(fs.ReadDirFile); !ok {
_ = fsc.CloseFile(fd)
return ErrnoNotdir
}
return ErrnoSuccess
}
// pathReadlink is the WASI function named PathReadlinkName that reads the
// contents of a symbolic link.
//
@@ -1453,7 +1445,7 @@ func pathRemoveDirectoryFn(_ context.Context, mod api.Module, params []uint64) E
return errno
}
if err := fsc.Rmdir(pathName); err != nil {
if err := fsc.FS().Rmdir(pathName); err != nil {
return ToErrno(err)
}
@@ -1512,7 +1504,7 @@ func pathRenameFn(_ context.Context, mod api.Module, params []uint64) Errno {
return errno
}
if err := fsc.Rename(oldPathName, newPathName); err != nil {
if err := fsc.FS().Rename(oldPathName, newPathName); err != nil {
return ToErrno(err)
}
@@ -1568,20 +1560,9 @@ func pathUnlinkFileFn(_ context.Context, mod api.Module, params []uint64) Errno
return errno
}
if err := fsc.Unlink(pathName); err != nil {
if err := fsc.FS().Unlink(pathName); err != nil {
return ToErrno(err)
}
return ErrnoSuccess
}
// statFile attempts to stat the file at the given path. Errors coerce to WASI
// Errno.
func statFile(fsc *sys.FSContext, name string) (stat fs.FileInfo, errno Errno) {
var err error
stat, err = fsc.StatPath(name)
if err != nil {
errno = ToErrno(err)
}
return
}

View File

@@ -67,11 +67,11 @@ func Test_fdClose(t *testing.T) {
`, "\n"+log.String())
// Verify fdToClose is closed and removed from the opened FDs.
_, ok := fsc.OpenedFile(fdToClose)
_, ok := fsc.LookupFile(fdToClose)
require.False(t, ok)
// Verify fdToKeep is not closed
_, ok = fsc.OpenedFile(fdToKeep)
_, ok = fsc.LookupFile(fdToKeep)
require.True(t, ok)
log.Reset()
@@ -104,10 +104,10 @@ func Test_fdFdstatGet(t *testing.T) {
// open both paths without using WASI
fsc := mod.(*wasm.CallContext).Sys.FS()
fileFd, err := fsc.OpenFile(file, os.O_RDONLY, 0)
fileFD, err := fsc.OpenFile(file, os.O_RDONLY, 0)
require.NoError(t, err)
dirFd, err := fsc.OpenFile(dir, os.O_RDONLY, 0)
dirFD, err := fsc.OpenFile(dir, os.O_RDONLY, 0)
require.NoError(t, err)
tests := []struct {
@@ -175,7 +175,7 @@ func Test_fdFdstatGet(t *testing.T) {
},
{
name: "file",
fd: fileFd,
fd: fileFD,
expectedMemory: []byte{
4, 0, // fs_filetype
0, 0, 0, 0, 0, 0, // fs_flags
@@ -189,7 +189,7 @@ func Test_fdFdstatGet(t *testing.T) {
},
{
name: "dir",
fd: dirFd,
fd: dirFD,
expectedMemory: []byte{
3, 0, // fs_filetype
0, 0, 0, 0, 0, 0, // fs_flags
@@ -212,7 +212,7 @@ func Test_fdFdstatGet(t *testing.T) {
},
{
name: "resultFdstat exceeds the maximum valid address by 1",
fd: dirFd,
fd: dirFD,
resultFdstat: memorySize - 24 + 1,
expectedErrno: ErrnoFault,
expectedLog: `
@@ -273,10 +273,10 @@ func Test_fdFilestatGet(t *testing.T) {
// open both paths without using WASI
fsc := mod.(*wasm.CallContext).Sys.FS()
fileFd, err := fsc.OpenFile(file, os.O_RDONLY, 0)
fileFD, err := fsc.OpenFile(file, os.O_RDONLY, 0)
require.NoError(t, err)
dirFd, err := fsc.OpenFile(dir, os.O_RDONLY, 0)
dirFD, err := fsc.OpenFile(dir, os.O_RDONLY, 0)
require.NoError(t, err)
tests := []struct {
@@ -363,7 +363,7 @@ func Test_fdFilestatGet(t *testing.T) {
},
{
name: "file",
fd: fileFd,
fd: fileFD,
expectedMemory: []byte{
0, 0, 0, 0, 0, 0, 0, 0, // dev
0, 0, 0, 0, 0, 0, 0, 0, // ino
@@ -381,7 +381,7 @@ func Test_fdFilestatGet(t *testing.T) {
},
{
name: "dir",
fd: dirFd,
fd: dirFD,
expectedMemory: []byte{
0, 0, 0, 0, 0, 0, 0, 0, // dev
0, 0, 0, 0, 0, 0, 0, 0, // ino
@@ -408,7 +408,7 @@ func Test_fdFilestatGet(t *testing.T) {
},
{
name: "resultFilestat exceeds the maximum valid address by 1",
fd: dirFd,
fd: dirFD,
resultFilestat: memorySize - 64 + 1,
expectedErrno: ErrnoFault,
expectedLog: `
@@ -1059,7 +1059,7 @@ func Test_fdRead_Errors(t *testing.T) {
var (
fdReadDirFs = fstest.MapFS{
"notdir": {},
"file": {},
"emptydir": {Mode: fs.ModeDir},
"dir": {Mode: fs.ModeDir},
"dir/-": {}, // len = 24+1 = 25
@@ -1341,7 +1341,7 @@ func Test_fdReaddir(t *testing.T) {
defer log.Reset()
// Assign the state we are testing
file, ok := fsc.OpenedFile(fd)
file, ok := fsc.LookupFile(fd)
require.True(t, ok)
dir := tc.dir()
defer dir.File.Close()
@@ -1383,10 +1383,10 @@ func Test_fdReaddir_Errors(t *testing.T) {
fsc := mod.(*wasm.CallContext).Sys.FS()
dirFD, err := fsc.OpenFile("dir", os.O_RDONLY, 0)
fileFD, err := fsc.OpenFile("file", os.O_RDONLY, 0)
require.NoError(t, err)
fileFD, err := fsc.OpenFile("notdir", os.O_RDONLY, 0)
dirFD, err := fsc.OpenFile("dir", os.O_RDONLY, 0)
require.NoError(t, err)
tests := []struct {
@@ -1405,7 +1405,7 @@ func Test_fdReaddir_Errors(t *testing.T) {
bufLen: 1000,
expectedErrno: ErrnoFault,
expectedLog: `
==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=65536,buf_len=1000,cookie=0,result.bufused=0)
==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=65536,buf_len=1000,cookie=0,result.bufused=0)
<== errno=EFAULT
`,
},
@@ -1427,19 +1427,8 @@ func Test_fdReaddir_Errors(t *testing.T) {
resultBufused: 1000, // arbitrary
expectedErrno: ErrnoBadf,
expectedLog: `
==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=0,buf_len=24,cookie=0,result.bufused=1000)
==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=0,buf_len=24,cookie=0,result.bufused=1000)
<== errno=EBADF
`,
},
{
name: "out-of-memory reading buf",
fd: dirFD,
buf: memLen,
bufLen: 1000,
expectedErrno: ErrnoFault,
expectedLog: `
==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=65536,buf_len=1000,cookie=0,result.bufused=0)
<== errno=EFAULT
`,
},
{
@@ -1449,7 +1438,7 @@ func Test_fdReaddir_Errors(t *testing.T) {
bufLen: 1000,
expectedErrno: ErrnoFault,
expectedLog: `
==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=65535,buf_len=1000,cookie=0,result.bufused=0)
==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=65535,buf_len=1000,cookie=0,result.bufused=0)
<== errno=EFAULT
`,
},
@@ -1460,7 +1449,7 @@ func Test_fdReaddir_Errors(t *testing.T) {
resultBufused: 1000,
expectedErrno: ErrnoInval,
expectedLog: `
==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=0,buf_len=1,cookie=0,result.bufused=1000)
==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=0,buf_len=1,cookie=0,result.bufused=1000)
<== errno=EINVAL
`,
},
@@ -1472,19 +1461,7 @@ func Test_fdReaddir_Errors(t *testing.T) {
resultBufused: 2000,
expectedErrno: ErrnoInval,
expectedLog: `
==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=0,buf_len=1000,cookie=1,result.bufused=2000)
<== errno=EINVAL
`,
},
{
name: "cookie invalid when no prior state",
fd: dirFD,
buf: 0, bufLen: 1000,
cookie: 1,
resultBufused: 2000,
expectedErrno: ErrnoInval,
expectedLog: `
==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=0,buf_len=1000,cookie=1,result.bufused=2000)
==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=0,buf_len=1000,cookie=1,result.bufused=2000)
<== errno=EINVAL
`,
},
@@ -1497,7 +1474,7 @@ func Test_fdReaddir_Errors(t *testing.T) {
resultBufused: 2000,
expectedErrno: ErrnoInval,
expectedLog: `
==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=0,buf_len=1000,cookie=-1,result.bufused=2000)
==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=0,buf_len=1000,cookie=-1,result.bufused=2000)
<== errno=EINVAL
`,
},
@@ -1509,7 +1486,7 @@ func Test_fdReaddir_Errors(t *testing.T) {
defer log.Reset()
// Reset the directory so that tests don't taint each other.
if file, ok := fsc.OpenedFile(tc.fd); ok && tc.fd == dirFD {
if file, ok := fsc.LookupFile(tc.fd); ok && tc.fd == dirFD {
dir, err := fdReadDirFs.Open("dir")
require.NoError(t, err)
defer dir.Close()
@@ -1604,7 +1581,7 @@ func Test_fdSeek(t *testing.T) {
// Since we initialized this file, we know it is a seeker (because it is a MapFile)
fsc := mod.(*wasm.CallContext).Sys.FS()
f, ok := fsc.OpenedFile(fd)
f, ok := fsc.LookupFile(fd)
require.True(t, ok)
seeker := f.File.(io.Seeker)
@@ -1961,17 +1938,6 @@ func Test_pathCreateDirectory_Errors(t *testing.T) {
expectedLog: `
==> wasi_snapshot_preview1.path_create_directory(fd=3,path=OOM(65536,1))
<== errno=EFAULT
`,
},
{
name: "path invalid",
fd: sys.FdRoot,
pathName: "../foo",
pathLen: 6,
expectedErrno: ErrnoInval,
expectedLog: `
==> wasi_snapshot_preview1.path_create_directory(fd=3,path=../foo)
<== errno=EINVAL
`,
},
{
@@ -2043,10 +2009,10 @@ func Test_pathFilestatGet(t *testing.T) {
// open both paths without using WASI
fsc := mod.(*wasm.CallContext).Sys.FS()
fileFd, err := fsc.OpenFile(file, os.O_RDONLY, 0)
fileFD, err := fsc.OpenFile(file, os.O_RDONLY, 0)
require.NoError(t, err)
dirFd, err := fsc.OpenFile(dir, os.O_RDONLY, 0)
dirFD, err := fsc.OpenFile(dir, os.O_RDONLY, 0)
require.NoError(t, err)
tests := []struct {
@@ -2080,7 +2046,7 @@ func Test_pathFilestatGet(t *testing.T) {
},
{
name: "file under dir",
fd: dirFd, // root
fd: dirFD, // root
memory: initialMemoryFile,
pathLen: 1,
resultFilestat: 2,
@@ -2133,7 +2099,7 @@ func Test_pathFilestatGet(t *testing.T) {
},
{
name: "bad FD - not dir",
fd: fileFd,
fd: fileFD,
memory: initialMemoryFile,
pathLen: 1,
resultFilestat: 2,
@@ -2157,7 +2123,7 @@ func Test_pathFilestatGet(t *testing.T) {
},
{
name: "path under dir doesn't exist",
fd: dirFd,
fd: dirFD,
memory: initialMemoryNotExists,
pathLen: 1,
resultFilestat: 2,
@@ -2169,7 +2135,7 @@ func Test_pathFilestatGet(t *testing.T) {
},
{
name: "path invalid",
fd: dirFd,
fd: dirFD,
memory: []byte("?../foo"),
pathLen: 6,
resultFilestat: 7,
@@ -2321,9 +2287,9 @@ func Test_pathOpen(t *testing.T) {
fdflags: FD_APPEND,
expected: func(t *testing.T, fsc *sys.FSContext) {
contents := []byte("hello")
_, err := fsc.FdWriter(expectedOpenedFd).Write(contents)
_, err := sys.WriterForFile(fsc, expectedOpenedFd).Write(contents)
require.NoError(t, err)
require.True(t, fsc.CloseFile(expectedOpenedFd))
require.NoError(t, fsc.CloseFile(expectedOpenedFd))
// verify the contents were appended
b := readFile(t, dir, appendName)
@@ -2353,9 +2319,9 @@ func Test_pathOpen(t *testing.T) {
expected: func(t *testing.T, fsc *sys.FSContext) {
// expect to create a new file
contents := []byte("hello")
_, err := fsc.FdWriter(expectedOpenedFd).Write(contents)
_, err := sys.WriterForFile(fsc, expectedOpenedFd).Write(contents)
require.NoError(t, err)
require.True(t, fsc.CloseFile(expectedOpenedFd))
require.NoError(t, fsc.CloseFile(expectedOpenedFd))
// verify the contents were written
b := readFile(t, dir, "creat")
@@ -2385,9 +2351,9 @@ func Test_pathOpen(t *testing.T) {
expected: func(t *testing.T, fsc *sys.FSContext) {
// expect to create a new file
contents := []byte("hello")
_, err := fsc.FdWriter(expectedOpenedFd).Write(contents)
_, err := sys.WriterForFile(fsc, expectedOpenedFd).Write(contents)
require.NoError(t, err)
require.True(t, fsc.CloseFile(expectedOpenedFd))
require.NoError(t, fsc.CloseFile(expectedOpenedFd))
// verify the contents were written
b := readFile(t, dir, path.Join(dirName, "O_CREAT-O_TRUNC"))
@@ -2404,7 +2370,7 @@ func Test_pathOpen(t *testing.T) {
oflags: O_DIRECTORY,
path: func(*testing.T) string { return dirName },
expected: func(t *testing.T, fsc *sys.FSContext) {
stat, err := fsc.StatFile(expectedOpenedFd)
stat, err := sys.StatFile(fsc, expectedOpenedFd)
require.NoError(t, err)
require.True(t, stat.IsDir())
},
@@ -2419,7 +2385,7 @@ func Test_pathOpen(t *testing.T) {
path: func(*testing.T) string { return dirName },
oflags: O_DIRECTORY,
expected: func(t *testing.T, fsc *sys.FSContext) {
stat, err := fsc.StatFile(expectedOpenedFd)
stat, err := sys.StatFile(fsc, expectedOpenedFd)
require.NoError(t, err)
require.True(t, stat.IsDir())
},
@@ -2446,9 +2412,9 @@ func Test_pathOpen(t *testing.T) {
oflags: O_TRUNC,
expected: func(t *testing.T, fsc *sys.FSContext) {
contents := []byte("hello")
_, err := fsc.FdWriter(expectedOpenedFd).Write(contents)
_, err := sys.WriterForFile(fsc, expectedOpenedFd).Write(contents)
require.NoError(t, err)
require.True(t, fsc.CloseFile(expectedOpenedFd))
require.NoError(t, fsc.CloseFile(expectedOpenedFd))
// verify the contents were truncated
b := readFile(t, dir, "trunc")
@@ -2499,12 +2465,12 @@ func Test_pathOpen(t *testing.T) {
func requireContents(t *testing.T, fsc *sys.FSContext, expectedOpenedFd uint32, fileName string, fileContents []byte) {
// verify the file was actually opened
f, ok := fsc.OpenedFile(expectedOpenedFd)
f, ok := fsc.LookupFile(expectedOpenedFd)
require.True(t, ok)
require.Equal(t, fileName, f.Name)
// verify the contents are readable
b, err := io.ReadAll(fsc.FdReader(expectedOpenedFd))
b, err := io.ReadAll(f.File)
require.NoError(t, err)
require.Equal(t, fileContents, b)
}
@@ -2528,7 +2494,7 @@ func writeFile(t *testing.T, tmpDir, file string, contents []byte) {
func Test_pathOpen_Errors(t *testing.T) {
validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err
dirName := "wazero"
fileName := "notdir" // name length as wazero
fileName := "file" // name length as wazero
testFS := fstest.MapFS{
dirName: &fstest.MapFile{Mode: os.ModeDir},
fileName: &fstest.MapFile{},
@@ -2563,18 +2529,6 @@ func Test_pathOpen_Errors(t *testing.T) {
expectedLog: `
==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=OOM(65536,6),oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
<== (opened_fd=,errno=EFAULT)
`,
},
{
name: "path invalid",
fd: validFD,
pathName: "../foo",
pathLen: 6,
// fstest.MapFS returns file not found instead of invalid on invalid path
expectedErrno: ErrnoNoent,
expectedLog: `
==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=../foo,oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
<== (opened_fd=,errno=ENOENT)
`,
},
{
@@ -2619,10 +2573,10 @@ func Test_pathOpen_Errors(t *testing.T) {
fd: validFD,
pathName: fileName,
path: validPath,
pathLen: validPathLen,
pathLen: uint32(len(fileName)),
expectedErrno: ErrnoNotdir,
expectedLog: `
==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=notdir,oflags=DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=)
==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=file,oflags=DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=)
<== (opened_fd=,errno=ENOTDIR)
`,
},
@@ -2632,10 +2586,10 @@ func Test_pathOpen_Errors(t *testing.T) {
fd: validFD,
pathName: fileName,
path: validPath,
pathLen: validPathLen,
pathLen: uint32(len(fileName)),
expectedErrno: ErrnoInval,
expectedLog: `
==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=notdir,oflags=CREAT|DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=)
==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=file,oflags=CREAT|DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=)
<== (opened_fd=,errno=EINVAL)
`,
},
@@ -2741,17 +2695,6 @@ func Test_pathRemoveDirectory_Errors(t *testing.T) {
expectedLog: `
==> wasi_snapshot_preview1.path_remove_directory(fd=3,path=OOM(65536,1))
<== errno=EFAULT
`,
},
{
name: "path invalid",
fd: sys.FdRoot,
pathName: "../foo",
pathLen: 6,
expectedErrno: ErrnoInval,
expectedLog: `
==> wasi_snapshot_preview1.path_remove_directory(fd=3,path=../foo)
<== errno=EINVAL
`,
},
{
@@ -2945,32 +2888,6 @@ func Test_pathRename_Errors(t *testing.T) {
expectedLog: `
==> wasi_snapshot_preview1.path_rename(fd=3,old_path=a,new_fd=3,new_path=OOM(65536,1))
<== errno=EFAULT
`,
},
{
name: "old path invalid",
oldFd: sys.FdRoot,
newFd: sys.FdRoot,
oldPathName: "../foo",
oldPathLen: 6,
expectedErrno: ErrnoInval,
expectedLog: `
==> wasi_snapshot_preview1.path_rename(fd=3,old_path=../foo,new_fd=3,new_path=)
<== errno=EINVAL
`,
},
{
name: "new path invalid",
oldFd: sys.FdRoot,
newFd: sys.FdRoot,
oldPathName: file,
oldPathLen: uint32(len(file)),
newPathName: "../foo",
newPathLen: 6,
expectedErrno: ErrnoInval,
expectedLog: `
==> wasi_snapshot_preview1.path_rename(fd=3,old_path=../f,new_fd=3,new_path=../foo)
<== errno=EINVAL
`,
},
{
@@ -3120,17 +3037,6 @@ func Test_pathUnlinkFile_Errors(t *testing.T) {
expectedLog: `
==> wasi_snapshot_preview1.path_unlink_file(fd=3,path=OOM(65536,1))
<== errno=EFAULT
`,
},
{
name: "path invalid",
fd: sys.FdRoot,
pathName: "../foo",
pathLen: 6,
expectedErrno: ErrnoInval,
expectedLog: `
==> wasi_snapshot_preview1.path_unlink_file(fd=3,path=../foo)
<== errno=EINVAL
`,
},
{
@@ -3191,6 +3097,7 @@ func requireOpenFile(t *testing.T, pathName string, data []byte) (api.Module, ui
testFS := fstest.MapFS{pathName[1:]: mapFile} // strip the leading slash
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
fsc := mod.(*wasm.CallContext).Sys.FS()
fd, err := fsc.OpenFile(pathName, os.O_RDONLY, 0)
require.NoError(t, err)
return mod, fd, log, r
@@ -3198,29 +3105,17 @@ func requireOpenFile(t *testing.T, pathName string, data []byte) (api.Module, ui
// requireOpenWritableFile is temporary until we add the ability to open files for writing.
func requireOpenWritableFile(t *testing.T, tmpDir string, pathName string) (api.Module, uint32, *bytes.Buffer, api.Closer) {
writeable, testFS := createWriteableFile(t, tmpDir, pathName, []byte{})
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
fsc := mod.(*wasm.CallContext).Sys.FS()
fd, err := fsc.OpenFile(pathName, os.O_RDWR, 0)
absolutePath := path.Join(tmpDir, pathName)
require.NoError(t, os.WriteFile(absolutePath, []byte{}, 0o600))
writeFS, err := syscallfs.NewDirFS(tmpDir)
require.NoError(t, err)
// Swap the read-only file with a writeable one until #390
f, ok := fsc.OpenedFile(fd)
require.True(t, ok)
f.File.Close()
f.File = writeable
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(writeFS))
fsc := mod.(*wasm.CallContext).Sys.FS()
fd, err := fsc.OpenFile(pathName, os.O_RDWR, 0o600)
require.NoError(t, err)
return mod, fd, log, r
}
// createWriteableFile uses real files when io.Writer tests are needed.
func createWriteableFile(t *testing.T, tmpDir string, pathName string, data []byte) (fs.File, fs.FS) {
require.NotNil(t, data)
absolutePath := path.Join(tmpDir, pathName)
require.NoError(t, os.WriteFile(absolutePath, data, 0o600))
// open the file for writing in a custom way until #390
f, err := os.OpenFile(absolutePath, os.O_RDWR, 0o600)
require.NoError(t, err)
return f, os.DirFS(tmpDir)
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"github.com/tetratelabs/wazero/api"
internalsys "github.com/tetratelabs/wazero/internal/sys"
. "github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -132,9 +133,11 @@ func processFDEvent(mod api.Module, eventType byte, inBuf []byte) Errno {
// Choose the best error, which falls back to unsupported, until we support
// files.
errno := ErrnoNotsup
if eventType == EventTypeFdRead && fsc.FdReader(fd) == nil {
errno = ErrnoBadf
} else if eventType == EventTypeFdWrite && fsc.FdWriter(fd) == nil {
if eventType == EventTypeFdRead {
if _, ok := fsc.LookupFile(fd); !ok {
errno = ErrnoBadf
}
} else if eventType == EventTypeFdWrite && internalsys.WriterForFile(fsc, fd) == nil {
errno = ErrnoBadf
}

View File

@@ -178,11 +178,11 @@ func Benchmark_fdReaddir(b *testing.B) {
if err != nil {
b.Fatal(err)
}
f, ok := fsc.OpenedFile(fd)
f, ok := fsc.LookupFile(fd)
if !ok {
b.Fatal("couldn't open fd ", fd)
}
defer fsc.CloseFile(fd)
defer fsc.CloseFile(fd) //nolint
b.ResetTimer()
b.ReportAllocs()
@@ -284,7 +284,7 @@ func Benchmark_pathFilestat(b *testing.B) {
if err != nil {
b.Fatal(err)
}
defer fsc.CloseFile(fd)
defer fsc.CloseFile(fd) //nolint
}
fn := mod.ExportedFunction(PathFilestatGetName)

View File

@@ -22,6 +22,7 @@ import (
gojs "github.com/tetratelabs/wazero/imports/go"
internalgojs "github.com/tetratelabs/wazero/internal/gojs"
"github.com/tetratelabs/wazero/internal/gojs/run"
"github.com/tetratelabs/wazero/internal/syscallfs"
)
func compileAndRun(ctx context.Context, arg string, config wazero.ModuleConfig) (stdout, stderr string, err error) {
@@ -65,12 +66,12 @@ var testBin []byte
// testCtx is configured in TestMain to re-use wazero's compilation cache.
var (
testCtx context.Context
testFS = fstest.MapFS{
testFS = syscallfs.Adapt(fstest.MapFS{
"empty.txt": {},
"test.txt": {Data: []byte("animals\n"), Mode: 0o644},
"sub": {Mode: fs.ModeDir | 0o755},
"sub/test.txt": {Data: []byte("greet sub dir\n"), Mode: 0o444},
}
})
rt wazero.Runtime
)

View File

@@ -13,6 +13,7 @@ import (
"github.com/tetratelabs/wazero/internal/gojs/goos"
"github.com/tetratelabs/wazero/internal/platform"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/syscallfs"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -93,6 +94,7 @@ func (jsfsOpen) invoke(ctx context.Context, mod api.Module, args ...interface{})
callback := args[3].(funcWrapper)
fsc := mod.(*wasm.CallContext).Sys.FS()
fd, err := fsc.OpenFile(path, int(flags), fs.FileMode(perm))
return callback.invoke(ctx, mod, goos.RefJsfs, err, fd) // note: error first
@@ -112,13 +114,13 @@ func (jsfsStat) invoke(ctx context.Context, mod api.Module, args ...interface{})
}
// syscallStat is like syscall.Stat
func syscallStat(mod api.Module, name string) (*jsSt, error) {
func syscallStat(mod api.Module, path string) (*jsSt, error) {
fsc := mod.(*wasm.CallContext).Sys.FS()
if fd, err := fsc.OpenFile(name, os.O_RDONLY, 0); err != nil {
if stat, err := syscallfs.StatPath(fsc.FS(), path); err != nil {
return nil, err
} else {
defer fsc.CloseFile(fd)
return syscallFstat(fsc, fd)
return newJsSt(stat), nil
}
}
@@ -168,21 +170,23 @@ const (
// syscallFstat is like syscall.Fstat
func syscallFstat(fsc *internalsys.FSContext, fd uint32) (*jsSt, error) {
if f, ok := fsc.OpenedFile(fd); !ok {
return nil, syscall.EBADF
} else if stat, err := f.File.Stat(); err != nil {
stat, err := internalsys.StatFile(fsc, fd)
if err != nil {
return nil, err
} else {
ret := &jsSt{}
ret.isDir = stat.IsDir()
ret.mode = getJsMode(stat.Mode())
ret.size = stat.Size()
atimeNsec, mtimeNsec, ctimeNsec := platform.StatTimes(stat)
ret.atimeMs = atimeNsec / 1e6
ret.mtimeMs = mtimeNsec / 1e6
ret.ctimeMs = ctimeNsec / 1e6
return ret, nil
}
return newJsSt(stat), nil
}
func newJsSt(stat fs.FileInfo) *jsSt {
ret := &jsSt{}
ret.isDir = stat.IsDir()
ret.mode = getJsMode(stat.Mode())
ret.size = stat.Size()
atimeNsec, mtimeNsec, ctimeNsec := platform.StatTimes(stat)
ret.atimeMs = atimeNsec / 1e6
ret.mtimeMs = mtimeNsec / 1e6
ret.ctimeMs = ctimeNsec / 1e6
return ret
}
// getJsMode is required because the mode property read in `GOOS=js` is
@@ -230,10 +234,7 @@ func (jsfsClose) invoke(ctx context.Context, mod api.Module, args ...interface{}
fd := toUint32(args[0])
callback := args[1].(funcWrapper)
var err error
if ok := fsc.CloseFile(fd); !ok {
err = syscall.EBADF // already closed
}
err := fsc.CloseFile(fd)
return jsfsInvoke(ctx, mod, callback, err)
}
@@ -263,13 +264,13 @@ func (jsfsRead) invoke(ctx context.Context, mod api.Module, args ...interface{})
func syscallRead(mod api.Module, fd uint32, offset interface{}, p []byte) (n uint32, err error) {
fsc := mod.(*wasm.CallContext).Sys.FS()
r := fsc.FdReader(fd)
if r == nil {
f, ok := fsc.LookupFile(fd)
if !ok {
err = syscall.EBADF
}
if offset != nil {
if s, ok := r.(io.Seeker); ok {
if s, ok := f.File.(io.Seeker); ok {
if _, err := s.Seek(toInt64(offset), io.SeekStart); err != nil {
return 0, err
}
@@ -278,7 +279,7 @@ func syscallRead(mod api.Module, fd uint32, offset interface{}, p []byte) (n uin
}
}
if nRead, e := r.Read(p); e == nil || e == io.EOF {
if nRead, e := f.File.Read(p); e == nil || e == io.EOF {
// fs_js.go cannot parse io.EOF so coerce it to nil.
// See https://github.com/golang/go/issues/43913
n = uint32(nRead)
@@ -317,7 +318,7 @@ func (jsfsWrite) invoke(ctx context.Context, mod api.Module, args ...interface{}
func syscallWrite(mod api.Module, fd uint32, offset interface{}, p []byte) (n uint32, err error) {
fsc := mod.(*wasm.CallContext).Sys.FS()
if writer := fsc.FdWriter(fd); writer == nil {
if writer := internalsys.WriterForFile(fsc, fd); writer == nil {
err = syscall.EBADF
} else if nWritten, e := writer.Write(p); e == nil || e == io.EOF {
// fs_js.go cannot parse io.EOF so coerce it to nil.
@@ -346,15 +347,14 @@ func (jsfsReaddir) invoke(ctx context.Context, mod api.Module, args ...interface
func syscallReaddir(_ context.Context, mod api.Module, name string) (*objectArray, error) {
fsc := mod.(*wasm.CallContext).Sys.FS()
fd, err := fsc.OpenFile(name, os.O_RDONLY, 0)
// don't allocate a file descriptor
f, err := fsc.FS().OpenFile(name, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
defer fsc.CloseFile(fd)
defer f.Close() //nolint
if f, ok := fsc.OpenedFile(fd); !ok {
return nil, syscall.EBADF
} else if d, ok := f.File.(fs.ReadDirFile); !ok {
if d, ok := f.(fs.ReadDirFile); !ok {
return nil, syscall.ENOTDIR
} else if l, err := d.ReadDir(-1); err != nil {
return nil, err
@@ -399,20 +399,11 @@ func (processCwd) invoke(ctx context.Context, _ api.Module, _ ...interface{}) (i
type processChdir struct{}
func (processChdir) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
fsc := mod.(*wasm.CallContext).Sys.FS()
path := args[0].(string)
// TODO: refactor so that sys has path-based ops, also needed in WASI.
if fd, err := fsc.OpenFile(path, os.O_RDONLY, 0); err != nil {
return nil, syscall.ENOENT
} else if f, ok := fsc.OpenedFile(fd); !ok {
return nil, syscall.ENOENT
} else if s, err := f.File.Stat(); err != nil {
fsc.CloseFile(fd)
return nil, syscall.ENOENT
} else if !s.IsDir() {
fsc.CloseFile(fd)
if s, err := syscallStat(mod, path); err != nil {
return nil, mapJSError(err)
} else if !s.isDir {
return nil, syscall.ENOTDIR
} else {
getState(ctx).cwd = path
@@ -431,7 +422,12 @@ func (jsfsMkdir) invoke(ctx context.Context, mod api.Module, args ...interface{}
callback := args[2].(funcWrapper)
fsc := mod.(*wasm.CallContext).Sys.FS()
fd, err := fsc.Mkdir(path, fs.FileMode(perm))
var fd uint32
var err error
if err = fsc.FS().Mkdir(path, fs.FileMode(perm)); err == nil {
fd, err = fsc.OpenFile(path, os.O_RDONLY, 0)
}
return callback.invoke(ctx, mod, goos.RefJsfs, err, fd) // note: error first
}
@@ -446,7 +442,7 @@ func (jsfsRmdir) invoke(ctx context.Context, mod api.Module, args ...interface{}
callback := args[1].(funcWrapper)
fsc := mod.(*wasm.CallContext).Sys.FS()
err := fsc.Rmdir(path)
err := fsc.FS().Rmdir(path)
return jsfsInvoke(ctx, mod, callback, err)
}
@@ -462,7 +458,7 @@ func (jsfsRename) invoke(ctx context.Context, mod api.Module, args ...interface{
callback := args[2].(funcWrapper)
fsc := mod.(*wasm.CallContext).Sys.FS()
err := fsc.Rename(from, to)
err := fsc.FS().Rename(from, to)
return jsfsInvoke(ctx, mod, callback, err)
}
@@ -477,7 +473,7 @@ func (jsfsUnlink) invoke(ctx context.Context, mod api.Module, args ...interface{
callback := args[1].(funcWrapper)
fsc := mod.(*wasm.CallContext).Sys.FS()
err := fsc.Unlink(path)
err := fsc.FS().Unlink(path)
return jsfsInvoke(ctx, mod, callback, err)
}
@@ -494,7 +490,7 @@ func (jsfsUtimes) invoke(ctx context.Context, mod api.Module, args ...interface{
callback := args[3].(funcWrapper)
fsc := mod.(*wasm.CallContext).Sys.FS()
err := fsc.Utimes(path, atimeSec*1e9, mtimeSec*1e9)
err := fsc.FS().Utimes(path, atimeSec*1e9, mtimeSec*1e9)
return jsfsInvoke(ctx, mod, callback, err)
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/gojs/custom"
"github.com/tetratelabs/wazero/internal/gojs/goarch"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -39,12 +40,9 @@ func wasmWrite(_ context.Context, mod api.Module, stack goarch.Stack) {
p := stack.ParamBytes(mod.Memory(), 1 /*, 2 */)
fsc := mod.(*wasm.CallContext).Sys.FS()
writer := fsc.FdWriter(fd)
if writer == nil {
panic(fmt.Errorf("unexpected fd %d", fd))
}
if _, err := writer.Write(p); err != nil {
if writer := internalsys.WriterForFile(fsc, fd); writer == nil {
panic(fmt.Errorf("fd %d invalid", fd))
} else if _, err := writer.Write(p); err != nil {
panic(fmt.Errorf("error writing p: %w", err))
}
}

View File

@@ -38,22 +38,6 @@ const (
modeCharDevice = uint32(fs.ModeCharDevice | 0o640)
)
// EmptyFS is exported to special-case an empty file system.
var EmptyFS = &emptyFS{}
type emptyFS struct{}
// compile-time check to ensure emptyFS implements fs.FS
var _ fs.FS = &emptyFS{}
// Open implements the same method as documented on fs.FS.
func (f *emptyFS) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
}
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
}
// An emptyRootDir is a fake "/" directory.
type emptyRootDir struct{}
@@ -175,7 +159,7 @@ type ReadDir struct {
type FSContext struct {
// fs is the root ("/") mount.
fs fs.FS
fs syscallfs.FS
// openedFiles is a map of file descriptor numbers (>=FdRoot) to open files
// (or directories) and defaults to empty.
@@ -190,27 +174,18 @@ var errNotDir = errors.New("not a directory")
// Otherwise, `root` is assigned file descriptor FdRoot and the returned
// context can open files in that file system. Any error on opening "." is
// returned.
func NewFSContext(stdin io.Reader, stdout, stderr io.Writer, root fs.FS) (fsc *FSContext, err error) {
func NewFSContext(stdin io.Reader, stdout, stderr io.Writer, root syscallfs.FS) (fsc *FSContext, err error) {
fsc = &FSContext{fs: root}
fsc.openedFiles.Insert(stdinReader(stdin))
fsc.openedFiles.Insert(stdioWriter(stdout, noopStdoutStat))
fsc.openedFiles.Insert(stdioWriter(stderr, noopStderrStat))
if root == EmptyFS {
if root == syscallfs.EmptyFS {
return fsc, nil
}
// Open the root directory by using "." as "/" is not relevant in fs.FS.
// This not only validates the file system, but also allows us to test if
// this is a real file or not. ex. `file.(*os.File)`.
//
// Note: We don't use fs.ReadDirFS as this isn't implemented by os.DirFS.
var rootDir fs.File
if sfs, ok := root.(syscallfs.FS); ok {
rootDir, err = sfs.OpenFile(".", os.O_RDONLY, 0)
} else {
rootDir, err = root.Open(".")
}
// Test if this is a real file or not. ex. `file.(*os.File)`.
rootDir, err := root.OpenFile(".", os.O_RDONLY, 0)
if err != nil {
// This could fail because someone made a special-purpose file system,
// which only passes certain filenames and not ".".
@@ -255,19 +230,6 @@ func stdioStat(f interface{}, defaultStat stdioFileInfo) fs.FileInfo {
return defaultStat
}
// OpenedFile returns a file and true if it was opened or nil and false, if syscall.EBADF.
func (c *FSContext) OpenedFile(fd uint32) (*FileEntry, bool) {
return c.openedFiles.Lookup(fd)
}
func (c *FSContext) StatFile(fd uint32) (fs.FileInfo, error) {
f, ok := c.openedFiles.Lookup(fd)
if !ok {
return nil, syscall.EBADF
}
return f.File.Stat()
}
// fileModeStat is a fake fs.FileInfo which only returns its mode.
// This is used for character devices.
type fileModeStat fs.FileMode
@@ -281,156 +243,37 @@ func (s fileModeStat) Sys() interface{} { return nil }
func (s fileModeStat) Name() string { return "" }
func (s fileModeStat) IsDir() bool { return false }
// Mkdir is like syscall.Mkdir and returns the file descriptor of the new
// directory or an error.
func (c *FSContext) Mkdir(name string, perm fs.FileMode) (newFD uint32, err error) {
name = c.cleanPath(name)
if wfs, ok := c.fs.(syscallfs.FS); ok {
if err = wfs.Mkdir(name, perm); err != nil {
return
}
// TODO: Determine how to handle when a directory already
// exists or is a file.
return c.OpenFile(name, os.O_RDONLY, perm)
}
err = syscall.ENOSYS
return
// FS returns the underlying filesystem. Any files that should be added to the
// table should be inserted via InsertFile.
func (c *FSContext) FS() syscallfs.FS {
return c.fs
}
// OpenFile is like syscall.Open and returns the file descriptor of the new file or an error.
func (c *FSContext) OpenFile(path string, flags int, perm fs.FileMode) (newFD uint32, err error) {
var f fs.File
if wfs, ok := c.fs.(syscallfs.FS); ok {
path = c.cleanPath(path)
f, err = wfs.OpenFile(path, flags, perm)
} else {
// While os.Open says it is read-only, in reality the files returned
// are often writable. Fail only on the flags which won't work.
switch {
case flags&os.O_APPEND != 0:
fallthrough
case flags&os.O_CREATE != 0:
fallthrough
case flags&os.O_TRUNC != 0:
return 0, syscall.ENOSYS
default:
// only time fs.FS is used
f, err = c.fs.Open(c.cleanPath(path))
}
}
if err != nil {
// OpenFile opens the file into the table and returns its file descriptor.
// The result must be closed by CloseFile or Close.
func (c *FSContext) OpenFile(path string, flag int, perm fs.FileMode) (uint32, error) {
if f, err := c.fs.OpenFile(path, flag, perm); err != nil {
return 0, err
}
newFD = c.openedFiles.Insert(&FileEntry{Name: pathutil.Base(path), File: f})
return newFD, nil
}
// Rmdir is like syscall.Rmdir.
func (c *FSContext) Rmdir(path string) (err error) {
if wfs, ok := c.fs.(syscallfs.FS); ok {
path = c.cleanPath(path)
return wfs.Rmdir(path)
}
err = syscall.ENOSYS
return
}
func (c *FSContext) StatPath(path string) (fs.FileInfo, error) {
fd, err := c.OpenFile(path, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
defer c.CloseFile(fd)
return c.StatFile(fd)
}
// Rename is like syscall.Rename.
func (c *FSContext) Rename(from, to string) (err error) {
if wfs, ok := c.fs.(syscallfs.FS); ok {
from = c.cleanPath(from)
to = c.cleanPath(to)
return wfs.Rename(from, to)
}
err = syscall.ENOSYS
return
}
// Unlink is like syscall.Unlink.
func (c *FSContext) Unlink(path string) (err error) {
if wfs, ok := c.fs.(syscallfs.FS); ok {
path = c.cleanPath(path)
return wfs.Unlink(path)
}
err = syscall.ENOSYS
return
}
// Utimes is like syscall.Utimes.
func (c *FSContext) Utimes(path string, atimeNsec, mtimeNsec int64) (err error) {
if wfs, ok := c.fs.(syscallfs.FS); ok {
path = c.cleanPath(path)
return wfs.Utimes(path, atimeNsec, mtimeNsec)
}
err = syscall.ENOSYS
return
}
func (c *FSContext) cleanPath(path string) string {
if len(path) == 0 {
return path
}
// fs.ValidFile cannot be rooted (start with '/')
cleaned := path
if path[0] == '/' {
cleaned = path[1:]
}
cleaned = pathutil.Clean(cleaned) // e.g. "sub/." -> "sub"
return cleaned
}
// FdWriter returns a valid writer for the given file descriptor or nil if syscall.EBADF.
func (c *FSContext) FdWriter(fd uint32) io.Writer {
// Check to see if the file descriptor is available
if f, ok := c.openedFiles.Lookup(fd); !ok {
return nil
} else if writer, ok := f.File.(io.Writer); !ok {
// Go's syscall.Write also returns EBADF if the FD is present, but not writeable
return nil
} else {
return writer
newFD := c.openedFiles.Insert(&FileEntry{Name: pathutil.Base(path), File: f})
return newFD, nil
}
}
// FdReader returns a valid reader for the given file descriptor or nil if syscall.EBADF.
func (c *FSContext) FdReader(fd uint32) io.Reader {
switch fd {
case FdStdout, FdStderr:
return nil // writer, not a readable file.
case FdRoot:
return nil // directory, not a readable file.
}
if f, ok := c.openedFiles.Lookup(fd); !ok {
return nil // TODO: could be a directory not a file.
} else {
return f.File
}
// LookupFile returns a file if it is in the table.
func (c *FSContext) LookupFile(fd uint32) (*FileEntry, bool) {
f, ok := c.openedFiles.Lookup(fd)
return f, ok
}
// CloseFile returns true if a file was opened and closed without error, or false if syscall.EBADF.
func (c *FSContext) CloseFile(fd uint32) bool {
// CloseFile returns any error closing the existing file.
func (c *FSContext) CloseFile(fd uint32) error {
f, ok := c.openedFiles.Lookup(fd)
if !ok {
return false
return syscall.EBADF
}
c.openedFiles.Delete(fd)
if err := f.File.Close(); err != nil {
return false
}
return true
return f.File.Close()
}
// Close implements api.Closer
@@ -447,3 +290,23 @@ func (c *FSContext) Close(context.Context) (err error) {
c.openedFiles = FileTable{}
return
}
// StatFile is a convenience that calls FSContext.LookupFile then fs.File Stat.
// syscall.EBADF is returned on lookup failure.
func StatFile(fsc *FSContext, fd uint32) (stat fs.FileInfo, err error) {
if f, ok := fsc.LookupFile(fd); !ok {
err = syscall.EBADF
} else {
stat, err = f.File.Stat()
}
return
}
// WriterForFile returns a writer for the given file descriptor or nil if not
// opened or not writeable (e.g. a directory or a file not opened for writes).
func WriterForFile(fsc *FSContext, fd uint32) (writer io.Writer) {
if f, ok := fsc.LookupFile(fd); ok {
writer = f.File.(io.Writer)
}
return
}

View File

@@ -7,10 +7,10 @@ import (
"io"
"io/fs"
"os"
"path"
"testing"
"testing/fstest"
"github.com/tetratelabs/wazero/internal/syscallfs"
testfs "github.com/tetratelabs/wazero/internal/testing/fs"
"github.com/tetratelabs/wazero/internal/testing/require"
)
@@ -31,26 +31,31 @@ func TestNewFSContext(t *testing.T) {
embedFS, err := fs.Sub(testdata, "testdata")
require.NoError(t, err)
dirfs, err := syscallfs.NewDirFS(".")
require.NoError(t, err)
// Test various usual configuration for the file system.
tests := []struct {
name string
fs fs.FS
expectOsFile bool
name string
fs syscallfs.FS
}{
{
name: "embed.FS",
fs: embedFS,
fs: syscallfs.Adapt(embedFS),
},
{
name: "os.DirFS",
name: "syscallfs.NewDirFS",
// Don't use "testdata" because it may not be present in
// cross-architecture (a.k.a. scratch) build containers.
fs: os.DirFS("."),
expectOsFile: true,
fs: dirfs,
},
{
name: "syscallfs.NewReadFS",
fs: syscallfs.NewReadFS(dirfs),
},
{
name: "fstest.MapFS",
fs: fstest.MapFS{},
fs: syscallfs.Adapt(fstest.MapFS{}),
},
}
@@ -67,17 +72,11 @@ func TestNewFSContext(t *testing.T) {
require.NotNil(t, rootFile)
require.Equal(t, "/", rootFile.Name)
_, osFile := rootFile.File.(*os.File)
require.Equal(t, tc.expectOsFile, osFile)
f0, err := fsc.OpenFile("/", 0, 0)
require.NoError(t, err)
// Verify that each call to OpenFile returns a different file
// descriptor.
f1, err := fsc.OpenFile("/", 0, 0)
require.NoError(t, err)
require.NotEqual(t, f0, f1)
require.NotEqual(t, FdRoot, f1)
// Verify that file descriptors are reused.
//
@@ -87,119 +86,38 @@ func TestNewFSContext(t *testing.T) {
// test to ensure that our implementation properly reuses descriptor
// numbers but if we were to change the reuse strategy, this test
// would likely break and need to be updated.
require.True(t, fsc.CloseFile(f0))
require.NoError(t, fsc.CloseFile(f1))
f2, err := fsc.OpenFile("/", 0, 0)
require.NoError(t, err)
require.Equal(t, f0, f2)
require.Equal(t, f1, f2)
})
}
}
func TestEmptyFS(t *testing.T) {
testFS := EmptyFS
t.Run("validates path", func(t *testing.T) {
f, err := testFS.Open("/foo.txt")
require.Nil(t, f)
require.EqualError(t, err, "open /foo.txt: invalid argument")
})
t.Run("path not found", func(t *testing.T) {
f, err := testFS.Open("foo.txt")
require.Nil(t, f)
require.EqualError(t, err, "open foo.txt: file does not exist")
})
}
func TestEmptyFSContext(t *testing.T) {
testFS, err := NewFSContext(nil, nil, nil, EmptyFS)
testFS, err := NewFSContext(nil, nil, nil, syscallfs.EmptyFS)
require.NoError(t, err)
expected := &FSContext{fs: EmptyFS}
expected := &FSContext{fs: syscallfs.EmptyFS}
expected.openedFiles.Insert(noopStdin)
expected.openedFiles.Insert(noopStdout)
expected.openedFiles.Insert(noopStderr)
t.Run("OpenFile doesn't affect state", func(t *testing.T) {
fd, err := testFS.OpenFile("foo.txt", os.O_RDONLY, 0)
require.Zero(t, fd)
require.EqualError(t, err, "open foo.txt: file does not exist")
// Ensure this didn't modify state
require.Equal(t, expected, testFS)
})
t.Run("Close closes", func(t *testing.T) {
err := testFS.Close(testCtx)
require.NoError(t, err)
// Closes opened files
require.Equal(t, &FSContext{fs: EmptyFS}, testFS)
require.Equal(t, &FSContext{fs: syscallfs.EmptyFS}, testFS)
})
}
func TestContext_File(t *testing.T) {
embedFS, err := fs.Sub(testdata, "testdata")
require.NoError(t, err)
fsc, err := NewFSContext(nil, nil, nil, embedFS)
require.NoError(t, err)
defer fsc.Close(testCtx)
tests := []struct {
name string
expected string
}{
{
name: "empty.txt",
},
{
name: "test.txt",
expected: "animals\n",
},
{
name: "sub/test.txt",
expected: "greet sub dir\n",
},
{
name: "sub/sub/test.txt",
expected: "greet sub sub dir\n",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(b *testing.T) {
fd, err := fsc.OpenFile(tc.name, os.O_RDONLY, 0)
require.NoError(t, err)
defer fsc.CloseFile(fd)
f, ok := fsc.OpenedFile(fd)
require.True(t, ok)
stat, err := f.File.Stat()
require.NoError(t, err)
// Ensure the name is the basename and matches the stat name.
require.Equal(t, path.Base(tc.name), f.Name)
require.Equal(t, f.Name, stat.Name())
buf := make([]byte, stat.Size())
size, err := f.File.Read(buf)
if err != nil {
require.Equal(t, io.EOF, err)
}
require.Equal(t, stat.Size(), int64(size))
require.Equal(t, tc.expected, string(buf[:size]))
})
}
}
func TestContext_Close(t *testing.T) {
fsc, err := NewFSContext(nil, nil, nil, testfs.FS{"foo": &testfs.File{}})
testFS := syscallfs.Adapt(testfs.FS{"foo": &testfs.File{}})
fsc, err := NewFSContext(nil, nil, nil, testFS)
require.NoError(t, err)
// Verify base case
require.Equal(t, 1+FdRoot, uint32(fsc.openedFiles.Len()))
@@ -219,7 +137,10 @@ func TestContext_Close(t *testing.T) {
func TestContext_Close_Error(t *testing.T) {
file := &testfs.File{CloseErr: errors.New("error closing")}
fsc, err := NewFSContext(nil, nil, nil, testfs.FS{"foo": file})
testFS := syscallfs.Adapt(testfs.FS{"foo": file})
fsc, err := NewFSContext(nil, nil, nil, testFS)
require.NoError(t, err)
// open another file

View File

@@ -8,6 +8,7 @@ import (
"time"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/syscallfs"
"github.com/tetratelabs/wazero/sys"
)
@@ -181,9 +182,9 @@ func NewContext(
}
if fs != nil {
sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, fs)
sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, syscallfs.Adapt(fs))
} else {
sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, EmptyFS)
sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, syscallfs.EmptyFS)
}
return

View File

@@ -6,15 +6,16 @@ import (
"time"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/syscallfs"
testfs "github.com/tetratelabs/wazero/internal/testing/fs"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/sys"
)
func TestContext_FS(t *testing.T) {
sysCtx := DefaultContext(testfs.FS{})
sysCtx := DefaultContext(syscallfs.EmptyFS)
fsc, err := NewFSContext(nil, nil, nil, testfs.FS{})
fsc, err := NewFSContext(nil, nil, nil, syscallfs.EmptyFS)
require.NoError(t, err)
require.Equal(t, fsc, sysCtx.FS())
@@ -50,7 +51,8 @@ func TestDefaultSysContext(t *testing.T) {
require.Equal(t, &ns, sysCtx.nanosleep)
require.Equal(t, platform.NewFakeRandSource(), sysCtx.RandSource())
expectedFS, _ := NewFSContext(nil, nil, nil, testfs.FS{})
testFS := syscallfs.Adapt(testfs.FS{})
expectedFS, _ := NewFSContext(nil, nil, nil, testFS)
expectedOpenedFiles := FileTable{}
expectedOpenedFiles.Insert(noopStdin)

View File

@@ -0,0 +1,77 @@
package syscallfs
import (
"fmt"
"io/fs"
"os"
pathutil "path"
"syscall"
)
// Adapt returns a read-only FS unless the input is already one.
func Adapt(fs fs.FS) FS {
if sys, ok := fs.(FS); ok {
return sys
}
return &adapter{fs}
}
type adapter struct{ fs fs.FS }
// Open implements the same method as documented on fs.FS
func (ro *adapter) Open(name string) (fs.File, error) {
panic(fmt.Errorf("unexpected to call fs.FS.Open(%s)", name))
}
// OpenFile implements FS.OpenFile
func (ro *adapter) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) {
if flag != 0 && flag != os.O_RDONLY {
return nil, syscall.ENOSYS
}
path = cleanPath(path)
f, err := ro.fs.Open(path)
if err != nil {
// wrapped is fine while FS.OpenFile emulates os.OpenFile vs syscall.OpenFile.
return nil, err
}
return maskForReads(f), nil
}
func cleanPath(name string) string {
if len(name) == 0 {
return name
}
// fs.ValidFile cannot be rooted (start with '/')
cleaned := name
if name[0] == '/' {
cleaned = name[1:]
}
cleaned = pathutil.Clean(cleaned) // e.g. "sub/." -> "sub"
return cleaned
}
// Mkdir implements FS.Mkdir
func (ro *adapter) Mkdir(path string, perm fs.FileMode) error {
return syscall.ENOSYS
}
// Rename implements FS.Rename
func (ro *adapter) Rename(from, to string) error {
return syscall.ENOSYS
}
// Rmdir implements FS.Rmdir
func (ro *adapter) Rmdir(path string) error {
return syscall.ENOSYS
}
// Unlink implements FS.Unlink
func (ro *adapter) Unlink(path string) error {
return syscall.ENOSYS
}
// Utimes implements FS.Utimes
func (ro *adapter) Utimes(path string, atimeNsec, mtimeNsec int64) error {
return syscall.ENOSYS
}

View File

@@ -0,0 +1,99 @@
package syscallfs
import (
"errors"
"io/fs"
"os"
pathutil "path"
"syscall"
"testing"
"github.com/tetratelabs/wazero/internal/testing/require"
)
func TestAdapt_MkDir(t *testing.T) {
dir := t.TempDir()
testFS := Adapt(os.DirFS(dir))
err := testFS.Mkdir("mkdir", fs.ModeDir)
require.Equal(t, syscall.ENOSYS, err)
}
func TestAdapt_Rename(t *testing.T) {
tmpDir := t.TempDir()
testFS := Adapt(os.DirFS(tmpDir))
file1 := "file1"
file1Path := pathutil.Join(tmpDir, file1)
file1Contents := []byte{1}
err := os.WriteFile(file1Path, file1Contents, 0o600)
require.NoError(t, err)
file2 := "file2"
file2Path := pathutil.Join(tmpDir, file2)
file2Contents := []byte{2}
err = os.WriteFile(file2Path, file2Contents, 0o600)
require.NoError(t, err)
err = testFS.Rename(file1, file2)
require.Equal(t, syscall.ENOSYS, err)
}
func TestAdapt_Rmdir(t *testing.T) {
dir := t.TempDir()
testFS := Adapt(os.DirFS(dir))
path := "rmdir"
realPath := pathutil.Join(dir, path)
require.NoError(t, os.Mkdir(realPath, 0o700))
err := testFS.Rmdir(path)
require.Equal(t, syscall.ENOSYS, err)
}
func TestAdapt_Unlink(t *testing.T) {
dir := t.TempDir()
testFS := Adapt(os.DirFS(dir))
path := "unlink"
realPath := pathutil.Join(dir, path)
require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
err := testFS.Unlink(path)
require.Equal(t, syscall.ENOSYS, err)
}
func TestAdapt_Utimes(t *testing.T) {
dir := t.TempDir()
testFS := Adapt(os.DirFS(dir))
path := "utimes"
realPath := pathutil.Join(dir, path)
require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
err := testFS.Utimes(path, 1, 1)
require.Equal(t, syscall.ENOSYS, err)
}
func TestAdapt_Open_Read(t *testing.T) {
tmpDir := t.TempDir()
// Create a subdirectory, so we can test reads outside the FS root.
tmpDir = pathutil.Join(tmpDir, t.Name())
require.NoError(t, os.Mkdir(tmpDir, 0o700))
testFS := Adapt(os.DirFS(tmpDir))
testFS_Open_Read(t, tmpDir, testFS)
t.Run("path outside root invalid", func(t *testing.T) {
_, err := testFS.OpenFile("../foo", os.O_RDONLY, 0)
// fs.FS doesn't allow relative path lookups
require.True(t, errors.Is(err, fs.ErrInvalid))
})
}

View File

@@ -29,10 +29,6 @@ func (dir dirFS) Open(name string) (fs.File, error) {
// OpenFile implements FS.OpenFile
func (dir dirFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
}
f, err := os.OpenFile(path.Join(string(dir), name), flag, perm)
if err != nil {
return nil, err
@@ -46,23 +42,12 @@ func (dir dirFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, err
// Mkdir implements FS.Mkdir
func (dir dirFS) Mkdir(name string, perm fs.FileMode) error {
if !fs.ValidPath(name) {
return &fs.PathError{Op: "mkdir", Path: name, Err: fs.ErrInvalid}
}
err := os.Mkdir(path.Join(string(dir), name), perm)
return adjustMkdirError(err)
}
// Rename implements FS.Rename
func (dir dirFS) Rename(from, to string) error {
if !fs.ValidPath(from) {
return syscall.EINVAL
}
if !fs.ValidPath(to) {
return syscall.EINVAL
}
if from == to {
return nil
}
@@ -71,31 +56,18 @@ func (dir dirFS) Rename(from, to string) error {
// Rmdir implements FS.Rmdir
func (dir dirFS) Rmdir(name string) error {
if !fs.ValidPath(name) {
return syscall.EINVAL
}
err := syscall.Rmdir(path.Join(string(dir), name))
return adjustRmdirError(err)
}
// Unlink implements FS.Unlink
func (dir dirFS) Unlink(name string) error {
if !fs.ValidPath(name) {
return syscall.EINVAL
}
err := syscall.Unlink(path.Join(string(dir), name))
return adjustUnlinkError(err)
}
// Utimes implements FS.Utimes
func (dir dirFS) Utimes(name string, atimeNsec, mtimeNsec int64) error {
if !fs.ValidPath(name) {
return syscall.EINVAL
}
return syscall.UtimesNano(path.Join(string(dir), name), []syscall.Timespec{
syscall.NsecToTimespec(atimeNsec),
syscall.NsecToTimespec(mtimeNsec),

View File

@@ -4,7 +4,7 @@ import (
"errors"
"io/fs"
"os"
"path"
pathutil "path"
"runtime"
"syscall"
"testing"
@@ -18,7 +18,7 @@ func TestDirFS_MkDir(t *testing.T) {
testFS := dirFS(dir)
name := "mkdir"
realPath := path.Join(dir, name)
realPath := pathutil.Join(dir, name)
t.Run("doesn't exist", func(t *testing.T) {
require.NoError(t, testFS.Mkdir(name, fs.ModeDir))
@@ -48,7 +48,7 @@ func TestDirFS_Rename(t *testing.T) {
testFS := dirFS(tmpDir)
file1 := "file1"
file1Path := path.Join(tmpDir, file1)
file1Path := pathutil.Join(tmpDir, file1)
err := os.WriteFile(file1Path, []byte{1}, 0o600)
require.NoError(t, err)
@@ -60,13 +60,13 @@ func TestDirFS_Rename(t *testing.T) {
testFS := dirFS(tmpDir)
file1 := "file1"
file1Path := path.Join(tmpDir, file1)
file1Path := pathutil.Join(tmpDir, file1)
file1Contents := []byte{1}
err := os.WriteFile(file1Path, file1Contents, 0o600)
require.NoError(t, err)
file2 := "file2"
file2Path := path.Join(tmpDir, file2)
file2Path := pathutil.Join(tmpDir, file2)
err = testFS.Rename(file1, file2)
require.NoError(t, err)
@@ -83,11 +83,11 @@ func TestDirFS_Rename(t *testing.T) {
testFS := dirFS(tmpDir)
dir1 := "dir1"
dir1Path := path.Join(tmpDir, dir1)
dir1Path := pathutil.Join(tmpDir, dir1)
require.NoError(t, os.Mkdir(dir1Path, 0o700))
dir2 := "dir2"
dir2Path := path.Join(tmpDir, dir2)
dir2Path := pathutil.Join(tmpDir, dir2)
err := testFS.Rename(dir1, dir2)
require.NoError(t, err)
@@ -104,11 +104,11 @@ func TestDirFS_Rename(t *testing.T) {
testFS := dirFS(tmpDir)
dir1 := "dir1"
dir1Path := path.Join(tmpDir, dir1)
dir1Path := pathutil.Join(tmpDir, dir1)
require.NoError(t, os.Mkdir(dir1Path, 0o700))
dir2 := "dir2"
dir2Path := path.Join(tmpDir, dir2)
dir2Path := pathutil.Join(tmpDir, dir2)
// write a file to that path
err := os.WriteFile(dir2Path, []byte{2}, 0o600)
@@ -131,13 +131,13 @@ func TestDirFS_Rename(t *testing.T) {
testFS := dirFS(tmpDir)
file1 := "file1"
file1Path := path.Join(tmpDir, file1)
file1Path := pathutil.Join(tmpDir, file1)
file1Contents := []byte{1}
err := os.WriteFile(file1Path, file1Contents, 0o600)
require.NoError(t, err)
dir1 := "dir1"
dir1Path := path.Join(tmpDir, dir1)
dir1Path := pathutil.Join(tmpDir, dir1)
require.NoError(t, os.Mkdir(dir1Path, 0o700))
err = testFS.Rename(file1, dir1)
@@ -148,18 +148,18 @@ func TestDirFS_Rename(t *testing.T) {
testFS := dirFS(tmpDir)
dir1 := "dir1"
dir1Path := path.Join(tmpDir, dir1)
dir1Path := pathutil.Join(tmpDir, dir1)
require.NoError(t, os.Mkdir(dir1Path, 0o700))
// add a file to that directory
file1 := "file1"
file1Path := path.Join(dir1Path, file1)
file1Path := pathutil.Join(dir1Path, file1)
file1Contents := []byte{1}
err := os.WriteFile(file1Path, file1Contents, 0o600)
require.NoError(t, err)
dir2 := "dir2"
dir2Path := path.Join(tmpDir, dir2)
dir2Path := pathutil.Join(tmpDir, dir2)
require.NoError(t, os.Mkdir(dir2Path, 0o700))
err = testFS.Rename(dir1, dir2)
@@ -175,7 +175,7 @@ func TestDirFS_Rename(t *testing.T) {
require.Equal(t, syscall.ENOENT, errors.Unwrap(err))
// Show the file inside that directory moved
s, err := os.Stat(path.Join(dir2Path, file1))
s, err := os.Stat(pathutil.Join(dir2Path, file1))
require.NoError(t, err)
require.False(t, s.IsDir())
})
@@ -184,13 +184,13 @@ func TestDirFS_Rename(t *testing.T) {
testFS := dirFS(tmpDir)
file1 := "file1"
file1Path := path.Join(tmpDir, file1)
file1Path := pathutil.Join(tmpDir, file1)
file1Contents := []byte{1}
err := os.WriteFile(file1Path, file1Contents, 0o600)
require.NoError(t, err)
file2 := "file2"
file2Path := path.Join(tmpDir, file2)
file2Path := pathutil.Join(tmpDir, file2)
file2Contents := []byte{2}
err = os.WriteFile(file2Path, file2Contents, 0o600)
require.NoError(t, err)
@@ -212,7 +212,7 @@ func TestDirFS_Rename(t *testing.T) {
testFS := dirFS(tmpDir)
dir1 := "dir1"
dir1Path := path.Join(tmpDir, dir1)
dir1Path := pathutil.Join(tmpDir, dir1)
require.NoError(t, os.Mkdir(dir1Path, 0o700))
err := testFS.Rename(dir1, dir1)
@@ -227,7 +227,7 @@ func TestDirFS_Rename(t *testing.T) {
testFS := dirFS(tmpDir)
file1 := "file1"
file1Path := path.Join(tmpDir, file1)
file1Path := pathutil.Join(tmpDir, file1)
file1Contents := []byte{1}
err := os.WriteFile(file1Path, file1Contents, 0o600)
require.NoError(t, err)
@@ -247,7 +247,7 @@ func TestDirFS_Rmdir(t *testing.T) {
testFS := dirFS(dir)
name := "rmdir"
realPath := path.Join(dir, name)
realPath := pathutil.Join(dir, name)
t.Run("doesn't exist", func(t *testing.T) {
err := testFS.Rmdir(name)
@@ -256,7 +256,7 @@ func TestDirFS_Rmdir(t *testing.T) {
t.Run("dir not empty", func(t *testing.T) {
require.NoError(t, os.Mkdir(realPath, 0o700))
fileInDir := path.Join(realPath, "file")
fileInDir := pathutil.Join(realPath, "file")
require.NoError(t, os.WriteFile(fileInDir, []byte{}, 0o600))
err := testFS.Rmdir(name)
@@ -287,7 +287,7 @@ func TestDirFS_Unlink(t *testing.T) {
testFS := dirFS(dir)
name := "unlink"
realPath := path.Join(dir, name)
realPath := pathutil.Join(dir, name)
t.Run("doesn't exist", func(t *testing.T) {
err := testFS.Unlink(name)
@@ -323,7 +323,18 @@ func TestDirFS_Utimes(t *testing.T) {
func TestDirFS_Open_Read(t *testing.T) {
tmpDir := t.TempDir()
testFS := dirFS(tmpDir)
// Create a subdirectory, so we can test reads outside the FS root.
tmpDir = pathutil.Join(tmpDir, t.Name())
require.NoError(t, os.Mkdir(tmpDir, 0o700))
testFS := Adapt(dirFS(tmpDir))
testFS_Open_Read(t, tmpDir, testFS)
t.Run("path outside root valid", func(t *testing.T) {
_, err := testFS.OpenFile("../foo", os.O_RDONLY, 0)
// syscall.FS allows relative path lookups
require.True(t, errors.Is(err, fs.ErrNotExist))
})
}

View File

@@ -0,0 +1,48 @@
package syscallfs
import (
"fmt"
"io/fs"
"syscall"
)
// EmptyFS is an FS that returns syscall.ENOENT for all read functions, and
// syscall.ENOSYS otherwise.
var EmptyFS FS = unsupported{}
type unsupported struct{}
// Open implements the same method as documented on fs.FS
func (unsupported) Open(name string) (fs.File, error) {
panic(fmt.Errorf("unexpected to call fs.FS.Open(%s)", name))
}
// OpenFile implements FS.OpenFile
func (unsupported) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) {
return nil, &fs.PathError{Op: "open", Path: path, Err: syscall.ENOENT}
}
// Mkdir implements FS.Mkdir
func (unsupported) Mkdir(path string, perm fs.FileMode) error {
return syscall.ENOSYS
}
// Rename implements FS.Rename
func (unsupported) Rename(from, to string) error {
return syscall.ENOSYS
}
// Rmdir implements FS.Rmdir
func (unsupported) Rmdir(path string) error {
return syscall.ENOSYS
}
// Unlink implements FS.Unlink
func (unsupported) Unlink(path string) error {
return syscall.ENOSYS
}
// Utimes implements FS.Utimes
func (unsupported) Utimes(path string, atimeNsec, mtimeNsec int64) error {
return syscall.ENOSYS
}

View File

@@ -22,15 +22,15 @@ func (ro *readFS) Open(name string) (fs.File, error) {
}
// OpenFile implements FS.OpenFile
func (ro *readFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) {
func (ro *readFS) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) {
if flag == 0 || flag == os.O_RDONLY {
return ro.fs.OpenFile(name, flag, perm)
return ro.fs.OpenFile(path, flag, perm)
}
return nil, syscall.ENOSYS
}
// Mkdir implements FS.Mkdir
func (ro *readFS) Mkdir(name string, perm fs.FileMode) error {
func (ro *readFS) Mkdir(path string, perm fs.FileMode) error {
return syscall.ENOSYS
}
@@ -40,16 +40,16 @@ func (ro *readFS) Rename(from, to string) error {
}
// Rmdir implements FS.Rmdir
func (ro *readFS) Rmdir(name string) error {
func (ro *readFS) Rmdir(path string) error {
return syscall.ENOSYS
}
// Unlink implements FS.Unlink
func (ro *readFS) Unlink(name string) error {
func (ro *readFS) Unlink(path string) error {
return syscall.ENOSYS
}
// Utimes implements FS.Utimes
func (ro *readFS) Utimes(name string, atimeNsec, mtimeNsec int64) error {
return ro.fs.Utimes(name, atimeNsec, mtimeNsec)
func (ro *readFS) Utimes(path string, atimeNsec, mtimeNsec int64) error {
return syscall.ENOSYS
}

View File

@@ -3,7 +3,7 @@ package syscallfs
import (
"io/fs"
"os"
"path"
pathutil "path"
"syscall"
"testing"
@@ -24,13 +24,13 @@ func TestReadFS_Rename(t *testing.T) {
testFS := NewReadFS(dirFS(tmpDir))
file1 := "file1"
file1Path := path.Join(tmpDir, file1)
file1Path := pathutil.Join(tmpDir, file1)
file1Contents := []byte{1}
err := os.WriteFile(file1Path, file1Contents, 0o600)
require.NoError(t, err)
file2 := "file2"
file2Path := path.Join(tmpDir, file2)
file2Path := pathutil.Join(tmpDir, file2)
file2Contents := []byte{2}
err = os.WriteFile(file2Path, file2Contents, 0o600)
require.NoError(t, err)
@@ -44,11 +44,11 @@ func TestReadFS_Rmdir(t *testing.T) {
testFS := NewReadFS(dirFS(dir))
name := "rmdir"
realPath := path.Join(dir, name)
path := "rmdir"
realPath := pathutil.Join(dir, path)
require.NoError(t, os.Mkdir(realPath, 0o700))
err := testFS.Rmdir(name)
err := testFS.Rmdir(path)
require.Equal(t, syscall.ENOSYS, err)
}
@@ -57,20 +57,25 @@ func TestReadFS_Unlink(t *testing.T) {
testFS := NewReadFS(dirFS(dir))
name := "unlink"
realPath := path.Join(dir, name)
path := "unlink"
realPath := pathutil.Join(dir, path)
require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
err := testFS.Unlink(name)
err := testFS.Unlink(path)
require.Equal(t, syscall.ENOSYS, err)
}
func TestReadFS_Utimes(t *testing.T) {
tmpDir := t.TempDir()
dir := t.TempDir()
testFS := NewReadFS(dirFS(tmpDir))
testFS := NewReadFS(dirFS(dir))
testFS_Utimes(t, tmpDir, testFS)
path := "utimes"
realPath := pathutil.Join(dir, path)
require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
err := testFS.Utimes(path, 1, 1)
require.Equal(t, syscall.ENOSYS, err)
}
func TestReadFS_Open_Read(t *testing.T) {

View File

@@ -3,6 +3,7 @@ package syscallfs
import (
"io"
"io/fs"
"os"
)
// FS is a writeable fs.FS bridge backed by syscall functions needed for ABI
@@ -19,13 +20,13 @@ type FS interface {
// OpenFile is similar to os.OpenFile, except the path is relative to this
// file system.
OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error)
OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error)
// ^^ TODO: Consider syscall.Open, though this implies defining and
// coercing flags and perms similar to what is done in os.OpenFile.
// Mkdir is similar to os.Mkdir, except the path is relative to this file
// system.
Mkdir(name string, perm fs.FileMode) error
Mkdir(path string, perm fs.FileMode) error
// ^^ TODO: Consider syscall.Mkdir, though this implies defining and
// coercing flags and perms similar to what is done in os.Mkdir.
@@ -141,3 +142,14 @@ func maskForReads(f fs.File) fs.File {
panic("BUG: unhandled pattern")
}
}
// StatPath is a convenience that calls FS.OpenFile until there is a stat
// method.
func StatPath(fs FS, path string) (fs.FileInfo, error) {
f, err := fs.OpenFile(path, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
defer f.Close()
return f.Stat()
}

View File

@@ -155,14 +155,14 @@ func TestCallContext_Close(t *testing.T) {
// We use side effects to determine if Close in fact called Context.Close (without repeating sys_test.go).
// One side effect of Context.Close is that it clears the openedFiles map. Verify our base case.
_, ok := fsCtx.OpenedFile(3)
_, ok := fsCtx.LookupFile(3)
require.True(t, ok, "sysCtx.openedFiles was empty")
// Closing should not err.
require.NoError(t, m.Close(testCtx))
// Verify our intended side-effect
_, ok = fsCtx.OpenedFile(3)
_, ok = fsCtx.LookupFile(3)
require.False(t, ok, "expected no opened files")
// Verify no error closing again.
@@ -184,7 +184,7 @@ func TestCallContext_Close(t *testing.T) {
require.EqualError(t, m.Close(testCtx), "error closing")
// Verify our intended side-effect
_, ok := fsCtx.OpenedFile(3)
_, ok := fsCtx.LookupFile(3)
require.False(t, ok, "expected no opened files")
})
}
@@ -251,14 +251,14 @@ func TestCallContext_CallDynamic(t *testing.T) {
// We use side effects to determine if Close in fact called Context.Close (without repeating sys_test.go).
// One side effect of Context.Close is that it clears the openedFiles map. Verify our base case.
_, ok := fsCtx.OpenedFile(3)
_, ok := fsCtx.LookupFile(3)
require.True(t, ok, "sysCtx.openedFiles was empty")
// Closing should not err.
require.NoError(t, m.Close(testCtx))
// Verify our intended side-effect
_, ok = fsCtx.OpenedFile(3)
_, ok = fsCtx.LookupFile(3)
require.False(t, ok, "expected no opened files")
// Verify no error closing again.
@@ -271,7 +271,8 @@ func TestCallContext_CallDynamic(t *testing.T) {
sysCtx := sys.DefaultContext(testFS)
fsCtx := sysCtx.FS()
_, err := fsCtx.OpenFile("/foo", os.O_RDONLY, 0)
path := "/foo"
_, err := fsCtx.OpenFile(path, os.O_RDONLY, 0)
require.NoError(t, err)
m, err := s.Instantiate(context.Background(), ns, &Module{}, t.Name(), sysCtx)
@@ -280,7 +281,7 @@ func TestCallContext_CallDynamic(t *testing.T) {
require.EqualError(t, m.Close(testCtx), "error closing")
// Verify our intended side-effect
_, ok := fsCtx.OpenedFile(3)
_, ok := fsCtx.LookupFile(3)
require.False(t, ok, "expected no opened files")
})
}