Skip to content

Commit

Permalink
adds experimental sys.Errno to begin decoupling from the syscall pack…
Browse files Browse the repository at this point in the history
…age (#1582)

Signed-off-by: Adrian Cole <adrian@tetrate.io>
  • Loading branch information
codefromthecrypt committed Jul 17, 2023
1 parent 1dafce0 commit 2f8dd23
Show file tree
Hide file tree
Showing 94 changed files with 1,583 additions and 1,366 deletions.
2 changes: 1 addition & 1 deletion cmd/wazero/wazero_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ Consider switching to GOOS=wasip1.
==> go.runtime.getRandomData(r_len=8)
<==
==> go.syscall/js.valueCall(fs.open(path=/bear.txt,flags=,perm=----------))
<== (err=function not implemented,fd=0)
<== (err=functionality not supported,fd=0)
`, // Test only shows logging happens in two scopes; it is ok to fail.
expectedExitCode: 1,
}
Expand Down
95 changes: 95 additions & 0 deletions experimental/sys/errno.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package sys

import "strconv"

// Errno is a subset of POSIX errno used by wazero interfaces. Zero is not an
// error. Other values should not be interpreted numerically, rather by constants
// prefixed with 'E'.
//
// See https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/errno.h.html
type Errno uint16

// ^-- Note: This will eventually move to the public /sys package. It is
// experimental until we audit the socket related APIs to ensure we have all
// the Errno it returns, and we export fs.FS. This is not in /internal/sys as
// that would introduce a package cycle.

// This is a subset of errors to reduce implementation burden. `wasip` defines
// almost all POSIX error numbers, but not all are used in practice. wazero
// will add ones needed in POSIX order, as needed by functions that explicitly
// document returning them.
//
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-errno-enumu16
const (
EACCES Errno = iota + 1
EAGAIN
EBADF
EEXIST
EFAULT
EINTR
EINVAL
EIO
EISDIR
ELOOP
ENAMETOOLONG
ENOENT
ENOSYS
ENOTDIR
ENOTEMPTY
ENOTSOCK
ENOTSUP
EPERM
EROFS

// NOTE ENOTCAPABLE is defined in wasip1, but not in POSIX. wasi-libc
// converts it to EBADF, ESPIPE or EINVAL depending on the call site.
// It isn't known if compilers who don't use ENOTCAPABLE would crash on it.
)

// Error implements error
func (e Errno) Error() string {
switch e {
case 0: // not an error
return "success"
case EACCES:
return "permission denied"
case EAGAIN:
return "resource unavailable, try again"
case EBADF:
return "bad file descriptor"
case EEXIST:
return "file exists"
case EFAULT:
return "bad address"
case EINTR:
return "interrupted function"
case EINVAL:
return "invalid argument"
case EIO:
return "input/output error"
case EISDIR:
return "is a directory"
case ELOOP:
return "too many levels of symbolic links"
case ENAMETOOLONG:
return "filename too long"
case ENOENT:
return "no such file or directory"
case ENOSYS:
return "functionality not supported"
case ENOTDIR:
return "not a directory or a symbolic link to a directory"
case ENOTEMPTY:
return "directory not empty"
case ENOTSOCK:
return "not a socket"
case ENOTSUP:
return "not supported (may be the same value as [EOPNOTSUPP])"
case EPERM:
return "operation not permitted"
case EROFS:
return "read-only file system"
default:
return "Errno(" + strconv.Itoa(int(e)) + ")"
}
}
27 changes: 10 additions & 17 deletions internal/platform/error.go → experimental/sys/error.go
Original file line number Diff line number Diff line change
@@ -1,39 +1,32 @@
package platform
package sys

import (
"io"
"io/fs"
"os"
"syscall"
)

// UnwrapOSError returns a syscall.Errno or zero if the input is nil.
func UnwrapOSError(err error) syscall.Errno {
// UnwrapOSError returns an Errno or zero if the input is nil.
func UnwrapOSError(err error) Errno {
if err == nil {
return 0
}
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, io.EOF:
return 0 // EOF is not a syscall.Errno
return 0 // EOF is not a Errno
case fs.ErrInvalid:
return syscall.EINVAL
return EINVAL
case fs.ErrPermission:
return syscall.EPERM
return EPERM
case fs.ErrExist:
return syscall.EEXIST
return EEXIST
case fs.ErrNotExist:
return syscall.ENOENT
return ENOENT
case fs.ErrClosed:
return syscall.EBADF
return EBADF
}
return syscall.EIO
return errorToErrno(err)
}

// underlyingError returns the underlying error if a well-known OS error type.
Expand Down
46 changes: 24 additions & 22 deletions internal/platform/error_test.go → experimental/sys/error_test.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
package platform
package sys

import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"syscall"
"testing"

"github.com/tetratelabs/wazero/internal/testing/require"
)

func TestUnwrapOSError(t *testing.T) {
tests := []struct {
name string
input error
expected syscall.Errno
expected Errno
}{
{
name: "io.EOF is not an error",
Expand All @@ -26,64 +23,69 @@ func TestUnwrapOSError(t *testing.T) {
{
name: "LinkError ErrInvalid",
input: &os.LinkError{Err: fs.ErrInvalid},
expected: syscall.EINVAL,
expected: EINVAL,
},
{
name: "PathError ErrInvalid",
input: &os.PathError{Err: fs.ErrInvalid},
expected: syscall.EINVAL,
expected: EINVAL,
},
{
name: "SyscallError ErrInvalid",
input: &os.SyscallError{Err: fs.ErrInvalid},
expected: syscall.EINVAL,
expected: EINVAL,
},
{
name: "PathError ErrPermission",
input: &os.PathError{Err: os.ErrPermission},
expected: syscall.EPERM,
expected: EPERM,
},
{
name: "PathError ErrExist",
input: &os.PathError{Err: os.ErrExist},
expected: syscall.EEXIST,
expected: EEXIST,
},
{
name: "PathError syscall.ErrnotExist",
name: "PathError ErrnotExist",
input: &os.PathError{Err: os.ErrNotExist},
expected: syscall.ENOENT,
expected: ENOENT,
},
{
name: "PathError ErrClosed",
input: &os.PathError{Err: os.ErrClosed},
expected: syscall.EBADF,
expected: EBADF,
},
{
name: "PathError unknown == syscall.EIO",
name: "PathError unknown == EIO",
input: &os.PathError{Err: errors.New("ice cream")},
expected: syscall.EIO,
expected: EIO,
},
{
name: "unknown == syscall.EIO",
name: "unknown == EIO",
input: errors.New("ice cream"),
expected: syscall.EIO,
expected: EIO,
},
{
name: "very wrapped unknown == syscall.EIO",
name: "very wrapped unknown == EIO",
input: fmt.Errorf("%w", fmt.Errorf("%w", fmt.Errorf("%w", errors.New("ice cream")))),
expected: syscall.EIO,
expected: EIO,
},
}

for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
errno := UnwrapOSError(tc.input)
require.EqualErrno(t, tc.expected, errno)
// don't use require package as that introduces a package cycle
if want, have := tc.expected, UnwrapOSError(tc.input); have != want {
t.Fatalf("unexpected errno: %v != %v", have, want)
}
})
}

t.Run("nil -> zero", func(t *testing.T) {
require.Zero(t, UnwrapOSError(nil))
// don't use require package as that introduces a package cycle
if want, have := Errno(0), UnwrapOSError(nil); have != want {
t.Fatalf("unexpected errno: %v != %v", have, want)
}
})
}
98 changes: 98 additions & 0 deletions experimental/sys/syscall_errno.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package sys

import "syscall"

func syscallToErrno(errno syscall.Errno) Errno {
switch errno {
case 0:
return 0
case syscall.EACCES:
return EACCES
case syscall.EAGAIN:
return EAGAIN
case syscall.EBADF:
return EBADF
case syscall.EEXIST:
return EEXIST
case syscall.EFAULT:
return EFAULT
case syscall.EINTR:
return EINTR
case syscall.EINVAL:
return EINVAL
case syscall.EIO:
return EIO
case syscall.EISDIR:
return EISDIR
case syscall.ELOOP:
return ELOOP
case syscall.ENAMETOOLONG:
return ENAMETOOLONG
case syscall.ENOENT:
return ENOENT
case syscall.ENOSYS:
return ENOSYS
case syscall.ENOTDIR:
return ENOTDIR
case syscall.ENOTEMPTY:
return ENOTEMPTY
case syscall.ENOTSOCK:
return ENOTSOCK
case syscall.ENOTSUP:
return ENOTSUP
case syscall.EPERM:
return EPERM
case syscall.EROFS:
return EROFS
default:
return EIO
}
}

// Unwrap is a convenience for runtime.GOOS which define syscall.Errno.
func (e Errno) Unwrap() error {
switch e {
case 0:
return nil
case EACCES:
return syscall.EACCES
case EAGAIN:
return syscall.EAGAIN
case EBADF:
return syscall.EBADF
case EEXIST:
return syscall.EEXIST
case EFAULT:
return syscall.EFAULT
case EINTR:
return syscall.EINTR
case EINVAL:
return syscall.EINVAL
case EIO:
return syscall.EIO
case EISDIR:
return syscall.EISDIR
case ELOOP:
return syscall.ELOOP
case ENAMETOOLONG:
return syscall.ENAMETOOLONG
case ENOENT:
return syscall.ENOENT
case ENOSYS:
return syscall.ENOSYS
case ENOTDIR:
return syscall.ENOTDIR
case ENOTEMPTY:
return syscall.ENOTEMPTY
case ENOTSOCK:
return syscall.ENOTSOCK
case ENOTSUP:
return syscall.ENOTSUP
case EPERM:
return syscall.EPERM
case EROFS:
return syscall.EROFS
default:
return syscall.EIO
}
}
16 changes: 16 additions & 0 deletions experimental/sys/syscall_errno_notwindows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build !windows

package sys

import "syscall"

func errorToErrno(err error) Errno {
switch err := err.(type) {
case Errno:
return err
case syscall.Errno:
return syscallToErrno(err)
default:
return EIO
}
}
Loading

0 comments on commit 2f8dd23

Please sign in to comment.