gojs: implements remaining link functions (#1198)
This implements the last remaining link functions using the same logic as WASI. In doing so, this changes the signature for FS.ReadLink to be more similar to the rest of the functions. Particularly, it stops reading the result into a buffer. After this, the only syscalls left to implement in gojs are chown. Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
@@ -1730,17 +1730,16 @@ func pathReadlinkFn(_ context.Context, mod api.Module, params []uint64) Errno {
|
|||||||
return en
|
return en
|
||||||
}
|
}
|
||||||
|
|
||||||
buf, ok := mem.Read(bufPtr, bufLen)
|
dst, err := preopen.Readlink(p)
|
||||||
if !ok {
|
|
||||||
return ErrnoFault
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := preopen.Readlink(p, buf)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ToErrno(err)
|
return ToErrno(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !mem.WriteUint32Le(resultBufUsedPtr, uint32(n)) {
|
if ok := mem.WriteString(bufPtr, dst); !ok {
|
||||||
|
return ErrnoFault
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mem.WriteUint32Le(resultBufUsedPtr, uint32(len(dst))) {
|
||||||
return ErrnoFault
|
return ErrnoFault
|
||||||
}
|
}
|
||||||
return ErrnoSuccess
|
return ErrnoSuccess
|
||||||
|
|||||||
@@ -3826,14 +3826,30 @@ func Test_pathReadlink(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("errors", func(t *testing.T) {
|
t.Run("errors", func(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
errno Errno
|
errno Errno
|
||||||
dirFd, pathPtr, pathLen, bufPtr, bufLen, resultBufUsedPtr uint64
|
dirFd, pathPtr, pathLen, bufPtr, bufLen, resultBufUsedPtr uint64
|
||||||
}{
|
}{
|
||||||
{errno: ErrnoInval},
|
{errno: ErrnoInval},
|
||||||
{errno: ErrnoInval, pathLen: 100},
|
{errno: ErrnoInval, pathLen: 100},
|
||||||
{errno: ErrnoInval, bufLen: 100},
|
{errno: ErrnoInval, bufLen: 100},
|
||||||
{errno: ErrnoFault, dirFd: uint64(topFd), bufLen: 100, pathLen: 100, bufPtr: math.MaxUint32},
|
{
|
||||||
{errno: ErrnoFault, bufLen: 100, pathLen: 100, bufPtr: 50, pathPtr: math.MaxUint32},
|
name: "bufLen too short",
|
||||||
|
errno: ErrnoFault,
|
||||||
|
dirFd: uint64(topFd),
|
||||||
|
bufLen: 10,
|
||||||
|
pathPtr: destinationPathNamePtr,
|
||||||
|
pathLen: uint64(len(destinationPathName)),
|
||||||
|
bufPtr: math.MaxUint32,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pathPtr past memory",
|
||||||
|
errno: ErrnoFault,
|
||||||
|
bufLen: 100,
|
||||||
|
pathLen: 100,
|
||||||
|
bufPtr: 50,
|
||||||
|
pathPtr: math.MaxUint32,
|
||||||
|
},
|
||||||
{errno: ErrnoNotdir, bufLen: 100, pathLen: 100, bufPtr: 50, pathPtr: 50, dirFd: 1},
|
{errno: ErrnoNotdir, bufLen: 100, pathLen: 100, bufPtr: 50, pathPtr: 50, dirFd: 1},
|
||||||
{errno: ErrnoBadf, bufLen: 100, pathLen: 100, bufPtr: 50, pathPtr: 50, dirFd: 1000},
|
{errno: ErrnoBadf, bufLen: 100, pathLen: 100, bufPtr: 50, pathPtr: 50, dirFd: 1000},
|
||||||
{
|
{
|
||||||
@@ -3843,12 +3859,15 @@ func Test_pathReadlink(t *testing.T) {
|
|||||||
dirFd: uint64(topFd),
|
dirFd: uint64(topFd),
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
name := ErrnoName(tc.errno)
|
name := tc.name
|
||||||
|
if name == "" {
|
||||||
|
name = ErrnoName(tc.errno)
|
||||||
|
}
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
requireErrnoResult(t, tc.errno, mod, PathReadlinkName,
|
requireErrnoResult(t, tc.errno, mod, PathReadlinkName,
|
||||||
tc.dirFd, tc.pathPtr, tc.pathLen, tc.bufPtr,
|
tc.dirFd, tc.pathPtr, tc.pathLen, tc.bufPtr,
|
||||||
tc.bufLen, tc.resultBufUsedPtr)
|
tc.bufLen, tc.resultBufUsedPtr)
|
||||||
require.Contains(t, log.String(), name)
|
require.Contains(t, log.String(), ErrnoName(tc.errno))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -664,9 +664,8 @@ func (jsfsReadlink) invoke(ctx context.Context, mod api.Module, args ...interfac
|
|||||||
path := args[0].(string)
|
path := args[0].(string)
|
||||||
callback := args[1].(funcWrapper)
|
callback := args[1].(funcWrapper)
|
||||||
|
|
||||||
_ = path // TODO
|
fsc := mod.(*wasm.CallContext).Sys.FS()
|
||||||
var dst string
|
dst, err := fsc.RootFS().Readlink(path)
|
||||||
var err error = syscall.ENOSYS
|
|
||||||
|
|
||||||
return callback.invoke(ctx, mod, goos.RefJsfs, err, dst) // note: error first
|
return callback.invoke(ctx, mod, goos.RefJsfs, err, dst) // note: error first
|
||||||
}
|
}
|
||||||
@@ -681,8 +680,8 @@ func (jsfsLink) invoke(ctx context.Context, mod api.Module, args ...interface{})
|
|||||||
link := args[1].(string)
|
link := args[1].(string)
|
||||||
callback := args[2].(funcWrapper)
|
callback := args[2].(funcWrapper)
|
||||||
|
|
||||||
_, _ = path, link // TODO
|
fsc := mod.(*wasm.CallContext).Sys.FS()
|
||||||
var err error = syscall.ENOSYS
|
err := fsc.RootFS().Link(path, link)
|
||||||
|
|
||||||
return jsfsInvoke(ctx, mod, callback, err)
|
return jsfsInvoke(ctx, mod, callback, err)
|
||||||
}
|
}
|
||||||
|
|||||||
45
internal/gojs/testdata/writefs/main.go
vendored
45
internal/gojs/testdata/writefs/main.go
vendored
@@ -122,22 +122,53 @@ func Main() {
|
|||||||
log.Panicln("unexpected contents:", string(bytes))
|
log.Panicln("unexpected contents:", string(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test lstat which should be about the link not its target.
|
// create a hard link
|
||||||
link := file1 + "-link"
|
link := file1 + "-link"
|
||||||
if err = os.Symlink(file1, link); err != nil {
|
if err = os.Link(file1, link); err != nil {
|
||||||
log.Panicln(err)
|
log.Panicln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
lstat, err := os.Lstat(link)
|
// Ensure this is a hard link, so they have the same inode.
|
||||||
|
file1Stat, err := os.Lstat(file1)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
linkStat, err := os.Lstat(link)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if !os.SameFile(file1Stat, linkStat) {
|
||||||
|
log.Panicln("expected file == link stat", file1Stat, linkStat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a symbolic link
|
||||||
|
symlink := file1 + "-symlink"
|
||||||
|
if err = os.Symlink(file1, symlink); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify we can read the symbolic link back
|
||||||
|
if dst, err := os.Readlink(symlink); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
} else if dst != dst {
|
||||||
|
log.Panicln("expected link dst = old value", dst, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test lstat which should be about the link not its target.
|
||||||
|
symlinkStat, err := os.Lstat(symlink)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panicln(err)
|
log.Panicln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if lstat.Mode().Type() != fs.ModeSymlink {
|
if symlinkStat.Mode().Type() != fs.ModeSymlink {
|
||||||
log.Panicln("expected type = symlink", lstat.Mode().Type())
|
log.Panicln("expected type = symlink", symlinkStat.Mode().Type())
|
||||||
}
|
}
|
||||||
if size := int64(len(file1)); lstat.Size() != size {
|
if size := int64(len(file1)); symlinkStat.Size() != size {
|
||||||
log.Panicln("unexpected symlink size", lstat.Size(), size)
|
log.Panicln("unexpected symlink size", symlinkStat.Size(), size)
|
||||||
|
}
|
||||||
|
// A symbolic link isn't the same file as what it points to.
|
||||||
|
if os.SameFile(file1Stat, symlinkStat) {
|
||||||
|
log.Panicln("expected file != link stat", file1Stat, symlinkStat)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test removing a non-empty empty directory
|
// Test removing a non-empty empty directory
|
||||||
|
|||||||
6
internal/platform/path.go
Normal file
6
internal/platform/path.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package platform
|
||||||
|
|
||||||
|
// ToPosixPath returns the input, as only windows might return backslashes.
|
||||||
|
func ToPosixPath(in string) string { return in }
|
||||||
14
internal/platform/path_test.go
Normal file
14
internal/platform/path_test.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package platform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestToPosixPath(t *testing.T) {
|
||||||
|
orig := filepath.Join("a", "b", "c")
|
||||||
|
fixed := ToPosixPath(orig)
|
||||||
|
require.Equal(t, "a/b/c", fixed)
|
||||||
|
}
|
||||||
17
internal/platform/path_windows.go
Normal file
17
internal/platform/path_windows.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package platform
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// ToPosixPath returns the input, converting any backslashes to forward ones.
|
||||||
|
func ToPosixPath(in string) string {
|
||||||
|
// strings.Map only allocates on change, which is good enough especially as
|
||||||
|
// path.Join uses forward slash even on windows.
|
||||||
|
return strings.Map(windowsToPosixSeparator, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowsToPosixSeparator(r rune) rune {
|
||||||
|
if r == '\\' {
|
||||||
|
return '/'
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
|
|
||||||
package platform
|
|
||||||
|
|
||||||
// SanitizeSeparator sanitizes the path separator in the given buffer.
|
|
||||||
// This does nothing on the non-windows platforms.
|
|
||||||
func SanitizeSeparator([]byte) {}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package platform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSanitizeSeparator(t *testing.T) {
|
|
||||||
orig := []byte(filepath.Join("a", "b", "c"))
|
|
||||||
SanitizeSeparator(orig)
|
|
||||||
require.Equal(t, "a/b/c", string(orig))
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package platform
|
|
||||||
|
|
||||||
// SanitizeSeparator sanitizes the path separator in the given buffer.
|
|
||||||
func SanitizeSeparator(in []byte) {
|
|
||||||
for i := range in {
|
|
||||||
if in[i] == '\\' {
|
|
||||||
in[i] = '/'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -78,23 +78,14 @@ func (d *dirFS) Rename(from, to string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Readlink implements FS.Readlink
|
// Readlink implements FS.Readlink
|
||||||
func (d *dirFS) Readlink(path string, buf []byte) (n int, err error) {
|
func (d *dirFS) Readlink(path string) (string, error) {
|
||||||
// Note: do not use syscall.Readlink as that causes race on Windows.
|
// Note: do not use syscall.Readlink as that causes race on Windows.
|
||||||
// In any case, syscall.Readlink does almost the same logic as os.Readlink.
|
// In any case, syscall.Readlink does almost the same logic as os.Readlink.
|
||||||
res, err := os.Readlink(d.join(path))
|
dst, err := os.Readlink(d.join(path))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = platform.UnwrapOSError(err)
|
return "", platform.UnwrapOSError(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
return platform.ToPosixPath(dst), nil
|
||||||
// We need to copy here, but syscall.Readlink does copy internally, so the cost is the same.
|
|
||||||
copy(buf, res)
|
|
||||||
n = len(res)
|
|
||||||
if n > len(buf) {
|
|
||||||
n = len(buf)
|
|
||||||
}
|
|
||||||
platform.SanitizeSeparator(buf[:n])
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Link implements FS.Link.
|
// Link implements FS.Link.
|
||||||
|
|||||||
@@ -152,6 +152,6 @@ func (r *readFS) Stat(path string, stat *platform.Stat_t) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Readlink implements FS.Readlink
|
// Readlink implements FS.Readlink
|
||||||
func (r *readFS) Readlink(path string, buf []byte) (n int, err error) {
|
func (r *readFS) Readlink(path string) (dst string, err error) {
|
||||||
return r.fs.Readlink(path, buf)
|
return r.fs.Readlink(path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ type FS interface {
|
|||||||
// - On Windows, the path separator is different from other platforms,
|
// - On Windows, the path separator is different from other platforms,
|
||||||
// but to provide consistent results to Wasm, this normalizes to a "/"
|
// but to provide consistent results to Wasm, this normalizes to a "/"
|
||||||
// separator.
|
// separator.
|
||||||
Readlink(path string, buf []byte) (n int, err error)
|
Readlink(path string) (string, error)
|
||||||
|
|
||||||
// Truncate is similar to syscall.Truncate, except the path is relative to
|
// Truncate is similar to syscall.Truncate, except the path is relative to
|
||||||
// this file system.
|
// this file system.
|
||||||
|
|||||||
@@ -370,23 +370,21 @@ func testReadlink(t *testing.T, readFS, writeFS FS) {
|
|||||||
{old: "sub/test.txt", dst: "symlinked-zoo.txt"},
|
{old: "sub/test.txt", dst: "symlinked-zoo.txt"},
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := make([]byte, 200)
|
|
||||||
for _, tl := range testLinks {
|
for _, tl := range testLinks {
|
||||||
err := writeFS.Symlink(tl.old, tl.dst) // not os.Symlink for windows compat
|
err := writeFS.Symlink(tl.old, tl.dst) // not os.Symlink for windows compat
|
||||||
require.NoError(t, err, "%v", tl)
|
require.NoError(t, err, "%v", tl)
|
||||||
|
|
||||||
n, err := readFS.Readlink(tl.dst, buf)
|
dst, err := readFS.Readlink(tl.dst)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, tl.old, string(buf[:n]))
|
require.Equal(t, tl.old, dst)
|
||||||
require.Equal(t, len(tl.old), n)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("errors", func(t *testing.T) {
|
t.Run("errors", func(t *testing.T) {
|
||||||
_, err := readFS.Readlink("sub/test.txt", buf)
|
_, err := readFS.Readlink("sub/test.txt")
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
_, err = readFS.Readlink("", buf)
|
_, err = readFS.Readlink("")
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
_, err = readFS.Readlink("animals.txt", buf)
|
_, err = readFS.Readlink("animals.txt")
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ func (UnimplementedFS) Rmdir(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Readlink implements FS.Readlink
|
// Readlink implements FS.Readlink
|
||||||
func (UnimplementedFS) Readlink(string, []byte) (int, error) {
|
func (UnimplementedFS) Readlink(path string) (string, error) {
|
||||||
return 0, syscall.ENOSYS
|
return "", syscall.ENOSYS
|
||||||
}
|
}
|
||||||
|
|
||||||
// Link implements FS.Link
|
// Link implements FS.Link
|
||||||
|
|||||||
Reference in New Issue
Block a user