From a60debc8d2bd6bff444045a2cf8c0e1decb2498a Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Mon, 30 Jan 2023 19:08:10 +0200 Subject: [PATCH] wasi: implements fd_filestat_set_size and fd_filestat_set_times (#1082) This implements fd_filestat_set_size and fd_filestat_set_times, which passes one more test in the rust wasi-testsuite. Signed-off-by: Adrian Cole Co-authored-by: Takeshi Yoneda --- imports/wasi_snapshot_preview1/clock.go | 4 +- imports/wasi_snapshot_preview1/fs.go | 105 +++++++- imports/wasi_snapshot_preview1/fs_test.go | 254 +++++++++++++++++++- imports/wasi_snapshot_preview1/wasi_test.go | 2 +- internal/gojs/fs.go | 18 +- internal/gojs/testdata/writefs/main.go | 22 ++ internal/sys/fs.go | 7 +- internal/sys/sys.go | 8 +- internal/sys/sys_test.go | 6 + internal/sysfs/adapter.go | 2 +- internal/sysfs/dirfs.go | 12 +- internal/sysfs/dirfs_test.go | 82 +++++++ internal/sysfs/rootfs.go | 13 +- internal/sysfs/syscall.go | 4 + internal/sysfs/syscall_windows.go | 22 +- internal/sysfs/sysfs.go | 37 ++- internal/sysfs/unsupported.go | 5 + internal/wasi_snapshot_preview1/fs.go | 8 + site/content/specs.md | 4 +- 19 files changed, 567 insertions(+), 48 deletions(-) diff --git a/imports/wasi_snapshot_preview1/clock.go b/imports/wasi_snapshot_preview1/clock.go index 299324ee..99fa924f 100644 --- a/imports/wasi_snapshot_preview1/clock.go +++ b/imports/wasi_snapshot_preview1/clock.go @@ -2,7 +2,6 @@ package wasi_snapshot_preview1 import ( "context" - "time" "github.com/tetratelabs/wazero/api" . "github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1" @@ -102,8 +101,7 @@ func clockTimeGetFn(_ context.Context, mod api.Module, params []uint64) Errno { var val int64 switch id { case ClockIDRealtime: - sec, nsec := sysCtx.Walltime() - val = (sec * time.Second.Nanoseconds()) + int64(nsec) + val = sysCtx.WalltimeNanos() case ClockIDMonotonic: val = sysCtx.Nanotime() default: diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go index 30596638..5def6212 100644 --- a/imports/wasi_snapshot_preview1/fs.go +++ b/imports/wasi_snapshot_preview1/fs.go @@ -284,18 +284,108 @@ func writeFilestat(buf []byte, stat fs.FileInfo) { // adjusts the size of an open file. // // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_set_sizefd-fd-size-filesize---errno -var fdFilestatSetSize = stubFunction(FdFilestatSetSizeName, []wasm.ValueType{i32, i64}, "fd", "size") +var fdFilestatSetSize = newHostFunc(FdFilestatSetSizeName, fdFilestatSetSizeFn, []wasm.ValueType{i32, i64}, "fd", "size") + +func fdFilestatSetSizeFn(_ context.Context, mod api.Module, params []uint64) Errno { + fd := uint32(params[0]) + size := uint32(params[1]) + + fsc := mod.(*wasm.CallContext).Sys.FS() + + // Check to see if the file descriptor is available + if f, ok := fsc.LookupFile(fd); !ok { + return ErrnoBadf + } else if truncater, ok := f.File.(truncater); !ok { + return ErrnoBadf // possibly a fake file + } else if err := truncater.Truncate(int64(size)); err != nil { + return ToErrno(err) + } + return ErrnoSuccess +} // fdFilestatSetTimes is the WASI function named functionFdFilestatSetTimes // which adjusts the times of an open file. // // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_set_timesfd-fd-atim-timestamp-mtim-timestamp-fst_flags-fstflags---errno -var fdFilestatSetTimes = stubFunction( - FdFilestatSetTimesName, +var fdFilestatSetTimes = newHostFunc( + FdFilestatSetTimesName, fdFilestatSetTimesFn, []wasm.ValueType{i32, i64, i64, i32}, "fd", "atim", "mtim", "fst_flags", ) +func fdFilestatSetTimesFn(_ context.Context, mod api.Module, params []uint64) Errno { + fd := uint32(params[0]) + fstFlags := uint16(params[3]) + + sys := mod.(*wasm.CallContext).Sys + fsc := sys.FS() + + f, ok := fsc.LookupFile(fd) + if !ok { + return ErrnoBadf + } + + // Unchanging a part of time spec while executing utimes is extremely complex to add support for all platforms, + // and actually there's an outstanding issue on Go + // - https://github.com/golang/go/issues/32558. + // - https://go-review.googlesource.com/c/go/+/219638 (unmerged) + // + // Here, we emulate the behavior for empty flag (meaning "do not change") by get the current time stamp + // by explicitly executing File.Stat() prior to Utimes. + var atime, mtime int64 + var nowAtime, statAtime, nowMtime, statMtime bool + if set, now := fstFlags&FileStatAdjustFlagsAtim != 0, fstFlags&FileStatAdjustFlagsAtimNow != 0; set && now { + return ErrnoInval + } else if set { + atime = int64(params[1]) + } else if now { + nowAtime = true + } else { + statAtime = true + } + if set, now := fstFlags&FileStatAdjustFlagsMtim != 0, fstFlags&FileStatAdjustFlagsMtimNow != 0; set && now { + return ErrnoInval + } else if set { + mtime = int64(params[2]) + } else if now { + nowMtime = true + } else { + statMtime = true + } + + // Handle if either parameter should be now. + if nowAtime || nowMtime { + now := sys.WalltimeNanos() + if nowAtime { + atime = now + } + if nowMtime { + mtime = now + } + } + + // Handle if either parameter should be taken from stat. + if statAtime || statMtime { + // Get the current timestamp via Stat in order to un-change after calling FS.Utimes(). + st, err := f.Stat() + if err != nil { + return ErrnoBadf + } + atimeNsec, mtimeNsec, _ := platform.StatTimes(st) + if statAtime { + atime = atimeNsec + } + if statMtime { + mtime = mtimeNsec + } + } + + if err := f.FS.Utimes(f.Name, atime, mtime); err != nil { + return ToErrno(err) + } + return ErrnoSuccess +} + // fdPread is the WASI function named FdPreadName which reads from a file // descriptor, without using and updating the file descriptor's offset. // @@ -924,17 +1014,20 @@ func fdSeekFn(_ context.Context, mod api.Module, params []uint64) Errno { // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_syncfd-fd---errno var fdSync = newHostFunc(FdSyncName, fdSyncFn, []api.ValueType{i32}, "fd") +type ( + syncer interface{ Sync() error } + truncater interface{ Truncate(size int64) error } +) + func fdSyncFn(_ context.Context, mod api.Module, params []uint64) Errno { fsc := mod.(*wasm.CallContext).Sys.FS() fd := uint32(params[0]) - type syncer interface{ Sync() error } // Check to see if the file descriptor is available if f, ok := fsc.LookupFile(fd); !ok { return ErrnoBadf - // fs.FS doesn't declare Sync, but implementations such as os.File implement it. } else if syncer, ok := f.File.(syncer); !ok { - return ErrnoBadf + return ErrnoBadf // possibly a fake file } else if err := syncer.Sync(); err != nil { return ErrnoIo } diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go index d43163a0..cb4e156a 100644 --- a/imports/wasi_snapshot_preview1/fs_test.go +++ b/imports/wasi_snapshot_preview1/fs_test.go @@ -11,11 +11,13 @@ import ( "path" "runtime" "testing" + "time" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/internal/fstest" "github.com/tetratelabs/wazero/internal/leb128" + "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" @@ -428,22 +430,250 @@ func Test_fdFilestatGet(t *testing.T) { } } -// Test_fdFilestatSetSize only tests it is stubbed for GrainLang per #271 func Test_fdFilestatSetSize(t *testing.T) { - log := requireErrnoNosys(t, FdFilestatSetSizeName, 0, 0) - require.Equal(t, ` ---> wasi_snapshot_preview1.fd_filestat_set_size(fd=0,size=0) -<-- errno=ENOSYS -`, log) + tmpDir := t.TempDir() + + tests := []struct { + name string + size uint32 + content, expectedContent []byte + expectedLog string + expectedErrno Errno + }{ + { + name: "badf", + content: []byte("badf"), + expectedContent: []byte("badf"), + expectedErrno: ErrnoBadf, + expectedLog: ` +==> wasi_snapshot_preview1.fd_filestat_set_size(fd=5,size=0) +<== errno=EBADF +`, + }, + { + name: "truncate", + content: []byte("123456"), + expectedContent: []byte("12345"), + size: 5, + expectedErrno: ErrnoSuccess, + expectedLog: ` +==> wasi_snapshot_preview1.fd_filestat_set_size(fd=4,size=5) +<== errno=ESUCCESS +`, + }, + { + name: "truncate to zero", + content: []byte("123456"), + expectedContent: []byte(""), + size: 0, + expectedErrno: ErrnoSuccess, + expectedLog: ` +==> wasi_snapshot_preview1.fd_filestat_set_size(fd=4,size=0) +<== errno=ESUCCESS +`, + }, + { + name: "truncate to expand", + content: []byte("123456"), + expectedContent: append([]byte("123456"), make([]byte, 100)...), + size: 106, + expectedErrno: ErrnoSuccess, + expectedLog: ` +==> wasi_snapshot_preview1.fd_filestat_set_size(fd=4,size=106) +<== errno=ESUCCESS +`, + }, + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + filepath := path.Base(t.Name()) + mod, fd, log, r := requireOpenFile(t, tmpDir, filepath, tc.content, false) + defer r.Close(testCtx) + + if filepath == "badf" { + fd++ + } + requireErrno(t, tc.expectedErrno, mod, FdFilestatSetSizeName, uint64(fd), uint64(tc.size)) + + actual, err := os.ReadFile(path.Join(tmpDir, filepath)) + require.NoError(t, err) + require.Equal(t, tc.expectedContent, actual) + + require.Equal(t, tc.expectedLog, "\n"+log.String()) + }) + } } -// Test_fdFilestatSetTimes only tests it is stubbed for GrainLang per #271 func Test_fdFilestatSetTimes(t *testing.T) { - log := requireErrnoNosys(t, FdFilestatSetTimesName, 0, 0, 0, 0) - require.Equal(t, ` ---> wasi_snapshot_preview1.fd_filestat_set_times(fd=0,atim=0,mtim=0,fst_flags=0) -<-- errno=ENOSYS -`, log) + tmpDir := t.TempDir() + + tests := []struct { + name string + mtime, atime int64 + flags uint16 + expectedLog string + expectedErrno Errno + }{ + { + name: "badf", + expectedErrno: ErrnoBadf, + expectedLog: ` +==> wasi_snapshot_preview1.fd_filestat_set_times(fd=5,atim=0,mtim=0,fst_flags=0) +<== errno=EBADF +`, + }, + { + name: "a=omit,m=omit", + mtime: 1234, // Must be ignored. + atime: 123451, // Must be ignored. + expectedErrno: ErrnoSuccess, + expectedLog: ` +==> wasi_snapshot_preview1.fd_filestat_set_times(fd=4,atim=123451,mtim=1234,fst_flags=0) +<== errno=ESUCCESS +`, + }, + { + name: "a=now,m=omit", + expectedErrno: ErrnoSuccess, + mtime: 1234, // Must be ignored. + atime: 123451, // Must be ignored. + flags: FileStatAdjustFlagsAtimNow, + expectedLog: ` +==> wasi_snapshot_preview1.fd_filestat_set_times(fd=4,atim=123451,mtim=1234,fst_flags=2) +<== errno=ESUCCESS +`, + }, + { + name: "a=omit,m=now", + expectedErrno: ErrnoSuccess, + mtime: 1234, // Must be ignored. + atime: 123451, // Must be ignored. + flags: FileStatAdjustFlagsMtimNow, + expectedLog: ` +==> wasi_snapshot_preview1.fd_filestat_set_times(fd=4,atim=123451,mtim=1234,fst_flags=8) +<== errno=ESUCCESS +`, + }, + { + name: "a=now,m=now", + expectedErrno: ErrnoSuccess, + mtime: 1234, // Must be ignored. + atime: 123451, // Must be ignored. + flags: FileStatAdjustFlagsAtimNow | FileStatAdjustFlagsMtimNow, + expectedLog: ` +==> wasi_snapshot_preview1.fd_filestat_set_times(fd=4,atim=123451,mtim=1234,fst_flags=10) +<== errno=ESUCCESS +`, + }, + { + name: "a=set,m=omit", + expectedErrno: ErrnoSuccess, + mtime: 1234, // Must be ignored. + atime: 55555500, + flags: FileStatAdjustFlagsAtim, + expectedLog: ` +==> wasi_snapshot_preview1.fd_filestat_set_times(fd=4,atim=55555500,mtim=1234,fst_flags=1) +<== errno=ESUCCESS +`, + }, + { + name: "a=set,m=now", + expectedErrno: ErrnoSuccess, + mtime: 1234, // Must be ignored. + atime: 55555500, + flags: FileStatAdjustFlagsAtim | FileStatAdjustFlagsMtimNow, + expectedLog: ` +==> wasi_snapshot_preview1.fd_filestat_set_times(fd=4,atim=55555500,mtim=1234,fst_flags=9) +<== errno=ESUCCESS +`, + }, + { + name: "a=omit,m=set", + expectedErrno: ErrnoSuccess, + mtime: 55555500, + atime: 1234, // Must be ignored. + flags: FileStatAdjustFlagsMtim, + expectedLog: ` +==> wasi_snapshot_preview1.fd_filestat_set_times(fd=4,atim=1234,mtim=55555500,fst_flags=4) +<== errno=ESUCCESS +`, + }, + { + name: "a=now,m=set", + expectedErrno: ErrnoSuccess, + mtime: 55555500, + atime: 1234, // Must be ignored. + flags: FileStatAdjustFlagsAtimNow | FileStatAdjustFlagsMtim, + expectedLog: ` +==> wasi_snapshot_preview1.fd_filestat_set_times(fd=4,atim=1234,mtim=55555500,fst_flags=6) +<== errno=ESUCCESS +`, + }, + { + name: "a=set,m=set", + expectedErrno: ErrnoSuccess, + mtime: 55555500, + atime: 6666666600, + flags: FileStatAdjustFlagsAtim | FileStatAdjustFlagsMtim, + expectedLog: ` +==> wasi_snapshot_preview1.fd_filestat_set_times(fd=4,atim=6666666600,mtim=55555500,fst_flags=5) +<== errno=ESUCCESS +`, + }, + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + filepath := path.Base(t.Name()) + mod, fd, log, r := requireOpenFile(t, tmpDir, filepath, []byte("anything"), false) + defer r.Close(testCtx) + + sys := mod.(*wasm.CallContext).Sys + fsc := sys.FS() + + paramFd := fd + if filepath == "badf" { + paramFd = fd + 1 + } + + f, ok := fsc.LookupFile(fd) + require.True(t, ok) + stat, err := f.Stat() + require.NoError(t, err) + prevAtime, prevMtime, _ := platform.StatTimes(stat) + + requireErrno(t, tc.expectedErrno, mod, FdFilestatSetTimesName, + uint64(paramFd), uint64(tc.atime), uint64(tc.mtime), + uint64(tc.flags), + ) + + if tc.expectedErrno == ErrnoSuccess { + f, ok := fsc.LookupFile(fd) + require.True(t, ok) + stat, err := f.Stat() + require.NoError(t, err) + atime, mtime, _ := platform.StatTimes(stat) + if tc.flags&FileStatAdjustFlagsAtim != 0 { + require.Equal(t, tc.atime, atime) + } else if tc.flags&FileStatAdjustFlagsAtimNow != 0 { + require.True(t, (sys.WalltimeNanos()-atime) < time.Second.Nanoseconds()) + } else { + require.Equal(t, prevAtime, atime) + } + if tc.flags&FileStatAdjustFlagsMtim != 0 { + require.Equal(t, tc.mtime, mtime) + } else if tc.flags&FileStatAdjustFlagsMtimNow != 0 { + require.True(t, (sys.WalltimeNanos()-mtime) < time.Second.Nanoseconds()) + } else { + require.Equal(t, prevMtime, mtime) + } + } + require.Equal(t, tc.expectedLog, "\n"+log.String()) + }) + } } func Test_fdPread(t *testing.T) { diff --git a/imports/wasi_snapshot_preview1/wasi_test.go b/imports/wasi_snapshot_preview1/wasi_test.go index 5ecd56c3..30b66672 100644 --- a/imports/wasi_snapshot_preview1/wasi_test.go +++ b/imports/wasi_snapshot_preview1/wasi_test.go @@ -145,5 +145,5 @@ func requireErrno(t *testing.T, expectedErrno Errno, mod api.Closer, funcName st results, err := mod.(api.Module).ExportedFunction(funcName).Call(testCtx, params...) require.NoError(t, err) errno := Errno(results[0]) - require.Equal(t, expectedErrno, errno, ErrnoName(errno)) + require.Equal(t, expectedErrno, errno, "want %s but got %s", ErrnoName(expectedErrno), ErrnoName(errno)) } diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go index c6bd933f..249d6114 100644 --- a/internal/gojs/fs.go +++ b/internal/gojs/fs.go @@ -598,8 +598,8 @@ func (jsfsTruncate) invoke(ctx context.Context, mod api.Module, args ...interfac length := toInt64(args[1]) callback := args[2].(funcWrapper) - _, _ = path, length // TODO - var err error = syscall.ENOSYS + fsc := mod.(*wasm.CallContext).Sys.FS() + err := fsc.RootFS().Truncate(path, length) return jsfsInvoke(ctx, mod, callback, err) } @@ -609,13 +609,23 @@ func (jsfsTruncate) invoke(ctx context.Context, mod api.Module, args ...interfac // _, err := fsCall("ftruncate", fd, length) // syscall.Ftruncate type jsfsFtruncate struct{} +type truncater interface{ Truncate(size int64) error } + func (jsfsFtruncate) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) { fd := goos.ValueToUint32(args[0]) length := toInt64(args[1]) callback := args[2].(funcWrapper) - _, _ = fd, length // TODO - var err error = syscall.ENOSYS + // Check to see if the file descriptor is available + fsc := mod.(*wasm.CallContext).Sys.FS() + var err error + if f, ok := fsc.LookupFile(fd); !ok { + err = syscall.EBADF + } else if truncater, ok := f.File.(truncater); !ok { + err = syscall.EBADF // possibly a fake file + } else { + err = truncater.Truncate(length) + } return jsfsInvoke(ctx, mod, callback, err) } diff --git a/internal/gojs/testdata/writefs/main.go b/internal/gojs/testdata/writefs/main.go index 4ae4dc64..1b18f306 100644 --- a/internal/gojs/testdata/writefs/main.go +++ b/internal/gojs/testdata/writefs/main.go @@ -65,6 +65,28 @@ func Main() { log.Panicln("unexpected contents:", string(bytes)) } + // Next, truncate it. + if err = f.Truncate(2); err != nil { + log.Panicln(err) + } + if err = f.Close(); err != nil { + log.Panicln(err) + } + if bytes, err := os.ReadFile(file1); err != nil { + log.Panicln(err) + } else if string(bytes) != "wa" { + log.Panicln("unexpected contents:", string(bytes)) + } + + // Now, truncate it by path + if err = os.Truncate(file1, 1); err != nil { + log.Panicln(err) + } else if bytes, err := os.ReadFile(file1); err != nil { + log.Panicln(err) + } else if string(bytes) != "w" { + log.Panicln("unexpected contents:", string(bytes)) + } + // Test removing a non-empty empty directory if err = syscall.Rmdir(dir); err != syscall.ENOTEMPTY { log.Panicln("unexpected error", err) diff --git a/internal/sys/fs.go b/internal/sys/fs.go index 0e311492..d69d68ba 100644 --- a/internal/sys/fs.go +++ b/internal/sys/fs.go @@ -298,10 +298,13 @@ func (c *FSContext) OpenFile(fs sysfs.FS, path string, flag int, perm fs.FileMod if f, err := fs.OpenFile(path, flag, perm); err != nil { return 0, err } else { + fe := &FileEntry{FS: fs, File: f} if path == "/" || path == "." { - path = "" + fe.Name = "" + } else { + fe.Name = path } - newFD := c.openedFiles.Insert(&FileEntry{Name: path, FS: fs, File: f}) + newFD := c.openedFiles.Insert(fe) return newFD, nil } } diff --git a/internal/sys/sys.go b/internal/sys/sys.go index b3afeb90..a239d8ba 100644 --- a/internal/sys/sys.go +++ b/internal/sys/sys.go @@ -62,11 +62,17 @@ func (c *Context) EnvironSize() uint32 { return c.environSize } -// Walltime implements sys.Walltime. +// Walltime implements platform.Walltime. func (c *Context) Walltime() (sec int64, nsec int32) { return (*(c.walltime))() } +// WalltimeNanos returns platform.Walltime as epoch nanoseconds. +func (c *Context) WalltimeNanos() int64 { + sec, nsec := c.Walltime() + return (sec * time.Second.Nanoseconds()) + int64(nsec) +} + // WalltimeResolution returns resolution of Walltime. func (c *Context) WalltimeResolution() sys.ClockResolution { return c.walltimeResolution diff --git a/internal/sys/sys_test.go b/internal/sys/sys_test.go index 774eda52..9cccbe4f 100644 --- a/internal/sys/sys_test.go +++ b/internal/sys/sys_test.go @@ -21,6 +21,12 @@ func TestContext_FS(t *testing.T) { require.Equal(t, fsc, sysCtx.FS()) } +func TestContext_WalltimeNanos(t *testing.T) { + sysCtx := DefaultContext(nil) + + require.Equal(t, int64(1640995200000000000), sysCtx.WalltimeNanos()) +} + func TestDefaultSysContext(t *testing.T) { testFS := sysfs.Adapt(testfs.FS{}) diff --git a/internal/sysfs/adapter.go b/internal/sysfs/adapter.go index 28f52466..950e516a 100644 --- a/internal/sysfs/adapter.go +++ b/internal/sysfs/adapter.go @@ -44,7 +44,7 @@ func (a *adapter) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, er f, err := a.fs.Open(path) if err != nil { - return nil, unwrapPathError(err) + return nil, unwrapOSError(err) } else if osF, ok := f.(*os.File); ok { // If this is an OS file, it has same portability issues as dirFS. return maybeWrapFile(osF), nil diff --git a/internal/sysfs/dirfs.go b/internal/sysfs/dirfs.go index ccb3fe8e..f99a4c19 100644 --- a/internal/sysfs/dirfs.go +++ b/internal/sysfs/dirfs.go @@ -42,7 +42,7 @@ func (d *dirFS) Open(name string) (fs.File, error) { func (d *dirFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) { f, err := os.OpenFile(d.join(name), flag, perm) if err != nil { - return nil, unwrapPathError(err) + return nil, unwrapOSError(err) } return maybeWrapFile(f), nil } @@ -50,7 +50,7 @@ func (d *dirFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, erro // Mkdir implements FS.Mkdir func (d *dirFS) Mkdir(name string, perm fs.FileMode) error { err := os.Mkdir(d.join(name), perm) - err = unwrapPathError(err) + err = unwrapOSError(err) return adjustMkdirError(err) } @@ -82,6 +82,14 @@ func (d *dirFS) Utimes(name string, atimeNsec, mtimeNsec int64) error { }) } +// Truncate implements FS.Truncate +func (d *dirFS) Truncate(name string, size int64) error { + // Use os.Truncate as syscall.Truncate doesn't exist on Windows. + err := os.Truncate(d.join(name), size) + err = unwrapOSError(err) + return adjustTruncateError(err) +} + func (d *dirFS) join(name string) string { switch name { case "", ".", "/": diff --git a/internal/sysfs/dirfs_test.go b/internal/sysfs/dirfs_test.go index 0387f1ef..00af3179 100644 --- a/internal/sysfs/dirfs_test.go +++ b/internal/sysfs/dirfs_test.go @@ -369,6 +369,88 @@ func TestDirFS_Open(t *testing.T) { }) } +func TestDirFS_Truncate(t *testing.T) { + content := []byte("123456") + + tests := []struct { + name string + size int64 + expectedContent []byte + expectedErr error + }{ + { + name: "one less", + size: 5, + expectedContent: []byte("12345"), + }, + { + name: "same", + size: 6, + expectedContent: content, + }, + { + name: "zero", + size: 0, + expectedContent: []byte(""), + }, + { + name: "larger", + size: 106, + expectedContent: append(content, make([]byte, 100)...), + }, + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + tmpDir := t.TempDir() + testFS := NewDirFS(tmpDir) + + name := "truncate" + realPath := pathutil.Join(tmpDir, name) + require.NoError(t, os.WriteFile(realPath, content, 0o0600)) + + err := testFS.Truncate(name, tc.size) + require.NoError(t, err) + + actual, err := os.ReadFile(realPath) + require.NoError(t, err) + require.Equal(t, tc.expectedContent, actual) + }) + } + + tmpDir := t.TempDir() + testFS := NewDirFS(tmpDir) + + name := "truncate" + realPath := pathutil.Join(tmpDir, name) + + if runtime.GOOS != "windows" { + // TODO: os.Truncate on windows can create the file even when it + // doesn't exist. + t.Run("doesn't exist", func(t *testing.T) { + err := testFS.Truncate(name, 0) + require.Equal(t, syscall.ENOENT, err) + }) + } + + t.Run("not file", func(t *testing.T) { + require.NoError(t, os.Mkdir(realPath, 0o700)) + + err := testFS.Truncate(name, 0) + require.Equal(t, syscall.EISDIR, err) + + require.NoError(t, os.Remove(realPath)) + }) + + t.Run("negative", func(t *testing.T) { + require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600)) + + err := testFS.Truncate(name, -1) + require.Equal(t, syscall.EINVAL, err) + }) +} + func TestDirFS_TestFS(t *testing.T) { t.Parallel() diff --git a/internal/sysfs/rootfs.go b/internal/sysfs/rootfs.go index 800630ae..1c9ce515 100644 --- a/internal/sysfs/rootfs.go +++ b/internal/sysfs/rootfs.go @@ -430,11 +430,10 @@ func (fakeRootDir) Read([]byte) (int, error) { type fakeRootDirInfo struct{} -func (fakeRootDirInfo) Name() string { return "/" } -func (fakeRootDirInfo) Size() int64 { return 0 } -func (fakeRootDirInfo) Mode() fs.FileMode { return fs.ModeDir | 0o500 } -func (fakeRootDirInfo) ModTime() time.Time { return time.Unix(0, 0) } -func (fakeRootDirInfo) IsDir() bool { return true } -func (fakeRootDirInfo) Sys() interface{} { return nil } - +func (fakeRootDirInfo) Name() string { return "/" } +func (fakeRootDirInfo) Size() int64 { return 0 } +func (fakeRootDirInfo) Mode() fs.FileMode { return fs.ModeDir | 0o500 } +func (fakeRootDirInfo) ModTime() time.Time { return time.Unix(0, 0) } +func (fakeRootDirInfo) IsDir() bool { return true } +func (fakeRootDirInfo) Sys() interface{} { return nil } func (fakeRootDir) ReadDir(int) (dirents []fs.DirEntry, err error) { return } diff --git a/internal/sysfs/syscall.go b/internal/sysfs/syscall.go index b9ecc951..2776f787 100644 --- a/internal/sysfs/syscall.go +++ b/internal/sysfs/syscall.go @@ -12,6 +12,10 @@ func adjustRmdirError(err error) error { return err } +func adjustTruncateError(err error) error { + return err +} + func adjustUnlinkError(err error) error { if err == syscall.EPERM { return syscall.EISDIR diff --git a/internal/sysfs/syscall_windows.go b/internal/sysfs/syscall_windows.go index 863dc456..e655d6c3 100644 --- a/internal/sysfs/syscall_windows.go +++ b/internal/sysfs/syscall_windows.go @@ -18,6 +18,14 @@ const ( // instead of syscall.EBADF ERROR_INVALID_HANDLE = syscall.Errno(6) + // ERROR_NEGATIVE_SEEK is a Windows error returned by os.Truncate + // instead of syscall.EINVAL + ERROR_NEGATIVE_SEEK = syscall.Errno(131) + + // ERROR_DIR_NOT_EMPTY is a Windows error returned by syscall.Rmdir + // instead of syscall.ENOTEMPTY + ERROR_DIR_NOT_EMPTY = syscall.Errno(145) + // ERROR_ALREADY_EXISTS is a Windows error returned by os.Mkdir // instead of syscall.EEXIST ERROR_ALREADY_EXISTS = syscall.Errno(183) @@ -25,10 +33,6 @@ const ( // ERROR_DIRECTORY is a Windows error returned by syscall.Rmdir // instead of syscall.ENOTDIR ERROR_DIRECTORY = syscall.Errno(267) - - // ERROR_DIR_NOT_EMPTY is a Windows error returned by syscall.Rmdir - // instead of syscall.ENOTEMPTY - ERROR_DIR_NOT_EMPTY = syscall.Errno(145) ) func adjustMkdirError(err error) error { @@ -48,6 +52,13 @@ func adjustRmdirError(err error) error { return err } +func adjustTruncateError(err error) error { + if err == ERROR_NEGATIVE_SEEK { + return syscall.EINVAL + } + return err +} + func adjustUnlinkError(err error) error { if err == ERROR_ACCESS_DENIED { return syscall.EISDIR @@ -98,7 +109,8 @@ func maybeWrapFile(f file) file { io.Writer io.WriterAt // for pwrite syncer - }{f, &windowsWriter{f}, f, f} + truncater + }{f, &windowsWriter{f}, f, f, f} } // windowsWriter translates error codes not mapped properly by Go. diff --git a/internal/sysfs/sysfs.go b/internal/sysfs/sysfs.go index 3395d2d0..d0097d14 100644 --- a/internal/sysfs/sysfs.go +++ b/internal/sysfs/sysfs.go @@ -6,6 +6,7 @@ package sysfs import ( + "errors" "io" "io/fs" "os" @@ -124,6 +125,16 @@ type FS interface { // - syscall.UtimesNano cannot change the ctime. Also, neither WASI nor // runtime.GOOS=js support changing it. Hence, ctime it is absent here. Utimes(path string, atimeNsec, mtimeNsec int64) error + + // Truncate is similar to syscall.Truncate, except the path is relative to + // this file system. + // + // # Errors + // + // The following errors are expected: + // - syscall.EINVAL: `path` is invalid or size is negative. + // - syscall.ENOENT: `path` doesn't exist + Truncate(name string, size int64) error } // StatPath is a convenience that calls FS.OpenFile until there is a stat @@ -150,9 +161,13 @@ type file interface { io.Writer io.WriterAt // for pwrite syncer + truncater } -type syncer interface{ Sync() error } +type ( + syncer interface{ Sync() error } + truncater interface{ Truncate(size int64) error } +) // ReaderAtOffset gets an io.Reader from a fs.File that reads from an offset, // yet doesn't affect the underlying position. This is used to implement @@ -266,11 +281,15 @@ func (r *writerAtOffset) Write(p []byte) (int, error) { return n, err } -func unwrapPathError(err error) error { +func unwrapOSError(err error) error { if pe, ok := err.(*fs.PathError); ok { err = pe.Err + } else if le, ok := err.(*os.LinkError); ok { + err = le.Err } + switch err { + case nil: case fs.ErrInvalid: return syscall.EINVAL case fs.ErrPermission: @@ -281,6 +300,20 @@ func unwrapPathError(err error) error { return syscall.ENOENT case fs.ErrClosed: return syscall.EBADF + case os.ErrInvalid: + return syscall.EINVAL + case os.ErrExist: + return syscall.EEXIST + default: + if errors.Is(err, os.ErrExist) { + return syscall.EEXIST + } + if errors.Is(err, os.ErrNotExist) { + return syscall.ENOENT + } + if errors.Is(err, os.ErrPermission) { + return syscall.EPERM + } } return err } diff --git a/internal/sysfs/unsupported.go b/internal/sysfs/unsupported.go index fb95e073..a649bd9e 100644 --- a/internal/sysfs/unsupported.go +++ b/internal/sysfs/unsupported.go @@ -48,3 +48,8 @@ func (UnimplementedFS) Unlink(path string) error { func (UnimplementedFS) Utimes(path string, atimeNsec, mtimeNsec int64) error { return syscall.ENOSYS } + +// Truncate implements FS.Truncate +func (UnimplementedFS) Truncate(string, int64) error { + return syscall.ENOSYS +} diff --git a/internal/wasi_snapshot_preview1/fs.go b/internal/wasi_snapshot_preview1/fs.go index 164c40be..b5990ca8 100644 --- a/internal/wasi_snapshot_preview1/fs.go +++ b/internal/wasi_snapshot_preview1/fs.go @@ -133,3 +133,11 @@ var filetypeToString = [...]string{ "SOCKET_STREAM", "SYMBOLIC_LINK", } + +// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fstflags +const ( + FileStatAdjustFlagsAtim uint16 = 1 << iota + FileStatAdjustFlagsAtimNow + FileStatAdjustFlagsMtim + FileStatAdjustFlagsMtimNow +) diff --git a/site/content/specs.md b/site/content/specs.md index ab8448ae..f6380341 100644 --- a/site/content/specs.md +++ b/site/content/specs.md @@ -96,8 +96,8 @@ Note: C (via clang) supports the maximum WASI functions due to [wasi-libc][16]. | fd_fdstat_set_flags | ❌ | | | fd_fdstat_set_rights | 💀 | | | fd_filestat_get | ✅ | Zig | -| fd_filestat_set_size | ❌ | | -| fd_filestat_set_times | ❌ | | +| fd_filestat_set_size | ✅ | Rust,Zig | +| fd_filestat_set_times | ✅ | Rust,Zig | | fd_pread | ✅ | Zig | | fd_prestat_get | ✅ | Rust,TinyGo,Zig | | fd_prestat_dir_name | ✅ | Rust,TinyGo,Zig |