Skip to content

Commit

Permalink
Implements rename in GOOS=js and WASI (#991)
Browse files Browse the repository at this point in the history
This implements rename, which is the last function needed to pass TinyGo
os package tests:

Signed-off-by: Adrian Cole <[email protected]>
  • Loading branch information
codefromthecrypt authored Dec 31, 2022
1 parent e4740ef commit 94491fe
Show file tree
Hide file tree
Showing 11 changed files with 625 additions and 40 deletions.
83 changes: 69 additions & 14 deletions imports/wasi_snapshot_preview1/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -1291,7 +1291,7 @@ var pathOpen = newHostFunc(
func pathOpenFn(_ context.Context, mod api.Module, params []uint64) Errno {
fsc := mod.(*wasm.CallContext).Sys.FS()

dirfd := uint32(params[0])
preopenFD := uint32(params[0])

// TODO: dirflags is a lookupflags, and it only has one bit: symlink_follow
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#lookupflags
Expand All @@ -1308,7 +1308,7 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) Errno {
fdflags := uint16(params[7])
resultOpenedFd := uint32(params[8])

pathName, errno := atPath(fsc, mod.Memory(), dirfd, path, pathLen)
pathName, errno := atPath(fsc, mod.Memory(), preopenFD, path, pathLen)
if errno != ErrnoSuccess {
return errno
}
Expand Down Expand Up @@ -1346,15 +1346,19 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) Errno {
// 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 {
func atPath(fsc *sys.FSContext, mem api.Memory, dirFd, path, pathLen uint32) (string, Errno) {
if dirFd != sys.FdRoot { //nolint
// TODO: Research if dirFd is always a pre-open. If so, it should
// always be rootFd (3), until we support multiple pre-opens.
//
// Otherwise, the dirFd could be a file created dynamically, and mean
// paths 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".
}

if _, ok := fsc.OpenedFile(dirFd); !ok {
return "", ErrnoBadf
}

Expand Down Expand Up @@ -1456,14 +1460,65 @@ func pathRemoveDirectoryFn(_ context.Context, mod api.Module, params []uint64) E
return ErrnoSuccess
}

// pathRename is the WASI function named PathRenameName which renames a
// file or directory.
var pathRename = stubFunction(
PathRenameName,
// pathRename is the WASI function named PathRenameName which renames a file or
// directory.
//
// # Parameters
//
// - fd: file descriptor of a directory that `old_path` is relative to
// - old_path: offset in api.Memory to read the old path string from
// - old_path_len: length of `old_path`
// - new_fd: file descriptor of a directory that `new_path` is relative to
// - new_path: offset in api.Memory to read the new path string from
// - new_path_len: length of `new_path`
//
// # Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// - ErrnoBadf: `fd` or `new_fd` are invalid
// - ErrnoNoent: `old_path` does not exist.
// - ErrnoNotdir: `old` is a directory and `new` exists, but is a file.
// - ErrnoIsdir: `old` is a file and `new` exists, but is a directory.
//
// # Notes
// - This is similar to unlinkat in POSIX.
// See https://linux.die.net/man/2/renameat
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-path_renamefd-fd-old_path-string-new_fd-fd-new_path-string---errno
var pathRename = newHostFunc(
PathRenameName, pathRenameFn,
[]wasm.ValueType{i32, i32, i32, i32, i32, i32},
"fd", "old_path", "old_path_len", "new_fd", "new_path", "new_path_len",
)

func pathRenameFn(_ context.Context, mod api.Module, params []uint64) Errno {
fsc := mod.(*wasm.CallContext).Sys.FS()

oldDirFd := uint32(params[0])
oldPath := uint32(params[1])
oldPathLen := uint32(params[2])

newDirFd := uint32(params[3])
newPath := uint32(params[4])
newPathLen := uint32(params[5])

oldPathName, errno := atPath(fsc, mod.Memory(), oldDirFd, oldPath, oldPathLen)
if errno != ErrnoSuccess {
return errno
}

newPathName, errno := atPath(fsc, mod.Memory(), newDirFd, newPath, newPathLen)
if errno != ErrnoSuccess {
return errno
}

if err := fsc.Rename(oldPathName, newPathName); err != nil {
return ToErrno(err)
}

return ErrnoSuccess
}

// pathSymlink is the WASI function named PathSymlinkName which creates a
// symbolic link.
//
Expand Down
223 changes: 214 additions & 9 deletions imports/wasi_snapshot_preview1/fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2830,15 +2830,6 @@ func errNotDir() Errno {
return ErrnoNotdir
}

// Test_pathRename only tests it is stubbed for GrainLang per #271
func Test_pathRename(t *testing.T) {
log := requireErrnoNosys(t, PathRenameName, 0, 0, 0, 0, 0, 0)
require.Equal(t, `
--> wasi_snapshot_preview1.path_rename(fd=0,old_path=,new_fd=0,new_path=)
<-- errno=ENOSYS
`, log)
}

// Test_pathSymlink only tests it is stubbed for GrainLang per #271
func Test_pathSymlink(t *testing.T) {
log := requireErrnoNosys(t, PathSymlinkName, 0, 0, 0, 0, 0)
Expand All @@ -2848,6 +2839,220 @@ func Test_pathSymlink(t *testing.T) {
`, log)
}

func Test_pathRename(t *testing.T) {
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
fs, err := syscallfs.NewDirFS(tmpDir)
require.NoError(t, err)

mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
defer r.Close(testCtx)

// set up the initial memory to include the old path name starting at an offset.
oldDirFD := sys.FdRoot
oldPathName := "wazero"
realOldPath := path.Join(tmpDir, oldPathName)
oldPath := uint32(0)
oldPathLen := len(oldPathName)
ok := mod.Memory().Write(oldPath, []byte(oldPathName))
require.True(t, ok)

// create the file
err = os.WriteFile(realOldPath, []byte{}, 0o600)
require.NoError(t, err)

newDirFD := sys.FdRoot
newPathName := "wahzero"
realNewPath := path.Join(tmpDir, newPathName)
newPath := uint32(16)
newPathLen := len(newPathName)
ok = mod.Memory().Write(newPath, []byte(newPathName))
require.True(t, ok)

requireErrno(t, ErrnoSuccess, mod, PathRenameName,
uint64(oldDirFD), uint64(oldPath), uint64(oldPathLen),
uint64(newDirFD), uint64(newPath), uint64(newPathLen))
require.Equal(t, `
==> wasi_snapshot_preview1.path_rename(fd=3,old_path=wazero,new_fd=3,new_path=wahzero)
<== errno=ESUCCESS
`, "\n"+log.String())

// ensure the file was renamed
_, err = os.Stat(realOldPath)
require.Error(t, err)
_, err = os.Stat(realNewPath)
require.NoError(t, err)
}

func Test_pathRename_Errors(t *testing.T) {
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
fs, err := syscallfs.NewDirFS(tmpDir)
require.NoError(t, err)

mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fs))
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, oldPathName, newPathName string
oldFd, oldPath, oldPathLen uint32
newFd, newPath, newPathLen uint32
expectedErrno Errno
expectedLog string
}{
{
name: "invalid old fd",
oldFd: 42, // arbitrary invalid fd
newFd: sys.FdRoot,
expectedErrno: ErrnoBadf,
expectedLog: `
==> wasi_snapshot_preview1.path_rename(fd=42,old_path=,new_fd=3,new_path=)
<== errno=EBADF
`,
},
{
name: "invalid new fd",
oldFd: sys.FdRoot,
newFd: 42, // arbitrary invalid fd
expectedErrno: ErrnoBadf,
expectedLog: `
==> wasi_snapshot_preview1.path_rename(fd=3,old_path=,new_fd=42,new_path=)
<== errno=EBADF
`,
},
{
name: "out-of-memory reading old path",
oldFd: sys.FdRoot,
newFd: sys.FdRoot,
oldPath: mod.Memory().Size(),
oldPathLen: 1,
expectedErrno: ErrnoFault,
expectedLog: `
==> wasi_snapshot_preview1.path_rename(fd=3,old_path=OOM(65536,1),new_fd=3,new_path=)
<== errno=EFAULT
`,
},
{
name: "out-of-memory reading new path",
oldFd: sys.FdRoot,
newFd: sys.FdRoot,
oldPath: 0,
oldPathName: "a",
oldPathLen: 1,
newPath: mod.Memory().Size(),
newPathLen: 1,
expectedErrno: ErrnoFault,
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
`,
},
{
name: "out-of-memory reading old pathLen",
oldFd: sys.FdRoot,
newFd: sys.FdRoot,
oldPath: 0,
oldPathLen: mod.Memory().Size() + 1, // path is in the valid memory range, but pathLen is OOM for path
expectedErrno: ErrnoFault,
expectedLog: `
==> wasi_snapshot_preview1.path_rename(fd=3,old_path=OOM(0,65537),new_fd=3,new_path=)
<== errno=EFAULT
`,
},
{
name: "out-of-memory reading new pathLen",
oldFd: sys.FdRoot,
newFd: sys.FdRoot,
oldPathName: file,
oldPathLen: uint32(len(file)),
newPath: 0,
newPathLen: mod.Memory().Size() + 1, // path is in the valid memory range, but pathLen is OOM for path
expectedErrno: ErrnoFault,
expectedLog: `
==> wasi_snapshot_preview1.path_rename(fd=3,old_path=file,new_fd=3,new_path=OOM(0,65537))
<== errno=EFAULT
`,
},
{
name: "no such file exists",
oldFd: sys.FdRoot,
newFd: sys.FdRoot,
oldPathName: file,
oldPathLen: uint32(len(file)) - 1,
newPath: 16,
newPathName: file,
newPathLen: uint32(len(file)),
expectedErrno: ErrnoNoent,
expectedLog: `
==> wasi_snapshot_preview1.path_rename(fd=3,old_path=fil,new_fd=3,new_path=file)
<== errno=ENOENT
`,
},
{
name: "dir not file",
oldFd: sys.FdRoot,
newFd: sys.FdRoot,
oldPathName: file,
oldPathLen: uint32(len(file)),
newPath: 16,
newPathName: dir,
newPathLen: uint32(len(dir)),
expectedErrno: ErrnoIsdir,
expectedLog: `
==> wasi_snapshot_preview1.path_rename(fd=3,old_path=file,new_fd=3,new_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.oldPath, []byte(tc.oldPathName))
mod.Memory().Write(tc.newPath, []byte(tc.newPathName))

requireErrno(t, tc.expectedErrno, mod, PathRenameName,
uint64(tc.oldFd), uint64(tc.oldPath), uint64(tc.oldPathLen),
uint64(tc.newFd), uint64(tc.newPath), uint64(tc.newPathLen))
require.Equal(t, tc.expectedLog, "\n"+log.String())
})
}
}

func Test_pathUnlinkFile(t *testing.T) {
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
fs, err := syscallfs.NewDirFS(tmpDir)
Expand Down
8 changes: 7 additions & 1 deletion internal/gojs/custom/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ const (
NameFsFstat = "fstat"
NameFsLstat = "lstat"
NameFsClose = "close"
NameFsRead = "read"
NameFsWrite = "write"
NameFsRead = "read"
NameFsReaddir = "readdir"
NameFsMkdir = "mkdir"
NameFsRmdir = "rmdir"
NameFsRename = "rename"
NameFsUnlink = "unlink"
NameFsUtimes = "utimes"
)
Expand Down Expand Up @@ -72,6 +73,11 @@ var FsNameSection = map[string]*Names{
ParamNames: []string{"path", NameCallback},
ResultNames: []string{"err", "ok"},
},
NameFsRename: {
Name: NameFsRename,
ParamNames: []string{"from", "to", NameCallback},
ResultNames: []string{"err", "ok"},
},
NameFsUnlink: {
Name: NameFsUnlink,
ParamNames: []string{"path", NameCallback},
Expand Down
Loading

0 comments on commit 94491fe

Please sign in to comment.