diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go index 033b70cb..3a451711 100644 --- a/imports/wasi_snapshot_preview1/fs.go +++ b/imports/wasi_snapshot_preview1/fs.go @@ -15,7 +15,6 @@ import ( "github.com/tetratelabs/wazero/internal/fsapi" socketapi "github.com/tetratelabs/wazero/internal/sock" "github.com/tetratelabs/wazero/internal/sys" - "github.com/tetratelabs/wazero/internal/sysfs" "github.com/tetratelabs/wazero/internal/wasip1" "github.com/tetratelabs/wazero/internal/wasm" sysapi "github.com/tetratelabs/wazero/sys" @@ -503,50 +502,55 @@ func fdFilestatSetTimesFn(_ context.Context, mod api.Module, params []uint64) ex return experimentalsys.EBADF } - times, errno := toTimes(atim, mtim, fstFlags) + atim, mtim, errno := toTimes(sys.WalltimeNanos, atim, mtim, fstFlags) if errno != 0 { return errno } // Try to update the file timestamps by file-descriptor. - errno = f.File.Utimens(×) + errno = f.File.Utimens(atim, mtim) // Fall back to path based, despite it being less precise. switch errno { case experimentalsys.EPERM, experimentalsys.ENOSYS: - errno = f.FS.Utimens(f.Name, ×) + errno = f.FS.Utimens(f.Name, atim, mtim) } return errno } -func toTimes(atim, mtime int64, fstFlags uint16) (times [2]syscall.Timespec, errno experimentalsys.Errno) { +func toTimes(walltime func() int64, atim, mtim int64, fstFlags uint16) (int64, int64, experimentalsys.Errno) { // times[0] == atim, times[1] == mtim + var nowTim int64 + // coerce atim into a timespec if set, now := fstFlags&wasip1.FstflagsAtim != 0, fstFlags&wasip1.FstflagsAtimNow != 0; set && now { - errno = experimentalsys.EINVAL - return + return 0, 0, experimentalsys.EINVAL } else if set { - times[0] = syscall.NsecToTimespec(atim) + // atim is already correct } else if now { - times[0].Nsec = sysfs.UTIME_NOW + nowTim = walltime() + atim = nowTim } else { - times[0].Nsec = sysfs.UTIME_OMIT + atim = fsapi.UTIME_OMIT } // coerce mtim into a timespec if set, now := fstFlags&wasip1.FstflagsMtim != 0, fstFlags&wasip1.FstflagsMtimNow != 0; set && now { - errno = experimentalsys.EINVAL - return + return 0, 0, experimentalsys.EINVAL } else if set { - times[1] = syscall.NsecToTimespec(mtime) + // mtim is already correct } else if now { - times[1].Nsec = sysfs.UTIME_NOW + if nowTim != 0 { + mtim = nowTim + } else { + mtim = walltime() + } } else { - times[1].Nsec = sysfs.UTIME_OMIT + mtim = fsapi.UTIME_OMIT } - return + return atim, mtim, 0 } // fdPread is the WASI function named FdPreadName which reads from a file @@ -1445,7 +1449,7 @@ func pathFilestatSetTimesFn(_ context.Context, mod api.Module, params []uint64) sys := mod.(*wasm.ModuleInstance).Sys fsc := sys.FS() - times, errno := toTimes(atim, mtim, fstFlags) + atim, mtim, errno := toTimes(sys.WalltimeNanos, atim, mtim, fstFlags) if errno != 0 { return errno } @@ -1457,14 +1461,14 @@ func pathFilestatSetTimesFn(_ context.Context, mod api.Module, params []uint64) symlinkFollow := flags&wasip1.LOOKUP_SYMLINK_FOLLOW != 0 if symlinkFollow { - return preopen.Utimens(pathName, ×) + return preopen.Utimens(pathName, atim, mtim) } // Otherwise, we need to emulate don't follow by opening the file by path. if f, errno := preopen.OpenFile(pathName, syscall.O_WRONLY, 0); errno != 0 { return errno } else { defer f.Close() - return f.Utimens(×) + return f.Utimens(atim, mtim) } } diff --git a/internal/fsapi/file.go b/internal/fsapi/file.go index cd0ccfba..0c50175d 100644 --- a/internal/fsapi/file.go +++ b/internal/fsapi/file.go @@ -1,8 +1,6 @@ package fsapi import ( - "syscall" - experimentalsys "github.com/tetratelabs/wazero/experimental/sys" "github.com/tetratelabs/wazero/sys" ) @@ -346,10 +344,9 @@ type File interface { // // # Parameters // - // The `times` parameter includes the access and modification timestamps to - // assign. Special syscall.Timespec NSec values UTIME_NOW and UTIME_OMIT may be - // specified instead of real timestamps. A nil `times` parameter behaves the - // same as if both were set to UTIME_NOW. + // The `atim` and `mtim` parameters refer to access and modification time + // stamps as defined in sys.Stat_t. To retain one or the other, substitute + // it with the pseudo-timestamp UTIME_OMIT. // // # Errors // @@ -363,7 +360,7 @@ type File interface { // https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html // - Windows requires files to be open with fsapi.O_RDWR, which means you // cannot use this to update timestamps on a directory (EPERM). - Utimens(times *[2]syscall.Timespec) experimentalsys.Errno + Utimens(atim, mtim int64) experimentalsys.Errno // Close closes the underlying file. // diff --git a/internal/fsapi/fs.go b/internal/fsapi/fs.go index 50bf6168..0b0d6790 100644 --- a/internal/fsapi/fs.go +++ b/internal/fsapi/fs.go @@ -2,7 +2,6 @@ package fsapi import ( "io/fs" - "syscall" experimentalsys "github.com/tetratelabs/wazero/experimental/sys" "github.com/tetratelabs/wazero/sys" @@ -272,11 +271,12 @@ type FS interface { // // # Parameters // - // The `times` parameter includes the access and modification timestamps to - // assign. Special syscall.Timespec NSec values UTIME_NOW and UTIME_OMIT - // may be specified instead of real timestamps. A nil `times` parameter - // behaves the same as if both were set to UTIME_NOW. If the path is a - // symbolic link, the target of expanding that link is updated. + // If the path is a symbolic link, the target of expanding that link is + // updated. + // + // The `atim` and `mtim` parameters refer to access and modification time + // stamps as defined in sys.Stat_t. To retain one or the other, substitute + // it with the pseudo-timestamp UTIME_OMIT. // // # Errors // @@ -290,7 +290,5 @@ type FS interface { // // - 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) experimentalsys.Errno - // TODO: change impl to not use syscall package, - // possibly by being just a pair of int64s.. + Utimens(path string, atim, mtim int64) experimentalsys.Errno } diff --git a/internal/fsapi/time.go b/internal/fsapi/time.go new file mode 100644 index 00000000..1cfcea5d --- /dev/null +++ b/internal/fsapi/time.go @@ -0,0 +1,10 @@ +package fsapi + +import "math" + +// UTIME_OMIT is a special constant for use in updating times via FS.Utimens +// or File.Utimens. When used for atim or mtim, the value is retained. +// +// Note: This may be implemented via a stat when the underlying filesystem +// does not support this value. +const UTIME_OMIT int64 = math.MinInt64 diff --git a/internal/fsapi/unimplemented.go b/internal/fsapi/unimplemented.go index 77567a24..b1284bc8 100644 --- a/internal/fsapi/unimplemented.go +++ b/internal/fsapi/unimplemented.go @@ -2,7 +2,6 @@ package fsapi import ( "io/fs" - "syscall" experimentalsys "github.com/tetratelabs/wazero/experimental/sys" "github.com/tetratelabs/wazero/sys" @@ -78,7 +77,7 @@ func (UnimplementedFS) Unlink(path string) experimentalsys.Errno { } // Utimens implements FS.Utimens -func (UnimplementedFS) Utimens(path string, times *[2]syscall.Timespec) experimentalsys.Errno { +func (UnimplementedFS) Utimens(path string, atim, mtim int64) experimentalsys.Errno { return experimentalsys.ENOSYS } @@ -184,7 +183,7 @@ func (UnimplementedFile) Datasync() experimentalsys.Errno { } // Utimens implements File.Utimens -func (UnimplementedFile) Utimens(*[2]syscall.Timespec) experimentalsys.Errno { +func (UnimplementedFile) Utimens(int64, int64) experimentalsys.Errno { return experimentalsys.ENOSYS } diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go index ad223fd6..d0cd422e 100644 --- a/internal/gojs/fs.go +++ b/internal/gojs/fs.go @@ -3,7 +3,6 @@ package gojs import ( "context" "fmt" - "syscall" "github.com/tetratelabs/wazero/api" experimentalsys "github.com/tetratelabs/wazero/experimental/sys" @@ -432,10 +431,7 @@ func (u *jsfsUtimes) invoke(ctx context.Context, mod api.Module, args ...interfa callback := args[3].(funcWrapper) fsc := mod.(*wasm.ModuleInstance).Sys.FS() - times := [2]syscall.Timespec{ - syscall.NsecToTimespec(atimeSec * 1e9), syscall.NsecToTimespec(mtimeSec * 1e9), - } - errno := fsc.RootFS().Utimens(path, ×) + errno := fsc.RootFS().Utimens(path, atimeSec*1e9, mtimeSec*1e9) return jsfsInvoke(ctx, mod, callback, errno) } diff --git a/internal/sys/lazy.go b/internal/sys/lazy.go index 011a5a4d..1cb01a19 100644 --- a/internal/sys/lazy.go +++ b/internal/sys/lazy.go @@ -1,8 +1,6 @@ package sys import ( - "syscall" - experimentalsys "github.com/tetratelabs/wazero/experimental/sys" "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/sys" @@ -103,11 +101,11 @@ func (r *lazyDir) Datasync() experimentalsys.Errno { } // Utimens implements the same method as documented on fsapi.File -func (r *lazyDir) Utimens(times *[2]syscall.Timespec) experimentalsys.Errno { +func (r *lazyDir) Utimens(atim, mtim int64) experimentalsys.Errno { if f, ok := r.file(); !ok { return experimentalsys.EBADF } else { - return f.Utimens(times) + return f.Utimens(atim, mtim) } } diff --git a/internal/sysfs/adapter_test.go b/internal/sysfs/adapter_test.go index f79f4bdc..265d386c 100644 --- a/internal/sysfs/adapter_test.go +++ b/internal/sysfs/adapter_test.go @@ -90,7 +90,7 @@ func TestAdapt_UtimesNano(t *testing.T) { realPath := joinPath(tmpDir, path) require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600)) - err := testFS.Utimens(path, nil) + err := testFS.Utimens(path, fsapi.UTIME_OMIT, fsapi.UTIME_OMIT) require.EqualErrno(t, experimentalsys.ENOSYS, err) } diff --git a/internal/sysfs/bench_test.go b/internal/sysfs/bench_test.go index b5c34fd1..b31a7cb8 100644 --- a/internal/sysfs/bench_test.go +++ b/internal/sysfs/bench_test.go @@ -5,8 +5,8 @@ import ( "io/fs" "os" "path" - "syscall" "testing" + "time" "github.com/tetratelabs/wazero/experimental/sys" "github.com/tetratelabs/wazero/internal/fsapi" @@ -19,14 +19,12 @@ func BenchmarkFsFileUtimesNs(b *testing.B) { } defer f.Close() - times := &[2]syscall.Timespec{ - {Sec: 123, Nsec: 4 * 1e3}, - {Sec: 123, Nsec: 4 * 1e3}, - } + atim := int64(123*time.Second + 4*time.Microsecond) + mtim := atim b.ResetTimer() for i := 0; i < b.N; i++ { - if errno := f.Utimens(times); errno != 0 { + if errno := f.Utimens(atim, mtim); errno != 0 { b.Fatal(errno) } } diff --git a/internal/sysfs/dirfs.go b/internal/sysfs/dirfs.go index c908d6c5..f681e97b 100644 --- a/internal/sysfs/dirfs.go +++ b/internal/sysfs/dirfs.go @@ -116,8 +116,8 @@ func (d *dirFS) Symlink(oldName, link string) experimentalsys.Errno { } // Utimens implements the same method as documented on fsapi.FS -func (d *dirFS) Utimens(path string, times *[2]syscall.Timespec) experimentalsys.Errno { - return Utimens(d.join(path), times) +func (d *dirFS) Utimens(path string, atim, mtim int64) experimentalsys.Errno { + return utimens(d.join(path), atim, mtim) } func (d *dirFS) join(path string) string { diff --git a/internal/sysfs/dirfs_test.go b/internal/sysfs/dirfs_test.go index 078df6df..b13146a0 100644 --- a/internal/sysfs/dirfs_test.go +++ b/internal/sysfs/dirfs_test.go @@ -7,7 +7,6 @@ import ( "os" "path" "runtime" - "syscall" "testing" "time" @@ -531,7 +530,7 @@ func TestDirFS_Utimesns(t *testing.T) { require.NoError(t, err) t.Run("doesn't exist", func(t *testing.T) { - err := testFS.Utimens("nope", nil) + err := testFS.Utimens("nope", 0, 0) require.EqualErrno(t, sys.ENOENT, err) }) @@ -540,60 +539,31 @@ func TestDirFS_Utimesns(t *testing.T) { // // Negative isn't tested as most platforms don't return consistent results. tests := []struct { - name string - times *[2]syscall.Timespec + name string + atim, mtim int64 }{ { name: "nil", }, { name: "a=omit,m=omit", - times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: UTIME_OMIT}, - {Sec: 123, Nsec: UTIME_OMIT}, - }, + atim: fsapi.UTIME_OMIT, + mtim: fsapi.UTIME_OMIT, }, { - name: "a=now,m=omit", - times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: UTIME_NOW}, - {Sec: 123, Nsec: UTIME_OMIT}, - }, + name: "a=set,m=omit", + atim: int64(123*time.Second + 4*time.Microsecond), + mtim: fsapi.UTIME_OMIT, }, { - name: "a=omit,m=now", - times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: UTIME_OMIT}, - {Sec: 123, Nsec: UTIME_NOW}, - }, - }, - { - name: "a=now,m=now", - times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: UTIME_NOW}, - {Sec: 123, Nsec: UTIME_NOW}, - }, - }, - { - name: "a=now,m=set", - times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: UTIME_NOW}, - {Sec: 123, Nsec: 4 * 1e3}, - }, - }, - { - name: "a=set,m=now", - times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: 4 * 1e3}, - {Sec: 123, Nsec: UTIME_NOW}, - }, + name: "a=omit,m=set", + atim: fsapi.UTIME_OMIT, + mtim: int64(123*time.Second + 4*time.Microsecond), }, { name: "a=set,m=set", - times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: 4 * 1e3}, - {Sec: 223, Nsec: 5 * 1e3}, - }, + atim: int64(123*time.Second + 4*time.Microsecond), + mtim: int64(223*time.Second + 5*time.Microsecond), }, } @@ -636,31 +606,25 @@ func TestDirFS_Utimesns(t *testing.T) { oldSt, errno := testFS.Lstat(statPath) require.EqualErrno(t, 0, errno) - errno = testFS.Utimens(path, tc.times) + errno = testFS.Utimens(path, tc.atim, tc.mtim) require.EqualErrno(t, 0, errno) newSt, errno := testFS.Lstat(statPath) require.EqualErrno(t, 0, errno) if platform.CompilerSupported() { - if tc.times != nil && tc.times[0].Nsec == UTIME_OMIT { + if tc.atim == fsapi.UTIME_OMIT { require.Equal(t, oldSt.Atim, newSt.Atim) - } 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 { - require.Equal(t, tc.times[0].Nano(), newSt.Atim) + require.Equal(t, tc.atim, newSt.Atim) } } // When compiler isn't supported, we can still check mtim. - if tc.times != nil && tc.times[1].Nsec == UTIME_OMIT { + if tc.mtim == fsapi.UTIME_OMIT { require.Equal(t, oldSt.Mtim, newSt.Mtim) - } 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 { - require.Equal(t, tc.times[1].Nano(), newSt.Mtim) + require.Equal(t, tc.mtim, newSt.Mtim) } }) } diff --git a/internal/sysfs/file.go b/internal/sysfs/file.go index b3285cab..b9b96076 100644 --- a/internal/sysfs/file.go +++ b/internal/sysfs/file.go @@ -4,6 +4,7 @@ import ( "io" "io/fs" "os" + "time" experimentalsys "github.com/tetratelabs/wazero/experimental/sys" "github.com/tetratelabs/wazero/internal/fsapi" @@ -465,3 +466,40 @@ func pwrite(w io.WriterAt, buf []byte, off int64) (n int, errno experimentalsys. n, err := w.WriteAt(buf, off) return n, experimentalsys.UnwrapOSError(err) } + +func chtimes(path string, atim, mtim int64) (errno experimentalsys.Errno) { //nolint:unused + // When both inputs are omitted, there is nothing to change. + if atim == fsapi.UTIME_OMIT && mtim == fsapi.UTIME_OMIT { + return + } + + // UTIME_OMIT is expensive until progress is made in Go, as it requires a + // 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 sys.Stat_t + if atim == fsapi.UTIME_OMIT || mtim == fsapi.UTIME_OMIT { + if st, errno = stat(path); errno != 0 { + return + } + } + + var atime, mtime time.Time + if atim == fsapi.UTIME_OMIT { + atime = epochNanosToTime(st.Atim) + mtime = epochNanosToTime(mtim) + } else if mtim == fsapi.UTIME_OMIT { + atime = epochNanosToTime(atim) + mtime = epochNanosToTime(st.Mtim) + } else { + atime = epochNanosToTime(atim) + mtime = epochNanosToTime(mtim) + } + return experimentalsys.UnwrapOSError(os.Chtimes(path, atime, mtime)) +} + +func epochNanosToTime(epochNanos int64) time.Time { //nolint:unused + seconds := epochNanos / 1e9 + nanos := epochNanos % 1e9 + return time.Unix(seconds, nanos) +} diff --git a/internal/sysfs/file_test.go b/internal/sysfs/file_test.go index 0c1d35be..4e94093f 100644 --- a/internal/sysfs/file_test.go +++ b/internal/sysfs/file_test.go @@ -986,10 +986,10 @@ func TestFileUtimens(t *testing.T) { testUtimens(t, true) testEBADFIfFileClosed(t, func(f fsapi.File) experimentalsys.Errno { - return f.Utimens(nil) + return f.Utimens(fsapi.UTIME_OMIT, fsapi.UTIME_OMIT) }) testEBADFIfDirClosed(t, func(d fsapi.File) experimentalsys.Errno { - return d.Utimens(nil) + return d.Utimens(fsapi.UTIME_OMIT, fsapi.UTIME_OMIT) }) } diff --git a/internal/sysfs/futimens.go b/internal/sysfs/futimens.go index 9144126b..c122da79 100644 --- a/internal/sysfs/futimens.go +++ b/internal/sysfs/futimens.go @@ -1,53 +1,14 @@ +//go:build linux || darwin + package sysfs import ( "syscall" - "time" "unsafe" - experimentalsys "github.com/tetratelabs/wazero/experimental/sys" - "github.com/tetratelabs/wazero/sys" + "github.com/tetratelabs/wazero/internal/fsapi" ) -const ( - // UTIME_NOW is a special syscall.Timespec NSec value used to set the - // file's timestamp to a value close to, but not greater than the current - // system time. - UTIME_NOW = _UTIME_NOW - - // UTIME_OMIT is a special syscall.Timespec NSec value used to avoid - // setting the file's timestamp. - UTIME_OMIT = _UTIME_OMIT -) - -// Utimens set file access and modification times on a path resolved to the -// current working directory, at nanosecond precision. -// -// # Parameters -// -// The `times` parameter includes the access and modification timestamps to -// assign. Special syscall.Timespec NSec values UTIME_NOW and UTIME_OMIT may be -// specified instead of real timestamps. A nil `times` parameter behaves the -// same as if both were set to UTIME_NOW. If the path is a symbolic link, the -// target of expanding that link is updated. -// -// # Errors -// -// A zero sys.Errno is success. The below are expected otherwise: -// - sys.ENOSYS: the implementation does not support this function. -// - sys.EINVAL: `path` is invalid. -// - sys.EEXIST: `path` exists and is a directory. -// - sys.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 -func Utimens(path string, times *[2]syscall.Timespec) experimentalsys.Errno { - err := utimens(path, times) - return experimentalsys.UnwrapOSError(err) -} - func timesToPtr(times *[2]syscall.Timespec) unsafe.Pointer { //nolint:unused if times != nil { return unsafe.Pointer(×[0]) @@ -55,67 +16,22 @@ func timesToPtr(times *[2]syscall.Timespec) unsafe.Pointer { //nolint:unused return unsafe.Pointer(nil) } -func utimensPortable(path string, times *[2]syscall.Timespec) error { //nolint:unused - // Handle when both inputs are current system time. - if times == nil || times[0].Nsec == UTIME_NOW && times[1].Nsec == UTIME_NOW { - ts := nowTimespec() - return syscall.UtimesNano(path, []syscall.Timespec{ts, ts}) - } - +func timesToTimespecs(atim int64, mtim int64) (times *[2]syscall.Timespec) { // When both inputs are omitted, there is nothing to change. - if times[0].Nsec == UTIME_OMIT && times[1].Nsec == UTIME_OMIT { - return nil + if atim == fsapi.UTIME_OMIT && mtim == fsapi.UTIME_OMIT { + return } - // Handle when neither input are special values - if times[0].Nsec != UTIME_NOW && times[1].Nsec != UTIME_NOW && - times[0].Nsec != UTIME_OMIT && times[1].Nsec != UTIME_OMIT { - return syscall.UtimesNano(path, times[:]) - } - - // Now, either atim or mtim is a special value, but not both. - - // Now, either one of the inputs is a special value, or neither. This means - // we don't have a risk of re-reading the clock or re-doing stat. - if atim, err := normalizeTimespec(path, times, 0); err != 0 { - return err - } else if mtim, err := normalizeTimespec(path, times, 1); err != 0 { - return err + times = &[2]syscall.Timespec{} + if atim == fsapi.UTIME_OMIT { + times[0] = syscall.Timespec{Nsec: _UTIME_OMIT} + times[1] = syscall.NsecToTimespec(mtim) + } else if mtim == fsapi.UTIME_OMIT { + times[0] = syscall.NsecToTimespec(atim) + times[1] = syscall.Timespec{Nsec: _UTIME_OMIT} } else { - return syscall.UtimesNano(path, []syscall.Timespec{atim, mtim}) + times[0] = syscall.NsecToTimespec(atim) + times[1] = syscall.NsecToTimespec(mtim) } -} - -func normalizeTimespec(path string, times *[2]syscall.Timespec, i int) (ts syscall.Timespec, err experimentalsys.Errno) { //nolint:unused - switch times[i].Nsec { - case UTIME_NOW: // declined in Go per golang/go#31880. - ts = nowTimespec() - return - case UTIME_OMIT: - // UTIME_OMIT is expensive until progress is made in Go, as it requires a - // 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 sys.Stat_t - if st, err = stat(path); err != 0 { - return - } - switch i { - case 0: - ts = syscall.NsecToTimespec(st.Atim) - case 1: - ts = syscall.NsecToTimespec(st.Mtim) - default: - panic("BUG") - } - return - default: // not special - ts = times[i] - return - } -} - -func nowTimespec() syscall.Timespec { //nolint:unused - now := time.Now().UnixNano() - return syscall.NsecToTimespec(now) + return } diff --git a/internal/sysfs/futimens_darwin.go b/internal/sysfs/futimens_darwin.go index a5663ede..88e4008f 100644 --- a/internal/sysfs/futimens_darwin.go +++ b/internal/sysfs/futimens_darwin.go @@ -2,13 +2,14 @@ package sysfs import ( "syscall" - _ "unsafe" // for go:linkname + _ "unsafe" + + experimentalsys "github.com/tetratelabs/wazero/experimental/sys" ) const ( _AT_FDCWD = -0x2 _AT_SYMLINK_NOFOLLOW = 0x0020 - _UTIME_NOW = -1 _UTIME_OMIT = -2 ) @@ -16,20 +17,25 @@ const ( //go:linkname utimensat syscall.utimensat func utimensat(dirfd int, path string, times *[2]syscall.Timespec, flags int) error -func utimens(path string, times *[2]syscall.Timespec) error { +func utimens(path string, atim, mtim int64) experimentalsys.Errno { + times := timesToTimespecs(atim, mtim) + if times == nil { + return 0 + } var flags int - return utimensat(_AT_FDCWD, path, times, flags) + return experimentalsys.UnwrapOSError(utimensat(_AT_FDCWD, path, times, flags)) } -func futimens(fd uintptr, times *[2]syscall.Timespec) error { +func futimens(fd uintptr, atim, mtim int64) experimentalsys.Errno { + times := timesToTimespecs(atim, mtim) + if times == nil { + return 0 + } _p0 := timesToPtr(times) // Warning: futimens only exists since High Sierra (10.13). _, _, e1 := syscall_syscall6(libc_futimens_trampoline_addr, fd, uintptr(_p0), 0, 0, 0, 0) - if e1 != 0 { - return e1 - } - return nil + return experimentalsys.UnwrapOSError(e1) } // libc_futimens_trampoline_addr is the address of the diff --git a/internal/sysfs/futimens_linux.go b/internal/sysfs/futimens_linux.go index 5008ca81..3ec68537 100644 --- a/internal/sysfs/futimens_linux.go +++ b/internal/sysfs/futimens_linux.go @@ -3,28 +3,38 @@ package sysfs import ( "syscall" "unsafe" - _ "unsafe" // for go:linkname + _ "unsafe" + + experimentalsys "github.com/tetratelabs/wazero/experimental/sys" ) const ( _AT_FDCWD = -0x64 - _UTIME_NOW = (1 << 30) - 1 _UTIME_OMIT = (1 << 30) - 2 ) -func utimens(path string, times *[2]syscall.Timespec) (err error) { +func utimens(path string, atim, mtim int64) experimentalsys.Errno { + times := timesToTimespecs(atim, mtim) + if times == nil { + return 0 + } + var flags int var _p0 *byte - _p0, err = syscall.BytePtrFromString(path) - if err != nil { - return + _p0, err := syscall.BytePtrFromString(path) + if err == nil { + err = utimensat(_AT_FDCWD, uintptr(unsafe.Pointer(_p0)), times, flags) } - return utimensat(_AT_FDCWD, uintptr(unsafe.Pointer(_p0)), times, flags) + return experimentalsys.UnwrapOSError(err) } // On linux, implement futimens via utimensat with the NUL path. -func futimens(fd uintptr, times *[2]syscall.Timespec) error { - return utimensat(int(fd), 0 /* NUL */, times, 0) +func futimens(fd uintptr, atim, mtim int64) experimentalsys.Errno { + times := timesToTimespecs(atim, mtim) + if times == nil { + return 0 + } + return experimentalsys.UnwrapOSError(utimensat(int(fd), 0 /* NUL */, times, 0)) } // utimensat is like syscall.utimensat special-cased to accept a NUL string for the path value. diff --git a/internal/sysfs/futimens_test.go b/internal/sysfs/futimens_test.go index b87d46f1..ba6a3f3b 100644 --- a/internal/sysfs/futimens_test.go +++ b/internal/sysfs/futimens_test.go @@ -1,10 +1,11 @@ +//go:build windows || linux || darwin + package sysfs import ( "os" "path" "runtime" - "syscall" "testing" "time" @@ -16,7 +17,7 @@ import ( func TestUtimens(t *testing.T) { t.Run("doesn't exist", func(t *testing.T) { - err := Utimens("nope", nil) + err := utimens("nope", 0, 0) require.EqualErrno(t, sys.ENOENT, err) }) testUtimens(t, false) @@ -28,74 +29,31 @@ func testUtimens(t *testing.T, futimes bool) { // // Negative isn't tested as most platforms don't return consistent results. tests := []struct { - name string - times *[2]syscall.Timespec + name string + atim, mtim int64 }{ { name: "nil", }, { name: "a=omit,m=omit", - times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: UTIME_OMIT}, - {Sec: 123, Nsec: UTIME_OMIT}, - }, - }, - { - name: "a=now,m=omit", - times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: UTIME_NOW}, - {Sec: 123, Nsec: UTIME_OMIT}, - }, - }, - { - name: "a=omit,m=now", - times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: UTIME_OMIT}, - {Sec: 123, Nsec: UTIME_NOW}, - }, - }, - { - name: "a=now,m=now", - times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: UTIME_NOW}, - {Sec: 123, Nsec: UTIME_NOW}, - }, - }, - { - name: "a=now,m=set", - times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: UTIME_NOW}, - {Sec: 123, Nsec: 4 * 1e3}, - }, - }, - { - name: "a=set,m=now", - times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: 4 * 1e3}, - {Sec: 123, Nsec: UTIME_NOW}, - }, + atim: fsapi.UTIME_OMIT, + mtim: fsapi.UTIME_OMIT, }, { name: "a=set,m=omit", - times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: 4 * 1e3}, - {Sec: 123, Nsec: UTIME_OMIT}, - }, + atim: int64(123*time.Second + 4*time.Microsecond), + mtim: fsapi.UTIME_OMIT, }, { name: "a=omit,m=set", - times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: UTIME_OMIT}, - {Sec: 123, Nsec: 4 * 1e3}, - }, + atim: fsapi.UTIME_OMIT, + mtim: int64(123*time.Second + 4*time.Microsecond), }, { name: "a=set,m=set", - times: &[2]syscall.Timespec{ - {Sec: 123, Nsec: 4 * 1e3}, - {Sec: 223, Nsec: 5 * 1e3}, - }, + atim: int64(123*time.Second + 4*time.Microsecond), + mtim: int64(223*time.Second + 5*time.Microsecond), }, } for _, fileType := range []string{"dir", "file", "link"} { @@ -136,7 +94,7 @@ func testUtimens(t *testing.T, futimes bool) { require.EqualErrno(t, 0, errno) if !futimes { - errno = Utimens(path, tc.times) + errno = utimens(path, tc.atim, tc.mtim) require.EqualErrno(t, 0, errno) } else { flag := fsapi.O_RDWR @@ -150,7 +108,7 @@ func testUtimens(t *testing.T, futimes bool) { f := requireOpenFile(t, path, flag, 0) - errno = f.Utimens(tc.times) + errno = f.Utimens(tc.atim, tc.mtim) require.EqualErrno(t, 0, f.Close()) require.EqualErrno(t, 0, errno) } @@ -159,24 +117,18 @@ func testUtimens(t *testing.T, futimes bool) { require.EqualErrno(t, 0, errno) if platform.CompilerSupported() { - if tc.times != nil && tc.times[0].Nsec == UTIME_OMIT { + if tc.atim == fsapi.UTIME_OMIT { require.Equal(t, oldSt.Atim, newSt.Atim) - } 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 { - require.Equal(t, tc.times[0].Nano(), newSt.Atim) + require.Equal(t, tc.atim, newSt.Atim) } } // When compiler isn't supported, we can still check mtim. - if tc.times != nil && tc.times[1].Nsec == UTIME_OMIT { + if tc.mtim == fsapi.UTIME_OMIT { require.Equal(t, oldSt.Mtim, newSt.Mtim) - } 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 { - require.Equal(t, tc.times[1].Nano(), newSt.Mtim) + require.Equal(t, tc.mtim, newSt.Mtim) } }) } diff --git a/internal/sysfs/futimens_unsupported.go b/internal/sysfs/futimens_unsupported.go index 2816cb84..c78a16b4 100644 --- a/internal/sysfs/futimens_unsupported.go +++ b/internal/sysfs/futimens_unsupported.go @@ -3,22 +3,14 @@ package sysfs import ( - "syscall" - "github.com/tetratelabs/wazero/experimental/sys" ) -// Define values even if not used except as sentinels. -const ( - _UTIME_NOW = -1 - _UTIME_OMIT = -2 -) - -func utimens(path string, times *[2]syscall.Timespec) error { - return utimensPortable(path, times) +func utimens(path string, atim, mtim int64) sys.Errno { + return chtimes(path, atim, mtim) } -func futimens(fd uintptr, times *[2]syscall.Timespec) error { +func futimens(fd uintptr, atim, mtim int64) error { // Go exports syscall.Futimes, which is microsecond granularity, and // WASI tests expect nanosecond. We don't yet have a way to invoke the // futimens syscall portably. diff --git a/internal/sysfs/futimens_windows.go b/internal/sysfs/futimens_windows.go index 26d9c2a4..68223f5e 100644 --- a/internal/sysfs/futimens_windows.go +++ b/internal/sysfs/futimens_windows.go @@ -2,24 +2,17 @@ package sysfs import ( "syscall" - "time" "github.com/tetratelabs/wazero/experimental/sys" + "github.com/tetratelabs/wazero/internal/fsapi" "github.com/tetratelabs/wazero/internal/platform" ) -// Define values even if not used except as sentinels. -const ( - _UTIME_NOW = -1 - _UTIME_OMIT = -2 - SupportsSymlinkNoFollow = false -) - -func utimens(path string, times *[2]syscall.Timespec) error { - return utimensPortable(path, times) +func utimens(path string, atim, mtim int64) sys.Errno { + return chtimes(path, atim, mtim) } -func futimens(fd uintptr, times *[2]syscall.Timespec) error { +func futimens(fd uintptr, atim, mtim int64) 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 !platform.IsAtLeastGo120 { @@ -27,9 +20,9 @@ func futimens(fd uintptr, times *[2]syscall.Timespec) error { } // Per docs, zero isn't a valid timestamp as it cannot be differentiated - // from nil. In both cases, it is a marker like syscall.UTIME_OMIT. + // from nil. In both cases, it is a marker like fsapi.UTIME_OMIT. // See https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfiletime - a, w := timespecToFiletime(times) + a, w := timespecToFiletime(atim, mtim) if a == nil && w == nil { return nil // both omitted, so nothing to change @@ -42,32 +35,16 @@ func futimens(fd uintptr, times *[2]syscall.Timespec) error { return syscall.SetFileTime(h, nil, a, w) } -func timespecToFiletime(times *[2]syscall.Timespec) (a, w *syscall.Filetime) { - // Handle when both inputs are current system time. - if times == nil || times[0].Nsec == UTIME_NOW && times[1].Nsec == UTIME_NOW { - now := time.Now().UnixNano() - ft := syscall.NsecToFiletime(now) - return &ft, &ft - } - - // Now, either one of the inputs is current time, or neither. This - // means we don't have a risk of re-reading the clock. - a = timespecToFileTime(times, 0) - w = timespecToFileTime(times, 1) +func timespecToFiletime(atim, mtim int64) (a, w *syscall.Filetime) { + a = timespecToFileTime(atim) + w = timespecToFileTime(mtim) return } -func timespecToFileTime(times *[2]syscall.Timespec, i int) *syscall.Filetime { - if times[i].Nsec == UTIME_OMIT { +func timespecToFileTime(tim int64) *syscall.Filetime { + if tim == fsapi.UTIME_OMIT { return nil } - - var nsec int64 - if times[i].Nsec == UTIME_NOW { - nsec = time.Now().UnixNano() - } else { - nsec = syscall.TimespecToNsec(times[i]) - } - ft := syscall.NsecToFiletime(nsec) + ft := syscall.NsecToFiletime(tim) return &ft } diff --git a/internal/sysfs/osfile.go b/internal/sysfs/osfile.go index 410a4d0e..ead9e49a 100644 --- a/internal/sysfs/osfile.go +++ b/internal/sysfs/osfile.go @@ -5,7 +5,6 @@ import ( "io/fs" "os" "runtime" - "syscall" experimentalsys "github.com/tetratelabs/wazero/experimental/sys" "github.com/tetratelabs/wazero/internal/fsapi" @@ -244,12 +243,12 @@ func (f *osFile) Datasync() experimentalsys.Errno { } // Utimens implements the same method as documented on fsapi.File -func (f *osFile) Utimens(times *[2]syscall.Timespec) experimentalsys.Errno { +func (f *osFile) Utimens(atim, mtim int64) experimentalsys.Errno { if f.closed { return experimentalsys.EBADF } - err := futimens(f.fd, times) + err := futimens(f.fd, atim, mtim) return experimentalsys.UnwrapOSError(err) } diff --git a/internal/sysfs/poll_windows.go b/internal/sysfs/poll_windows.go index 9b1cab9f..82c8b2ba 100644 --- a/internal/sysfs/poll_windows.go +++ b/internal/sysfs/poll_windows.go @@ -42,9 +42,6 @@ const pollInterval = 100 * time.Millisecond // _poll implements poll on Windows, for a subset of cases. // -// pollWithContext emulates the behavior of POSIX poll(2) on Windows, for a subset of cases, -// and it supports context cancellation. -// // fds may contain any number of file handles, but regular files and pipes are only processed for _POLLIN. // Stdin is a pipe, thus it is checked for readiness when present. Pipes are checked using PeekNamedPipe. // Regular files always immediately reported as ready, regardless their actual state and timeouts. diff --git a/internal/sysfs/readfs.go b/internal/sysfs/readfs.go index 1e96e2b4..6e5fd650 100644 --- a/internal/sysfs/readfs.go +++ b/internal/sysfs/readfs.go @@ -2,7 +2,6 @@ package sysfs import ( "io/fs" - "syscall" experimentalsys "github.com/tetratelabs/wazero/experimental/sys" "github.com/tetratelabs/wazero/internal/fsapi" @@ -98,7 +97,7 @@ func (r *readFS) Unlink(path string) experimentalsys.Errno { } // Utimens implements the same method as documented on fsapi.FS -func (r *readFS) Utimens(path string, times *[2]syscall.Timespec) experimentalsys.Errno { +func (r *readFS) Utimens(path string, atim, mtim int64) experimentalsys.Errno { return experimentalsys.EROFS } @@ -135,7 +134,7 @@ func (r *readFile) Datasync() experimentalsys.Errno { } // Utimens implements the same method as documented on fsapi.File. -func (r *readFile) Utimens(*[2]syscall.Timespec) experimentalsys.Errno { +func (r *readFile) Utimens(int64, int64) experimentalsys.Errno { return experimentalsys.EBADF } diff --git a/internal/sysfs/readfs_test.go b/internal/sysfs/readfs_test.go index b7b2c609..6e877ad7 100644 --- a/internal/sysfs/readfs_test.go +++ b/internal/sysfs/readfs_test.go @@ -114,7 +114,7 @@ func TestReadFS_UtimesNano(t *testing.T) { realPath := joinPath(tmpDir, path) require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600)) - err := testFS.Utimens(path, nil) + err := testFS.Utimens(path, fsapi.UTIME_OMIT, fsapi.UTIME_OMIT) require.EqualErrno(t, sys.EROFS, err) }