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 <adrian@tetrate.io>
Co-authored-by: Takeshi Yoneda <takeshi@tetrate.io>
This commit is contained in:
Crypt Keeper
2023-01-30 19:08:10 +02:00
committed by GitHub
parent f9db80624d
commit a60debc8d2
19 changed files with 567 additions and 48 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -44,7 +44,7 @@ func (a *adapter) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, er
f, err := a.fs.Open(path)
if err != nil {
return nil, 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

View File

@@ -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 "", ".", "/":

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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