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:
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user