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:
Crypt Keeper
2023-02-09 19:50:05 -10:00
committed by GitHub
parent e2fde2500e
commit 882b764437
16 changed files with 289 additions and 269 deletions

View File

@@ -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)
}

View File

@@ -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:

View File

@@ -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 {

View 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)
})
}

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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
}
}
}

View File

@@ -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
}

View File

@@ -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())
}

View File

@@ -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:

View File

@@ -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 {