From 4ca0858e578fee8353e4c6e7ba0601e44ca7b36a Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Tue, 21 Feb 2023 10:13:37 +0800 Subject: [PATCH] sysfs: adds FS.Stat and companions in platform (#1140) This centralizes filestat logic by making our own `Stat_t` similar to `syscall.Stat_t`. This exposes utilities in the platform package and adds a new function `FS.Stat` which avoids having to use `fs.File` to get the same info. Doing so at the FS abstraction allows us to optimize how it is implemented internally using portable means (e.g. `os.StatFile`) or OS-specific means where necessary, e.g. in windows. This also ensures `platform.OpenFile` returns syscall.Errno and centralizes error checking with a new `require.EqualErrno` test. Signed-off-by: Adrian Cole --- .github/workflows/integration.yaml | 4 + Makefile | 4 +- imports/wasi_snapshot_preview1/args_test.go | 8 +- imports/wasi_snapshot_preview1/clock_test.go | 12 +- .../wasi_snapshot_preview1/environ_test.go | 8 +- imports/wasi_snapshot_preview1/fs.go | 69 +++---- imports/wasi_snapshot_preview1/fs_test.go | 192 +++++++++--------- imports/wasi_snapshot_preview1/poll_test.go | 4 +- imports/wasi_snapshot_preview1/random_test.go | 6 +- imports/wasi_snapshot_preview1/sched_test.go | 2 +- imports/wasi_snapshot_preview1/wasi_test.go | 4 +- internal/fstest/fstest.go | 9 +- internal/gojs/errno.go | 4 +- internal/gojs/errno_test.go | 7 +- internal/gojs/fs.go | 36 ++-- internal/platform/errno.go | 7 + internal/platform/errno_windows.go | 64 ++++++ internal/platform/error.go | 50 +++++ internal/platform/error_test.go | 83 ++++++++ internal/platform/open_file.go | 7 +- internal/platform/open_file_js.go | 6 +- internal/platform/open_file_test.go | 8 +- internal/platform/open_file_windows.go | 44 ++-- internal/platform/rename_test.go | 16 +- internal/platform/stat.go | 100 +++++++-- internal/platform/stat_bsd.go | 22 +- internal/platform/stat_linux.go | 21 +- internal/platform/stat_test.go | 159 +++++++++++---- internal/platform/stat_unsupported.go | 14 +- internal/platform/stat_windows.go | 39 +--- internal/sys/fs.go | 11 +- internal/sysfs/adapter.go | 15 +- internal/sysfs/adapter_test.go | 22 +- internal/sysfs/dirfs.go | 37 ++-- internal/sysfs/dirfs_test.go | 51 +++-- internal/sysfs/readfs.go | 7 + internal/sysfs/readfs_test.go | 21 +- internal/sysfs/rootfs.go | 40 ++-- internal/sysfs/rootfs_test.go | 17 +- internal/sysfs/syscall.go | 12 -- internal/sysfs/syscall_windows.go | 69 +------ internal/sysfs/sysfs.go | 77 ++----- internal/sysfs/sysfs_test.go | 158 ++++---------- internal/sysfs/unsupported.go | 7 + internal/testing/require/require.go | 14 ++ .../testing/require/require_errno_test.go | 66 ++++++ internal/wasi_snapshot_preview1/errno.go | 4 +- internal/wasi_snapshot_preview1/errno_test.go | 9 +- 48 files changed, 967 insertions(+), 679 deletions(-) create mode 100644 internal/platform/errno.go create mode 100644 internal/platform/errno_windows.go create mode 100644 internal/platform/error.go create mode 100644 internal/platform/error_test.go create mode 100644 internal/testing/require/require_errno_test.go diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index 31c67e77..77063475 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -234,6 +234,10 @@ jobs: - name: Install wazero run: go install ./cmd/wazero + # This runs the previously compiled TinyGo tests with wazero. If you need + # to troubleshoot one, you can add "-hostlogging=filesystem" and also a + # trailing argument narrowing which test to execute. + # e.g. "-test.run '^TestStatBadDir$'" - name: Run standard library tests run: | for bin in *.test; do diff --git a/Makefile b/Makefile index 2d635d05..bba12b22 100644 --- a/Makefile +++ b/Makefile @@ -220,7 +220,9 @@ check: # This makes sure the intepreter can be used. Most often the package that can # drift here is "platform" or "sysfs": # -# Ensure we build on an arbitrary operating system +# Ensure we build on windows: + @GOARCH=amd64 GOOS=windows go build ./... +# Ensure we build on an arbitrary operating system: @GOARCH=amd64 GOOS=dragonfly go build ./... # Ensure we build on linux arm for Dapr: # gh release view -R dapr/dapr --json assets --jq 'first(.assets[] | select(.name = "daprd_linux_arm.tar.gz") | {url, downloadCount})' diff --git a/imports/wasi_snapshot_preview1/args_test.go b/imports/wasi_snapshot_preview1/args_test.go index 0173cda2..c6478fb1 100644 --- a/imports/wasi_snapshot_preview1/args_test.go +++ b/imports/wasi_snapshot_preview1/args_test.go @@ -26,7 +26,7 @@ func Test_argsGet(t *testing.T) { maskMemory(t, mod, len(expectedMemory)+int(argvBuf)) // Invoke argsGet and check the memory side effects. - requireErrno(t, ErrnoSuccess, mod, ArgsGetName, uint64(argv), uint64(argvBuf)) + requireErrnoResult(t, ErrnoSuccess, mod, ArgsGetName, uint64(argv), uint64(argvBuf)) require.Equal(t, ` ==> wasi_snapshot_preview1.args_get(argv=22,argv_buf=16) <== errno=ESUCCESS @@ -95,7 +95,7 @@ func Test_argsGet_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { defer log.Reset() - requireErrno(t, ErrnoFault, mod, ArgsGetName, uint64(tc.argv), uint64(tc.argvBuf)) + requireErrnoResult(t, ErrnoFault, mod, ArgsGetName, uint64(tc.argv), uint64(tc.argvBuf)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -118,7 +118,7 @@ func Test_argsSizesGet(t *testing.T) { maskMemory(t, mod, int(resultArgc)+len(expectedMemory)) // Invoke argsSizesGet and check the memory side effects. - requireErrno(t, ErrnoSuccess, mod, ArgsSizesGetName, uint64(resultArgc), uint64(resultArgvLen)) + requireErrnoResult(t, ErrnoSuccess, mod, ArgsSizesGetName, uint64(resultArgc), uint64(resultArgvLen)) require.Equal(t, ` ==> wasi_snapshot_preview1.args_sizes_get(result.argc=16,result.argv_len=21) <== errno=ESUCCESS @@ -185,7 +185,7 @@ func Test_argsSizesGet_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { defer log.Reset() - requireErrno(t, ErrnoFault, mod, ArgsSizesGetName, uint64(tc.argc), uint64(tc.argvLen)) + requireErrnoResult(t, ErrnoFault, mod, ArgsSizesGetName, uint64(tc.argc), uint64(tc.argvLen)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } diff --git a/imports/wasi_snapshot_preview1/clock_test.go b/imports/wasi_snapshot_preview1/clock_test.go index 52b755ac..71dc438a 100644 --- a/imports/wasi_snapshot_preview1/clock_test.go +++ b/imports/wasi_snapshot_preview1/clock_test.go @@ -60,7 +60,7 @@ func Test_clockResGet(t *testing.T) { resultResolution := 16 // arbitrary offset maskMemory(t, mod, resultResolution+len(tc.expectedMemory)) - requireErrno(t, ErrnoSuccess, mod, ClockResGetName, uint64(tc.clockID), uint64(resultResolution)) + requireErrnoResult(t, ErrnoSuccess, mod, ClockResGetName, uint64(tc.clockID), uint64(resultResolution)) require.Equal(t, tc.expectedLog, "\n"+log.String()) actual, ok := mod.Memory().Read(uint32(resultResolution-1), uint32(len(tc.expectedMemory))) @@ -115,7 +115,7 @@ func Test_clockResGet_Unsupported(t *testing.T) { defer log.Reset() resultResolution := 16 // arbitrary offset - requireErrno(t, tc.expectedErrno, mod, ClockResGetName, uint64(tc.clockID), uint64(resultResolution)) + requireErrnoResult(t, tc.expectedErrno, mod, ClockResGetName, uint64(tc.clockID), uint64(resultResolution)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -167,7 +167,7 @@ func Test_clockTimeGet(t *testing.T) { resultTimestamp := 16 // arbitrary offset maskMemory(t, mod, resultTimestamp+len(tc.expectedMemory)) - requireErrno(t, ErrnoSuccess, mod, ClockTimeGetName, uint64(tc.clockID), 0 /* TODO: precision */, uint64(resultTimestamp)) + requireErrnoResult(t, ErrnoSuccess, mod, ClockTimeGetName, uint64(tc.clockID), 0 /* TODO: precision */, uint64(resultTimestamp)) require.Equal(t, tc.expectedLog, "\n"+log.String()) actual, ok := mod.Memory().Read(uint32(resultTimestamp-1), uint32(len(tc.expectedMemory))) @@ -186,7 +186,7 @@ func Test_clockTimeGet_monotonic(t *testing.T) { getMonotonicTime := func() uint64 { const offset uint32 = 0 - requireErrno(t, ErrnoSuccess, mod, ClockTimeGetName, uint64(ClockIDMonotonic), + requireErrnoResult(t, ErrnoSuccess, mod, ClockTimeGetName, uint64(ClockIDMonotonic), 0 /* TODO: precision */, uint64(offset)) timestamp, ok := mod.Memory().ReadUint64Le(offset) require.True(t, ok) @@ -249,7 +249,7 @@ func Test_clockTimeGet_Unsupported(t *testing.T) { defer log.Reset() resultTimestamp := 16 // arbitrary offset - requireErrno(t, tc.expectedErrno, mod, ClockTimeGetName, uint64(tc.clockID), uint64(0) /* TODO: precision */, uint64(resultTimestamp)) + requireErrnoResult(t, tc.expectedErrno, mod, ClockTimeGetName, uint64(tc.clockID), uint64(0) /* TODO: precision */, uint64(resultTimestamp)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -290,7 +290,7 @@ func Test_clockTimeGet_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { defer log.Reset() - requireErrno(t, ErrnoFault, mod, ClockTimeGetName, uint64(0) /* TODO: id */, uint64(0) /* TODO: precision */, uint64(tc.resultTimestamp)) + requireErrnoResult(t, ErrnoFault, mod, ClockTimeGetName, uint64(0) /* TODO: id */, uint64(0) /* TODO: precision */, uint64(tc.resultTimestamp)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } diff --git a/imports/wasi_snapshot_preview1/environ_test.go b/imports/wasi_snapshot_preview1/environ_test.go index a5c11a43..19cd54e7 100644 --- a/imports/wasi_snapshot_preview1/environ_test.go +++ b/imports/wasi_snapshot_preview1/environ_test.go @@ -28,7 +28,7 @@ func Test_environGet(t *testing.T) { maskMemory(t, mod, len(expectedMemory)+int(resultEnvironBuf)) // Invoke environGet and check the memory side effects. - requireErrno(t, ErrnoSuccess, mod, EnvironGetName, uint64(resultEnviron), uint64(resultEnvironBuf)) + requireErrnoResult(t, ErrnoSuccess, mod, EnvironGetName, uint64(resultEnviron), uint64(resultEnvironBuf)) require.Equal(t, ` ==> wasi_snapshot_preview1.environ_get(environ=26,environ_buf=16) <== errno=ESUCCESS @@ -98,7 +98,7 @@ func Test_environGet_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { defer log.Reset() - requireErrno(t, ErrnoFault, mod, EnvironGetName, uint64(tc.environ), uint64(tc.environBuf)) + requireErrnoResult(t, ErrnoFault, mod, EnvironGetName, uint64(tc.environ), uint64(tc.environBuf)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -122,7 +122,7 @@ func Test_environSizesGet(t *testing.T) { maskMemory(t, mod, len(expectedMemory)+int(resultEnvironc)) // Invoke environSizesGet and check the memory side effects. - requireErrno(t, ErrnoSuccess, mod, EnvironSizesGetName, uint64(resultEnvironc), uint64(resultEnvironvLen)) + requireErrnoResult(t, ErrnoSuccess, mod, EnvironSizesGetName, uint64(resultEnvironc), uint64(resultEnvironvLen)) require.Equal(t, ` ==> wasi_snapshot_preview1.environ_sizes_get(result.environc=16,result.environv_len=21) <== errno=ESUCCESS @@ -190,7 +190,7 @@ func Test_environSizesGet_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { defer log.Reset() - requireErrno(t, ErrnoFault, mod, EnvironSizesGetName, uint64(tc.environc), uint64(tc.environLen)) + requireErrnoResult(t, ErrnoFault, mod, EnvironSizesGetName, uint64(tc.environc), uint64(tc.environLen)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go index 1bce1ff2..4d3c575a 100644 --- a/imports/wasi_snapshot_preview1/fs.go +++ b/imports/wasi_snapshot_preview1/fs.go @@ -6,7 +6,6 @@ import ( "io" "io/fs" "math" - "os" pathutil "path" "reflect" "syscall" @@ -97,28 +96,22 @@ func fdAllocateFn(_ context.Context, mod api.Module, params []uint64) Errno { return ErrnoInval } - st, err := f.Stat() - if err != nil { + var st platform.Stat_t + if err := f.Stat(&st); err != nil { return ToErrno(err) } - if st.Size() >= tail { + if st.Size >= tail { // We already have enough space. return ErrnoSuccess } - // This is implemented the implementation of fs.File by all platforms. - // TODO: this should be removed once we have fs.File. - type truncatable interface { - Truncate(size int64) error - } - - osf, ok := f.File.(truncatable) + osf, ok := f.File.(truncater) if !ok { return ErrnoBadf } - if err = osf.Truncate(tail); err != nil { + if err := osf.Truncate(tail); err != nil { return ToErrno(err) } return ErrnoSuccess @@ -357,12 +350,12 @@ func fdFilestatGetFunc(mod api.Module, fd, resultBuf uint32) Errno { return ErrnoBadf } - stat, err := f.Stat() - if err != nil { + var st platform.Stat_t + if err := f.Stat(&st); err != nil { return ToErrno(err) } - if err = writeFilestat(buf, f.File, stat); err != nil { + if err := writeFilestat(buf, &st); err != nil { return ToErrno(err) } @@ -385,22 +378,15 @@ func getWasiFiletype(fileMode fs.FileMode) uint8 { return wasiFileType } -func writeFilestat(buf []byte, f fs.File, stat fs.FileInfo) (err error) { - filetype := getWasiFiletype(stat.Mode()) - filesize := uint64(stat.Size()) - atimeNsec, mtimeNsec, ctimeNsec, nlink, device, inode, err := platform.Stat(f, stat) - if err != nil { - return err - } - - le.PutUint64(buf, device) - le.PutUint64(buf[8:], inode) - le.PutUint64(buf[16:], uint64(filetype)) - le.PutUint64(buf[24:], nlink) - le.PutUint64(buf[32:], filesize) - le.PutUint64(buf[40:], uint64(atimeNsec)) - le.PutUint64(buf[48:], uint64(mtimeNsec)) - le.PutUint64(buf[56:], uint64(ctimeNsec)) +func writeFilestat(buf []byte, st *platform.Stat_t) (err error) { + le.PutUint64(buf, st.Dev) + le.PutUint64(buf[8:], st.Ino) + le.PutUint64(buf[16:], uint64(getWasiFiletype(st.Mode))) + le.PutUint64(buf[24:], st.Nlink) + le.PutUint64(buf[32:], uint64(st.Size)) + le.PutUint64(buf[40:], uint64(st.Atim)) + le.PutUint64(buf[48:], uint64(st.Mtim)) + le.PutUint64(buf[56:], uint64(st.Ctim)) return } @@ -491,16 +477,15 @@ func fdFilestatSetTimesFn(_ context.Context, mod api.Module, params []uint64) Er // Handle if either parameter should be taken from stat. if statAtime || statMtime { // Get the current timestamp via Stat in order to un-change after calling FS.Utimes(). - st, err := f.Stat() - if err != nil { - return ErrnoBadf + var st platform.Stat_t + if err := f.Stat(&st); err != nil { + return ToErrno(err) } - atimeNsec, mtimeNsec, _ := platform.StatTimes(st) if statAtime { - atime = atimeNsec + atime = st.Atim } if statMtime { - mtime = mtimeNsec + mtime = st.Mtim } } @@ -1418,12 +1403,8 @@ func pathFilestatGetFn(_ context.Context, mod api.Module, params []uint64) Errno } // Stat the file without allocating a file descriptor - f, err := preopen.OpenFile(pathName, os.O_RDONLY, 0) - if err != nil { - return ToErrno(err) - } - stat, err := sysfs.StatFile(f) - if err != nil { + var st platform.Stat_t + if err := preopen.Stat(pathName, &st); err != nil { return ToErrno(err) } @@ -1434,7 +1415,7 @@ func pathFilestatGetFn(_ context.Context, mod api.Module, params []uint64) Errno return ErrnoFault } - if err = writeFilestat(buf, f, stat); err != nil { + if err := writeFilestat(buf, &st); err != nil { return ToErrno(err) } return ErrnoSuccess diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go index 0cf69596..f0a1e98a 100644 --- a/imports/wasi_snapshot_preview1/fs_test.go +++ b/imports/wasi_snapshot_preview1/fs_test.go @@ -27,14 +27,14 @@ import ( func Test_fdAdvise(t *testing.T) { mod, r, _ := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fstest.FS)) defer r.Close(testCtx) - requireErrno(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceNormal)) - requireErrno(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceSequential)) - requireErrno(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceRandom)) - requireErrno(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceWillNeed)) - requireErrno(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceDontNeed)) - requireErrno(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceNoReuse)) - requireErrno(t, ErrnoInval, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceNoReuse+1)) - requireErrno(t, ErrnoBadf, mod, FdAdviseName, uint64(1111111), 0, 0, uint64(FdAdviceNoReuse+1)) + requireErrnoResult(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceNormal)) + requireErrnoResult(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceSequential)) + requireErrnoResult(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceRandom)) + requireErrnoResult(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceWillNeed)) + requireErrnoResult(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceDontNeed)) + requireErrnoResult(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceNoReuse)) + requireErrnoResult(t, ErrnoInval, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceNoReuse+1)) + requireErrnoResult(t, ErrnoBadf, mod, FdAdviseName, uint64(1111111), 0, 0, uint64(FdAdviceNoReuse+1)) } // Test_fdAllocate only tests it is stubbed for GrainLang per #271 @@ -60,17 +60,17 @@ func Test_fdAllocate(t *testing.T) { require.True(t, ok) requireSizeEqual := func(exp int64) { - st, err := f.Stat() - require.NoError(t, err) - require.Equal(t, exp, st.Size()) + var st platform.Stat_t + require.NoError(t, f.Stat(&st)) + require.Equal(t, exp, st.Size) } t.Run("errors", func(t *testing.T) { - requireErrno(t, ErrnoBadf, mod, FdAllocateName, uint64(12345), 0, 0) + requireErrnoResult(t, ErrnoBadf, mod, FdAllocateName, uint64(12345), 0, 0) minusOne := int64(-1) - requireErrno(t, ErrnoInval, mod, FdAllocateName, uint64(fd), uint64(minusOne), uint64(minusOne)) - requireErrno(t, ErrnoInval, mod, FdAllocateName, uint64(fd), 0, uint64(minusOne)) - requireErrno(t, ErrnoInval, mod, FdAllocateName, uint64(fd), uint64(minusOne), 0) + requireErrnoResult(t, ErrnoInval, mod, FdAllocateName, uint64(fd), uint64(minusOne), uint64(minusOne)) + requireErrnoResult(t, ErrnoInval, mod, FdAllocateName, uint64(fd), 0, uint64(minusOne)) + requireErrnoResult(t, ErrnoInval, mod, FdAllocateName, uint64(fd), uint64(minusOne), 0) }) t.Run("do not change size", func(t *testing.T) { @@ -81,7 +81,7 @@ func Test_fdAllocate(t *testing.T) { {offset: 10, length: 0}, } { // This shouldn't change the size. - requireErrno(t, ErrnoSuccess, mod, FdAllocateName, + requireErrnoResult(t, ErrnoSuccess, mod, FdAllocateName, uint64(fd), tc.offset, tc.length) requireSizeEqual(10) } @@ -89,7 +89,7 @@ func Test_fdAllocate(t *testing.T) { t.Run("increase", func(t *testing.T) { // 10 + 10 > the current size -> increase the size. - requireErrno(t, ErrnoSuccess, mod, FdAllocateName, + requireErrnoResult(t, ErrnoSuccess, mod, FdAllocateName, uint64(fd), 10, 10) requireSizeEqual(20) @@ -138,7 +138,7 @@ func Test_fdClose(t *testing.T) { require.NoError(t, err) // Close - requireErrno(t, ErrnoSuccess, mod, FdCloseName, uint64(fdToClose)) + requireErrnoResult(t, ErrnoSuccess, mod, FdCloseName, uint64(fdToClose)) require.Equal(t, ` ==> wasi_snapshot_preview1.fd_close(fd=4) <== errno=ESUCCESS @@ -154,7 +154,7 @@ func Test_fdClose(t *testing.T) { log.Reset() t.Run("ErrnoBadF for an invalid FD", func(t *testing.T) { - requireErrno(t, ErrnoBadf, mod, FdCloseName, uint64(42)) // 42 is an arbitrary invalid FD + requireErrnoResult(t, ErrnoBadf, mod, FdCloseName, uint64(42)) // 42 is an arbitrary invalid FD require.Equal(t, ` ==> wasi_snapshot_preview1.fd_close(fd=42) <== errno=EBADF @@ -162,7 +162,7 @@ func Test_fdClose(t *testing.T) { }) log.Reset() t.Run("ErrnoNotsup for a preopen", func(t *testing.T) { - requireErrno(t, ErrnoNotsup, mod, FdCloseName, uint64(sys.FdPreopen)) + requireErrnoResult(t, ErrnoNotsup, mod, FdCloseName, uint64(sys.FdPreopen)) require.Equal(t, ` ==> wasi_snapshot_preview1.fd_close(fd=3) <== errno=ENOTSUP @@ -208,7 +208,7 @@ func Test_fdDatasync(t *testing.T) { t.Run(tc.name, func(t *testing.T) { defer log.Reset() - requireErrno(t, tc.expectedErrno, mod, FdDatasyncName, uint64(tc.fd)) + requireErrnoResult(t, tc.expectedErrno, mod, FdDatasyncName, uint64(tc.fd)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -350,7 +350,7 @@ func Test_fdFdstatGet(t *testing.T) { maskMemory(t, mod, len(tc.expectedMemory)) - requireErrno(t, tc.expectedErrno, mod, FdFdstatGetName, uint64(tc.fd), uint64(tc.resultFdstat)) + requireErrnoResult(t, tc.expectedErrno, mod, FdFdstatGetName, uint64(tc.fd), uint64(tc.resultFdstat)) require.Equal(t, tc.expectedLog, "\n"+log.String()) actual, ok := mod.Memory().Read(0, uint32(len(tc.expectedMemory))) @@ -398,7 +398,7 @@ func Test_fdFdstatSetFlags(t *testing.T) { ok := mod.Memory().Write(0, initialMemory) require.True(t, ok) - requireErrno(t, ErrnoSuccess, mod, FdWriteName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultNwritten)) + requireErrnoResult(t, ErrnoSuccess, mod, FdWriteName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultNwritten)) require.Equal(t, ` ==> wasi_snapshot_preview1.fd_write(fd=4,iovs=1,iovs_len=2) <== (nwritten=6,errno=ESUCCESS) @@ -417,7 +417,7 @@ func Test_fdFdstatSetFlags(t *testing.T) { requireFileContent("0123456789" + "wazero") // Let's remove O_APPEND. - requireErrno(t, ErrnoSuccess, mod, FdFdstatSetFlagsName, uint64(fd), uint64(0)) + requireErrnoResult(t, ErrnoSuccess, mod, FdFdstatSetFlagsName, uint64(fd), uint64(0)) require.Equal(t, ` ==> wasi_snapshot_preview1.fd_fdstat_set_flags(fd=4,flags=0) <== errno=ESUCCESS @@ -429,7 +429,7 @@ func Test_fdFdstatSetFlags(t *testing.T) { requireFileContent("wazero6789" + "wazero") // Restore the O_APPEND flag. - requireErrno(t, ErrnoSuccess, mod, FdFdstatSetFlagsName, uint64(fd), uint64(FD_APPEND)) + requireErrnoResult(t, ErrnoSuccess, mod, FdFdstatSetFlagsName, uint64(fd), uint64(FD_APPEND)) require.Equal(t, ` ==> wasi_snapshot_preview1.fd_fdstat_set_flags(fd=4,flags=1) <== errno=ESUCCESS @@ -441,12 +441,12 @@ func Test_fdFdstatSetFlags(t *testing.T) { requireFileContent("wazero6789" + "wazero" + "wazero") t.Run("errors", func(t *testing.T) { - requireErrno(t, ErrnoInval, mod, FdFdstatSetFlagsName, uint64(fd), uint64(FD_DSYNC)) - requireErrno(t, ErrnoInval, mod, FdFdstatSetFlagsName, uint64(fd), uint64(FD_NONBLOCK)) - requireErrno(t, ErrnoInval, mod, FdFdstatSetFlagsName, uint64(fd), uint64(FD_RSYNC)) - requireErrno(t, ErrnoInval, mod, FdFdstatSetFlagsName, uint64(fd), uint64(FD_SYNC)) - requireErrno(t, ErrnoBadf, mod, FdFdstatSetFlagsName, uint64(12345), uint64(FD_APPEND)) - requireErrno(t, ErrnoIsdir, mod, FdFdstatSetFlagsName, uint64(3) /* preopen */, uint64(FD_APPEND)) + requireErrnoResult(t, ErrnoInval, mod, FdFdstatSetFlagsName, uint64(fd), uint64(FD_DSYNC)) + requireErrnoResult(t, ErrnoInval, mod, FdFdstatSetFlagsName, uint64(fd), uint64(FD_NONBLOCK)) + requireErrnoResult(t, ErrnoInval, mod, FdFdstatSetFlagsName, uint64(fd), uint64(FD_RSYNC)) + requireErrnoResult(t, ErrnoInval, mod, FdFdstatSetFlagsName, uint64(fd), uint64(FD_SYNC)) + requireErrnoResult(t, ErrnoBadf, mod, FdFdstatSetFlagsName, uint64(12345), uint64(FD_APPEND)) + requireErrnoResult(t, ErrnoIsdir, mod, FdFdstatSetFlagsName, uint64(3) /* preopen */, uint64(FD_APPEND)) }) } @@ -622,7 +622,7 @@ func Test_fdFilestatGet(t *testing.T) { maskMemory(t, mod, len(tc.expectedMemory)) - requireErrno(t, tc.expectedErrno, mod, FdFilestatGetName, uint64(tc.fd), uint64(tc.resultFilestat)) + requireErrnoResult(t, tc.expectedErrno, mod, FdFilestatGetName, uint64(tc.fd), uint64(tc.resultFilestat)) require.Equal(t, tc.expectedLog, "\n"+log.String()) actual, ok := mod.Memory().Read(0, uint32(len(tc.expectedMemory))) @@ -697,7 +697,7 @@ func Test_fdFilestatSetSize(t *testing.T) { if filepath == "badf" { fd++ } - requireErrno(t, tc.expectedErrno, mod, FdFilestatSetSizeName, uint64(fd), uint64(tc.size)) + requireErrnoResult(t, tc.expectedErrno, mod, FdFilestatSetSizeName, uint64(fd), uint64(tc.size)) actual, err := os.ReadFile(path.Join(tmpDir, filepath)) require.NoError(t, err) @@ -843,11 +843,12 @@ func Test_fdFilestatSetTimes(t *testing.T) { f, ok := fsc.LookupFile(fd) require.True(t, ok) - stat, err := f.Stat() - require.NoError(t, err) - prevAtime, prevMtime, _ := platform.StatTimes(stat) - requireErrno(t, tc.expectedErrno, mod, FdFilestatSetTimesName, + var st platform.Stat_t + require.NoError(t, f.Stat(&st)) + prevAtime, prevMtime := st.Atim, st.Mtim + + requireErrnoResult(t, tc.expectedErrno, mod, FdFilestatSetTimesName, uint64(paramFd), uint64(tc.atime), uint64(tc.mtime), uint64(tc.flags), ) @@ -855,22 +856,22 @@ func Test_fdFilestatSetTimes(t *testing.T) { if tc.expectedErrno == ErrnoSuccess { f, ok := fsc.LookupFile(fd) require.True(t, ok) - stat, err := f.Stat() - require.NoError(t, err) - atime, mtime, _ := platform.StatTimes(stat) + + var st platform.Stat_t + require.NoError(t, f.Stat(&st)) if tc.flags&FileStatAdjustFlagsAtim != 0 { - require.Equal(t, tc.atime, atime) + require.Equal(t, tc.atime, st.Atim) } else if tc.flags&FileStatAdjustFlagsAtimNow != 0 { - require.True(t, (sys.WalltimeNanos()-atime) < time.Second.Nanoseconds()) + require.True(t, (sys.WalltimeNanos()-st.Atim) < time.Second.Nanoseconds()) } else { - require.Equal(t, prevAtime, atime) + require.Equal(t, prevAtime, st.Atim) } if tc.flags&FileStatAdjustFlagsMtim != 0 { - require.Equal(t, tc.mtime, mtime) + require.Equal(t, tc.mtime, st.Mtim) } else if tc.flags&FileStatAdjustFlagsMtimNow != 0 { - require.True(t, (sys.WalltimeNanos()-mtime) < time.Second.Nanoseconds()) + require.True(t, (sys.WalltimeNanos()-st.Mtim) < time.Second.Nanoseconds()) } else { - require.Equal(t, prevMtime, mtime) + require.Equal(t, prevMtime, st.Mtim) } } require.Equal(t, tc.expectedLog, "\n"+log.String()) @@ -946,7 +947,7 @@ func Test_fdPread(t *testing.T) { ok := mod.Memory().Write(0, initialMemory) require.True(t, ok) - requireErrno(t, ErrnoSuccess, mod, FdPreadName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(tc.offset), uint64(resultNread)) + requireErrnoResult(t, ErrnoSuccess, mod, FdPreadName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(tc.offset), uint64(resultNread)) require.Equal(t, tc.expectedLog, "\n"+log.String()) actual, ok := mod.Memory().Read(0, uint32(len(tc.expectedMemory))) @@ -988,7 +989,7 @@ func Test_fdPread_offset(t *testing.T) { ok := mod.Memory().Write(0, initialMemory) require.True(t, ok) - requireErrno(t, ErrnoSuccess, mod, FdPreadName, uint64(fd), uint64(iovs), uint64(iovsCount), 2, uint64(resultNread)) + requireErrnoResult(t, ErrnoSuccess, mod, FdPreadName, uint64(fd), uint64(iovs), uint64(iovsCount), 2, uint64(resultNread)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) require.Equal(t, expectedMemory, actual) @@ -1005,7 +1006,7 @@ func Test_fdPread_offset(t *testing.T) { '?', ) - requireErrno(t, ErrnoSuccess, mod, FdReadName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultNread)) + requireErrnoResult(t, ErrnoSuccess, mod, FdReadName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultNread)) actual, ok = mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) require.Equal(t, expectedMemory, actual) @@ -1147,7 +1148,7 @@ func Test_fdPread_Errors(t *testing.T) { memoryWriteOK := mod.Memory().Write(offset, tc.memory) require.True(t, memoryWriteOK) - requireErrno(t, tc.expectedErrno, mod, FdPreadName, uint64(tc.fd), uint64(tc.iovs+offset), uint64(tc.iovsCount), uint64(tc.offset), uint64(tc.resultNread+offset)) + requireErrnoResult(t, tc.expectedErrno, mod, FdPreadName, uint64(tc.fd), uint64(tc.iovs+offset), uint64(tc.iovsCount), uint64(tc.offset), uint64(tc.resultNread+offset)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -1171,7 +1172,7 @@ func Test_fdPrestatGet(t *testing.T) { maskMemory(t, mod, len(expectedMemory)) - requireErrno(t, ErrnoSuccess, mod, FdPrestatGetName, uint64(dirFD), uint64(resultPrestat)) + requireErrnoResult(t, ErrnoSuccess, mod, FdPrestatGetName, uint64(dirFD), uint64(resultPrestat)) require.Equal(t, ` ==> wasi_snapshot_preview1.fd_prestat_get(fd=3) <== (prestat={pr_name_len=1},errno=ESUCCESS) @@ -1232,7 +1233,7 @@ func Test_fdPrestatGet_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { defer log.Reset() - requireErrno(t, tc.expectedErrno, mod, FdPrestatGetName, uint64(tc.fd), uint64(tc.resultPrestat)) + requireErrnoResult(t, tc.expectedErrno, mod, FdPrestatGetName, uint64(tc.fd), uint64(tc.resultPrestat)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -1252,7 +1253,7 @@ func Test_fdPrestatDirName(t *testing.T) { maskMemory(t, mod, len(expectedMemory)) - requireErrno(t, ErrnoSuccess, mod, FdPrestatDirNameName, uint64(dirFD), uint64(path), uint64(pathLen)) + requireErrnoResult(t, ErrnoSuccess, mod, FdPrestatDirNameName, uint64(dirFD), uint64(path), uint64(pathLen)) require.Equal(t, ` ==> wasi_snapshot_preview1.fd_prestat_dir_name(fd=3) <== (path=,errno=ESUCCESS) @@ -1344,7 +1345,7 @@ func Test_fdPrestatDirName_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { defer log.Reset() - requireErrno(t, tc.expectedErrno, mod, FdPrestatDirNameName, uint64(tc.fd), uint64(tc.path), uint64(tc.pathLen)) + requireErrnoResult(t, tc.expectedErrno, mod, FdPrestatDirNameName, uint64(tc.fd), uint64(tc.path), uint64(tc.pathLen)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -1421,7 +1422,7 @@ func Test_fdPwrite(t *testing.T) { ok := mod.Memory().Write(0, initialMemory) require.True(t, ok) - requireErrno(t, ErrnoSuccess, mod, FdPwriteName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(tc.offset), uint64(resultNwritten)) + requireErrnoResult(t, ErrnoSuccess, mod, FdPwriteName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(tc.offset), uint64(resultNwritten)) require.Equal(t, tc.expectedLog, "\n"+log.String()) actual, ok := mod.Memory().Read(0, uint32(len(tc.expectedMemory))) @@ -1469,7 +1470,7 @@ func Test_fdPwrite_offset(t *testing.T) { require.True(t, ok) // Write the last half first, to offset 3 - requireErrno(t, ErrnoSuccess, mod, FdPwriteName, uint64(fd), uint64(iovs), uint64(iovsCount), 3, uint64(resultNwritten)) + requireErrnoResult(t, ErrnoSuccess, mod, FdPwriteName, uint64(fd), uint64(iovs), uint64(iovsCount), 3, uint64(resultNwritten)) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) require.Equal(t, expectedMemory, actual) @@ -1493,7 +1494,7 @@ func Test_fdPwrite_offset(t *testing.T) { ok = mod.Memory().Write(0, writeMemory) require.True(t, ok) - requireErrno(t, ErrnoSuccess, mod, FdWriteName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultNwritten)) + requireErrnoResult(t, ErrnoSuccess, mod, FdWriteName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultNwritten)) actual, ok = mod.Memory().Read(0, uint32(len(expectedMemory))) require.True(t, ok) require.Equal(t, expectedMemory, actual) @@ -1640,7 +1641,7 @@ func Test_fdPwrite_Errors(t *testing.T) { memoryWriteOK := mod.Memory().Write(offset, tc.memory) require.True(t, memoryWriteOK) - requireErrno(t, tc.expectedErrno, mod, FdPwriteName, uint64(tc.fd), uint64(tc.iovs+offset), uint64(tc.iovsCount), uint64(tc.offset), uint64(tc.resultNwritten+offset)) + requireErrnoResult(t, tc.expectedErrno, mod, FdPwriteName, uint64(tc.fd), uint64(tc.iovs+offset), uint64(tc.iovsCount), uint64(tc.offset), uint64(tc.resultNwritten+offset)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -1676,7 +1677,7 @@ func Test_fdRead(t *testing.T) { ok := mod.Memory().Write(0, initialMemory) require.True(t, ok) - requireErrno(t, ErrnoSuccess, mod, FdReadName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultNread)) + requireErrnoResult(t, ErrnoSuccess, mod, FdReadName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultNread)) require.Equal(t, ` ==> wasi_snapshot_preview1.fd_read(fd=4,iovs=1,iovs_len=2) <== (nread=6,errno=ESUCCESS) @@ -1793,7 +1794,7 @@ func Test_fdRead_Errors(t *testing.T) { memoryWriteOK := mod.Memory().Write(offset, tc.memory) require.True(t, memoryWriteOK) - requireErrno(t, tc.expectedErrno, mod, FdReadName, uint64(tc.fd), uint64(tc.iovs+offset), uint64(tc.iovsCount+offset), uint64(tc.resultNread+offset)) + requireErrnoResult(t, tc.expectedErrno, mod, FdReadName, uint64(tc.fd), uint64(tc.iovs+offset), uint64(tc.iovsCount+offset), uint64(tc.resultNread+offset)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -2087,7 +2088,7 @@ func Test_fdReaddir(t *testing.T) { resultBufused := uint32(0) // where to write the amount used out of bufLen buf := uint32(8) // where to start the dirents - requireErrno(t, ErrnoSuccess, mod, FdReaddirName, + requireErrnoResult(t, ErrnoSuccess, mod, FdReaddirName, uint64(fd), uint64(buf), uint64(tc.bufLen), uint64(tc.cookie), uint64(resultBufused)) // read back the bufused and compare memory against it @@ -2122,7 +2123,7 @@ func Test_fdReaddir_Rewind(t *testing.T) { mem := mod.Memory() const resultBufUsed, buf, bufSize = 0, 8, 100 read := func(cookie, bufSize uint64) (bufUsed uint32) { - requireErrno(t, ErrnoSuccess, mod, FdReaddirName, + requireErrnoResult(t, ErrnoSuccess, mod, FdReaddirName, uint64(fd), buf, bufSize, cookie, uint64(resultBufUsed)) bufUsed, ok := mem.ReadUint32Le(resultBufUsed) @@ -2284,7 +2285,7 @@ func Test_fdReaddir_Errors(t *testing.T) { file.ReadDir = nil } - requireErrno(t, tc.expectedErrno, mod, FdReaddirName, + requireErrnoResult(t, tc.expectedErrno, mod, FdReaddirName, uint64(tc.fd), uint64(tc.buf), uint64(tc.bufLen), uint64(tc.cookie), uint64(tc.resultBufused)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) @@ -2380,7 +2381,7 @@ func Test_fdRenumber(t *testing.T) { require.NoError(t, err) require.Equal(t, uint32(dirFd), dirFdAssigned) - requireErrno(t, tc.expectedErrno, mod, FdRenumberName, uint64(tc.from), uint64(tc.to)) + requireErrnoResult(t, tc.expectedErrno, mod, FdRenumberName, uint64(tc.from), uint64(tc.to)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -2465,7 +2466,7 @@ func Test_fdSeek(t *testing.T) { require.NoError(t, err) require.Equal(t, int64(1), offset) - requireErrno(t, ErrnoSuccess, mod, FdSeekName, uint64(fd), uint64(tc.offset), uint64(tc.whence), uint64(resultNewoffset)) + requireErrnoResult(t, ErrnoSuccess, mod, FdSeekName, uint64(fd), uint64(tc.offset), uint64(tc.whence), uint64(resultNewoffset)) require.Equal(t, tc.expectedLog, "\n"+log.String()) actual, ok := mod.Memory().Read(0, uint32(len(tc.expectedMemory))) @@ -2542,7 +2543,7 @@ func Test_fdSeek_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { defer log.Reset() - requireErrno(t, tc.expectedErrno, mod, FdSeekName, uint64(tc.fd), tc.offset, uint64(tc.whence), uint64(tc.resultNewoffset)) + requireErrnoResult(t, tc.expectedErrno, mod, FdSeekName, uint64(tc.fd), tc.offset, uint64(tc.whence), uint64(tc.resultNewoffset)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -2586,7 +2587,7 @@ func Test_fdSync(t *testing.T) { t.Run(tc.name, func(t *testing.T) { defer log.Reset() - requireErrno(t, tc.expectedErrno, mod, FdSyncName, uint64(tc.fd)) + requireErrnoResult(t, tc.expectedErrno, mod, FdSyncName, uint64(tc.fd)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -2623,7 +2624,7 @@ func Test_fdTell(t *testing.T) { require.NoError(t, err) require.Equal(t, int64(1), offset) - requireErrno(t, ErrnoSuccess, mod, FdTellName, uint64(fd), uint64(resultNewoffset)) + requireErrnoResult(t, ErrnoSuccess, mod, FdTellName, uint64(fd), uint64(resultNewoffset)) require.Equal(t, expectedLog, "\n"+log.String()) actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory))) @@ -2674,7 +2675,7 @@ func Test_fdTell_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { defer log.Reset() - requireErrno(t, tc.expectedErrno, mod, FdTellName, uint64(tc.fd), uint64(tc.resultNewoffset)) + requireErrnoResult(t, tc.expectedErrno, mod, FdTellName, uint64(tc.fd), uint64(tc.resultNewoffset)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -2711,7 +2712,7 @@ func Test_fdWrite(t *testing.T) { ok := mod.Memory().Write(0, initialMemory) require.True(t, ok) - requireErrno(t, ErrnoSuccess, mod, FdWriteName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultNwritten)) + requireErrnoResult(t, ErrnoSuccess, mod, FdWriteName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultNwritten)) require.Equal(t, ` ==> wasi_snapshot_preview1.fd_write(fd=4,iovs=1,iovs_len=2) <== (nwritten=6,errno=ESUCCESS) @@ -2762,7 +2763,7 @@ func Test_fdWrite_discard(t *testing.T) { require.True(t, ok) fd := sys.FdStdout - requireErrno(t, ErrnoSuccess, mod, FdWriteName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultNwritten)) + requireErrnoResult(t, ErrnoSuccess, mod, FdWriteName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultNwritten)) // Should not amplify logging require.Zero(t, len(log.Bytes())) @@ -2860,7 +2861,7 @@ func Test_fdWrite_Errors(t *testing.T) { 'h', 'i', // iovs[0].length bytes )) - requireErrno(t, tc.expectedErrno, mod, FdWriteName, uint64(tc.fd), uint64(tc.iovs), uint64(iovsCount), + requireErrnoResult(t, tc.expectedErrno, mod, FdWriteName, uint64(tc.fd), uint64(tc.iovs), uint64(iovsCount), uint64(tc.resultNwritten)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) @@ -2883,7 +2884,7 @@ func Test_pathCreateDirectory(t *testing.T) { name := 1 nameLen := len(pathName) - requireErrno(t, ErrnoSuccess, mod, PathCreateDirectoryName, uint64(preopenedFD), uint64(name), uint64(nameLen)) + requireErrnoResult(t, ErrnoSuccess, mod, PathCreateDirectoryName, uint64(preopenedFD), uint64(name), uint64(nameLen)) require.Equal(t, ` ==> wasi_snapshot_preview1.path_create_directory(fd=3,path=wazero) <== errno=ESUCCESS @@ -2993,7 +2994,7 @@ func Test_pathCreateDirectory_Errors(t *testing.T) { mod.Memory().Write(tc.path, []byte(tc.pathName)) - requireErrno(t, tc.expectedErrno, mod, PathCreateDirectoryName, uint64(tc.fd), uint64(tc.path), uint64(tc.pathLen)) + requireErrnoResult(t, tc.expectedErrno, mod, PathCreateDirectoryName, uint64(tc.fd), uint64(tc.path), uint64(tc.pathLen)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -3154,7 +3155,7 @@ func Test_pathFilestatGet(t *testing.T) { mod.Memory().Write(0, tc.memory) flags := uint32(0) - requireErrno(t, tc.expectedErrno, mod, PathFilestatGetName, uint64(tc.fd), uint64(flags), uint64(1), uint64(tc.pathLen), uint64(tc.resultFilestat)) + requireErrnoResult(t, tc.expectedErrno, mod, PathFilestatGetName, uint64(tc.fd), uint64(flags), uint64(1), uint64(tc.pathLen), uint64(tc.resultFilestat)) require.Equal(t, tc.expectedLog, "\n"+log.String()) actual, ok := mod.Memory().Read(0, uint32(len(tc.expectedMemory))) @@ -3209,7 +3210,7 @@ func Test_pathLink(t *testing.T) { destinationRealPath := path.Join(tmpDir, newDirName, destinationName) t.Run("success", func(t *testing.T) { - requireErrno(t, ErrnoSuccess, mod, PathLinkName, + requireErrnoResult(t, ErrnoSuccess, mod, PathLinkName, uint64(oldFd), 0, fileNamePtr, uint64(len(filename)), uint64(newFd), destinationNamePtr, uint64(len(destinationName))) require.Contains(t, log.String(), ErrnoName(ErrnoSuccess)) @@ -3219,13 +3220,12 @@ func Test_pathLink(t *testing.T) { defer func() { require.NoError(t, f.Close()) }() - st, err := f.Stat() - require.NoError(t, err) - require.False(t, st.Mode()&os.ModeSymlink == os.ModeSymlink) - _, _, _, nlink, _, _, err := platform.Stat(f, st) + var st platform.Stat_t + require.NoError(t, platform.StatFile(f, &st)) require.NoError(t, err) - require.Equal(t, uint64(2), nlink) + require.False(t, st.Mode&os.ModeSymlink == os.ModeSymlink) + require.Equal(t, uint64(2), st.Nlink) }) t.Run("errors", func(t *testing.T) { @@ -3251,7 +3251,7 @@ func Test_pathLink(t *testing.T) { } { name := ErrnoName(tc.errno) t.Run(name, func(t *testing.T) { - requireErrno(t, tc.errno, mod, PathLinkName, + requireErrnoResult(t, tc.errno, mod, PathLinkName, tc.oldDirFd, 0, tc.oldPtr, tc.oldLen, tc.newDirFd, tc.newPtr, tc.newLen) require.Contains(t, log.String(), name) @@ -3500,7 +3500,7 @@ func Test_pathOpen(t *testing.T) { // rights aren't used fsRightsBase, fsRightsInheriting := uint64(0), uint64(0) - requireErrno(t, tc.expectedErrno, mod, PathOpenName, uint64(dirfd), uint64(dirflags), uint64(path), + requireErrnoResult(t, tc.expectedErrno, mod, PathOpenName, uint64(dirfd), uint64(dirflags), uint64(path), uint64(pathLen), uint64(tc.oflags), fsRightsBase, fsRightsInheriting, uint64(tc.fdflags), uint64(resultOpenedFd)) require.Equal(t, tc.expectedLog, "\n"+log.String()) @@ -3678,7 +3678,7 @@ func Test_pathOpen_Errors(t *testing.T) { mod.Memory().Write(tc.path, []byte(tc.pathName)) - requireErrno(t, tc.expectedErrno, mod, PathOpenName, uint64(tc.fd), uint64(0), uint64(tc.path), + requireErrnoResult(t, tc.expectedErrno, mod, PathOpenName, uint64(tc.fd), uint64(0), uint64(tc.path), uint64(tc.pathLen), uint64(tc.oflags), 0, 0, 0, uint64(tc.resultOpenedFd)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) @@ -3743,7 +3743,7 @@ func Test_pathReadlink(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { const bufPtr, bufSize, resultBufUsedPtr = 0x100, 0xff, 0x200 - requireErrno(t, ErrnoSuccess, mod, PathReadlinkName, + requireErrnoResult(t, ErrnoSuccess, mod, PathReadlinkName, uint64(topFd), tc.pathPtr, tc.pathLen, bufPtr, bufSize, resultBufUsedPtr) @@ -3779,7 +3779,7 @@ func Test_pathReadlink(t *testing.T) { } { name := ErrnoName(tc.errno) t.Run(name, func(t *testing.T) { - requireErrno(t, tc.errno, mod, PathReadlinkName, + requireErrnoResult(t, tc.errno, mod, PathReadlinkName, tc.dirFd, tc.pathPtr, tc.pathLen, tc.bufPtr, tc.bufLen, tc.resultBufUsedPtr) require.Contains(t, log.String(), name) @@ -3808,7 +3808,7 @@ func Test_pathRemoveDirectory(t *testing.T) { name := 1 nameLen := len(pathName) - requireErrno(t, ErrnoSuccess, mod, PathRemoveDirectoryName, uint64(dirFD), uint64(name), uint64(nameLen)) + requireErrnoResult(t, ErrnoSuccess, mod, PathRemoveDirectoryName, uint64(dirFD), uint64(name), uint64(nameLen)) require.Equal(t, ` ==> wasi_snapshot_preview1.path_remove_directory(fd=3,path=wazero) <== errno=ESUCCESS @@ -3932,7 +3932,7 @@ func Test_pathRemoveDirectory_Errors(t *testing.T) { mod.Memory().Write(tc.path, []byte(tc.pathName)) - requireErrno(t, tc.expectedErrno, mod, PathRemoveDirectoryName, uint64(tc.fd), uint64(tc.path), uint64(tc.pathLen)) + requireErrnoResult(t, tc.expectedErrno, mod, PathRemoveDirectoryName, uint64(tc.fd), uint64(tc.path), uint64(tc.pathLen)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -3966,7 +3966,7 @@ func Test_pathSymlink_errors(t *testing.T) { require.True(t, ok) t.Run("success", func(t *testing.T) { - requireErrno(t, ErrnoSuccess, mod, PathSymlinkName, + requireErrnoResult(t, ErrnoSuccess, mod, PathSymlinkName, fileNamePtr, uint64(len(filename)), uint64(fd), destinationNamePtr, uint64(len(destinationName))) require.Contains(t, log.String(), ErrnoName(ErrnoSuccess)) st, err := os.Lstat(path.Join(tmpDir, dirname, destinationName)) @@ -4002,7 +4002,7 @@ func Test_pathSymlink_errors(t *testing.T) { } { name := ErrnoName(tc.errno) t.Run(name, func(t *testing.T) { - requireErrno(t, tc.errno, mod, PathSymlinkName, + requireErrnoResult(t, tc.errno, mod, PathSymlinkName, tc.oldPtr, tc.oldLen, tc.dirFd, tc.newPtr, tc.newLen) require.Contains(t, log.String(), name) }) @@ -4037,7 +4037,7 @@ func Test_pathRename(t *testing.T) { ok = mod.Memory().Write(newPath, []byte(newPathName)) require.True(t, ok) - requireErrno(t, ErrnoSuccess, mod, PathRenameName, + requireErrnoResult(t, ErrnoSuccess, mod, PathRenameName, uint64(oldDirFD), uint64(oldPath), uint64(oldPathLen), uint64(newDirFD), uint64(newPath), uint64(newPathLen)) require.Equal(t, ` @@ -4218,7 +4218,7 @@ func Test_pathRename_Errors(t *testing.T) { mod.Memory().Write(tc.oldPath, []byte(tc.oldPathName)) mod.Memory().Write(tc.newPath, []byte(tc.newPathName)) - requireErrno(t, tc.expectedErrno, mod, PathRenameName, + requireErrnoResult(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()) @@ -4246,7 +4246,7 @@ func Test_pathUnlinkFile(t *testing.T) { name := 1 nameLen := len(pathName) - requireErrno(t, ErrnoSuccess, mod, PathUnlinkFileName, uint64(dirFD), uint64(name), uint64(nameLen)) + requireErrnoResult(t, ErrnoSuccess, mod, PathUnlinkFileName, uint64(dirFD), uint64(name), uint64(nameLen)) require.Equal(t, ` ==> wasi_snapshot_preview1.path_unlink_file(fd=3,path=wazero) <== errno=ESUCCESS @@ -4351,7 +4351,7 @@ func Test_pathUnlinkFile_Errors(t *testing.T) { mod.Memory().Write(tc.path, []byte(tc.pathName)) - requireErrno(t, tc.expectedErrno, mod, PathUnlinkFileName, uint64(tc.fd), uint64(tc.path), uint64(tc.pathLen)) + requireErrnoResult(t, tc.expectedErrno, mod, PathUnlinkFileName, uint64(tc.fd), uint64(tc.path), uint64(tc.pathLen)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -4404,7 +4404,7 @@ func Test_fdReaddir_opened_file_written(t *testing.T) { const readDirTarget = "dir" mem.Write(0, []byte(readDirTarget)) - requireErrno(t, ErrnoSuccess, mod, PathCreateDirectoryName, + requireErrnoResult(t, ErrnoSuccess, mod, PathCreateDirectoryName, uint64(sys.FdPreopen), uint64(0), uint64(len(readDirTarget))) // Open the directory, before writing files! @@ -4419,7 +4419,7 @@ func Test_fdReaddir_opened_file_written(t *testing.T) { // Try list them! resultBufused := uint32(0) // where to write the amount used out of bufLen buf := uint32(8) // where to start the dirents - requireErrno(t, ErrnoSuccess, mod, FdReaddirName, + requireErrnoResult(t, ErrnoSuccess, mod, FdReaddirName, uint64(dirFd), uint64(buf), uint64(0x2000), 0, uint64(resultBufused)) used, _ := mem.ReadUint32Le(resultBufused) diff --git a/imports/wasi_snapshot_preview1/poll_test.go b/imports/wasi_snapshot_preview1/poll_test.go index 00801336..d2956297 100644 --- a/imports/wasi_snapshot_preview1/poll_test.go +++ b/imports/wasi_snapshot_preview1/poll_test.go @@ -39,7 +39,7 @@ func Test_pollOneoff(t *testing.T) { maskMemory(t, mod, 1024) mod.Memory().Write(in, mem) - requireErrno(t, ErrnoSuccess, mod, PollOneoffName, uint64(in), uint64(out), uint64(nsubscriptions), + requireErrnoResult(t, ErrnoSuccess, mod, PollOneoffName, uint64(in), uint64(out), uint64(nsubscriptions), uint64(resultNevents)) require.Equal(t, ` ==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=1) @@ -146,7 +146,7 @@ func Test_pollOneoff_Errors(t *testing.T) { mod.Memory().Write(tc.in, tc.mem) } - requireErrno(t, tc.expectedErrno, mod, PollOneoffName, uint64(tc.in), uint64(tc.out), + requireErrnoResult(t, tc.expectedErrno, mod, PollOneoffName, uint64(tc.in), uint64(tc.out), uint64(tc.nsubscriptions), uint64(tc.resultNevents)) require.Equal(t, tc.expectedLog, "\n"+log.String()) diff --git a/imports/wasi_snapshot_preview1/random_test.go b/imports/wasi_snapshot_preview1/random_test.go index a65ecf0a..5fa2e1dd 100644 --- a/imports/wasi_snapshot_preview1/random_test.go +++ b/imports/wasi_snapshot_preview1/random_test.go @@ -28,7 +28,7 @@ func Test_randomGet(t *testing.T) { maskMemory(t, mod, len(expectedMemory)) // Invoke randomGet and check the memory side effects! - requireErrno(t, ErrnoSuccess, mod, RandomGetName, uint64(offset), uint64(length)) + requireErrnoResult(t, ErrnoSuccess, mod, RandomGetName, uint64(offset), uint64(length)) require.Equal(t, ` ==> wasi_snapshot_preview1.random_get(buf=1,buf_len=5) <== errno=ESUCCESS @@ -76,7 +76,7 @@ func Test_randomGet_Errors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { defer log.Reset() - requireErrno(t, ErrnoFault, mod, RandomGetName, uint64(tc.offset), uint64(tc.length)) + requireErrnoResult(t, ErrnoFault, mod, RandomGetName, uint64(tc.offset), uint64(tc.length)) require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } @@ -113,7 +113,7 @@ func Test_randomGet_SourceError(t *testing.T) { WithRandSource(tc.randSource)) defer r.Close(testCtx) - requireErrno(t, ErrnoIo, mod, RandomGetName, uint64(1), uint64(5)) // arbitrary offset and length + requireErrnoResult(t, ErrnoIo, mod, RandomGetName, uint64(1), uint64(5)) // arbitrary offset and length require.Equal(t, tc.expectedLog, "\n"+log.String()) }) } diff --git a/imports/wasi_snapshot_preview1/sched_test.go b/imports/wasi_snapshot_preview1/sched_test.go index fb3c2256..596563c0 100644 --- a/imports/wasi_snapshot_preview1/sched_test.go +++ b/imports/wasi_snapshot_preview1/sched_test.go @@ -15,7 +15,7 @@ func Test_schedYield(t *testing.T) { yielded = true })) defer r.Close(testCtx) - requireErrno(t, ErrnoSuccess, mod, SchedYieldName) + requireErrnoResult(t, ErrnoSuccess, mod, SchedYieldName) require.Equal(t, ` ==> wasi_snapshot_preview1.sched_yield() <== errno=ESUCCESS diff --git a/imports/wasi_snapshot_preview1/wasi_test.go b/imports/wasi_snapshot_preview1/wasi_test.go index a223e0d6..bcc25cd5 100644 --- a/imports/wasi_snapshot_preview1/wasi_test.go +++ b/imports/wasi_snapshot_preview1/wasi_test.go @@ -137,11 +137,11 @@ func requireErrnoNosys(t *testing.T, funcName string, params ...uint64) string { mod, err := r.InstantiateModule(ctx, proxyCompiled, wazero.NewModuleConfig()) require.NoError(t, err) - requireErrno(t, ErrnoNosys, mod, funcName, params...) + requireErrnoResult(t, ErrnoNosys, mod, funcName, params...) return "\n" + log.String() } -func requireErrno(t *testing.T, expectedErrno Errno, mod api.Closer, funcName string, params ...uint64) { +func requireErrnoResult(t *testing.T, expectedErrno Errno, mod api.Closer, funcName string, params ...uint64) { results, err := mod.(api.Module).ExportedFunction(funcName).Call(testCtx, params...) require.NoError(t, err) errno := Errno(results[0]) diff --git a/internal/fstest/fstest.go b/internal/fstest/fstest.go index 03310309..8bd223a0 100644 --- a/internal/fstest/fstest.go +++ b/internal/fstest/fstest.go @@ -100,17 +100,16 @@ func WriteTestFiles(tmpDir string) (err error) { } // os.Stat uses GetFileInformationByHandle internally. - stat, err := os.Stat(path) - if err != nil { + var stat platform.Stat_t + if err = platform.Stat(path, &stat); err != nil { return err } - if stat.ModTime() == info.ModTime() { + if stat.Mtim == info.ModTime().UnixNano() { return nil // synced! } // Otherwise, we need to sync the timestamps. - atimeNsec, mtimeNsec, _ := platform.StatTimes(stat) - return os.Chtimes(path, time.Unix(0, atimeNsec), time.Unix(0, mtimeNsec)) + return os.Chtimes(path, time.Unix(0, stat.Atim), time.Unix(0, stat.Mtim)) }) } return diff --git a/internal/gojs/errno.go b/internal/gojs/errno.go index ea09bb48..6ee5e0fb 100644 --- a/internal/gojs/errno.go +++ b/internal/gojs/errno.go @@ -3,7 +3,7 @@ package gojs import ( "syscall" - "github.com/tetratelabs/wazero/internal/sysfs" + "github.com/tetratelabs/wazero/internal/platform" ) // Errno is a (GOARCH=wasm) error, which must match a key in mapJSError. @@ -60,7 +60,7 @@ var ( // // This should match wasi_snapshot_preview1.ToErrno for maintenance ease. func ToErrno(err error) *Errno { - errno := sysfs.UnwrapOSError(err) + errno := platform.UnwrapOSError(err) switch errno { case syscall.EACCES: diff --git a/internal/gojs/errno_test.go b/internal/gojs/errno_test.go index 47bc972a..b99c482f 100644 --- a/internal/gojs/errno_test.go +++ b/internal/gojs/errno_test.go @@ -3,8 +3,6 @@ package gojs import ( "syscall" "testing" - - "github.com/tetratelabs/wazero/internal/testing/require" ) func TestToErrno(t *testing.T) { @@ -103,8 +101,9 @@ func TestToErrno(t *testing.T) { for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { - errno := ToErrno(tc.input) - require.Equal(t, tc.expected, errno) + if errno := ToErrno(tc.input); errno != tc.expected { + t.Fatalf("expected %#v but was %#v", tc.expected, errno) + } }) } } diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go index d1f468d0..d691b324 100644 --- a/internal/gojs/fs.go +++ b/internal/gojs/fs.go @@ -127,16 +127,11 @@ func (jsfsStat) invoke(ctx context.Context, mod api.Module, args ...interface{}) func syscallStat(mod api.Module, path string) (*jsSt, error) { fsc := mod.(*wasm.CallContext).Sys.FS() - f, err := fsc.RootFS().OpenFile(path, os.O_RDONLY, 0) - if err != nil { + var stat platform.Stat_t + if err := fsc.RootFS().Stat(path, &stat); err != nil { return nil, err } - defer f.Close() - if stat, err := sysfs.StatFile(f); err != nil { - return nil, err - } else { - return newJsSt(stat, f), nil - } + return newJsSt(&stat), nil } // jsfsLstat implements jsFn for syscall.Lstat @@ -190,23 +185,24 @@ func syscallFstat(fsc *internalsys.FSContext, fd uint32) (*jsSt, error) { return nil, syscall.EBADF } - stat, err := f.Stat() - if err != nil { + var st platform.Stat_t + if err := f.Stat(&st); err != nil { return nil, err } - return newJsSt(stat, f.File), nil + return newJsSt(&st), nil } -func newJsSt(stat fs.FileInfo, f fs.File) *jsSt { +func newJsSt(stat *platform.Stat_t) *jsSt { ret := &jsSt{} - ret.isDir = stat.IsDir() - _, _, _, _, ret.dev, ret.ino, _ = platform.Stat(f, stat) - ret.mode = getJsMode(stat.Mode()) - ret.size = stat.Size() - atimeNsec, mtimeNsec, ctimeNsec := platform.StatTimes(stat) - ret.atimeMs = atimeNsec / 1e6 - ret.mtimeMs = mtimeNsec / 1e6 - ret.ctimeMs = ctimeNsec / 1e6 + ret.isDir = stat.Mode.IsDir() + ret.dev = stat.Dev + ret.ino = stat.Ino + ret.mode = getJsMode(stat.Mode) + ret.nlink = uint32(stat.Nlink) + ret.size = stat.Size + ret.atimeMs = stat.Atim / 1e6 + ret.mtimeMs = stat.Mtim / 1e6 + ret.ctimeMs = stat.Ctim / 1e6 return ret } diff --git a/internal/platform/errno.go b/internal/platform/errno.go new file mode 100644 index 00000000..c2941934 --- /dev/null +++ b/internal/platform/errno.go @@ -0,0 +1,7 @@ +//go:build !windows + +package platform + +func adjustErrno(err error) error { + return err +} diff --git a/internal/platform/errno_windows.go b/internal/platform/errno_windows.go new file mode 100644 index 00000000..a860ac6a --- /dev/null +++ b/internal/platform/errno_windows.go @@ -0,0 +1,64 @@ +package platform + +import "syscall" + +// See https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499- +const ( + // ERROR_ACCESS_DENIED is a Windows error returned by syscall.Unlink + // instead of syscall.EPERM + ERROR_ACCESS_DENIED = syscall.Errno(5) + + // ERROR_INVALID_HANDLE is a Windows error returned by syscall.Write + // instead of syscall.EBADF + ERROR_INVALID_HANDLE = syscall.Errno(6) + + // ERROR_FILE_EXISTS is a Windows error returned by os.OpenFile + // instead of syscall.EEXIST + ERROR_FILE_EXISTS = syscall.Errno(0x50) + + // ERROR_NEGATIVE_SEEK is a Windows error returned by os.Truncate + // instead of syscall.EINVAL + ERROR_NEGATIVE_SEEK = syscall.Errno(0x83) + + // ERROR_DIR_NOT_EMPTY is a Windows error returned by syscall.Rmdir + // instead of syscall.ENOTEMPTY + ERROR_DIR_NOT_EMPTY = syscall.Errno(0x91) + + // ERROR_ALREADY_EXISTS is a Windows error returned by os.Mkdir + // instead of syscall.EEXIST + ERROR_ALREADY_EXISTS = syscall.Errno(0xB7) + + // ERROR_DIRECTORY is a Windows error returned by syscall.Rmdir + // instead of syscall.ENOTDIR + ERROR_DIRECTORY = syscall.Errno(0x10B) +) + +// See https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--1300-1699- +const ( + // 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(0x522) +) + +func adjustErrno(err syscall.Errno) error { + // Note: In windows, ERROR_PATH_NOT_FOUND(0x3) maps to syscall.ENOTDIR + switch err { + case ERROR_ALREADY_EXISTS: + return syscall.EEXIST + case ERROR_DIRECTORY: + return syscall.ENOTDIR + case ERROR_DIR_NOT_EMPTY: + return syscall.ENOTEMPTY + case ERROR_FILE_EXISTS: + return syscall.EEXIST + case ERROR_INVALID_HANDLE: + return syscall.EBADF + case ERROR_ACCESS_DENIED, ERROR_PRIVILEGE_NOT_HELD: + return syscall.EPERM + case ERROR_NEGATIVE_SEEK: + return syscall.EINVAL + } + return err +} diff --git a/internal/platform/error.go b/internal/platform/error.go new file mode 100644 index 00000000..7730715a --- /dev/null +++ b/internal/platform/error.go @@ -0,0 +1,50 @@ +package platform + +import ( + "io/fs" + "os" + "syscall" +) + +// 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: + return syscall.EINVAL + case fs.ErrPermission: + return syscall.EPERM + case fs.ErrExist: + return syscall.EEXIST + case fs.ErrNotExist: + return syscall.ENOENT + case fs.ErrClosed: + return syscall.EBADF + } + 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 +} diff --git a/internal/platform/error_test.go b/internal/platform/error_test.go new file mode 100644 index 00000000..f24b90ea --- /dev/null +++ b/internal/platform/error_test.go @@ -0,0 +1,83 @@ +package platform + +import ( + "errors" + "fmt" + "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 + }{ + { + 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.EqualErrno(t, tc.expected, errno) + }) + } + + t.Run("nil", func(t *testing.T) { + require.Nil(t, UnwrapOSError(nil)) + }) +} diff --git a/internal/platform/open_file.go b/internal/platform/open_file.go index bed40d37..cff965a3 100644 --- a/internal/platform/open_file.go +++ b/internal/platform/open_file.go @@ -15,6 +15,9 @@ const ( O_NOFOLLOW = syscall.O_NOFOLLOW ) -func OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) { - return os.OpenFile(name, flag, perm) +// OpenFile is like os.OpenFile except it returns syscall.Errno +func OpenFile(name string, flag int, perm fs.FileMode) (f *os.File, err error) { + f, err = os.OpenFile(name, flag, perm) + err = UnwrapOSError(err) + return } diff --git a/internal/platform/open_file_js.go b/internal/platform/open_file_js.go index 32cdc62d..496e966a 100644 --- a/internal/platform/open_file_js.go +++ b/internal/platform/open_file_js.go @@ -13,7 +13,9 @@ const ( O_NOFOLLOW = 1 << 30 ) -func OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) { +func OpenFile(name string, flag int, perm fs.FileMode) (f *os.File, err error) { flag &= ^(O_DIRECTORY | O_NOFOLLOW) // erase placeholders - return os.OpenFile(name, flag, perm) + f, err = os.OpenFile(name, flag, perm) + err = UnwrapOSError(err) + return } diff --git a/internal/platform/open_file_test.go b/internal/platform/open_file_test.go index 0f0a029c..3ab2e256 100644 --- a/internal/platform/open_file_test.go +++ b/internal/platform/open_file_test.go @@ -15,7 +15,7 @@ func TestOpenFile_Errors(t *testing.T) { 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) + require.EqualErrno(t, syscall.ENOENT, err) }) // This is the same as https://github.com/ziglang/zig/blob/d24ebf1d12cf66665b52136a2807f97ff021d78d/lib/std/os/test.zig#L105-L112 @@ -26,7 +26,7 @@ func TestOpenFile_Errors(t *testing.T) { require.NoError(t, err) _, err = OpenFile(filepath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o666) - require.ErrorIs(t, err, syscall.EEXIST) + require.EqualErrno(t, syscall.EEXIST, err) }) // This is similar to https://github.com/WebAssembly/wasi-testsuite/blob/dc7f8d27be1030cd4788ebdf07d9b57e5d23441e/tests/rust/src/bin/dangling_symlink.rs @@ -38,9 +38,9 @@ func TestOpenFile_Errors(t *testing.T) { require.NoError(t, err) _, err = OpenFile(symlink, O_DIRECTORY|O_NOFOLLOW, 0o0666) - require.ErrorIs(t, err, syscall.ENOTDIR) + require.EqualErrno(t, syscall.ENOTDIR, err) _, err = OpenFile(symlink, O_NOFOLLOW, 0o0666) - require.ErrorIs(t, err, syscall.ELOOP) + require.EqualErrno(t, syscall.ELOOP, err) }) } diff --git a/internal/platform/open_file_windows.go b/internal/platform/open_file_windows.go index 8955366b..66414a47 100644 --- a/internal/platform/open_file_windows.go +++ b/internal/platform/open_file_windows.go @@ -1,7 +1,6 @@ package platform import ( - "errors" "io/fs" "os" "syscall" @@ -32,41 +31,44 @@ func OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) { isDir := flag&O_DIRECTORY > 0 flag &= ^(O_DIRECTORY | O_NOFOLLOW) // erase placeholders + // TODO: document why we are opening twice fd, err := open(name, flag|syscall.O_CLOEXEC, uint32(perm)) if err == nil { return os.NewFile(uintptr(fd), name), nil } + // TODO: Set FILE_SHARE_DELETE for directory as well. 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 - } else if notFound := errors.Is(err, syscall.ERROR_FILE_NOT_FOUND); notFound && isDir { - // Either symlink or hard link directory not found. We change the returned errno depending on - // if it is symlink or not to have consistent behavior across OSes. - st, e := os.Lstat(name) - if e == nil && st.Mode()&os.ModeSymlink != 0 { - // Dangling symlink dir must raise ENOTIDR. + if err = UnwrapOSError(err); err == nil { + return f, nil + } + + switch err { + case syscall.ENOTDIR: + err = syscall.ENOENT + case syscall.ENOENT: + if isSymlink(name) { + // Either symlink or hard link not found. We change the returned + // errno depending on if it is symlink or not to have consistent + // behavior across OSes. + if isDir { + // Dangling symlink dir must raise ENOTDIR. err = syscall.ENOTDIR } else { - err = syscall.ENOENT - } - } else if notFound { - // Either symlink or hard link file not found. We change the returned errno depending on - // if it is symlink or not to have consistent behavior across OSes. - st, e := os.Lstat(name) - if e == nil && st.Mode()&os.ModeSymlink != 0 { err = syscall.ELOOP - } else { - err = syscall.ENOENT } } } return f, err } +func isSymlink(path string) bool { + if st, e := os.Lstat(path); e == nil && st.Mode()&os.ModeSymlink != 0 { + return true + } + return false +} + // The following is lifted from syscall_windows.go to add support for setting FILE_SHARE_DELETE. // https://github.com/golang/go/blob/go1.20/src/syscall/syscall_windows.go#L308-L379 func open(path string, mode int, perm uint32) (fd syscall.Handle, err error) { diff --git a/internal/platform/rename_test.go b/internal/platform/rename_test.go index 42755aa5..e2f29bc4 100644 --- a/internal/platform/rename_test.go +++ b/internal/platform/rename_test.go @@ -19,7 +19,7 @@ func TestRename(t *testing.T) { require.NoError(t, err) err = Rename(path.Join(tmpDir, "non-exist"), file1Path) - require.Equal(t, syscall.ENOENT, err) + require.EqualErrno(t, syscall.ENOENT, err) }) t.Run("file to non-exist", func(t *testing.T) { tmpDir := t.TempDir() @@ -35,7 +35,7 @@ func TestRename(t *testing.T) { // Show the prior path no longer exists _, err = os.Stat(file1Path) - require.Equal(t, syscall.ENOENT, errors.Unwrap(err)) + require.EqualErrno(t, syscall.ENOENT, errors.Unwrap(err)) s, err := os.Stat(file2Path) require.NoError(t, err) @@ -53,7 +53,7 @@ func TestRename(t *testing.T) { // Show the prior path no longer exists _, err = os.Stat(dir1Path) - require.Equal(t, syscall.ENOENT, errors.Unwrap(err)) + require.EqualErrno(t, syscall.ENOENT, errors.Unwrap(err)) s, err := os.Stat(dir2Path) require.NoError(t, err) @@ -72,7 +72,7 @@ func TestRename(t *testing.T) { require.NoError(t, err) err = Rename(dir1Path, dir2Path) - require.Equal(t, syscall.ENOTDIR, err) + require.EqualErrno(t, syscall.ENOTDIR, err) }) t.Run("file to dir", func(t *testing.T) { tmpDir := t.TempDir() @@ -86,7 +86,7 @@ func TestRename(t *testing.T) { require.NoError(t, os.Mkdir(dir1Path, 0o700)) err = Rename(file1Path, dir1Path) - require.Equal(t, syscall.EISDIR, err) + require.EqualErrno(t, syscall.EISDIR, err) }) // Similar to https://github.com/ziglang/zig/blob/0.10.1/lib/std/fs/test.zig#L567-L582 @@ -112,7 +112,7 @@ func TestRename(t *testing.T) { // Show the prior path no longer exists _, err = os.Stat(dir1Path) - require.Equal(t, syscall.ENOENT, errors.Unwrap(err)) + require.EqualErrno(t, syscall.ENOENT, errors.Unwrap(err)) // Show the file inside that directory moved s, err := os.Stat(path.Join(dir2Path, file1)) @@ -142,7 +142,7 @@ func TestRename(t *testing.T) { require.NoError(t, err) err = Rename(dir1Path, dir2Path) - require.ErrorIs(t, syscall.ENOTEMPTY, err) + require.EqualErrno(t, syscall.ENOTEMPTY, err) }) t.Run("file to file", func(t *testing.T) { @@ -163,7 +163,7 @@ func TestRename(t *testing.T) { // Show the prior path no longer exists _, err = os.Stat(file1Path) - require.Equal(t, syscall.ENOENT, errors.Unwrap(err)) + require.EqualErrno(t, syscall.ENOENT, errors.Unwrap(err)) // Show the file1 overwrote file2 b, err := os.ReadFile(file2Path) diff --git a/internal/platform/stat.go b/internal/platform/stat.go index 26904166..a19685a3 100644 --- a/internal/platform/stat.go +++ b/internal/platform/stat.go @@ -2,32 +2,94 @@ package platform import ( "io/fs" - "os" + "syscall" ) -// StatTimes returns platform-specific values if os.FileInfo Sys is available. -// Otherwise, it returns the mod time for all values. -func StatTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) { - if t.Sys() == nil { // possibly fake filesystem - atimeNsec, mtimeNsec, ctimeNsec = mtimes(t) - return - } - return statTimes(t) +// Stat_t is similar to syscall.Stat_t, and fields frequently used by +// WebAssembly ABI including WASI snapshot-01, GOOS=js and wasi-filesystem. +// +// # Note +// +// Zero values may be returned where not available. For example, fs.FileInfo +// implementations may not be able to provide Ino values. +type Stat_t struct { + // Dev is the device ID of device containing the file. + Dev uint64 + + // Ino is the file serial number. + Ino uint64 + + // Mode is the same as Mode on fs.FileInfo containing bits to identify the + // type of the file and its permissions (fs.ModePerm). + Mode fs.FileMode + + /// Nlink is the number of hard links to the file. + Nlink uint64 + // ^^ uint64 not uint16 to accept widest syscall.Stat_t.Nlink + + // Size is the length in bytes for regular files. For symbolic links, this + // is length in bytes of the pathname contained in the symbolic link. + Size int64 + // ^^ int64 not uint64 to defer to fs.FileInfo + + // Atim is the last data access timestamp in epoch nanoseconds. + Atim int64 + + // Mtim is the last data modification timestamp in epoch nanoseconds. + Mtim int64 + + // Ctim is the last file status change timestamp in epoch nanoseconds. + Ctim int64 } -// Stat returns platform-specific values if os.FileInfo Sys is available. -func Stat(f fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlink, dev, inode uint64, err error) { - if t.Sys() == nil { // possibly fake filesystem - atimeNsec, mtimeNsec, ctimeNsec = mtimes(t) - nlink = 1 +// Stat is like syscall.Stat. This returns syscall.ENOENT if the path doesn't +// exist. +func Stat(path string, stat *Stat_t) (err error) { + // TODO: The current windows needs the file to be an open handle. See if + // we can avoid this and call os.Stat instead. + f, err := OpenFile(path, syscall.O_RDONLY, 0) + if err != nil { return } - return stat(f, t) + defer f.Close() + return StatFile(f, stat) } -func mtimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) { - mtimeNsec = t.ModTime().UnixNano() - atimeNsec = mtimeNsec - ctimeNsec = mtimeNsec +// StatFile is like syscall.Fstat, but for fs.File instead of a file +// descriptor. This returns syscall.EBADF if the file or directory was closed. +// Note: windows allows you to stat a closed directory. +func StatFile(f fs.File, stat *Stat_t) (err error) { + t, err := f.Stat() + if err = UnwrapOSError(err); err != nil { + if err == syscall.EIO { // linux/darwin returns this on a closed file. + err = syscall.EBADF // windows returns this, which is better. + } + return + } + return fillStat(stat, f, t) +} + +// fder is implemented by os.File in file_unix.go and file_windows.go +// Note: we use this until we finalize our own FD-scoped file. +type fder interface{ Fd() (fd uintptr) } + +func fillStat(stat *Stat_t, f fs.File, t fs.FileInfo) (err error) { + if of, ok := f.(fder); !ok { // possibly fake filesystem + fillStatFromFileInfo(stat, t) + } else { + err = fillStatFromOpenFile(stat, of.Fd(), t) + } return } + +func fillStatFromFileInfo(stat *Stat_t, t fs.FileInfo) { + stat.Ino = 0 + stat.Dev = 0 + stat.Mode = t.Mode() + stat.Nlink = 1 + stat.Size = t.Size() + mtim := t.ModTime().UnixNano() // Set all times to the mod time + stat.Atim = mtim + stat.Mtim = mtim + stat.Ctim = mtim +} diff --git a/internal/platform/stat_bsd.go b/internal/platform/stat_bsd.go index 4d065e62..4c7e7ab3 100644 --- a/internal/platform/stat_bsd.go +++ b/internal/platform/stat_bsd.go @@ -3,24 +3,22 @@ package platform import ( - "io/fs" "os" "syscall" ) -func statTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) { +func fillStatFromOpenFile(stat *Stat_t, fd uintptr, t os.FileInfo) (err error) { d := t.Sys().(*syscall.Stat_t) + stat.Ino = d.Ino + stat.Dev = uint64(d.Dev) + stat.Mode = t.Mode() + stat.Nlink = uint64(d.Nlink) + stat.Size = d.Size atime := d.Atimespec + stat.Atim = atime.Sec*1e9 + atime.Nsec mtime := d.Mtimespec + stat.Mtim = mtime.Sec*1e9 + mtime.Nsec ctime := d.Ctimespec - return atime.Sec*1e9 + atime.Nsec, mtime.Sec*1e9 + mtime.Nsec, ctime.Sec*1e9 + ctime.Nsec -} - -func stat(_ fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlink, dev, inode uint64, err error) { - d := t.Sys().(*syscall.Stat_t) - atime := d.Atimespec - mtime := d.Mtimespec - ctime := d.Ctimespec - return atime.Sec*1e9 + atime.Nsec, mtime.Sec*1e9 + mtime.Nsec, ctime.Sec*1e9 + ctime.Nsec, - uint64(d.Nlink), uint64(d.Dev), uint64(d.Ino), nil + stat.Ctim = ctime.Sec*1e9 + ctime.Nsec + return } diff --git a/internal/platform/stat_linux.go b/internal/platform/stat_linux.go index ca6ee60f..e665cbee 100644 --- a/internal/platform/stat_linux.go +++ b/internal/platform/stat_linux.go @@ -6,23 +6,22 @@ package platform import ( - "io/fs" "os" "syscall" ) -func statTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) { +func fillStatFromOpenFile(stat *Stat_t, fd uintptr, t os.FileInfo) (err error) { d := t.Sys().(*syscall.Stat_t) + stat.Ino = uint64(d.Ino) + stat.Dev = uint64(d.Dev) + stat.Mode = t.Mode() + stat.Nlink = uint64(d.Nlink) + stat.Size = d.Size atime := d.Atim + stat.Atim = atime.Sec*1e9 + atime.Nsec mtime := d.Mtim + stat.Mtim = mtime.Sec*1e9 + mtime.Nsec ctime := d.Ctim - return atime.Sec*1e9 + atime.Nsec, mtime.Sec*1e9 + mtime.Nsec, ctime.Sec*1e9 + ctime.Nsec -} - -func stat(_ fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlink, dev, inode uint64, err error) { - d := t.Sys().(*syscall.Stat_t) - atime := d.Atim - mtime := d.Mtim - ctime := d.Ctim - return atime.Sec*1e9 + atime.Nsec, mtime.Sec*1e9 + mtime.Nsec, ctime.Sec*1e9 + ctime.Nsec, uint64(d.Nlink), uint64(d.Dev), uint64(d.Ino), nil + stat.Ctim = ctime.Sec*1e9 + ctime.Nsec + return } diff --git a/internal/platform/stat_test.go b/internal/platform/stat_test.go index e5962534..79340d25 100644 --- a/internal/platform/stat_test.go +++ b/internal/platform/stat_test.go @@ -4,13 +4,109 @@ import ( "os" "path" "runtime" + "syscall" "testing" "time" "github.com/tetratelabs/wazero/internal/testing/require" ) -func Test_Stat(t *testing.T) { +func TestStat(t *testing.T) { + tmpDir := t.TempDir() + + var stat Stat_t + require.EqualErrno(t, syscall.ENOENT, Stat(path.Join(tmpDir, "cat"), &stat)) + require.EqualErrno(t, syscall.ENOENT, Stat(path.Join(tmpDir, "sub/cat"), &stat)) + + t.Run("dir", func(t *testing.T) { + err := Stat(tmpDir, &stat) + require.NoError(t, err) + require.True(t, stat.Mode.IsDir()) + }) + + t.Run("file", func(t *testing.T) { + file := path.Join(tmpDir, "file") + require.NoError(t, os.WriteFile(file, nil, 0o400)) + + require.NoError(t, Stat(file, &stat)) + require.False(t, stat.Mode.IsDir()) + }) + + t.Run("subdir", func(t *testing.T) { + subdir := path.Join(tmpDir, "sub") + require.NoError(t, os.Mkdir(subdir, 0o500)) + + require.NoError(t, Stat(subdir, &stat)) + require.True(t, stat.Mode.IsDir()) + }) +} + +func TestStatFile(t *testing.T) { + tmpDir := t.TempDir() + + var stat Stat_t + + tmpDirF, err := OpenFile(tmpDir, syscall.O_RDONLY, 0) + if err != nil { + return + } + defer tmpDirF.Close() + + t.Run("dir", func(t *testing.T) { + err = StatFile(tmpDirF, &stat) + require.NoError(t, err) + require.True(t, stat.Mode.IsDir()) + }) + + if runtime.GOOS != "windows" { // windows allows you to stat a closed dir + t.Run("closed dir", func(t *testing.T) { + require.NoError(t, tmpDirF.Close()) + require.EqualErrno(t, syscall.EBADF, StatFile(tmpDirF, &stat)) + }) + } + + file := path.Join(tmpDir, "file") + require.NoError(t, os.WriteFile(file, nil, 0o400)) + fileF, err := OpenFile(file, syscall.O_RDONLY, 0) + if err != nil { + return + } + defer fileF.Close() + + t.Run("file", func(t *testing.T) { + err = StatFile(fileF, &stat) + require.NoError(t, err) + require.False(t, stat.Mode.IsDir()) + }) + + t.Run("closed file", func(t *testing.T) { + require.NoError(t, fileF.Close()) + require.EqualErrno(t, syscall.EBADF, StatFile(fileF, &stat)) + }) + + subdir := path.Join(tmpDir, "sub") + require.NoError(t, os.Mkdir(subdir, 0o500)) + subdirF, err := OpenFile(subdir, syscall.O_RDONLY, 0) + if err != nil { + return + } + defer subdirF.Close() + + t.Run("subdir", func(t *testing.T) { + err = StatFile(subdirF, &stat) + require.NoError(t, err) + require.True(t, stat.Mode.IsDir()) + }) + + if runtime.GOOS != "windows" { // windows allows you to stat a closed dir + t.Run("closed subdir", func(t *testing.T) { + require.NoError(t, subdirF.Close()) + require.EqualErrno(t, syscall.EBADF, StatFile(subdirF, &stat)) + }) + } +} + +func Test_StatFile_times(t *testing.T) { tmpDir := t.TempDir() file := path.Join(tmpDir, "file") @@ -49,63 +145,58 @@ func Test_Stat(t *testing.T) { err := os.Chtimes(file, time.UnixMicro(tc.atimeNsec/1e3), time.UnixMicro(tc.mtimeNsec/1e3)) require.NoError(t, err) - stat, err := os.Stat(file) + file, err := os.Open(file) require.NoError(t, err) + defer file.Close() - atimeNsec, mtimeNsec, _ := StatTimes(stat) - require.Equal(t, atimeNsec, tc.atimeNsec) - require.Equal(t, mtimeNsec, tc.mtimeNsec) + var stat Stat_t + require.NoError(t, StatFile(file, &stat)) + require.Equal(t, stat.Atim, tc.atimeNsec) + require.Equal(t, stat.Mtim, tc.mtimeNsec) }) } } -func TestStat_dev_inode(t *testing.T) { +func TestStatFile_dev_inode(t *testing.T) { tmpDir := t.TempDir() path1 := path.Join(tmpDir, "1") - fa, err := os.Create(path1) + f1, err := os.Create(path1) require.NoError(t, err) path2 := path.Join(tmpDir, "2") - fb, err := os.Create(path2) + f2, err := os.Create(path2) require.NoError(t, err) - stat1, err := fa.Stat() - require.NoError(t, err) - _, _, _, _, device1, inode1, err := Stat(fa, stat1) - require.NoError(t, err) + var stat1 Stat_t + require.NoError(t, StatFile(f1, &stat1)) - stat2, err := fb.Stat() - require.NoError(t, err) - _, _, _, _, device2, inode2, err := Stat(fb, stat2) - require.NoError(t, err) + var stat2 Stat_t + require.NoError(t, StatFile(f2, &stat2)) // The files should be on the same device, but different inodes - require.Equal(t, device1, device2) - require.NotEqual(t, inode1, inode2) + require.Equal(t, stat1.Dev, stat2.Dev) + require.NotEqual(t, stat1.Ino, stat2.Ino) // Redoing stat should result in the same inodes - stat1Again, err := os.Stat(path1) - require.NoError(t, err) - _, _, _, _, device1Again, inode1Again, err := Stat(fa, stat1Again) - require.NoError(t, err) - require.Equal(t, device1, device1Again) - require.Equal(t, inode1, inode1Again) + var stat1Again Stat_t + require.NoError(t, StatFile(f1, &stat1Again)) + + require.Equal(t, stat1.Dev, stat1Again.Dev) + require.Equal(t, stat1.Ino, stat1Again.Ino) // On Windows, we cannot rename while opening. // So we manually close here before renaming. - require.NoError(t, fa.Close()) - require.NoError(t, fb.Close()) + require.NoError(t, f1.Close()) + require.NoError(t, f2.Close()) // Renaming a file shouldn't change its inodes. require.NoError(t, Rename(path1, path2)) - fa, err = os.Open(path2) + f1, err = os.Open(path2) require.NoError(t, err) - defer func() { require.NoError(t, fa.Close()) }() - stat1Again, err = os.Stat(path2) - require.NoError(t, err) - _, _, _, _, device1Again, inode1Again, err = Stat(fa, stat1Again) - require.NoError(t, err) - require.Equal(t, device1, device1Again) - require.Equal(t, inode1, inode1Again) + defer f1.Close() + + require.NoError(t, StatFile(f1, &stat1Again)) + require.Equal(t, stat1.Dev, stat1Again.Dev) + require.Equal(t, stat1.Ino, stat1Again.Ino) } diff --git a/internal/platform/stat_unsupported.go b/internal/platform/stat_unsupported.go index 29fd0b6c..83f204ca 100644 --- a/internal/platform/stat_unsupported.go +++ b/internal/platform/stat_unsupported.go @@ -2,17 +2,9 @@ package platform -import ( - "io/fs" - "os" -) +import "os" -func statTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) { - atimeNsec, mtimeNsec, ctimeNsec = mtimes(t) - return -} - -func stat(_ fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlink, dev, inode uint64, err error) { - atimeNsec, mtimeNsec, ctimeNsec = mtimes(t) +func fillStatFromOpenFile(stat *Stat_t, fd uintptr, t os.FileInfo) (err error) { + fillStatFromFileInfo(stat, t) return } diff --git a/internal/platform/stat_windows.go b/internal/platform/stat_windows.go index 43fdc13c..cd1df421 100644 --- a/internal/platform/stat_windows.go +++ b/internal/platform/stat_windows.go @@ -3,37 +3,13 @@ package platform import ( - "io/fs" "os" "syscall" ) -// The following interfaces are used until we finalize our own FD-scoped file. -type ( - // fder is implemented by os.File in file_unix.go and file_windows.go - fder interface{ Fd() (fd uintptr) } -) - -func statTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) { +func fillStatFromOpenFile(stat *Stat_t, fd uintptr, t os.FileInfo) (err error) { d := t.Sys().(*syscall.Win32FileAttributeData) - atimeNsec = d.LastAccessTime.Nanoseconds() - mtimeNsec = d.LastWriteTime.Nanoseconds() - ctimeNsec = d.CreationTime.Nanoseconds() - return -} - -func stat(f fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlink, dev, inode uint64, err error) { - d := t.Sys().(*syscall.Win32FileAttributeData) - atimeNsec = d.LastAccessTime.Nanoseconds() - mtimeNsec = d.LastWriteTime.Nanoseconds() - ctimeNsec = d.CreationTime.Nanoseconds() - - of, ok := f.(fder) - if !ok { - return - } - - handle := syscall.Handle(of.Fd()) + handle := syscall.Handle(fd) var info syscall.ByHandleFileInformation if err = syscall.GetFileInformationByHandle(handle, &info); err != nil { // If the file descriptor is already closed, we have to re-open just like @@ -47,9 +23,16 @@ func stat(f fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlin err = nil } } - nlink, dev = uint64(info.NumberOfLinks), uint64(info.VolumeSerialNumber) + // FileIndex{High,Low} can be combined and used as a unique identifier like inode. // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/ns-fileapi-by_handle_file_information - inode = (uint64(info.FileIndexHigh) << 32) | uint64(info.FileIndexLow) + stat.Ino = (uint64(info.FileIndexHigh) << 32) | uint64(info.FileIndexLow) + stat.Dev = uint64(info.VolumeSerialNumber) + stat.Mode = t.Mode() + stat.Nlink = uint64(info.NumberOfLinks) + stat.Size = t.Size() + stat.Atim = d.LastAccessTime.Nanoseconds() + stat.Mtim = d.LastWriteTime.Nanoseconds() + stat.Ctim = d.CreationTime.Nanoseconds() return } diff --git a/internal/sys/fs.go b/internal/sys/fs.go index 8d554595..2a51efc8 100644 --- a/internal/sys/fs.go +++ b/internal/sys/fs.go @@ -181,17 +181,18 @@ func (f *FileEntry) IsDir() bool { if f.isDirectory { return true } - _, _ = f.Stat() // Maybe the file hasn't had stat yet. + var stat platform.Stat_t + _ = f.Stat(&stat) // Maybe the file hasn't had stat yet. return f.isDirectory } // Stat returns the underlying stat of this file. -func (f *FileEntry) Stat() (stat fs.FileInfo, err error) { - stat, err = sysfs.StatFile(f.File) - if err == nil && stat.IsDir() { +func (f *FileEntry) Stat(stat *platform.Stat_t) (err error) { + err = platform.StatFile(f.File, stat) + if err == nil && stat.Mode.IsDir() { f.isDirectory = true } - return stat, err + return } // ReadDir is the status of a prior fs.ReadDirFile call. diff --git a/internal/sysfs/adapter.go b/internal/sysfs/adapter.go index d0962860..aaecbf28 100644 --- a/internal/sysfs/adapter.go +++ b/internal/sysfs/adapter.go @@ -7,6 +7,8 @@ import ( pathutil "path" "runtime" "strings" + + "github.com/tetratelabs/wazero/internal/platform" ) // Adapt adapts the input to FS unless it is already one. NewDirFS should be @@ -44,7 +46,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, platform.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 @@ -52,6 +54,17 @@ func (a *adapter) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, er return f, nil } +// Stat implements FS.Stat +func (a *adapter) Stat(path string, stat *platform.Stat_t) error { + name := cleanPath(path) + f, err := a.fs.Open(name) + if err != nil { + return platform.UnwrapOSError(err) + } + defer f.Close() + return platform.StatFile(f, stat) +} + func cleanPath(name string) string { if len(name) == 0 { return name diff --git a/internal/sysfs/adapter_test.go b/internal/sysfs/adapter_test.go index fcbcb164..770a1909 100644 --- a/internal/sysfs/adapter_test.go +++ b/internal/sysfs/adapter_test.go @@ -21,14 +21,14 @@ func TestAdapt_MkDir(t *testing.T) { testFS := Adapt(os.DirFS(t.TempDir())) err := testFS.Mkdir("mkdir", fs.ModeDir) - require.Equal(t, syscall.ENOSYS, err) + require.EqualErrno(t, syscall.ENOSYS, err) } func TestAdapt_Chmod(t *testing.T) { testFS := Adapt(os.DirFS(t.TempDir())) err := testFS.Chmod("chmod", fs.ModeDir) - require.Equal(t, syscall.ENOSYS, err) + require.EqualErrno(t, syscall.ENOSYS, err) } func TestAdapt_Rename(t *testing.T) { @@ -48,7 +48,7 @@ func TestAdapt_Rename(t *testing.T) { require.NoError(t, err) err = testFS.Rename(file1, file2) - require.Equal(t, syscall.ENOSYS, err) + require.EqualErrno(t, syscall.ENOSYS, err) } func TestAdapt_Rmdir(t *testing.T) { @@ -60,7 +60,7 @@ func TestAdapt_Rmdir(t *testing.T) { require.NoError(t, os.Mkdir(realPath, 0o700)) err := testFS.Rmdir(path) - require.Equal(t, syscall.ENOSYS, err) + require.EqualErrno(t, syscall.ENOSYS, err) } func TestAdapt_Unlink(t *testing.T) { @@ -72,7 +72,7 @@ func TestAdapt_Unlink(t *testing.T) { require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600)) err := testFS.Unlink(path) - require.Equal(t, syscall.ENOSYS, err) + require.EqualErrno(t, syscall.ENOSYS, err) } func TestAdapt_Utimes(t *testing.T) { @@ -84,7 +84,7 @@ func TestAdapt_Utimes(t *testing.T) { require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600)) err := testFS.Utimes(path, 1, 1) - require.Equal(t, syscall.ENOSYS, err) + require.EqualErrno(t, syscall.ENOSYS, err) } func TestAdapt_Open_Read(t *testing.T) { @@ -100,10 +100,18 @@ func TestAdapt_Open_Read(t *testing.T) { _, err := testFS.OpenFile("../foo", os.O_RDONLY, 0) // fs.FS doesn't allow relative path lookups - require.Equal(t, syscall.EINVAL, err) + require.EqualErrno(t, syscall.EINVAL, err) }) } +func TestAdapt_Stat(t *testing.T) { + tmpDir := t.TempDir() + require.NoError(t, fstest.WriteTestFiles(tmpDir)) + + testFS := Adapt(os.DirFS(tmpDir)) + testStat(t, testFS) +} + // hackFS cheats the fs.FS contract by opening for write (os.O_RDWR). // // Until we have an alternate public interface for filesystems, some users will diff --git a/internal/sysfs/dirfs.go b/internal/sysfs/dirfs.go index 552588c4..027ebf51 100644 --- a/internal/sysfs/dirfs.go +++ b/internal/sysfs/dirfs.go @@ -41,18 +41,23 @@ func (d *dirFS) Open(name string) (fs.File, error) { } // OpenFile implements FS.OpenFile -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) +func (d *dirFS) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) { + if f, err := platform.OpenFile(d.join(path), flag, perm); err != nil { + return nil, err + } else { + return maybeWrapFile(f, d, path, flag, perm), nil } - return maybeWrapFile(f, d, name, flag, perm), nil +} + +// Stat implements FS.Stat +func (d *dirFS) Stat(path string, stat *platform.Stat_t) error { + return platform.Stat(d.join(path), stat) } // Mkdir implements FS.Mkdir func (d *dirFS) Mkdir(name string, perm fs.FileMode) (err error) { err = os.Mkdir(d.join(name), perm) - if err = UnwrapOSError(err); err == syscall.ENOTDIR { + if err = platform.UnwrapOSError(err); err == syscall.ENOTDIR { err = syscall.ENOENT } return @@ -61,14 +66,14 @@ func (d *dirFS) Mkdir(name string, perm fs.FileMode) (err error) { // Chmod implements FS.Chmod func (d *dirFS) Chmod(name string, perm fs.FileMode) error { err := os.Chmod(d.join(name), perm) - return UnwrapOSError(err) + return platform.UnwrapOSError(err) } // Rename implements FS.Rename func (d *dirFS) Rename(from, to string) error { from, to = d.join(from), d.join(to) err := platform.Rename(from, to) - return UnwrapOSError(err) + return platform.UnwrapOSError(err) } // Readlink implements FS.Readlink @@ -77,7 +82,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 = platform.UnwrapOSError(err) return } @@ -94,20 +99,19 @@ func (d *dirFS) Readlink(path string, buf []byte) (n int, err error) { // Link implements FS.Link. func (d *dirFS) Link(oldName, newName string) error { err := os.Link(d.join(oldName), d.join(newName)) - return UnwrapOSError(err) + return platform.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) + return platform.UnwrapOSError(err) } // Unlink implements FS.Unlink func (d *dirFS) Unlink(name string) (err error) { err = syscall.Unlink(d.join(name)) - if err = UnwrapOSError(err); err == syscall.EPERM { + if err = platform.UnwrapOSError(err); err == syscall.EPERM { err = syscall.EISDIR } return @@ -119,7 +123,7 @@ func (d *dirFS) Symlink(oldName, link string) (err error) { // 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)) - return UnwrapOSError(err) + return platform.UnwrapOSError(err) } // Utimes implements FS.Utimes @@ -128,15 +132,14 @@ func (d *dirFS) Utimes(name string, atimeNsec, mtimeNsec int64) error { syscall.NsecToTimespec(atimeNsec), syscall.NsecToTimespec(mtimeNsec), }) - return UnwrapOSError(err) + return platform.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) - return adjustTruncateError(err) + return platform.UnwrapOSError(err) } func (d *dirFS) join(name string) string { diff --git a/internal/sysfs/dirfs_test.go b/internal/sysfs/dirfs_test.go index 4ae5ec4e..f9f54de0 100644 --- a/internal/sysfs/dirfs_test.go +++ b/internal/sysfs/dirfs_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/tetratelabs/wazero/internal/fstest" + "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/testing/require" ) @@ -24,7 +25,7 @@ func TestNewDirFS(t *testing.T) { t.Run("host path not found", func(t *testing.T) { testFS := NewDirFS("a") _, err = testFS.OpenFile(".", os.O_RDONLY, 0) - require.Equal(t, syscall.ENOENT, err) + require.EqualErrno(t, syscall.ENOENT, err) }) t.Run("host path not a directory", func(t *testing.T) { arg0 := os.Args[0] // should be safe in scratch tests which don't have the source mounted. @@ -33,7 +34,7 @@ func TestNewDirFS(t *testing.T) { d, err := testFS.OpenFile(".", os.O_RDONLY, 0) require.NoError(t, err) _, err = d.(fs.ReadDirFile).ReadDir(-1) - require.Equal(t, syscall.ENOTDIR, errors.Unwrap(err)) + require.EqualErrno(t, syscall.ENOTDIR, errors.Unwrap(err)) }) } @@ -61,7 +62,7 @@ func TestDirFS_MkDir(t *testing.T) { t.Run("dir exists", func(t *testing.T) { err := testFS.Mkdir(name, fs.ModeDir) - require.Equal(t, syscall.EEXIST, err) + require.EqualErrno(t, syscall.EEXIST, err) }) t.Run("file exists", func(t *testing.T) { @@ -69,12 +70,12 @@ func TestDirFS_MkDir(t *testing.T) { require.NoError(t, os.Mkdir(realPath, 0o700)) err := testFS.Mkdir(name, fs.ModeDir) - require.Equal(t, syscall.EEXIST, err) + require.EqualErrno(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) + require.EqualErrno(t, syscall.ENOENT, err) }) // Remove the path so that we can test creating it with perms. @@ -112,9 +113,9 @@ func testChmod(t *testing.T, testFS FS, path string) { } func requireMode(t *testing.T, testFS FS, path string, mode fs.FileMode) { - stat, err := StatPath(testFS, path) - require.NoError(t, err) - require.Equal(t, mode, stat.Mode()&fs.ModePerm) + var stat platform.Stat_t + require.NoError(t, testFS.Stat(path, &stat)) + require.Equal(t, mode, stat.Mode.Perm()) } func TestDirFS_Rename(t *testing.T) { @@ -128,7 +129,7 @@ func TestDirFS_Rename(t *testing.T) { require.NoError(t, err) err = testFS.Rename("file2", file1) - require.Equal(t, syscall.ENOENT, err) + require.EqualErrno(t, syscall.ENOENT, err) }) t.Run("file to non-exist", func(t *testing.T) { tmpDir := t.TempDir() @@ -147,7 +148,7 @@ func TestDirFS_Rename(t *testing.T) { // Show the prior path no longer exists _, err = os.Stat(file1Path) - require.Equal(t, syscall.ENOENT, errors.Unwrap(err)) + require.EqualErrno(t, syscall.ENOENT, errors.Unwrap(err)) s, err := os.Stat(file2Path) require.NoError(t, err) @@ -168,7 +169,7 @@ func TestDirFS_Rename(t *testing.T) { // Show the prior path no longer exists _, err = os.Stat(dir1Path) - require.Equal(t, syscall.ENOENT, errors.Unwrap(err)) + require.EqualErrno(t, syscall.ENOENT, errors.Unwrap(err)) s, err := os.Stat(dir2Path) require.NoError(t, err) @@ -191,7 +192,7 @@ func TestDirFS_Rename(t *testing.T) { require.NoError(t, f.Close()) err = testFS.Rename(dir1, dir2) - require.Equal(t, syscall.ENOTDIR, err) + require.EqualErrno(t, syscall.ENOTDIR, err) }) t.Run("file to dir", func(t *testing.T) { tmpDir := t.TempDir() @@ -208,7 +209,7 @@ func TestDirFS_Rename(t *testing.T) { require.NoError(t, os.Mkdir(dir1Path, 0o700)) err = testFS.Rename(file1, dir1) - require.Equal(t, syscall.EISDIR, err) + require.EqualErrno(t, syscall.EISDIR, err) }) // Similar to https://github.com/ziglang/zig/blob/0.10.1/lib/std/fs/test.zig#L567-L582 @@ -236,7 +237,7 @@ func TestDirFS_Rename(t *testing.T) { // Show the prior path no longer exists _, err = os.Stat(dir1Path) - require.Equal(t, syscall.ENOENT, errors.Unwrap(err)) + require.EqualErrno(t, syscall.ENOENT, errors.Unwrap(err)) // Show the file inside that directory moved s, err := os.Stat(pathutil.Join(dir2Path, file1)) @@ -269,7 +270,7 @@ func TestDirFS_Rename(t *testing.T) { require.NoError(t, err) err = testFS.Rename(dir1, dir2) - require.ErrorIs(t, syscall.ENOTEMPTY, err) + require.EqualErrno(t, syscall.ENOTEMPTY, err) }) t.Run("file to file", func(t *testing.T) { @@ -293,7 +294,7 @@ func TestDirFS_Rename(t *testing.T) { // Show the prior path no longer exists _, err = os.Stat(file1Path) - require.Equal(t, syscall.ENOENT, errors.Unwrap(err)) + require.EqualErrno(t, syscall.ENOENT, errors.Unwrap(err)) // Show the file1 overwrote file2 b, err := os.ReadFile(file2Path) @@ -343,7 +344,7 @@ func TestDirFS_Rmdir(t *testing.T) { t.Run("doesn't exist", func(t *testing.T) { err := testFS.Rmdir(name) - require.Equal(t, syscall.ENOENT, err) + require.EqualErrno(t, syscall.ENOENT, err) }) t.Run("dir not empty", func(t *testing.T) { @@ -352,7 +353,7 @@ func TestDirFS_Rmdir(t *testing.T) { require.NoError(t, os.WriteFile(fileInDir, []byte{}, 0o600)) err := testFS.Rmdir(name) - require.Equal(t, syscall.ENOTEMPTY, err) + require.EqualErrno(t, syscall.ENOTEMPTY, err) require.NoError(t, os.Remove(fileInDir)) }) @@ -367,7 +368,7 @@ func TestDirFS_Rmdir(t *testing.T) { require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600)) err := testFS.Rmdir(name) - require.Equal(t, syscall.ENOTDIR, err) + require.EqualErrno(t, syscall.ENOTDIR, err) require.NoError(t, os.Remove(realPath)) }) @@ -382,14 +383,14 @@ func TestDirFS_Unlink(t *testing.T) { t.Run("doesn't exist", func(t *testing.T) { err := testFS.Unlink(name) - require.Equal(t, syscall.ENOENT, err) + require.EqualErrno(t, syscall.ENOENT, err) }) t.Run("not file", func(t *testing.T) { require.NoError(t, os.Mkdir(realPath, 0o700)) err := testFS.Unlink(name) - require.Equal(t, syscall.EISDIR, err) + require.EqualErrno(t, syscall.EISDIR, err) require.NoError(t, os.Remove(realPath)) }) @@ -431,6 +432,14 @@ func TestDirFS_OpenFile(t *testing.T) { }) } +func TestDirFS_Stat(t *testing.T) { + tmpDir := t.TempDir() + require.NoError(t, fstest.WriteTestFiles(tmpDir)) + + testFS := NewDirFS(tmpDir) + testStat(t, testFS) +} + func TestDirFS_Truncate(t *testing.T) { content := []byte("123456") diff --git a/internal/sysfs/readfs.go b/internal/sysfs/readfs.go index 001e2ca4..630cc3e2 100644 --- a/internal/sysfs/readfs.go +++ b/internal/sysfs/readfs.go @@ -5,6 +5,8 @@ import ( "io/fs" "os" "syscall" + + "github.com/tetratelabs/wazero/internal/platform" ) // NewReadFS is used to mask an existing FS for reads. Notably, this allows @@ -125,3 +127,8 @@ func maskForReads(f fs.File) fs.File { panic("BUG: unhandled pattern") } } + +// Stat implements FS.Stat +func (r *readFS) Stat(path string, stat *platform.Stat_t) error { + return r.fs.Stat(path, stat) +} diff --git a/internal/sysfs/readfs_test.go b/internal/sysfs/readfs_test.go index 68c1dea9..591c3556 100644 --- a/internal/sysfs/readfs_test.go +++ b/internal/sysfs/readfs_test.go @@ -38,7 +38,7 @@ func TestReadFS_MkDir(t *testing.T) { testFS := NewReadFS(writeable) err := testFS.Mkdir("mkdir", fs.ModeDir) - require.Equal(t, syscall.ENOSYS, err) + require.EqualErrno(t, syscall.ENOSYS, err) } func TestReadFS_Chmod(t *testing.T) { @@ -46,7 +46,7 @@ func TestReadFS_Chmod(t *testing.T) { testFS := NewReadFS(writeable) err := testFS.Chmod("chmod", fs.ModeDir) - require.Equal(t, syscall.ENOSYS, err) + require.EqualErrno(t, syscall.ENOSYS, err) } func TestReadFS_Rename(t *testing.T) { @@ -67,7 +67,7 @@ func TestReadFS_Rename(t *testing.T) { require.NoError(t, err) err = testFS.Rename(file1, file2) - require.Equal(t, syscall.ENOSYS, err) + require.EqualErrno(t, syscall.ENOSYS, err) } func TestReadFS_Rmdir(t *testing.T) { @@ -80,7 +80,7 @@ func TestReadFS_Rmdir(t *testing.T) { require.NoError(t, os.Mkdir(realPath, 0o700)) err := testFS.Rmdir(path) - require.Equal(t, syscall.ENOSYS, err) + require.EqualErrno(t, syscall.ENOSYS, err) } func TestReadFS_Unlink(t *testing.T) { @@ -93,7 +93,7 @@ func TestReadFS_Unlink(t *testing.T) { require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600)) err := testFS.Unlink(path) - require.Equal(t, syscall.ENOSYS, err) + require.EqualErrno(t, syscall.ENOSYS, err) } func TestReadFS_Utimes(t *testing.T) { @@ -106,7 +106,7 @@ func TestReadFS_Utimes(t *testing.T) { require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600)) err := testFS.Utimes(path, 1, 1) - require.Equal(t, syscall.ENOSYS, err) + require.EqualErrno(t, syscall.ENOSYS, err) } func TestReadFS_Open_Read(t *testing.T) { @@ -117,6 +117,15 @@ func TestReadFS_Open_Read(t *testing.T) { testOpen_Read(t, tmpDir, testFS) } +func TestReadFS_Stat(t *testing.T) { + tmpDir := t.TempDir() + require.NoError(t, fstest.WriteTestFiles(tmpDir)) + + writeable := NewDirFS(tmpDir) + testFS := NewReadFS(writeable) + testStat(t, testFS) +} + func TestReadFS_TestFS(t *testing.T) { t.Parallel() diff --git a/internal/sysfs/rootfs.go b/internal/sysfs/rootfs.go index 1c9ce515..f39841cb 100644 --- a/internal/sysfs/rootfs.go +++ b/internal/sysfs/rootfs.go @@ -7,6 +7,8 @@ import ( "strings" "syscall" "time" + + "github.com/tetratelabs/wazero/internal/platform" ) func NewRootFS(fs []FS, guestPaths []string) (FS, error) { @@ -190,27 +192,33 @@ func (d *openRootDir) readDir() (err error) { } func (d *openRootDir) rootEntry(name string, fsI int) (fs.DirEntry, error) { - if fi, err := StatPath(d.c.fs[fsI], "."); err != nil { + var stat platform.Stat_t + if err := d.c.fs[fsI].Stat(".", &stat); err != nil { return nil, err } else { - return fs.FileInfoToDirEntry(&renamedFileInfo{name, fi}), nil + return &dirInfo{name, &stat}, nil } } -// renamedFileInfo is needed to retain the stat info for a mount, knowing the -// directory is masked. For example, we don't want to leak the underlying host -// directory name. -type renamedFileInfo struct { +// dirInfo is a DirEntry based on a FileInfo. +type dirInfo struct { + // name is needed to retain the stat info for a mount, knowing the + // directory is masked. For example, we don't want to leak the underlying + // host directory name. name string - f fs.FileInfo + stat *platform.Stat_t } -func (i *renamedFileInfo) Name() string { return i.name } -func (i *renamedFileInfo) Size() int64 { return i.f.Size() } -func (i *renamedFileInfo) Mode() fs.FileMode { return i.f.Mode() } -func (i *renamedFileInfo) ModTime() time.Time { return i.f.ModTime() } -func (i *renamedFileInfo) IsDir() bool { return i.f.IsDir() } -func (i *renamedFileInfo) Sys() interface{} { return i.f.Sys() } +func (i *dirInfo) Name() string { return i.name } +func (i *dirInfo) Type() fs.FileMode { return i.stat.Mode.Type() } +func (i *dirInfo) Info() (fs.FileInfo, error) { return i, nil } +func (i *dirInfo) Size() int64 { return i.stat.Size } +func (i *dirInfo) Mode() fs.FileMode { return i.stat.Mode } +func (i *dirInfo) ModTime() time.Time { + return time.Unix(i.stat.Mtim/1e9, i.stat.Mtim%1e9) +} +func (i *dirInfo) IsDir() bool { return i.stat.Mode.IsDir() } +func (i *dirInfo) Sys() interface{} { return nil } func (d *openRootDir) ReadDir(count int) ([]fs.DirEntry, error) { if d.dirents == nil { @@ -238,6 +246,12 @@ func (d *openRootDir) ReadDir(count int) ([]fs.DirEntry, error) { return list, nil } +// Stat implements FS.Stat +func (c *CompositeFS) Stat(path string, stat *platform.Stat_t) error { + matchIndex, relativePath := c.chooseFS(path) + return c.fs[matchIndex].Stat(relativePath, stat) +} + // Mkdir implements FS.Mkdir func (c *CompositeFS) Mkdir(path string, perm fs.FileMode) error { matchIndex, relativePath := c.chooseFS(path) diff --git a/internal/sysfs/rootfs_test.go b/internal/sysfs/rootfs_test.go index 7b33b23f..bb42f5a6 100644 --- a/internal/sysfs/rootfs_test.go +++ b/internal/sysfs/rootfs_test.go @@ -142,7 +142,10 @@ func TestRootFS_Open(t *testing.T) { tmpDir = pathutil.Join(tmpDir, t.Name()) require.NoError(t, os.Mkdir(tmpDir, 0o700)) - testFS := NewDirFS(tmpDir) + testRootFS := NewDirFS(tmpDir) + testDirFS := NewDirFS(t.TempDir()) + testFS, err := NewRootFS([]FS{testRootFS, testDirFS}, []string{"/", "/emptydir"}) + require.NoError(t, err) testOpen_Read(t, tmpDir, testFS) @@ -156,6 +159,16 @@ func TestRootFS_Open(t *testing.T) { }) } +func TestRootFS_Stat(t *testing.T) { + tmpDir := t.TempDir() + require.NoError(t, fstest.WriteTestFiles(tmpDir)) + + tmpFS := NewDirFS(t.TempDir()) + testFS, err := NewRootFS([]FS{NewDirFS(tmpDir), tmpFS}, []string{"/", "/tmp"}) + require.NoError(t, err) + testStat(t, testFS) +} + func TestRootFS_TestFS(t *testing.T) { t.Parallel() @@ -276,7 +289,7 @@ func TestRootFS_examples(t *testing.T) { for _, p := range tc.unexpected { _, err := root.OpenFile(p, os.O_RDONLY, 0) - require.Equal(t, syscall.ENOENT, err) + require.EqualErrno(t, syscall.ENOENT, err) } }) } diff --git a/internal/sysfs/syscall.go b/internal/sysfs/syscall.go index a37da970..38ebab47 100644 --- a/internal/sysfs/syscall.go +++ b/internal/sysfs/syscall.go @@ -6,18 +6,6 @@ import ( "io/fs" ) -func adjustErrno(err error) error { - return err -} - -func adjustRmdirError(err error) error { - return err -} - -func adjustTruncateError(err error) error { - return err -} - func maybeWrapFile(f file, _ FS, _ string, _ int, _ fs.FileMode) file { return f } diff --git a/internal/sysfs/syscall_windows.go b/internal/sysfs/syscall_windows.go index 35685a91..bc87c8c5 100644 --- a/internal/sysfs/syscall_windows.go +++ b/internal/sysfs/syscall_windows.go @@ -3,70 +3,10 @@ package sysfs import ( "io/fs" "syscall" + + "github.com/tetratelabs/wazero/internal/platform" ) -// See https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499- -const ( - // ERROR_ACCESS_DENIED is a Windows error returned by syscall.Unlink - // instead of syscall.EPERM - ERROR_ACCESS_DENIED = syscall.Errno(5) - - // ERROR_INVALID_HANDLE is a Windows error returned by syscall.Write - // instead of syscall.EBADF - ERROR_INVALID_HANDLE = syscall.Errno(6) - - // ERROR_NEGATIVE_SEEK is a Windows error returned by os.Truncate - // instead of syscall.EINVAL - ERROR_NEGATIVE_SEEK = syscall.Errno(131) - - // ERROR_DIR_NOT_EMPTY is a Windows error returned by syscall.Rmdir - // instead of syscall.ENOTEMPTY - ERROR_DIR_NOT_EMPTY = syscall.Errno(145) - - // ERROR_ALREADY_EXISTS is a Windows error returned by os.Mkdir - // instead of syscall.EEXIST - ERROR_ALREADY_EXISTS = syscall.Errno(183) - - // 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 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 -} - -func adjustRmdirError(err error) error { - switch err { - case ERROR_DIRECTORY: - return syscall.ENOTDIR - } - return err -} - -func adjustTruncateError(err error) error { - if err == ERROR_NEGATIVE_SEEK { - return syscall.EINVAL - } - return err -} - // 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. // @@ -116,9 +56,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 { - if pe.Err = UnwrapOSError(pe.Err); pe.Err == syscall.EPERM { + if pe.Err = platform.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() { + var stat platform.Stat_t + if statErr := w.fs.Stat(w.path, &stat); statErr == nil && stat.Mode.IsDir() { pe.Err = syscall.EBADF } } diff --git a/internal/sysfs/sysfs.go b/internal/sysfs/sysfs.go index d0d3ed99..e099b615 100644 --- a/internal/sysfs/sysfs.go +++ b/internal/sysfs/sysfs.go @@ -8,7 +8,6 @@ package sysfs import ( "io" "io/fs" - "os" "syscall" "github.com/tetratelabs/wazero/internal/platform" @@ -62,6 +61,20 @@ type FS interface { // ^^ TODO: Consider syscall.Open, though this implies defining and // coercing flags and perms similar to what is done in os.OpenFile. + // Stat is similar to syscall.Stat, except the path is relative to this + // file system. + // + // # Errors + // + // The following errors are expected: + // - syscall.ENOENT: `path` doesn't exist. + // + // # Notes + // + // - An fs.FileInfo backed implementation sets atim, mtim and ctim to the + // same value. + Stat(path string, stat *platform.Stat_t) error + // Mkdir is similar to os.Mkdir, except the path is relative to this file // system, and syscall.Errno are returned instead of a os.PathError. // @@ -218,25 +231,6 @@ type FS interface { Utimes(path string, atimeNsec, mtimeNsec int64) 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 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. type readFile interface { fs.ReadDirFile @@ -393,46 +387,3 @@ func (r *writerAtOffset) Write(p []byte) (int, error) { r.offset += int64(n) return n, 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: - return syscall.EINVAL - case fs.ErrPermission: - return syscall.EPERM - case fs.ErrExist: - return syscall.EEXIST - case fs.ErrNotExist: - return syscall.ENOENT - case fs.ErrClosed: - return syscall.EBADF - } - 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 -} diff --git a/internal/sysfs/sysfs_test.go b/internal/sysfs/sysfs_test.go index 813ff57a..1b59ed25 100644 --- a/internal/sysfs/sysfs_test.go +++ b/internal/sysfs/sysfs_test.go @@ -5,7 +5,6 @@ import ( "embed" _ "embed" "errors" - "fmt" "io" "io/fs" "os" @@ -17,7 +16,6 @@ import ( gofstest "testing/fstest" "time" - "github.com/tetratelabs/wazero/internal/fstest" "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/testing/require" ) @@ -60,7 +58,7 @@ func testOpen_O_RDWR(t *testing.T, tmpDir string, testFS FS) { if runtime.GOOS != "windows" { // If the read-only flag was honored, we should not be able to write! _, err = w.Write(fileContents) - require.Equal(t, syscall.EBADF, UnwrapOSError(err)) + require.EqualErrno(t, syscall.EBADF, errors.Unwrap(err)) } // Verify stat on the file @@ -84,11 +82,16 @@ func testOpen_Read(t *testing.T, tmpDir string, testFS FS) { fileInDir := path.Join(dirRealPath, file1) require.NoError(t, os.WriteFile(fileInDir, []byte{2}, 0o600)) + emptydir := "emptydir" + emptydirRealPath := path.Join(tmpDir, emptydir) + err = os.Mkdir(emptydirRealPath, 0o700) + require.NoError(t, err) + t.Run("doesn't exist", func(t *testing.T) { _, err := testFS.OpenFile("nope", os.O_RDONLY, 0) // We currently follow os.Open not syscall.Open, so the error is wrapped. - require.Equal(t, syscall.ENOENT, err) + require.EqualErrno(t, syscall.ENOENT, err) }) t.Run(". opens root", func(t *testing.T) { @@ -97,11 +100,22 @@ func testOpen_Read(t *testing.T, tmpDir string, testFS FS) { defer f.Close() entries := requireReadDir(t, f) - require.Equal(t, 2, len(entries)) + require.Equal(t, 3, len(entries)) require.True(t, entries[0].IsDir()) require.Equal(t, dir, entries[0].Name()) - require.False(t, entries[1].IsDir()) - require.Equal(t, file, entries[1].Name()) + require.True(t, entries[1].IsDir()) + require.Equal(t, emptydir, entries[1].Name()) + require.False(t, entries[2].IsDir()) + require.Equal(t, file, entries[2].Name()) + }) + + t.Run("empty dir exists", func(t *testing.T) { + f, err := testFS.OpenFile(emptydir, os.O_RDONLY, 0) + require.NoError(t, err) + defer f.Close() + + entries := requireReadDir(t, f) + require.Zero(t, len(entries)) }) t.Run("dir exists", func(t *testing.T) { @@ -143,9 +157,9 @@ func testOpen_Read(t *testing.T, tmpDir string, testFS FS) { if w, ok := f.(io.Writer); ok { _, err := w.Write([]byte("hello")) if runtime.GOOS == "windows" { - requireErrno(t, syscall.EPERM, err) + require.EqualErrno(t, syscall.EPERM, errors.Unwrap(err)) } else { - requireErrno(t, syscall.EBADF, err) + require.EqualErrno(t, syscall.EBADF, errors.Unwrap(err)) } } }) @@ -162,12 +176,26 @@ func testOpen_Read(t *testing.T, tmpDir string, testFS FS) { }) } +func testStat(t *testing.T, testFS FS) { + var stat platform.Stat_t + require.EqualErrno(t, syscall.ENOENT, testFS.Stat("cat", &stat)) + require.EqualErrno(t, syscall.ENOENT, testFS.Stat("sub/cat", &stat)) + + err := testFS.Stat("sub/test.txt", &stat) + require.NoError(t, err) + require.False(t, stat.Mode.IsDir()) + + err = testFS.Stat("sub", &stat) + require.NoError(t, err) + require.True(t, stat.Mode.IsDir()) +} + // requireReadDir ensures the input file is a directory, and returns its // entries. func requireReadDir(t *testing.T, f fs.File) []fs.DirEntry { if w, ok := f.(io.Writer); ok { _, err := w.Write([]byte("hello")) - requireErrno(t, syscall.EBADF, err) + require.EqualErrno(t, syscall.EBADF, errors.Unwrap(err)) } // Ensure it implements fs.ReadDirFile dir, ok := f.(fs.ReadDirFile) @@ -191,7 +219,7 @@ func testUtimes(t *testing.T, tmpDir string, testFS FS) { err := testFS.Utimes("nope", time.Unix(123, 4*1e3).UnixNano(), time.Unix(567, 8*1e3).UnixNano()) - require.Equal(t, syscall.ENOENT, err) + require.EqualErrno(t, syscall.ENOENT, err) }) type test struct { @@ -243,24 +271,16 @@ func testUtimes(t *testing.T, tmpDir string, testFS FS) { err := testFS.Utimes(tc.path, tc.atimeNsec, tc.mtimeNsec) require.NoError(t, err) - stat, err := os.Stat(path.Join(tmpDir, tc.path)) - require.NoError(t, err) - - atimeNsec, mtimeNsec, _ := platform.StatTimes(stat) + var stat platform.Stat_t + require.NoError(t, testFS.Stat(tc.path, &stat)) if platform.CompilerSupported() { - require.Equal(t, atimeNsec, tc.atimeNsec) + require.Equal(t, stat.Atim, tc.atimeNsec) } // else only mtimes will return. - require.Equal(t, mtimeNsec, tc.mtimeNsec) + require.Equal(t, stat.Mtim, tc.mtimeNsec) }) } } -// requireErrno should only be used for functions that wrap the underlying -// syscall.Errno. -func requireErrno(t *testing.T, expected syscall.Errno, actual error) { - require.True(t, errors.Is(actual, expected), "expected %v, but was %v", expected, actual) -} - var ( //go:embed testdata/*.txt readerAtFS embed.FS @@ -535,98 +555,6 @@ func TestWriterAtOffset_Unsupported(t *testing.T) { 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()) -} - // Test_FileSync doesn't guarantee sync works because the operating system may // sync anyway. There is no test in Go for os.File Sync, but closest is similar // to below. Effectively, this only tests that things don't error. diff --git a/internal/sysfs/unsupported.go b/internal/sysfs/unsupported.go index e9c73b6d..70d1206a 100644 --- a/internal/sysfs/unsupported.go +++ b/internal/sysfs/unsupported.go @@ -3,6 +3,8 @@ package sysfs import ( "io/fs" "syscall" + + "github.com/tetratelabs/wazero/internal/platform" ) // UnimplementedFS is an FS that returns syscall.ENOSYS for all functions, @@ -24,6 +26,11 @@ func (UnimplementedFS) OpenFile(path string, flag int, perm fs.FileMode) (fs.Fil return nil, syscall.ENOSYS } +// Stat implements FS.Stat +func (UnimplementedFS) Stat(path string, stat *platform.Stat_t) error { + return syscall.ENOSYS +} + // Mkdir implements FS.Mkdir func (UnimplementedFS) Mkdir(path string, perm fs.FileMode) error { return syscall.ENOSYS diff --git a/internal/testing/require/require.go b/internal/testing/require/require.go index 35755265..b926c11c 100644 --- a/internal/testing/require/require.go +++ b/internal/testing/require/require.go @@ -13,6 +13,7 @@ import ( "reflect" "runtime" "strings" + "syscall" "unicode" "unicode/utf8" ) @@ -126,6 +127,19 @@ func Error(t TestingT, err error, formatWithArgs ...interface{}) { } } +// EqualErrno should be used for functions that return syscall.Errno or nil. +func EqualErrno(t TestingT, expected syscall.Errno, err error, formatWithArgs ...interface{}) { + if err == nil { + fail(t, "expected a syscall.Errno, but was nil", "", formatWithArgs...) + return + } + if se, ok := err.(syscall.Errno); !ok { + fail(t, fmt.Sprintf("expected %v to be a syscall.Errno", err), "", formatWithArgs...) + } else if se != expected { + fail(t, fmt.Sprintf("expected Errno %#[1]v(%[1]s), but was %#[2]v(%[2]s)", expected, err), "", formatWithArgs...) + } +} + // ErrorIs fails if the err is nil or errors.Is fails against the expected. // // - formatWithArgs are optional. When the first is a string that contains '%', it is treated like fmt.Sprintf. diff --git a/internal/testing/require/require_errno_test.go b/internal/testing/require/require_errno_test.go new file mode 100644 index 00000000..c70da689 --- /dev/null +++ b/internal/testing/require/require_errno_test.go @@ -0,0 +1,66 @@ +package require + +import ( + "io" + "runtime" + "syscall" + "testing" +) + +func TestEqualErrno(t *testing.T) { + // This specifically chooses ENOENT and EIO as outside windows, they tend + // to have the same errno literal and text message. + if runtime.GOOS == "windows" { + t.Skipf("error literals are different on windows") + } + + tests := []struct { + name string + require func(TestingT) + expectedLog string + }{ + { + name: "EqualErrno passes on equal", + require: func(t TestingT) { + EqualErrno(t, syscall.ENOENT, syscall.ENOENT) + }, + }, + { + name: "EqualErrno fails on nil", + require: func(t TestingT) { + EqualErrno(t, syscall.ENOENT, nil) + }, + expectedLog: "expected a syscall.Errno, but was nil", + }, + { + name: "EqualErrno fails on not Errno", + require: func(t TestingT) { + EqualErrno(t, syscall.ENOENT, io.EOF) + }, + expectedLog: `expected EOF to be a syscall.Errno`, + }, + { + name: "EqualErrno fails on not equal", + require: func(t TestingT) { + EqualErrno(t, syscall.ENOENT, syscall.EIO) + }, + expectedLog: `expected Errno 0x2(no such file or directory), but was 0x5(input/output error)`, + }, + { + name: "EqualErrno fails on not equal with format", + require: func(t TestingT) { + EqualErrno(t, syscall.ENOENT, syscall.EIO, "pay me %d", 5) + }, + expectedLog: `expected Errno 0x2(no such file or directory), but was 0x5(input/output error): pay me 5`, + }, + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + m := &mockT{t: t} + tc.require(m) + m.require(tc.expectedLog) + }) + } +} diff --git a/internal/wasi_snapshot_preview1/errno.go b/internal/wasi_snapshot_preview1/errno.go index 42e0ac96..4ba41f43 100644 --- a/internal/wasi_snapshot_preview1/errno.go +++ b/internal/wasi_snapshot_preview1/errno.go @@ -4,7 +4,7 @@ import ( "fmt" "syscall" - "github.com/tetratelabs/wazero/internal/sysfs" + "github.com/tetratelabs/wazero/internal/platform" ) // Errno is neither uint16 nor an alias for parity with wasm.ValueType. @@ -266,7 +266,7 @@ var errnoToString = [...]string{ // error codes. For example, wasi-filesystem and GOOS=js don't map to these // Errno. func ToErrno(err error) Errno { - errno := sysfs.UnwrapOSError(err) + errno := platform.UnwrapOSError(err) switch errno { case syscall.EACCES: diff --git a/internal/wasi_snapshot_preview1/errno_test.go b/internal/wasi_snapshot_preview1/errno_test.go index a9e90d47..a40c1073 100644 --- a/internal/wasi_snapshot_preview1/errno_test.go +++ b/internal/wasi_snapshot_preview1/errno_test.go @@ -3,8 +3,6 @@ package wasi_snapshot_preview1 import ( "syscall" "testing" - - "github.com/tetratelabs/wazero/internal/testing/require" ) func TestToErrno(t *testing.T) { @@ -94,7 +92,7 @@ func TestToErrno(t *testing.T) { expected: ErrnoPerm, }, { - name: "syscall.Errno unexpected == ErrnoIo", + name: "syscall.EqualErrno unexpected == ErrnoIo", input: syscall.Errno(0xfe), expected: ErrnoIo, }, @@ -103,8 +101,9 @@ func TestToErrno(t *testing.T) { for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { - errno := ToErrno(tc.input) - require.Equal(t, tc.expected, errno) + if errno := ToErrno(tc.input); errno != tc.expected { + t.Fatalf("expected %s but got %s", ErrnoName(tc.expected), ErrnoName(errno)) + } }) } }