wasi: implements path_(create|remove)_directory path_unlink_file (#976)

This implements path_(create|remove)_directory path_unlink_file in wasi, particularly needed to use TinyGo tests to verify our interpretation of WASI. Use of this requires the experimental `writefs.DirFS`.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-12-30 07:30:21 +08:00
committed by GitHub
parent 62be68dfc5
commit 6d53a0033a
6 changed files with 590 additions and 76 deletions

View File

@@ -1030,7 +1030,7 @@ var fdWrite = newHostFunc(
"fd", "iovs", "iovs_len", "result.nwritten",
)
func fdWriteFn(ctx context.Context, mod api.Module, params []uint64) Errno {
func fdWriteFn(_ context.Context, mod api.Module, params []uint64) Errno {
mem := mod.Memory()
fsc := mod.(*wasm.CallContext).Sys.FS()
@@ -1078,16 +1078,54 @@ func fdWriteFn(ctx context.Context, mod api.Module, params []uint64) Errno {
return ErrnoSuccess
}
// pathCreateDirectory is the WASI function named PathCreateDirectoryName
// which creates a directory.
// pathCreateDirectory is the WASI function named PathCreateDirectoryName which
// creates a directory.
//
// # Parameters
//
// - fd: file descriptor of a directory that `path` is relative to
// - path: offset in api.Memory to read the path string from
// - pathLen: length of `path`
//
// # Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// - ErrnoBadf: `fd` is invalid
// - ErrnoNoent: `path` does not exist.
// - ErrnoNotdir: `path` is a file
//
// # Notes
// - This is similar to mkdirat in POSIX.
// See https://linux.die.net/man/2/mkdirat
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_create_directoryfd-fd-path-string---errno
var pathCreateDirectory = stubFunction(
PathCreateDirectoryName,
var pathCreateDirectory = newHostFunc(
PathCreateDirectoryName, pathCreateDirectoryFn,
[]wasm.ValueType{i32, i32, i32},
"fd", "path", "path_len",
)
func pathCreateDirectoryFn(_ context.Context, mod api.Module, params []uint64) Errno {
fsc := mod.(*wasm.CallContext).Sys.FS()
dirfd := uint32(params[0])
path := uint32(params[1])
pathLen := uint32(params[2])
pathName, errno := atPath(fsc, mod.Memory(), dirfd, path, pathLen)
if errno != ErrnoSuccess {
return errno
}
if fd, err := fsc.Mkdir(pathName, 0o700); err != nil {
return ToErrno(err)
} else {
_ = fsc.CloseFile(fd)
}
return ErrnoSuccess
}
// pathFilestatGet is the WASI function named PathFilestatGetName which
// returns the stat attributes of a file or directory.
//
@@ -1272,29 +1310,13 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) Errno {
fdflags := uint16(params[7])
resultOpenedFd := uint32(params[8])
// Note: We don't handle AT_FDCWD, as that's resolved in the compiler.
// There's no working directory function in WASI, so CWD cannot be handled
// here in any way except assuming it is "/".
//
// See https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/sources/at_fdcwd.c#L24-L26
if _, ok := fsc.OpenedFile(dirfd); !ok {
return ErrnoBadf
}
b, ok := mod.Memory().Read(path, pathLen)
if !ok {
return ErrnoFault
pathName, errno := atPath(fsc, mod.Memory(), dirfd, path, pathLen)
if errno != ErrnoSuccess {
return errno
}
fileOpenFlags, isDir := openFlags(oflags, fdflags)
// TODO: path is not precise here, as it should be a path relative to the
// FD, which isn't always rootFD (3). This means the path for Open may need
// to be built up. For example, if dirfd represents "/tmp/foo" and
// path="bar", this should open "/tmp/foo/bar" not "/bar".
//
// See https://linux.die.net/man/2/openat
pathName := string(b)
var newFD uint32
var err error
if isDir && oflags&O_CREAT != 0 {
@@ -1321,6 +1343,30 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) Errno {
return ErrnoSuccess
}
// Note: We don't handle AT_FDCWD, as that's resolved in the compiler.
// There's no working directory function in WASI, so CWD cannot be handled
// here in any way except assuming it is "/".
//
// See https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/sources/at_fdcwd.c#L24-L26
//
// TODO: path is not precise here, as it should be a path relative to the
// FD, which isn't always rootFD (3). This means the path for Open may need
// to be built up. For example, if dirfd represents "/tmp/foo" and
// path="bar", this should open "/tmp/foo/bar" not "/bar".
//
// See https://linux.die.net/man/2/openat
func atPath(fsc *sys.FSContext, mem api.Memory, dirfd, path, pathLen uint32) (string, Errno) {
if _, ok := fsc.OpenedFile(dirfd); !ok {
return "", ErrnoBadf
}
b, ok := mem.Read(path, pathLen)
if !ok {
return "", ErrnoFault
}
return string(b), ErrnoSuccess
}
func openFlags(oflags, fdflags uint16) (openFlags int, isDir bool) {
isDir = oflags&O_DIRECTORY != 0
openFlags = os.O_RDONLY
@@ -1366,16 +1412,53 @@ var pathReadlink = stubFunction(
"fd", "path", "path_len", "buf", "buf_len", "result.bufused",
)
// pathRemoveDirectory is the WASI function named PathRemoveDirectoryName
// which removes a directory.
// pathRemoveDirectory is the WASI function named PathRemoveDirectoryName which
// removes a directory.
//
// # Parameters
//
// - fd: file descriptor of a directory that `path` is relative to
// - path: offset in api.Memory to read the path string from
// - pathLen: length of `path`
//
// # Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// - ErrnoBadf: `fd` is invalid
// - ErrnoNoent: `path` does not exist.
// - ErrnoNotempty: `path` is not empty
// - ErrnoNotdir: `path` is a file
//
// # Notes
// - This is similar to unlinkat with AT_REMOVEDIR in POSIX.
// See https://linux.die.net/man/2/unlinkat
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_remove_directoryfd-fd-path-string---errno
var pathRemoveDirectory = stubFunction(
PathRemoveDirectoryName,
var pathRemoveDirectory = newHostFunc(
PathRemoveDirectoryName, pathRemoveDirectoryFn,
[]wasm.ValueType{i32, i32, i32},
"fd", "path", "path_len",
)
func pathRemoveDirectoryFn(_ context.Context, mod api.Module, params []uint64) Errno {
fsc := mod.(*wasm.CallContext).Sys.FS()
dirfd := uint32(params[0])
path := uint32(params[1])
pathLen := uint32(params[2])
pathName, errno := atPath(fsc, mod.Memory(), dirfd, path, pathLen)
if errno != ErrnoSuccess {
return errno
}
if err := fsc.Rmdir(pathName); err != nil {
return ToErrno(err)
}
return ErrnoSuccess
}
// pathRename is the WASI function named PathRenameName which renames a
// file or directory.
var pathRename = stubFunction(
@@ -1394,16 +1477,52 @@ var pathSymlink = stubFunction(
"old_path", "old_path_len", "fd", "new_path", "new_path_len",
)
// pathUnlinkFile is the WASI function named PathUnlinkFileName which
// unlinks a file.
// pathUnlinkFile is the WASI function named PathUnlinkFileName which unlinks a
// file.
//
// # Parameters
//
// - fd: file descriptor of a directory that `path` is relative to
// - path: offset in api.Memory to read the path string from
// - pathLen: length of `path`
//
// # Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// - ErrnoBadf: `fd` is invalid
// - ErrnoNoent: `path` does not exist.
// - ErrnoIsdir: `path` is a directory
//
// # Notes
// - This is similar to unlinkat without AT_REMOVEDIR in POSIX.
// See https://linux.die.net/man/2/unlinkat
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_unlink_filefd-fd-path-string---errno
var pathUnlinkFile = stubFunction(
PathUnlinkFileName,
var pathUnlinkFile = newHostFunc(
PathUnlinkFileName, pathUnlinkFileFn,
[]wasm.ValueType{i32, i32, i32},
"fd", "path", "path_len",
)
func pathUnlinkFileFn(_ context.Context, mod api.Module, params []uint64) Errno {
fsc := mod.(*wasm.CallContext).Sys.FS()
dirfd := uint32(params[0])
path := uint32(params[1])
pathLen := uint32(params[2])
pathName, errno := atPath(fsc, mod.Memory(), dirfd, path, pathLen)
if errno != ErrnoSuccess {
return errno
}
if err := fsc.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) {

View File

@@ -3,17 +3,20 @@ package wasi_snapshot_preview1_test
import (
"bytes"
_ "embed"
"fmt"
"io"
"io/fs"
"math"
"os"
"path"
"runtime"
"testing"
"testing/fstest"
"time"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental/writefs"
"github.com/tetratelabs/wazero/internal/leb128"
"github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/testing/require"
@@ -1887,13 +1890,132 @@ func Test_fdWrite_Errors(t *testing.T) {
}
}
// Test_pathCreateDirectory only tests it is stubbed for GrainLang per #271
func Test_pathCreateDirectory(t *testing.T) {
log := requireErrnoNosys(t, PathCreateDirectoryName, 0, 0, 0)
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(writefs.DirFS(tmpDir)))
defer r.Close(testCtx)
// set up the initial memory to include the path name starting at an offset.
pathName := "wazero"
realPath := path.Join(tmpDir, pathName)
ok := mod.Memory().Write(0, append([]byte{'?'}, pathName...))
require.True(t, ok)
dirFD := sys.FdRoot
name := 1
nameLen := len(pathName)
requireErrno(t, ErrnoSuccess, mod, PathCreateDirectoryName, uint64(dirFD), uint64(name), uint64(nameLen))
require.Equal(t, `
--> wasi_snapshot_preview1.path_create_directory(fd=0,path=)
<-- errno=ENOSYS
`, log)
==> wasi_snapshot_preview1.path_create_directory(fd=3,path=wazero)
<== errno=ESUCCESS
`, "\n"+log.String())
// ensure the directory was created
stat, err := os.Stat(realPath)
require.NoError(t, err)
require.True(t, stat.IsDir())
require.Equal(t, pathName, stat.Name())
}
func Test_pathCreateDirectory_Errors(t *testing.T) {
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(writefs.DirFS(tmpDir)))
defer r.Close(testCtx)
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)
tests := []struct {
name, pathName string
fd, path, pathLen uint32
expectedErrno Errno
expectedLog string
}{
{
name: "invalid fd",
fd: 42, // arbitrary invalid fd
expectedErrno: ErrnoBadf,
expectedLog: `
==> wasi_snapshot_preview1.path_create_directory(fd=42,path=)
<== errno=EBADF
`,
},
{
name: "out-of-memory reading path",
fd: sys.FdRoot,
path: mod.Memory().Size(),
pathLen: 1,
expectedErrno: ErrnoFault,
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
`,
},
{
name: "out-of-memory reading pathLen",
fd: sys.FdRoot,
path: 0,
pathLen: mod.Memory().Size() + 1, // path is in the valid memory range, but pathLen is OOM for path
expectedErrno: ErrnoFault,
expectedLog: `
==> wasi_snapshot_preview1.path_create_directory(fd=3,path=OOM(0,65537))
<== errno=EFAULT
`,
},
{
name: "file exists",
fd: sys.FdRoot,
pathName: file,
path: 0,
pathLen: uint32(len(file)),
expectedErrno: ErrnoExist,
expectedLog: `
==> wasi_snapshot_preview1.path_create_directory(fd=3,path=file)
<== errno=EEXIST
`,
},
{
name: "dir exists",
fd: sys.FdRoot,
pathName: dir,
path: 0,
pathLen: uint32(len(dir)),
expectedErrno: ErrnoExist,
expectedLog: `
==> wasi_snapshot_preview1.path_create_directory(fd=3,path=dir)
<== errno=EEXIST
`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
defer log.Reset()
mod.Memory().Write(tc.path, []byte(tc.pathName))
requireErrno(t, tc.expectedErrno, mod, PathCreateDirectoryName, uint64(tc.fd), uint64(tc.path), uint64(tc.pathLen))
require.Equal(t, tc.expectedLog, "\n"+log.String())
})
}
}
func Test_pathFilestatGet(t *testing.T) {
@@ -1915,8 +2037,6 @@ func Test_pathFilestatGet(t *testing.T) {
// open both paths without using WASI
fsc := mod.(*wasm.CallContext).Sys.FS()
rootFd := uint32(3) // after stderr
fileFd, err := fsc.OpenFile(file, os.O_RDONLY, 0)
require.NoError(t, err)
@@ -1932,7 +2052,7 @@ func Test_pathFilestatGet(t *testing.T) {
}{
{
name: "file under root",
fd: rootFd,
fd: sys.FdRoot,
memory: initialMemoryFile,
pathLen: 1,
resultFilestat: 2,
@@ -1976,7 +2096,7 @@ func Test_pathFilestatGet(t *testing.T) {
},
{
name: "dir under root",
fd: rootFd,
fd: sys.FdRoot,
memory: initialMemoryDir,
pathLen: 1,
resultFilestat: 2,
@@ -2019,7 +2139,7 @@ func Test_pathFilestatGet(t *testing.T) {
},
{
name: "path under root doesn't exist",
fd: rootFd,
fd: sys.FdRoot,
memory: initialMemoryNotExists,
pathLen: 1,
resultFilestat: 2,
@@ -2055,7 +2175,7 @@ func Test_pathFilestatGet(t *testing.T) {
},
{
name: "path is out of memory",
fd: rootFd,
fd: sys.FdRoot,
memory: initialMemoryFile,
pathLen: memorySize,
expectedErrno: ErrnoNametoolong,
@@ -2066,7 +2186,7 @@ func Test_pathFilestatGet(t *testing.T) {
},
{
name: "resultFilestat exceeds the maximum valid address by 1",
fd: rootFd,
fd: sys.FdRoot,
memory: initialMemoryFile,
pathLen: 1,
resultFilestat: memorySize - 64 + 1,
@@ -2117,8 +2237,7 @@ func Test_pathLink(t *testing.T) {
}
func Test_pathOpen(t *testing.T) {
rootFD := uint32(3) // after 0, 1, and 2, that are stdin/out/err
expectedFD := rootFD + 1
expectedFD := sys.FdRoot + 1
// set up the initial memory to include the path name starting at an offset.
pathName := "wazero"
initialMemory := append([]byte{'?'}, pathName...)
@@ -2147,7 +2266,7 @@ func Test_pathOpen(t *testing.T) {
ok := mod.Memory().Write(0, initialMemory)
require.True(t, ok)
requireErrno(t, ErrnoSuccess, mod, PathOpenName, uint64(rootFD), uint64(dirflags), uint64(path),
requireErrno(t, ErrnoSuccess, mod, PathOpenName, uint64(sys.FdRoot), uint64(dirflags), uint64(path),
uint64(pathLen), uint64(oflags), fsRightsBase, fsRightsInheriting, uint64(fdflags), uint64(resultOpenedFd))
require.Equal(t, `
==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=wazero,oflags=,fs_rights_base=FD_DATASYNC,fs_rights_inheriting=FD_READ,fdflags=)
@@ -2291,13 +2410,158 @@ func Test_pathReadlink(t *testing.T) {
`, log)
}
// Test_pathRemoveDirectory only tests it is stubbed for GrainLang per #271
func Test_pathRemoveDirectory(t *testing.T) {
log := requireErrnoNosys(t, PathRemoveDirectoryName, 0, 0, 0)
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(writefs.DirFS(tmpDir)))
defer r.Close(testCtx)
// set up the initial memory to include the path name starting at an offset.
pathName := "wazero"
realPath := path.Join(tmpDir, pathName)
ok := mod.Memory().Write(0, append([]byte{'?'}, pathName...))
require.True(t, ok)
// create the directory
err := os.Mkdir(realPath, 0o700)
require.NoError(t, err)
dirFD := sys.FdRoot
name := 1
nameLen := len(pathName)
requireErrno(t, ErrnoSuccess, mod, PathRemoveDirectoryName, uint64(dirFD), uint64(name), uint64(nameLen))
require.Equal(t, `
--> wasi_snapshot_preview1.path_remove_directory(fd=0,path=)
<-- errno=ENOSYS
`, log)
==> wasi_snapshot_preview1.path_remove_directory(fd=3,path=wazero)
<== errno=ESUCCESS
`, "\n"+log.String())
// ensure the directory was removed
_, err = os.Stat(realPath)
require.Error(t, err)
}
func Test_pathRemoveDirectory_Errors(t *testing.T) {
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(writefs.DirFS(tmpDir)))
defer r.Close(testCtx)
file := "file"
err := os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
require.NoError(t, err)
dirNotEmpty := "notempty"
err = os.Mkdir(path.Join(tmpDir, dirNotEmpty), 0o700)
require.NoError(t, err)
dir := "dir"
err = os.Mkdir(path.Join(tmpDir, dirNotEmpty, dir), 0o700)
require.NoError(t, err)
tests := []struct {
name, pathName string
fd, path, pathLen uint32
expectedErrno Errno
expectedLog string
}{
{
name: "invalid fd",
fd: 42, // arbitrary invalid fd
expectedErrno: ErrnoBadf,
expectedLog: `
==> wasi_snapshot_preview1.path_remove_directory(fd=42,path=)
<== errno=EBADF
`,
},
{
name: "out-of-memory reading path",
fd: sys.FdRoot,
path: mod.Memory().Size(),
pathLen: 1,
expectedErrno: ErrnoFault,
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
`,
},
{
name: "out-of-memory reading pathLen",
fd: sys.FdRoot,
path: 0,
pathLen: mod.Memory().Size() + 1, // path is in the valid memory range, but pathLen is OOM for path
expectedErrno: ErrnoFault,
expectedLog: `
==> wasi_snapshot_preview1.path_remove_directory(fd=3,path=OOM(0,65537))
<== errno=EFAULT
`,
},
{
name: "no such file exists",
fd: sys.FdRoot,
pathName: file,
path: 0,
pathLen: uint32(len(file) - 1),
expectedErrno: ErrnoNoent,
expectedLog: `
==> wasi_snapshot_preview1.path_remove_directory(fd=3,path=fil)
<== errno=ENOENT
`,
},
{
name: "file not dir",
fd: sys.FdRoot,
pathName: file,
path: 0,
pathLen: uint32(len(file)),
expectedErrno: errNotDir(),
expectedLog: fmt.Sprintf(`
==> wasi_snapshot_preview1.path_remove_directory(fd=3,path=file)
<== errno=%s
`, ErrnoName(errNotDir())),
},
{
name: "dir not empty",
fd: sys.FdRoot,
pathName: dirNotEmpty,
path: 0,
pathLen: uint32(len(dirNotEmpty)),
expectedErrno: ErrnoNotempty,
expectedLog: `
==> wasi_snapshot_preview1.path_remove_directory(fd=3,path=notempty)
<== errno=ENOTEMPTY
`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
defer log.Reset()
mod.Memory().Write(tc.path, []byte(tc.pathName))
requireErrno(t, tc.expectedErrno, mod, PathRemoveDirectoryName, uint64(tc.fd), uint64(tc.path), uint64(tc.pathLen))
require.Equal(t, tc.expectedLog, "\n"+log.String())
})
}
}
func errNotDir() Errno {
if runtime.GOOS == "windows" {
// As of Go 1.19, Windows maps syscall.ENOTDIR to syscall.ENOENT
return ErrnoNoent
}
return ErrnoNotdir
}
// Test_pathRename only tests it is stubbed for GrainLang per #271
@@ -2318,13 +2582,134 @@ func Test_pathSymlink(t *testing.T) {
`, log)
}
// Test_pathUnlinkFile only tests it is stubbed for GrainLang per #271
func Test_pathUnlinkFile(t *testing.T) {
log := requireErrnoNosys(t, PathUnlinkFileName, 0, 0, 0)
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(writefs.DirFS(tmpDir)))
defer r.Close(testCtx)
// set up the initial memory to include the path name starting at an offset.
pathName := "wazero"
realPath := path.Join(tmpDir, pathName)
ok := mod.Memory().Write(0, append([]byte{'?'}, pathName...))
require.True(t, ok)
// create the file
err := os.WriteFile(realPath, []byte{}, 0o600)
require.NoError(t, err)
dirFD := sys.FdRoot
name := 1
nameLen := len(pathName)
requireErrno(t, ErrnoSuccess, mod, PathUnlinkFileName, uint64(dirFD), uint64(name), uint64(nameLen))
require.Equal(t, `
--> wasi_snapshot_preview1.path_unlink_file(fd=0,path=)
<-- errno=ENOSYS
`, log)
==> wasi_snapshot_preview1.path_unlink_file(fd=3,path=wazero)
<== errno=ESUCCESS
`, "\n"+log.String())
// ensure the file was removed
_, err = os.Stat(realPath)
require.Error(t, err)
}
func Test_pathUnlinkFile_Errors(t *testing.T) {
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(writefs.DirFS(tmpDir)))
defer r.Close(testCtx)
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)
tests := []struct {
name, pathName string
fd, path, pathLen uint32
expectedErrno Errno
expectedLog string
}{
{
name: "invalid fd",
fd: 42, // arbitrary invalid fd
expectedErrno: ErrnoBadf,
expectedLog: `
==> wasi_snapshot_preview1.path_unlink_file(fd=42,path=)
<== errno=EBADF
`,
},
{
name: "out-of-memory reading path",
fd: sys.FdRoot,
path: mod.Memory().Size(),
pathLen: 1,
expectedErrno: ErrnoFault,
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
`,
},
{
name: "out-of-memory reading pathLen",
fd: sys.FdRoot,
path: 0,
pathLen: mod.Memory().Size() + 1, // path is in the valid memory range, but pathLen is OOM for path
expectedErrno: ErrnoFault,
expectedLog: `
==> wasi_snapshot_preview1.path_unlink_file(fd=3,path=OOM(0,65537))
<== errno=EFAULT
`,
},
{
name: "no such file exists",
fd: sys.FdRoot,
pathName: file,
path: 0,
pathLen: uint32(len(file) - 1),
expectedErrno: ErrnoNoent,
expectedLog: `
==> wasi_snapshot_preview1.path_unlink_file(fd=3,path=fil)
<== errno=ENOENT
`,
},
{
name: "dir not file",
fd: sys.FdRoot,
pathName: dir,
path: 0,
pathLen: uint32(len(dir)),
expectedErrno: ErrnoIsdir,
expectedLog: `
==> wasi_snapshot_preview1.path_unlink_file(fd=3,path=dir)
<== errno=EISDIR
`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
defer log.Reset()
mod.Memory().Write(tc.path, []byte(tc.pathName))
requireErrno(t, tc.expectedErrno, mod, PathUnlinkFileName, uint64(tc.fd), uint64(tc.path), uint64(tc.pathLen))
require.Equal(t, tc.expectedLog, "\n"+log.String())
})
}
}
func requireOpenFile(t *testing.T, pathName string, data []byte) (api.Module, uint32, *bytes.Buffer, api.Closer) {

View File

@@ -375,13 +375,16 @@ func (c *FSContext) openFile(name string) (fs.File, error) {
}
func (c *FSContext) cleanPath(name string) string {
// fs.ValidFile cannot be rooted (start with '/')
fsOpenPath := name
if name[0] == '/' {
fsOpenPath = name[1:]
if len(name) == 0 {
return name
}
fsOpenPath = path.Clean(fsOpenPath) // e.g. "sub/." -> "sub"
return fsOpenPath
// fs.ValidFile cannot be rooted (start with '/')
cleaned := name
if name[0] == '/' {
cleaned = name[1:]
}
cleaned = path.Clean(cleaned) // e.g. "sub/." -> "sub"
return cleaned
}
// FdWriter returns a valid writer for the given file descriptor or nil if syscall.EBADF.

View File

@@ -266,20 +266,23 @@ var errnoToString = [...]string{
// error codes. For example, wasi-filesystem and GOOS=js don't map to these
// Errno.
func ToErrno(err error) Errno {
// handle all the cases of FS.Open or wasi_snapshot_preview1 to FSContext.OpenFile
switch {
case errors.Is(err, syscall.EBADF), errors.Is(err, fs.ErrClosed):
return ErrnoBadf
case errors.Is(err, syscall.EINVAL), errors.Is(err, fs.ErrInvalid):
return ErrnoInval
case errors.Is(err, syscall.EISDIR):
return ErrnoIsdir
case errors.Is(err, syscall.ENOTEMPTY):
return ErrnoNotempty
case errors.Is(err, syscall.EEXIST), errors.Is(err, fs.ErrExist):
return ErrnoExist
case errors.Is(err, syscall.ENOENT), errors.Is(err, fs.ErrNotExist):
return ErrnoNoent
case errors.Is(err, syscall.ENOSYS):
return ErrnoNosys
case errors.Is(err, fs.ErrInvalid):
return ErrnoInval
case errors.Is(err, fs.ErrNotExist):
// fs.FS is allowed to return this instead of ErrInvalid on an invalid path
return ErrnoNoent
case errors.Is(err, fs.ErrExist):
return ErrnoExist
case errors.Is(err, syscall.EBADF):
// fsc.OpenFile currently returns this on out of file descriptors
return ErrnoBadf
case errors.Is(err, syscall.ENOTDIR):
return ErrnoNotdir
default:
return ErrnoIo
}

View File

@@ -33,6 +33,10 @@ type FS interface {
// - syscall.ENOENT: `path` doesn't exist.
// - syscall.ENOTDIR: `path` exists, but isn't a directory.
// - syscall.ENOTEMPTY: `path` exists, but isn't empty.
//
// # Notes
//
// - As of Go 1.19, Windows maps syscall.ENOTDIR to syscall.ENOENT.
Rmdir(path string) error
// Unlink is similar to syscall.Unlink, except the path is relative to this

View File

@@ -100,7 +100,7 @@ func TestDirFS_Rmdir(t *testing.T) {
require.Error(t, err)
})
t.Run("file exists", func(t *testing.T) {
t.Run("not directory", func(t *testing.T) {
require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
err := testFS.Rmdir(name)
@@ -123,7 +123,7 @@ func TestDirFS_Unlink(t *testing.T) {
require.Equal(t, syscall.ENOENT, err)
})
t.Run("dir exists", func(t *testing.T) {
t.Run("not file", func(t *testing.T) {
require.NoError(t, os.Mkdir(realPath, 0o700))
err := testFS.Unlink(name)