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:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
22
internal/gojs/testdata/writefs/main.go
vendored
22
internal/gojs/testdata/writefs/main.go
vendored
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "", ".", "/":
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user