diff --git a/config.go b/config.go index 5172967f..ea5a9e7e 100644 --- a/config.go +++ b/config.go @@ -13,10 +13,10 @@ import ( "github.com/tetratelabs/wazero/internal/engine/compiler" "github.com/tetratelabs/wazero/internal/engine/interpreter" "github.com/tetratelabs/wazero/internal/filecache" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/internalapi" "github.com/tetratelabs/wazero/internal/platform" internalsys "github.com/tetratelabs/wazero/internal/sys" - "github.com/tetratelabs/wazero/internal/sysfs" "github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/sys" ) @@ -835,7 +835,7 @@ func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) { environ = append(environ, result) } - var fs sysfs.FS + var fs fsapi.FS if f, ok := c.fsConfig.(*fsConfig); ok { if fs, err = f.toFS(); err != nil { return diff --git a/fsconfig.go b/fsconfig.go index deeb7d7d..099a7917 100644 --- a/fsconfig.go +++ b/fsconfig.go @@ -3,6 +3,7 @@ package wazero import ( "io/fs" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/sysfs" ) @@ -122,7 +123,7 @@ type FSConfig interface { type fsConfig struct { // fs are the currently configured filesystems. - fs []sysfs.FS + fs []fsapi.FS // guestPaths are the user-supplied names of the filesystems, retained for // error messages and fmt.Stringer. guestPaths []string @@ -139,7 +140,7 @@ func NewFSConfig() FSConfig { // clone makes a deep copy of this module config. func (c *fsConfig) clone() *fsConfig { ret := *c // copy except slice and maps which share a ref - ret.fs = make([]sysfs.FS, 0, len(c.fs)) + ret.fs = make([]fsapi.FS, 0, len(c.fs)) ret.fs = append(ret.fs, c.fs...) ret.guestPaths = make([]string, 0, len(c.guestPaths)) ret.guestPaths = append(ret.guestPaths, c.guestPaths...) @@ -165,7 +166,7 @@ func (c *fsConfig) WithFSMount(fs fs.FS, guestPath string) FSConfig { return c.withMount(sysfs.Adapt(fs), guestPath) } -func (c *fsConfig) withMount(fs sysfs.FS, guestPath string) FSConfig { +func (c *fsConfig) withMount(fs fsapi.FS, guestPath string) FSConfig { cleaned := sysfs.StripPrefixesAndTrailingSlash(guestPath) ret := c.clone() if i, ok := ret.guestPathToFS[cleaned]; ok { @@ -179,6 +180,6 @@ func (c *fsConfig) withMount(fs sysfs.FS, guestPath string) FSConfig { return ret } -func (c *fsConfig) toFS() (sysfs.FS, error) { +func (c *fsConfig) toFS() (fsapi.FS, error) { return sysfs.NewRootFS(c.fs, c.guestPaths) } diff --git a/fsconfig_test.go b/fsconfig_test.go index b114dafe..a6c58bbd 100644 --- a/fsconfig_test.go +++ b/fsconfig_test.go @@ -3,6 +3,7 @@ package wazero import ( "testing" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/sysfs" testfs "github.com/tetratelabs/wazero/internal/testing/fs" "github.com/tetratelabs/wazero/internal/testing/require" @@ -18,12 +19,12 @@ func TestFSConfig(t *testing.T) { tests := []struct { name string input FSConfig - expected sysfs.FS + expected fsapi.FS }{ { name: "empty", input: base, - expected: sysfs.UnimplementedFS{}, + expected: fsapi.UnimplementedFS{}, }, { name: "WithFSMount", @@ -38,7 +39,7 @@ func TestFSConfig(t *testing.T) { { name: "WithFsMount nil", input: base.WithFSMount(nil, "/"), - expected: sysfs.UnimplementedFS{}, + expected: fsapi.UnimplementedFS{}, }, { name: "WithDirMount overwrites", @@ -48,9 +49,9 @@ func TestFSConfig(t *testing.T) { { name: "Composition", input: base.WithReadOnlyDirMount(".", "/").WithDirMount("/tmp", "/tmp"), - expected: func() sysfs.FS { + expected: func() fsapi.FS { f, err := sysfs.NewRootFS( - []sysfs.FS{sysfs.NewReadFS(sysfs.NewDirFS(".")), sysfs.NewDirFS("/tmp")}, + []fsapi.FS{sysfs.NewReadFS(sysfs.NewDirFS(".")), sysfs.NewDirFS("/tmp")}, []string{"/", "/tmp"}, ) require.NoError(t, err) diff --git a/imports/assemblyscript/assemblyscript.go b/imports/assemblyscript/assemblyscript.go index 7c96bdc8..4cb1b3db 100644 --- a/imports/assemblyscript/assemblyscript.go +++ b/imports/assemblyscript/assemblyscript.go @@ -36,7 +36,7 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" . "github.com/tetratelabs/wazero/internal/assemblyscript" - "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/fsapi" internalsys "github.com/tetratelabs/wazero/internal/sys" "github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/sys" @@ -225,7 +225,7 @@ var traceStderr = traceStdout.WithGoModuleFunc(func(_ context.Context, mod api.M // (import "env" "trace" (func $~lib/builtins/trace (param i32 i32 f64 f64 f64 f64 f64))) // // See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L61 -func traceTo(mod api.Module, params []uint64, file platform.File) { +func traceTo(mod api.Module, params []uint64, file fsapi.File) { message := uint32(params[0]) nArgs := uint32(params[1]) arg0 := api.DecodeF64(params[2]) diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go index 8520d663..f4b40594 100644 --- a/imports/wasi_snapshot_preview1/fs.go +++ b/imports/wasi_snapshot_preview1/fs.go @@ -11,7 +11,7 @@ import ( "unsafe" "github.com/tetratelabs/wazero/api" - "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/sys" "github.com/tetratelabs/wazero/internal/sysfs" "github.com/tetratelabs/wazero/internal/wasip1" @@ -195,7 +195,7 @@ func fdFdstatGetFn(_ context.Context, mod api.Module, params []uint64) syscall.E } var fdflags uint16 - var st platform.Stat_t + var st fsapi.Stat_t var errno syscall.Errno if f, ok := fsc.LookupFile(fd); !ok { return syscall.EBADF @@ -424,7 +424,7 @@ func getWasiFiletype(fm fs.FileMode) uint8 { } } -func writeFilestat(buf []byte, st *platform.Stat_t) (errno syscall.Errno) { +func writeFilestat(buf []byte, st *fsapi.Stat_t) (errno syscall.Errno) { le.PutUint64(buf, st.Dev) le.PutUint64(buf[8:], st.Ino) le.PutUint64(buf[16:], uint64(getWasiFiletype(st.Mode))) @@ -507,9 +507,9 @@ func toTimes(atim, mtime int64, fstFlags uint16) (times [2]syscall.Timespec, err } else if set { times[0] = syscall.NsecToTimespec(atim) } else if now { - times[0].Nsec = platform.UTIME_NOW + times[0].Nsec = sysfs.UTIME_NOW } else { - times[0].Nsec = platform.UTIME_OMIT + times[0].Nsec = sysfs.UTIME_OMIT } // coerce mtim into a timespec @@ -519,9 +519,9 @@ func toTimes(atim, mtime int64, fstFlags uint16) (times [2]syscall.Timespec, err } else if set { times[1] = syscall.NsecToTimespec(mtime) } else if now { - times[1].Nsec = platform.UTIME_NOW + times[1].Nsec = sysfs.UTIME_NOW } else { - times[1].Nsec = platform.UTIME_OMIT + times[1].Nsec = sysfs.UTIME_OMIT } return } @@ -722,11 +722,11 @@ var fdRead = newHostFunc( // preader tracks an offset across multiple reads. type preader struct { - f platform.File + f fsapi.File offset int64 } -// Read implements the same function as documented on platform.File. +// Read implements the same function as documented on internalapi.File. func (w *preader) Read(buf []byte) (n int, errno syscall.Errno) { if len(buf) == 0 { return 0, 0 // less overhead on zero-length reads. @@ -925,7 +925,7 @@ func fdReaddirFn(_ context.Context, mod api.Module, params []uint64) syscall.Err // dotDirents returns "." and "..", where "." because wasi-testsuite does inode // validation. -func dotDirents(f *sys.FileEntry) ([]platform.Dirent, syscall.Errno) { +func dotDirents(f *sys.FileEntry) ([]fsapi.Dirent, syscall.Errno) { if isDir, errno := f.File.IsDir(); errno != 0 { return nil, errno } else if !isDir { @@ -943,7 +943,7 @@ func dotDirents(f *sys.FileEntry) ([]platform.Dirent, syscall.Errno) { dotDotIno = st.Ino } } - return []platform.Dirent{ + return []fsapi.Dirent{ {Name: ".", Ino: dotIno, Type: fs.ModeDir}, {Name: "..", Ino: dotDotIno, Type: fs.ModeDir}, }, 0 @@ -952,7 +952,7 @@ func dotDirents(f *sys.FileEntry) ([]platform.Dirent, syscall.Errno) { const largestDirent = int64(math.MaxUint32 - wasip1.DirentSize) // lastDirents is broken out from fdReaddirFn for testability. -func lastDirents(dir *sys.ReadDir, cookie int64) (dirents []platform.Dirent, errno syscall.Errno) { +func lastDirents(dir *sys.ReadDir, cookie int64) (dirents []fsapi.Dirent, errno syscall.Errno) { if cookie < 0 { errno = syscall.EINVAL // invalid as we will never send a negative cookie. return @@ -995,7 +995,7 @@ func lastDirents(dir *sys.ReadDir, cookie int64) (dirents []platform.Dirent, err // // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_readdir // See https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/cloudlibc/src/libc/dirent/readdir.c#L44 -func maxDirents(dirents []platform.Dirent, bufLen uint32) (bufused, direntCount uint32, writeTruncatedEntry bool) { +func maxDirents(dirents []fsapi.Dirent, bufLen uint32) (bufused, direntCount uint32, writeTruncatedEntry bool) { lenRemaining := bufLen for i := range dirents { d := dirents[i] @@ -1053,7 +1053,7 @@ func maxDirents(dirents []platform.Dirent, bufLen uint32) (bufused, direntCount // based on maxDirents. truncatedEntryLen means write one past entryCount, // without its name. See maxDirents for why func writeDirents( - dirents []platform.Dirent, + dirents []fsapi.Dirent, direntCount uint32, writeTruncatedEntry bool, buf []byte, @@ -1301,11 +1301,11 @@ func fdWriteFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno // pwriter tracks an offset across multiple writes. type pwriter struct { - f platform.File + f fsapi.File offset int64 } -// Write implements the same function as documented on platform.File. +// Write implements the same function as documented on internalapi.File. func (w *pwriter) Write(buf []byte) (n int, errno syscall.Errno) { if len(buf) == 0 { return 0, 0 // less overhead on zero-length writes. @@ -1461,7 +1461,7 @@ func pathFilestatGetFn(_ context.Context, mod api.Module, params []uint64) sysca } // Stat the file without allocating a file descriptor. - var st platform.Stat_t + var st fsapi.Stat_t if (flags & wasip1.LOOKUP_SYMLINK_FOLLOW) == 0 { st, errno = preopen.Lstat(pathName) @@ -1645,7 +1645,7 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) syscall.Errn } fileOpenFlags := openFlags(dirflags, oflags, fdflags, rights) - isDir := fileOpenFlags&platform.O_DIRECTORY != 0 + isDir := fileOpenFlags&fsapi.O_DIRECTORY != 0 if isDir && oflags&wasip1.O_CREAT != 0 { return syscall.EINVAL // use pathCreateDirectory! @@ -1692,7 +1692,7 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) syscall.Errn // // See https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/sources/at_fdcwd.c // See https://linux.die.net/man/2/openat -func atPath(fsc *sys.FSContext, mem api.Memory, fd int32, p, pathLen uint32) (sysfs.FS, string, syscall.Errno) { +func atPath(fsc *sys.FSContext, mem api.Memory, fd int32, p, pathLen uint32) (fsapi.FS, string, syscall.Errno) { b, ok := mem.Read(p, pathLen) if !ok { return nil, "", syscall.EFAULT @@ -1748,10 +1748,10 @@ func preopenPath(fsc *sys.FSContext, fd int32) (string, syscall.Errno) { func openFlags(dirflags, oflags, fdflags uint16, rights uint32) (openFlags int) { if dirflags&wasip1.LOOKUP_SYMLINK_FOLLOW == 0 { - openFlags |= platform.O_NOFOLLOW + openFlags |= fsapi.O_NOFOLLOW } if oflags&wasip1.O_DIRECTORY != 0 { - openFlags |= platform.O_DIRECTORY + openFlags |= fsapi.O_DIRECTORY return // Early return for directories as the rest of flags doesn't make sense for it. } else if oflags&wasip1.O_EXCL != 0 { openFlags |= syscall.O_EXCL diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go index 2c15b8fa..1d90fcb7 100644 --- a/imports/wasi_snapshot_preview1/fs_test.go +++ b/imports/wasi_snapshot_preview1/fs_test.go @@ -18,6 +18,7 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/fstest" "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/sys" @@ -240,7 +241,7 @@ func Test_fdFdstatGet(t *testing.T) { preopen := fsc.RootFS() // replace stdin with a fake TTY file. - // TODO: Make this easier once we have in-memory platform.File + // TODO: Make this easier once we have in-memory internalapi.File stdin, _ := fsc.LookupFile(sys.FdStdin) stdinFile, errno := sysfs.Adapt(&gofstest.MapFS{"stdin": &gofstest.MapFile{ Mode: fs.ModeDevice | fs.ModeCharDevice | 0o600, @@ -1950,8 +1951,8 @@ func Test_fdRead_Errors(t *testing.T) { } var ( - testDirents = func() []platform.Dirent { - d, errno := platform.OpenFSFile(fstest.FS, "dir", 0, 0) + testDirents = func() []fsapi.Dirent { + d, errno := sysfs.OpenFSFile(fstest.FS, "dir", 0, 0) if errno != 0 { panic(errno) } @@ -1960,7 +1961,7 @@ var ( if errno != 0 { panic(errno) } - dots := []platform.Dirent{ + dots := []fsapi.Dirent{ {Name: ".", Type: fs.ModeDir}, {Name: "..", Type: fs.ModeDir}, } @@ -3657,7 +3658,7 @@ func Test_pathFilestatSetTimes(t *testing.T) { t.Run(tc.name, func(t *testing.T) { defer log.Reset() - if tc.flags == 0 && !platform.SupportsSymlinkNoFollow { + if tc.flags == 0 && !sysfs.SupportsSymlinkNoFollow { tc.expectedErrno = wasip1.ErrnoNosys tc.expectedLog = strings.ReplaceAll(tc.expectedLog, "ESUCCESS", "ENOSYS") } @@ -3675,7 +3676,7 @@ func Test_pathFilestatSetTimes(t *testing.T) { sys := mod.(*wasm.ModuleInstance).Sys fsc := sys.FS() - var oldSt platform.Stat_t + var oldSt fsapi.Stat_t var errno syscall.Errno if tc.expectedErrno == wasip1.ErrnoSuccess { oldSt, errno = fsc.RootFS().Stat(pathName) @@ -3832,7 +3833,7 @@ func Test_pathOpen(t *testing.T) { tests := []struct { name string - fs sysfs.FS + fs fsapi.FS path func(t *testing.T) string oflags uint16 fdflags uint16 @@ -4098,7 +4099,7 @@ func requireContents(t *testing.T, fsc *sys.FSContext, expectedOpenedFd int32, f require.Equal(t, fileContents, buf) } -func readAll(t *testing.T, f platform.File) []byte { +func readAll(t *testing.T, f fsapi.File) []byte { st, errno := f.Stat() require.EqualErrno(t, 0, errno) buf := make([]byte, st.Size) @@ -5176,8 +5177,8 @@ func joinPath(dirName, baseName string) string { return path.Join(dirName, baseName) } -func openFile(t *testing.T, path string, flag int, perm fs.FileMode) platform.File { - f, errno := platform.OpenOSFile(path, flag, perm) +func openFile(t *testing.T, path string, flag int, perm fs.FileMode) fsapi.File { + f, errno := sysfs.OpenOSFile(path, flag, perm) require.EqualErrno(t, 0, errno) return f } diff --git a/imports/wasi_snapshot_preview1/fs_unit_test.go b/imports/wasi_snapshot_preview1/fs_unit_test.go index d0c07932..e7b31fda 100644 --- a/imports/wasi_snapshot_preview1/fs_unit_test.go +++ b/imports/wasi_snapshot_preview1/fs_unit_test.go @@ -5,9 +5,10 @@ import ( "syscall" "testing" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/fstest" - "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/sys" + "github.com/tetratelabs/wazero/internal/sysfs" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasip1" ) @@ -17,7 +18,7 @@ func Test_lastDirents(t *testing.T) { name string f *sys.ReadDir cookie int64 - expectedDirents []platform.Dirent + expectedDirents []fsapi.Dirent expectedErrno syscall.Errno }{ { @@ -102,7 +103,7 @@ func Test_lastDirents(t *testing.T) { func Test_maxDirents(t *testing.T) { tests := []struct { name string - dirents []platform.Dirent + dirents []fsapi.Dirent maxLen uint32 expectedCount uint32 expectedwriteTruncatedEntry bool @@ -194,9 +195,9 @@ func Test_maxDirents(t *testing.T) { } var ( - testDirents = func() []platform.Dirent { + testDirents = func() []fsapi.Dirent { dPath := "dir" - d, errno := platform.OpenFSFile(fstest.FS, dPath, syscall.O_RDONLY, 0) + d, errno := sysfs.OpenFSFile(fstest.FS, dPath, syscall.O_RDONLY, 0) if errno != 0 { panic(errno) } @@ -234,7 +235,7 @@ var ( func Test_writeDirents(t *testing.T) { tests := []struct { name string - entries []platform.Dirent + entries []fsapi.Dirent entryCount uint32 writeTruncatedEntry bool expectedEntriesBuf []byte @@ -291,37 +292,37 @@ func Test_openFlags(t *testing.T) { }{ { name: "oflags=0", - expectedOpenFlags: platform.O_NOFOLLOW | syscall.O_RDONLY, + expectedOpenFlags: fsapi.O_NOFOLLOW | syscall.O_RDONLY, }, { name: "oflags=O_CREAT", oflags: wasip1.O_CREAT, - expectedOpenFlags: platform.O_NOFOLLOW | syscall.O_RDWR | syscall.O_CREAT, + expectedOpenFlags: fsapi.O_NOFOLLOW | syscall.O_RDWR | syscall.O_CREAT, }, { name: "oflags=O_DIRECTORY", oflags: wasip1.O_DIRECTORY, - expectedOpenFlags: platform.O_NOFOLLOW | platform.O_DIRECTORY, + expectedOpenFlags: fsapi.O_NOFOLLOW | fsapi.O_DIRECTORY, }, { name: "oflags=O_EXCL", oflags: wasip1.O_EXCL, - expectedOpenFlags: platform.O_NOFOLLOW | syscall.O_RDONLY | syscall.O_EXCL, + expectedOpenFlags: fsapi.O_NOFOLLOW | syscall.O_RDONLY | syscall.O_EXCL, }, { name: "oflags=O_TRUNC", oflags: wasip1.O_TRUNC, - expectedOpenFlags: platform.O_NOFOLLOW | syscall.O_RDWR | syscall.O_TRUNC, + expectedOpenFlags: fsapi.O_NOFOLLOW | syscall.O_RDWR | syscall.O_TRUNC, }, { name: "fdflags=FD_APPEND", fdflags: wasip1.FD_APPEND, - expectedOpenFlags: platform.O_NOFOLLOW | syscall.O_RDWR | syscall.O_APPEND, + expectedOpenFlags: fsapi.O_NOFOLLOW | syscall.O_RDWR | syscall.O_APPEND, }, { name: "oflags=O_TRUNC|O_CREAT", oflags: wasip1.O_TRUNC | wasip1.O_CREAT, - expectedOpenFlags: platform.O_NOFOLLOW | syscall.O_RDWR | syscall.O_TRUNC | syscall.O_CREAT, + expectedOpenFlags: fsapi.O_NOFOLLOW | syscall.O_RDWR | syscall.O_TRUNC | syscall.O_CREAT, }, { name: "dirflags=LOOKUP_SYMLINK_FOLLOW", @@ -331,17 +332,17 @@ func Test_openFlags(t *testing.T) { { name: "rights=FD_READ", rights: wasip1.RIGHT_FD_READ, - expectedOpenFlags: platform.O_NOFOLLOW | syscall.O_RDONLY, + expectedOpenFlags: fsapi.O_NOFOLLOW | syscall.O_RDONLY, }, { name: "rights=FD_WRITE", rights: wasip1.RIGHT_FD_WRITE, - expectedOpenFlags: platform.O_NOFOLLOW | syscall.O_WRONLY, + expectedOpenFlags: fsapi.O_NOFOLLOW | syscall.O_WRONLY, }, { name: "rights=FD_READ|FD_WRITE", rights: wasip1.RIGHT_FD_READ | wasip1.RIGHT_FD_WRITE, - expectedOpenFlags: platform.O_NOFOLLOW | syscall.O_RDWR, + expectedOpenFlags: fsapi.O_NOFOLLOW | syscall.O_RDWR, }, } diff --git a/imports/wasi_snapshot_preview1/poll_test.go b/imports/wasi_snapshot_preview1/poll_test.go index 8b2f0279..f0920328 100644 --- a/imports/wasi_snapshot_preview1/poll_test.go +++ b/imports/wasi_snapshot_preview1/poll_test.go @@ -9,7 +9,7 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" - "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/sys" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasip1" @@ -154,7 +154,7 @@ func Test_pollOneoff_Stdin(t *testing.T) { name string in, out, nsubscriptions, resultNevents uint32 mem []byte // at offset in - stdin platform.File + stdin fsapi.File expectedErrno wasip1.Errno expectedMem []byte // at offset out expectedLog string @@ -394,7 +394,7 @@ func Test_pollOneoff_Stdin(t *testing.T) { } } -func setStdin(t *testing.T, mod api.Module, stdin platform.File) { +func setStdin(t *testing.T, mod api.Module, stdin fsapi.File) { fsc := mod.(*wasm.ModuleInstance).Sys.FS() f, ok := fsc.LookupFile(sys.FdStdin) require.True(t, ok) @@ -533,9 +533,9 @@ var fdReadSub = fdReadSubFd(byte(sys.FdStdin)) // https://github.com/mattn/go-isatty/blob/v0.0.18/isatty_tcgets.go#LL11C1-L12C1 type ttyStat struct{} -// Stat implements the same method as documented on platform.File -func (ttyStat) Stat() (platform.Stat_t, syscall.Errno) { - return platform.Stat_t{ +// Stat implements the same method as documented on internalapi.File +func (ttyStat) Stat() (fsapi.Stat_t, syscall.Errno) { + return fsapi.Stat_t{ Mode: fs.ModeDevice | fs.ModeCharDevice, Nlink: 1, }, 0 @@ -551,7 +551,7 @@ type neverReadyTtyStdinFile struct { ttyStat } -// PollRead implements the same method as documented on platform.File +// PollRead implements the same method as documented on internalapi.File func (neverReadyTtyStdinFile) PollRead(timeout *time.Duration) (ready bool, errno syscall.Errno) { time.Sleep(*timeout) return false, 0 @@ -563,7 +563,7 @@ type pollStdinFile struct { ready bool } -// PollRead implements the same method as documented on platform.File +// PollRead implements the same method as documented on internalapi.File func (p *pollStdinFile) PollRead(*time.Duration) (ready bool, errno syscall.Errno) { return p.ready, 0 } diff --git a/imports/wasi_snapshot_preview1/wasi_stdlib_test.go b/imports/wasi_snapshot_preview1/wasi_stdlib_test.go index 462a6e09..031e5ffb 100644 --- a/imports/wasi_snapshot_preview1/wasi_stdlib_test.go +++ b/imports/wasi_snapshot_preview1/wasi_stdlib_test.go @@ -13,7 +13,7 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" - "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/fsapi" internalsys "github.com/tetratelabs/wazero/internal/sys" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/sys" @@ -201,7 +201,7 @@ func compileAndRun(t *testing.T, config wazero.ModuleConfig, bin []byte) (consol return compileAndRunWithStdin(t, config, bin, nil) } -func compileAndRunWithStdin(t *testing.T, config wazero.ModuleConfig, bin []byte, stdin platform.File) (console string) { +func compileAndRunWithStdin(t *testing.T, config wazero.ModuleConfig, bin []byte, stdin fsapi.File) (console string) { // same for console and stderr as sometimes the stack trace is in one or the other. var consoleBuf bytes.Buffer @@ -239,7 +239,7 @@ func Test_Poll(t *testing.T) { tests := []struct { name string args []string - stdin platform.File + stdin fsapi.File expectedOutput string expectedTimeout time.Duration }{ diff --git a/internal/fsapi/constants.go b/internal/fsapi/constants.go new file mode 100644 index 00000000..868b9c16 --- /dev/null +++ b/internal/fsapi/constants.go @@ -0,0 +1,13 @@ +//go:build !windows && !js && !illumos && !solaris + +package fsapi + +import "syscall" + +// Simple aliases to constants in the syscall package for portability with +// platforms which do not have them (e.g. windows) +const ( + O_DIRECTORY = syscall.O_DIRECTORY + O_NOFOLLOW = syscall.O_NOFOLLOW + O_NONBLOCK = syscall.O_NONBLOCK +) diff --git a/internal/fsapi/constants_js.go b/internal/fsapi/constants_js.go new file mode 100644 index 00000000..af73ddb6 --- /dev/null +++ b/internal/fsapi/constants_js.go @@ -0,0 +1,8 @@ +package fsapi + +// See the comments on the same constants in constants_windows.go +const ( + O_DIRECTORY = 1 << 29 + O_NOFOLLOW = 1 << 30 + O_NONBLOCK = 1 << 31 +) diff --git a/internal/fsapi/constants_sun.go b/internal/fsapi/constants_sun.go new file mode 100644 index 00000000..a0de49b7 --- /dev/null +++ b/internal/fsapi/constants_sun.go @@ -0,0 +1,12 @@ +//go:build illumos || solaris + +package fsapi + +import "syscall" + +// See https://github.com/illumos/illumos-gate/blob/edd580643f2cf1434e252cd7779e83182ea84945/usr/src/uts/common/sys/fcntl.h#L90 +const ( + O_DIRECTORY = 0x1000000 + O_NOFOLLOW = syscall.O_NOFOLLOW + O_NONBLOCK = syscall.O_NONBLOCK +) diff --git a/internal/fsapi/constants_windows.go b/internal/fsapi/constants_windows.go new file mode 100644 index 00000000..33aed870 --- /dev/null +++ b/internal/fsapi/constants_windows.go @@ -0,0 +1,24 @@ +package fsapi + +import "syscall" + +// Windows does not have these constants, we declare placeholders which should +// not conflict with other open flags. These placeholders are not declared as +// value zero so code written in a way which expects them to be bit flags still +// works as expected. +// +// Since those placeholder are not interpreted by the open function, the unix +// features they represent are also not implemented on windows: +// +// - O_DIRECTORY allows programs to ensure that the opened file is a directory. +// This could be emulated by doing a stat call on the file after opening it +// to verify that it is in fact a directory, then closing it and returning an +// error if it is not. +// +// - O_NOFOLLOW allows programs to ensure that if the opened file is a symbolic +// link, the link itself is opened instead of its target. +const ( + O_DIRECTORY = 1 << 29 + O_NOFOLLOW = 1 << 30 + O_NONBLOCK = syscall.O_NONBLOCK +) diff --git a/internal/platform/dir.go b/internal/fsapi/dir.go similarity index 81% rename from internal/platform/dir.go rename to internal/fsapi/dir.go index 994c951b..c28783b4 100644 --- a/internal/platform/dir.go +++ b/internal/fsapi/dir.go @@ -1,8 +1,7 @@ -package platform +package fsapi import ( "fmt" - "io" "io/fs" "syscall" "time" @@ -36,23 +35,6 @@ func (d *Dirent) IsDir() bool { return d.Type == fs.ModeDir } -func adjustReaddirErr(f File, isClosed bool, err error) syscall.Errno { - if err == io.EOF { - return 0 // e.g. Readdir on darwin returns io.EOF, but linux doesn't. - } else if errno := UnwrapOSError(err); errno != 0 { - errno = dirError(f, isClosed, errno) - // Ignore errors when the file was closed or removed. - switch errno { - case syscall.EIO, syscall.EBADF: // closed while open - return 0 - case syscall.ENOENT: // Linux error when removed while open - return 0 - } - return errno - } - return 0 -} - // DirFile is embeddable to reduce the amount of functions to implement a file. type DirFile struct{} diff --git a/internal/platform/file.go b/internal/fsapi/file.go similarity index 50% rename from internal/platform/file.go rename to internal/fsapi/file.go index eed20ef7..2d6ad037 100644 --- a/internal/platform/file.go +++ b/internal/fsapi/file.go @@ -1,9 +1,7 @@ -package platform +package fsapi import ( - "io" "io/fs" - "os" "syscall" "time" ) @@ -382,530 +380,3 @@ type File interface { // https://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html Close() syscall.Errno } - -// UnimplementedFile is a File that returns syscall.ENOSYS for all functions, -// This should be embedded to have forward compatible implementations. -type UnimplementedFile struct{} - -// Ino implements File.Ino -func (UnimplementedFile) Ino() (uint64, syscall.Errno) { - return 0, 0 -} - -// IsAppend implements File.IsAppend -func (UnimplementedFile) IsAppend() bool { - return false -} - -// SetAppend implements File.SetAppend -func (UnimplementedFile) SetAppend(bool) syscall.Errno { - return syscall.ENOSYS -} - -// IsNonblock implements File.IsNonblock -func (UnimplementedFile) IsNonblock() bool { - return false -} - -// SetNonblock implements File.SetNonblock -func (UnimplementedFile) SetNonblock(bool) syscall.Errno { - return syscall.ENOSYS -} - -// Stat implements File.Stat -func (UnimplementedFile) Stat() (Stat_t, syscall.Errno) { - return Stat_t{}, syscall.ENOSYS -} - -// IsDir implements File.IsDir -func (UnimplementedFile) IsDir() (bool, syscall.Errno) { - return false, syscall.ENOSYS -} - -// Read implements File.Read -func (UnimplementedFile) Read([]byte) (int, syscall.Errno) { - return 0, syscall.ENOSYS -} - -// Pread implements File.Pread -func (UnimplementedFile) Pread([]byte, int64) (int, syscall.Errno) { - return 0, syscall.ENOSYS -} - -// Seek implements File.Seek -func (UnimplementedFile) Seek(int64, int) (int64, syscall.Errno) { - return 0, syscall.ENOSYS -} - -// Readdir implements File.Readdir -func (UnimplementedFile) Readdir(int) (dirents []Dirent, errno syscall.Errno) { - return nil, syscall.ENOSYS -} - -// PollRead implements File.PollRead -func (UnimplementedFile) PollRead(*time.Duration) (ready bool, errno syscall.Errno) { - return false, syscall.ENOSYS -} - -// Write implements File.Write -func (UnimplementedFile) Write([]byte) (int, syscall.Errno) { - return 0, syscall.ENOSYS -} - -// Pwrite implements File.Pwrite -func (UnimplementedFile) Pwrite([]byte, int64) (int, syscall.Errno) { - return 0, syscall.ENOSYS -} - -// Truncate implements File.Truncate -func (UnimplementedFile) Truncate(int64) syscall.Errno { - return syscall.ENOSYS -} - -// Sync implements File.Sync -func (UnimplementedFile) Sync() syscall.Errno { - return 0 // not syscall.ENOSYS -} - -// Datasync implements File.Datasync -func (UnimplementedFile) Datasync() syscall.Errno { - return 0 // not syscall.ENOSYS -} - -// Chmod implements File.Chmod -func (UnimplementedFile) Chmod(fs.FileMode) syscall.Errno { - return syscall.ENOSYS -} - -// Chown implements File.Chown -func (UnimplementedFile) Chown(int, int) syscall.Errno { - return syscall.ENOSYS -} - -// Utimens implements File.Utimens -func (UnimplementedFile) Utimens(*[2]syscall.Timespec) syscall.Errno { - return syscall.ENOSYS -} - -func NewStdioFile(stdin bool, f fs.File) (File, error) { - // Return constant stat, which has fake times, but keep the underlying - // file mode. Fake times are needed to pass wasi-testsuite. - // https://github.com/WebAssembly/wasi-testsuite/blob/af57727/tests/rust/src/bin/fd_filestat_get.rs#L1-L19 - var mode fs.FileMode - if st, err := f.Stat(); err != nil { - return nil, err - } else { - mode = st.Mode() - } - var flag int - if stdin { - flag = syscall.O_RDONLY - } else { - flag = syscall.O_WRONLY - } - var file File - if of, ok := f.(*os.File); ok { - // This is ok because functions that need path aren't used by stdioFile - file = newOsFile("", flag, 0, of) - } else { - file = &fsFile{file: f} - } - return &stdioFile{File: file, st: Stat_t{Mode: mode, Nlink: 1}}, nil -} - -func OpenFile(path string, flag int, perm fs.FileMode) (*os.File, syscall.Errno) { - if flag&O_DIRECTORY != 0 && flag&(syscall.O_WRONLY|syscall.O_RDWR) != 0 { - return nil, syscall.EISDIR // invalid to open a directory writeable - } - return openFile(path, flag, perm) -} - -func OpenOSFile(path string, flag int, perm fs.FileMode) (File, syscall.Errno) { - f, errno := OpenFile(path, flag, perm) - if errno != 0 { - return nil, errno - } - return newOsFile(path, flag, perm, f), 0 -} - -func OpenFSFile(fs fs.FS, path string, flag int, perm fs.FileMode) (File, syscall.Errno) { - if flag&O_DIRECTORY != 0 && flag&(syscall.O_WRONLY|syscall.O_RDWR) != 0 { - return nil, syscall.EISDIR // invalid to open a directory writeable - } - f, err := fs.Open(path) - if errno := UnwrapOSError(err); errno != 0 { - return nil, errno - } - // Don't return an os.File because the path is not absolute. osFile needs - // the path to be real and certain fs.File impls are subrooted. - return &fsFile{fs: fs, name: path, file: f}, 0 -} - -type stdioFile struct { - File - st Stat_t -} - -// IsDir implements File.IsDir -func (f *stdioFile) IsDir() (bool, syscall.Errno) { - return false, 0 -} - -// Stat implements File.Stat -func (f *stdioFile) Stat() (Stat_t, syscall.Errno) { - return f.st, 0 -} - -// Close implements File.Close -func (f *stdioFile) Close() syscall.Errno { - return 0 -} - -// fsFile is used for wrapped os.File, like os.Stdin or any fs.File -// implementation. Notably, this does not have access to the full file path. -// so certain operations can't be supported, such as inode lookups on Windows. -type fsFile struct { - UnimplementedFile - - // fs is the file-system that opened the file, or nil when wrapped for - // pre-opens like stdio. - fs fs.FS - - // name is what was used in fs for Open, so it may not be the actual path. - name string - - // file is always set, possibly an os.File like os.Stdin. - file fs.File - - // closed is true when closed was called. This ensures proper syscall.EBADF - closed bool - - // cachedStat includes fields that won't change while a file is open. - cachedSt *cachedStat -} - -type cachedStat struct { - // fileType is the same as what's documented on Dirent. - fileType fs.FileMode - - // ino is the same as what's documented on Dirent. - ino uint64 -} - -// cachedStat returns the cacheable parts of platform.Stat_t or an error if -// they couldn't be retrieved. -func (f *fsFile) cachedStat() (fileType fs.FileMode, ino uint64, errno syscall.Errno) { - if f.cachedSt == nil { - if _, errno = f.Stat(); errno != 0 { - return - } - } - return f.cachedSt.fileType, f.cachedSt.ino, 0 -} - -// Ino implements File.Ino -func (f *fsFile) Ino() (uint64, syscall.Errno) { - if _, ino, errno := f.cachedStat(); errno != 0 { - return 0, errno - } else { - return ino, 0 - } -} - -// IsAppend implements File.IsAppend -func (f *fsFile) IsAppend() bool { - return false -} - -// SetAppend implements File.SetAppend -func (f *fsFile) SetAppend(bool) (errno syscall.Errno) { - return fileError(f, f.closed, syscall.ENOSYS) -} - -// IsDir implements File.IsDir -func (f *fsFile) IsDir() (bool, syscall.Errno) { - if ft, _, errno := f.cachedStat(); errno != 0 { - return false, errno - } else if ft.Type() == fs.ModeDir { - return true, 0 - } - return false, 0 -} - -// Stat implements File.Stat -func (f *fsFile) Stat() (st Stat_t, errno syscall.Errno) { - if f.closed { - errno = syscall.EBADF - return - } - - // While some functions in platform.File need the full path, especially in - // Windows, stat does not. Casting here allows os.DirFS to return inode - // information. - if of, ok := f.file.(*os.File); ok { - if st, errno = statFile(of); errno != 0 { - return - } - return f.cacheStat(st) - } else if t, err := f.file.Stat(); err != nil { - errno = UnwrapOSError(err) - return - } else { - st = statFromDefaultFileInfo(t) - return f.cacheStat(st) - } -} - -func (f *fsFile) cacheStat(st Stat_t) (Stat_t, syscall.Errno) { - f.cachedSt = &cachedStat{fileType: st.Mode & fs.ModeType, ino: st.Ino} - return st, 0 -} - -// Read implements File.Read -func (f *fsFile) Read(buf []byte) (n int, errno syscall.Errno) { - if n, errno = read(f.file, buf); errno != 0 { - // Defer validation overhead until we've already had an error. - errno = fileError(f, f.closed, errno) - } - return -} - -// Pread implements File.Pread -func (f *fsFile) Pread(buf []byte, off int64) (n int, errno syscall.Errno) { - if ra, ok := f.file.(io.ReaderAt); ok { - if n, errno = pread(ra, buf, off); errno != 0 { - // Defer validation overhead until we've already had an error. - errno = fileError(f, f.closed, errno) - } - return - } - - // See /RATIONALE.md "fd_pread: io.Seeker fallback when io.ReaderAt is not supported" - if rs, ok := f.file.(io.ReadSeeker); ok { - // Determine the current position in the file, as we need to revert it. - currentOffset, err := rs.Seek(0, io.SeekCurrent) - if err != nil { - return 0, fileError(f, f.closed, UnwrapOSError(err)) - } - - // Put the read position back when complete. - defer func() { _, _ = rs.Seek(currentOffset, io.SeekStart) }() - - // If the current offset isn't in sync with this reader, move it. - if off != currentOffset { - if _, err = rs.Seek(off, io.SeekStart); err != nil { - return 0, fileError(f, f.closed, UnwrapOSError(err)) - } - } - - n, err = rs.Read(buf) - if errno = UnwrapOSError(err); errno != 0 { - // Defer validation overhead until we've already had an error. - errno = fileError(f, f.closed, errno) - } - } else { - errno = syscall.ENOSYS // unsupported - } - return -} - -// Seek implements File.Seek. -func (f *fsFile) Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno) { - // If this is a directory, and we're attempting to seek to position zero, - // we have to re-open the file to ensure the directory state is reset. - var isDir bool - if offset == 0 && whence == io.SeekStart { - if isDir, errno = f.IsDir(); errno != 0 { - return - } else if isDir { - return 0, f.reopen() - } - } - - if s, ok := f.file.(io.Seeker); ok { - if newOffset, errno = seek(s, offset, whence); errno != 0 { - // Defer validation overhead until we've already had an error. - errno = fileError(f, f.closed, errno) - } - } else { - errno = syscall.ENOSYS // unsupported - } - return -} - -func (f *fsFile) reopen() syscall.Errno { - _ = f.close() - var err error - f.file, err = f.fs.Open(f.name) - return UnwrapOSError(err) -} - -// Readdir implements File.Readdir. Notably, this uses fs.ReadDirFile if -// available. -func (f *fsFile) Readdir(n int) (dirents []Dirent, errno syscall.Errno) { - if of, ok := f.file.(*os.File); ok { - // We can't use f.name here because it is the path up to the fs.FS, not - // necessarily the real path. For this reason, Windows may not be able - // to populate inodes. However, Darwin and Linux will. - if dirents, errno = readdir(of, "", n); errno != 0 { - errno = adjustReaddirErr(f, f.closed, errno) - } - return - } - - // Try with fs.ReadDirFile which is available on fs.FS implementations - // like embed:fs. - if rdf, ok := f.file.(fs.ReadDirFile); ok { - entries, e := rdf.ReadDir(n) - if errno = adjustReaddirErr(f, f.closed, e); errno != 0 { - return - } - dirents = make([]Dirent, 0, len(entries)) - for _, e := range entries { - // By default, we don't attempt to read inode data - dirents = append(dirents, Dirent{Name: e.Name(), Type: e.Type()}) - } - } else { - errno = syscall.ENOTDIR - } - return -} - -// Write implements File.Write -func (f *fsFile) Write(buf []byte) (n int, errno syscall.Errno) { - if w, ok := f.file.(io.Writer); ok { - if n, errno = write(w, buf); errno != 0 { - // Defer validation overhead until we've already had an error. - errno = fileError(f, f.closed, errno) - } - } else { - errno = syscall.ENOSYS // unsupported - } - return -} - -// Pwrite implements File.Pwrite -func (f *fsFile) Pwrite(buf []byte, off int64) (n int, errno syscall.Errno) { - if wa, ok := f.file.(io.WriterAt); ok { - if n, errno = pwrite(wa, buf, off); errno != 0 { - // Defer validation overhead until we've already had an error. - errno = fileError(f, f.closed, errno) - } - } else { - errno = syscall.ENOSYS // unsupported - } - return -} - -// Close implements File.Close -func (f *fsFile) Close() syscall.Errno { - if f.closed { - return 0 - } - f.closed = true - return f.close() -} - -func (f *fsFile) close() syscall.Errno { - return UnwrapOSError(f.file.Close()) -} - -// dirError is used for commands that work against a directory, but not a file. -func dirError(f File, isClosed bool, errno syscall.Errno) syscall.Errno { - if vErrno := validate(f, isClosed, false, true); vErrno != 0 { - return vErrno - } - return errno -} - -// fileError is used for commands that work against a file, but not a directory. -func fileError(f File, isClosed bool, errno syscall.Errno) syscall.Errno { - if vErrno := validate(f, isClosed, true, false); vErrno != 0 { - return vErrno - } - return errno -} - -// validate is used to making syscalls which will fail. -func validate(f File, isClosed, wantFile, wantDir bool) syscall.Errno { - if isClosed { - return syscall.EBADF - } - - isDir, errno := f.IsDir() - if errno != 0 { - return errno - } - - if wantFile && isDir { - return syscall.EISDIR - } else if wantDir && !isDir { - return syscall.ENOTDIR - } - return 0 -} - -func read(r io.Reader, buf []byte) (n int, errno syscall.Errno) { - if len(buf) == 0 { - return 0, 0 // less overhead on zero-length reads. - } - - n, err := r.Read(buf) - return n, UnwrapOSError(err) -} - -func pread(ra io.ReaderAt, buf []byte, off int64) (n int, errno syscall.Errno) { - if len(buf) == 0 { - return 0, 0 // less overhead on zero-length reads. - } - - n, err := ra.ReadAt(buf, off) - return n, UnwrapOSError(err) -} - -func seek(s io.Seeker, offset int64, whence int) (int64, syscall.Errno) { - if uint(whence) > io.SeekEnd { - return 0, syscall.EINVAL // negative or exceeds the largest valid whence - } - - newOffset, err := s.Seek(offset, whence) - return newOffset, UnwrapOSError(err) -} - -func readdir(f *os.File, path string, n int) (dirents []Dirent, errno syscall.Errno) { - fis, e := f.Readdir(n) - if errno = UnwrapOSError(e); errno != 0 { - return - } - - dirents = make([]Dirent, 0, len(fis)) - - // linux/darwin won't have to fan out to lstat, but windows will. - var ino uint64 - for fi := range fis { - t := fis[fi] - if ino, errno = inoFromFileInfo(path, t); errno != 0 { - return - } - dirents = append(dirents, Dirent{Name: t.Name(), Ino: ino, Type: t.Mode().Type()}) - } - return -} - -func write(w io.Writer, buf []byte) (n int, errno syscall.Errno) { - if len(buf) == 0 { - return 0, 0 // less overhead on zero-length writes. - } - - n, err := w.Write(buf) - return n, UnwrapOSError(err) -} - -func pwrite(w io.WriterAt, buf []byte, off int64) (n int, errno syscall.Errno) { - if len(buf) == 0 { - return 0, 0 // less overhead on zero-length writes. - } - - n, err := w.WriteAt(buf, off) - return n, UnwrapOSError(err) -} diff --git a/internal/fsapi/fs.go b/internal/fsapi/fs.go new file mode 100644 index 00000000..9a46bfe6 --- /dev/null +++ b/internal/fsapi/fs.go @@ -0,0 +1,365 @@ +package fsapi + +import ( + "io/fs" + "syscall" +) + +// FS is a writeable fs.FS bridge backed by syscall functions needed for ABI +// including WASI and runtime.GOOS=js. +// +// Implementations should embed UnimplementedFS for forward compatability. Any +// unsupported method or parameter should return syscall.ENO +// +// # Errors +// +// All methods that can return an error return a syscall.Errno, which is zero +// on success. +// +// Restricting to syscall.Errno matches current WebAssembly host functions, +// which are constrained to well-known error codes. For example, `GOOS=js` maps +// hard coded values and panics otherwise. More commonly, WASI maps syscall +// errors to u32 numeric values. +// +// # Notes +// +// A writable filesystem abstraction is not yet implemented as of Go 1.20. See +// https://github.com/golang/go/issues/45757 +type FS interface { + // String should return a human-readable format of the filesystem + // + // For example, if this filesystem is backed by the real directory + // "/tmp/wasm", the expected value is "/tmp/wasm". + // + // When the host filesystem isn't a real filesystem, substitute a symbolic, + // human-readable name. e.g. "virtual" + String() string + + // OpenFile opens a file. It should be closed via Close on File. + // + // # Errors + // + // A zero syscall.Errno is success. The below are expected otherwise: + // - syscall.ENOSYS: the implementation does not support this function. + // - syscall.EINVAL: `path` or `flag` is invalid. + // - syscall.EISDIR: the path was a directory, but flag included + // syscall.O_RDWR or syscall.O_WRONLY + // - syscall.ENOENT: `path` doesn't exist and `flag` doesn't contain + // os.O_CREATE. + // + // # Constraints on the returned file + // + // Implementations that can read flags should enforce them regardless of + // the type returned. For example, while os.File implements io.Writer, + // attempts to write to a directory or a file opened with os.O_RDONLY fail + // with a syscall.EBADF. + // + // Some implementations choose whether to enforce read-only opens, namely + // fs.FS. While fs.FS is supported (Adapt), wazero cannot runtime enforce + // open flags. Instead, we encourage good behavior and test our built-in + // implementations. + // + // # Notes + // + // - This is like os.OpenFile, except the path is relative to this file + // system, and syscall.Errno is returned instead of os.PathError. + // - flag are the same as os.OpenFile, for example, os.O_CREATE. + // - Implications of permissions when os.O_CREATE are described in Chmod + // notes. + // - This is like `open` in POSIX. See + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html + OpenFile(path string, flag int, perm fs.FileMode) (File, syscall.Errno) + // ^^ TODO: Consider syscall.Open, though this implies defining and + // coercing flags and perms similar to what is done in os.OpenFile. + + // Lstat gets file status without following symbolic links. + // + // # Errors + // + // A zero syscall.Errno is success. The below are expected otherwise: + // - syscall.ENOSYS: the implementation does not support this function. + // - syscall.ENOENT: `path` doesn't exist. + // + // # Notes + // + // - This is like syscall.Lstat, except the `path` is relative to this + // file system. + // - This is like `lstat` in POSIX. See + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/lstat.html + // - An fs.FileInfo backed implementation sets atim, mtim and ctim to the + // same value. + // - When the path is a symbolic link, the stat returned is for the link, + // not the file it refers to. + Lstat(path string) (Stat_t, syscall.Errno) + + // Stat gets file status. + // + // # Errors + // + // A zero syscall.Errno is success. The below are expected otherwise: + // - syscall.ENOSYS: the implementation does not support this function. + // - syscall.ENOENT: `path` doesn't exist. + // + // # Notes + // + // - This is like syscall.Stat, except the `path` is relative to this + // file system. + // - This is like `stat` in POSIX. See + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/stat.html + // - An fs.FileInfo backed implementation sets atim, mtim and ctim to the + // same value. + // - When the path is a symbolic link, the stat returned is for the file + // it refers to. + Stat(path string) (Stat_t, syscall.Errno) + + // Mkdir makes a directory. + // + // # Errors + // + // A zero syscall.Errno is success. The below are expected otherwise: + // - syscall.ENOSYS: the implementation does not support this function. + // - syscall.EINVAL: `path` is invalid. + // - syscall.EEXIST: `path` exists and is a directory. + // - syscall.ENOTDIR: `path` exists and is a file. + // + // # Notes + // + // - This is like syscall.Mkdir, except the `path` is relative to this + // file system. + // - This is like `mkdir` in POSIX. See + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/mkdir.html + // - Implications of permissions are described in Chmod notes. + Mkdir(path string, perm fs.FileMode) syscall.Errno + // ^^ TODO: Consider syscall.Mkdir, though this implies defining and + // coercing flags and perms similar to what is done in os.Mkdir. + + // Chmod changes the mode of the file. + // + // # Errors + // + // A zero syscall.Errno is success. The below are expected otherwise: + // - syscall.ENOSYS: the implementation does not support this function. + // - syscall.EINVAL: `path` is invalid. + // - syscall.ENOENT: `path` does not exist. + // + // # Notes + // + // - This is like syscall.Chmod, except the `path` is relative to this + // file system. + // - This is like `chmod` in POSIX. See + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/chmod.html + // - Windows ignores the execute bit, and any permissions come back as + // group and world. For example, chmod of 0400 reads back as 0444, and + // 0700 0666. Also, permissions on directories aren't supported at all. + Chmod(path string, perm fs.FileMode) syscall.Errno + + // Chown changes the owner and group of a file. + // + // # Errors + // + // A zero syscall.Errno is success. The below are expected otherwise: + // - syscall.ENOSYS: the implementation does not support this function. + // - syscall.EINVAL: `path` is invalid. + // - syscall.ENOENT: `path` does not exist. + // + // # Notes + // + // - This is like syscall.Chown, except the `path` is relative to this + // file system. + // - This is like `chown` in POSIX. See + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/chown.html + // - This always returns syscall.ENOSYS on windows. + Chown(path string, uid, gid int) syscall.Errno + + // Lchown changes the owner and group of a symbolic link. + // + // # Errors + // + // A zero syscall.Errno is success. The below are expected otherwise: + // - syscall.ENOSYS: the implementation does not support this function. + // - syscall.EINVAL: `path` is invalid. + // - syscall.ENOENT: `path` does not exist. + // + // # Notes + // + // - This is like syscall.Lchown, except the `path` is relative to this + // file system. + // - This is like `lchown` in POSIX. See + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/lchown.html + // - Windows will always return syscall.ENOSYS + Lchown(path string, uid, gid int) syscall.Errno + + // Rename renames file or directory. + // + // # Errors + // + // A zero syscall.Errno is success. The below are expected otherwise: + // - syscall.ENOSYS: the implementation does not support this function. + // - syscall.EINVAL: `from` or `to` is invalid. + // - syscall.ENOENT: `from` or `to` don't exist. + // - syscall.ENOTDIR: `from` is a directory and `to` exists as a file. + // - syscall.EISDIR: `from` is a file and `to` exists as a directory. + // - syscall.ENOTEMPTY: `both from` and `to` are existing directory, but + // `to` is not empty. + // + // # Notes + // + // - This is like syscall.Rename, except the paths are relative to this + // file system. + // - This is like `rename` in POSIX. See + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/rename.html + // - Windows doesn't let you overwrite an existing directory. + Rename(from, to string) syscall.Errno + + // Rmdir removes a directory. + // + // # Errors + // + // A zero syscall.Errno is success. The below are expected otherwise: + // - syscall.ENOSYS: the implementation does not support this function. + // - syscall.EINVAL: `path` is invalid. + // - syscall.ENOENT: `path` doesn't exist. + // - syscall.ENOTDIR: `path` exists, but isn't a directory. + // - syscall.ENOTEMPTY: `path` exists, but isn't empty. + // + // # Notes + // + // - This is like syscall.Rmdir, except the `path` is relative to this + // file system. + // - This is like `rmdir` in POSIX. See + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/rmdir.html + // - As of Go 1.19, Windows maps syscall.ENOTDIR to syscall.ENOENT. + Rmdir(path string) syscall.Errno + + // Unlink removes a directory entry. + // + // # Errors + // + // A zero syscall.Errno is success. The below are expected otherwise: + // - syscall.ENOSYS: the implementation does not support this function. + // - syscall.EINVAL: `path` is invalid. + // - syscall.ENOENT: `path` doesn't exist. + // - syscall.EISDIR: `path` exists, but is a directory. + // + // # Notes + // + // - This is like syscall.Unlink, except the `path` is relative to this + // file system. + // - This is like `unlink` in POSIX. See + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/unlink.html + // - On Windows, syscall.Unlink doesn't delete symlink to directory unlike other platforms. Implementations might + // want to combine syscall.RemoveDirectory with syscall.Unlink in order to delete such links on Windows. + // See https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-removedirectorya + Unlink(path string) syscall.Errno + + // Link creates a "hard" link from oldPath to newPath, in contrast to a + // soft link (via Symlink). + // + // # Errors + // + // A zero syscall.Errno is success. The below are expected otherwise: + // - syscall.ENOSYS: the implementation does not support this function. + // - syscall.EPERM: `oldPath` is invalid. + // - syscall.ENOENT: `oldPath` doesn't exist. + // - syscall.EISDIR: `newPath` exists, but is a directory. + // + // # Notes + // + // - This is like syscall.Link, except the `oldPath` is relative to this + // file system. + // - This is like `link` in POSIX. See + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/link.html + Link(oldPath, newPath string) syscall.Errno + + // Symlink creates a "soft" link from oldPath to newPath, in contrast to a + // hard link (via Link). + // + // # Errors + // + // A zero syscall.Errno is success. The below are expected otherwise: + // - syscall.ENOSYS: the implementation does not support this function. + // - syscall.EPERM: `oldPath` or `newPath` is invalid. + // - syscall.EEXIST: `newPath` exists. + // + // # Notes + // + // - This is like syscall.Symlink, except the `oldPath` is relative to + // this file system. + // - This is like `symlink` in POSIX. See + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/symlink.html + // - Only `newPath` is relative to this file system and `oldPath` is kept + // as-is. That is because the link is only resolved relative to the + // directory when dereferencing it (e.g. ReadLink). + // See https://github.com/bytecodealliance/cap-std/blob/v1.0.4/cap-std/src/fs/dir.rs#L404-L409 + // for how others implement this. + // - Symlinks in Windows requires `SeCreateSymbolicLinkPrivilege`. + // Otherwise, syscall.EPERM results. + // See https://learn.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/create-symbolic-links + Symlink(oldPath, linkName string) syscall.Errno + + // Readlink reads the contents of a symbolic link. + // + // # Errors + // + // A zero syscall.Errno is success. The below are expected otherwise: + // - syscall.ENOSYS: the implementation does not support this function. + // - syscall.EINVAL: `path` is invalid. + // + // # Notes + // + // - This is like syscall.Readlink, except the path is relative to this + // filesystem. + // - This is like `readlink` in POSIX. See + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/readlink.html + // - On Windows, the path separator is different from other platforms, + // but to provide consistent results to Wasm, this normalizes to a "/" + // separator. + Readlink(path string) (string, syscall.Errno) + + // Truncate truncates a file to a specified length. + // + // # Errors + // + // A zero syscall.Errno is success. The below are expected otherwise: + // - syscall.ENOSYS: the implementation does not support this function. + // - syscall.EINVAL: `path` is invalid or size is negative. + // - syscall.ENOENT: `path` doesn't exist. + // - syscall.EISDIR: `path` is a directory. + // - syscall.EACCES: `path` doesn't have write access. + // + // # Notes + // + // - This is like syscall.Truncate, except the path is relative to this + // filesystem. + // - This is like `truncate` in POSIX. See + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/truncate.html + Truncate(path string, size int64) syscall.Errno + + // Utimens set file access and modification times on a path relative to + // this file system, at nanosecond precision. + // + // # Parameters + // + // The `times` parameter includes the access and modification timestamps to + // assign. Special syscall.Timespec NSec values platform.UTIME_NOW and + // platform.UTIME_OMIT may be specified instead of real timestamps. A nil + // `times` parameter behaves the same as if both were set to + // platform.UTIME_NOW. + // + // When the `symlinkFollow` parameter is true and the path is a symbolic link, + // the target of expanding that link is updated. + // + // # Errors + // + // A zero syscall.Errno is success. The below are expected otherwise: + // - syscall.ENOSYS: the implementation does not support this function. + // - syscall.EINVAL: `path` is invalid. + // - syscall.EEXIST: `path` exists and is a directory. + // - syscall.ENOTDIR: `path` exists and is a file. + // + // # Notes + // + // - This is like syscall.UtimesNano and `utimensat` with `AT_FDCWD` in + // POSIX. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html + Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno +} diff --git a/internal/platform/stat.go b/internal/fsapi/stat.go similarity index 57% rename from internal/platform/stat.go rename to internal/fsapi/stat.go index 038f77a8..3901a6af 100644 --- a/internal/platform/stat.go +++ b/internal/fsapi/stat.go @@ -1,10 +1,6 @@ -package platform +package fsapi -import ( - "io/fs" - "os" - "syscall" -) +import "io/fs" // Stat_t is similar to syscall.Stat_t, and fields frequently used by // WebAssembly ABI including WASI snapshot-01, GOOS=js and wasi-filesystem. @@ -50,43 +46,3 @@ type Stat_t struct { // Ctim is the last file status change timestamp in epoch nanoseconds. Ctim int64 } - -// Lstat is like syscall.Lstat. This returns syscall.ENOENT if the path doesn't -// exist. -// -// # Notes -// -// The primary difference between this and Stat is, when the path is a -// symbolic link, the stat is about the link, not its target, such as directory -// listings. -func Lstat(path string) (Stat_t, syscall.Errno) { - return lstat(path) // extracted to override more expensively in windows -} - -// Stat is like syscall.Stat. This returns syscall.ENOENT if the path doesn't -// exist. -func Stat(path string) (Stat_t, syscall.Errno) { - return stat(path) // extracted to override more expensively in windows -} - -func defaultStatFile(f *os.File) (Stat_t, syscall.Errno) { - if t, err := f.Stat(); err != nil { - return Stat_t{}, UnwrapOSError(err) - } else { - return statFromFileInfo(t), 0 - } -} - -func statFromDefaultFileInfo(t fs.FileInfo) Stat_t { - st := Stat_t{} - st.Ino = 0 - st.Dev = 0 - st.Mode = t.Mode() - st.Nlink = 1 - st.Size = t.Size() - mtim := t.ModTime().UnixNano() // Set all times to the mod time - st.Atim = mtim - st.Mtim = mtim - st.Ctim = mtim - return st -} diff --git a/internal/fsapi/unimplemented.go b/internal/fsapi/unimplemented.go new file mode 100644 index 00000000..7a57468b --- /dev/null +++ b/internal/fsapi/unimplemented.go @@ -0,0 +1,205 @@ +package fsapi + +import ( + "io/fs" + "syscall" + "time" +) + +// UnimplementedFS is an FS that returns syscall.ENOSYS for all functions, +// This should be embedded to have forward compatible implementations. +type UnimplementedFS struct{} + +// String implements fmt.Stringer +func (UnimplementedFS) String() string { + return "Unimplemented:/" +} + +// Open implements the same method as documented on fs.FS +func (UnimplementedFS) Open(name string) (fs.File, error) { + return nil, &fs.PathError{Op: "open", Path: name, Err: syscall.ENOSYS} +} + +// OpenFile implements FS.OpenFile +func (UnimplementedFS) OpenFile(path string, flag int, perm fs.FileMode) (File, syscall.Errno) { + return nil, syscall.ENOSYS +} + +// Lstat implements FS.Lstat +func (UnimplementedFS) Lstat(path string) (Stat_t, syscall.Errno) { + return Stat_t{}, syscall.ENOSYS +} + +// Stat implements FS.Stat +func (UnimplementedFS) Stat(path string) (Stat_t, syscall.Errno) { + return Stat_t{}, syscall.ENOSYS +} + +// Readlink implements FS.Readlink +func (UnimplementedFS) Readlink(path string) (string, syscall.Errno) { + return "", syscall.ENOSYS +} + +// Mkdir implements FS.Mkdir +func (UnimplementedFS) Mkdir(path string, perm fs.FileMode) syscall.Errno { + return syscall.ENOSYS +} + +// Chmod implements FS.Chmod +func (UnimplementedFS) Chmod(path string, perm fs.FileMode) syscall.Errno { + return syscall.ENOSYS +} + +// Chown implements FS.Chown +func (UnimplementedFS) Chown(path string, uid, gid int) syscall.Errno { + return syscall.ENOSYS +} + +// Lchown implements FS.Lchown +func (UnimplementedFS) Lchown(path string, uid, gid int) syscall.Errno { + return syscall.ENOSYS +} + +// Rename implements FS.Rename +func (UnimplementedFS) Rename(from, to string) syscall.Errno { + return syscall.ENOSYS +} + +// Rmdir implements FS.Rmdir +func (UnimplementedFS) Rmdir(path string) syscall.Errno { + return syscall.ENOSYS +} + +// Link implements FS.Link +func (UnimplementedFS) Link(_, _ string) syscall.Errno { + return syscall.ENOSYS +} + +// Symlink implements FS.Symlink +func (UnimplementedFS) Symlink(_, _ string) syscall.Errno { + return syscall.ENOSYS +} + +// Unlink implements FS.Unlink +func (UnimplementedFS) Unlink(path string) syscall.Errno { + return syscall.ENOSYS +} + +// Utimens implements FS.Utimens +func (UnimplementedFS) Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno { + return syscall.ENOSYS +} + +// Truncate implements FS.Truncate +func (UnimplementedFS) Truncate(string, int64) syscall.Errno { + return syscall.ENOSYS +} + +// UnimplementedFile is a File that returns syscall.ENOSYS for all functions, +// except where no-op are otherwise documented. +// +// This should be embedded to have forward compatible implementations. +type UnimplementedFile struct{} + +// Ino implements File.Ino +func (UnimplementedFile) Ino() (uint64, syscall.Errno) { + return 0, 0 +} + +// IsAppend implements File.IsAppend +func (UnimplementedFile) IsAppend() bool { + return false +} + +// SetAppend implements File.SetAppend +func (UnimplementedFile) SetAppend(bool) syscall.Errno { + return syscall.ENOSYS +} + +// IsNonblock implements File.IsNonblock +func (UnimplementedFile) IsNonblock() bool { + return false +} + +// SetNonblock implements File.SetNonblock +func (UnimplementedFile) SetNonblock(bool) syscall.Errno { + return syscall.ENOSYS +} + +// Stat implements File.Stat +func (UnimplementedFile) Stat() (Stat_t, syscall.Errno) { + return Stat_t{}, syscall.ENOSYS +} + +// IsDir implements File.IsDir +func (UnimplementedFile) IsDir() (bool, syscall.Errno) { + return false, syscall.ENOSYS +} + +// Read implements File.Read +func (UnimplementedFile) Read([]byte) (int, syscall.Errno) { + return 0, syscall.ENOSYS +} + +// Pread implements File.Pread +func (UnimplementedFile) Pread([]byte, int64) (int, syscall.Errno) { + return 0, syscall.ENOSYS +} + +// Seek implements File.Seek +func (UnimplementedFile) Seek(int64, int) (int64, syscall.Errno) { + return 0, syscall.ENOSYS +} + +// Readdir implements File.Readdir +func (UnimplementedFile) Readdir(int) (dirents []Dirent, errno syscall.Errno) { + return nil, syscall.ENOSYS +} + +// PollRead implements File.PollRead +func (UnimplementedFile) PollRead(*time.Duration) (ready bool, errno syscall.Errno) { + return false, syscall.ENOSYS +} + +// Write implements File.Write +func (UnimplementedFile) Write([]byte) (int, syscall.Errno) { + return 0, syscall.ENOSYS +} + +// Pwrite implements File.Pwrite +func (UnimplementedFile) Pwrite([]byte, int64) (int, syscall.Errno) { + return 0, syscall.ENOSYS +} + +// Truncate implements File.Truncate +func (UnimplementedFile) Truncate(int64) syscall.Errno { + return syscall.ENOSYS +} + +// Sync implements File.Sync +func (UnimplementedFile) Sync() syscall.Errno { + return 0 // not syscall.ENOSYS +} + +// Datasync implements File.Datasync +func (UnimplementedFile) Datasync() syscall.Errno { + return 0 // not syscall.ENOSYS +} + +// Chmod implements File.Chmod +func (UnimplementedFile) Chmod(fs.FileMode) syscall.Errno { + return syscall.ENOSYS +} + +// Chown implements File.Chown +func (UnimplementedFile) Chown(int, int) syscall.Errno { + return syscall.ENOSYS +} + +// Utimens implements File.Utimens +func (UnimplementedFile) Utimens(*[2]syscall.Timespec) syscall.Errno { + return syscall.ENOSYS +} + +// Close implements File.Close +func (UnimplementedFile) Close() (errno syscall.Errno) { return } diff --git a/internal/fstest/fstest.go b/internal/fstest/fstest.go index a9f81eca..5a337ab7 100644 --- a/internal/fstest/fstest.go +++ b/internal/fstest/fstest.go @@ -26,8 +26,6 @@ import ( "runtime" "testing/fstest" "time" - - "github.com/tetratelabs/wazero/internal/platform" ) var files = []struct { @@ -100,16 +98,16 @@ func WriteTestFiles(tmpDir string) (err error) { } // os.Stat uses GetFileInformationByHandle internally. - st, errno := platform.Stat(path) - if errno != 0 { - return errno - } - if st.Mtim == info.ModTime().UnixNano() { + st, err := os.Stat(path) + if err != nil { + return err + } else if st.ModTime() == info.ModTime() { return nil // synced! } // Otherwise, we need to sync the timestamps. - return os.Chtimes(path, time.Unix(0, st.Atim), time.Unix(0, st.Mtim)) + atimeNsec, mtimeNsec := timesFromFileInfo(st) + return os.Chtimes(path, time.Unix(0, atimeNsec), time.Unix(0, mtimeNsec)) }) } return diff --git a/internal/fstest/times_notwindows.go b/internal/fstest/times_notwindows.go new file mode 100644 index 00000000..712cf2cc --- /dev/null +++ b/internal/fstest/times_notwindows.go @@ -0,0 +1,9 @@ +//go:build !windows + +package fstest + +import "io/fs" + +func timesFromFileInfo(t fs.FileInfo) (atim, mtime int64) { + panic("unexpected") +} diff --git a/internal/fstest/times_windows.go b/internal/fstest/times_windows.go new file mode 100644 index 00000000..78ffe370 --- /dev/null +++ b/internal/fstest/times_windows.go @@ -0,0 +1,14 @@ +package fstest + +import ( + "io/fs" + "syscall" +) + +func timesFromFileInfo(t fs.FileInfo) (atim, mtime int64) { + if d, ok := t.Sys().(*syscall.Win32FileAttributeData); ok { + return d.LastAccessTime.Nanoseconds(), d.LastWriteTime.Nanoseconds() + } else { + panic("unexpected") + } +} diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go index cc7c66a4..3ecd5d0c 100644 --- a/internal/gojs/fs.go +++ b/internal/gojs/fs.go @@ -7,10 +7,10 @@ import ( "syscall" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/gojs/custom" "github.com/tetratelabs/wazero/internal/gojs/goos" "github.com/tetratelabs/wazero/internal/gojs/util" - "github.com/tetratelabs/wazero/internal/platform" internalsys "github.com/tetratelabs/wazero/internal/sys" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -184,7 +184,7 @@ func syscallFstat(fsc *internalsys.FSContext, fd int32) (*jsSt, error) { } } -func newJsSt(st platform.Stat_t) *jsSt { +func newJsSt(st fsapi.Stat_t) *jsSt { ret := &jsSt{} ret.isDir = st.Mode.IsDir() ret.dev = st.Dev diff --git a/internal/platform/chown_unix.go b/internal/platform/chown_unix.go deleted file mode 100644 index 5e0753da..00000000 --- a/internal/platform/chown_unix.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !windows - -package platform - -import "syscall" - -func fchown(fd uintptr, uid, gid int) syscall.Errno { - return UnwrapOSError(syscall.Fchown(int(fd), uid, gid)) -} diff --git a/internal/platform/datasync_linux.go b/internal/platform/datasync_linux.go deleted file mode 100644 index 515664e3..00000000 --- a/internal/platform/datasync_linux.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build linux - -package platform - -import ( - "os" - "syscall" -) - -func datasync(f *os.File) syscall.Errno { - return UnwrapOSError(syscall.Fdatasync(int(f.Fd()))) -} diff --git a/internal/platform/rename.go b/internal/platform/rename.go deleted file mode 100644 index 2b537fae..00000000 --- a/internal/platform/rename.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !windows - -package platform - -import "syscall" - -func Rename(from, to string) syscall.Errno { - if from == to { - return 0 - } - return UnwrapOSError(syscall.Rename(from, to)) -} diff --git a/internal/platform/select_unsupported.go b/internal/platform/select_unsupported.go deleted file mode 100644 index 672ca400..00000000 --- a/internal/platform/select_unsupported.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !darwin && !linux && !windows - -package platform - -import ( - "syscall" - "time" -) - -func syscall_select(n int, r, w, e *FdSet, timeout *time.Duration) (int, error) { - return -1, syscall.ENOSYS -} diff --git a/internal/platform/stat_unsupported.go b/internal/platform/stat_unsupported.go deleted file mode 100644 index f3bb313d..00000000 --- a/internal/platform/stat_unsupported.go +++ /dev/null @@ -1,39 +0,0 @@ -//go:build (!((amd64 || arm64 || riscv64) && linux) && !((amd64 || arm64) && (darwin || freebsd)) && !((amd64 || arm64) && windows)) || js - -package platform - -import ( - "io/fs" - "os" - "syscall" -) - -func lstat(path string) (Stat_t, syscall.Errno) { - t, err := os.Lstat(path) - if errno := UnwrapOSError(err); errno == 0 { - return statFromFileInfo(t), 0 - } else { - return Stat_t{}, errno - } -} - -func stat(path string) (Stat_t, syscall.Errno) { - t, err := os.Stat(path) - if errno := UnwrapOSError(err); errno == 0 { - return statFromFileInfo(t), 0 - } else { - return Stat_t{}, errno - } -} - -func statFile(f *os.File) (Stat_t, syscall.Errno) { - return defaultStatFile(f) -} - -func inoFromFileInfo(_ string, t fs.FileInfo) (ino uint64, err syscall.Errno) { - return -} - -func statFromFileInfo(t fs.FileInfo) Stat_t { - return statFromDefaultFileInfo(t) -} diff --git a/internal/platform/sync.go b/internal/platform/sync.go deleted file mode 100644 index b9c080b9..00000000 --- a/internal/platform/sync.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !windows - -package platform - -import ( - "os" - "syscall" -) - -func sync(f *os.File) syscall.Errno { - return UnwrapOSError(f.Sync()) -} diff --git a/internal/platform/unlink.go b/internal/platform/unlink.go deleted file mode 100644 index 9555da51..00000000 --- a/internal/platform/unlink.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build !windows - -package platform - -import "syscall" - -func Unlink(name string) (errno syscall.Errno) { - err := syscall.Unlink(name) - if errno = UnwrapOSError(err); errno == syscall.EPERM { - errno = syscall.EISDIR - } - return errno -} diff --git a/internal/sys/fs.go b/internal/sys/fs.go index a09687c2..f151f90f 100644 --- a/internal/sys/fs.go +++ b/internal/sys/fs.go @@ -3,11 +3,10 @@ package sys import ( "io" "io/fs" - "os" "syscall" - "time" "github.com/tetratelabs/wazero/internal/descriptor" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/sysfs" ) @@ -32,214 +31,6 @@ const ( const modeDevice = fs.ModeDevice | 0o640 -// StdinFile is a fs.ModeDevice file for use implementing FdStdin. -// This is safer than reading from os.DevNull as it can never overrun -// operating system file descriptors. -type StdinFile struct { - noopStdinFile - io.Reader -} - -// Read implements the same method as documented on platform.File -func (f *StdinFile) Read(buf []byte) (int, syscall.Errno) { - n, err := f.Reader.Read(buf) - return n, platform.UnwrapOSError(err) -} - -type writerFile struct { - noopStdoutFile - - w io.Writer -} - -// Write implements the same method as documented on platform.File -func (f *writerFile) Write(buf []byte) (int, syscall.Errno) { - n, err := f.w.Write(buf) - return n, platform.UnwrapOSError(err) -} - -// noopStdinFile is a fs.ModeDevice file for use implementing FdStdin. This is -// safer than reading from os.DevNull as it can never overrun operating system -// file descriptors. -type noopStdinFile struct { - noopStdioFile -} - -// AccessMode implements the same method as documented on platform.File -func (noopStdinFile) AccessMode() int { - return syscall.O_RDONLY -} - -// Read implements the same method as documented on platform.File -func (noopStdinFile) Read([]byte) (int, syscall.Errno) { - return 0, 0 // Always EOF -} - -// PollRead implements the same method as documented on platform.File -func (noopStdinFile) PollRead(*time.Duration) (ready bool, errno syscall.Errno) { - return true, 0 // always ready to read nothing -} - -// noopStdoutFile is a fs.ModeDevice file for use implementing FdStdout and -// FdStderr. -type noopStdoutFile struct { - noopStdioFile -} - -// AccessMode implements the same method as documented on platform.File -func (noopStdoutFile) AccessMode() int { - return syscall.O_WRONLY -} - -// Write implements the same method as documented on platform.File -func (noopStdoutFile) Write(buf []byte) (int, syscall.Errno) { - return len(buf), 0 // same as io.Discard -} - -type noopStdioFile struct { - platform.UnimplementedFile -} - -// Stat implements the same method as documented on platform.File -func (noopStdioFile) Stat() (platform.Stat_t, syscall.Errno) { - return platform.Stat_t{Mode: modeDevice, Nlink: 1}, 0 -} - -// IsDir implements the same method as documented on platform.File -func (noopStdioFile) IsDir() (bool, syscall.Errno) { - return false, 0 -} - -// Close implements the same method as documented on platform.File -func (noopStdioFile) Close() (errno syscall.Errno) { return } - -// compile-time check to ensure lazyDir implements platform.File. -var _ platform.File = (*lazyDir)(nil) - -type lazyDir struct { - platform.DirFile - - fs sysfs.FS - f platform.File -} - -// Ino implements the same method as documented on platform.File -func (r *lazyDir) Ino() (uint64, syscall.Errno) { - if f, ok := r.file(); !ok { - return 0, syscall.EBADF - } else { - return f.Ino() - } -} - -// IsAppend implements the same method as documented on platform.File -func (r *lazyDir) IsAppend() bool { - return false -} - -// SetAppend implements the same method as documented on platform.File -func (r *lazyDir) SetAppend(bool) syscall.Errno { - return syscall.EISDIR -} - -// Seek implements the same method as documented on platform.File -func (r *lazyDir) Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno) { - if f, ok := r.file(); !ok { - return 0, syscall.EBADF - } else { - return f.Seek(offset, whence) - } -} - -// Stat implements the same method as documented on platform.File -func (r *lazyDir) Stat() (platform.Stat_t, syscall.Errno) { - if f, ok := r.file(); !ok { - return platform.Stat_t{}, syscall.EBADF - } else { - return f.Stat() - } -} - -// Readdir implements the same method as documented on platform.File -func (r *lazyDir) Readdir(n int) (dirents []platform.Dirent, errno syscall.Errno) { - if f, ok := r.file(); !ok { - return nil, syscall.EBADF - } else { - return f.Readdir(n) - } -} - -// Sync implements the same method as documented on platform.File -func (r *lazyDir) Sync() syscall.Errno { - if f, ok := r.file(); !ok { - return syscall.EBADF - } else { - return f.Sync() - } -} - -// Datasync implements the same method as documented on platform.File -func (r *lazyDir) Datasync() syscall.Errno { - if f, ok := r.file(); !ok { - return syscall.EBADF - } else { - return f.Datasync() - } -} - -// Chmod implements the same method as documented on platform.File -func (r *lazyDir) Chmod(mode fs.FileMode) syscall.Errno { - if f, ok := r.file(); !ok { - return syscall.EBADF - } else { - return f.Chmod(mode) - } -} - -// Chown implements the same method as documented on platform.File -func (r *lazyDir) Chown(uid, gid int) syscall.Errno { - if f, ok := r.file(); !ok { - return syscall.EBADF - } else { - return f.Chown(uid, gid) - } -} - -// Utimens implements the same method as documented on platform.File -func (r *lazyDir) Utimens(times *[2]syscall.Timespec) syscall.Errno { - if f, ok := r.file(); !ok { - return syscall.EBADF - } else { - return f.Utimens(times) - } -} - -// file returns the underlying file or false if it doesn't exist. -func (r *lazyDir) file() (platform.File, bool) { - if f := r.f; r.f != nil { - return f, true - } - var errno syscall.Errno - r.f, errno = r.fs.OpenFile(".", os.O_RDONLY, 0) - switch errno { - case 0: - return r.f, true - case syscall.ENOENT: - return nil, false - default: - panic(errno) // unexpected - } -} - -// Close implements fs.File -func (r *lazyDir) Close() syscall.Errno { - f := r.f - if f == nil { - return 0 // never opened - } - return f.Close() -} - // FileEntry maps a path to an open file in a file system. type FileEntry struct { // Name is the name of the directory up to its pre-open, or the pre-open @@ -256,10 +47,10 @@ type FileEntry struct { IsPreopen bool // FS is the filesystem associated with the pre-open. - FS sysfs.FS + FS fsapi.FS // File is always non-nil. - File platform.File + File fsapi.File // ReadDir is present when this File is a fs.ReadDirFile and `ReadDir` // was called. @@ -279,12 +70,12 @@ type ReadDir struct { // In wasi preview1, dot and dot-dot entries are required to exist, but the // reverse is true for preview2. More importantly, preview2 holds separate // stateful dir-entry-streams per file. - Dirents []platform.Dirent + Dirents []fsapi.Dirent } type FSContext struct { // rootFS is the root ("/") mount. - rootFS sysfs.FS + rootFS fsapi.FS // openedFiles is a map of file descriptor numbers (>=FdPreopen) to open files // (or directories) and defaults to empty. @@ -296,92 +87,15 @@ type FSContext struct { // descriptors to file entries. type FileTable = descriptor.Table[int32, *FileEntry] -// NewFSContext creates a FSContext with stdio streams and an optional -// pre-opened filesystem. -// -// If `preopened` is not sysfs.UnimplementedFS, it is inserted into -// the file descriptor table as FdPreopen. -func (c *Context) NewFSContext(stdin io.Reader, stdout, stderr io.Writer, rootFS sysfs.FS) (err error) { - c.fsc.rootFS = rootFS - inFile, err := stdinFile(stdin) - if err != nil { - return err - } - c.fsc.openedFiles.Insert(inFile) - outWriter, err := stdioWriterFile("stdout", stdout) - if err != nil { - return err - } - c.fsc.openedFiles.Insert(outWriter) - errWriter, err := stdioWriterFile("stderr", stderr) - if err != nil { - return err - } - c.fsc.openedFiles.Insert(errWriter) - - if _, ok := rootFS.(sysfs.UnimplementedFS); ok { - return nil - } - - if comp, ok := rootFS.(*sysfs.CompositeFS); ok { - preopens := comp.FS() - for i, p := range comp.GuestPaths() { - c.fsc.openedFiles.Insert(&FileEntry{ - FS: preopens[i], - Name: p, - IsPreopen: true, - File: &lazyDir{fs: rootFS}, - }) - } - } else { - c.fsc.openedFiles.Insert(&FileEntry{ - FS: rootFS, - Name: "/", - IsPreopen: true, - File: &lazyDir{fs: rootFS}, - }) - } - - return nil -} - -func stdinFile(r io.Reader) (*FileEntry, error) { - if r == nil { - return &FileEntry{Name: "stdin", IsPreopen: true, File: &noopStdinFile{}}, nil - } else if f, ok := r.(*os.File); ok { - if f, err := platform.NewStdioFile(true, f); err != nil { - return nil, err - } else { - return &FileEntry{Name: "stdin", IsPreopen: true, File: f}, nil - } - } else { - return &FileEntry{Name: "stdin", IsPreopen: true, File: &StdinFile{Reader: r}}, nil - } -} - -func stdioWriterFile(name string, w io.Writer) (*FileEntry, error) { - if w == nil { - return &FileEntry{Name: name, IsPreopen: true, File: &noopStdoutFile{}}, nil - } else if f, ok := w.(*os.File); ok { - if f, err := platform.NewStdioFile(false, f); err != nil { - return nil, err - } else { - return &FileEntry{Name: name, IsPreopen: true, File: f}, nil - } - } else { - return &FileEntry{Name: name, IsPreopen: true, File: &writerFile{w: w}}, nil - } -} - // RootFS returns the underlying filesystem. Any files that should be added to // the table should be inserted via InsertFile. -func (c *FSContext) RootFS() sysfs.FS { +func (c *FSContext) RootFS() fsapi.FS { return c.rootFS } // OpenFile opens the file into the table and returns its file descriptor. // The result must be closed by CloseFile or Close. -func (c *FSContext) OpenFile(fs sysfs.FS, path string, flag int, perm fs.FileMode) (int32, syscall.Errno) { +func (c *FSContext) OpenFile(fs fsapi.FS, path string, flag int, perm fs.FileMode) (int32, syscall.Errno) { if f, errno := fs.OpenFile(path, flag, perm); errno != 0 { return 0, errno } else { @@ -456,3 +170,52 @@ func (c *FSContext) Close() (err error) { c.openedFiles = FileTable{} return } + +// NewFSContext creates a FSContext with stdio streams and an optional +// pre-opened filesystem. +// +// If `preopened` is not UnimplementedFS, it is inserted into +// the file descriptor table as FdPreopen. +func (c *Context) NewFSContext(stdin io.Reader, stdout, stderr io.Writer, rootFS fsapi.FS) (err error) { + c.fsc.rootFS = rootFS + inFile, err := stdinFile(stdin) + if err != nil { + return err + } + c.fsc.openedFiles.Insert(inFile) + outWriter, err := stdioWriterFile("stdout", stdout) + if err != nil { + return err + } + c.fsc.openedFiles.Insert(outWriter) + errWriter, err := stdioWriterFile("stderr", stderr) + if err != nil { + return err + } + c.fsc.openedFiles.Insert(errWriter) + + if _, ok := rootFS.(fsapi.UnimplementedFS); ok { + return nil + } + + if comp, ok := rootFS.(*sysfs.CompositeFS); ok { + preopens := comp.FS() + for i, p := range comp.GuestPaths() { + c.fsc.openedFiles.Insert(&FileEntry{ + FS: preopens[i], + Name: p, + IsPreopen: true, + File: &lazyDir{fs: rootFS}, + }) + } + } else { + c.fsc.openedFiles.Insert(&FileEntry{ + FS: rootFS, + Name: "/", + IsPreopen: true, + File: &lazyDir{fs: rootFS}, + }) + } + + return nil +} diff --git a/internal/sys/fs_test.go b/internal/sys/fs_test.go index 3f3accd6..99586d07 100644 --- a/internal/sys/fs_test.go +++ b/internal/sys/fs_test.go @@ -9,6 +9,7 @@ import ( "testing" "testing/fstest" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/sysfs" testfs "github.com/tetratelabs/wazero/internal/testing/fs" "github.com/tetratelabs/wazero/internal/testing/require" @@ -26,20 +27,20 @@ func TestNewFSContext(t *testing.T) { // Test various usual configuration for the file system. tests := []struct { name string - fs sysfs.FS + fs fsapi.FS }{ { name: "embed.FS", fs: sysfs.Adapt(embedFS), }, { - name: "sysfs.NewDirFS", + name: "NewDirFS", // Don't use "testdata" because it may not be present in // cross-architecture (a.k.a. scratch) build containers. fs: dirfs, }, { - name: "sysfs.NewReadFS", + name: "NewReadFS", fs: sysfs.NewReadFS(dirfs), }, { @@ -123,12 +124,12 @@ func TestFSContext_CloseFile(t *testing.T) { func TestUnimplementedFSContext(t *testing.T) { c := Context{} - err := c.NewFSContext(nil, nil, nil, sysfs.UnimplementedFS{}) + err := c.NewFSContext(nil, nil, nil, fsapi.UnimplementedFS{}) require.NoError(t, err) testFS := &c.fsc require.NoError(t, err) - expected := &FSContext{rootFS: sysfs.UnimplementedFS{}} + expected := &FSContext{rootFS: fsapi.UnimplementedFS{}} noopStdin, _ := stdinFile(nil) expected.openedFiles.Insert(noopStdin) noopStdout, _ := stdioWriterFile("stdout", nil) @@ -141,7 +142,7 @@ func TestUnimplementedFSContext(t *testing.T) { require.NoError(t, err) // Closes opened files - require.Equal(t, &FSContext{rootFS: sysfs.UnimplementedFS{}}, testFS) + require.Equal(t, &FSContext{rootFS: fsapi.UnimplementedFS{}}, testFS) }) } @@ -152,7 +153,7 @@ func TestCompositeFSContext(t *testing.T) { tmpDir2 := t.TempDir() testFS2 := sysfs.NewDirFS(tmpDir2) - rootFS, err := sysfs.NewRootFS([]sysfs.FS{testFS2, testFS1}, []string{"/tmp", "/"}) + rootFS, err := sysfs.NewRootFS([]fsapi.FS{testFS2, testFS1}, []string{"/tmp", "/"}) require.NoError(t, err) c := Context{} @@ -273,132 +274,3 @@ func TestFSContext_Renumber(t *testing.T) { require.Equal(t, syscall.ENOTSUP, fsc.Renumber(3, 3)) }) } - -func TestStdio(t *testing.T) { - // simulate regular file attached to stdin - f, err := os.CreateTemp(t.TempDir(), "somefile") - require.NoError(t, err) - defer f.Close() - - stdin, err := stdinFile(os.Stdin) - require.NoError(t, err) - stdinStat, err := os.Stdin.Stat() - require.NoError(t, err) - - stdinNil, err := stdinFile(nil) - require.NoError(t, err) - - stdinFile, err := stdinFile(f) - require.NoError(t, err) - - stdout, err := stdioWriterFile("stdout", os.Stdout) - require.NoError(t, err) - stdoutStat, err := os.Stdout.Stat() - require.NoError(t, err) - - stdoutNil, err := stdioWriterFile("stdout", nil) - require.NoError(t, err) - - stdoutFile, err := stdioWriterFile("stdout", f) - require.NoError(t, err) - - stderr, err := stdioWriterFile("stderr", os.Stderr) - require.NoError(t, err) - stderrStat, err := os.Stderr.Stat() - require.NoError(t, err) - - stderrNil, err := stdioWriterFile("stderr", nil) - require.NoError(t, err) - - stderrFile, err := stdioWriterFile("stderr", f) - require.NoError(t, err) - - tests := []struct { - name string - f *FileEntry - // Depending on how the tests run, os.Stdin won't necessarily be a char - // device. We compare against an os.File, to account for this. - expectedType fs.FileMode - }{ - { - name: "stdin", - f: stdin, - expectedType: stdinStat.Mode().Type(), - }, - { - name: "stdin noop", - f: stdinNil, - expectedType: fs.ModeDevice, - }, - { - name: "stdin file", - f: stdinFile, - expectedType: 0, // normal file - }, - { - name: "stdout", - f: stdout, - expectedType: stdoutStat.Mode().Type(), - }, - { - name: "stdout noop", - f: stdoutNil, - expectedType: fs.ModeDevice, - }, - { - name: "stdout file", - f: stdoutFile, - expectedType: 0, // normal file - }, - { - name: "stderr", - f: stderr, - expectedType: stderrStat.Mode().Type(), - }, - { - name: "stderr noop", - f: stderrNil, - expectedType: fs.ModeDevice, - }, - { - name: "stderr file", - f: stderrFile, - expectedType: 0, // normal file - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name+" Stat", func(t *testing.T) { - st, errno := tc.f.File.Stat() - require.EqualErrno(t, 0, errno) - require.Equal(t, tc.expectedType, st.Mode&fs.ModeType) - require.Equal(t, uint64(1), st.Nlink) - - // Fake times are needed to pass wasi-testsuite. - // See https://github.com/WebAssembly/wasi-testsuite/blob/af57727/tests/rust/src/bin/fd_filestat_get.rs#L1-L19 - require.Zero(t, st.Ctim) - require.Zero(t, st.Mtim) - require.Zero(t, st.Atim) - }) - - buf := make([]byte, 5) - switch tc.f { - case stdinNil: - t.Run(tc.name+" returns zero on Read", func(t *testing.T) { - n, errno := tc.f.File.Read(buf) - require.EqualErrno(t, 0, errno) - require.Zero(t, n) // like reading io.EOF - }) - case stdoutNil, stderrNil: - // This is important because some code will loop forever attempting - // to write data. This happened in TestShortHash. - t.Run(tc.name+" returns length on Write", func(t *testing.T) { - n, errno := tc.f.File.Write(buf) - require.EqualErrno(t, 0, errno) - require.Equal(t, len(buf), n) // like io.Discard - }) - } - } -} diff --git a/internal/sys/lazy.go b/internal/sys/lazy.go new file mode 100644 index 00000000..df68b51b --- /dev/null +++ b/internal/sys/lazy.go @@ -0,0 +1,136 @@ +package sys + +import ( + "io/fs" + "os" + "syscall" + + "github.com/tetratelabs/wazero/internal/fsapi" +) + +// compile-time check to ensure lazyDir implements internalapi.File. +var _ fsapi.File = (*lazyDir)(nil) + +type lazyDir struct { + fsapi.DirFile + + fs fsapi.FS + f fsapi.File +} + +// Ino implements the same method as documented on internalapi.File +func (r *lazyDir) Ino() (uint64, syscall.Errno) { + if f, ok := r.file(); !ok { + return 0, syscall.EBADF + } else { + return f.Ino() + } +} + +// IsAppend implements the same method as documented on internalapi.File +func (r *lazyDir) IsAppend() bool { + return false +} + +// SetAppend implements the same method as documented on internalapi.File +func (r *lazyDir) SetAppend(bool) syscall.Errno { + return syscall.EISDIR +} + +// Seek implements the same method as documented on internalapi.File +func (r *lazyDir) Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno) { + if f, ok := r.file(); !ok { + return 0, syscall.EBADF + } else { + return f.Seek(offset, whence) + } +} + +// Stat implements the same method as documented on internalapi.File +func (r *lazyDir) Stat() (fsapi.Stat_t, syscall.Errno) { + if f, ok := r.file(); !ok { + return fsapi.Stat_t{}, syscall.EBADF + } else { + return f.Stat() + } +} + +// Readdir implements the same method as documented on internalapi.File +func (r *lazyDir) Readdir(n int) (dirents []fsapi.Dirent, errno syscall.Errno) { + if f, ok := r.file(); !ok { + return nil, syscall.EBADF + } else { + return f.Readdir(n) + } +} + +// Sync implements the same method as documented on internalapi.File +func (r *lazyDir) Sync() syscall.Errno { + if f, ok := r.file(); !ok { + return syscall.EBADF + } else { + return f.Sync() + } +} + +// Datasync implements the same method as documented on internalapi.File +func (r *lazyDir) Datasync() syscall.Errno { + if f, ok := r.file(); !ok { + return syscall.EBADF + } else { + return f.Datasync() + } +} + +// Chmod implements the same method as documented on internalapi.File +func (r *lazyDir) Chmod(mode fs.FileMode) syscall.Errno { + if f, ok := r.file(); !ok { + return syscall.EBADF + } else { + return f.Chmod(mode) + } +} + +// Chown implements the same method as documented on internalapi.File +func (r *lazyDir) Chown(uid, gid int) syscall.Errno { + if f, ok := r.file(); !ok { + return syscall.EBADF + } else { + return f.Chown(uid, gid) + } +} + +// Utimens implements the same method as documented on internalapi.File +func (r *lazyDir) Utimens(times *[2]syscall.Timespec) syscall.Errno { + if f, ok := r.file(); !ok { + return syscall.EBADF + } else { + return f.Utimens(times) + } +} + +// file returns the underlying file or false if it doesn't exist. +func (r *lazyDir) file() (fsapi.File, bool) { + if f := r.f; r.f != nil { + return f, true + } + var errno syscall.Errno + r.f, errno = r.fs.OpenFile(".", os.O_RDONLY, 0) + switch errno { + case 0: + return r.f, true + case syscall.ENOENT: + return nil, false + default: + panic(errno) // unexpected + } +} + +// Close implements fs.File +func (r *lazyDir) Close() syscall.Errno { + f := r.f + if f == nil { + return 0 // never opened + } + return f.Close() +} diff --git a/internal/sys/stdio.go b/internal/sys/stdio.go new file mode 100644 index 00000000..69bc8a49 --- /dev/null +++ b/internal/sys/stdio.go @@ -0,0 +1,121 @@ +package sys + +import ( + "io" + "os" + "syscall" + "time" + + "github.com/tetratelabs/wazero/internal/fsapi" + "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/sysfs" +) + +// StdinFile is a fs.ModeDevice file for use implementing FdStdin. +// This is safer than reading from os.DevNull as it can never overrun +// operating system file descriptors. +type StdinFile struct { + noopStdinFile + io.Reader +} + +// Read implements the same method as documented on internalapi.File +func (f *StdinFile) Read(buf []byte) (int, syscall.Errno) { + n, err := f.Reader.Read(buf) + return n, platform.UnwrapOSError(err) +} + +type writerFile struct { + noopStdoutFile + + w io.Writer +} + +// Write implements the same method as documented on internalapi.File +func (f *writerFile) Write(buf []byte) (int, syscall.Errno) { + n, err := f.w.Write(buf) + return n, platform.UnwrapOSError(err) +} + +// noopStdinFile is a fs.ModeDevice file for use implementing FdStdin. This is +// safer than reading from os.DevNull as it can never overrun operating system +// file descriptors. +type noopStdinFile struct { + noopStdioFile +} + +// AccessMode implements the same method as documented on internalapi.File +func (noopStdinFile) AccessMode() int { + return syscall.O_RDONLY +} + +// Read implements the same method as documented on internalapi.File +func (noopStdinFile) Read([]byte) (int, syscall.Errno) { + return 0, 0 // Always EOF +} + +// PollRead implements the same method as documented on internalapi.File +func (noopStdinFile) PollRead(*time.Duration) (ready bool, errno syscall.Errno) { + return true, 0 // always ready to read nothing +} + +// noopStdoutFile is a fs.ModeDevice file for use implementing FdStdout and +// FdStderr. +type noopStdoutFile struct { + noopStdioFile +} + +// AccessMode implements the same method as documented on internalapi.File +func (noopStdoutFile) AccessMode() int { + return syscall.O_WRONLY +} + +// Write implements the same method as documented on internalapi.File +func (noopStdoutFile) Write(buf []byte) (int, syscall.Errno) { + return len(buf), 0 // same as io.Discard +} + +type noopStdioFile struct { + fsapi.UnimplementedFile +} + +// Stat implements the same method as documented on internalapi.File +func (noopStdioFile) Stat() (fsapi.Stat_t, syscall.Errno) { + return fsapi.Stat_t{Mode: modeDevice, Nlink: 1}, 0 +} + +// IsDir implements the same method as documented on internalapi.File +func (noopStdioFile) IsDir() (bool, syscall.Errno) { + return false, 0 +} + +// Close implements the same method as documented on internalapi.File +func (noopStdioFile) Close() (errno syscall.Errno) { return } + +func stdinFile(r io.Reader) (*FileEntry, error) { + if r == nil { + return &FileEntry{Name: "stdin", IsPreopen: true, File: &noopStdinFile{}}, nil + } else if f, ok := r.(*os.File); ok { + if f, err := sysfs.NewStdioFile(true, f); err != nil { + return nil, err + } else { + return &FileEntry{Name: "stdin", IsPreopen: true, File: f}, nil + } + } else { + return &FileEntry{Name: "stdin", IsPreopen: true, File: &StdinFile{Reader: r}}, nil + } +} + +func stdioWriterFile(name string, w io.Writer) (*FileEntry, error) { + if w == nil { + return &FileEntry{Name: name, IsPreopen: true, File: &noopStdoutFile{}}, nil + } else if f, ok := w.(*os.File); ok { + if f, err := sysfs.NewStdioFile(false, f); err != nil { + return nil, err + } else { + return &FileEntry{Name: name, IsPreopen: true, File: f}, nil + } + } else { + return &FileEntry{Name: name, IsPreopen: true, File: &writerFile{w: w}}, nil + } +} diff --git a/internal/sys/stdio_test.go b/internal/sys/stdio_test.go new file mode 100644 index 00000000..eedc7833 --- /dev/null +++ b/internal/sys/stdio_test.go @@ -0,0 +1,138 @@ +package sys + +import ( + "io/fs" + "os" + "testing" + + "github.com/tetratelabs/wazero/internal/testing/require" +) + +func TestStdio(t *testing.T) { + // simulate regular file attached to stdin + f, err := os.CreateTemp(t.TempDir(), "somefile") + require.NoError(t, err) + defer f.Close() + + stdin, err := stdinFile(os.Stdin) + require.NoError(t, err) + stdinStat, err := os.Stdin.Stat() + require.NoError(t, err) + + stdinNil, err := stdinFile(nil) + require.NoError(t, err) + + stdinFile, err := stdinFile(f) + require.NoError(t, err) + + stdout, err := stdioWriterFile("stdout", os.Stdout) + require.NoError(t, err) + stdoutStat, err := os.Stdout.Stat() + require.NoError(t, err) + + stdoutNil, err := stdioWriterFile("stdout", nil) + require.NoError(t, err) + + stdoutFile, err := stdioWriterFile("stdout", f) + require.NoError(t, err) + + stderr, err := stdioWriterFile("stderr", os.Stderr) + require.NoError(t, err) + stderrStat, err := os.Stderr.Stat() + require.NoError(t, err) + + stderrNil, err := stdioWriterFile("stderr", nil) + require.NoError(t, err) + + stderrFile, err := stdioWriterFile("stderr", f) + require.NoError(t, err) + + tests := []struct { + name string + f *FileEntry + // Depending on how the tests run, os.Stdin won't necessarily be a char + // device. We compare against an os.File, to account for this. + expectedType fs.FileMode + }{ + { + name: "stdin", + f: stdin, + expectedType: stdinStat.Mode().Type(), + }, + { + name: "stdin noop", + f: stdinNil, + expectedType: fs.ModeDevice, + }, + { + name: "stdin file", + f: stdinFile, + expectedType: 0, // normal file + }, + { + name: "stdout", + f: stdout, + expectedType: stdoutStat.Mode().Type(), + }, + { + name: "stdout noop", + f: stdoutNil, + expectedType: fs.ModeDevice, + }, + { + name: "stdout file", + f: stdoutFile, + expectedType: 0, // normal file + }, + { + name: "stderr", + f: stderr, + expectedType: stderrStat.Mode().Type(), + }, + { + name: "stderr noop", + f: stderrNil, + expectedType: fs.ModeDevice, + }, + { + name: "stderr file", + f: stderrFile, + expectedType: 0, // normal file + }, + } + + for _, tt := range tests { + tc := tt + + t.Run(tc.name+" Stat", func(t *testing.T) { + st, errno := tc.f.File.Stat() + require.EqualErrno(t, 0, errno) + require.Equal(t, tc.expectedType, st.Mode&fs.ModeType) + require.Equal(t, uint64(1), st.Nlink) + + // Fake times are needed to pass wasi-testsuite. + // See https://github.com/WebAssembly/wasi-testsuite/blob/af57727/tests/rust/src/bin/fd_filestat_get.rs#L1-L19 + require.Zero(t, st.Ctim) + require.Zero(t, st.Mtim) + require.Zero(t, st.Atim) + }) + + buf := make([]byte, 5) + switch tc.f { + case stdinNil: + t.Run(tc.name+" returns zero on Read", func(t *testing.T) { + n, errno := tc.f.File.Read(buf) + require.EqualErrno(t, 0, errno) + require.Zero(t, n) // like reading io.EOF + }) + case stdoutNil, stderrNil: + // This is important because some code will loop forever attempting + // to write data. This happened in TestShortHash. + t.Run(tc.name+" returns length on Write", func(t *testing.T) { + n, errno := tc.f.File.Write(buf) + require.EqualErrno(t, 0, errno) + require.Equal(t, len(buf), n) // like io.Discard + }) + } + } +} diff --git a/internal/sys/sys.go b/internal/sys/sys.go index cdf05802..aaa9dabf 100644 --- a/internal/sys/sys.go +++ b/internal/sys/sys.go @@ -6,8 +6,8 @@ import ( "io" "time" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/platform" - "github.com/tetratelabs/wazero/internal/sysfs" "github.com/tetratelabs/wazero/sys" ) @@ -97,7 +97,7 @@ func (c *Context) Osyield() { c.osyield() } -// FS returns the possibly empty (sysfs.UnimplementedFS) file system context. +// FS returns the possibly empty (UnimplementedFS) file system context. func (c *Context) FS() *FSContext { return &c.fsc } @@ -111,7 +111,7 @@ func (c *Context) RandSource() io.Reader { // DefaultContext returns Context with no values set except a possible nil fs.FS // // This is only used for testing. -func DefaultContext(fs sysfs.FS) *Context { +func DefaultContext(fs fsapi.FS) *Context { if sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil, nil, fs); err != nil { panic(fmt.Errorf("BUG: DefaultContext should never error: %w", err)) } else { @@ -133,7 +133,7 @@ func NewContext( nanotimeResolution sys.ClockResolution, nanosleep sys.Nanosleep, osyield sys.Osyield, - rootFS sysfs.FS, + rootFS fsapi.FS, ) (sysCtx *Context, err error) { sysCtx = &Context{args: args, environ: environ} @@ -188,7 +188,7 @@ func NewContext( if rootFS != nil { err = sysCtx.NewFSContext(stdin, stdout, stderr, rootFS) } else { - err = sysCtx.NewFSContext(stdin, stdout, stderr, sysfs.UnimplementedFS{}) + err = sysCtx.NewFSContext(stdin, stdout, stderr, fsapi.UnimplementedFS{}) } return diff --git a/internal/sysfs/adapter.go b/internal/sysfs/adapter.go index 87189aba..022ca697 100644 --- a/internal/sysfs/adapter.go +++ b/internal/sysfs/adapter.go @@ -6,28 +6,28 @@ import ( "path" "syscall" - "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/fsapi" ) -// Adapt adapts the input to FS unless it is already one. Use NewDirFS instead +// Adapt adapts the input to api.FS unless it is already one. Use NewDirFS instead // of os.DirFS as it handles interop issues such as windows support. // -// Note: This performs no flag verification on FS.OpenFile. fs.FS cannot read -// flags as there is no parameter to pass them through with. Moreover, fs.FS +// Note: This performs no flag verification on OpenFile. fsapi.FS cannot read +// flags as there is no parameter to pass them through with. Moreover, fsapi.FS // documentation does not require the file to be present. In summary, we can't // enforce flag behavior. -func Adapt(fs fs.FS) FS { +func Adapt(fs fs.FS) fsapi.FS { if fs == nil { - return UnimplementedFS{} + return fsapi.UnimplementedFS{} } - if sys, ok := fs.(FS); ok { + if sys, ok := fs.(fsapi.FS); ok { return sys } return &adapter{fs: fs} } type adapter struct { - UnimplementedFS + fsapi.UnimplementedFS fs fs.FS } @@ -36,26 +36,26 @@ func (a *adapter) String() string { return fmt.Sprintf("%v", a.fs) } -// OpenFile implements FS.OpenFile -func (a *adapter) OpenFile(path string, flag int, perm fs.FileMode) (platform.File, syscall.Errno) { - return platform.OpenFSFile(a.fs, cleanPath(path), flag, perm) +// OpenFile implements the same method as documented on api.FS +func (a *adapter) OpenFile(path string, flag int, perm fs.FileMode) (fsapi.File, syscall.Errno) { + return OpenFSFile(a.fs, cleanPath(path), flag, perm) } -// Stat implements FS.Stat -func (a *adapter) Stat(path string) (platform.Stat_t, syscall.Errno) { +// Stat implements the same method as documented on api.FS +func (a *adapter) Stat(path string) (fsapi.Stat_t, syscall.Errno) { f, errno := a.OpenFile(path, syscall.O_RDONLY, 0) if errno != 0 { - return platform.Stat_t{}, errno + return fsapi.Stat_t{}, errno } defer f.Close() return f.Stat() } -// Lstat implements FS.Lstat -func (a *adapter) Lstat(path string) (platform.Stat_t, syscall.Errno) { - // At this time, we make the assumption that fs.FS instances do not support +// Lstat implements the same method as documented on api.FS +func (a *adapter) Lstat(path string) (fsapi.Stat_t, syscall.Errno) { + // At this time, we make the assumption that api.FS instances do not support // symbolic links, therefore Lstat is the same as Stat. This is obviously - // not true but until fs.FS has a solid story for how to handle symlinks we + // not true but until api.FS has a solid story for how to handle symlinks we // are better off not making a decision that would be difficult to revert // later on. // diff --git a/internal/sysfs/adapter_test.go b/internal/sysfs/adapter_test.go index 054c9b84..ed6dae7a 100644 --- a/internal/sysfs/adapter_test.go +++ b/internal/sysfs/adapter_test.go @@ -9,13 +9,14 @@ import ( "syscall" "testing" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/fstest" "github.com/tetratelabs/wazero/internal/testing/require" ) func TestAdapt_nil(t *testing.T) { testFS := Adapt(nil) - _, ok := testFS.(UnimplementedFS) + _, ok := testFS.(fsapi.UnimplementedFS) require.True(t, ok) } @@ -95,7 +96,7 @@ func TestAdapt_UtimesNano(t *testing.T) { } func TestAdapt_Open_Read(t *testing.T) { - // Create a subdirectory, so we can test reads outside the FS root. + // Create a subdirectory, so we can test reads outside the fsapi.FS root. tmpDir := t.TempDir() tmpDir = joinPath(tmpDir, t.Name()) require.NoError(t, os.Mkdir(tmpDir, 0o700)) @@ -111,7 +112,7 @@ func TestAdapt_Open_Read(t *testing.T) { t.Run("path outside root invalid", func(t *testing.T) { _, err := testFS.OpenFile("../foo", os.O_RDONLY, 0) - // fs.FS doesn't allow relative path lookups + // fsapi.FS doesn't allow relative path lookups require.EqualErrno(t, syscall.EINVAL, err) }) } @@ -139,7 +140,7 @@ func TestAdapt_Stat(t *testing.T) { testStat(t, testFS) } -// hackFS cheats the fs.FS contract by opening for write (os.O_RDWR). +// hackFS cheats the api.FS contract by opening for write (os.O_RDWR). // // Until we have an alternate public interface for filesystems, some users will // rely on this. Via testing, we ensure we don't accidentally break them. @@ -160,7 +161,7 @@ func (dir hackFS) Open(name string) (fs.File, error) { } // TestAdapt_HackedWrites ensures we allow writes even if they violate the -// fs.FS contract. +// api.FS contract. func TestAdapt_HackedWrites(t *testing.T) { tmpDir := t.TempDir() testFS := Adapt(hackFS(tmpDir)) diff --git a/internal/platform/bench_test.go b/internal/sysfs/bench_test.go similarity index 92% rename from internal/platform/bench_test.go rename to internal/sysfs/bench_test.go index a3512474..23b20fe5 100644 --- a/internal/platform/bench_test.go +++ b/internal/sysfs/bench_test.go @@ -1,4 +1,4 @@ -package platform +package sysfs import ( "io" @@ -43,8 +43,8 @@ func BenchmarkFsFileRead(b *testing.B) { }{ {name: "os.DirFS Read", fs: dirFS, pread: false}, {name: "os.DirFS Pread", fs: dirFS, pread: true}, - {name: "embed.FS Read", fs: embedFS, pread: false}, - {name: "embed.FS Pread", fs: embedFS, pread: true}, + {name: "embed.api.FS Read", fs: embedFS, pread: false}, + {name: "embed.api.FS Pread", fs: embedFS, pread: true}, } buf := make([]byte, 3) diff --git a/internal/platform/chown.go b/internal/sysfs/chown.go similarity index 85% rename from internal/platform/chown.go rename to internal/sysfs/chown.go index dfbf6bdb..93c774c9 100644 --- a/internal/platform/chown.go +++ b/internal/sysfs/chown.go @@ -1,8 +1,10 @@ -package platform +package sysfs import ( "os" "syscall" + + "github.com/tetratelabs/wazero/internal/platform" ) // Chown is like os.Chown, except it returns a syscall.Errno, not a @@ -13,7 +15,7 @@ import ( // See https://linux.die.net/man/3/chown func Chown(path string, uid, gid int) syscall.Errno { err := os.Chown(path, uid, gid) - return UnwrapOSError(err) + return platform.UnwrapOSError(err) } // Lchown is like os.Lchown, except it returns a syscall.Errno, not a @@ -24,5 +26,5 @@ func Chown(path string, uid, gid int) syscall.Errno { // See https://linux.die.net/man/3/lchown func Lchown(path string, uid, gid int) syscall.Errno { err := os.Lchown(path, uid, gid) - return UnwrapOSError(err) + return platform.UnwrapOSError(err) } diff --git a/internal/sysfs/chown_unix.go b/internal/sysfs/chown_unix.go new file mode 100644 index 00000000..5907a9d9 --- /dev/null +++ b/internal/sysfs/chown_unix.go @@ -0,0 +1,13 @@ +//go:build !windows + +package sysfs + +import ( + "syscall" + + "github.com/tetratelabs/wazero/internal/platform" +) + +func fchown(fd uintptr, uid, gid int) syscall.Errno { + return platform.UnwrapOSError(syscall.Fchown(int(fd), uid, gid)) +} diff --git a/internal/platform/chown_unix_test.go b/internal/sysfs/chown_unix_test.go similarity index 92% rename from internal/platform/chown_unix_test.go rename to internal/sysfs/chown_unix_test.go index 5eecdf79..e0fae498 100644 --- a/internal/platform/chown_unix_test.go +++ b/internal/sysfs/chown_unix_test.go @@ -1,6 +1,6 @@ //go:build !windows -package platform +package sysfs import ( "fmt" @@ -166,13 +166,3 @@ func TestLchown(t *testing.T) { require.EqualErrno(t, syscall.ENOENT, Lchown(path.Join(tmpDir, "a"), -1, gid)) }) } - -// checkUidGid uses lstat to ensure the comparison is against the file, not the -// target of a symbolic link. -func checkUidGid(t *testing.T, path string, uid, gid uint32) { - ls, err := os.Lstat(path) - require.NoError(t, err) - sys := ls.Sys().(*syscall.Stat_t) - require.Equal(t, uid, sys.Uid) - require.Equal(t, gid, sys.Gid) -} diff --git a/internal/platform/chown_unsupported.go b/internal/sysfs/chown_unsupported.go similarity index 93% rename from internal/platform/chown_unsupported.go rename to internal/sysfs/chown_unsupported.go index 38b7187d..5e7fd79b 100644 --- a/internal/platform/chown_unsupported.go +++ b/internal/sysfs/chown_unsupported.go @@ -1,6 +1,6 @@ //go:build windows -package platform +package sysfs import "syscall" diff --git a/internal/sysfs/datasync_linux.go b/internal/sysfs/datasync_linux.go new file mode 100644 index 00000000..715a952d --- /dev/null +++ b/internal/sysfs/datasync_linux.go @@ -0,0 +1,14 @@ +//go:build linux + +package sysfs + +import ( + "os" + "syscall" + + "github.com/tetratelabs/wazero/internal/platform" +) + +func datasync(f *os.File) syscall.Errno { + return platform.UnwrapOSError(syscall.Fdatasync(int(f.Fd()))) +} diff --git a/internal/platform/datasync_unsupported.go b/internal/sysfs/datasync_unsupported.go similarity index 91% rename from internal/platform/datasync_unsupported.go rename to internal/sysfs/datasync_unsupported.go index 9d3127fc..91a2acf2 100644 --- a/internal/platform/datasync_unsupported.go +++ b/internal/sysfs/datasync_unsupported.go @@ -1,6 +1,6 @@ //go:build !linux -package platform +package sysfs import ( "os" diff --git a/internal/sysfs/dir.go b/internal/sysfs/dir.go new file mode 100644 index 00000000..8462e846 --- /dev/null +++ b/internal/sysfs/dir.go @@ -0,0 +1,26 @@ +package sysfs + +import ( + "io" + "syscall" + + "github.com/tetratelabs/wazero/internal/fsapi" + "github.com/tetratelabs/wazero/internal/platform" +) + +func adjustReaddirErr(f fsapi.File, isClosed bool, err error) syscall.Errno { + if err == io.EOF { + return 0 // e.g. Readdir on darwin returns io.EOF, but linux doesn't. + } else if errno := platform.UnwrapOSError(err); errno != 0 { + errno = dirError(f, isClosed, errno) + // Ignore errors when the file was closed or removed. + switch errno { + case syscall.EIO, syscall.EBADF: // closed while open + return 0 + case syscall.ENOENT: // Linux error when removed while open + return 0 + } + return errno + } + return 0 +} diff --git a/internal/platform/dir_test.go b/internal/sysfs/dir_test.go similarity index 85% rename from internal/platform/dir_test.go rename to internal/sysfs/dir_test.go index 561c74ca..937afac3 100644 --- a/internal/platform/dir_test.go +++ b/internal/sysfs/dir_test.go @@ -1,4 +1,4 @@ -package platform_test +package sysfs_test import ( "io" @@ -10,8 +10,9 @@ import ( "syscall" "testing" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/fstest" - "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/sysfs" "github.com/tetratelabs/wazero/internal/testing/require" ) @@ -35,7 +36,7 @@ func TestReaddir(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { - dotF, errno := platform.OpenFSFile(tc.fs, ".", syscall.O_RDONLY, 0) + dotF, errno := sysfs.OpenFSFile(tc.fs, ".", syscall.O_RDONLY, 0) require.EqualErrno(t, 0, errno) defer dotF.Close() @@ -63,7 +64,7 @@ func TestReaddir(t *testing.T) { require.EqualErrno(t, 0, errno) }) - fileF, errno := platform.OpenFSFile(tc.fs, "empty.txt", syscall.O_RDONLY, 0) + fileF, errno := sysfs.OpenFSFile(tc.fs, "empty.txt", syscall.O_RDONLY, 0) require.EqualErrno(t, 0, errno) defer fileF.Close() @@ -72,7 +73,7 @@ func TestReaddir(t *testing.T) { require.EqualErrno(t, syscall.ENOTDIR, errno) }) - dirF, errno := platform.OpenFSFile(tc.fs, "dir", syscall.O_RDONLY, 0) + dirF, errno := sysfs.OpenFSFile(tc.fs, "dir", syscall.O_RDONLY, 0) require.EqualErrno(t, 0, errno) defer dirF.Close() @@ -90,7 +91,7 @@ func TestReaddir(t *testing.T) { require.EqualErrno(t, 0, errno) require.Equal(t, 1, len(dirents3)) - dirents := []platform.Dirent{dirents1[0], dirents2[0], dirents3[0]} + dirents := []fsapi.Dirent{dirents1[0], dirents2[0], dirents3[0]} sort.Slice(dirents, func(i, j int) bool { return dirents[i].Name < dirents[j].Name }) requireIno(t, dirents, tc.expectIno) @@ -100,7 +101,7 @@ func TestReaddir(t *testing.T) { dirents[i].Ino = 0 } - require.Equal(t, []platform.Dirent{ + require.Equal(t, []fsapi.Dirent{ {Name: "-", Type: 0}, {Name: "a-", Type: fs.ModeDir}, {Name: "ab-", Type: 0}, @@ -111,7 +112,7 @@ func TestReaddir(t *testing.T) { require.EqualErrno(t, 0, errno) }) - subdirF, errno := platform.OpenFSFile(tc.fs, "sub", syscall.O_RDONLY, 0) + subdirF, errno := sysfs.OpenFSFile(tc.fs, "sub", syscall.O_RDONLY, 0) require.EqualErrno(t, 0, errno) defer subdirF.Close() @@ -129,7 +130,7 @@ func TestReaddir(t *testing.T) { // Don't err if something else removed the directory while reading. t.Run("removed while open", func(t *testing.T) { - dirF, errno := platform.OpenFSFile(dirFS, "dir", syscall.O_RDONLY, 0) + dirF, errno := sysfs.OpenFSFile(dirFS, "dir", syscall.O_RDONLY, 0) require.EqualErrno(t, 0, errno) defer dirF.Close() @@ -152,7 +153,7 @@ func TestReaddir(t *testing.T) { }) } -func testReaddirAll(t *testing.T, dotF platform.File, expectIno bool) { +func testReaddirAll(t *testing.T, dotF fsapi.File, expectIno bool) { dirents, errno := dotF.Readdir(-1) require.EqualErrno(t, 0, errno) // no io.EOF when -1 is used sort.Slice(dirents, func(i, j int) bool { return dirents[i].Name < dirents[j].Name }) @@ -164,7 +165,7 @@ func testReaddirAll(t *testing.T, dotF platform.File, expectIno bool) { dirents[i].Ino = 0 } - require.Equal(t, []platform.Dirent{ + require.Equal(t, []fsapi.Dirent{ {Name: "animals.txt", Type: 0}, {Name: "dir", Type: fs.ModeDir}, {Name: "empty.txt", Type: 0}, @@ -173,7 +174,7 @@ func testReaddirAll(t *testing.T, dotF platform.File, expectIno bool) { }, dirents) } -func requireIno(t *testing.T, dirents []platform.Dirent, expectIno bool) { +func requireIno(t *testing.T, dirents []fsapi.Dirent, expectIno bool) { for _, e := range dirents { if expectIno { require.NotEqual(t, uint64(0), e.Ino, "%+v", e) diff --git a/internal/sysfs/dirfs.go b/internal/sysfs/dirfs.go index ab2203ef..5f2645de 100644 --- a/internal/sysfs/dirfs.go +++ b/internal/sysfs/dirfs.go @@ -5,10 +5,11 @@ import ( "os" "syscall" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/platform" ) -func NewDirFS(dir string) FS { +func NewDirFS(dir string) fsapi.FS { return &dirFS{ dir: dir, cleanedDir: ensureTrailingPathSeparator(dir), @@ -23,7 +24,7 @@ func ensureTrailingPathSeparator(dir string) string { } type dirFS struct { - UnimplementedFS + fsapi.UnimplementedFS dir string // cleanedDir is for easier OS-specific concatenation, as it always has // a trailing path separator. @@ -35,22 +36,22 @@ func (d *dirFS) String() string { return d.dir } -// OpenFile implements FS.OpenFile -func (d *dirFS) OpenFile(path string, flag int, perm fs.FileMode) (platform.File, syscall.Errno) { - return platform.OpenOSFile(d.join(path), flag, perm) +// OpenFile implements the same method as documented on api.FS +func (d *dirFS) OpenFile(path string, flag int, perm fs.FileMode) (fsapi.File, syscall.Errno) { + return OpenOSFile(d.join(path), flag, perm) } -// Lstat implements FS.Lstat -func (d *dirFS) Lstat(path string) (platform.Stat_t, syscall.Errno) { - return platform.Lstat(d.join(path)) +// Lstat implements the same method as documented on api.FS +func (d *dirFS) Lstat(path string) (fsapi.Stat_t, syscall.Errno) { + return lstat(d.join(path)) } -// Stat implements FS.Stat -func (d *dirFS) Stat(path string) (platform.Stat_t, syscall.Errno) { - return platform.Stat(d.join(path)) +// Stat implements the same method as documented on api.FS +func (d *dirFS) Stat(path string) (fsapi.Stat_t, syscall.Errno) { + return stat(d.join(path)) } -// Mkdir implements FS.Mkdir +// Mkdir implements the same method as documented on api.FS func (d *dirFS) Mkdir(path string, perm fs.FileMode) (errno syscall.Errno) { err := os.Mkdir(d.join(path), perm) if errno = platform.UnwrapOSError(err); errno == syscall.ENOTDIR { @@ -59,29 +60,29 @@ func (d *dirFS) Mkdir(path string, perm fs.FileMode) (errno syscall.Errno) { return } -// Chmod implements FS.Chmod +// Chmod implements the same method as documented on api.FS func (d *dirFS) Chmod(path string, perm fs.FileMode) syscall.Errno { err := os.Chmod(d.join(path), perm) return platform.UnwrapOSError(err) } -// Chown implements FS.Chown +// Chown implements the same method as documented on api.FS func (d *dirFS) Chown(path string, uid, gid int) syscall.Errno { - return platform.Chown(d.join(path), uid, gid) + return Chown(d.join(path), uid, gid) } -// Lchown implements FS.Lchown +// Lchown implements the same method as documented on api.FS func (d *dirFS) Lchown(path string, uid, gid int) syscall.Errno { - return platform.Lchown(d.join(path), uid, gid) + return Lchown(d.join(path), uid, gid) } -// Rename implements FS.Rename +// Rename implements the same method as documented on api.FS func (d *dirFS) Rename(from, to string) syscall.Errno { from, to = d.join(from), d.join(to) - return platform.Rename(from, to) + return Rename(from, to) } -// Readlink implements FS.Readlink +// Readlink implements the same method as documented on api.FS func (d *dirFS) Readlink(path string) (string, syscall.Errno) { // Note: do not use syscall.Readlink as that causes race on Windows. // In any case, syscall.Readlink does almost the same logic as os.Readlink. @@ -92,24 +93,24 @@ func (d *dirFS) Readlink(path string) (string, syscall.Errno) { return platform.ToPosixPath(dst), 0 } -// Link implements FS.Link. +// Link implements the same method as documented on api.FS func (d *dirFS) Link(oldName, newName string) syscall.Errno { err := os.Link(d.join(oldName), d.join(newName)) return platform.UnwrapOSError(err) } -// Rmdir implements FS.Rmdir +// Rmdir implements the same method as documented on api.FS func (d *dirFS) Rmdir(path string) syscall.Errno { err := syscall.Rmdir(d.join(path)) return platform.UnwrapOSError(err) } -// Unlink implements FS.Unlink +// Unlink implements the same method as documented on api.FS func (d *dirFS) Unlink(path string) (err syscall.Errno) { - return platform.Unlink(d.join(path)) + return Unlink(d.join(path)) } -// Symlink implements FS.Symlink +// Symlink implements the same method as documented on api.FS func (d *dirFS) Symlink(oldName, link string) syscall.Errno { // Note: do not resolve `oldName` relative to this dirFS. The link result is always resolved // when dereference the `link` on its usage (e.g. readlink, read, etc). @@ -118,12 +119,12 @@ func (d *dirFS) Symlink(oldName, link string) syscall.Errno { return platform.UnwrapOSError(err) } -// Utimens implements FS.Utimens +// Utimens implements the same method as documented on api.FS func (d *dirFS) Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno { - return platform.Utimens(d.join(path), times, symlinkFollow) + return Utimens(d.join(path), times, symlinkFollow) } -// Truncate implements FS.Truncate +// Truncate implements the same method as documented on api.FS func (d *dirFS) Truncate(path string, size int64) syscall.Errno { // Use os.Truncate as syscall.Truncate doesn't exist on Windows. err := os.Truncate(d.join(path), size) diff --git a/internal/sysfs/dirfs_test.go b/internal/sysfs/dirfs_test.go index 633e4393..5e052ead 100644 --- a/internal/sysfs/dirfs_test.go +++ b/internal/sysfs/dirfs_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/fstest" "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/testing/require" @@ -126,7 +127,7 @@ func TestDirFS_MkDir(t *testing.T) { }) } -func testChmod(t *testing.T, testFS FS, path string) { +func testChmod(t *testing.T, testFS fsapi.FS, path string) { // Test base case, using 0o444 not 0o400 for read-back on windows. requireMode(t, testFS, path, 0o444) @@ -141,7 +142,7 @@ func testChmod(t *testing.T, testFS FS, path string) { } } -func requireMode(t *testing.T, testFS FS, path string, mode fs.FileMode) { +func requireMode(t *testing.T, testFS fsapi.FS, path string, mode fs.FileMode) { st, errno := testFS.Stat(path) require.EqualErrno(t, 0, errno) @@ -431,7 +432,7 @@ func TestDirFS_Rmdir(t *testing.T) { realPath := path.Join(tmpDir, name) require.NoError(t, os.Mkdir(realPath, 0o700)) - f, errno := testFS.OpenFile(name, platform.O_DIRECTORY, 0o700) + f, errno := testFS.OpenFile(name, fsapi.O_DIRECTORY, 0o700) require.EqualErrno(t, 0, errno) defer f.Close() @@ -531,7 +532,7 @@ func TestDirFS_Utimesns(t *testing.T) { err := testFS.Utimens("nope", nil, true) require.EqualErrno(t, syscall.ENOENT, err) err = testFS.Utimens("nope", nil, false) - if platform.SupportsSymlinkNoFollow { + if SupportsSymlinkNoFollow { require.EqualErrno(t, syscall.ENOENT, err) } else { require.EqualErrno(t, syscall.ENOSYS, err) @@ -552,35 +553,35 @@ func TestDirFS_Utimesns(t *testing.T) { { name: "a=omit,m=omit", times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: platform.UTIME_OMIT}, - {Sec: 123, Nsec: platform.UTIME_OMIT}, + {Sec: 123, Nsec: UTIME_OMIT}, + {Sec: 123, Nsec: UTIME_OMIT}, }, }, { name: "a=now,m=omit", times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: platform.UTIME_NOW}, - {Sec: 123, Nsec: platform.UTIME_OMIT}, + {Sec: 123, Nsec: UTIME_NOW}, + {Sec: 123, Nsec: UTIME_OMIT}, }, }, { name: "a=omit,m=now", times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: platform.UTIME_OMIT}, - {Sec: 123, Nsec: platform.UTIME_NOW}, + {Sec: 123, Nsec: UTIME_OMIT}, + {Sec: 123, Nsec: UTIME_NOW}, }, }, { name: "a=now,m=now", times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: platform.UTIME_NOW}, - {Sec: 123, Nsec: platform.UTIME_NOW}, + {Sec: 123, Nsec: UTIME_NOW}, + {Sec: 123, Nsec: UTIME_NOW}, }, }, { name: "a=now,m=set", times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: platform.UTIME_NOW}, + {Sec: 123, Nsec: UTIME_NOW}, {Sec: 123, Nsec: 4 * 1e3}, }, }, @@ -588,7 +589,7 @@ func TestDirFS_Utimesns(t *testing.T) { name: "a=set,m=now", times: &[2]syscall.Timespec{ {Sec: 123, Nsec: 4 * 1e3}, - {Sec: 123, Nsec: platform.UTIME_NOW}, + {Sec: 123, Nsec: UTIME_NOW}, }, }, { @@ -644,7 +645,7 @@ func TestDirFS_Utimesns(t *testing.T) { require.EqualErrno(t, 0, errno) errno = testFS.Utimens(path, tc.times, !symlinkNoFollow) - if symlinkNoFollow && !platform.SupportsSymlinkNoFollow { + if symlinkNoFollow && !SupportsSymlinkNoFollow { require.EqualErrno(t, syscall.ENOSYS, errno) return } @@ -654,9 +655,9 @@ func TestDirFS_Utimesns(t *testing.T) { require.EqualErrno(t, 0, errno) if platform.CompilerSupported() { - if tc.times != nil && tc.times[0].Nsec == platform.UTIME_OMIT { + if tc.times != nil && tc.times[0].Nsec == UTIME_OMIT { require.Equal(t, oldSt.Atim, newSt.Atim) - } else if tc.times == nil || tc.times[0].Nsec == platform.UTIME_NOW { + } else if tc.times == nil || tc.times[0].Nsec == UTIME_NOW { now := time.Now().UnixNano() require.True(t, newSt.Atim <= now, "expected atim %d <= now %d", newSt.Atim, now) } else { @@ -665,9 +666,9 @@ func TestDirFS_Utimesns(t *testing.T) { } // When compiler isn't supported, we can still check mtim. - if tc.times != nil && tc.times[1].Nsec == platform.UTIME_OMIT { + if tc.times != nil && tc.times[1].Nsec == UTIME_OMIT { require.Equal(t, oldSt.Mtim, newSt.Mtim) - } else if tc.times == nil || tc.times[1].Nsec == platform.UTIME_NOW { + } else if tc.times == nil || tc.times[1].Nsec == UTIME_NOW { now := time.Now().UnixNano() require.True(t, newSt.Mtim <= now, "expected mtim %d <= now %d", newSt.Mtim, now) } else { @@ -681,7 +682,7 @@ func TestDirFS_Utimesns(t *testing.T) { func TestDirFS_OpenFile(t *testing.T) { tmpDir := t.TempDir() - // Create a subdirectory, so we can test reads outside the FS root. + // Create a subdirectory, so we can test reads outside the fsapi.FS root. tmpDir = path.Join(tmpDir, t.Name()) require.NoError(t, os.Mkdir(tmpDir, 0o700)) require.NoError(t, fstest.WriteTestFiles(tmpDir)) @@ -695,7 +696,7 @@ func TestDirFS_OpenFile(t *testing.T) { t.Run("path outside root valid", func(t *testing.T) { _, err := testFS.OpenFile("../foo", os.O_RDONLY, 0) - // syscall.FS allows relative path lookups + // fsapi.FS allows relative path lookups require.True(t, errors.Is(err, fs.ErrNotExist)) }) } diff --git a/internal/sysfs/file.go b/internal/sysfs/file.go new file mode 100644 index 00000000..f4a7cbf8 --- /dev/null +++ b/internal/sysfs/file.go @@ -0,0 +1,434 @@ +package sysfs + +import ( + "io" + "io/fs" + "os" + "syscall" + + "github.com/tetratelabs/wazero/internal/fsapi" + "github.com/tetratelabs/wazero/internal/platform" +) + +func NewStdioFile(stdin bool, f fs.File) (fsapi.File, error) { + // Return constant stat, which has fake times, but keep the underlying + // file mode. Fake times are needed to pass wasi-testsuite. + // https://github.com/WebAssembly/wasi-testsuite/blob/af57727/tests/rust/src/bin/fd_filestat_get.rs#L1-L19 + var mode fs.FileMode + if st, err := f.Stat(); err != nil { + return nil, err + } else { + mode = st.Mode() + } + var flag int + if stdin { + flag = syscall.O_RDONLY + } else { + flag = syscall.O_WRONLY + } + var file fsapi.File + if of, ok := f.(*os.File); ok { + // This is ok because functions that need path aren't used by stdioFile + file = newOsFile("", flag, 0, of) + } else { + file = &fsFile{file: f} + } + return &stdioFile{File: file, st: fsapi.Stat_t{Mode: mode, Nlink: 1}}, nil +} + +func OpenFile(path string, flag int, perm fs.FileMode) (*os.File, syscall.Errno) { + if flag&fsapi.O_DIRECTORY != 0 && flag&(syscall.O_WRONLY|syscall.O_RDWR) != 0 { + return nil, syscall.EISDIR // invalid to open a directory writeable + } + return openFile(path, flag, perm) +} + +func OpenOSFile(path string, flag int, perm fs.FileMode) (fsapi.File, syscall.Errno) { + f, errno := OpenFile(path, flag, perm) + if errno != 0 { + return nil, errno + } + return newOsFile(path, flag, perm, f), 0 +} + +func OpenFSFile(fs fs.FS, path string, flag int, perm fs.FileMode) (fsapi.File, syscall.Errno) { + if flag&fsapi.O_DIRECTORY != 0 && flag&(syscall.O_WRONLY|syscall.O_RDWR) != 0 { + return nil, syscall.EISDIR // invalid to open a directory writeable + } + f, err := fs.Open(path) + if errno := platform.UnwrapOSError(err); errno != 0 { + return nil, errno + } + // Don't return an os.File because the path is not absolute. osFile needs + // the path to be real and certain fs.File impls are subrooted. + return &fsFile{fs: fs, name: path, file: f}, 0 +} + +type stdioFile struct { + fsapi.File + st fsapi.Stat_t +} + +// IsDir implements File.IsDir +func (f *stdioFile) IsDir() (bool, syscall.Errno) { + return false, 0 +} + +// Stat implements File.Stat +func (f *stdioFile) Stat() (fsapi.Stat_t, syscall.Errno) { + return f.st, 0 +} + +// Close implements File.Close +func (f *stdioFile) Close() syscall.Errno { + return 0 +} + +// fsFile is used for wrapped os.File, like os.Stdin or any fs.File +// implementation. Notably, this does not have access to the full file path. +// so certain operations can't be supported, such as inode lookups on Windows. +type fsFile struct { + fsapi.UnimplementedFile + + // fs is the file-system that opened the file, or nil when wrapped for + // pre-opens like stdio. + fs fs.FS + + // name is what was used in fs for Open, so it may not be the actual path. + name string + + // file is always set, possibly an os.File like os.Stdin. + file fs.File + + // closed is true when closed was called. This ensures proper syscall.EBADF + closed bool + + // cachedStat includes fields that won't change while a file is open. + cachedSt *cachedStat +} + +type cachedStat struct { + // fileType is the same as what's documented on Dirent. + fileType fs.FileMode + + // ino is the same as what's documented on Dirent. + ino uint64 +} + +// cachedStat returns the cacheable parts of platform.sys.Stat_t or an error if +// they couldn't be retrieved. +func (f *fsFile) cachedStat() (fileType fs.FileMode, ino uint64, errno syscall.Errno) { + if f.cachedSt == nil { + if _, errno = f.Stat(); errno != 0 { + return + } + } + return f.cachedSt.fileType, f.cachedSt.ino, 0 +} + +// Ino implements File.Ino +func (f *fsFile) Ino() (uint64, syscall.Errno) { + if _, ino, errno := f.cachedStat(); errno != 0 { + return 0, errno + } else { + return ino, 0 + } +} + +// IsAppend implements File.IsAppend +func (f *fsFile) IsAppend() bool { + return false +} + +// SetAppend implements File.SetAppend +func (f *fsFile) SetAppend(bool) (errno syscall.Errno) { + return fileError(f, f.closed, syscall.ENOSYS) +} + +// IsDir implements File.IsDir +func (f *fsFile) IsDir() (bool, syscall.Errno) { + if ft, _, errno := f.cachedStat(); errno != 0 { + return false, errno + } else if ft.Type() == fs.ModeDir { + return true, 0 + } + return false, 0 +} + +// Stat implements File.Stat +func (f *fsFile) Stat() (st fsapi.Stat_t, errno syscall.Errno) { + if f.closed { + errno = syscall.EBADF + return + } + + // While some functions in fsapi.File need the full path, especially in + // Windows, stat does not. Casting here allows os.DirFS to return inode + // information. + if of, ok := f.file.(*os.File); ok { + if st, errno = statFile(of); errno != 0 { + return + } + return f.cacheStat(st) + } else if t, err := f.file.Stat(); err != nil { + errno = platform.UnwrapOSError(err) + return + } else { + st = StatFromDefaultFileInfo(t) + return f.cacheStat(st) + } +} + +func (f *fsFile) cacheStat(st fsapi.Stat_t) (fsapi.Stat_t, syscall.Errno) { + f.cachedSt = &cachedStat{fileType: st.Mode & fs.ModeType, ino: st.Ino} + return st, 0 +} + +// Read implements File.Read +func (f *fsFile) Read(buf []byte) (n int, errno syscall.Errno) { + if n, errno = read(f.file, buf); errno != 0 { + // Defer validation overhead until we've already had an error. + errno = fileError(f, f.closed, errno) + } + return +} + +// Pread implements File.Pread +func (f *fsFile) Pread(buf []byte, off int64) (n int, errno syscall.Errno) { + if ra, ok := f.file.(io.ReaderAt); ok { + if n, errno = pread(ra, buf, off); errno != 0 { + // Defer validation overhead until we've already had an error. + errno = fileError(f, f.closed, errno) + } + return + } + + // See /RATIONALE.md "fd_pread: io.Seeker fallback when io.ReaderAt is not supported" + if rs, ok := f.file.(io.ReadSeeker); ok { + // Determine the current position in the file, as we need to revert it. + currentOffset, err := rs.Seek(0, io.SeekCurrent) + if err != nil { + return 0, fileError(f, f.closed, platform.UnwrapOSError(err)) + } + + // Put the read position back when complete. + defer func() { _, _ = rs.Seek(currentOffset, io.SeekStart) }() + + // If the current offset isn't in sync with this reader, move it. + if off != currentOffset { + if _, err = rs.Seek(off, io.SeekStart); err != nil { + return 0, fileError(f, f.closed, platform.UnwrapOSError(err)) + } + } + + n, err = rs.Read(buf) + if errno = platform.UnwrapOSError(err); errno != 0 { + // Defer validation overhead until we've already had an error. + errno = fileError(f, f.closed, errno) + } + } else { + errno = syscall.ENOSYS // unsupported + } + return +} + +// Seek implements File.Seek. +func (f *fsFile) Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno) { + // If this is a directory, and we're attempting to seek to position zero, + // we have to re-open the file to ensure the directory state is reset. + var isDir bool + if offset == 0 && whence == io.SeekStart { + if isDir, errno = f.IsDir(); errno != 0 { + return + } else if isDir { + return 0, f.reopen() + } + } + + if s, ok := f.file.(io.Seeker); ok { + if newOffset, errno = seek(s, offset, whence); errno != 0 { + // Defer validation overhead until we've already had an error. + errno = fileError(f, f.closed, errno) + } + } else { + errno = syscall.ENOSYS // unsupported + } + return +} + +func (f *fsFile) reopen() syscall.Errno { + _ = f.close() + var err error + f.file, err = f.fs.Open(f.name) + return platform.UnwrapOSError(err) +} + +// Readdir implements File.Readdir. Notably, this uses fs.ReadDirFile if +// available. +func (f *fsFile) Readdir(n int) (dirents []fsapi.Dirent, errno syscall.Errno) { + if of, ok := f.file.(*os.File); ok { + // We can't use f.name here because it is the path up to the fsapi.FS, + // not necessarily the real path. For this reason, Windows may not be + // able to populate inodes. However, Darwin and Linux will. + if dirents, errno = readdir(of, "", n); errno != 0 { + errno = adjustReaddirErr(f, f.closed, errno) + } + return + } + + // Try with fs.ReadDirFile which is available on api.FS implementations + // like embed:fs. + if rdf, ok := f.file.(fs.ReadDirFile); ok { + entries, e := rdf.ReadDir(n) + if errno = adjustReaddirErr(f, f.closed, e); errno != 0 { + return + } + dirents = make([]fsapi.Dirent, 0, len(entries)) + for _, e := range entries { + // By default, we don't attempt to read inode data + dirents = append(dirents, fsapi.Dirent{Name: e.Name(), Type: e.Type()}) + } + } else { + errno = syscall.ENOTDIR + } + return +} + +// Write implements File.Write +func (f *fsFile) Write(buf []byte) (n int, errno syscall.Errno) { + if w, ok := f.file.(io.Writer); ok { + if n, errno = write(w, buf); errno != 0 { + // Defer validation overhead until we've already had an error. + errno = fileError(f, f.closed, errno) + } + } else { + errno = syscall.ENOSYS // unsupported + } + return +} + +// Pwrite implements File.Pwrite +func (f *fsFile) Pwrite(buf []byte, off int64) (n int, errno syscall.Errno) { + if wa, ok := f.file.(io.WriterAt); ok { + if n, errno = pwrite(wa, buf, off); errno != 0 { + // Defer validation overhead until we've already had an error. + errno = fileError(f, f.closed, errno) + } + } else { + errno = syscall.ENOSYS // unsupported + } + return +} + +// Close implements File.Close +func (f *fsFile) Close() syscall.Errno { + if f.closed { + return 0 + } + f.closed = true + return f.close() +} + +func (f *fsFile) close() syscall.Errno { + return platform.UnwrapOSError(f.file.Close()) +} + +// dirError is used for commands that work against a directory, but not a file. +func dirError(f fsapi.File, isClosed bool, errno syscall.Errno) syscall.Errno { + if vErrno := validate(f, isClosed, false, true); vErrno != 0 { + return vErrno + } + return errno +} + +// fileError is used for commands that work against a file, but not a directory. +func fileError(f fsapi.File, isClosed bool, errno syscall.Errno) syscall.Errno { + if vErrno := validate(f, isClosed, true, false); vErrno != 0 { + return vErrno + } + return errno +} + +// validate is used to making syscalls which will fail. +func validate(f fsapi.File, isClosed, wantFile, wantDir bool) syscall.Errno { + if isClosed { + return syscall.EBADF + } + + isDir, errno := f.IsDir() + if errno != 0 { + return errno + } + + if wantFile && isDir { + return syscall.EISDIR + } else if wantDir && !isDir { + return syscall.ENOTDIR + } + return 0 +} + +func read(r io.Reader, buf []byte) (n int, errno syscall.Errno) { + if len(buf) == 0 { + return 0, 0 // less overhead on zero-length reads. + } + + n, err := r.Read(buf) + return n, platform.UnwrapOSError(err) +} + +func pread(ra io.ReaderAt, buf []byte, off int64) (n int, errno syscall.Errno) { + if len(buf) == 0 { + return 0, 0 // less overhead on zero-length reads. + } + + n, err := ra.ReadAt(buf, off) + return n, platform.UnwrapOSError(err) +} + +func seek(s io.Seeker, offset int64, whence int) (int64, syscall.Errno) { + if uint(whence) > io.SeekEnd { + return 0, syscall.EINVAL // negative or exceeds the largest valid whence + } + + newOffset, err := s.Seek(offset, whence) + return newOffset, platform.UnwrapOSError(err) +} + +func readdir(f *os.File, path string, n int) (dirents []fsapi.Dirent, errno syscall.Errno) { + fis, e := f.Readdir(n) + if errno = platform.UnwrapOSError(e); errno != 0 { + return + } + + dirents = make([]fsapi.Dirent, 0, len(fis)) + + // linux/darwin won't have to fan out to lstat, but windows will. + var ino uint64 + for fi := range fis { + t := fis[fi] + if ino, errno = inoFromFileInfo(path, t); errno != 0 { + return + } + dirents = append(dirents, fsapi.Dirent{Name: t.Name(), Ino: ino, Type: t.Mode().Type()}) + } + return +} + +func write(w io.Writer, buf []byte) (n int, errno syscall.Errno) { + if len(buf) == 0 { + return 0, 0 // less overhead on zero-length writes. + } + + n, err := w.Write(buf) + return n, platform.UnwrapOSError(err) +} + +func pwrite(w io.WriterAt, buf []byte, off int64) (n int, errno syscall.Errno) { + if len(buf) == 0 { + return 0, 0 // less overhead on zero-length writes. + } + + n, err := w.WriteAt(buf, off) + return n, platform.UnwrapOSError(err) +} diff --git a/internal/platform/file_test.go b/internal/sysfs/file_test.go similarity index 88% rename from internal/platform/file_test.go rename to internal/sysfs/file_test.go index 8bbb5fc7..36909681 100644 --- a/internal/platform/file_test.go +++ b/internal/sysfs/file_test.go @@ -1,4 +1,4 @@ -package platform +package sysfs import ( "embed" @@ -13,21 +13,11 @@ import ( gofstest "testing/fstest" "time" + "github.com/tetratelabs/wazero/internal/fsapi" + "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/testing/require" ) -var _ File = NoopFile{} - -// NoopFile shows the minimal methods a type embedding UnimplementedFile must -// implement. -type NoopFile struct { - UnimplementedFile -} - -// The current design requires the user to consciously implement Close. -// However, we could change UnimplementedFile to return zero. -func (NoopFile) Close() (errno syscall.Errno) { return } - //go:embed file_test.go var embedFS embed.FS @@ -98,7 +88,7 @@ func TestFileIno(t *testing.T) { dirFS, embedFS, mapFS := dirEmbedMapFS(t, tmpDir) // get the expected inode - st, errno := Stat(tmpDir) + st, errno := stat(tmpDir) require.EqualErrno(t, 0, errno) tests := []struct { @@ -107,7 +97,7 @@ func TestFileIno(t *testing.T) { expectedIno uint64 }{ {name: "os.DirFS", fs: dirFS, expectedIno: st.Ino}, - {name: "embed.FS", fs: embedFS}, + {name: "embed.api.FS", fs: embedFS}, {name: "fstest.MapFS", fs: mapFS}, } @@ -159,7 +149,7 @@ func TestFileIsDir(t *testing.T) { fs fs.FS }{ {name: "os.DirFS", fs: dirFS}, - {name: "embed.FS", fs: embedFS}, + {name: "embed.api.FS", fs: embedFS}, {name: "fstest.MapFS", fs: mapFS}, } @@ -208,7 +198,7 @@ func TestFileReadAndPread(t *testing.T) { fs fs.FS }{ {name: "os.DirFS", fs: dirFS}, - {name: "embed.FS", fs: embedFS}, + {name: "embed.api.FS", fs: embedFS}, {name: "fstest.MapFS", fs: mapFS}, } @@ -282,13 +272,13 @@ func TestFilePollRead(t *testing.T) { require.Equal(t, expected, buf[:len(expected)]) } -func requireRead(t *testing.T, f File, buf []byte) { +func requireRead(t *testing.T, f fsapi.File, buf []byte) { n, errno := f.Read(buf) require.EqualErrno(t, 0, errno) require.Equal(t, len(buf), n) } -func requirePread(t *testing.T, f File, buf []byte, off int64) { +func requirePread(t *testing.T, f fsapi.File, buf []byte, off int64) { n, errno := f.Pread(buf, off) require.EqualErrno(t, 0, errno) require.Equal(t, len(buf), n) @@ -302,7 +292,7 @@ func TestFileRead_empty(t *testing.T) { fs fs.FS }{ {name: "os.DirFS", fs: dirFS}, - {name: "embed.FS", fs: embedFS}, + {name: "embed.api.FS", fs: embedFS}, {name: "fstest.MapFS", fs: mapFS}, } @@ -366,13 +356,13 @@ func TestFileRead_Errors(t *testing.T) { tests := []struct { name string - fn func(File) syscall.Errno + fn func(fsapi.File) syscall.Errno }{ - {name: "Read", fn: func(f File) syscall.Errno { + {name: "Read", fn: func(f fsapi.File) syscall.Errno { _, errno := f.Read(buf) return errno }}, - {name: "Pread", fn: func(f File) syscall.Errno { + {name: "Pread", fn: func(f fsapi.File) syscall.Errno { _, errno := f.Pread(buf, 0) return errno }}, @@ -400,7 +390,7 @@ func TestFileSeek(t *testing.T) { fs fs.FS }{ {name: "os.DirFS", fs: dirFS}, - {name: "embed.FS", fs: embedFS}, + {name: "embed.api.FS", fs: embedFS}, {name: "fstest.MapFS", fs: mapFS}, } @@ -464,21 +454,21 @@ func TestFileSeek(t *testing.T) { } t.Run("os.File directory seek to zero", func(t *testing.T) { - d := requireOpenFile(t, os.TempDir(), syscall.O_RDONLY|O_DIRECTORY, 0o666) + d := requireOpenFile(t, os.TempDir(), syscall.O_RDONLY|fsapi.O_DIRECTORY, 0o666) defer d.Close() _, errno := d.Seek(0, io.SeekStart) require.EqualErrno(t, 0, errno) }) - seekToZero := func(f File) syscall.Errno { + seekToZero := func(f fsapi.File) syscall.Errno { _, errno := f.Seek(0, io.SeekStart) return errno } testEBADFIfFileClosed(t, seekToZero) } -func requireSeek(t *testing.T, f File, off int64, whence int) int64 { +func requireSeek(t *testing.T, f fsapi.File, off int64, whence int) int64 { n, errno := f.Seek(off, whence) require.EqualErrno(t, 0, errno) return n @@ -492,7 +482,7 @@ func TestFileSeek_empty(t *testing.T) { fs fs.FS }{ {name: "os.DirFS", fs: dirFS}, - {name: "embed.FS", fs: embedFS}, + {name: "embed.api.FS", fs: embedFS}, {name: "fstest.MapFS", fs: mapFS}, } @@ -532,7 +522,7 @@ func TestFileSeek_Unsupported(t *testing.T) { } func TestFileWriteAndPwrite(t *testing.T) { - // fs.FS doesn't support writes, and there is no other built-in + // fsapi.FS doesn't support writes, and there is no other built-in // implementation except os.File. path := path.Join(t.TempDir(), wazeroFile) f := requireOpenFile(t, path, syscall.O_RDWR|os.O_CREATE, 0o600) @@ -572,20 +562,20 @@ func TestFileWriteAndPwrite(t *testing.T) { require.Equal(t, "wazerowazeroero", string(b)) } -func requireWrite(t *testing.T, f File, buf []byte) { +func requireWrite(t *testing.T, f fsapi.File, buf []byte) { n, errno := f.Write(buf) require.EqualErrno(t, 0, errno) require.Equal(t, len(buf), n) } -func requirePwrite(t *testing.T, f File, buf []byte, off int64) { +func requirePwrite(t *testing.T, f fsapi.File, buf []byte, off int64) { n, errno := f.Pwrite(buf, off) require.EqualErrno(t, 0, errno) require.Equal(t, len(buf), n) } func TestFileWrite_empty(t *testing.T) { - // fs.FS doesn't support writes, and there is no other built-in + // fsapi.FS doesn't support writes, and there is no other built-in // implementation except os.File. path := path.Join(t.TempDir(), emptyFile) f := requireOpenFile(t, path, syscall.O_RDWR|os.O_CREATE, 0o600) @@ -593,15 +583,15 @@ func TestFileWrite_empty(t *testing.T) { tests := []struct { name string - fn func(File, []byte) (int, syscall.Errno) + fn func(fsapi.File, []byte) (int, syscall.Errno) }{ - {name: "Write", fn: func(f File, buf []byte) (int, syscall.Errno) { + {name: "Write", fn: func(f fsapi.File, buf []byte) (int, syscall.Errno) { return f.Write(buf) }}, - {name: "Pwrite from zero", fn: func(f File, buf []byte) (int, syscall.Errno) { + {name: "Pwrite from zero", fn: func(f fsapi.File, buf []byte) (int, syscall.Errno) { return f.Pwrite(buf, 0) }}, - {name: "Pwrite from 3", fn: func(f File, buf []byte) (int, syscall.Errno) { + {name: "Pwrite from 3", fn: func(f fsapi.File, buf []byte) (int, syscall.Errno) { return f.Pwrite(buf, 3) }}, } @@ -635,12 +625,12 @@ func TestFileWrite_Unsupported(t *testing.T) { tests := []struct { name string - fn func(File, []byte) (int, syscall.Errno) + fn func(fsapi.File, []byte) (int, syscall.Errno) }{ - {name: "Write", fn: func(f File, buf []byte) (int, syscall.Errno) { + {name: "Write", fn: func(f fsapi.File, buf []byte) (int, syscall.Errno) { return f.Write(buf) }}, - {name: "Pwrite", fn: func(f File, buf []byte) (int, syscall.Errno) { + {name: "Pwrite", fn: func(f fsapi.File, buf []byte) (int, syscall.Errno) { return f.Pwrite(buf, 0) }}, } @@ -672,13 +662,13 @@ func TestFileWrite_Errors(t *testing.T) { tests := []struct { name string - fn func(File) syscall.Errno + fn func(fsapi.File) syscall.Errno }{ - {name: "Write", fn: func(f File) syscall.Errno { + {name: "Write", fn: func(f fsapi.File) syscall.Errno { _, errno := f.Write(buf) return errno }}, - {name: "Pwrite", fn: func(f File) syscall.Errno { + {name: "Pwrite", fn: func(f fsapi.File) syscall.Errno { _, errno := f.Pwrite(buf, 0) return errno }}, @@ -699,14 +689,14 @@ func TestFileWrite_Errors(t *testing.T) { } func TestFileSync_NoError(t *testing.T) { - testSync_NoError(t, File.Sync) + testSync_NoError(t, fsapi.File.Sync) } func TestFileDatasync_NoError(t *testing.T) { - testSync_NoError(t, File.Datasync) + testSync_NoError(t, fsapi.File.Datasync) } -func testSync_NoError(t *testing.T, sync func(File) syscall.Errno) { +func testSync_NoError(t *testing.T, sync func(fsapi.File) syscall.Errno) { roPath := "file_test.go" ro, errno := OpenFSFile(embedFS, roPath, syscall.O_RDONLY, 0) require.EqualErrno(t, 0, errno) @@ -719,9 +709,9 @@ func testSync_NoError(t *testing.T, sync func(File) syscall.Errno) { tests := []struct { name string - f File + f fsapi.File }{ - {name: "UnimplementedFile", f: NoopFile{}}, + {name: "UnimplementedFile", f: fsapi.UnimplementedFile{}}, {name: "File of read-only fs.File", f: ro}, {name: "File of os.File", f: rw}, } @@ -736,17 +726,17 @@ func testSync_NoError(t *testing.T, sync func(File) syscall.Errno) { } func TestFileSync(t *testing.T) { - testSync(t, File.Sync) + testSync(t, fsapi.File.Sync) } func TestFileDatasync(t *testing.T) { - testSync(t, File.Datasync) + testSync(t, fsapi.File.Datasync) } // testSync doesn't guarantee sync works because the operating system may // sync anyway. There is no test in Go for syscall.Fdatasync, but closest is // similar to below. Effectively, this only tests that things don't error. -func testSync(t *testing.T, sync func(File) syscall.Errno) { +func testSync(t *testing.T, sync func(fsapi.File) syscall.Errno) { // Even though it is invalid, try to sync a directory dPath := t.TempDir() d := requireOpenFile(t, dPath, syscall.O_RDONLY, 0) @@ -838,7 +828,7 @@ func TestFileTruncate(t *testing.T) { }) } - truncateToZero := func(f File) syscall.Errno { + truncateToZero := func(f fsapi.File) syscall.Errno { return f.Truncate(0) } @@ -865,7 +855,7 @@ func TestFileUtimens(t *testing.T) { case "linux", "darwin": // supported case "freebsd": // TODO: support freebsd w/o CGO case "windows": - if !IsGo120 { + if !platform.IsGo120 { t.Skip("windows only works after Go 1.20") // TODO: possibly 1.19 ;) } default: // expect ENOSYS and callers need to fall back to Utimens @@ -874,10 +864,10 @@ func TestFileUtimens(t *testing.T) { testUtimens(t, true) - testEBADFIfFileClosed(t, func(f File) syscall.Errno { + testEBADFIfFileClosed(t, func(f fsapi.File) syscall.Errno { return f.Utimens(nil) }) - testEBADFIfDirClosed(t, func(d File) syscall.Errno { + testEBADFIfDirClosed(t, func(d fsapi.File) syscall.Errno { return d.Utimens(nil) }) } @@ -906,7 +896,7 @@ func TestNewStdioFile(t *testing.T) { tests := []struct { name string - f File + f fsapi.File // Depending on how the tests run, os.Stdin won't necessarily be a char // device. We compare against an os.File, to account for this. expectedType fs.FileMode @@ -951,7 +941,7 @@ func TestNewStdioFile(t *testing.T) { } } -func testEBADFIfDirClosed(t *testing.T, fn func(File) syscall.Errno) bool { +func testEBADFIfDirClosed(t *testing.T, fn func(fsapi.File) syscall.Errno) bool { return t.Run("EBADF if dir closed", func(t *testing.T) { d := requireOpenFile(t, t.TempDir(), syscall.O_RDONLY, 0o755) @@ -962,7 +952,7 @@ func testEBADFIfDirClosed(t *testing.T, fn func(File) syscall.Errno) bool { }) } -func testEBADFIfFileClosed(t *testing.T, fn func(File) syscall.Errno) bool { +func testEBADFIfFileClosed(t *testing.T, fn func(fsapi.File) syscall.Errno) bool { return t.Run("EBADF if file closed", func(t *testing.T) { tmpDir := t.TempDir() @@ -975,21 +965,24 @@ func testEBADFIfFileClosed(t *testing.T, fn func(File) syscall.Errno) bool { }) } -func testEISDIR(t *testing.T, fn func(File) syscall.Errno) bool { +func testEISDIR(t *testing.T, fn func(fsapi.File) syscall.Errno) bool { return t.Run("EISDIR if directory", func(t *testing.T) { - f := requireOpenFile(t, os.TempDir(), syscall.O_RDONLY|O_DIRECTORY, 0o666) + f := requireOpenFile(t, os.TempDir(), syscall.O_RDONLY|fsapi.O_DIRECTORY, 0o666) defer f.Close() require.EqualErrno(t, syscall.EISDIR, fn(f)) }) } -func openForWrite(t *testing.T, path string, content []byte) File { +func openForWrite(t *testing.T, path string, content []byte) fsapi.File { require.NoError(t, os.WriteFile(path, content, 0o0666)) - return requireOpenFile(t, path, syscall.O_RDWR, 0o666) + f := requireOpenFile(t, path, syscall.O_RDWR, 0o666) + _, errno := f.Write(content) + require.EqualErrno(t, 0, errno) + return f } -func requireOpenFile(t *testing.T, path string, flag int, perm fs.FileMode) File { +func requireOpenFile(t *testing.T, path string, flag int, perm fs.FileMode) fsapi.File { f, errno := OpenOSFile(path, flag, perm) require.EqualErrno(t, 0, errno) return f diff --git a/internal/platform/futimens.go b/internal/sysfs/futimens.go similarity index 94% rename from internal/platform/futimens.go rename to internal/sysfs/futimens.go index 084b4836..1c2bcead 100644 --- a/internal/platform/futimens.go +++ b/internal/sysfs/futimens.go @@ -1,9 +1,12 @@ -package platform +package sysfs import ( "syscall" "time" "unsafe" + + "github.com/tetratelabs/wazero/internal/fsapi" + "github.com/tetratelabs/wazero/internal/platform" ) const ( @@ -44,9 +47,11 @@ const ( // POSIX. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html func Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno { err := utimens(path, times, symlinkFollow) - return UnwrapOSError(err) + return platform.UnwrapOSError(err) } +var _zero uintptr //nolint:unused + func timesToPtr(times *[2]syscall.Timespec) unsafe.Pointer { //nolint:unused var _p0 unsafe.Pointer if times != nil { @@ -102,7 +107,7 @@ func normalizeTimespec(path string, times *[2]syscall.Timespec, i int) (ts sysca // stat to read-back the value to re-apply. // - https://github.com/golang/go/issues/32558. // - https://go-review.googlesource.com/c/go/+/219638 (unmerged) - var st Stat_t + var st fsapi.Stat_t if st, err = stat(path); err != 0 { return } diff --git a/internal/platform/futimens_darwin.go b/internal/sysfs/futimens_darwin.go similarity index 98% rename from internal/platform/futimens_darwin.go rename to internal/sysfs/futimens_darwin.go index 1dfd1326..f4ede337 100644 --- a/internal/platform/futimens_darwin.go +++ b/internal/sysfs/futimens_darwin.go @@ -1,4 +1,4 @@ -package platform +package sysfs import ( "syscall" diff --git a/internal/platform/futimens_darwin.s b/internal/sysfs/futimens_darwin.s similarity index 100% rename from internal/platform/futimens_darwin.s rename to internal/sysfs/futimens_darwin.s diff --git a/internal/platform/futimens_linux.go b/internal/sysfs/futimens_linux.go similarity index 98% rename from internal/platform/futimens_linux.go rename to internal/sysfs/futimens_linux.go index 07ebf2e2..a7ae264d 100644 --- a/internal/platform/futimens_linux.go +++ b/internal/sysfs/futimens_linux.go @@ -1,4 +1,4 @@ -package platform +package sysfs import ( "syscall" diff --git a/internal/platform/futimens_test.go b/internal/sysfs/futimens_test.go similarity index 96% rename from internal/platform/futimens_test.go rename to internal/sysfs/futimens_test.go index a33636c1..eb8318dc 100644 --- a/internal/platform/futimens_test.go +++ b/internal/sysfs/futimens_test.go @@ -1,4 +1,4 @@ -package platform +package sysfs import ( "os" @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/testing/require" ) @@ -147,7 +148,7 @@ func testUtimens(t *testing.T, futimes bool) { panic(tc) } - oldSt, errno := Lstat(statPath) + oldSt, errno := lstat(statPath) require.EqualErrno(t, 0, errno) if !futimes { @@ -174,10 +175,10 @@ func testUtimens(t *testing.T, futimes bool) { require.EqualErrno(t, 0, errno) } - newSt, errno := Lstat(statPath) + newSt, errno := lstat(statPath) require.EqualErrno(t, 0, errno) - if CompilerSupported() { + if platform.CompilerSupported() { if tc.times != nil && tc.times[0].Nsec == UTIME_OMIT { require.Equal(t, oldSt.Atim, newSt.Atim) } else if tc.times == nil || tc.times[0].Nsec == UTIME_NOW { diff --git a/internal/platform/futimens_unsupported.go b/internal/sysfs/futimens_unsupported.go similarity index 97% rename from internal/platform/futimens_unsupported.go rename to internal/sysfs/futimens_unsupported.go index 3fb3c92b..60860e6c 100644 --- a/internal/platform/futimens_unsupported.go +++ b/internal/sysfs/futimens_unsupported.go @@ -1,6 +1,6 @@ //go:build !windows && !linux && !darwin -package platform +package sysfs import "syscall" diff --git a/internal/platform/futimens_windows.go b/internal/sysfs/futimens_windows.go similarity index 95% rename from internal/platform/futimens_windows.go rename to internal/sysfs/futimens_windows.go index c0bc03fa..39696067 100644 --- a/internal/platform/futimens_windows.go +++ b/internal/sysfs/futimens_windows.go @@ -1,8 +1,10 @@ -package platform +package sysfs import ( "syscall" "time" + + "github.com/tetratelabs/wazero/internal/platform" ) // Define values even if not used except as sentinels. @@ -19,7 +21,7 @@ func utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) error func futimens(fd uintptr, times *[2]syscall.Timespec) error { // Before Go 1.20, ERROR_INVALID_HANDLE was returned for too many reasons. // Kick out so that callers can use path-based operations instead. - if !IsGo120 { + if !platform.IsGo120 { return syscall.ENOSYS } diff --git a/internal/platform/nonblock_unix.go b/internal/sysfs/nonblock_unix.go similarity index 88% rename from internal/platform/nonblock_unix.go rename to internal/sysfs/nonblock_unix.go index 0af1471b..1ac13e53 100644 --- a/internal/platform/nonblock_unix.go +++ b/internal/sysfs/nonblock_unix.go @@ -1,6 +1,6 @@ //go:build !windows -package platform +package sysfs import "syscall" diff --git a/internal/platform/nonblock_windows.go b/internal/sysfs/nonblock_windows.go similarity index 89% rename from internal/platform/nonblock_windows.go rename to internal/sysfs/nonblock_windows.go index 0bac6918..d4a29ac3 100644 --- a/internal/platform/nonblock_windows.go +++ b/internal/sysfs/nonblock_windows.go @@ -1,6 +1,6 @@ //go:build windows -package platform +package sysfs import "syscall" diff --git a/internal/platform/open_file.go b/internal/sysfs/open_file.go similarity index 58% rename from internal/platform/open_file.go rename to internal/sysfs/open_file.go index fc68986f..473f8ca6 100644 --- a/internal/platform/open_file.go +++ b/internal/sysfs/open_file.go @@ -1,22 +1,17 @@ //go:build !windows && !js && !illumos && !solaris -package platform +package sysfs import ( "io/fs" "os" "syscall" + + "github.com/tetratelabs/wazero/internal/fsapi" + "github.com/tetratelabs/wazero/internal/platform" ) -// Simple aliases to constants in the syscall package for portability with -// platforms which do not have them (e.g. windows) -const ( - O_DIRECTORY = syscall.O_DIRECTORY - O_NOFOLLOW = syscall.O_NOFOLLOW - O_NONBLOCK = syscall.O_NONBLOCK -) - -func newOsFile(openPath string, openFlag int, openPerm fs.FileMode, f *os.File) File { +func newOsFile(openPath string, openFlag int, openPerm fs.FileMode, f *os.File) fsapi.File { return newDefaultOsFile(openPath, openFlag, openPerm, f) } @@ -24,8 +19,8 @@ func newOsFile(openPath string, openFlag int, openPerm fs.FileMode, f *os.File) // syscall.Errno is success. func openFile(path string, flag int, perm fs.FileMode) (*os.File, syscall.Errno) { f, err := os.OpenFile(path, flag, perm) - // Note: This does not return a platform.File because sysfs.FS that returns + // Note: This does not return a fsapi.File because fsapi.FS that returns // one may want to hide the real OS path. For example, this is needed for // pre-opens. - return f, UnwrapOSError(err) + return f, platform.UnwrapOSError(err) } diff --git a/internal/platform/open_file_js.go b/internal/sysfs/open_file_js.go similarity index 66% rename from internal/platform/open_file_js.go rename to internal/sysfs/open_file_js.go index e4389a13..e473acbe 100644 --- a/internal/platform/open_file_js.go +++ b/internal/sysfs/open_file_js.go @@ -1,16 +1,11 @@ -package platform +package sysfs import ( "io/fs" "os" "syscall" -) -// See the comments on the same constants in open_file_windows.go -const ( - O_DIRECTORY = 1 << 29 - O_NOFOLLOW = 1 << 30 - O_NONBLOCK = 1 << 31 + "github.com/tetratelabs/wazero/internal/platform" ) func newOsFile(openPath string, openFlag int, openPerm fs.FileMode, f *os.File) File { @@ -20,5 +15,5 @@ func newOsFile(openPath string, openFlag int, openPerm fs.FileMode, f *os.File) func openFile(path string, flag int, perm fs.FileMode) (*os.File, syscall.Errno) { flag &= ^(O_DIRECTORY | O_NOFOLLOW) // erase placeholders f, err := os.OpenFile(path, flag, perm) - return f, UnwrapOSError(err) + return f, platform.UnwrapOSError(err) } diff --git a/internal/platform/open_file_sun.go b/internal/sysfs/open_file_sun.go similarity index 51% rename from internal/platform/open_file_sun.go rename to internal/sysfs/open_file_sun.go index 9714cc1b..e23b7185 100644 --- a/internal/platform/open_file_sun.go +++ b/internal/sysfs/open_file_sun.go @@ -1,25 +1,21 @@ //go:build illumos || solaris -package platform +package sysfs import ( "io/fs" "os" "syscall" + + "github.com/tetratelabs/wazero/internal/fsapi" + "github.com/tetratelabs/wazero/internal/platform" ) -const ( - // See https://github.com/illumos/illumos-gate/blob/edd580643f2cf1434e252cd7779e83182ea84945/usr/src/uts/common/sys/fcntl.h#L90 - O_DIRECTORY = 0x1000000 - O_NOFOLLOW = syscall.O_NOFOLLOW - O_NONBLOCK = syscall.O_NONBLOCK -) - -func newOsFile(openPath string, openFlag int, openPerm fs.FileMode, f *os.File) File { +func newOsFile(openPath string, openFlag int, openPerm fs.FileMode, f *os.File) fsapi.File { return newDefaultOsFile(openPath, openFlag, openPerm, f) } func openFile(path string, flag int, perm fs.FileMode) (*os.File, syscall.Errno) { f, err := os.OpenFile(path, flag, perm) - return f, UnwrapOSError(err) + return f, platform.UnwrapOSError(err) } diff --git a/internal/platform/open_file_test.go b/internal/sysfs/open_file_test.go similarity index 90% rename from internal/platform/open_file_test.go rename to internal/sysfs/open_file_test.go index 03954dde..30e98db3 100644 --- a/internal/platform/open_file_test.go +++ b/internal/sysfs/open_file_test.go @@ -1,4 +1,4 @@ -package platform +package sysfs import ( "os" @@ -7,6 +7,7 @@ import ( "syscall" "testing" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/testing/require" ) @@ -95,18 +96,18 @@ func TestOpenFile_Errors(t *testing.T) { err := os.Symlink(target, symlink) require.NoError(t, err) - _, errno := OpenFile(symlink, O_DIRECTORY|O_NOFOLLOW, 0o0666) + _, errno := OpenFile(symlink, fsapi.O_DIRECTORY|fsapi.O_NOFOLLOW, 0o0666) require.EqualErrno(t, syscall.ENOTDIR, errno) - _, errno = OpenFile(symlink, O_NOFOLLOW, 0o0666) + _, errno = OpenFile(symlink, fsapi.O_NOFOLLOW, 0o0666) require.EqualErrno(t, syscall.ELOOP, errno) }) t.Run("opening a directory writeable is EISDIR", func(t *testing.T) { - _, errno := OpenFile(tmpDir, O_DIRECTORY|syscall.O_WRONLY, 0o0666) + _, errno := OpenFile(tmpDir, fsapi.O_DIRECTORY|syscall.O_WRONLY, 0o0666) require.EqualErrno(t, syscall.EISDIR, errno) - _, errno = OpenFile(tmpDir, O_DIRECTORY|syscall.O_WRONLY, 0o0666) + _, errno = OpenFile(tmpDir, fsapi.O_DIRECTORY|syscall.O_WRONLY, 0o0666) require.EqualErrno(t, syscall.EISDIR, errno) }) } diff --git a/internal/platform/open_file_windows.go b/internal/sysfs/open_file_windows.go similarity index 82% rename from internal/platform/open_file_windows.go rename to internal/sysfs/open_file_windows.go index 96ab2711..0800dc7b 100644 --- a/internal/platform/open_file_windows.go +++ b/internal/sysfs/open_file_windows.go @@ -1,4 +1,4 @@ -package platform +package sysfs import ( "io/fs" @@ -6,38 +6,20 @@ import ( "strings" "syscall" "unsafe" + + "github.com/tetratelabs/wazero/internal/fsapi" + "github.com/tetratelabs/wazero/internal/platform" ) -// Windows does not have these constants, we declare placeholders which should -// not conflict with other open flags. These placeholders are not declared as -// value zero so code written in a way which expects them to be bit flags still -// works as expected. -// -// Since those placeholder are not interpreted by the open function, the unix -// features they represent are also not implemented on windows: -// -// - O_DIRECTORY allows programs to ensure that the opened file is a directory. -// This could be emulated by doing a stat call on the file after opening it -// to verify that it is in fact a directory, then closing it and returning an -// error if it is not. -// -// - O_NOFOLLOW allows programs to ensure that if the opened file is a symbolic -// link, the link itself is opened instead of its target. -const ( - O_DIRECTORY = 1 << 29 - O_NOFOLLOW = 1 << 30 - O_NONBLOCK = syscall.O_NONBLOCK -) - -func newOsFile(openPath string, openFlag int, openPerm fs.FileMode, f *os.File) File { +func newOsFile(openPath string, openFlag int, openPerm fs.FileMode, f *os.File) fsapi.File { return &windowsOsFile{ osFile: osFile{path: openPath, flag: openFlag, perm: openPerm, file: f}, } } func openFile(path string, flag int, perm fs.FileMode) (*os.File, syscall.Errno) { - isDir := flag&O_DIRECTORY > 0 - flag &= ^(O_DIRECTORY | O_NOFOLLOW) // erase placeholders + isDir := flag&fsapi.O_DIRECTORY > 0 + flag &= ^(fsapi.O_DIRECTORY | fsapi.O_NOFOLLOW) // erase placeholders // TODO: document why we are opening twice fd, err := open(path, flag|syscall.O_CLOEXEC, uint32(perm)) @@ -47,7 +29,7 @@ func openFile(path string, flag int, perm fs.FileMode) (*os.File, syscall.Errno) // TODO: Set FILE_SHARE_DELETE for directory as well. f, err := os.OpenFile(path, flag, perm) - errno := UnwrapOSError(err) + errno := platform.UnwrapOSError(err) if errno == 0 { return f, 0 } @@ -160,7 +142,7 @@ func open(path string, mode int, perm uint32) (fd syscall.Handle, err error) { } } - if IsGo120 { + if platform.IsGo120 { // This shouldn't be included before 1.20 to have consistent behavior. // https://github.com/golang/go/commit/0f0aa5d8a6a0253627d58b3aa083b24a1091933f if createmode == syscall.OPEN_EXISTING && access == syscall.GENERIC_READ { @@ -181,7 +163,7 @@ type windowsOsFile struct { } // Readdir implements File.Readdir -func (f *windowsOsFile) Readdir(n int) (dirents []Dirent, errno syscall.Errno) { +func (f *windowsOsFile) Readdir(n int) (dirents []fsapi.Dirent, errno syscall.Errno) { if errno = f.maybeInitDir(); errno != 0 { return } diff --git a/internal/platform/osfile.go b/internal/sysfs/osfile.go similarity index 71% rename from internal/platform/osfile.go rename to internal/sysfs/osfile.go index 8560f52d..df39d16b 100644 --- a/internal/platform/osfile.go +++ b/internal/sysfs/osfile.go @@ -1,4 +1,4 @@ -package platform +package sysfs import ( "io" @@ -6,14 +6,17 @@ import ( "os" "syscall" "time" + + "github.com/tetratelabs/wazero/internal/fsapi" + "github.com/tetratelabs/wazero/internal/platform" ) -func newDefaultOsFile(openPath string, openFlag int, openPerm fs.FileMode, f *os.File) File { +func newDefaultOsFile(openPath string, openFlag int, openPerm fs.FileMode, f *os.File) fsapi.File { return &osFile{path: openPath, flag: openFlag, perm: openPerm, file: f} } // osFile is a file opened with this package, and uses os.File or syscalls to -// implement platform.File. +// implement api.File. type osFile struct { path string flag int @@ -27,7 +30,7 @@ type osFile struct { cachedSt *cachedStat } -// cachedStat returns the cacheable parts of platform.Stat_t or an error if +// cachedStat returns the cacheable parts of platform.sys.Stat_t or an error if // they couldn't be retrieved. func (f *osFile) cachedStat() (fileType fs.FileMode, ino uint64, errno syscall.Errno) { if f.cachedSt == nil { @@ -38,7 +41,7 @@ func (f *osFile) cachedStat() (fileType fs.FileMode, ino uint64, errno syscall.E return f.cachedSt.fileType, f.cachedSt.ino, 0 } -// Ino implements File.Ino +// Ino implements the same method as documented on api.File func (f *osFile) Ino() (uint64, syscall.Errno) { if _, ino, errno := f.cachedStat(); errno != 0 { return 0, errno @@ -52,7 +55,7 @@ func (f *osFile) IsAppend() bool { return f.flag&syscall.O_APPEND == syscall.O_APPEND } -// SetAppend implements File.SetAppend +// SetAppend implements the same method as documented on api.File func (f *osFile) SetAppend(enable bool) (errno syscall.Errno) { if enable { f.flag |= syscall.O_APPEND @@ -77,25 +80,25 @@ func (f *osFile) reopen() (errno syscall.Errno) { return } -// IsNonblock implements File.IsNonblock +// IsNonblock implements the same method as documented on api.File func (f *osFile) IsNonblock() bool { - return f.flag&O_NONBLOCK == O_NONBLOCK + return f.flag&fsapi.O_NONBLOCK == fsapi.O_NONBLOCK } -// SetNonblock implements File.SetNonblock +// SetNonblock implements the same method as documented on api.File func (f *osFile) SetNonblock(enable bool) (errno syscall.Errno) { if enable { - f.flag |= O_NONBLOCK + f.flag |= fsapi.O_NONBLOCK } else { - f.flag &= ^O_NONBLOCK + f.flag &= ^fsapi.O_NONBLOCK } if err := setNonblock(f.file.Fd(), enable); err != nil { - return fileError(f, f.closed, UnwrapOSError(err)) + return fileError(f, f.closed, platform.UnwrapOSError(err)) } return 0 } -// IsDir implements File.IsDir +// IsDir implements the same method as documented on api.File func (f *osFile) IsDir() (bool, syscall.Errno) { if ft, _, errno := f.cachedStat(); errno != 0 { return false, errno @@ -105,10 +108,10 @@ func (f *osFile) IsDir() (bool, syscall.Errno) { return false, 0 } -// Stat implements File.Stat -func (f *osFile) Stat() (Stat_t, syscall.Errno) { +// Stat implements the same method as documented on api.File +func (f *osFile) Stat() (fsapi.Stat_t, syscall.Errno) { if f.closed { - return Stat_t{}, syscall.EBADF + return fsapi.Stat_t{}, syscall.EBADF } st, errno := statFile(f.file) @@ -121,7 +124,7 @@ func (f *osFile) Stat() (Stat_t, syscall.Errno) { return st, errno } -// Read implements File.Read +// Read implements the same method as documented on api.File func (f *osFile) Read(buf []byte) (n int, errno syscall.Errno) { if n, errno = read(f.file, buf); errno != 0 { // Defer validation overhead until we've already had an error. @@ -130,7 +133,7 @@ func (f *osFile) Read(buf []byte) (n int, errno syscall.Errno) { return } -// Pread implements File.Pread +// Pread implements the same method as documented on api.File func (f *osFile) Pread(buf []byte, off int64) (n int, errno syscall.Errno) { if n, errno = pread(f.file, buf, off); errno != 0 { // Defer validation overhead until we've already had an error. @@ -139,7 +142,7 @@ func (f *osFile) Pread(buf []byte, off int64) (n int, errno syscall.Errno) { return } -// Seek implements File.Seek +// Seek implements the same method as documented on api.File func (f *osFile) Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno) { if newOffset, errno = seek(f.file, offset, whence); errno != 0 { // Defer validation overhead until we've already had an error. @@ -154,14 +157,14 @@ func (f *osFile) Seek(offset int64, whence int) (newOffset int64, errno syscall. return } -// PollRead implements File.PollRead +// PollRead implements the same method as documented on api.File func (f *osFile) PollRead(timeout *time.Duration) (ready bool, errno syscall.Errno) { - fdSet := FdSet{} + fdSet := platform.FdSet{} fd := int(f.file.Fd()) fdSet.Set(fd) nfds := fd + 1 // See https://man7.org/linux/man-pages/man2/select.2.html#:~:text=condition%20has%20occurred.-,nfds,-This%20argument%20should count, err := _select(nfds, &fdSet, nil, nil, timeout) - if errno = UnwrapOSError(err); errno != 0 { + if errno = platform.UnwrapOSError(err); errno != 0 { // Defer validation overhead until we've already had an error. errno = fileError(f, f.closed, errno) } @@ -170,14 +173,14 @@ func (f *osFile) PollRead(timeout *time.Duration) (ready bool, errno syscall.Err // Readdir implements File.Readdir. Notably, this uses "Readdir", not // "ReadDir", from os.File. -func (f *osFile) Readdir(n int) (dirents []Dirent, errno syscall.Errno) { +func (f *osFile) Readdir(n int) (dirents []fsapi.Dirent, errno syscall.Errno) { if dirents, errno = readdir(f.file, f.path, n); errno != 0 { errno = adjustReaddirErr(f, f.closed, errno) } return } -// Write implements File.Write +// Write implements the same method as documented on api.File func (f *osFile) Write(buf []byte) (n int, errno syscall.Errno) { if n, errno = write(f.file, buf); errno != 0 { // Defer validation overhead until we've already had an error. @@ -186,7 +189,7 @@ func (f *osFile) Write(buf []byte) (n int, errno syscall.Errno) { return } -// Pwrite implements File.Pwrite +// Pwrite implements the same method as documented on api.File func (f *osFile) Pwrite(buf []byte, off int64) (n int, errno syscall.Errno) { if n, errno = pwrite(f.file, buf, off); errno != 0 { // Defer validation overhead until we've already had an error. @@ -195,35 +198,35 @@ func (f *osFile) Pwrite(buf []byte, off int64) (n int, errno syscall.Errno) { return } -// Truncate implements File.Truncate +// Truncate implements the same method as documented on api.File func (f *osFile) Truncate(size int64) (errno syscall.Errno) { - if errno = UnwrapOSError(f.file.Truncate(size)); errno != 0 { + if errno = platform.UnwrapOSError(f.file.Truncate(size)); errno != 0 { // Defer validation overhead until we've already had an error. errno = fileError(f, f.closed, errno) } return } -// Sync implements File.Sync +// Sync implements the same method as documented on api.File func (f *osFile) Sync() syscall.Errno { return sync(f.file) } -// Datasync implements File.Datasync +// Datasync implements the same method as documented on api.File func (f *osFile) Datasync() syscall.Errno { return datasync(f.file) } -// Chmod implements File.Chmod +// Chmod implements the same method as documented on api.File func (f *osFile) Chmod(mode fs.FileMode) syscall.Errno { if f.closed { return syscall.EBADF } - return UnwrapOSError(f.file.Chmod(mode)) + return platform.UnwrapOSError(f.file.Chmod(mode)) } -// Chown implements File.Chown +// Chown implements the same method as documented on api.File func (f *osFile) Chown(uid, gid int) syscall.Errno { if f.closed { return syscall.EBADF @@ -232,17 +235,17 @@ func (f *osFile) Chown(uid, gid int) syscall.Errno { return fchown(f.file.Fd(), uid, gid) } -// Utimens implements File.Utimens +// Utimens implements the same method as documented on api.File func (f *osFile) Utimens(times *[2]syscall.Timespec) syscall.Errno { if f.closed { return syscall.EBADF } err := futimens(f.file.Fd(), times) - return UnwrapOSError(err) + return platform.UnwrapOSError(err) } -// Close implements File.Close +// Close implements the same method as documented on api.File func (f *osFile) Close() syscall.Errno { if f.closed { return 0 @@ -252,5 +255,5 @@ func (f *osFile) Close() syscall.Errno { } func (f *osFile) close() syscall.Errno { - return UnwrapOSError(f.file.Close()) + return platform.UnwrapOSError(f.file.Close()) } diff --git a/internal/sysfs/readfs.go b/internal/sysfs/readfs.go index 0d6852a3..f4158dfd 100644 --- a/internal/sysfs/readfs.go +++ b/internal/sysfs/readfs.go @@ -6,24 +6,24 @@ import ( "syscall" "time" - "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/fsapi" ) -// NewReadFS is used to mask an existing FS for reads. Notably, this allows +// NewReadFS is used to mask an existing api.FS for reads. Notably, this allows // the CLI to do read-only mounts of directories the host user can write, but // doesn't want the guest wasm to. For example, Python libraries shouldn't be // written to at runtime by the python wasm file. -func NewReadFS(fs FS) FS { +func NewReadFS(fs fsapi.FS) fsapi.FS { if _, ok := fs.(*readFS); ok { return fs - } else if _, ok = fs.(UnimplementedFS); ok { + } else if _, ok = fs.(fsapi.UnimplementedFS); ok { return fs // unimplemented is read-only } return &readFS{fs: fs} } type readFS struct { - fs FS + fs fsapi.FS } // String implements fmt.Stringer @@ -31,8 +31,8 @@ func (r *readFS) String() string { return r.fs.String() } -// OpenFile implements FS.OpenFile -func (r *readFS) OpenFile(path string, flag int, perm fs.FileMode) (platform.File, syscall.Errno) { +// OpenFile implements the same method as documented on api.FS +func (r *readFS) OpenFile(path string, flag int, perm fs.FileMode) (fsapi.File, syscall.Errno) { // TODO: Once the real implementation is complete, move the below to // /RATIONALE.md. Doing this while the type is unstable creates // documentation drift as we expect a lot of reshaping meanwhile. @@ -54,7 +54,7 @@ func (r *readFS) OpenFile(path string, flag int, perm fs.FileMode) (platform.Fil // check if they are the opposite of read or not. switch flag & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR) { case os.O_WRONLY, os.O_RDWR: - if flag&platform.O_DIRECTORY != 0 { + if flag&fsapi.O_DIRECTORY != 0 { return nil, syscall.EISDIR } return nil, syscall.ENOSYS @@ -68,104 +68,104 @@ func (r *readFS) OpenFile(path string, flag int, perm fs.FileMode) (platform.Fil return &readFile{f: f}, 0 } -// compile-time check to ensure readFile implements platform.File. -var _ platform.File = (*readFile)(nil) +// compile-time check to ensure readFile implements api.File. +var _ fsapi.File = (*readFile)(nil) type readFile struct { - f platform.File + f fsapi.File } -// Ino implements the same method as documented on platform.File. +// Ino implements the same method as documented on api.File. func (r *readFile) Ino() (uint64, syscall.Errno) { return r.f.Ino() } -// IsNonblock implements the same method as documented on platform.File. +// IsNonblock implements the same method as documented on api.File. func (r *readFile) IsNonblock() bool { return r.f.IsNonblock() } -// SetNonblock implements the same method as documented on platform.File. +// SetNonblock implements the same method as documented on api.File. func (r *readFile) SetNonblock(enabled bool) syscall.Errno { return r.f.SetNonblock(enabled) } -// IsAppend implements the same method as documented on platform.File. +// IsAppend implements the same method as documented on api.File. func (r *readFile) IsAppend() bool { return r.f.IsAppend() } -// SetAppend implements the same method as documented on platform.File. +// SetAppend implements the same method as documented on api.File. func (r *readFile) SetAppend(enabled bool) syscall.Errno { return r.f.SetAppend(enabled) } -// Stat implements the same method as documented on platform.File. -func (r *readFile) Stat() (platform.Stat_t, syscall.Errno) { +// Stat implements the same method as documented on api.File. +func (r *readFile) Stat() (fsapi.Stat_t, syscall.Errno) { return r.f.Stat() } -// IsDir implements the same method as documented on platform.File. +// IsDir implements the same method as documented on api.File. func (r *readFile) IsDir() (bool, syscall.Errno) { return r.f.IsDir() } -// Read implements the same method as documented on platform.File. +// Read implements the same method as documented on api.File. func (r *readFile) Read(buf []byte) (int, syscall.Errno) { return r.f.Read(buf) } -// Pread implements the same method as documented on platform.File. +// Pread implements the same method as documented on api.File. func (r *readFile) Pread(buf []byte, offset int64) (int, syscall.Errno) { return r.f.Pread(buf, offset) } -// Seek implements the same method as documented on platform.File. +// Seek implements the same method as documented on api.File. func (r *readFile) Seek(offset int64, whence int) (int64, syscall.Errno) { return r.f.Seek(offset, whence) } -// Readdir implements the same method as documented on platform.File. -func (r *readFile) Readdir(n int) (dirents []platform.Dirent, errno syscall.Errno) { +// Readdir implements the same method as documented on api.File. +func (r *readFile) Readdir(n int) (dirents []fsapi.Dirent, errno syscall.Errno) { return r.f.Readdir(n) } -// Write implements the same method as documented on platform.File. +// Write implements the same method as documented on api.File. func (r *readFile) Write([]byte) (int, syscall.Errno) { return 0, r.writeErr() } -// Pwrite implements the same method as documented on platform.File. +// Pwrite implements the same method as documented on api.File. func (r *readFile) Pwrite([]byte, int64) (n int, errno syscall.Errno) { return 0, r.writeErr() } -// Truncate implements the same method as documented on platform.File. +// Truncate implements the same method as documented on api.File. func (r *readFile) Truncate(int64) syscall.Errno { return r.writeErr() } -// Sync implements the same method as documented on platform.File. +// Sync implements the same method as documented on api.File. func (r *readFile) Sync() syscall.Errno { return syscall.EBADF } -// Datasync implements the same method as documented on platform.File. +// Datasync implements the same method as documented on api.File. func (r *readFile) Datasync() syscall.Errno { return syscall.EBADF } -// Chmod implements the same method as documented on platform.File. +// Chmod implements the same method as documented on api.File. func (r *readFile) Chmod(fs.FileMode) syscall.Errno { return syscall.EBADF } -// Chown implements the same method as documented on platform.File. +// Chown implements the same method as documented on api.File. func (r *readFile) Chown(int, int) syscall.Errno { return syscall.EBADF } -// Utimens implements the same method as documented on platform.File. +// Utimens implements the same method as documented on api.File. func (r *readFile) Utimens(*[2]syscall.Timespec) syscall.Errno { return syscall.EBADF } @@ -179,7 +179,7 @@ func (r *readFile) writeErr() syscall.Errno { return syscall.EBADF } -// Close implements the same method as documented on platform.File. +// Close implements the same method as documented on api.File. func (r *readFile) Close() syscall.Errno { return r.f.Close() } @@ -189,72 +189,72 @@ func (r *readFile) PollRead(timeout *time.Duration) (ready bool, errno syscall.E return r.f.PollRead(timeout) } -// Lstat implements FS.Lstat -func (r *readFS) Lstat(path string) (platform.Stat_t, syscall.Errno) { +// Lstat implements the same method as documented on api.FS +func (r *readFS) Lstat(path string) (fsapi.Stat_t, syscall.Errno) { return r.fs.Lstat(path) } -// Stat implements FS.Stat -func (r *readFS) Stat(path string) (platform.Stat_t, syscall.Errno) { +// Stat implements the same method as documented on api.FS +func (r *readFS) Stat(path string) (fsapi.Stat_t, syscall.Errno) { return r.fs.Stat(path) } -// Readlink implements FS.Readlink +// Readlink implements the same method as documented on api.FS func (r *readFS) Readlink(path string) (dst string, err syscall.Errno) { return r.fs.Readlink(path) } -// Mkdir implements FS.Mkdir +// Mkdir implements the same method as documented on api.FS func (r *readFS) Mkdir(path string, perm fs.FileMode) syscall.Errno { return syscall.EROFS } -// Chmod implements FS.Chmod +// Chmod implements the same method as documented on api.FS func (r *readFS) Chmod(path string, perm fs.FileMode) syscall.Errno { return syscall.EROFS } -// Chown implements FS.Chown +// Chown implements the same method as documented on api.FS func (r *readFS) Chown(path string, uid, gid int) syscall.Errno { return syscall.EROFS } -// Lchown implements FS.Lchown +// Lchown implements the same method as documented on api.FS func (r *readFS) Lchown(path string, uid, gid int) syscall.Errno { return syscall.EROFS } -// Rename implements FS.Rename +// Rename implements the same method as documented on api.FS func (r *readFS) Rename(from, to string) syscall.Errno { return syscall.EROFS } -// Rmdir implements FS.Rmdir +// Rmdir implements the same method as documented on api.FS func (r *readFS) Rmdir(path string) syscall.Errno { return syscall.EROFS } -// Link implements FS.Link +// Link implements the same method as documented on api.FS func (r *readFS) Link(_, _ string) syscall.Errno { return syscall.EROFS } -// Symlink implements FS.Symlink +// Symlink implements the same method as documented on api.FS func (r *readFS) Symlink(_, _ string) syscall.Errno { return syscall.EROFS } -// Unlink implements FS.Unlink +// Unlink implements the same method as documented on api.FS func (r *readFS) Unlink(path string) syscall.Errno { return syscall.EROFS } -// Utimens implements FS.Utimens +// Utimens implements the same method as documented on api.FS func (r *readFS) Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno { return syscall.EROFS } -// Truncate implements FS.Truncate +// Truncate implements the same method as documented on api.FS func (r *readFS) Truncate(string, int64) syscall.Errno { return syscall.EROFS } diff --git a/internal/sysfs/readfs_test.go b/internal/sysfs/readfs_test.go index 8eb2f242..b8be36e0 100644 --- a/internal/sysfs/readfs_test.go +++ b/internal/sysfs/readfs_test.go @@ -7,6 +7,7 @@ import ( "syscall" "testing" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/fstest" "github.com/tetratelabs/wazero/internal/testing/require" ) @@ -15,9 +16,9 @@ func TestNewReadFS(t *testing.T) { tmpDir := t.TempDir() // Doesn't double-wrap file systems that are already read-only - require.Equal(t, UnimplementedFS{}, NewReadFS(UnimplementedFS{})) + require.Equal(t, fsapi.UnimplementedFS{}, NewReadFS(fsapi.UnimplementedFS{})) - // Wraps a fs.FS because it allows access to Write + // Wraps a fsapi.FS because it allows access to Write adapted := Adapt(os.DirFS(tmpDir)) require.NotEqual(t, adapted, NewReadFS(adapted)) @@ -133,7 +134,7 @@ func TestReadFS_Open_Read(t *testing.T) { type test struct { name string - fs FS + fs fsapi.FS expectIno bool } diff --git a/internal/sysfs/rename.go b/internal/sysfs/rename.go new file mode 100644 index 00000000..b107bc19 --- /dev/null +++ b/internal/sysfs/rename.go @@ -0,0 +1,16 @@ +//go:build !windows + +package sysfs + +import ( + "syscall" + + "github.com/tetratelabs/wazero/internal/platform" +) + +func Rename(from, to string) syscall.Errno { + if from == to { + return 0 + } + return platform.UnwrapOSError(syscall.Rename(from, to)) +} diff --git a/internal/platform/rename_test.go b/internal/sysfs/rename_test.go similarity index 97% rename from internal/platform/rename_test.go rename to internal/sysfs/rename_test.go index 3a5e0cc4..84d17f33 100644 --- a/internal/platform/rename_test.go +++ b/internal/sysfs/rename_test.go @@ -1,4 +1,4 @@ -package platform +package sysfs import ( "errors" @@ -7,6 +7,7 @@ import ( "syscall" "testing" + "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/testing/require" ) @@ -53,7 +54,7 @@ func TestRename(t *testing.T) { // Show the prior path no longer exists _, err := os.Stat(dir1Path) - require.EqualErrno(t, syscall.ENOENT, UnwrapOSError(err)) + require.EqualErrno(t, syscall.ENOENT, platform.UnwrapOSError(err)) s, err := os.Stat(dir2Path) require.NoError(t, err) diff --git a/internal/platform/rename_windows.go b/internal/sysfs/rename_windows.go similarity index 77% rename from internal/platform/rename_windows.go rename to internal/sysfs/rename_windows.go index c3b406e5..25e53d4f 100644 --- a/internal/platform/rename_windows.go +++ b/internal/sysfs/rename_windows.go @@ -1,9 +1,11 @@ -package platform +package sysfs import ( "errors" "os" "syscall" + + "github.com/tetratelabs/wazero/internal/platform" ) func Rename(from, to string) syscall.Errno { @@ -25,21 +27,21 @@ func Rename(from, to string) syscall.Errno { } else if !fromIsDir && !toIsDir { // file to file // Use os.Rename instead of syscall.Rename in order to allow the overrides of the existing file. // Underneath os.Rename, it uses MoveFileEx instead of MoveFile (used by syscall.Rename). - return UnwrapOSError(os.Rename(from, to)) + return platform.UnwrapOSError(os.Rename(from, to)) } else { // dir to dir if dirs, _ := os.ReadDir(to); len(dirs) == 0 { // On Windows, renaming to the empty dir will be rejected, // so first we remove the empty dir, and then rename to it. if err := os.Remove(to); err != nil { - return UnwrapOSError(err) + return platform.UnwrapOSError(err) } - return UnwrapOSError(syscall.Rename(from, to)) + return platform.UnwrapOSError(syscall.Rename(from, to)) } return syscall.ENOTEMPTY } } else if !errors.Is(err, syscall.ENOENT) { // Failed to stat the destination. - return UnwrapOSError(err) + return platform.UnwrapOSError(err) } else { // Destination not-exist. - return UnwrapOSError(syscall.Rename(from, to)) + return platform.UnwrapOSError(syscall.Rename(from, to)) } } diff --git a/internal/sysfs/rootfs.go b/internal/sysfs/rootfs.go index fa0e621d..dcab08b7 100644 --- a/internal/sysfs/rootfs.go +++ b/internal/sysfs/rootfs.go @@ -7,13 +7,13 @@ import ( "strings" "syscall" - "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/fsapi" ) -func NewRootFS(fs []FS, guestPaths []string) (FS, error) { +func NewRootFS(fs []fsapi.FS, guestPaths []string) (fsapi.FS, error) { switch len(fs) { case 0: - return UnimplementedFS{}, nil + return fsapi.UnimplementedFS{}, nil case 1: if StripPrefixesAndTrailingSlash(guestPaths[0]) == "" { return fs[0], nil @@ -22,7 +22,7 @@ func NewRootFS(fs []FS, guestPaths []string) (FS, error) { ret := &CompositeFS{ string: stringFS(fs, guestPaths), - fs: make([]FS, len(fs)), + fs: make([]fsapi.FS, len(fs)), guestPaths: make([]string, len(fs)), cleanedGuestPaths: make([]string, len(fs)), rootGuestPaths: map[string]int{}, @@ -61,11 +61,11 @@ func NewRootFS(fs []FS, guestPaths []string) (FS, error) { } type CompositeFS struct { - UnimplementedFS + fsapi.UnimplementedFS // string is cached for convenience. string string // fs is index-correlated with cleanedGuestPaths - fs []FS + fs []fsapi.FS // guestPaths are the original paths supplied by the end user, cleaned as // cleanedGuestPaths. guestPaths []string @@ -83,7 +83,7 @@ func (c *CompositeFS) String() string { return c.string } -func stringFS(fs []FS, guestPaths []string) string { +func stringFS(fs []fsapi.FS, guestPaths []string) string { var ret strings.Builder ret.WriteString("[") writeMount(&ret, fs[0], guestPaths[0]) @@ -95,7 +95,7 @@ func stringFS(fs []FS, guestPaths []string) string { return ret.String() } -func writeMount(ret *strings.Builder, f FS, guestPath string) { +func writeMount(ret *strings.Builder, f fsapi.FS, guestPath string) { ret.WriteString(f.String()) ret.WriteString(":") ret.WriteString(guestPath) @@ -110,14 +110,14 @@ func (c *CompositeFS) GuestPaths() (guestPaths []string) { } // FS returns the underlying filesystems in original order. -func (c *CompositeFS) FS() (fs []FS) { - fs = make([]FS, len(c.guestPaths)) +func (c *CompositeFS) FS() (fs []fsapi.FS) { + fs = make([]fsapi.FS, len(c.guestPaths)) copy(fs, c.fs) return } -// OpenFile implements FS.OpenFile -func (c *CompositeFS) OpenFile(path string, flag int, perm fs.FileMode) (f platform.File, err syscall.Errno) { +// OpenFile implements the same method as documented on api.FS +func (c *CompositeFS) OpenFile(path string, flag int, perm fs.FileMode) (f fsapi.File, err syscall.Errno) { matchIndex, relativePath := c.chooseFS(path) f, err = c.fs[matchIndex].OpenFile(relativePath, flag, perm) @@ -140,26 +140,26 @@ func (c *CompositeFS) OpenFile(path string, flag int, perm fs.FileMode) (f platf // An openRootDir is a root directory open for reading, which has mounts inside // of it. type openRootDir struct { - platform.DirFile + fsapi.DirFile path string c *CompositeFS - f platform.File // the directory file itself - dirents []platform.Dirent // the directory contents - direntsI int // the read offset, an index into the files slice + f fsapi.File // the directory file itself + dirents []fsapi.Dirent // the directory contents + direntsI int // the read offset, an index into the files slice } -// Ino implements the same method as documented on platform.File +// Ino implements the same method as documented on api.File func (d *openRootDir) Ino() (uint64, syscall.Errno) { return d.f.Ino() } -// Stat implements the same method as documented on platform.File -func (d *openRootDir) Stat() (platform.Stat_t, syscall.Errno) { +// Stat implements the same method as documented on api.File +func (d *openRootDir) Stat() (fsapi.Stat_t, syscall.Errno) { return d.f.Stat() } -// Seek implements the same method as documented on platform.File +// Seek implements the same method as documented on api.File func (d *openRootDir) Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno) { if offset != 0 || whence != io.SeekStart { errno = syscall.ENOSYS @@ -170,8 +170,8 @@ func (d *openRootDir) Seek(offset int64, whence int) (newOffset int64, errno sys return d.f.Seek(offset, whence) } -// Readdir implements the same method as documented on platform.File -func (d *openRootDir) Readdir(count int) (dirents []platform.Dirent, errno syscall.Errno) { +// Readdir implements the same method as documented on api.File +func (d *openRootDir) Readdir(count int) (dirents []fsapi.Dirent, errno syscall.Errno) { if d.dirents == nil { if errno = d.readdir(); errno != 0 { return @@ -186,7 +186,7 @@ func (d *openRootDir) Readdir(count int) (dirents []platform.Dirent, errno sysca if count > 0 && n > count { n = count } - dirents = make([]platform.Dirent, n) + dirents = make([]fsapi.Dirent, n) for i := range dirents { dirents[i] = d.dirents[d.direntsI+i] } @@ -216,7 +216,7 @@ func (d *openRootDir) readdir() (errno syscall.Errno) { } } - var di platform.Dirent + var di fsapi.Dirent for n, fsI := range remaining { if di, errno = d.rootEntry(n, fsI); errno != 0 { return @@ -226,27 +226,27 @@ func (d *openRootDir) readdir() (errno syscall.Errno) { return } -// Sync implements the same method as documented on platform.File +// Sync implements the same method as documented on api.File func (d *openRootDir) Sync() syscall.Errno { return d.f.Sync() } -// Datasync implements the same method as documented on platform.File +// Datasync implements the same method as documented on api.File func (d *openRootDir) Datasync() syscall.Errno { return d.f.Datasync() } -// Chmod implements the same method as documented on platform.File +// Chmod implements the same method as documented on api.File func (d *openRootDir) Chmod(fs.FileMode) syscall.Errno { return syscall.ENOSYS } -// Chown implements the same method as documented on platform.File +// Chown implements the same method as documented on api.File func (d *openRootDir) Chown(int, int) syscall.Errno { return syscall.ENOSYS } -// Utimens implements the same method as documented on platform.File +// Utimens implements the same method as documented on api.File func (d *openRootDir) Utimens(*[2]syscall.Timespec) syscall.Errno { return syscall.ENOSYS } @@ -256,51 +256,51 @@ func (d *openRootDir) Close() syscall.Errno { return d.f.Close() } -func (d *openRootDir) rootEntry(name string, fsI int) (platform.Dirent, syscall.Errno) { +func (d *openRootDir) rootEntry(name string, fsI int) (fsapi.Dirent, syscall.Errno) { if st, errno := d.c.fs[fsI].Stat("."); errno != 0 { - return platform.Dirent{}, errno + return fsapi.Dirent{}, errno } else { - return platform.Dirent{Name: name, Ino: st.Ino, Type: st.Mode.Type()}, 0 + return fsapi.Dirent{Name: name, Ino: st.Ino, Type: st.Mode.Type()}, 0 } } -// Lstat implements FS.Lstat -func (c *CompositeFS) Lstat(path string) (platform.Stat_t, syscall.Errno) { +// Lstat implements the same method as documented on api.FS +func (c *CompositeFS) Lstat(path string) (fsapi.Stat_t, syscall.Errno) { matchIndex, relativePath := c.chooseFS(path) return c.fs[matchIndex].Lstat(relativePath) } -// Stat implements FS.Stat -func (c *CompositeFS) Stat(path string) (platform.Stat_t, syscall.Errno) { +// Stat implements the same method as documented on api.FS +func (c *CompositeFS) Stat(path string) (fsapi.Stat_t, syscall.Errno) { matchIndex, relativePath := c.chooseFS(path) return c.fs[matchIndex].Stat(relativePath) } -// Mkdir implements FS.Mkdir +// Mkdir implements the same method as documented on api.FS func (c *CompositeFS) Mkdir(path string, perm fs.FileMode) syscall.Errno { matchIndex, relativePath := c.chooseFS(path) return c.fs[matchIndex].Mkdir(relativePath, perm) } -// Chmod implements FS.Chmod +// Chmod implements the same method as documented on api.FS func (c *CompositeFS) Chmod(path string, perm fs.FileMode) syscall.Errno { matchIndex, relativePath := c.chooseFS(path) return c.fs[matchIndex].Chmod(relativePath, perm) } -// Chown implements FS.Chown +// Chown implements the same method as documented on api.FS func (c *CompositeFS) Chown(path string, uid, gid int) syscall.Errno { matchIndex, relativePath := c.chooseFS(path) return c.fs[matchIndex].Chown(relativePath, uid, gid) } -// Lchown implements FS.Lchown +// Lchown implements the same method as documented on api.FS func (c *CompositeFS) Lchown(path string, uid, gid int) syscall.Errno { matchIndex, relativePath := c.chooseFS(path) return c.fs[matchIndex].Lchown(relativePath, uid, gid) } -// Rename implements FS.Rename +// Rename implements the same method as documented on api.FS func (c *CompositeFS) Rename(from, to string) syscall.Errno { fromFS, fromPath := c.chooseFS(from) toFS, toPath := c.chooseFS(to) @@ -310,13 +310,13 @@ func (c *CompositeFS) Rename(from, to string) syscall.Errno { return c.fs[fromFS].Rename(fromPath, toPath) } -// Readlink implements FS.Readlink +// Readlink implements the same method as documented on api.FS func (c *CompositeFS) Readlink(path string) (string, syscall.Errno) { matchIndex, relativePath := c.chooseFS(path) return c.fs[matchIndex].Readlink(relativePath) } -// Link implements FS.Link. +// Link implements the same method as documented on api.FS func (c *CompositeFS) Link(oldName, newName string) syscall.Errno { fromFS, oldNamePath := c.chooseFS(oldName) toFS, newNamePath := c.chooseFS(newName) @@ -326,13 +326,13 @@ func (c *CompositeFS) Link(oldName, newName string) syscall.Errno { return c.fs[fromFS].Link(oldNamePath, newNamePath) } -// Utimens implements FS.Utimens +// Utimens implements the same method as documented on api.FS func (c *CompositeFS) Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno { matchIndex, relativePath := c.chooseFS(path) return c.fs[matchIndex].Utimens(relativePath, times, symlinkFollow) } -// Symlink implements FS.Symlink +// Symlink implements the same method as documented on api.FS func (c *CompositeFS) Symlink(oldName, link string) (err syscall.Errno) { fromFS, oldNamePath := c.chooseFS(oldName) toFS, linkPath := c.chooseFS(link) @@ -342,19 +342,19 @@ func (c *CompositeFS) Symlink(oldName, link string) (err syscall.Errno) { return c.fs[fromFS].Symlink(oldNamePath, linkPath) } -// Truncate implements FS.Truncate +// Truncate implements the same method as documented on api.FS func (c *CompositeFS) Truncate(path string, size int64) syscall.Errno { matchIndex, relativePath := c.chooseFS(path) return c.fs[matchIndex].Truncate(relativePath, size) } -// Rmdir implements FS.Rmdir +// Rmdir implements the same method as documented on api.FS func (c *CompositeFS) Rmdir(path string) syscall.Errno { matchIndex, relativePath := c.chooseFS(path) return c.fs[matchIndex].Rmdir(relativePath) } -// Unlink implements FS.Unlink +// Unlink implements the same method as documented on api.FS func (c *CompositeFS) Unlink(path string) syscall.Errno { matchIndex, relativePath := c.chooseFS(path) return c.fs[matchIndex].Unlink(relativePath) @@ -372,7 +372,8 @@ func (c *CompositeFS) chooseFS(path string) (matchIndex int, relativePath string prefix := c.cleanedGuestPaths[i] if eq, match := hasPathPrefix(path, pathI, pathLen, prefix); eq { // When the input equals the prefix, there cannot be a longer match - // later. The relative path is the FS root, so return empty string. + // later. The relative path is the fsapi.FS root, so return empty + // string. matchIndex = i relativePath = "" return @@ -496,11 +497,11 @@ loop: } type fakeRootFS struct { - UnimplementedFS + fsapi.UnimplementedFS } -// OpenFile implements FS.OpenFile -func (fakeRootFS) OpenFile(path string, flag int, perm fs.FileMode) (platform.File, syscall.Errno) { +// OpenFile implements the same method as documented on api.FS +func (fakeRootFS) OpenFile(path string, flag int, perm fs.FileMode) (fsapi.File, syscall.Errno) { switch path { case ".", "/", "": return fakeRootDir{}, 0 @@ -509,50 +510,50 @@ func (fakeRootFS) OpenFile(path string, flag int, perm fs.FileMode) (platform.Fi } type fakeRootDir struct { - platform.DirFile + fsapi.DirFile } -// Ino implements the same method as documented on platform.File +// Ino implements the same method as documented on api.File func (fakeRootDir) Ino() (uint64, syscall.Errno) { return 0, 0 } -// Stat implements the same method as documented on platform.File -func (fakeRootDir) Stat() (platform.Stat_t, syscall.Errno) { - return platform.Stat_t{Mode: fs.ModeDir, Nlink: 1}, 0 +// Stat implements the same method as documented on api.File +func (fakeRootDir) Stat() (fsapi.Stat_t, syscall.Errno) { + return fsapi.Stat_t{Mode: fs.ModeDir, Nlink: 1}, 0 } -// Readdir implements the same method as documented on platform.File -func (fakeRootDir) Readdir(int) (dirents []platform.Dirent, errno syscall.Errno) { +// Readdir implements the same method as documented on api.File +func (fakeRootDir) Readdir(int) (dirents []fsapi.Dirent, errno syscall.Errno) { return // empty } -// Sync implements the same method as documented on platform.File +// Sync implements the same method as documented on api.File func (fakeRootDir) Sync() syscall.Errno { return 0 } -// Datasync implements the same method as documented on platform.File +// Datasync implements the same method as documented on api.File func (fakeRootDir) Datasync() syscall.Errno { return 0 } -// Chmod implements the same method as documented on platform.File +// Chmod implements the same method as documented on api.File func (fakeRootDir) Chmod(fs.FileMode) syscall.Errno { return syscall.ENOSYS } -// Chown implements the same method as documented on platform.File +// Chown implements the same method as documented on api.File func (fakeRootDir) Chown(int, int) syscall.Errno { return syscall.ENOSYS } -// Utimens implements the same method as documented on platform.File +// Utimens implements the same method as documented on api.File func (fakeRootDir) Utimens(*[2]syscall.Timespec) syscall.Errno { return syscall.ENOSYS } -// Close implements the same method as documented on platform.File +// Close implements the same method as documented on api.File func (fakeRootDir) Close() syscall.Errno { return 0 } diff --git a/internal/sysfs/rootfs_test.go b/internal/sysfs/rootfs_test.go index 270e494c..fc47d535 100644 --- a/internal/sysfs/rootfs_test.go +++ b/internal/sysfs/rootfs_test.go @@ -11,6 +11,7 @@ import ( "testing" gofstest "testing/fstest" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/fstest" testfs "github.com/tetratelabs/wazero/internal/testing/fs" "github.com/tetratelabs/wazero/internal/testing/require" @@ -21,12 +22,12 @@ func TestNewRootFS(t *testing.T) { rootFS, err := NewRootFS(nil, nil) require.NoError(t, err) - require.Equal(t, UnimplementedFS{}, rootFS) + require.Equal(t, fsapi.UnimplementedFS{}, rootFS) }) t.Run("only root", func(t *testing.T) { testFS := NewDirFS(t.TempDir()) - rootFS, err := NewRootFS([]FS{testFS}, []string{""}) + rootFS, err := NewRootFS([]fsapi.FS{testFS}, []string{""}) require.NoError(t, err) // Should not be a composite filesystem @@ -35,11 +36,11 @@ func TestNewRootFS(t *testing.T) { t.Run("only non root", func(t *testing.T) { testFS := NewDirFS(".") - rootFS, err := NewRootFS([]FS{testFS}, []string{"/tmp"}) + rootFS, err := NewRootFS([]fsapi.FS{testFS}, []string{"/tmp"}) require.NoError(t, err) // unwrapping returns in original order - require.Equal(t, []FS{testFS}, rootFS.(*CompositeFS).FS()) + require.Equal(t, []fsapi.FS{testFS}, rootFS.(*CompositeFS).FS()) require.Equal(t, []string{"/tmp"}, rootFS.(*CompositeFS).GuestPaths()) // String is human-readable @@ -63,13 +64,13 @@ func TestNewRootFS(t *testing.T) { t.Run("multiple roots unsupported", func(t *testing.T) { testFS := NewDirFS(".") - _, err := NewRootFS([]FS{testFS, testFS}, []string{"/", "/"}) + _, err := NewRootFS([]fsapi.FS{testFS, testFS}, []string{"/", "/"}) require.EqualError(t, err, "multiple root filesystems are invalid: [.:/ .:/]") }) t.Run("virtual paths unsupported", func(t *testing.T) { testFS := NewDirFS(".") - _, err := NewRootFS([]FS{testFS}, []string{"usr/bin"}) + _, err := NewRootFS([]fsapi.FS{testFS}, []string{"usr/bin"}) require.EqualError(t, err, "only single-level guest paths allowed: [.:usr/bin]") }) t.Run("multiple matches", func(t *testing.T) { @@ -82,11 +83,11 @@ func TestNewRootFS(t *testing.T) { testFS2 := NewDirFS(tmpDir2) require.NoError(t, os.WriteFile(path.Join(tmpDir2, "a"), []byte{2}, 0o600)) - rootFS, err := NewRootFS([]FS{testFS2, testFS1}, []string{"/tmp", "/"}) + rootFS, err := NewRootFS([]fsapi.FS{testFS2, testFS1}, []string{"/tmp", "/"}) require.NoError(t, err) // unwrapping returns in original order - require.Equal(t, []FS{testFS2, testFS1}, rootFS.(*CompositeFS).FS()) + require.Equal(t, []fsapi.FS{testFS2, testFS1}, rootFS.(*CompositeFS).FS()) require.Equal(t, []string{"/tmp", "/"}, rootFS.(*CompositeFS).GuestPaths()) // Should be a composite filesystem @@ -125,7 +126,7 @@ func TestRootFS_String(t *testing.T) { tmpFS := NewDirFS(".") rootFS := NewDirFS(".") - testFS, err := NewRootFS([]FS{rootFS, tmpFS}, []string{"/", "/tmp"}) + testFS, err := NewRootFS([]fsapi.FS{rootFS, tmpFS}, []string{"/", "/tmp"}) require.NoError(t, err) require.Equal(t, "[.:/ .:/tmp]", testFS.String()) @@ -134,14 +135,14 @@ func TestRootFS_String(t *testing.T) { func TestRootFS_Open(t *testing.T) { tmpDir := t.TempDir() - // Create a subdirectory, so we can test reads outside the FS root. + // Create a subdirectory, so we can test reads outside the fsapi.FS root. tmpDir = path.Join(tmpDir, t.Name()) require.NoError(t, os.Mkdir(tmpDir, 0o700)) require.NoError(t, fstest.WriteTestFiles(tmpDir)) testRootFS := NewDirFS(tmpDir) testDirFS := NewDirFS(t.TempDir()) - testFS, err := NewRootFS([]FS{testRootFS, testDirFS}, []string{"/", "/emptydir"}) + testFS, err := NewRootFS([]fsapi.FS{testRootFS, testDirFS}, []string{"/", "/emptydir"}) require.NoError(t, err) testOpen_Read(t, testFS, true) @@ -151,7 +152,7 @@ func TestRootFS_Open(t *testing.T) { t.Run("path outside root valid", func(t *testing.T) { _, err := testFS.OpenFile("../foo", os.O_RDONLY, 0) - // syscall.FS allows relative path lookups + // fsapi.FS allows relative path lookups require.True(t, errors.Is(err, fs.ErrNotExist)) }) } @@ -161,7 +162,7 @@ func TestRootFS_Stat(t *testing.T) { require.NoError(t, fstest.WriteTestFiles(tmpDir)) tmpFS := NewDirFS(t.TempDir()) - testFS, err := NewRootFS([]FS{NewDirFS(tmpDir), tmpFS}, []string{"/", "/tmp"}) + testFS, err := NewRootFS([]fsapi.FS{NewDirFS(tmpDir), tmpFS}, []string{"/", "/tmp"}) require.NoError(t, err) testStat(t, testFS) } @@ -169,7 +170,7 @@ func TestRootFS_Stat(t *testing.T) { func TestRootFS_examples(t *testing.T) { tests := []struct { name string - fs []FS + fs []fsapi.FS guestPaths []string expected, unexpected []string }{ @@ -178,7 +179,7 @@ func TestRootFS_examples(t *testing.T) { // $ wazero run -mount=src/text/template:/ template.wasm -test.v { name: "go test text/template", - fs: []FS{ + fs: []fsapi.FS{ &adapter{fs: testfs.FS{"go-example-stdout-ExampleTemplate-0.txt": &testfs.File{}}}, &adapter{fs: testfs.FS{"testdata/file1.tmpl": &testfs.File{}}}, }, @@ -191,7 +192,7 @@ func TestRootFS_examples(t *testing.T) { // $ wazero run -mount=$(go env GOROOT)/src/compress/flate:/ flate.wasm -test.v { name: "tinygo test compress/flate", - fs: []FS{ + fs: []fsapi.FS{ &adapter{fs: testfs.FS{}}, &adapter{fs: testfs.FS{"testdata/e.txt": &testfs.File{}}}, &adapter{fs: testfs.FS{"testdata/Isaac.Newton-Opticks.txt": &testfs.File{}}}, @@ -205,7 +206,7 @@ func TestRootFS_examples(t *testing.T) { // $ wazero run -mount=src/net:/ net.wasm -test.v -test.short { name: "go test net", - fs: []FS{ + fs: []fsapi.FS{ &adapter{fs: testfs.FS{"services": &testfs.File{}}}, &adapter{fs: testfs.FS{"testdata/aliases": &testfs.File{}}}, }, @@ -219,7 +220,7 @@ func TestRootFS_examples(t *testing.T) { // -env=PYTHONPATH=/opt/wasi-python/lib/python3.11 opt/wasi-python/bin/python3.wasm { name: "python", - fs: []FS{ + fs: []fsapi.FS{ &adapter{fs: gofstest.MapFS{ // to allow resolution of "." "pybuilddir.txt": &gofstest.MapFile{}, "opt/wasi-python/lib/python3.11/__phello__/__init__.py": &gofstest.MapFile{}, @@ -237,7 +238,7 @@ func TestRootFS_examples(t *testing.T) { // --test-cmd-bin -target wasm32-wasi --zig-lib-dir ./lib ./lib/std/std.zig { name: "zig", - fs: []FS{ + fs: []fsapi.FS{ &adapter{fs: testfs.FS{"zig-cache": &testfs.File{}}}, &adapter{fs: testfs.FS{"qSQRrUkgJX9L20mr": &testfs.File{}}}, }, diff --git a/internal/platform/select.go b/internal/sysfs/select.go similarity index 89% rename from internal/platform/select.go rename to internal/sysfs/select.go index 7325b4b0..ac0861fd 100644 --- a/internal/platform/select.go +++ b/internal/sysfs/select.go @@ -1,6 +1,10 @@ -package platform +package sysfs -import "time" +import ( + "time" + + "github.com/tetratelabs/wazero/internal/platform" +) // _select exposes the select(2) syscall. This is named as such to avoid // colliding with they keyword select while not exporting the function. @@ -27,6 +31,6 @@ import "time" // e.g. the read-end of a pipe or an eventfd on Linux. // When the context is canceled, we may unblock a Select call by writing to the fd, causing it to return immediately. // This however requires to do a bit of housekeeping to hide the "special" FD from the end-user. -func _select(n int, r, w, e *FdSet, timeout *time.Duration) (int, error) { +func _select(n int, r, w, e *platform.FdSet, timeout *time.Duration) (int, error) { return syscall_select(n, r, w, e, timeout) } diff --git a/internal/platform/select_darwin.go b/internal/sysfs/select_darwin.go similarity index 88% rename from internal/platform/select_darwin.go rename to internal/sysfs/select_darwin.go index 9e29fca7..eabf4f45 100644 --- a/internal/platform/select_darwin.go +++ b/internal/sysfs/select_darwin.go @@ -1,15 +1,17 @@ -package platform +package sysfs import ( "syscall" "time" "unsafe" + + "github.com/tetratelabs/wazero/internal/platform" ) // syscall_select invokes select on Darwin, with the given timeout Duration. // We implement our own version instead of relying on syscall.Select because the latter // only returns the error and discards the result. -func syscall_select(n int, r, w, e *FdSet, timeout *time.Duration) (int, error) { +func syscall_select(n int, r, w, e *platform.FdSet, timeout *time.Duration) (int, error) { var t *syscall.Timeval if timeout != nil { tv := syscall.NsecToTimeval(timeout.Nanoseconds()) diff --git a/internal/platform/select_darwin.s b/internal/sysfs/select_darwin.s similarity index 100% rename from internal/platform/select_darwin.s rename to internal/sysfs/select_darwin.s diff --git a/internal/platform/select_linux.go b/internal/sysfs/select_linux.go similarity index 67% rename from internal/platform/select_linux.go rename to internal/sysfs/select_linux.go index 8a38bec0..aae5e48f 100644 --- a/internal/platform/select_linux.go +++ b/internal/sysfs/select_linux.go @@ -1,12 +1,14 @@ -package platform +package sysfs import ( "syscall" "time" + + "github.com/tetratelabs/wazero/internal/platform" ) // syscall_select invokes select on Unix (unless Darwin), with the given timeout Duration. -func syscall_select(n int, r, w, e *FdSet, timeout *time.Duration) (int, error) { +func syscall_select(n int, r, w, e *platform.FdSet, timeout *time.Duration) (int, error) { var t *syscall.Timeval if timeout != nil { tv := syscall.NsecToTimeval(timeout.Nanoseconds()) diff --git a/internal/platform/select_test.go b/internal/sysfs/select_test.go similarity index 95% rename from internal/platform/select_test.go rename to internal/sysfs/select_test.go index fff86461..50d2e690 100644 --- a/internal/platform/select_test.go +++ b/internal/sysfs/select_test.go @@ -1,4 +1,4 @@ -package platform +package sysfs import ( "os" @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/testing/require" ) @@ -68,7 +69,7 @@ func TestSelect(t *testing.T) { _, err = ww.Write([]byte("TEST")) require.NoError(t, err) - rFdSet := &FdSet{} + rFdSet := &platform.FdSet{} fd := int(rr.Fd()) rFdSet.Set(fd) diff --git a/internal/sysfs/select_unsupported.go b/internal/sysfs/select_unsupported.go new file mode 100644 index 00000000..5244374b --- /dev/null +++ b/internal/sysfs/select_unsupported.go @@ -0,0 +1,14 @@ +//go:build !darwin && !linux && !windows + +package sysfs + +import ( + "syscall" + "time" + + "github.com/tetratelabs/wazero/internal/platform" +) + +func syscall_select(n int, r, w, e *platform.FdSet, timeout *time.Duration) (int, error) { + return -1, syscall.ENOSYS +} diff --git a/internal/platform/select_windows.go b/internal/sysfs/select_windows.go similarity index 95% rename from internal/platform/select_windows.go rename to internal/sysfs/select_windows.go index cab24e5c..811c648d 100644 --- a/internal/platform/select_windows.go +++ b/internal/sysfs/select_windows.go @@ -1,10 +1,12 @@ -package platform +package sysfs import ( "context" "syscall" "time" "unsafe" + + "github.com/tetratelabs/wazero/internal/platform" ) // wasiFdStdin is the constant value for stdin on Wasi. @@ -14,6 +16,8 @@ const wasiFdStdin = 0 // pollInterval is the interval between each calls to peekNamedPipe in pollNamedPipe const pollInterval = 100 * time.Millisecond +var kernel32 = syscall.NewLazyDLL("kernel32.dll") + // procPeekNamedPipe is the syscall.LazyProc in kernel32 for PeekNamedPipe var procPeekNamedPipe = kernel32.NewProc("PeekNamedPipe") @@ -29,7 +33,7 @@ var procPeekNamedPipe = kernel32.NewProc("PeekNamedPipe") // PeekNamedPipe: https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe // "GetFileType can assist in determining what device type the handle refers to. A console handle presents as FILE_TYPE_CHAR." // https://learn.microsoft.com/en-us/windows/console/console-handles -func syscall_select(n int, r, w, e *FdSet, timeout *time.Duration) (int, error) { +func syscall_select(n int, r, w, e *platform.FdSet, timeout *time.Duration) (int, error) { if n == 0 { // Don't block indefinitely. if timeout == nil { diff --git a/internal/platform/select_windows_test.go b/internal/sysfs/select_windows_test.go similarity index 99% rename from internal/platform/select_windows_test.go rename to internal/sysfs/select_windows_test.go index 561eb9cf..8a53565a 100644 --- a/internal/platform/select_windows_test.go +++ b/internal/sysfs/select_windows_test.go @@ -1,4 +1,4 @@ -package platform +package sysfs import ( "context" diff --git a/internal/sysfs/stat.go b/internal/sysfs/stat.go new file mode 100644 index 00000000..60690fd9 --- /dev/null +++ b/internal/sysfs/stat.go @@ -0,0 +1,32 @@ +package sysfs + +import ( + "io/fs" + "os" + "syscall" + + "github.com/tetratelabs/wazero/internal/fsapi" + "github.com/tetratelabs/wazero/internal/platform" +) + +func defaultStatFile(f *os.File) (fsapi.Stat_t, syscall.Errno) { + if t, err := f.Stat(); err != nil { + return fsapi.Stat_t{}, platform.UnwrapOSError(err) + } else { + return statFromFileInfo(t), 0 + } +} + +func StatFromDefaultFileInfo(t fs.FileInfo) fsapi.Stat_t { + st := fsapi.Stat_t{} + st.Ino = 0 + st.Dev = 0 + st.Mode = t.Mode() + st.Nlink = 1 + st.Size = t.Size() + mtim := t.ModTime().UnixNano() // Set all times to the mod time + st.Atim = mtim + st.Mtim = mtim + st.Ctim = mtim + return st +} diff --git a/internal/platform/stat_bsd.go b/internal/sysfs/stat_bsd.go similarity index 62% rename from internal/platform/stat_bsd.go rename to internal/sysfs/stat_bsd.go index e6f404d7..8297e850 100644 --- a/internal/platform/stat_bsd.go +++ b/internal/sysfs/stat_bsd.go @@ -1,30 +1,33 @@ //go:build (amd64 || arm64) && (darwin || freebsd) -package platform +package sysfs import ( "io/fs" "os" "syscall" + + "github.com/tetratelabs/wazero/internal/fsapi" + "github.com/tetratelabs/wazero/internal/platform" ) -func lstat(path string) (Stat_t, syscall.Errno) { +func lstat(path string) (fsapi.Stat_t, syscall.Errno) { if t, err := os.Lstat(path); err != nil { - return Stat_t{}, UnwrapOSError(err) + return fsapi.Stat_t{}, platform.UnwrapOSError(err) } else { return statFromFileInfo(t), 0 } } -func stat(path string) (Stat_t, syscall.Errno) { +func stat(path string) (fsapi.Stat_t, syscall.Errno) { if t, err := os.Stat(path); err != nil { - return Stat_t{}, UnwrapOSError(err) + return fsapi.Stat_t{}, platform.UnwrapOSError(err) } else { return statFromFileInfo(t), 0 } } -func statFile(f *os.File) (Stat_t, syscall.Errno) { +func statFile(f *os.File) (fsapi.Stat_t, syscall.Errno) { return defaultStatFile(f) } @@ -35,9 +38,9 @@ func inoFromFileInfo(_ string, t fs.FileInfo) (ino uint64, err syscall.Errno) { return } -func statFromFileInfo(t fs.FileInfo) Stat_t { +func statFromFileInfo(t fs.FileInfo) fsapi.Stat_t { if d, ok := t.Sys().(*syscall.Stat_t); ok { - st := Stat_t{} + st := fsapi.Stat_t{} st.Dev = uint64(d.Dev) st.Ino = d.Ino st.Uid = d.Uid @@ -53,5 +56,5 @@ func statFromFileInfo(t fs.FileInfo) Stat_t { st.Ctim = ctime.Sec*1e9 + ctime.Nsec return st } - return statFromDefaultFileInfo(t) + return StatFromDefaultFileInfo(t) } diff --git a/internal/platform/stat_linux.go b/internal/sysfs/stat_linux.go similarity index 66% rename from internal/platform/stat_linux.go rename to internal/sysfs/stat_linux.go index 2fe256c2..e90f29b5 100644 --- a/internal/platform/stat_linux.go +++ b/internal/sysfs/stat_linux.go @@ -3,31 +3,34 @@ // Note: This expression is not the same as compiler support, even if it looks // similar. Platform functions here are used in interpreter mode as well. -package platform +package sysfs import ( "io/fs" "os" "syscall" + + "github.com/tetratelabs/wazero/internal/fsapi" + "github.com/tetratelabs/wazero/internal/platform" ) -func lstat(path string) (Stat_t, syscall.Errno) { +func lstat(path string) (fsapi.Stat_t, syscall.Errno) { if t, err := os.Lstat(path); err != nil { - return Stat_t{}, UnwrapOSError(err) + return fsapi.Stat_t{}, platform.UnwrapOSError(err) } else { return statFromFileInfo(t), 0 } } -func stat(path string) (Stat_t, syscall.Errno) { +func stat(path string) (fsapi.Stat_t, syscall.Errno) { if t, err := os.Stat(path); err != nil { - return Stat_t{}, UnwrapOSError(err) + return fsapi.Stat_t{}, platform.UnwrapOSError(err) } else { return statFromFileInfo(t), 0 } } -func statFile(f *os.File) (Stat_t, syscall.Errno) { +func statFile(f *os.File) (fsapi.Stat_t, syscall.Errno) { return defaultStatFile(f) } @@ -38,9 +41,9 @@ func inoFromFileInfo(_ string, t fs.FileInfo) (ino uint64, err syscall.Errno) { return } -func statFromFileInfo(t fs.FileInfo) Stat_t { +func statFromFileInfo(t fs.FileInfo) fsapi.Stat_t { if d, ok := t.Sys().(*syscall.Stat_t); ok { - st := Stat_t{} + st := fsapi.Stat_t{} st.Dev = uint64(d.Dev) st.Ino = uint64(d.Ino) st.Uid = d.Uid @@ -56,5 +59,5 @@ func statFromFileInfo(t fs.FileInfo) Stat_t { st.Ctim = ctime.Sec*1e9 + ctime.Nsec return st } - return statFromDefaultFileInfo(t) + return StatFromDefaultFileInfo(t) } diff --git a/internal/platform/stat_test.go b/internal/sysfs/stat_test.go similarity index 74% rename from internal/platform/stat_test.go rename to internal/sysfs/stat_test.go index 2b0bb837..30024517 100644 --- a/internal/platform/stat_test.go +++ b/internal/sysfs/stat_test.go @@ -1,7 +1,6 @@ -package platform +package sysfs import ( - "io/fs" "os" "path" "runtime" @@ -9,100 +8,23 @@ import ( "testing" "time" + "github.com/tetratelabs/wazero/internal/fsapi" + "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/testing/require" ) -func TestLstat(t *testing.T) { - tmpDir := t.TempDir() - - _, errno := Lstat(path.Join(tmpDir, "cat")) - require.EqualErrno(t, syscall.ENOENT, errno) - _, errno = Lstat(path.Join(tmpDir, "sub/cat")) - require.EqualErrno(t, syscall.ENOENT, errno) - - var st Stat_t - t.Run("dir", func(t *testing.T) { - st, errno = Lstat(tmpDir) - require.EqualErrno(t, 0, errno) - - require.True(t, st.Mode.IsDir()) - require.NotEqual(t, uint64(0), st.Ino) - }) - - file := path.Join(tmpDir, "file") - var stFile Stat_t - - t.Run("file", func(t *testing.T) { - require.NoError(t, os.WriteFile(file, []byte{1, 2}, 0o400)) - stFile, errno = Lstat(file) - require.EqualErrno(t, 0, errno) - - require.Zero(t, stFile.Mode.Type()) - require.Equal(t, int64(2), stFile.Size) - require.NotEqual(t, uint64(0), stFile.Ino) - }) - - t.Run("link to file", func(t *testing.T) { - requireLinkStat(t, file, stFile) - }) - - subdir := path.Join(tmpDir, "sub") - var stSubdir Stat_t - t.Run("subdir", func(t *testing.T) { - require.NoError(t, os.Mkdir(subdir, 0o500)) - - stSubdir, errno = Lstat(subdir) - require.EqualErrno(t, 0, errno) - - require.True(t, stSubdir.Mode.IsDir()) - require.NotEqual(t, uint64(0), stSubdir.Ino) - }) - - t.Run("link to dir", func(t *testing.T) { - requireLinkStat(t, subdir, stSubdir) - }) - - t.Run("link to dir link", func(t *testing.T) { - pathLink := subdir + "-link" - stLink, errno := Lstat(pathLink) - require.EqualErrno(t, 0, errno) - - requireLinkStat(t, pathLink, stLink) - }) -} - -func requireLinkStat(t *testing.T, path string, stat Stat_t) { - link := path + "-link" - require.NoError(t, os.Symlink(path, link)) - - stLink, errno := Lstat(link) - require.EqualErrno(t, 0, errno) - - require.NotEqual(t, uint64(0), stLink.Ino) - require.NotEqual(t, stat.Ino, stLink.Ino) // inodes are not equal - require.Equal(t, fs.ModeSymlink, stLink.Mode.Type()) - // From https://linux.die.net/man/2/lstat: - // The size of a symbolic link is the length of the pathname it - // contains, without a terminating null byte. - if runtime.GOOS == "windows" { // size is zero, not the path length - require.Zero(t, stLink.Size) - } else { - require.Equal(t, int64(len(path)), stLink.Size) - } -} - func TestStat(t *testing.T) { tmpDir := t.TempDir() - _, errno := Stat(path.Join(tmpDir, "cat")) + _, errno := stat(path.Join(tmpDir, "cat")) require.EqualErrno(t, syscall.ENOENT, errno) - _, errno = Stat(path.Join(tmpDir, "sub/cat")) + _, errno = stat(path.Join(tmpDir, "sub/cat")) require.EqualErrno(t, syscall.ENOENT, errno) - var st Stat_t + var st fsapi.Stat_t t.Run("dir", func(t *testing.T) { - st, errno = Stat(tmpDir) + st, errno = stat(tmpDir) require.EqualErrno(t, 0, errno) require.True(t, st.Mode.IsDir()) @@ -110,12 +32,12 @@ func TestStat(t *testing.T) { }) file := path.Join(tmpDir, "file") - var stFile Stat_t + var stFile fsapi.Stat_t t.Run("file", func(t *testing.T) { require.NoError(t, os.WriteFile(file, nil, 0o400)) - stFile, errno = Stat(file) + stFile, errno = stat(file) require.EqualErrno(t, 0, errno) require.False(t, stFile.Mode.IsDir()) @@ -126,18 +48,18 @@ func TestStat(t *testing.T) { link := path.Join(tmpDir, "file-link") require.NoError(t, os.Symlink(file, link)) - stLink, errno := Stat(link) + stLink, errno := stat(link) require.EqualErrno(t, 0, errno) require.Equal(t, stFile, stLink) // resolves to the file }) subdir := path.Join(tmpDir, "sub") - var stSubdir Stat_t + var stSubdir fsapi.Stat_t t.Run("subdir", func(t *testing.T) { require.NoError(t, os.Mkdir(subdir, 0o500)) - stSubdir, errno = Stat(subdir) + stSubdir, errno = stat(subdir) require.EqualErrno(t, 0, errno) require.True(t, stSubdir.Mode.IsDir()) @@ -148,7 +70,7 @@ func TestStat(t *testing.T) { link := path.Join(tmpDir, "dir-link") require.NoError(t, os.Symlink(subdir, link)) - stLink, errno := Stat(link) + stLink, errno := stat(link) require.EqualErrno(t, 0, errno) require.Equal(t, stSubdir, stLink) // resolves to the dir @@ -333,10 +255,10 @@ func TestStatFile_dev_inode(t *testing.T) { require.Equal(t, st1.Ino, st1Again.Ino) } -func requireDirectoryDevIno(t *testing.T, st Stat_t) { +func requireDirectoryDevIno(t *testing.T, st fsapi.Stat_t) { // windows before go 1.20 has trouble reading the inode information on // directories. - if runtime.GOOS != "windows" || IsGo120 { + if runtime.GOOS != "windows" || platform.IsGo120 { require.NotEqual(t, uint64(0), st.Dev) require.NotEqual(t, uint64(0), st.Ino) } else { @@ -363,7 +285,7 @@ func TestStat_uid_gid(t *testing.T) { require.NoError(t, os.Mkdir(dir, 0o0777)) require.EqualErrno(t, 0, chgid(dir, gid)) - st, errno := Stat(dir) + st, errno := stat(dir) require.EqualErrno(t, 0, errno) require.Equal(t, uid, st.Uid) @@ -376,20 +298,20 @@ func TestStat_uid_gid(t *testing.T) { require.NoError(t, os.Symlink(tmpDir, link)) require.EqualErrno(t, 0, chgid(link, gid)) - st, errno := Lstat(link) + st, errno := lstat(link) require.EqualErrno(t, 0, errno) require.Equal(t, uid, st.Uid) require.Equal(t, gid, st.Gid) }) - t.Run("StatFile", func(t *testing.T) { + t.Run("statFile", func(t *testing.T) { tmpDir := t.TempDir() file := path.Join(tmpDir, "file") require.NoError(t, os.WriteFile(file, nil, 0o0666)) require.EqualErrno(t, 0, chgid(file, gid)) - st, errno := Lstat(file) + st, errno := lstat(file) require.EqualErrno(t, 0, errno) require.Equal(t, uid, st.Uid) diff --git a/internal/sysfs/stat_unsupported.go b/internal/sysfs/stat_unsupported.go new file mode 100644 index 00000000..4c2b7055 --- /dev/null +++ b/internal/sysfs/stat_unsupported.go @@ -0,0 +1,42 @@ +//go:build (!((amd64 || arm64 || riscv64) && linux) && !((amd64 || arm64) && (darwin || freebsd)) && !((amd64 || arm64) && windows)) || js + +package sysfs + +import ( + "io/fs" + "os" + "syscall" + + "github.com/tetratelabs/wazero/internal/fsapi" + "github.com/tetratelabs/wazero/internal/platform" +) + +func lstat(path string) (fsapi.Stat_t, syscall.Errno) { + t, err := os.Lstat(path) + if errno := platform.UnwrapOSError(err); errno == 0 { + return statFromFileInfo(t), 0 + } else { + return fsapi.Stat_t{}, errno + } +} + +func stat(path string) (fsapi.Stat_t, syscall.Errno) { + t, err := os.Stat(path) + if errno := platform.UnwrapOSError(err); errno == 0 { + return statFromFileInfo(t), 0 + } else { + return fsapi.Stat_t{}, errno + } +} + +func statFile(f *os.File) (fsapi.Stat_t, syscall.Errno) { + return defaultStatFile(f) +} + +func inoFromFileInfo(_ string, t fs.FileInfo) (ino uint64, err syscall.Errno) { + return +} + +func statFromFileInfo(t fs.FileInfo) fsapi.Stat_t { + return StatFromDefaultFileInfo(t) +} diff --git a/internal/platform/stat_windows.go b/internal/sysfs/stat_windows.go similarity index 79% rename from internal/platform/stat_windows.go rename to internal/sysfs/stat_windows.go index 9a938f5f..4b05727e 100644 --- a/internal/platform/stat_windows.go +++ b/internal/sysfs/stat_windows.go @@ -1,15 +1,18 @@ //go:build (amd64 || arm64) && windows -package platform +package sysfs import ( "io/fs" "os" "path" "syscall" + + "github.com/tetratelabs/wazero/internal/fsapi" + "github.com/tetratelabs/wazero/internal/platform" ) -func lstat(path string) (Stat_t, syscall.Errno) { +func lstat(path string) (fsapi.Stat_t, syscall.Errno) { attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS) // Use FILE_FLAG_OPEN_REPARSE_POINT, otherwise CreateFile will follow symlink. // See https://docs.microsoft.com/en-us/windows/desktop/FileIO/symbolic-link-effects-on-file-systems-functions#createfile-and-createfiletransacted @@ -17,18 +20,18 @@ func lstat(path string) (Stat_t, syscall.Errno) { return statPath(attrs, path) } -func stat(path string) (Stat_t, syscall.Errno) { +func stat(path string) (fsapi.Stat_t, syscall.Errno) { attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS) return statPath(attrs, path) } -func statPath(createFileAttrs uint32, path string) (Stat_t, syscall.Errno) { +func statPath(createFileAttrs uint32, path string) (fsapi.Stat_t, syscall.Errno) { if len(path) == 0 { - return Stat_t{}, syscall.ENOENT + return fsapi.Stat_t{}, syscall.ENOENT } pathp, err := syscall.UTF16PtrFromString(path) if err != nil { - return Stat_t{}, syscall.EINVAL + return fsapi.Stat_t{}, syscall.EINVAL } // open the file handle @@ -40,14 +43,14 @@ func statPath(createFileAttrs uint32, path string) (Stat_t, syscall.Errno) { if err == syscall.ENOTDIR { err = syscall.ENOENT } - return Stat_t{}, UnwrapOSError(err) + return fsapi.Stat_t{}, platform.UnwrapOSError(err) } defer syscall.CloseHandle(h) return statHandle(h) } -func statFile(f *os.File) (Stat_t, syscall.Errno) { +func statFile(f *os.File) (fsapi.Stat_t, syscall.Errno) { // Attempt to get the stat by handle, which works for normal files st, err := statHandle(syscall.Handle(f.Fd())) @@ -71,16 +74,16 @@ func inoFromFileInfo(filePath string, t fs.FileInfo) (ino uint64, errno syscall. } // ino is no not in Win32FileAttributeData inoPath := path.Clean(path.Join(filePath, t.Name())) - var st Stat_t - if st, errno = Lstat(inoPath); errno == 0 { + var st fsapi.Stat_t + if st, errno = lstat(inoPath); errno == 0 { ino = st.Ino } return } -func statFromFileInfo(t fs.FileInfo) Stat_t { +func statFromFileInfo(t fs.FileInfo) fsapi.Stat_t { if d, ok := t.Sys().(*syscall.Win32FileAttributeData); ok { - st := Stat_t{} + st := fsapi.Stat_t{} st.Ino = 0 // not in Win32FileAttributeData st.Dev = 0 // not in Win32FileAttributeData st.Mode = t.Mode() @@ -91,19 +94,19 @@ func statFromFileInfo(t fs.FileInfo) Stat_t { st.Ctim = d.CreationTime.Nanoseconds() return st } else { - return statFromDefaultFileInfo(t) + return StatFromDefaultFileInfo(t) } } -func statHandle(h syscall.Handle) (Stat_t, syscall.Errno) { +func statHandle(h syscall.Handle) (fsapi.Stat_t, syscall.Errno) { winFt, err := syscall.GetFileType(h) if err != nil { - return Stat_t{}, UnwrapOSError(err) + return fsapi.Stat_t{}, platform.UnwrapOSError(err) } var fi syscall.ByHandleFileInformation if err = syscall.GetFileInformationByHandle(h, &fi); err != nil { - return Stat_t{}, UnwrapOSError(err) + return fsapi.Stat_t{}, platform.UnwrapOSError(err) } var m fs.FileMode @@ -124,7 +127,7 @@ func statHandle(h syscall.Handle) (Stat_t, syscall.Errno) { m |= fs.ModeDir | 0o111 // e.g. 0o444 -> 0o555 } - st := Stat_t{} + st := fsapi.Stat_t{} // 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 st.Dev = uint64(fi.VolumeSerialNumber) diff --git a/internal/sysfs/sync.go b/internal/sysfs/sync.go new file mode 100644 index 00000000..edb673c1 --- /dev/null +++ b/internal/sysfs/sync.go @@ -0,0 +1,14 @@ +//go:build !windows + +package sysfs + +import ( + "os" + "syscall" + + "github.com/tetratelabs/wazero/internal/platform" +) + +func sync(f *os.File) syscall.Errno { + return platform.UnwrapOSError(f.Sync()) +} diff --git a/internal/platform/sync_windows.go b/internal/sysfs/sync_windows.go similarity index 74% rename from internal/platform/sync_windows.go rename to internal/sysfs/sync_windows.go index 534f7936..6a30957a 100644 --- a/internal/platform/sync_windows.go +++ b/internal/sysfs/sync_windows.go @@ -1,12 +1,14 @@ -package platform +package sysfs import ( "os" "syscall" + + "github.com/tetratelabs/wazero/internal/platform" ) func sync(f *os.File) syscall.Errno { - errno := UnwrapOSError(f.Sync()) + errno := platform.UnwrapOSError(f.Sync()) // Coerce error performing stat on a directory to 0, as it won't work // on Windows. switch errno { diff --git a/internal/platform/syscall6_darwin.go b/internal/sysfs/syscall6_darwin.go similarity index 95% rename from internal/platform/syscall6_darwin.go rename to internal/sysfs/syscall6_darwin.go index 273fb7dd..9fde5baa 100644 --- a/internal/platform/syscall6_darwin.go +++ b/internal/sysfs/syscall6_darwin.go @@ -1,4 +1,4 @@ -package platform +package sysfs import ( "syscall" diff --git a/internal/sysfs/sysfs.go b/internal/sysfs/sysfs.go index 2a697ba5..e0ebfe5b 100644 --- a/internal/sysfs/sysfs.go +++ b/internal/sysfs/sysfs.go @@ -4,369 +4,3 @@ // The name sysfs was chosen because wazero's public API has a "sys" package, // which was named after https://github.com/golang/sys. package sysfs - -import ( - "io/fs" - "syscall" - - "github.com/tetratelabs/wazero/internal/platform" -) - -// FS is a writeable fs.FS bridge backed by syscall functions needed for ABI -// including WASI and runtime.GOOS=js. -// -// Implementations should embed UnimplementedFS for forward compatability. Any -// unsupported method or parameter should return syscall.ENOSYS. -// -// # Errors -// -// All methods that can return an error return a syscall.Errno, which is zero -// on success. -// -// Restricting to syscall.Errno matches current WebAssembly host functions, -// which are constrained to well-known error codes. For example, `GOOS=js` maps -// hard coded values and panics otherwise. More commonly, WASI maps syscall -// errors to u32 numeric values. -// -// # Notes -// -// A writable filesystem abstraction is not yet implemented as of Go 1.20. See -// https://github.com/golang/go/issues/45757 -type FS interface { - // String should return a human-readable format of the filesystem - // - // For example, if this filesystem is backed by the real directory - // "/tmp/wasm", the expected value is "/tmp/wasm". - // - // When the host filesystem isn't a real filesystem, substitute a symbolic, - // human-readable name. e.g. "virtual" - String() string - - // OpenFile opens a file. It should be closed via Close on platform.File. - // - // # Errors - // - // A zero syscall.Errno is success. The below are expected otherwise: - // - syscall.ENOSYS: the implementation does not support this function. - // - syscall.EINVAL: `path` or `flag` is invalid. - // - syscall.EISDIR: the path was a directory, but flag included - // syscall.O_RDWR or syscall.O_WRONLY - // - syscall.ENOENT: `path` doesn't exist and `flag` doesn't contain - // os.O_CREATE. - // - // # Constraints on the returned file - // - // Implementations that can read flags should enforce them regardless of - // the type returned. For example, while os.File implements io.Writer, - // attempts to write to a directory or a file opened with os.O_RDONLY fail - // with a syscall.EBADF. - // - // Some implementations choose whether to enforce read-only opens, namely - // fs.FS. While fs.FS is supported (Adapt), wazero cannot runtime enforce - // open flags. Instead, we encourage good behavior and test our built-in - // implementations. - // - // # Notes - // - // - This is like os.OpenFile, except the path is relative to this file - // system, and syscall.Errno is returned instead of os.PathError. - // - flag are the same as os.OpenFile, for example, os.O_CREATE. - // - Implications of permissions when os.O_CREATE are described in Chmod - // notes. - // - This is like `open` in POSIX. See - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html - OpenFile(path string, flag int, perm fs.FileMode) (platform.File, syscall.Errno) - // ^^ TODO: Consider syscall.Open, though this implies defining and - // coercing flags and perms similar to what is done in os.OpenFile. - - // Lstat gets file status without following symbolic links. - // - // # Errors - // - // A zero syscall.Errno is success. The below are expected otherwise: - // - syscall.ENOSYS: the implementation does not support this function. - // - syscall.ENOENT: `path` doesn't exist. - // - // # Notes - // - // - This is like syscall.Lstat, except the `path` is relative to this - // file system. - // - This is like `lstat` in POSIX. See - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/lstat.html - // - An fs.FileInfo backed implementation sets atim, mtim and ctim to the - // same value. - // - When the path is a symbolic link, the stat returned is for the link, - // not the file it refers to. - Lstat(path string) (platform.Stat_t, syscall.Errno) - - // Stat gets file status. - // - // # Errors - // - // A zero syscall.Errno is success. The below are expected otherwise: - // - syscall.ENOSYS: the implementation does not support this function. - // - syscall.ENOENT: `path` doesn't exist. - // - // # Notes - // - // - This is like syscall.Stat, except the `path` is relative to this - // file system. - // - This is like `stat` in POSIX. See - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/stat.html - // - An fs.FileInfo backed implementation sets atim, mtim and ctim to the - // same value. - // - When the path is a symbolic link, the stat returned is for the file - // it refers to. - Stat(path string) (platform.Stat_t, syscall.Errno) - - // Mkdir makes a directory. - // - // # Errors - // - // A zero syscall.Errno is success. The below are expected otherwise: - // - syscall.ENOSYS: the implementation does not support this function. - // - syscall.EINVAL: `path` is invalid. - // - syscall.EEXIST: `path` exists and is a directory. - // - syscall.ENOTDIR: `path` exists and is a file. - // - // # Notes - // - // - This is like syscall.Mkdir, except the `path` is relative to this - // file system. - // - This is like `mkdir` in POSIX. See - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/mkdir.html - // - Implications of permissions are described in Chmod notes. - Mkdir(path string, perm fs.FileMode) syscall.Errno - // ^^ TODO: Consider syscall.Mkdir, though this implies defining and - // coercing flags and perms similar to what is done in os.Mkdir. - - // Chmod changes the mode of the file. - // - // # Errors - // - // A zero syscall.Errno is success. The below are expected otherwise: - // - syscall.ENOSYS: the implementation does not support this function. - // - syscall.EINVAL: `path` is invalid. - // - syscall.ENOENT: `path` does not exist. - // - // # Notes - // - // - This is like syscall.Chmod, except the `path` is relative to this - // file system. - // - This is like `chmod` in POSIX. See - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/chmod.html - // - Windows ignores the execute bit, and any permissions come back as - // group and world. For example, chmod of 0400 reads back as 0444, and - // 0700 0666. Also, permissions on directories aren't supported at all. - Chmod(path string, perm fs.FileMode) syscall.Errno - - // Chown changes the owner and group of a file. - // - // # Errors - // - // A zero syscall.Errno is success. The below are expected otherwise: - // - syscall.ENOSYS: the implementation does not support this function. - // - syscall.EINVAL: `path` is invalid. - // - syscall.ENOENT: `path` does not exist. - // - // # Notes - // - // - This is like syscall.Chown, except the `path` is relative to this - // file system. - // - This is like `chown` in POSIX. See - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/chown.html - // - This always returns syscall.ENOSYS on windows. - Chown(path string, uid, gid int) syscall.Errno - - // Lchown changes the owner and group of a symbolic link. - // - // # Errors - // - // A zero syscall.Errno is success. The below are expected otherwise: - // - syscall.ENOSYS: the implementation does not support this function. - // - syscall.EINVAL: `path` is invalid. - // - syscall.ENOENT: `path` does not exist. - // - // # Notes - // - // - This is like syscall.Lchown, except the `path` is relative to this - // file system. - // - This is like `lchown` in POSIX. See - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/lchown.html - // - Windows will always return syscall.ENOSYS - Lchown(path string, uid, gid int) syscall.Errno - - // Rename renames file or directory. - // - // # Errors - // - // A zero syscall.Errno is success. The below are expected otherwise: - // - syscall.ENOSYS: the implementation does not support this function. - // - syscall.EINVAL: `from` or `to` is invalid. - // - syscall.ENOENT: `from` or `to` don't exist. - // - syscall.ENOTDIR: `from` is a directory and `to` exists as a file. - // - syscall.EISDIR: `from` is a file and `to` exists as a directory. - // - syscall.ENOTEMPTY: `both from` and `to` are existing directory, but - // `to` is not empty. - // - // # Notes - // - // - This is like syscall.Rename, except the paths are relative to this - // file system. - // - This is like `rename` in POSIX. See - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/rename.html - // - Windows doesn't let you overwrite an existing directory. - Rename(from, to string) syscall.Errno - - // Rmdir removes a directory. - // - // # Errors - // - // A zero syscall.Errno is success. The below are expected otherwise: - // - syscall.ENOSYS: the implementation does not support this function. - // - syscall.EINVAL: `path` is invalid. - // - syscall.ENOENT: `path` doesn't exist. - // - syscall.ENOTDIR: `path` exists, but isn't a directory. - // - syscall.ENOTEMPTY: `path` exists, but isn't empty. - // - // # Notes - // - // - This is like syscall.Rmdir, except the `path` is relative to this - // file system. - // - This is like `rmdir` in POSIX. See - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/rmdir.html - // - As of Go 1.19, Windows maps syscall.ENOTDIR to syscall.ENOENT. - Rmdir(path string) syscall.Errno - - // Unlink removes a directory entry. - // - // # Errors - // - // A zero syscall.Errno is success. The below are expected otherwise: - // - syscall.ENOSYS: the implementation does not support this function. - // - syscall.EINVAL: `path` is invalid. - // - syscall.ENOENT: `path` doesn't exist. - // - syscall.EISDIR: `path` exists, but is a directory. - // - // # Notes - // - // - This is like syscall.Unlink, except the `path` is relative to this - // file system. - // - This is like `unlink` in POSIX. See - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/unlink.html - // - On Windows, syscall.Unlink doesn't delete symlink to directory unlike other platforms. Implementations might - // want to combine syscall.RemoveDirectory with syscall.Unlink in order to delete such links on Windows. - // See https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-removedirectorya - Unlink(path string) syscall.Errno - - // Link creates a "hard" link from oldPath to newPath, in contrast to a - // soft link (via Symlink). - // - // # Errors - // - // A zero syscall.Errno is success. The below are expected otherwise: - // - syscall.ENOSYS: the implementation does not support this function. - // - syscall.EPERM: `oldPath` is invalid. - // - syscall.ENOENT: `oldPath` doesn't exist. - // - syscall.EISDIR: `newPath` exists, but is a directory. - // - // # Notes - // - // - This is like syscall.Link, except the `oldPath` is relative to this - // file system. - // - This is like `link` in POSIX. See - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/link.html - Link(oldPath, newPath string) syscall.Errno - - // Symlink creates a "soft" link from oldPath to newPath, in contrast to a - // hard link (via Link). - // - // # Errors - // - // A zero syscall.Errno is success. The below are expected otherwise: - // - syscall.ENOSYS: the implementation does not support this function. - // - syscall.EPERM: `oldPath` or `newPath` is invalid. - // - syscall.EEXIST: `newPath` exists. - // - // # Notes - // - // - This is like syscall.Symlink, except the `oldPath` is relative to - // this file system. - // - This is like `symlink` in POSIX. See - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/symlink.html - // - Only `newPath` is relative to this file system and `oldPath` is kept - // as-is. That is because the link is only resolved relative to the - // directory when dereferencing it (e.g. ReadLink). - // See https://github.com/bytecodealliance/cap-std/blob/v1.0.4/cap-std/src/fs/dir.rs#L404-L409 - // for how others implement this. - // - Symlinks in Windows requires `SeCreateSymbolicLinkPrivilege`. - // Otherwise, syscall.EPERM results. - // See https://learn.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/create-symbolic-links - Symlink(oldPath, linkName string) syscall.Errno - - // Readlink reads the contents of a symbolic link. - // - // # Errors - // - // A zero syscall.Errno is success. The below are expected otherwise: - // - syscall.ENOSYS: the implementation does not support this function. - // - syscall.EINVAL: `path` is invalid. - // - // # Notes - // - // - This is like syscall.Readlink, except the path is relative to this - // filesystem. - // - This is like `readlink` in POSIX. See - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/readlink.html - // - On Windows, the path separator is different from other platforms, - // but to provide consistent results to Wasm, this normalizes to a "/" - // separator. - Readlink(path string) (string, syscall.Errno) - - // Truncate truncates a file to a specified length. - // - // # Errors - // - // A zero syscall.Errno is success. The below are expected otherwise: - // - syscall.ENOSYS: the implementation does not support this function. - // - syscall.EINVAL: `path` is invalid or size is negative. - // - syscall.ENOENT: `path` doesn't exist. - // - syscall.EISDIR: `path` is a directory. - // - syscall.EACCES: `path` doesn't have write access. - // - // # Notes - // - // - This is like syscall.Truncate, except the path is relative to this - // filesystem. - // - This is like `truncate` in POSIX. See - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/truncate.html - Truncate(path string, size int64) syscall.Errno - - // Utimens set file access and modification times on a path relative to - // this file system, at nanosecond precision. - // - // # Parameters - // - // The `times` parameter includes the access and modification timestamps to - // assign. Special syscall.Timespec NSec values platform.UTIME_NOW and - // platform.UTIME_OMIT may be specified instead of real timestamps. A nil - // `times` parameter behaves the same as if both were set to - // platform.UTIME_NOW. - // - // When the `symlinkFollow` parameter is true and the path is a symbolic link, - // the target of expanding that link is updated. - // - // # Errors - // - // A zero syscall.Errno is success. The below are expected otherwise: - // - syscall.ENOSYS: the implementation does not support this function. - // - syscall.EINVAL: `path` is invalid. - // - syscall.EEXIST: `path` exists and is a directory. - // - syscall.ENOTDIR: `path` exists and is a file. - // - // # Notes - // - // - This is like syscall.UtimesNano and `utimensat` with `AT_FDCWD` in - // POSIX. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html - Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno -} diff --git a/internal/sysfs/sysfs_test.go b/internal/sysfs/sysfs_test.go index 0be0eb84..2566ed1c 100644 --- a/internal/sysfs/sysfs_test.go +++ b/internal/sysfs/sysfs_test.go @@ -11,11 +11,12 @@ import ( "syscall" "testing" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/testing/require" ) -func testOpen_O_RDWR(t *testing.T, tmpDir string, testFS FS) { +func testOpen_O_RDWR(t *testing.T, tmpDir string, testFS fsapi.FS) { file := "file" realPath := path.Join(tmpDir, file) err := os.WriteFile(realPath, []byte{}, 0o600) @@ -68,7 +69,7 @@ func testOpen_O_RDWR(t *testing.T, tmpDir string, testFS FS) { } } -func testOpen_Read(t *testing.T, testFS FS, expectIno bool) { +func testOpen_Read(t *testing.T, testFS fsapi.FS, expectIno bool) { t.Run("doesn't exist", func(t *testing.T) { _, errno := testFS.OpenFile("nope", os.O_RDONLY, 0) @@ -88,7 +89,7 @@ func testOpen_Read(t *testing.T, testFS FS, expectIno bool) { dirents[i].Ino = 0 } - require.Equal(t, []platform.Dirent{ + require.Equal(t, []fsapi.Dirent{ {Name: "animals.txt", Type: 0}, {Name: "dir", Type: fs.ModeDir}, {Name: "empty.txt", Type: 0}, @@ -124,7 +125,7 @@ func testOpen_Read(t *testing.T, testFS FS, expectIno bool) { require.EqualErrno(t, 0, errno) require.Equal(t, 1, len(dirents3)) - dirents := []platform.Dirent{dirents1[0], dirents2[0], dirents3[0]} + dirents := []fsapi.Dirent{dirents1[0], dirents2[0], dirents3[0]} sort.Slice(dirents, func(i, j int) bool { return dirents[i].Name < dirents[j].Name }) requireIno(t, dirents, expectIno) @@ -134,7 +135,7 @@ func testOpen_Read(t *testing.T, testFS FS, expectIno bool) { dirents[i].Ino = 0 } - require.Equal(t, []platform.Dirent{ + require.Equal(t, []fsapi.Dirent{ {Name: "-", Type: 0}, {Name: "a-", Type: fs.ModeDir}, {Name: "ab-", Type: 0}, @@ -197,18 +198,18 @@ human }) t.Run("opening a directory with O_RDWR is EISDIR", func(t *testing.T) { - _, errno := testFS.OpenFile("sub", platform.O_DIRECTORY|os.O_RDWR, 0) + _, errno := testFS.OpenFile("sub", fsapi.O_DIRECTORY|os.O_RDWR, 0) require.EqualErrno(t, syscall.EISDIR, errno) }) } -func testLstat(t *testing.T, testFS FS) { +func testLstat(t *testing.T, testFS fsapi.FS) { _, errno := testFS.Lstat("cat") require.EqualErrno(t, syscall.ENOENT, errno) _, errno = testFS.Lstat("sub/cat") require.EqualErrno(t, syscall.ENOENT, errno) - var st platform.Stat_t + var st fsapi.Stat_t t.Run("dir", func(t *testing.T) { st, errno = testFS.Lstat(".") @@ -217,7 +218,7 @@ func testLstat(t *testing.T, testFS FS) { require.NotEqual(t, uint64(0), st.Ino) }) - var stFile platform.Stat_t + var stFile fsapi.Stat_t t.Run("file", func(t *testing.T) { stFile, errno = testFS.Lstat("animals.txt") @@ -232,7 +233,7 @@ func testLstat(t *testing.T, testFS FS) { requireLinkStat(t, testFS, "animals.txt", stFile) }) - var stSubdir platform.Stat_t + var stSubdir fsapi.Stat_t t.Run("subdir", func(t *testing.T) { stSubdir, errno = testFS.Lstat("sub") require.EqualErrno(t, 0, errno) @@ -254,7 +255,7 @@ func testLstat(t *testing.T, testFS FS) { }) } -func requireLinkStat(t *testing.T, testFS FS, path string, stat platform.Stat_t) { +func requireLinkStat(t *testing.T, testFS fsapi.FS, path string, stat fsapi.Stat_t) { link := path + "-link" stLink, errno := testFS.Lstat(link) require.EqualErrno(t, 0, errno) @@ -271,7 +272,7 @@ func requireLinkStat(t *testing.T, testFS FS, path string, stat platform.Stat_t) } } -func testStat(t *testing.T, testFS FS) { +func testStat(t *testing.T, testFS fsapi.FS) { _, errno := testFS.Stat("cat") require.EqualErrno(t, syscall.ENOENT, errno) _, errno = testFS.Stat("sub/cat") @@ -295,7 +296,7 @@ func testStat(t *testing.T, testFS FS) { } } -func readAll(t *testing.T, f platform.File) []byte { +func readAll(t *testing.T, f fsapi.File) []byte { st, errno := f.Stat() require.EqualErrno(t, 0, errno) buf := make([]byte, st.Size) @@ -306,7 +307,7 @@ func readAll(t *testing.T, f platform.File) []byte { // requireReaddir ensures the input file is a directory, and returns its // entries. -func requireReaddir(t *testing.T, f platform.File, n int, expectIno bool) []platform.Dirent { +func requireReaddir(t *testing.T, f fsapi.File, n int, expectIno bool) []fsapi.Dirent { entries, errno := f.Readdir(n) require.EqualErrno(t, 0, errno) @@ -315,7 +316,7 @@ func requireReaddir(t *testing.T, f platform.File, n int, expectIno bool) []plat return entries } -func testReadlink(t *testing.T, readFS, writeFS FS) { +func testReadlink(t *testing.T, readFS, writeFS fsapi.FS) { testLinks := []struct { old, dst string }{ @@ -347,7 +348,7 @@ func testReadlink(t *testing.T, readFS, writeFS FS) { }) } -func requireIno(t *testing.T, dirents []platform.Dirent, expectIno bool) { +func requireIno(t *testing.T, dirents []fsapi.Dirent, expectIno bool) { for i := range dirents { d := dirents[i] if expectIno { diff --git a/internal/sysfs/unimplemented.go b/internal/sysfs/unimplemented.go deleted file mode 100644 index 32373bcf..00000000 --- a/internal/sysfs/unimplemented.go +++ /dev/null @@ -1,97 +0,0 @@ -package sysfs - -import ( - "io/fs" - "syscall" - - "github.com/tetratelabs/wazero/internal/platform" -) - -// UnimplementedFS is an FS that returns syscall.ENOSYS for all functions, -// This should be embedded to have forward compatible implementations. -type UnimplementedFS struct{} - -// String implements fmt.Stringer -func (UnimplementedFS) String() string { - return "Unimplemented:/" -} - -// Open implements the same method as documented on fs.FS -func (UnimplementedFS) Open(name string) (fs.File, error) { - return nil, &fs.PathError{Op: "open", Path: name, Err: syscall.ENOSYS} -} - -// OpenFile implements FS.OpenFile -func (UnimplementedFS) OpenFile(path string, flag int, perm fs.FileMode) (platform.File, syscall.Errno) { - return nil, syscall.ENOSYS -} - -// Lstat implements FS.Lstat -func (UnimplementedFS) Lstat(path string) (platform.Stat_t, syscall.Errno) { - return platform.Stat_t{}, syscall.ENOSYS -} - -// Stat implements FS.Stat -func (UnimplementedFS) Stat(path string) (platform.Stat_t, syscall.Errno) { - return platform.Stat_t{}, syscall.ENOSYS -} - -// Readlink implements FS.Readlink -func (UnimplementedFS) Readlink(path string) (string, syscall.Errno) { - return "", syscall.ENOSYS -} - -// Mkdir implements FS.Mkdir -func (UnimplementedFS) Mkdir(path string, perm fs.FileMode) syscall.Errno { - return syscall.ENOSYS -} - -// Chmod implements FS.Chmod -func (UnimplementedFS) Chmod(path string, perm fs.FileMode) syscall.Errno { - return syscall.ENOSYS -} - -// Chown implements FS.Chown -func (UnimplementedFS) Chown(path string, uid, gid int) syscall.Errno { - return syscall.ENOSYS -} - -// Lchown implements FS.Lchown -func (UnimplementedFS) Lchown(path string, uid, gid int) syscall.Errno { - return syscall.ENOSYS -} - -// Rename implements FS.Rename -func (UnimplementedFS) Rename(from, to string) syscall.Errno { - return syscall.ENOSYS -} - -// Rmdir implements FS.Rmdir -func (UnimplementedFS) Rmdir(path string) syscall.Errno { - return syscall.ENOSYS -} - -// Link implements FS.Link -func (UnimplementedFS) Link(_, _ string) syscall.Errno { - return syscall.ENOSYS -} - -// Symlink implements FS.Symlink -func (UnimplementedFS) Symlink(_, _ string) syscall.Errno { - return syscall.ENOSYS -} - -// Unlink implements FS.Unlink -func (UnimplementedFS) Unlink(path string) syscall.Errno { - return syscall.ENOSYS -} - -// Utimens implements FS.Utimens -func (UnimplementedFS) Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno { - return syscall.ENOSYS -} - -// Truncate implements FS.Truncate -func (UnimplementedFS) Truncate(string, int64) syscall.Errno { - return syscall.ENOSYS -} diff --git a/internal/sysfs/unlink.go b/internal/sysfs/unlink.go new file mode 100644 index 00000000..37b74e18 --- /dev/null +++ b/internal/sysfs/unlink.go @@ -0,0 +1,17 @@ +//go:build !windows + +package sysfs + +import ( + "syscall" + + "github.com/tetratelabs/wazero/internal/platform" +) + +func Unlink(name string) (errno syscall.Errno) { + err := syscall.Unlink(name) + if errno = platform.UnwrapOSError(err); errno == syscall.EPERM { + errno = syscall.EISDIR + } + return errno +} diff --git a/internal/platform/unlink_test.go b/internal/sysfs/unlink_test.go similarity index 98% rename from internal/platform/unlink_test.go rename to internal/sysfs/unlink_test.go index cdd579b1..b44261e7 100644 --- a/internal/platform/unlink_test.go +++ b/internal/sysfs/unlink_test.go @@ -1,4 +1,4 @@ -package platform +package sysfs import ( "os" diff --git a/internal/platform/unlink_windows.go b/internal/sysfs/unlink_windows.go similarity index 68% rename from internal/platform/unlink_windows.go rename to internal/sysfs/unlink_windows.go index 5ff607ac..410ea844 100644 --- a/internal/platform/unlink_windows.go +++ b/internal/sysfs/unlink_windows.go @@ -1,10 +1,12 @@ //go:build windows -package platform +package sysfs import ( "os" "syscall" + + "github.com/tetratelabs/wazero/internal/platform" ) func Unlink(name string) syscall.Errno { @@ -12,11 +14,11 @@ func Unlink(name string) syscall.Errno { if err == nil { return 0 } - errno := UnwrapOSError(err) + errno := platform.UnwrapOSError(err) if errno == syscall.EBADF { lstat, errLstat := os.Lstat(name) if errLstat == nil && lstat.Mode()&os.ModeSymlink != 0 { - errno = UnwrapOSError(os.Remove(name)) + errno = platform.UnwrapOSError(os.Remove(name)) } else { errno = syscall.EISDIR } diff --git a/internal/wasm/module_instance_test.go b/internal/wasm/module_instance_test.go index e9eec315..3699f930 100644 --- a/internal/wasm/module_instance_test.go +++ b/internal/wasm/module_instance_test.go @@ -155,7 +155,7 @@ func TestModuleInstance_Close(t *testing.T) { m, err := s.Instantiate(testCtx, &Module{}, t.Name(), sysCtx, nil) require.NoError(t, err) - // In sysfs.FS, non syscall errors map to syscall.EIO. + // In internalapi.FS, non syscall errors map to syscall.EIO. require.EqualErrno(t, syscall.EIO, m.Close(testCtx)) // Verify our intended side-effect @@ -254,7 +254,7 @@ func TestModuleInstance_CallDynamic(t *testing.T) { m, err := s.Instantiate(testCtx, &Module{}, t.Name(), sysCtx, nil) require.NoError(t, err) - // In sysfs.FS, non syscall errors map to syscall.EIO. + // In internalapi.FS, non syscall errors map to syscall.EIO. require.EqualErrno(t, syscall.EIO, m.Close(testCtx)) // Verify our intended side-effect