sysfs: decouples FS and File from the syscall package (#1602)

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2023-07-30 16:06:22 +08:00
committed by GitHub
parent 8d3874d3c8
commit 2382bbf730
23 changed files with 203 additions and 353 deletions

View File

@@ -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(&times)
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, &times)
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, &times)
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(&times)
return f.Utimens(atim, mtim)
}
}

View File

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

View File

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

10
internal/fsapi/time.go Normal file
View File

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

View File

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

View File

@@ -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, &times)
errno := fsc.RootFS().Utimens(path, atimeSec*1e9, mtimeSec*1e9)
return jsfsInvoke(ctx, mod, callback, errno)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(&times[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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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