sysfs: consolidates errno coersion and maps EAGAIN and EINTR (#1113)
Signed-off-by: Adrian Cole <adrian@tetrate.io> Co-authored-by: Takeshi Yoneda <takeshi@tetrate.io>
This commit is contained in:
@@ -1407,7 +1407,7 @@ func pathFilestatGetFn(_ context.Context, mod api.Module, params []uint64) Errno
|
||||
if err != nil {
|
||||
return ToErrno(err)
|
||||
}
|
||||
stat, err := f.Stat()
|
||||
stat, err := sysfs.StatFile(f)
|
||||
if err != nil {
|
||||
return ToErrno(err)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package gojs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/sysfs"
|
||||
)
|
||||
|
||||
// Errno is a (GOARCH=wasm) error, which must match a key in mapJSError.
|
||||
@@ -22,10 +21,14 @@ func (e *Errno) Error() string {
|
||||
// This order match constants from wasi_snapshot_preview1.ErrnoSuccess for
|
||||
// easier maintenance.
|
||||
var (
|
||||
// ErrnoAgain Resource unavailable, or operation would block.
|
||||
ErrnoAgain = &Errno{"EAGAIN"}
|
||||
// ErrnoBadf Bad file descriptor.
|
||||
ErrnoBadf = &Errno{"EBADF"}
|
||||
// ErrnoExist File exists.
|
||||
ErrnoExist = &Errno{"EEXIST"}
|
||||
// ErrnoIntr Interrupted function.
|
||||
ErrnoIntr = &Errno{"EINTR"}
|
||||
// ErrnoInval Invalid argument.
|
||||
ErrnoInval = &Errno{"EINVAL"}
|
||||
// ErrnoIo I/O error.
|
||||
@@ -55,37 +58,17 @@ var (
|
||||
//
|
||||
// This should match wasi_snapshot_preview1.ToErrno for maintenance ease.
|
||||
func ToErrno(err error) *Errno {
|
||||
if pe, ok := err.(*os.PathError); ok {
|
||||
err = pe.Unwrap()
|
||||
}
|
||||
if se, ok := err.(syscall.Errno); ok {
|
||||
return errnoFromSyscall(se)
|
||||
}
|
||||
// Below are all the fs.ErrXXX in fs.go. errors.Is is more expensive, so
|
||||
// try it last. Note: Once we have our own file type, we should never see
|
||||
// these.
|
||||
switch {
|
||||
case errors.Is(err, fs.ErrInvalid):
|
||||
return ErrnoInval
|
||||
case errors.Is(err, fs.ErrPermission):
|
||||
return ErrnoPerm
|
||||
case errors.Is(err, fs.ErrExist):
|
||||
return ErrnoExist
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
return ErrnoNoent
|
||||
case errors.Is(err, fs.ErrClosed):
|
||||
return ErrnoBadf
|
||||
default:
|
||||
return ErrnoIo
|
||||
}
|
||||
}
|
||||
errno := sysfs.UnwrapOSError(err)
|
||||
|
||||
func errnoFromSyscall(errno syscall.Errno) *Errno {
|
||||
switch errno {
|
||||
case syscall.EAGAIN:
|
||||
return ErrnoAgain
|
||||
case syscall.EBADF:
|
||||
return ErrnoBadf
|
||||
case syscall.EEXIST:
|
||||
return ErrnoExist
|
||||
case syscall.EINTR:
|
||||
return ErrnoIntr
|
||||
case syscall.EINVAL:
|
||||
return ErrnoInval
|
||||
case syscall.EIO:
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package gojs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
@@ -17,6 +13,11 @@ func TestToErrno(t *testing.T) {
|
||||
input error
|
||||
expected *Errno
|
||||
}{
|
||||
{
|
||||
name: "syscall.EAGAIN",
|
||||
input: syscall.EAGAIN,
|
||||
expected: ErrnoAgain,
|
||||
},
|
||||
{
|
||||
name: "syscall.EBADF",
|
||||
input: syscall.EBADF,
|
||||
@@ -27,6 +28,11 @@ func TestToErrno(t *testing.T) {
|
||||
input: syscall.EEXIST,
|
||||
expected: ErrnoExist,
|
||||
},
|
||||
{
|
||||
name: "syscall.EINTR",
|
||||
input: syscall.EINTR,
|
||||
expected: ErrnoIntr,
|
||||
},
|
||||
{
|
||||
name: "syscall.EINVAL",
|
||||
input: syscall.EINVAL,
|
||||
@@ -87,46 +93,6 @@ func TestToErrno(t *testing.T) {
|
||||
input: syscall.Errno(0xfe),
|
||||
expected: ErrnoIo,
|
||||
},
|
||||
{
|
||||
name: "PathError ErrInvalid",
|
||||
input: &os.PathError{Err: fs.ErrInvalid},
|
||||
expected: ErrnoInval,
|
||||
},
|
||||
{
|
||||
name: "PathError ErrPermission",
|
||||
input: &os.PathError{Err: fs.ErrPermission},
|
||||
expected: ErrnoPerm,
|
||||
},
|
||||
{
|
||||
name: "PathError ErrExist",
|
||||
input: &os.PathError{Err: fs.ErrExist},
|
||||
expected: ErrnoExist,
|
||||
},
|
||||
{
|
||||
name: "PathError ErrNotExist",
|
||||
input: &os.PathError{Err: fs.ErrNotExist},
|
||||
expected: ErrnoNoent,
|
||||
},
|
||||
{
|
||||
name: "PathError ErrClosed",
|
||||
input: &os.PathError{Err: fs.ErrClosed},
|
||||
expected: ErrnoBadf,
|
||||
},
|
||||
{
|
||||
name: "PathError unknown == ErrnoIo",
|
||||
input: &os.PathError{Err: errors.New("ice cream")},
|
||||
expected: ErrnoIo,
|
||||
},
|
||||
{
|
||||
name: "unknown == ErrnoIo",
|
||||
input: errors.New("ice cream"),
|
||||
expected: ErrnoIo,
|
||||
},
|
||||
{
|
||||
name: "very wrapped unknown == ErrnoIo",
|
||||
input: fmt.Errorf("%w", fmt.Errorf("%w", fmt.Errorf("%w", errors.New("ice cream")))),
|
||||
expected: ErrnoIo,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
30
internal/platform/open_file_test.go
Normal file
30
internal/platform/open_file_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
)
|
||||
|
||||
func TestOpenFile_Errors(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
|
||||
t.Run("not found must be ENOENT", func(t *testing.T) {
|
||||
_, err := OpenFile(path.Join(tmp, "not-really-exist.txt"), os.O_RDONLY, 0o600)
|
||||
require.ErrorIs(t, err, syscall.ENOENT)
|
||||
})
|
||||
|
||||
// This is the same as https://github.com/ziglang/zig/blob/d24ebf1d12cf66665b52136a2807f97ff021d78d/lib/std/os/test.zig#L105-L112
|
||||
t.Run("try creating on existing file must be EEXIST", func(t *testing.T) {
|
||||
filepath := path.Join(tmp, "file.txt")
|
||||
f, err := OpenFile(filepath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o666)
|
||||
defer require.NoError(t, f.Close())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = OpenFile(filepath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o666)
|
||||
require.ErrorIs(t, err, syscall.EEXIST)
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"syscall"
|
||||
@@ -33,7 +34,15 @@ func OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
|
||||
return os.NewFile(uintptr(fd), name), nil
|
||||
}
|
||||
// TODO: Set FILE_SHARE_DELETE for directory as well.
|
||||
return os.OpenFile(name, flag, perm)
|
||||
f, err := os.OpenFile(name, flag, perm)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.ENOTDIR) {
|
||||
err = syscall.ENOENT
|
||||
} else if errors.Is(err, syscall.ERROR_FILE_EXISTS) {
|
||||
err = syscall.EEXIST
|
||||
}
|
||||
}
|
||||
return f, err
|
||||
}
|
||||
|
||||
// The following is lifted from syscall_windows.go to add support for setting FILE_SHARE_DELETE.
|
||||
|
||||
@@ -47,7 +47,7 @@ func stat(f fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlin
|
||||
// os.Stat does to allow the results on the closed files.
|
||||
// https://github.com/golang/go/blob/go1.20/src/os/stat_windows.go#L86
|
||||
//
|
||||
// TODO: once we have our File/Sta type, this shouldn't be necessary.
|
||||
// TODO: once we have our File/Stat type, this shouldn't be necessary.
|
||||
// But for now, ignore the error to pass the std library test for bad file descriptor.
|
||||
// https://github.com/ziglang/zig/blob/master/lib/std/os/test.zig#L167-L170
|
||||
if err == syscall.Errno(6) {
|
||||
|
||||
@@ -187,7 +187,7 @@ func (f *FileEntry) IsDir() bool {
|
||||
|
||||
// Stat returns the underlying stat of this file.
|
||||
func (f *FileEntry) Stat() (stat fs.FileInfo, err error) {
|
||||
stat, err = f.File.Stat()
|
||||
stat, err = sysfs.StatFile(f.File)
|
||||
if err == nil && stat.IsDir() {
|
||||
f.isDirectory = true
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func (a *adapter) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, er
|
||||
f, err := a.fs.Open(path)
|
||||
|
||||
if err != nil {
|
||||
return nil, unwrapOSError(err)
|
||||
return nil, UnwrapOSError(err)
|
||||
} else if osF, ok := f.(*os.File); ok {
|
||||
// If this is an OS file, it has same portability issues as dirFS.
|
||||
return maybeWrapFile(osF, a, path, flag, perm), nil
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sysfs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"syscall"
|
||||
@@ -44,7 +45,7 @@ func (d *dirFS) Open(name string) (fs.File, error) {
|
||||
func (d *dirFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) {
|
||||
f, err := platform.OpenFile(d.join(name), flag, perm)
|
||||
if err != nil {
|
||||
return nil, unwrapOSError(err)
|
||||
return nil, UnwrapOSError(err)
|
||||
}
|
||||
return maybeWrapFile(f, d, name, flag, perm), nil
|
||||
}
|
||||
@@ -52,14 +53,17 @@ func (d *dirFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, erro
|
||||
// Mkdir implements FS.Mkdir
|
||||
func (d *dirFS) Mkdir(name string, perm fs.FileMode) error {
|
||||
err := os.Mkdir(d.join(name), perm)
|
||||
err = unwrapOSError(err)
|
||||
return adjustMkdirError(err)
|
||||
if errors.Is(err, syscall.ENOTDIR) {
|
||||
return syscall.ENOENT
|
||||
}
|
||||
return UnwrapOSError(err)
|
||||
}
|
||||
|
||||
// Rename implements FS.Rename
|
||||
func (d *dirFS) Rename(from, to string) error {
|
||||
from, to = d.join(from), d.join(to)
|
||||
return platform.Rename(from, to)
|
||||
err := platform.Rename(from, to)
|
||||
return UnwrapOSError(err)
|
||||
}
|
||||
|
||||
// Readlink implements FS.Readlink
|
||||
@@ -68,7 +72,7 @@ func (d *dirFS) Readlink(path string, buf []byte) (n int, err error) {
|
||||
// In any case, syscall.Readlink does almost the same logic as os.Readlink.
|
||||
res, err := os.Readlink(d.join(path))
|
||||
if err != nil {
|
||||
err = unwrapOSError(err)
|
||||
err = UnwrapOSError(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -83,47 +87,50 @@ func (d *dirFS) Readlink(path string, buf []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
// Link implements FS.Link.
|
||||
func (d *dirFS) Link(oldName, newName string) (err error) {
|
||||
err = os.Link(d.join(oldName), d.join(newName))
|
||||
err = unwrapOSError(err)
|
||||
return
|
||||
func (d *dirFS) Link(oldName, newName string) error {
|
||||
err := os.Link(d.join(oldName), d.join(newName))
|
||||
return UnwrapOSError(err)
|
||||
}
|
||||
|
||||
// Rmdir implements FS.Rmdir
|
||||
func (d *dirFS) Rmdir(name string) error {
|
||||
err := syscall.Rmdir(d.join(name))
|
||||
err = UnwrapOSError(err)
|
||||
return adjustRmdirError(err)
|
||||
}
|
||||
|
||||
// Unlink implements FS.Unlink
|
||||
func (d *dirFS) Unlink(name string) error {
|
||||
err := syscall.Unlink(d.join(name))
|
||||
return adjustUnlinkError(err)
|
||||
func (d *dirFS) Unlink(name string) (err error) {
|
||||
err = syscall.Unlink(d.join(name))
|
||||
if err = UnwrapOSError(err); err == syscall.EPERM {
|
||||
err = syscall.EISDIR
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Symlink implements FS.Symlink
|
||||
func (d *dirFS) Symlink(oldName, link string) error {
|
||||
func (d *dirFS) Symlink(oldName, link string) (err error) {
|
||||
// Note: do not resolve `oldName` relative to this dirFS. The link result is always resolved
|
||||
// when dereference the `link` on its usage (e.g. readlink, read, etc).
|
||||
// https://github.com/bytecodealliance/cap-std/blob/v1.0.4/cap-std/src/fs/dir.rs#L404-L409
|
||||
err := os.Symlink(oldName, d.join(link))
|
||||
err = unwrapOSError(err)
|
||||
return err
|
||||
err = os.Symlink(oldName, d.join(link))
|
||||
return UnwrapOSError(err)
|
||||
}
|
||||
|
||||
// Utimes implements FS.Utimes
|
||||
func (d *dirFS) Utimes(name string, atimeNsec, mtimeNsec int64) error {
|
||||
return syscall.UtimesNano(d.join(name), []syscall.Timespec{
|
||||
err := syscall.UtimesNano(d.join(name), []syscall.Timespec{
|
||||
syscall.NsecToTimespec(atimeNsec),
|
||||
syscall.NsecToTimespec(mtimeNsec),
|
||||
})
|
||||
return UnwrapOSError(err)
|
||||
}
|
||||
|
||||
// Truncate implements FS.Truncate
|
||||
func (d *dirFS) Truncate(name string, size int64) error {
|
||||
// Use os.Truncate as syscall.Truncate doesn't exist on Windows.
|
||||
err := os.Truncate(d.join(name), size)
|
||||
err = unwrapOSError(err)
|
||||
err = UnwrapOSError(err)
|
||||
return adjustTruncateError(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,11 @@ func TestDirFS_MkDir(t *testing.T) {
|
||||
err := testFS.Mkdir(name, fs.ModeDir)
|
||||
require.Equal(t, syscall.EEXIST, err)
|
||||
})
|
||||
t.Run("try creating on file", func(t *testing.T) {
|
||||
filePath := pathutil.Join("non-existing-dir", "foo.txt")
|
||||
err := testFS.Mkdir(filePath, fs.ModeDir)
|
||||
require.Equal(t, syscall.ENOENT, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDirFS_Rename(t *testing.T) {
|
||||
@@ -577,8 +582,8 @@ func TestDirFS_Readlink(t *testing.T) {
|
||||
|
||||
buf := make([]byte, 200)
|
||||
for _, tl := range testLinks {
|
||||
err := os.Symlink(pathutil.Join(tl.old), pathutil.Join(tmpDir, tl.dst))
|
||||
require.NoError(t, err)
|
||||
err := testFS.Symlink(tl.old, tl.dst) // not os.Symlink for windows compat
|
||||
require.NoError(t, err, "%v", tl)
|
||||
|
||||
n, err := testFS.Readlink(tl.dst, buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -4,10 +4,9 @@ package sysfs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func adjustMkdirError(err error) error {
|
||||
func adjustErrno(err error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -19,13 +18,6 @@ func adjustTruncateError(err error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func adjustUnlinkError(err error) error {
|
||||
if err == syscall.EPERM {
|
||||
return syscall.EISDIR
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func maybeWrapFile(f file, _ FS, _ string, _ int, _ fs.FileMode) file {
|
||||
return f
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package sysfs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
@@ -32,11 +30,24 @@ const (
|
||||
// ERROR_DIRECTORY is a Windows error returned by syscall.Rmdir
|
||||
// instead of syscall.ENOTDIR
|
||||
ERROR_DIRECTORY = syscall.Errno(267)
|
||||
|
||||
// ERROR_PRIVILEGE_NOT_HELD is a Windows error returned by os.Symlink
|
||||
// instead of syscall.EPERM.
|
||||
//
|
||||
// Note: This can happen when trying to create symlinks w/o admin perms.
|
||||
ERROR_PRIVILEGE_NOT_HELD = syscall.Errno(1314)
|
||||
)
|
||||
|
||||
func adjustMkdirError(err error) error {
|
||||
if err == ERROR_ALREADY_EXISTS {
|
||||
func adjustErrno(err syscall.Errno) error {
|
||||
switch err {
|
||||
case ERROR_ALREADY_EXISTS:
|
||||
return syscall.EEXIST
|
||||
case ERROR_DIR_NOT_EMPTY:
|
||||
return syscall.ENOTEMPTY
|
||||
case ERROR_INVALID_HANDLE:
|
||||
return syscall.EBADF
|
||||
case ERROR_ACCESS_DENIED, ERROR_PRIVILEGE_NOT_HELD:
|
||||
return syscall.EPERM
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -45,8 +56,6 @@ func adjustRmdirError(err error) error {
|
||||
switch err {
|
||||
case ERROR_DIRECTORY:
|
||||
return syscall.ENOTDIR
|
||||
case ERROR_DIR_NOT_EMPTY:
|
||||
return syscall.ENOTEMPTY
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -58,43 +67,6 @@ func adjustTruncateError(err error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func adjustUnlinkError(err error) error {
|
||||
if err == ERROR_ACCESS_DENIED {
|
||||
return syscall.EISDIR
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// rename uses os.Rename as `windows.Rename` is internal in Go's source tree.
|
||||
func rename(old, new string) (err error) {
|
||||
if err = os.Rename(old, new); err == nil {
|
||||
return
|
||||
}
|
||||
err = errors.Unwrap(err) // unwrap the link error
|
||||
if err == ERROR_ACCESS_DENIED {
|
||||
var newIsDir bool
|
||||
if stat, statErr := os.Stat(new); statErr == nil && stat.IsDir() {
|
||||
newIsDir = true
|
||||
}
|
||||
|
||||
var oldIsDir bool
|
||||
if stat, statErr := os.Stat(old); statErr == nil && stat.IsDir() {
|
||||
oldIsDir = true
|
||||
}
|
||||
|
||||
if oldIsDir && newIsDir {
|
||||
// Windows doesn't let you overwrite a directory. If we aim to
|
||||
// allow this, we'll have to delete here and retry.
|
||||
return syscall.EINVAL
|
||||
} else if newIsDir {
|
||||
err = syscall.EISDIR
|
||||
} else { // use a mappable code
|
||||
err = syscall.EPERM
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// maybeWrapFile deals with errno portability issues in Windows. This code is
|
||||
// likely to change as we complete syscall support needed for WASI and GOOS=js.
|
||||
//
|
||||
@@ -144,15 +116,10 @@ func (w *windowsWrappedFile) Write(p []byte) (n int, err error) {
|
||||
|
||||
// os.File.Wrap wraps the syscall error in a path error
|
||||
if pe, ok := err.(*fs.PathError); ok {
|
||||
switch pe.Err {
|
||||
case ERROR_INVALID_HANDLE:
|
||||
pe.Err = syscall.EBADF
|
||||
case ERROR_ACCESS_DENIED:
|
||||
if pe.Err = UnwrapOSError(pe.Err); pe.Err == syscall.EPERM {
|
||||
// go1.20 returns access denied, not invalid handle, writing to a directory.
|
||||
if stat, statErr := StatPath(w.fs, w.path); statErr == nil && stat.IsDir() {
|
||||
pe.Err = syscall.EBADF
|
||||
} else {
|
||||
pe.Err = syscall.EPERM
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
package sysfs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
@@ -77,9 +76,10 @@ type FS interface {
|
||||
// The following errors are expected:
|
||||
// - syscall.EINVAL: `from` or `to` is invalid.
|
||||
// - syscall.ENOENT: `from` or `to` don't exist.
|
||||
// - syscall.ENOTDIR: `from` is a directory and `to` exists, but is a file.
|
||||
// - syscall.EISDIR: `from` is a file and `to` exists, but is a directory.
|
||||
// - syscall.ENOTEMPTY: `both from` and `to` are existing directory, but `to` is not empty.
|
||||
// - syscall.ENOTDIR: `from` is a directory and `to` exists as a file.
|
||||
// - syscall.EISDIR: `from` is a file and `to` exists as a directory.
|
||||
// - syscall.ENOTEMPTY: `both from` and `to` are existing directory, but
|
||||
// `to` is not empty.
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
@@ -105,6 +105,8 @@ type FS interface {
|
||||
// Unlink is similar to syscall.Unlink, except the path is relative to this
|
||||
// file system.
|
||||
//
|
||||
// # Errors
|
||||
//
|
||||
// The following errors are expected:
|
||||
// - syscall.EINVAL: `path` is invalid.
|
||||
// - syscall.ENOENT: `path` doesn't exist.
|
||||
@@ -112,7 +114,8 @@ type FS interface {
|
||||
Unlink(path string) error
|
||||
|
||||
// Link is similar to syscall.Link, except the path is relative to this
|
||||
// file system. This creates "hard" link from oldPath to newPath, in contrast to soft link as in Symlink.
|
||||
// file system. This creates "hard" link from oldPath to newPath, in
|
||||
// contrast to soft link as in Symlink.
|
||||
//
|
||||
// # Errors
|
||||
//
|
||||
@@ -122,8 +125,8 @@ type FS interface {
|
||||
// - syscall.EISDIR: `newPath` exists, but is a directory.
|
||||
Link(oldPath, newPath string) error
|
||||
|
||||
// Symlink is similar to syscall.Symlink, except the `oldPath` is relative to this
|
||||
// file system. This creates "soft" link from oldPath to newPath,
|
||||
// Symlink is similar to syscall.Symlink, except the `oldPath` is relative
|
||||
// to this file system. This creates "soft" link from oldPath to newPath,
|
||||
// in contrast to hard link as in Link.
|
||||
//
|
||||
// # Errors
|
||||
@@ -132,25 +135,30 @@ type FS interface {
|
||||
// - syscall.EPERM: `oldPath` or `newPath` is invalid.
|
||||
// - syscall.EEXIST: `newPath` exists.
|
||||
//
|
||||
// # Note
|
||||
// # Notes
|
||||
//
|
||||
// - Only `newPath` is relative to this file system and `oldPath` is kept as-is.
|
||||
// That is because the link is only resolved relative to the directory when
|
||||
// dereferencing it (e.g. ReadLink).
|
||||
// - Only `newPath` is relative to this file system and `oldPath` is kept
|
||||
// as-is. That is because the link is only resolved relative to the
|
||||
// directory when dereferencing it (e.g. ReadLink).
|
||||
// See https://github.com/bytecodealliance/cap-std/blob/v1.0.4/cap-std/src/fs/dir.rs#L404-L409
|
||||
// for how others implement this.
|
||||
// - Symlinks in Windows requires `SeCreateSymbolicLinkPrivilege`.
|
||||
// Otherwise, syscall.EPERM results.
|
||||
// See https://learn.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/create-symbolic-links
|
||||
Symlink(oldPath, linkName string) error
|
||||
|
||||
// Readlink is similar to syscall.Readlink, except the path is relative to this
|
||||
// file system.
|
||||
// Readlink is similar to syscall.Readlink, except the path is relative to
|
||||
// this file system.
|
||||
//
|
||||
// # Errors
|
||||
//
|
||||
// The following errors are expected:
|
||||
// - syscall.EINVAL: `path` is invalid.
|
||||
//
|
||||
// # Notes
|
||||
// - On windows, its path separator is different from other platforms, but to provide consistent result to Wasm,
|
||||
// the implementation is supposed to sanitize the result with the regular "/" separator.
|
||||
// - On Windows, the path separator is different from other platforms,
|
||||
// but to provide consistent results to Wasm, this normalizes to a "/"
|
||||
// separator.
|
||||
Readlink(path string, buf []byte) (n int, err error)
|
||||
|
||||
// Truncate is similar to syscall.Truncate, except the path is relative to
|
||||
@@ -180,15 +188,23 @@ type FS interface {
|
||||
Utimes(path string, atimeNsec, mtimeNsec int64) error
|
||||
}
|
||||
|
||||
// StatPath is a convenience that calls FS.OpenFile until there is a stat
|
||||
// method.
|
||||
func StatPath(fs FS, path string) (fs.FileInfo, error) {
|
||||
// StatPath is a convenience that calls FS.OpenFile, then StatFile, until there
|
||||
// is a stat method.
|
||||
func StatPath(fs FS, path string) (s fs.FileInfo, err error) {
|
||||
f, err := fs.OpenFile(path, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
return f.Stat()
|
||||
return StatFile(f)
|
||||
}
|
||||
|
||||
// StatFile is like the same method on fs.File, except the returned error is
|
||||
// nil or syscall.Errno.
|
||||
func StatFile(f fs.File) (s fs.FileInfo, err error) {
|
||||
s, err = f.Stat()
|
||||
err = UnwrapOSError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// readFile declares all read interfaces defined on os.File used by wazero.
|
||||
@@ -326,13 +342,18 @@ func (r *writerAtOffset) Write(p []byte) (int, error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
func unwrapOSError(err error) error {
|
||||
if pe, ok := err.(*fs.PathError); ok {
|
||||
err = pe.Err
|
||||
} else if le, ok := err.(*os.LinkError); ok {
|
||||
err = le.Err
|
||||
// UnwrapOSError returns a syscall.Errno or nil if the input is nil.
|
||||
func UnwrapOSError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = underlyingError(err)
|
||||
if se, ok := err.(syscall.Errno); ok {
|
||||
return adjustErrno(se)
|
||||
}
|
||||
// Below are all the fs.ErrXXX in fs.go.
|
||||
//
|
||||
// Note: Once we have our own file type, we should never see these.
|
||||
switch err {
|
||||
case nil:
|
||||
case fs.ErrInvalid:
|
||||
@@ -345,20 +366,21 @@ func unwrapOSError(err error) error {
|
||||
return syscall.ENOENT
|
||||
case fs.ErrClosed:
|
||||
return syscall.EBADF
|
||||
case os.ErrInvalid:
|
||||
return syscall.EINVAL
|
||||
case os.ErrExist:
|
||||
return syscall.EEXIST
|
||||
default:
|
||||
if errors.Is(err, os.ErrExist) {
|
||||
return syscall.EEXIST
|
||||
}
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return syscall.ENOENT
|
||||
}
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
return syscall.EPERM
|
||||
}
|
||||
}
|
||||
return syscall.EIO
|
||||
}
|
||||
|
||||
// underlyingError returns the underlying error if a well-known OS error type.
|
||||
//
|
||||
// This impl is basically the same as os.underlyingError in os/error.go
|
||||
func underlyingError(err error) error {
|
||||
switch err := err.(type) {
|
||||
case *os.PathError:
|
||||
return err.Err
|
||||
case *os.LinkError:
|
||||
return err.Err
|
||||
case *os.SyscallError:
|
||||
return err.Err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
@@ -12,9 +13,10 @@ import (
|
||||
"sort"
|
||||
"syscall"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
gofstest "testing/fstest"
|
||||
"time"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/fstest"
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
)
|
||||
@@ -254,7 +256,7 @@ func TestReaderAtOffset(t *testing.T) {
|
||||
bytes, err := io.ReadAll(d)
|
||||
require.NoError(t, err)
|
||||
|
||||
mapFS := fstest.MapFS{readerAtFile: &fstest.MapFile{Data: bytes}}
|
||||
mapFS := gofstest.MapFS{readerAtFile: &gofstest.MapFile{Data: bytes}}
|
||||
|
||||
// Write a file as can't open "testdata" in scratch tests because they
|
||||
// can't read the original filesystem.
|
||||
@@ -326,7 +328,7 @@ func TestReaderAtOffset_empty(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer d.Close()
|
||||
|
||||
mapFS := fstest.MapFS{emptyFile: &fstest.MapFile{}}
|
||||
mapFS := gofstest.MapFS{emptyFile: &gofstest.MapFile{}}
|
||||
|
||||
// Write a file as can't open "testdata" in scratch tests because they
|
||||
// can't read the original filesystem.
|
||||
@@ -509,3 +511,95 @@ func TestWriterAtOffset_Unsupported(t *testing.T) {
|
||||
_, err = ra.Write(buf)
|
||||
require.Equal(t, syscall.ENOSYS, err)
|
||||
}
|
||||
|
||||
func TestUnwrapOSError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input, expected error
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
},
|
||||
{
|
||||
name: "LinkError ErrInvalid",
|
||||
input: &os.LinkError{Err: fs.ErrInvalid},
|
||||
expected: syscall.EINVAL,
|
||||
},
|
||||
{
|
||||
name: "PathError ErrInvalid",
|
||||
input: &os.PathError{Err: fs.ErrInvalid},
|
||||
expected: syscall.EINVAL,
|
||||
},
|
||||
{
|
||||
name: "SyscallError ErrInvalid",
|
||||
input: &os.SyscallError{Err: fs.ErrInvalid},
|
||||
expected: syscall.EINVAL,
|
||||
},
|
||||
{
|
||||
name: "PathError ErrPermission",
|
||||
input: &os.PathError{Err: os.ErrPermission},
|
||||
expected: syscall.EPERM,
|
||||
},
|
||||
{
|
||||
name: "PathError ErrExist",
|
||||
input: &os.PathError{Err: os.ErrExist},
|
||||
expected: syscall.EEXIST,
|
||||
},
|
||||
{
|
||||
name: "PathError syscall.ErrnotExist",
|
||||
input: &os.PathError{Err: os.ErrNotExist},
|
||||
expected: syscall.ENOENT,
|
||||
},
|
||||
{
|
||||
name: "PathError ErrClosed",
|
||||
input: &os.PathError{Err: os.ErrClosed},
|
||||
expected: syscall.EBADF,
|
||||
},
|
||||
{
|
||||
name: "PathError unknown == syscall.EIO",
|
||||
input: &os.PathError{Err: errors.New("ice cream")},
|
||||
expected: syscall.EIO,
|
||||
},
|
||||
{
|
||||
name: "unknown == syscall.EIO",
|
||||
input: errors.New("ice cream"),
|
||||
expected: syscall.EIO,
|
||||
},
|
||||
{
|
||||
name: "very wrapped unknown == syscall.EIO",
|
||||
input: fmt.Errorf("%w", fmt.Errorf("%w", fmt.Errorf("%w", errors.New("ice cream")))),
|
||||
expected: syscall.EIO,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
errno := UnwrapOSError(tc.input)
|
||||
require.Equal(t, tc.expected, errno)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Set up the test files
|
||||
tmpDir := t.TempDir()
|
||||
require.NoError(t, fstest.WriteTestFiles(tmpDir))
|
||||
|
||||
testFS := NewDirFS(tmpDir)
|
||||
|
||||
_, err := StatPath(testFS, "cat")
|
||||
require.Equal(t, syscall.ENOENT, err)
|
||||
_, err = StatPath(testFS, "sub/cat")
|
||||
require.Equal(t, syscall.ENOENT, err)
|
||||
|
||||
s, err := StatPath(testFS, "sub/test.txt")
|
||||
require.NoError(t, err)
|
||||
require.False(t, s.IsDir())
|
||||
|
||||
s, err = StatPath(testFS, "sub")
|
||||
require.NoError(t, err)
|
||||
require.True(t, s.IsDir())
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package wasi_snapshot_preview1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/sysfs"
|
||||
)
|
||||
|
||||
// Errno is neither uint16 nor an alias for parity with wasm.ValueType.
|
||||
@@ -267,38 +266,18 @@ var errnoToString = [...]string{
|
||||
// error codes. For example, wasi-filesystem and GOOS=js don't map to these
|
||||
// Errno.
|
||||
func ToErrno(err error) Errno {
|
||||
if pe, ok := err.(*os.PathError); ok {
|
||||
err = pe.Unwrap()
|
||||
}
|
||||
if se, ok := err.(syscall.Errno); ok {
|
||||
return errnoFromSyscall(se)
|
||||
}
|
||||
// Below are all the fs.ErrXXX in fs.go. errors.Is is more expensive, so
|
||||
// try it last. Note: Once we have our own file type, we should never see
|
||||
// these.
|
||||
switch {
|
||||
case errors.Is(err, fs.ErrInvalid):
|
||||
return ErrnoInval
|
||||
case errors.Is(err, fs.ErrPermission):
|
||||
return ErrnoPerm
|
||||
case errors.Is(err, fs.ErrExist):
|
||||
return ErrnoExist
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
return ErrnoNoent
|
||||
case errors.Is(err, fs.ErrClosed):
|
||||
return ErrnoBadf
|
||||
default:
|
||||
return ErrnoIo
|
||||
}
|
||||
}
|
||||
errno := sysfs.UnwrapOSError(err)
|
||||
|
||||
func errnoFromSyscall(errno syscall.Errno) Errno {
|
||||
// The below Errno have references in existing WASI code.
|
||||
switch errno {
|
||||
case syscall.EAGAIN:
|
||||
return ErrnoAgain
|
||||
case syscall.EBADF:
|
||||
return ErrnoBadf
|
||||
case syscall.EEXIST:
|
||||
return ErrnoExist
|
||||
case syscall.EINTR:
|
||||
return ErrnoIntr
|
||||
case syscall.EINVAL:
|
||||
return ErrnoInval
|
||||
case syscall.EIO:
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package wasi_snapshot_preview1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
@@ -17,6 +13,11 @@ func TestToErrno(t *testing.T) {
|
||||
input error
|
||||
expected Errno
|
||||
}{
|
||||
{
|
||||
name: "syscall.EAGAIN",
|
||||
input: syscall.EAGAIN,
|
||||
expected: ErrnoAgain,
|
||||
},
|
||||
{
|
||||
name: "syscall.EBADF",
|
||||
input: syscall.EBADF,
|
||||
@@ -27,6 +28,11 @@ func TestToErrno(t *testing.T) {
|
||||
input: syscall.EEXIST,
|
||||
expected: ErrnoExist,
|
||||
},
|
||||
{
|
||||
name: "syscall.EINTR",
|
||||
input: syscall.EINTR,
|
||||
expected: ErrnoIntr,
|
||||
},
|
||||
{
|
||||
name: "syscall.EINVAL",
|
||||
input: syscall.EINVAL,
|
||||
@@ -87,46 +93,6 @@ func TestToErrno(t *testing.T) {
|
||||
input: syscall.Errno(0xfe),
|
||||
expected: ErrnoIo,
|
||||
},
|
||||
{
|
||||
name: "PathError ErrInvalid",
|
||||
input: &os.PathError{Err: fs.ErrInvalid},
|
||||
expected: ErrnoInval,
|
||||
},
|
||||
{
|
||||
name: "PathError ErrPermission",
|
||||
input: &os.PathError{Err: fs.ErrPermission},
|
||||
expected: ErrnoPerm,
|
||||
},
|
||||
{
|
||||
name: "PathError ErrExist",
|
||||
input: &os.PathError{Err: fs.ErrExist},
|
||||
expected: ErrnoExist,
|
||||
},
|
||||
{
|
||||
name: "PathError ErrNotExist",
|
||||
input: &os.PathError{Err: fs.ErrNotExist},
|
||||
expected: ErrnoNoent,
|
||||
},
|
||||
{
|
||||
name: "PathError ErrClosed",
|
||||
input: &os.PathError{Err: fs.ErrClosed},
|
||||
expected: ErrnoBadf,
|
||||
},
|
||||
{
|
||||
name: "PathError unknown == ErrnoIo",
|
||||
input: &os.PathError{Err: errors.New("ice cream")},
|
||||
expected: ErrnoIo,
|
||||
},
|
||||
{
|
||||
name: "unknown == ErrnoIo",
|
||||
input: errors.New("ice cream"),
|
||||
expected: ErrnoIo,
|
||||
},
|
||||
{
|
||||
name: "very wrapped unknown == ErrnoIo",
|
||||
input: fmt.Errorf("%w", fmt.Errorf("%w", fmt.Errorf("%w", errors.New("ice cream")))),
|
||||
expected: ErrnoIo,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
Reference in New Issue
Block a user