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:
Crypt Keeper
2023-07-10 11:46:20 +08:00
committed by GitHub
parent d3f09bdcff
commit 6efcf25505
41 changed files with 707 additions and 261 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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