Exposes sys.Stat_t as a portable alternative to syscall.Stat_t (#1567)
Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
62
RATIONALE.md
62
RATIONALE.md
@@ -1171,6 +1171,68 @@ See https://github.com/WebAssembly/stack-switching/discussions/38
|
||||
See https://github.com/WebAssembly/wasi-threads#what-can-be-skipped
|
||||
See https://slinkydeveloper.com/Kubernetes-controllers-A-New-Hope/
|
||||
|
||||
## sys.Stat_t
|
||||
|
||||
We expose `stat` information as `sys.Stat_t`, like `syscall.Stat_t` except
|
||||
defined without build constraints. For example, you can use `sys.Stat_t` on
|
||||
`GOOS=windows` which doesn't define `syscall.Stat_t`.
|
||||
|
||||
The first use case of this is to return inodes from `fs.FileInfo` without
|
||||
relying on platform-specifics. For example, a user could return `*sys.Stat_t`
|
||||
from `info.Sys()` and define a non-zero inode for a virtual file, or map a
|
||||
real inode to a virtual one.
|
||||
|
||||
Notable choices per field are listed below, where `sys.Stat_t` is unlike
|
||||
`syscall.Stat_t` on `GOOS=linux`, or needs clarification. One common issue
|
||||
not repeated below is that numeric fields are 64-bit when at least one platform
|
||||
defines it that large. Also, zero values are equivalent to nil or absent.
|
||||
|
||||
* `Dev` and `Ino` (`Inode`) are both defined unsigned as they are defined
|
||||
opaque, and most `syscall.Stat_t` also defined them unsigned. There are
|
||||
separate sections in this document discussing the impact of zero in `Ino`.
|
||||
* `Mode` is defined as a `fs.FileMode` even though that is not defined in POSIX
|
||||
and will not map to all possible values. This is because the current use is
|
||||
WASI, which doesn't define any types or features not already supported. By
|
||||
using `fs.FileMode`, we can re-use routine experience in Go.
|
||||
* `NLink` is unsigned because it is defined that way in `syscall.Stat_t`: there
|
||||
can never be less than zero links to a file. We suggest defaulting to 1 in
|
||||
conversions when information is not knowable because at least that many links
|
||||
exist.
|
||||
* `Size` is signed because it is defined that way in `syscall.Stat_t`: while
|
||||
regular files and directories will always be non-negative, irregular files
|
||||
are possibly negative or not defined. Notably sparse files are known to
|
||||
return negative values.
|
||||
* `Atim`, `Mtim` and `Ctim` are signed because they are defined that way in
|
||||
`syscall.Stat_t`: Negative values are time before 1970. The resolution is
|
||||
nanosecond because that's the maximum resolution currently supported in Go.
|
||||
|
||||
### Why do we use `sys.EpochNanos` instead of `time.Time` or similar?
|
||||
|
||||
To simplify documentation, we defined a type alias `sys.EpochNanos` for int64.
|
||||
`time.Time` is a data structure, and we could have used this for
|
||||
`syscall.Stat_t` time values. The most important reason we do not is conversion
|
||||
penalty deriving time from common types.
|
||||
|
||||
The most common ABI used in `wasip2`. This, and compatible ABI such as `wasix`,
|
||||
encode timestamps in memory as a 64-bit number. If we used `time.Time`, we
|
||||
would have to convert an underlying type like `syscall.Timespec` to `time.Time`
|
||||
only to later have to call `.UnixNano()` to convert it back to a 64-bit number.
|
||||
|
||||
In the future, the component model module "wasi-filesystem" may represent stat
|
||||
timestamps with a type shared with "wasi-clocks", abstractly structured similar
|
||||
to `time.Time`. However, component model intentionally does not define an ABI.
|
||||
It is likely that the canonical ABI for timestamp will be in two parts, but it
|
||||
is not required for it to be intermediately represented this way. A utility
|
||||
like `syscall.NsecToTimespec` could split an int64 so that it could be written
|
||||
to memory as 96 bytes (int64, int32), without allocating a struct.
|
||||
|
||||
Finally, some may confuse epoch nanoseconds with 32-bit epoch seconds. While
|
||||
32-bit epoch seconds has "The year 2038" problem, epoch nanoseconds has
|
||||
"The Year 2262" problem, which is even less concerning for this library. If
|
||||
the Go programming language and wazero exist in the 2200's, we can make a major
|
||||
version increment to adjust the `sys.EpochNanos` approach. Meanwhile, we have
|
||||
faster code.
|
||||
|
||||
## poll_oneoff
|
||||
|
||||
`poll_oneoff` is a WASI API for waiting for I/O events on multiple handles.
|
||||
|
||||
12
fsconfig.go
12
fsconfig.go
@@ -118,6 +118,18 @@ type FSConfig interface {
|
||||
// advise using WithDirMount instead. There will be behavior differences
|
||||
// between os.DirFS and WithDirMount, as the latter biases towards what's
|
||||
// expected from WASI implementations.
|
||||
//
|
||||
// # Custom fs.FileInfo
|
||||
//
|
||||
// The underlying implementation supports data not usually in fs.FileInfo
|
||||
// when `info.Sys` returns *sys.Stat_t. For example, a custom fs.FS can use
|
||||
// this approach to generate or mask sys.Inode data. Such a filesystem
|
||||
// needs to decorate any functions that can return fs.FileInfo:
|
||||
//
|
||||
// - `Stat` as defined on `fs.File` (always)
|
||||
// - `Readdir` as defined on `os.File` (if defined)
|
||||
//
|
||||
// See sys.NewStat_t for examples.
|
||||
WithFSMount(fs fs.FS, guestPath string) FSConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ var testdataIndex embed.FS
|
||||
var moduleConfig wazero.ModuleConfig
|
||||
|
||||
// This example shows how to configure an embed.FS.
|
||||
func Example_withFSConfig_embedFS() {
|
||||
func Example_fsConfig() {
|
||||
// Strip the embedded path testdata/
|
||||
rooted, err := fs.Sub(testdataIndex, "testdata")
|
||||
if err != nil {
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/tetratelabs/wazero/internal/sysfs"
|
||||
"github.com/tetratelabs/wazero/internal/wasip1"
|
||||
"github.com/tetratelabs/wazero/internal/wasm"
|
||||
sysapi "github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
// fdAdvise is the WASI function named FdAdviseName which provides file
|
||||
@@ -196,7 +197,7 @@ func fdFdstatGetFn(_ context.Context, mod api.Module, params []uint64) syscall.E
|
||||
}
|
||||
|
||||
var fdflags uint16
|
||||
var st fsapi.Stat_t
|
||||
var st sysapi.Stat_t
|
||||
var errno syscall.Errno
|
||||
f, ok := fsc.LookupFile(fd)
|
||||
if !ok {
|
||||
@@ -445,7 +446,7 @@ func getWasiFiletype(fm fs.FileMode) uint8 {
|
||||
}
|
||||
}
|
||||
|
||||
func writeFilestat(buf []byte, st *fsapi.Stat_t, ftype uint8) (errno syscall.Errno) {
|
||||
func writeFilestat(buf []byte, st *sysapi.Stat_t, ftype uint8) (errno syscall.Errno) {
|
||||
le.PutUint64(buf, st.Dev)
|
||||
le.PutUint64(buf[8:], st.Ino)
|
||||
le.PutUint64(buf[16:], uint64(ftype))
|
||||
@@ -1018,7 +1019,7 @@ func writeDirents(buf []byte, dirents []fsapi.Dirent, d_next uint64, direntCount
|
||||
}
|
||||
|
||||
// writeDirent writes DirentSize bytes
|
||||
func writeDirent(buf []byte, dNext uint64, ino fsapi.Ino, dNamlen uint32, dType fs.FileMode) {
|
||||
func writeDirent(buf []byte, dNext uint64, ino sysapi.Inode, dNamlen uint32, dType fs.FileMode) {
|
||||
le.PutUint64(buf, dNext) // d_next
|
||||
le.PutUint64(buf[8:], ino) // d_ino
|
||||
le.PutUint32(buf[16:], dNamlen) // d_namlen
|
||||
@@ -1399,7 +1400,7 @@ func pathFilestatGetFn(_ context.Context, mod api.Module, params []uint64) sysca
|
||||
}
|
||||
|
||||
// Stat the file without allocating a file descriptor.
|
||||
var st fsapi.Stat_t
|
||||
var st sysapi.Stat_t
|
||||
|
||||
if (flags & wasip1.LOOKUP_SYMLINK_FOLLOW) == 0 {
|
||||
st, errno = preopen.Lstat(pathName)
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/tetratelabs/wazero/internal/u64"
|
||||
"github.com/tetratelabs/wazero/internal/wasip1"
|
||||
"github.com/tetratelabs/wazero/internal/wasm"
|
||||
sysapi "github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
func Test_fdAdvise(t *testing.T) {
|
||||
@@ -3518,7 +3519,7 @@ func Test_pathFilestatSetTimes(t *testing.T) {
|
||||
sys := mod.(*wasm.ModuleInstance).Sys
|
||||
fsc := sys.FS()
|
||||
|
||||
var oldSt fsapi.Stat_t
|
||||
var oldSt sysapi.Stat_t
|
||||
var errno syscall.Errno
|
||||
if tc.expectedErrno == wasip1.ErrnoSuccess {
|
||||
oldSt, errno = fsc.RootFS().Stat(pathName)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
"github.com/tetratelabs/wazero/internal/wasip1"
|
||||
"github.com/tetratelabs/wazero/internal/wasm"
|
||||
sysapi "github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
func Test_pollOneoff(t *testing.T) {
|
||||
@@ -536,8 +537,8 @@ var fdReadSub = fdReadSubFd(byte(sys.FdStdin))
|
||||
type ttyStat struct{}
|
||||
|
||||
// Stat implements the same method as documented on fsapi.File
|
||||
func (ttyStat) Stat() (fsapi.Stat_t, syscall.Errno) {
|
||||
return fsapi.Stat_t{
|
||||
func (ttyStat) Stat() (sysapi.Stat_t, syscall.Errno) {
|
||||
return sysapi.Stat_t{
|
||||
Mode: fs.ModeDevice | fs.ModeCharDevice,
|
||||
Nlink: 1,
|
||||
}, 0
|
||||
|
||||
@@ -5,18 +5,9 @@ import (
|
||||
"io/fs"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Ino is the file serial number, or zero if unknown.
|
||||
//
|
||||
// The inode is used for a file equivalence, like os.SameFile, so any constant
|
||||
// value will interfere that.
|
||||
//
|
||||
// When zero is returned by File.Readdir, certain callers will fan-out to
|
||||
// File.Stat to retrieve a non-zero value. Callers using this for darwin's
|
||||
// definition of `getdirentries` conflate zero `d_fileno` with a deleted file
|
||||
// and skip the entry. See /RATIONALE.md for more on this.
|
||||
type Ino = uint64
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
// FileType is fs.FileMode masked on fs.ModeType. For example, zero is a
|
||||
// regular file, fs.ModeDir is a directory and fs.ModeIrregular is unknown.
|
||||
@@ -38,7 +29,7 @@ type FileType = fs.FileMode
|
||||
type Dirent struct {
|
||||
// Ino is the file serial number, or zero if not available. See Ino for
|
||||
// more details including impact returning a zero value.
|
||||
Ino Ino
|
||||
Ino sys.Inode
|
||||
|
||||
// Name is the base name of the directory entry. Empty is invalid.
|
||||
Name string
|
||||
|
||||
@@ -3,6 +3,8 @@ package fsapi
|
||||
import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
// File is a writeable fs.File bridge backed by syscall functions needed for ABI
|
||||
@@ -55,7 +57,7 @@ type File interface {
|
||||
//
|
||||
// - Implementations should cache this result.
|
||||
// - This combined with Dev can implement os.SameFile.
|
||||
Ino() (Ino, syscall.Errno)
|
||||
Ino() (sys.Inode, syscall.Errno)
|
||||
|
||||
// IsDir returns true if this file is a directory or an error there was an
|
||||
// error retrieving this information.
|
||||
@@ -132,7 +134,7 @@ type File interface {
|
||||
// - A fs.FileInfo backed implementation sets atim, mtim and ctim to the
|
||||
// same value.
|
||||
// - Windows allows you to stat a closed directory.
|
||||
Stat() (Stat_t, syscall.Errno)
|
||||
Stat() (sys.Stat_t, syscall.Errno)
|
||||
|
||||
// Read attempts to read all bytes in the file into `buf`, and returns the
|
||||
// count read even on error.
|
||||
|
||||
@@ -3,6 +3,8 @@ package fsapi
|
||||
import (
|
||||
"io/fs"
|
||||
"syscall"
|
||||
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
// FS is a writeable fs.FS bridge backed by syscall functions needed for ABI
|
||||
@@ -79,7 +81,7 @@ type FS interface {
|
||||
// same value.
|
||||
// - When the path is a symbolic link, the stat returned is for the link,
|
||||
// not the file it refers to.
|
||||
Lstat(path string) (Stat_t, syscall.Errno)
|
||||
Lstat(path string) (sys.Stat_t, syscall.Errno)
|
||||
|
||||
// Stat gets file status.
|
||||
//
|
||||
@@ -99,7 +101,7 @@ type FS interface {
|
||||
// same value.
|
||||
// - When the path is a symbolic link, the stat returned is for the file
|
||||
// it refers to.
|
||||
Stat(path string) (Stat_t, syscall.Errno)
|
||||
Stat(path string) (sys.Stat_t, syscall.Errno)
|
||||
|
||||
// Mkdir makes a directory.
|
||||
//
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package fsapi
|
||||
|
||||
import "io/fs"
|
||||
|
||||
// Stat_t is similar to syscall.Stat_t, and fields frequently used by
|
||||
// WebAssembly ABI including wasip1 and wasi-filesystem (a.k.a. wasip2).
|
||||
//
|
||||
// # Note
|
||||
//
|
||||
// Zero values may be returned where not available. For example, fs.FileInfo
|
||||
// implementations may not be able to provide Ino values.
|
||||
type Stat_t struct {
|
||||
// Dev is the device ID of device containing the file.
|
||||
Dev uint64
|
||||
|
||||
// Ino is the file serial number, or zero if not available. See Ino for
|
||||
// more details including impact returning a zero value.
|
||||
Ino Ino
|
||||
|
||||
// Mode is the same as Mode on fs.FileInfo containing bits to identify the
|
||||
// type of the file (fs.ModeType) and its permissions (fs.ModePerm).
|
||||
Mode fs.FileMode
|
||||
|
||||
/// Nlink is the number of hard links to the file.
|
||||
Nlink uint64
|
||||
// ^^ uint64 not uint16 to accept widest syscall.Stat_t.Nlink
|
||||
|
||||
// Size is the length in bytes for regular files. For symbolic links, this
|
||||
// is length in bytes of the pathname contained in the symbolic link.
|
||||
Size int64
|
||||
// ^^ int64 not uint64 to defer to fs.FileInfo
|
||||
|
||||
// Atim is the last data access timestamp in epoch nanoseconds.
|
||||
Atim int64
|
||||
|
||||
// Mtim is the last data modification timestamp in epoch nanoseconds.
|
||||
Mtim int64
|
||||
|
||||
// Ctim is the last file status change timestamp in epoch nanoseconds.
|
||||
Ctim int64
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"io/fs"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
// UnimplementedFS is an FS that returns syscall.ENOSYS for all functions,
|
||||
@@ -26,13 +28,13 @@ func (UnimplementedFS) OpenFile(path string, flag int, perm fs.FileMode) (File,
|
||||
}
|
||||
|
||||
// Lstat implements FS.Lstat
|
||||
func (UnimplementedFS) Lstat(path string) (Stat_t, syscall.Errno) {
|
||||
return Stat_t{}, syscall.ENOSYS
|
||||
func (UnimplementedFS) Lstat(path string) (sys.Stat_t, syscall.Errno) {
|
||||
return sys.Stat_t{}, syscall.ENOSYS
|
||||
}
|
||||
|
||||
// Stat implements FS.Stat
|
||||
func (UnimplementedFS) Stat(path string) (Stat_t, syscall.Errno) {
|
||||
return Stat_t{}, syscall.ENOSYS
|
||||
func (UnimplementedFS) Stat(path string) (sys.Stat_t, syscall.Errno) {
|
||||
return sys.Stat_t{}, syscall.ENOSYS
|
||||
}
|
||||
|
||||
// Readlink implements FS.Readlink
|
||||
@@ -97,7 +99,7 @@ func (UnimplementedFile) Dev() (uint64, syscall.Errno) {
|
||||
}
|
||||
|
||||
// Ino implements File.Ino
|
||||
func (UnimplementedFile) Ino() (Ino, syscall.Errno) {
|
||||
func (UnimplementedFile) Ino() (sys.Inode, syscall.Errno) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
@@ -127,8 +129,8 @@ func (UnimplementedFile) SetNonblock(bool) syscall.Errno {
|
||||
}
|
||||
|
||||
// Stat implements File.Stat
|
||||
func (UnimplementedFile) Stat() (Stat_t, syscall.Errno) {
|
||||
return Stat_t{}, syscall.ENOSYS
|
||||
func (UnimplementedFile) Stat() (sys.Stat_t, syscall.Errno) {
|
||||
return sys.Stat_t{}, syscall.ENOSYS
|
||||
}
|
||||
|
||||
// Read implements File.Read
|
||||
|
||||
@@ -4,6 +4,6 @@ package fstest
|
||||
|
||||
import "io/fs"
|
||||
|
||||
func timesFromFileInfo(t fs.FileInfo) (atim, mtime int64) {
|
||||
func timesFromFileInfo(fs.FileInfo) (atim, mtime int64) {
|
||||
panic("unexpected")
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func timesFromFileInfo(t fs.FileInfo) (atim, mtime int64) {
|
||||
if d, ok := t.Sys().(*syscall.Win32FileAttributeData); ok {
|
||||
func timesFromFileInfo(info fs.FileInfo) (atim, mtime int64) {
|
||||
if d, ok := info.Sys().(*syscall.Win32FileAttributeData); ok {
|
||||
return d.LastAccessTime.Nanoseconds(), d.LastWriteTime.Nanoseconds()
|
||||
} else {
|
||||
panic("unexpected")
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/internal/gojs/custom"
|
||||
"github.com/tetratelabs/wazero/internal/gojs/goos"
|
||||
"github.com/tetratelabs/wazero/internal/gojs/util"
|
||||
internalsys "github.com/tetratelabs/wazero/internal/sys"
|
||||
"github.com/tetratelabs/wazero/internal/wasm"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -184,7 +184,7 @@ func syscallFstat(fsc *internalsys.FSContext, fd int32) (*jsSt, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func newJsSt(st fsapi.Stat_t) *jsSt {
|
||||
func newJsSt(st sys.Stat_t) *jsSt {
|
||||
ret := &jsSt{}
|
||||
ret.isDir = st.Mode.IsDir()
|
||||
ret.dev = st.Dev
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
// compile-time check to ensure lazyDir implements fsapi.File.
|
||||
@@ -27,7 +28,7 @@ func (r *lazyDir) Dev() (uint64, syscall.Errno) {
|
||||
}
|
||||
|
||||
// Ino implements the same method as documented on fsapi.File
|
||||
func (r *lazyDir) Ino() (fsapi.Ino, syscall.Errno) {
|
||||
func (r *lazyDir) Ino() (sys.Inode, syscall.Errno) {
|
||||
if f, ok := r.file(); !ok {
|
||||
return 0, syscall.EBADF
|
||||
} else {
|
||||
@@ -66,9 +67,9 @@ func (r *lazyDir) Seek(offset int64, whence int) (newOffset int64, errno syscall
|
||||
}
|
||||
|
||||
// Stat implements the same method as documented on fsapi.File
|
||||
func (r *lazyDir) Stat() (fsapi.Stat_t, syscall.Errno) {
|
||||
func (r *lazyDir) Stat() (sys.Stat_t, syscall.Errno) {
|
||||
if f, ok := r.file(); !ok {
|
||||
return fsapi.Stat_t{}, syscall.EBADF
|
||||
return sys.Stat_t{}, syscall.EBADF
|
||||
} else {
|
||||
return f.Stat()
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
"github.com/tetratelabs/wazero/internal/sysfs"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
// StdinFile is a fs.ModeDevice file for use implementing FdStdin.
|
||||
@@ -80,8 +81,8 @@ type noopStdioFile struct {
|
||||
}
|
||||
|
||||
// Stat implements the same method as documented on fsapi.File
|
||||
func (noopStdioFile) Stat() (fsapi.Stat_t, syscall.Errno) {
|
||||
return fsapi.Stat_t{Mode: modeDevice, Nlink: 1}, 0
|
||||
func (noopStdioFile) Stat() (sys.Stat_t, syscall.Errno) {
|
||||
return sys.Stat_t{Mode: modeDevice, Nlink: 1}, 0
|
||||
}
|
||||
|
||||
// IsDir implements the same method as documented on fsapi.File
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
// Adapt adapts the input to fsapi.FS unless it is already one. Use NewDirFS instead
|
||||
@@ -42,17 +43,17 @@ func (a *adapter) OpenFile(path string, flag int, perm fs.FileMode) (fsapi.File,
|
||||
}
|
||||
|
||||
// Stat implements the same method as documented on fsapi.FS
|
||||
func (a *adapter) Stat(path string) (fsapi.Stat_t, syscall.Errno) {
|
||||
func (a *adapter) Stat(path string) (sys.Stat_t, syscall.Errno) {
|
||||
f, errno := a.OpenFile(path, syscall.O_RDONLY, 0)
|
||||
if errno != 0 {
|
||||
return fsapi.Stat_t{}, errno
|
||||
return sys.Stat_t{}, errno
|
||||
}
|
||||
defer f.Close()
|
||||
return f.Stat()
|
||||
}
|
||||
|
||||
// Lstat implements the same method as documented on fsapi.FS
|
||||
func (a *adapter) Lstat(path string) (fsapi.Stat_t, syscall.Errno) {
|
||||
func (a *adapter) Lstat(path string) (sys.Stat_t, syscall.Errno) {
|
||||
// At this time, we make the assumption that fsapi.FS instances do not support
|
||||
// symbolic links, therefore Lstat is the same as Stat. This is obviously
|
||||
// not true but until fsapi.FS has a solid story for how to handle symlinks we
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/internal/fstest"
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
func TestAdapt_nil(t *testing.T) {
|
||||
@@ -168,18 +169,69 @@ func TestAdapt_HackedWrites(t *testing.T) {
|
||||
// when a fs.FS returns an os.File or methods we use from it.
|
||||
type MaskOsFS struct {
|
||||
Fs fs.FS
|
||||
|
||||
// ZeroIno helps us test stat with sys.Stat_t work.
|
||||
ZeroIno bool
|
||||
}
|
||||
|
||||
func (f *MaskOsFS) Open(name string) (fs.File, error) {
|
||||
if f, err := f.Fs.Open(name); err != nil {
|
||||
// Open implements the same method as documented on fs.FS
|
||||
func (fs *MaskOsFS) Open(name string) (fs.File, error) {
|
||||
if f, err := fs.Fs.Open(name); err != nil {
|
||||
return nil, err
|
||||
} else if osF, ok := f.(*os.File); !ok {
|
||||
return nil, fmt.Errorf("input not an os.File %v", osF)
|
||||
} else if fs.ZeroIno {
|
||||
return &zeroInoOsFile{osF}, nil
|
||||
} else {
|
||||
return struct{ methodsUsedByFsAdapter }{osF}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// zeroInoOsFile wraps an os.File to override functions that can return
|
||||
// fs.FileInfo.
|
||||
type zeroInoOsFile struct{ *os.File }
|
||||
|
||||
// Readdir implements the same method as documented on os.File
|
||||
func (f *zeroInoOsFile) Readdir(n int) ([]fs.FileInfo, error) {
|
||||
infos, err := f.File.Readdir(n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range infos {
|
||||
infos[i] = withZeroIno(infos[i])
|
||||
}
|
||||
return infos, nil
|
||||
}
|
||||
|
||||
// Stat implements the same method as documented on fs.File
|
||||
func (f *zeroInoOsFile) Stat() (fs.FileInfo, error) {
|
||||
info, err := f.File.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return withZeroIno(info), nil
|
||||
}
|
||||
|
||||
// withZeroIno clears the sys.Inode which is non-zero on most operating
|
||||
// systems. We test for zero ensure stat logic always checks for sys.Stat_t
|
||||
// first. If that failed, at least one OS would return a non-zero value.
|
||||
func withZeroIno(info fs.FileInfo) fs.FileInfo {
|
||||
st := sys.NewStat_t(info)
|
||||
st.Ino = 0 // clear
|
||||
return &sysFileInfo{info, &st}
|
||||
}
|
||||
|
||||
// sysFileInfo wraps a fs.FileInfo to return *sys.Stat_t from Sys.
|
||||
type sysFileInfo struct {
|
||||
fs.FileInfo
|
||||
sys *sys.Stat_t
|
||||
}
|
||||
|
||||
// Sys implements the same method as documented on fs.FileInfo
|
||||
func (i *sysFileInfo) Sys() any {
|
||||
return i.sys
|
||||
}
|
||||
|
||||
// methodsUsedByFsAdapter includes all functions Adapt supports. This includes
|
||||
// the ability to write files and seek files or directories (directories only
|
||||
// to zero).
|
||||
|
||||
@@ -22,6 +22,7 @@ func TestFSFileReaddir(t *testing.T) {
|
||||
require.NoError(t, fstest.WriteTestFiles(tmpDir))
|
||||
dirFS := os.DirFS(tmpDir)
|
||||
maskFS := &sysfs.MaskOsFS{Fs: dirFS}
|
||||
maskFSZeroIno := &sysfs.MaskOsFS{Fs: os.DirFS(tmpDir), ZeroIno: true}
|
||||
|
||||
expectIno := runtime.GOOS != "windows"
|
||||
|
||||
@@ -30,9 +31,10 @@ func TestFSFileReaddir(t *testing.T) {
|
||||
fs fs.FS
|
||||
expectIno bool
|
||||
}{
|
||||
{name: "os.DirFS", fs: dirFS, expectIno: expectIno}, // To test readdirFile
|
||||
{name: "mask(os.DirFS)", fs: maskFS, expectIno: expectIno}, // To prove no reliance on os.File
|
||||
{name: "fstest.MapFS", fs: fstest.FS, expectIno: false}, // To test adaptation of ReadDirFile
|
||||
{name: "os.DirFS", fs: dirFS, expectIno: expectIno}, // To test readdirFile
|
||||
{name: "mask(os.DirFS)", fs: maskFS, expectIno: expectIno}, // To prove no reliance on os.File
|
||||
{name: "mask(os.DirFS) ZeroIno", fs: maskFSZeroIno, expectIno: false}, // To prove Stat_t overrides
|
||||
{name: "fstest.MapFS", fs: fstest.FS, expectIno: false}, // To test adaptation of ReadDirFile
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
func NewDirFS(dir string) fsapi.FS {
|
||||
@@ -42,12 +43,12 @@ func (d *dirFS) OpenFile(path string, flag int, perm fs.FileMode) (fsapi.File, s
|
||||
}
|
||||
|
||||
// Lstat implements the same method as documented on fsapi.FS
|
||||
func (d *dirFS) Lstat(path string) (fsapi.Stat_t, syscall.Errno) {
|
||||
func (d *dirFS) Lstat(path string) (sys.Stat_t, syscall.Errno) {
|
||||
return lstat(d.join(path))
|
||||
}
|
||||
|
||||
// Stat implements the same method as documented on fsapi.FS
|
||||
func (d *dirFS) Stat(path string) (fsapi.Stat_t, syscall.Errno) {
|
||||
func (d *dirFS) Stat(path string) (sys.Stat_t, syscall.Errno) {
|
||||
return stat(d.join(path))
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
func NewStdioFile(stdin bool, f fs.File) (fsapi.File, error) {
|
||||
@@ -33,7 +34,7 @@ func NewStdioFile(stdin bool, f fs.File) (fsapi.File, error) {
|
||||
} else {
|
||||
file = &fsFile{file: f}
|
||||
}
|
||||
return &stdioFile{File: file, st: fsapi.Stat_t{Mode: mode, Nlink: 1}}, nil
|
||||
return &stdioFile{File: file, st: sys.Stat_t{Mode: mode, Nlink: 1}}, nil
|
||||
}
|
||||
|
||||
func OpenFile(path string, flag int, perm fs.FileMode) (*os.File, syscall.Errno) {
|
||||
@@ -66,7 +67,7 @@ func OpenFSFile(fs fs.FS, path string, flag int, perm fs.FileMode) (fsapi.File,
|
||||
|
||||
type stdioFile struct {
|
||||
fsapi.File
|
||||
st fsapi.Stat_t
|
||||
st sys.Stat_t
|
||||
}
|
||||
|
||||
// SetAppend implements File.SetAppend
|
||||
@@ -81,7 +82,7 @@ func (f *stdioFile) IsAppend() bool {
|
||||
}
|
||||
|
||||
// Stat implements File.Stat
|
||||
func (f *stdioFile) Stat() (fsapi.Stat_t, syscall.Errno) {
|
||||
func (f *stdioFile) Stat() (sys.Stat_t, syscall.Errno) {
|
||||
return f.st, 0
|
||||
}
|
||||
|
||||
@@ -124,7 +125,7 @@ type cachedStat struct {
|
||||
dev uint64
|
||||
|
||||
// dev is the same as fsapi.Stat_t Ino.
|
||||
ino fsapi.Ino
|
||||
ino sys.Inode
|
||||
|
||||
// isDir is fsapi.Stat_t Mode masked with fs.ModeDir
|
||||
isDir bool
|
||||
@@ -132,7 +133,7 @@ type cachedStat struct {
|
||||
|
||||
// cachedStat returns the cacheable parts of fsapi.Stat_t or an error if they
|
||||
// couldn't be retrieved.
|
||||
func (f *fsFile) cachedStat() (dev uint64, ino fsapi.Ino, isDir bool, errno syscall.Errno) {
|
||||
func (f *fsFile) cachedStat() (dev uint64, ino sys.Inode, isDir bool, errno syscall.Errno) {
|
||||
if f.cachedSt == nil {
|
||||
if _, errno = f.Stat(); errno != 0 {
|
||||
return
|
||||
@@ -148,7 +149,7 @@ func (f *fsFile) Dev() (uint64, syscall.Errno) {
|
||||
}
|
||||
|
||||
// Ino implements the same method as documented on fsapi.File
|
||||
func (f *fsFile) Ino() (fsapi.Ino, syscall.Errno) {
|
||||
func (f *fsFile) Ino() (sys.Inode, syscall.Errno) {
|
||||
_, ino, _, errno := f.cachedStat()
|
||||
return ino, errno
|
||||
}
|
||||
@@ -170,9 +171,9 @@ func (f *fsFile) SetAppend(bool) (errno syscall.Errno) {
|
||||
}
|
||||
|
||||
// Stat implements the same method as documented on fsapi.File
|
||||
func (f *fsFile) Stat() (fsapi.Stat_t, syscall.Errno) {
|
||||
func (f *fsFile) Stat() (sys.Stat_t, syscall.Errno) {
|
||||
if f.closed {
|
||||
return fsapi.Stat_t{}, syscall.EBADF
|
||||
return sys.Stat_t{}, syscall.EBADF
|
||||
}
|
||||
|
||||
st, errno := statFile(f.file)
|
||||
@@ -435,9 +436,11 @@ func readdir(f readdirFile, path string, n int) (dirents []fsapi.Dirent, errno s
|
||||
dirents = make([]fsapi.Dirent, 0, len(fis))
|
||||
|
||||
// linux/darwin won't have to fan out to lstat, but windows will.
|
||||
var ino fsapi.Ino
|
||||
var ino sys.Inode
|
||||
for fi := range fis {
|
||||
t := fis[fi]
|
||||
// inoFromFileInfo is more efficient than sys.NewStat_t, as it gets the
|
||||
// inode without allocating an instance and filling other fields.
|
||||
if ino, errno = inoFromFileInfo(path, t); errno != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
//go:embed file_test.go
|
||||
@@ -147,7 +148,7 @@ func TestFileIno(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fs fs.FS
|
||||
expectedIno fsapi.Ino
|
||||
expectedIno sys.Inode
|
||||
}{
|
||||
{name: "os.DirFS", fs: dirFS, expectedIno: st.Ino},
|
||||
{name: "embed.api.FS", fs: embedFS},
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -107,7 +107,7 @@ func normalizeTimespec(path string, times *[2]syscall.Timespec, i int) (ts sysca
|
||||
// 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 fsapi.Stat_t
|
||||
var st sys.Stat_t
|
||||
if st, err = stat(path); err != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
func newOsFile(openPath string, openFlag int, openPerm fs.FileMode, f *os.File) fsapi.File {
|
||||
@@ -44,7 +45,7 @@ type osFile struct {
|
||||
|
||||
// cachedStat returns the cacheable parts of fsapi.Stat_t or an error if they
|
||||
// couldn't be retrieved.
|
||||
func (f *osFile) cachedStat() (dev uint64, ino fsapi.Ino, isDir bool, errno syscall.Errno) {
|
||||
func (f *osFile) cachedStat() (dev uint64, ino sys.Inode, isDir bool, errno syscall.Errno) {
|
||||
if f.cachedSt == nil {
|
||||
if _, errno = f.Stat(); errno != 0 {
|
||||
return
|
||||
@@ -60,7 +61,7 @@ func (f *osFile) Dev() (uint64, syscall.Errno) {
|
||||
}
|
||||
|
||||
// Ino implements the same method as documented on fsapi.File
|
||||
func (f *osFile) Ino() (fsapi.Ino, syscall.Errno) {
|
||||
func (f *osFile) Ino() (sys.Inode, syscall.Errno) {
|
||||
_, ino, _, errno := f.cachedStat()
|
||||
return ino, errno
|
||||
}
|
||||
@@ -123,9 +124,9 @@ func (f *osFile) SetNonblock(enable bool) (errno syscall.Errno) {
|
||||
}
|
||||
|
||||
// Stat implements the same method as documented on fsapi.File
|
||||
func (f *osFile) Stat() (fsapi.Stat_t, syscall.Errno) {
|
||||
func (f *osFile) Stat() (sys.Stat_t, syscall.Errno) {
|
||||
if f.closed {
|
||||
return fsapi.Stat_t{}, syscall.EBADF
|
||||
return sys.Stat_t{}, syscall.EBADF
|
||||
}
|
||||
|
||||
st, errno := statFile(f.file)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
// NewReadFS is used to mask an existing fsapi.FS for reads. Notably, this allows
|
||||
@@ -76,7 +77,7 @@ func (r *readFile) Dev() (uint64, syscall.Errno) {
|
||||
}
|
||||
|
||||
// Ino implements the same method as documented on fsapi.File.
|
||||
func (r *readFile) Ino() (fsapi.Ino, syscall.Errno) {
|
||||
func (r *readFile) Ino() (sys.Inode, syscall.Errno) {
|
||||
return r.f.Ino()
|
||||
}
|
||||
|
||||
@@ -106,7 +107,7 @@ func (r *readFile) SetAppend(enabled bool) syscall.Errno {
|
||||
}
|
||||
|
||||
// Stat implements the same method as documented on fsapi.File.
|
||||
func (r *readFile) Stat() (fsapi.Stat_t, syscall.Errno) {
|
||||
func (r *readFile) Stat() (sys.Stat_t, syscall.Errno) {
|
||||
return r.f.Stat()
|
||||
}
|
||||
|
||||
@@ -180,12 +181,12 @@ func (r *readFile) PollRead(timeout *time.Duration) (ready bool, errno syscall.E
|
||||
}
|
||||
|
||||
// Lstat implements the same method as documented on fsapi.FS
|
||||
func (r *readFS) Lstat(path string) (fsapi.Stat_t, syscall.Errno) {
|
||||
func (r *readFS) Lstat(path string) (sys.Stat_t, syscall.Errno) {
|
||||
return r.fs.Lstat(path)
|
||||
}
|
||||
|
||||
// Stat implements the same method as documented on fsapi.FS
|
||||
func (r *readFS) Stat(path string) (fsapi.Stat_t, syscall.Errno) {
|
||||
func (r *readFS) Stat(path string) (sys.Stat_t, syscall.Errno) {
|
||||
return r.fs.Stat(path)
|
||||
}
|
||||
|
||||
|
||||
@@ -152,10 +152,16 @@ func TestReadFS_Open_Read(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "mask(os.DirFS)",
|
||||
fs: NewReadFS(Adapt(&MaskOsFS{os.DirFS(tmpDir)})),
|
||||
fs: NewReadFS(Adapt(&MaskOsFS{os.DirFS(tmpDir), false})),
|
||||
expectFileIno: statSetsIno(),
|
||||
expectDirIno: runtime.GOOS != "windows",
|
||||
},
|
||||
{
|
||||
name: "mask(os.DirFS) ZeroIno",
|
||||
fs: NewReadFS(Adapt(&MaskOsFS{os.DirFS(tmpDir), true})),
|
||||
expectFileIno: false,
|
||||
expectDirIno: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
socketapi "github.com/tetratelabs/wazero/internal/sock"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
// NewTCPListenerFile creates a socketapi.TCPSock for a given *net.TCPListener.
|
||||
@@ -30,7 +31,7 @@ func (*baseSockFile) IsDir() (bool, syscall.Errno) {
|
||||
}
|
||||
|
||||
// Stat implements the same method as documented on File.Stat
|
||||
func (f *baseSockFile) Stat() (fs fsapi.Stat_t, errno syscall.Errno) {
|
||||
func (f *baseSockFile) Stat() (fs sys.Stat_t, errno syscall.Errno) {
|
||||
// The mode is not really important, but it should be neither a regular file nor a directory.
|
||||
fs.Mode = os.ModeIrregular
|
||||
return
|
||||
|
||||
@@ -4,28 +4,14 @@ import (
|
||||
"io/fs"
|
||||
"syscall"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
func defaultStatFile(f fs.File) (fsapi.Stat_t, syscall.Errno) {
|
||||
if t, err := f.Stat(); err != nil {
|
||||
return fsapi.Stat_t{}, platform.UnwrapOSError(err)
|
||||
func defaultStatFile(f fs.File) (sys.Stat_t, syscall.Errno) {
|
||||
if info, err := f.Stat(); err != nil {
|
||||
return sys.Stat_t{}, platform.UnwrapOSError(err)
|
||||
} else {
|
||||
return statFromFileInfo(t), 0
|
||||
return sys.NewStat_t(info), 0
|
||||
}
|
||||
}
|
||||
|
||||
func statFromDefaultFileInfo(t fs.FileInfo) fsapi.Stat_t {
|
||||
st := fsapi.Stat_t{}
|
||||
st.Ino = 0
|
||||
st.Dev = 0
|
||||
st.Mode = t.Mode()
|
||||
st.Nlink = 1
|
||||
st.Size = t.Size()
|
||||
mtim := t.ModTime().UnixNano() // Set all times to the mod time
|
||||
st.Atim = mtim
|
||||
st.Mtim = mtim
|
||||
st.Ctim = mtim
|
||||
return st
|
||||
}
|
||||
|
||||
@@ -7,52 +7,37 @@ import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
func lstat(path string) (fsapi.Stat_t, syscall.Errno) {
|
||||
if t, err := os.Lstat(path); err != nil {
|
||||
return fsapi.Stat_t{}, platform.UnwrapOSError(err)
|
||||
func lstat(path string) (sys.Stat_t, syscall.Errno) {
|
||||
if info, err := os.Lstat(path); err != nil {
|
||||
return sys.Stat_t{}, platform.UnwrapOSError(err)
|
||||
} else {
|
||||
return statFromFileInfo(t), 0
|
||||
return sys.NewStat_t(info), 0
|
||||
}
|
||||
}
|
||||
|
||||
func stat(path string) (fsapi.Stat_t, syscall.Errno) {
|
||||
if t, err := os.Stat(path); err != nil {
|
||||
return fsapi.Stat_t{}, platform.UnwrapOSError(err)
|
||||
func stat(path string) (sys.Stat_t, syscall.Errno) {
|
||||
if info, err := os.Stat(path); err != nil {
|
||||
return sys.Stat_t{}, platform.UnwrapOSError(err)
|
||||
} else {
|
||||
return statFromFileInfo(t), 0
|
||||
return sys.NewStat_t(info), 0
|
||||
}
|
||||
}
|
||||
|
||||
func statFile(f fs.File) (fsapi.Stat_t, syscall.Errno) {
|
||||
func statFile(f fs.File) (sys.Stat_t, syscall.Errno) {
|
||||
return defaultStatFile(f)
|
||||
}
|
||||
|
||||
func inoFromFileInfo(_ string, t fs.FileInfo) (ino fsapi.Ino, err syscall.Errno) {
|
||||
if d, ok := t.Sys().(*syscall.Stat_t); ok {
|
||||
ino = d.Ino
|
||||
func inoFromFileInfo(_ string, info fs.FileInfo) (sys.Inode, syscall.Errno) {
|
||||
switch v := info.Sys().(type) {
|
||||
case *sys.Stat_t:
|
||||
return v.Ino, 0
|
||||
case *syscall.Stat_t:
|
||||
return v.Ino, 0
|
||||
default:
|
||||
return 0, 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func statFromFileInfo(t fs.FileInfo) fsapi.Stat_t {
|
||||
if d, ok := t.Sys().(*syscall.Stat_t); ok {
|
||||
st := fsapi.Stat_t{}
|
||||
st.Dev = uint64(d.Dev)
|
||||
st.Ino = d.Ino
|
||||
st.Mode = t.Mode()
|
||||
st.Nlink = uint64(d.Nlink)
|
||||
st.Size = d.Size
|
||||
atime := d.Atimespec
|
||||
st.Atim = atime.Sec*1e9 + atime.Nsec
|
||||
mtime := d.Mtimespec
|
||||
st.Mtim = mtime.Sec*1e9 + mtime.Nsec
|
||||
ctime := d.Ctimespec
|
||||
st.Ctim = ctime.Sec*1e9 + ctime.Nsec
|
||||
return st
|
||||
}
|
||||
return statFromDefaultFileInfo(t)
|
||||
}
|
||||
|
||||
@@ -10,52 +10,37 @@ import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
func lstat(path string) (fsapi.Stat_t, syscall.Errno) {
|
||||
if t, err := os.Lstat(path); err != nil {
|
||||
return fsapi.Stat_t{}, platform.UnwrapOSError(err)
|
||||
func lstat(path string) (sys.Stat_t, syscall.Errno) {
|
||||
if info, err := os.Lstat(path); err != nil {
|
||||
return sys.Stat_t{}, platform.UnwrapOSError(err)
|
||||
} else {
|
||||
return statFromFileInfo(t), 0
|
||||
return sys.NewStat_t(info), 0
|
||||
}
|
||||
}
|
||||
|
||||
func stat(path string) (fsapi.Stat_t, syscall.Errno) {
|
||||
if t, err := os.Stat(path); err != nil {
|
||||
return fsapi.Stat_t{}, platform.UnwrapOSError(err)
|
||||
func stat(path string) (sys.Stat_t, syscall.Errno) {
|
||||
if info, err := os.Stat(path); err != nil {
|
||||
return sys.Stat_t{}, platform.UnwrapOSError(err)
|
||||
} else {
|
||||
return statFromFileInfo(t), 0
|
||||
return sys.NewStat_t(info), 0
|
||||
}
|
||||
}
|
||||
|
||||
func statFile(f fs.File) (fsapi.Stat_t, syscall.Errno) {
|
||||
func statFile(f fs.File) (sys.Stat_t, syscall.Errno) {
|
||||
return defaultStatFile(f)
|
||||
}
|
||||
|
||||
func inoFromFileInfo(_ string, t fs.FileInfo) (ino fsapi.Ino, err syscall.Errno) {
|
||||
if d, ok := t.Sys().(*syscall.Stat_t); ok {
|
||||
ino = d.Ino
|
||||
func inoFromFileInfo(_ string, info fs.FileInfo) (sys.Inode, syscall.Errno) {
|
||||
switch v := info.Sys().(type) {
|
||||
case *sys.Stat_t:
|
||||
return v.Ino, 0
|
||||
case *syscall.Stat_t:
|
||||
return v.Ino, 0
|
||||
default:
|
||||
return 0, 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func statFromFileInfo(t fs.FileInfo) fsapi.Stat_t {
|
||||
if d, ok := t.Sys().(*syscall.Stat_t); ok {
|
||||
st := fsapi.Stat_t{}
|
||||
st.Dev = uint64(d.Dev)
|
||||
st.Ino = uint64(d.Ino)
|
||||
st.Mode = t.Mode()
|
||||
st.Nlink = uint64(d.Nlink)
|
||||
st.Size = d.Size
|
||||
atime := d.Atim
|
||||
st.Atim = atime.Sec*1e9 + atime.Nsec
|
||||
mtime := d.Mtim
|
||||
st.Mtim = mtime.Sec*1e9 + mtime.Nsec
|
||||
ctime := d.Ctim
|
||||
st.Ctim = ctime.Sec*1e9 + ctime.Nsec
|
||||
return st
|
||||
}
|
||||
return statFromDefaultFileInfo(t)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
func TestStat(t *testing.T) {
|
||||
@@ -20,7 +21,7 @@ func TestStat(t *testing.T) {
|
||||
_, errno = stat(path.Join(tmpDir, "sub/cat"))
|
||||
require.EqualErrno(t, syscall.ENOENT, errno)
|
||||
|
||||
var st fsapi.Stat_t
|
||||
var st sys.Stat_t
|
||||
|
||||
t.Run("dir", func(t *testing.T) {
|
||||
st, errno = stat(tmpDir)
|
||||
@@ -31,7 +32,7 @@ func TestStat(t *testing.T) {
|
||||
})
|
||||
|
||||
file := path.Join(tmpDir, "file")
|
||||
var stFile fsapi.Stat_t
|
||||
var stFile sys.Stat_t
|
||||
|
||||
t.Run("file", func(t *testing.T) {
|
||||
require.NoError(t, os.WriteFile(file, nil, 0o400))
|
||||
@@ -54,7 +55,7 @@ func TestStat(t *testing.T) {
|
||||
})
|
||||
|
||||
subdir := path.Join(tmpDir, "sub")
|
||||
var stSubdir fsapi.Stat_t
|
||||
var stSubdir sys.Stat_t
|
||||
t.Run("subdir", func(t *testing.T) {
|
||||
require.NoError(t, os.Mkdir(subdir, 0o500))
|
||||
|
||||
@@ -258,7 +259,7 @@ func TestStatFile_dev_inode(t *testing.T) {
|
||||
require.Equal(t, st1.Ino, st1Again.Ino)
|
||||
}
|
||||
|
||||
func requireNotDir(t *testing.T, d fsapi.File, st fsapi.Stat_t) {
|
||||
func requireNotDir(t *testing.T, d fsapi.File, st sys.Stat_t) {
|
||||
// Verify cached state is correct
|
||||
isDir, errno := d.IsDir()
|
||||
require.EqualErrno(t, 0, errno)
|
||||
@@ -266,7 +267,7 @@ func requireNotDir(t *testing.T, d fsapi.File, st fsapi.Stat_t) {
|
||||
require.False(t, st.Mode.IsDir())
|
||||
}
|
||||
|
||||
func requireDir(t *testing.T, d fsapi.File, st fsapi.Stat_t) {
|
||||
func requireDir(t *testing.T, d fsapi.File, st sys.Stat_t) {
|
||||
// Verify cached state is correct
|
||||
isDir, errno := d.IsDir()
|
||||
require.EqualErrno(t, 0, errno)
|
||||
@@ -274,7 +275,7 @@ func requireDir(t *testing.T, d fsapi.File, st fsapi.Stat_t) {
|
||||
require.True(t, st.Mode.IsDir())
|
||||
}
|
||||
|
||||
func requireDevIno(t *testing.T, f fsapi.File, st fsapi.Stat_t) {
|
||||
func requireDevIno(t *testing.T, f fsapi.File, st sys.Stat_t) {
|
||||
// Results are inconsistent, so don't validate the opposite.
|
||||
if statSetsIno() {
|
||||
require.NotEqual(t, uint64(0), st.Dev)
|
||||
|
||||
@@ -7,36 +7,36 @@ import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
func lstat(path string) (fsapi.Stat_t, syscall.Errno) {
|
||||
t, err := os.Lstat(path)
|
||||
if errno := platform.UnwrapOSError(err); errno == 0 {
|
||||
return statFromFileInfo(t), 0
|
||||
// Note: go:build constraints must be the same as /sys.stat_unsupported.go for
|
||||
// the same reasons.
|
||||
|
||||
func lstat(path string) (sys.Stat_t, syscall.Errno) {
|
||||
if info, err := os.Lstat(path); err != nil {
|
||||
return sys.Stat_t{}, platform.UnwrapOSError(err)
|
||||
} else {
|
||||
return fsapi.Stat_t{}, errno
|
||||
return sys.NewStat_t(info), 0
|
||||
}
|
||||
}
|
||||
|
||||
func stat(path string) (fsapi.Stat_t, syscall.Errno) {
|
||||
t, err := os.Stat(path)
|
||||
if errno := platform.UnwrapOSError(err); errno == 0 {
|
||||
return statFromFileInfo(t), 0
|
||||
func stat(path string) (sys.Stat_t, syscall.Errno) {
|
||||
if info, err := os.Stat(path); err != nil {
|
||||
return sys.Stat_t{}, platform.UnwrapOSError(err)
|
||||
} else {
|
||||
return fsapi.Stat_t{}, errno
|
||||
return sys.NewStat_t(info), 0
|
||||
}
|
||||
}
|
||||
|
||||
func statFile(f fs.File) (fsapi.Stat_t, syscall.Errno) {
|
||||
func statFile(f fs.File) (sys.Stat_t, syscall.Errno) {
|
||||
return defaultStatFile(f)
|
||||
}
|
||||
|
||||
func inoFromFileInfo(_ string, t fs.FileInfo) (ino fsapi.Ino, err syscall.Errno) {
|
||||
return
|
||||
}
|
||||
|
||||
func statFromFileInfo(t fs.FileInfo) fsapi.Stat_t {
|
||||
return statFromDefaultFileInfo(t)
|
||||
func inoFromFileInfo(_ string, info fs.FileInfo) (sys.Inode, syscall.Errno) {
|
||||
if st, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||
return st.Ino, 0
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"path"
|
||||
"syscall"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
func lstat(path string) (fsapi.Stat_t, syscall.Errno) {
|
||||
func lstat(path string) (sys.Stat_t, syscall.Errno) {
|
||||
attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS)
|
||||
// Use FILE_FLAG_OPEN_REPARSE_POINT, otherwise CreateFile will follow symlink.
|
||||
// See https://docs.microsoft.com/en-us/windows/desktop/FileIO/symbolic-link-effects-on-file-systems-functions#createfile-and-createfiletransacted
|
||||
@@ -19,18 +19,18 @@ func lstat(path string) (fsapi.Stat_t, syscall.Errno) {
|
||||
return statPath(attrs, path)
|
||||
}
|
||||
|
||||
func stat(path string) (fsapi.Stat_t, syscall.Errno) {
|
||||
func stat(path string) (sys.Stat_t, syscall.Errno) {
|
||||
attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS)
|
||||
return statPath(attrs, path)
|
||||
}
|
||||
|
||||
func statPath(createFileAttrs uint32, path string) (fsapi.Stat_t, syscall.Errno) {
|
||||
func statPath(createFileAttrs uint32, path string) (sys.Stat_t, syscall.Errno) {
|
||||
if len(path) == 0 {
|
||||
return fsapi.Stat_t{}, syscall.ENOENT
|
||||
return sys.Stat_t{}, syscall.ENOENT
|
||||
}
|
||||
pathp, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return fsapi.Stat_t{}, syscall.EINVAL
|
||||
return sys.Stat_t{}, syscall.EINVAL
|
||||
}
|
||||
|
||||
// open the file handle
|
||||
@@ -42,7 +42,7 @@ func statPath(createFileAttrs uint32, path string) (fsapi.Stat_t, syscall.Errno)
|
||||
if err == syscall.ENOTDIR {
|
||||
err = syscall.ENOENT
|
||||
}
|
||||
return fsapi.Stat_t{}, platform.UnwrapOSError(err)
|
||||
return sys.Stat_t{}, platform.UnwrapOSError(err)
|
||||
}
|
||||
defer syscall.CloseHandle(h)
|
||||
|
||||
@@ -54,7 +54,7 @@ type fdFile interface {
|
||||
Fd() uintptr
|
||||
}
|
||||
|
||||
func statFile(f fs.File) (fsapi.Stat_t, syscall.Errno) {
|
||||
func statFile(f fs.File) (sys.Stat_t, syscall.Errno) {
|
||||
if osF, ok := f.(fdFile); ok {
|
||||
// Attempt to get the stat by handle, which works for normal files
|
||||
st, err := statHandle(syscall.Handle(osF.Fd()))
|
||||
@@ -72,47 +72,30 @@ func statFile(f fs.File) (fsapi.Stat_t, syscall.Errno) {
|
||||
}
|
||||
|
||||
// inoFromFileInfo uses stat to get the inode information of the file.
|
||||
func inoFromFileInfo(filePath string, t fs.FileInfo) (ino fsapi.Ino, errno syscall.Errno) {
|
||||
if filePath == "" {
|
||||
func inoFromFileInfo(dirPath string, info fs.FileInfo) (ino sys.Inode, errno syscall.Errno) {
|
||||
if dirPath == "" {
|
||||
// This is a fs.File backed implementation which doesn't have access to
|
||||
// the original file path.
|
||||
return
|
||||
}
|
||||
// ino is no not in Win32FileAttributeData
|
||||
inoPath := path.Clean(path.Join(filePath, t.Name()))
|
||||
var st fsapi.Stat_t
|
||||
// Ino is no not in Win32FileAttributeData
|
||||
inoPath := path.Clean(path.Join(dirPath, info.Name()))
|
||||
var st sys.Stat_t
|
||||
if st, errno = lstat(inoPath); errno == 0 {
|
||||
ino = st.Ino
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func statFromFileInfo(t fs.FileInfo) fsapi.Stat_t {
|
||||
if d, ok := t.Sys().(*syscall.Win32FileAttributeData); ok {
|
||||
st := fsapi.Stat_t{}
|
||||
st.Ino = 0 // not in Win32FileAttributeData
|
||||
st.Dev = 0 // not in Win32FileAttributeData
|
||||
st.Mode = t.Mode()
|
||||
st.Nlink = 1 // not in Win32FileAttributeData
|
||||
st.Size = t.Size()
|
||||
st.Atim = d.LastAccessTime.Nanoseconds()
|
||||
st.Mtim = d.LastWriteTime.Nanoseconds()
|
||||
st.Ctim = d.CreationTime.Nanoseconds()
|
||||
return st
|
||||
} else {
|
||||
return statFromDefaultFileInfo(t)
|
||||
}
|
||||
}
|
||||
|
||||
func statHandle(h syscall.Handle) (fsapi.Stat_t, syscall.Errno) {
|
||||
func statHandle(h syscall.Handle) (sys.Stat_t, syscall.Errno) {
|
||||
winFt, err := syscall.GetFileType(h)
|
||||
if err != nil {
|
||||
return fsapi.Stat_t{}, platform.UnwrapOSError(err)
|
||||
return sys.Stat_t{}, platform.UnwrapOSError(err)
|
||||
}
|
||||
|
||||
var fi syscall.ByHandleFileInformation
|
||||
if err = syscall.GetFileInformationByHandle(h, &fi); err != nil {
|
||||
return fsapi.Stat_t{}, platform.UnwrapOSError(err)
|
||||
return sys.Stat_t{}, platform.UnwrapOSError(err)
|
||||
}
|
||||
|
||||
var m fs.FileMode
|
||||
@@ -133,7 +116,7 @@ func statHandle(h syscall.Handle) (fsapi.Stat_t, syscall.Errno) {
|
||||
m |= fs.ModeDir | 0o111 // e.g. 0o444 -> 0o555
|
||||
}
|
||||
|
||||
st := fsapi.Stat_t{}
|
||||
st := sys.Stat_t{}
|
||||
// FileIndex{High,Low} can be combined and used as a unique identifier like inode.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/ns-fileapi-by_handle_file_information
|
||||
st.Dev = uint64(fi.VolumeSerialNumber)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/tetratelabs/wazero/internal/fsapi"
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
func testOpen_O_RDWR(t *testing.T, tmpDir string, testFS fsapi.FS) {
|
||||
@@ -242,7 +243,7 @@ func testLstat(t *testing.T, testFS fsapi.FS) {
|
||||
_, errno = testFS.Lstat("sub/cat")
|
||||
require.EqualErrno(t, syscall.ENOENT, errno)
|
||||
|
||||
var st fsapi.Stat_t
|
||||
var st sys.Stat_t
|
||||
|
||||
t.Run("dir", func(t *testing.T) {
|
||||
st, errno = testFS.Lstat(".")
|
||||
@@ -251,7 +252,7 @@ func testLstat(t *testing.T, testFS fsapi.FS) {
|
||||
require.NotEqual(t, uint64(0), st.Ino)
|
||||
})
|
||||
|
||||
var stFile fsapi.Stat_t
|
||||
var stFile sys.Stat_t
|
||||
|
||||
t.Run("file", func(t *testing.T) {
|
||||
stFile, errno = testFS.Lstat("animals.txt")
|
||||
@@ -266,7 +267,7 @@ func testLstat(t *testing.T, testFS fsapi.FS) {
|
||||
requireLinkStat(t, testFS, "animals.txt", stFile)
|
||||
})
|
||||
|
||||
var stSubdir fsapi.Stat_t
|
||||
var stSubdir sys.Stat_t
|
||||
t.Run("subdir", func(t *testing.T) {
|
||||
stSubdir, errno = testFS.Lstat("sub")
|
||||
require.EqualErrno(t, 0, errno)
|
||||
@@ -288,7 +289,7 @@ func testLstat(t *testing.T, testFS fsapi.FS) {
|
||||
})
|
||||
}
|
||||
|
||||
func requireLinkStat(t *testing.T, testFS fsapi.FS, path string, stat fsapi.Stat_t) {
|
||||
func requireLinkStat(t *testing.T, testFS fsapi.FS, path string, stat sys.Stat_t) {
|
||||
link := path + "-link"
|
||||
stLink, errno := testFS.Lstat(link)
|
||||
require.EqualErrno(t, 0, errno)
|
||||
|
||||
103
sys/stat.go
Normal file
103
sys/stat.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package sys
|
||||
|
||||
import "io/fs"
|
||||
|
||||
// Inode is the file serial number, or zero if unknown.
|
||||
//
|
||||
// Any constant value will invalidate functions that use this for
|
||||
// equivalence, such as os.SameFile (Stat_t.Ino).
|
||||
//
|
||||
// When zero is returned by a `readdir`, some compilers will attempt to
|
||||
// get a non-zero value with `lstat`. Those using this for darwin's definition
|
||||
// of `getdirentries` conflate zero `d_fileno` with a deleted file, so skip the
|
||||
// entry. See /RATIONALE.md for more on this.
|
||||
type Inode = uint64
|
||||
|
||||
// ^-- Inode is a type alias to consolidate documentation and aid in reference
|
||||
// searches. While only Stat_t is exposed publicly at the moment, this is used
|
||||
// internally for Dirent and several function return values.
|
||||
|
||||
// EpochNanos is a timestamp in epoch nanoseconds, or zero if unknown.
|
||||
//
|
||||
// This defines epoch time the same way as Walltime, except this value is
|
||||
// packed into an int64. Common conversions are detailed in the examples.
|
||||
type EpochNanos = int64
|
||||
|
||||
// Stat_t is similar to syscall.Stat_t, except available on all operating
|
||||
// systems, including Windows.
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - This is used for WebAssembly ABI emulating the POSIX `stat` system call.
|
||||
// Fields included are required for WebAssembly ABI including wasip1
|
||||
// (a.k.a. wasix) and wasi-filesystem (a.k.a. wasip2). See
|
||||
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/stat.html
|
||||
// - Fields here are required for WebAssembly ABI including wasip1
|
||||
// (a.k.a. wasix) and wasi-filesystem (a.k.a. wasip2).
|
||||
// - This isn't the same as syscall.Stat_t because wazero supports Windows,
|
||||
// which doesn't have that type. runtime.GOOS that has this already also
|
||||
// have inconsistent field lengths, which complicates wasm binding.
|
||||
// - Use NewStat_t to create this from an existing fs.FileInfo.
|
||||
// - For portability, numeric fields are 64-bit when at least one platform
|
||||
// defines it that large.
|
||||
type Stat_t struct {
|
||||
// Dev is the device ID of device containing the file.
|
||||
Dev uint64
|
||||
|
||||
// Ino is the file serial number, or zero if not available. See Inode for
|
||||
// more details including impact returning a zero value.
|
||||
Ino Inode
|
||||
|
||||
// Mode is the same as Mode on fs.FileInfo containing bits to identify the
|
||||
// type of the file (fs.ModeType) and its permissions (fs.ModePerm).
|
||||
Mode fs.FileMode
|
||||
|
||||
/// Nlink is the number of hard links to the file.
|
||||
Nlink uint64
|
||||
|
||||
// Size is the length in bytes for regular files. For symbolic links, this
|
||||
// is length in bytes of the pathname contained in the symbolic link.
|
||||
Size int64
|
||||
|
||||
// Atim is the last data access timestamp in epoch nanoseconds.
|
||||
Atim EpochNanos
|
||||
|
||||
// Mtim is the last data modification timestamp in epoch nanoseconds.
|
||||
Mtim EpochNanos
|
||||
|
||||
// Ctim is the last file status change timestamp in epoch nanoseconds.
|
||||
Ctim EpochNanos
|
||||
}
|
||||
|
||||
// NewStat_t fills a new Stat_t from `info`, including any runtime.GOOS-specific
|
||||
// details from fs.FileInfo `Sys`. When `Sys` is already a *Stat_t, it is
|
||||
// returned as-is.
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - When already in fs.FileInfo `Sys`, Stat_t must be a pointer.
|
||||
// - When runtime.GOOS is "windows" Stat_t.Ino will be zero.
|
||||
// - When fs.FileInfo `Sys` is nil or unknown, some fields not in fs.FileInfo
|
||||
// are defaulted: Stat_t.Atim and Stat_t.Ctim are set to `ModTime`, and
|
||||
// are set to ModTime and Stat_t.Nlink is set to 1.
|
||||
func NewStat_t(info fs.FileInfo) Stat_t {
|
||||
// Note: Pointer, not val, for parity with Go, which sets *syscall.Stat_t
|
||||
if st, ok := info.Sys().(*Stat_t); ok {
|
||||
return *st
|
||||
}
|
||||
return statFromFileInfo(info)
|
||||
}
|
||||
|
||||
func defaultStatFromFileInfo(info fs.FileInfo) Stat_t {
|
||||
st := Stat_t{}
|
||||
st.Ino = 0
|
||||
st.Dev = 0
|
||||
st.Mode = info.Mode()
|
||||
st.Nlink = 1
|
||||
st.Size = info.Size()
|
||||
mtim := info.ModTime().UnixNano() // Set all times to the mod time
|
||||
st.Atim = mtim
|
||||
st.Mtim = mtim
|
||||
st.Ctim = mtim
|
||||
return st
|
||||
}
|
||||
29
sys/stat_bsd.go
Normal file
29
sys/stat_bsd.go
Normal file
@@ -0,0 +1,29 @@
|
||||
//go:build (amd64 || arm64) && (darwin || freebsd)
|
||||
|
||||
package sys
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const sysParseable = true
|
||||
|
||||
func statFromFileInfo(info fs.FileInfo) Stat_t {
|
||||
if d, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||
st := Stat_t{}
|
||||
st.Dev = uint64(d.Dev)
|
||||
st.Ino = d.Ino
|
||||
st.Mode = info.Mode()
|
||||
st.Nlink = uint64(d.Nlink)
|
||||
st.Size = d.Size
|
||||
atime := d.Atimespec
|
||||
st.Atim = atime.Sec*1e9 + atime.Nsec
|
||||
mtime := d.Mtimespec
|
||||
st.Mtim = mtime.Sec*1e9 + mtime.Nsec
|
||||
ctime := d.Ctimespec
|
||||
st.Ctim = ctime.Sec*1e9 + ctime.Nsec
|
||||
return st
|
||||
}
|
||||
return defaultStatFromFileInfo(info)
|
||||
}
|
||||
38
sys/stat_example_test.go
Normal file
38
sys/stat_example_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package sys_test
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"math"
|
||||
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
|
||||
var (
|
||||
walltime sys.Walltime
|
||||
info fs.FileInfo
|
||||
st sys.Stat_t
|
||||
)
|
||||
|
||||
// This shows typical conversions to sys.EpochNanos type, for sys.Stat_t fields.
|
||||
func Example_epochNanos() {
|
||||
// Convert an adapted fs.File's fs.FileInfo to Mtim.
|
||||
st.Mtim = info.ModTime().UnixNano()
|
||||
|
||||
// Generate a fake Atim using sys.Walltime passed to wazero.ModuleConfig.
|
||||
sec, nsec := walltime()
|
||||
st.Atim = sec*1e9 + int64(nsec)
|
||||
}
|
||||
|
||||
type fileInfoWithSys struct {
|
||||
fs.FileInfo
|
||||
st sys.Stat_t
|
||||
}
|
||||
|
||||
func (f *fileInfoWithSys) Sys() any { return &f.st }
|
||||
|
||||
// This shows how to return data not defined in fs.FileInfo, notably sys.Inode.
|
||||
func Example_inode() {
|
||||
st := sys.NewStat_t(info)
|
||||
st.Ino = math.MaxUint64 // arbitrary non-zero value
|
||||
info = &fileInfoWithSys{info, st}
|
||||
}
|
||||
32
sys/stat_linux.go
Normal file
32
sys/stat_linux.go
Normal file
@@ -0,0 +1,32 @@
|
||||
//go:build (amd64 || arm64 || riscv64) && linux
|
||||
|
||||
// Note: This expression is not the same as compiler support, even if it looks
|
||||
// similar. Platform functions here are used in interpreter mode as well.
|
||||
|
||||
package sys
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const sysParseable = true
|
||||
|
||||
func statFromFileInfo(info fs.FileInfo) Stat_t {
|
||||
if d, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||
st := Stat_t{}
|
||||
st.Dev = uint64(d.Dev)
|
||||
st.Ino = uint64(d.Ino)
|
||||
st.Mode = info.Mode()
|
||||
st.Nlink = uint64(d.Nlink)
|
||||
st.Size = d.Size
|
||||
atime := d.Atim
|
||||
st.Atim = atime.Sec*1e9 + atime.Nsec
|
||||
mtime := d.Mtim
|
||||
st.Mtim = mtime.Sec*1e9 + mtime.Nsec
|
||||
ctime := d.Ctim
|
||||
st.Ctim = ctime.Sec*1e9 + ctime.Nsec
|
||||
return st
|
||||
}
|
||||
return defaultStatFromFileInfo(info)
|
||||
}
|
||||
156
sys/stat_test.go
Normal file
156
sys/stat_test.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
)
|
||||
|
||||
func Test_NewStat_t(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
fileData := []byte{1, 2, 3, 4}
|
||||
|
||||
dir := path.Join(tmpDir, "dir")
|
||||
require.NoError(t, os.Mkdir(dir, 0o700))
|
||||
osDirInfo, err := os.Stat(dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
file := path.Join(dir, "file")
|
||||
require.NoError(t, os.WriteFile(file, []byte{1, 2, 3, 4}, 0o400))
|
||||
osFileInfo, err := os.Stat(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
link := path.Join(dir, "file-link")
|
||||
require.NoError(t, os.Symlink(file, link))
|
||||
osSymlinkInfo, err := os.Lstat(link)
|
||||
require.NoError(t, err)
|
||||
|
||||
osFileSt := NewStat_t(osFileInfo)
|
||||
testFS := fstest.MapFS{
|
||||
"dir": {
|
||||
Mode: osDirInfo.Mode(),
|
||||
ModTime: osDirInfo.ModTime(),
|
||||
},
|
||||
"dir/file": {
|
||||
Data: fileData,
|
||||
Mode: osFileInfo.Mode(),
|
||||
ModTime: osFileInfo.ModTime(),
|
||||
},
|
||||
"dir/file-sys": {
|
||||
// intentionally skip other fields to prove sys is read.
|
||||
Sys: &osFileSt,
|
||||
},
|
||||
}
|
||||
|
||||
fsDirInfo, err := testFS.Stat("dir")
|
||||
require.NoError(t, err)
|
||||
fsFileInfo, err := testFS.Stat("dir/file")
|
||||
require.NoError(t, err)
|
||||
fsFileInfoWithSys, err := testFS.Stat("dir/file-sys")
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
info fs.FileInfo
|
||||
expectDevIno bool
|
||||
expectedMode fs.FileMode
|
||||
expectedSize int64
|
||||
expectAtimCtime bool
|
||||
}{
|
||||
{
|
||||
name: "os dir",
|
||||
info: osDirInfo,
|
||||
expectDevIno: true,
|
||||
expectedMode: fs.ModeDir | 0o0700,
|
||||
expectedSize: osDirInfo.Size(), // OS dependent
|
||||
expectAtimCtime: true,
|
||||
},
|
||||
{
|
||||
name: "fs dir",
|
||||
info: fsDirInfo,
|
||||
expectDevIno: false,
|
||||
expectedMode: fs.ModeDir | 0o0700,
|
||||
expectedSize: 0,
|
||||
expectAtimCtime: false,
|
||||
},
|
||||
{
|
||||
name: "os file",
|
||||
info: osFileInfo,
|
||||
expectDevIno: true,
|
||||
expectedMode: 0o0400,
|
||||
expectedSize: int64(len(fileData)),
|
||||
expectAtimCtime: true,
|
||||
},
|
||||
{
|
||||
name: "fs file",
|
||||
info: fsFileInfo,
|
||||
expectDevIno: false,
|
||||
expectedMode: 0o0400,
|
||||
expectedSize: int64(len(fileData)),
|
||||
expectAtimCtime: false,
|
||||
},
|
||||
{
|
||||
name: "fs file with Stat_t in Sys",
|
||||
info: fsFileInfoWithSys,
|
||||
expectDevIno: true,
|
||||
expectedMode: 0o0400,
|
||||
expectedSize: int64(len(fileData)),
|
||||
expectAtimCtime: true,
|
||||
},
|
||||
{
|
||||
name: "os symlink",
|
||||
info: osSymlinkInfo,
|
||||
expectDevIno: true,
|
||||
expectedMode: fs.ModeSymlink,
|
||||
expectedSize: osSymlinkInfo.Size(), // OS dependent
|
||||
expectAtimCtime: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
st := NewStat_t(tc.info)
|
||||
if tc.expectDevIno && runtime.GOOS != "windows" {
|
||||
require.NotEqual(t, uint64(0), st.Dev)
|
||||
require.NotEqual(t, uint64(0), st.Ino)
|
||||
} else {
|
||||
require.Zero(t, st.Dev)
|
||||
require.Zero(t, st.Ino)
|
||||
}
|
||||
|
||||
// link mode may differ on windows, so mask
|
||||
require.Equal(t, tc.expectedMode, st.Mode&tc.expectedMode)
|
||||
|
||||
if sysParseable && runtime.GOOS != "windows" {
|
||||
switch st.Nlink {
|
||||
case 2, 4: // dirents may include dot entries.
|
||||
require.Equal(t, fs.ModeDir, st.Mode.Type())
|
||||
default:
|
||||
require.Equal(t, uint64(1), st.Nlink)
|
||||
}
|
||||
} else { // Nlink is possibly wrong, but not zero.
|
||||
require.Equal(t, uint64(1), st.Nlink)
|
||||
}
|
||||
|
||||
require.Equal(t, tc.expectedSize, st.Size)
|
||||
|
||||
if tc.expectAtimCtime && sysParseable {
|
||||
// We don't validate times strictly because it is os-dependent
|
||||
// what updates times. There are edge cases for symlinks, too.
|
||||
require.NotEqual(t, EpochNanos(0), st.Ctim)
|
||||
require.NotEqual(t, EpochNanos(0), st.Mtim)
|
||||
require.NotEqual(t, EpochNanos(0), st.Mtim)
|
||||
} else { // mtim is used for atim and ctime
|
||||
require.Equal(t, st.Mtim, st.Ctim)
|
||||
require.NotEqual(t, EpochNanos(0), st.Mtim)
|
||||
require.Equal(t, st.Mtim, st.Atim)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
17
sys/stat_unsupported.go
Normal file
17
sys/stat_unsupported.go
Normal file
@@ -0,0 +1,17 @@
|
||||
//go:build (!((amd64 || arm64 || riscv64) && linux) && !((amd64 || arm64) && (darwin || freebsd)) && !((amd64 || arm64) && windows)) || js
|
||||
|
||||
package sys
|
||||
|
||||
import "io/fs"
|
||||
|
||||
// sysParseable is only used here as we define "supported" as being able to
|
||||
// parse `info.Sys()`. The above `go:build` constraints exclude 32-bit until
|
||||
// that's requested.
|
||||
//
|
||||
// TODO: When Go 1.21 is out, use the "unix" build constraint (as 1.21 makes
|
||||
// our floor Go version 1.19.
|
||||
const sysParseable = false
|
||||
|
||||
func statFromFileInfo(info fs.FileInfo) Stat_t {
|
||||
return defaultStatFromFileInfo(info)
|
||||
}
|
||||
26
sys/stat_windows.go
Normal file
26
sys/stat_windows.go
Normal file
@@ -0,0 +1,26 @@
|
||||
//go:build (amd64 || arm64) && windows
|
||||
|
||||
package sys
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const sysParseable = true
|
||||
|
||||
func statFromFileInfo(info fs.FileInfo) Stat_t {
|
||||
if d, ok := info.Sys().(*syscall.Win32FileAttributeData); ok {
|
||||
st := Stat_t{}
|
||||
st.Ino = 0 // not in Win32FileAttributeData
|
||||
st.Dev = 0 // not in Win32FileAttributeData
|
||||
st.Mode = info.Mode()
|
||||
st.Nlink = 1 // not in Win32FileAttributeData
|
||||
st.Size = info.Size()
|
||||
st.Atim = d.LastAccessTime.Nanoseconds()
|
||||
st.Mtim = d.LastWriteTime.Nanoseconds()
|
||||
st.Ctim = d.CreationTime.Nanoseconds()
|
||||
return st
|
||||
}
|
||||
return defaultStatFromFileInfo(info)
|
||||
}
|
||||
Reference in New Issue
Block a user