From 6efcf25505f55ea3a1924b68ff80dda80622a4be Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Mon, 10 Jul 2023 11:46:20 +0800 Subject: [PATCH] Exposes sys.Stat_t as a portable alternative to syscall.Stat_t (#1567) Signed-off-by: Adrian Cole --- RATIONALE.md | 62 ++++++++ fsconfig.go | 12 ++ fsconfig_example_test.go | 2 +- imports/wasi_snapshot_preview1/fs.go | 9 +- imports/wasi_snapshot_preview1/fs_test.go | 3 +- imports/wasi_snapshot_preview1/poll_test.go | 5 +- internal/fsapi/dir.go | 15 +- internal/fsapi/file.go | 6 +- internal/fsapi/fs.go | 6 +- internal/fsapi/stat.go | 41 ----- internal/fsapi/unimplemented.go | 16 +- internal/fstest/times_notwindows.go | 2 +- internal/fstest/times_windows.go | 4 +- internal/gojs/fs.go | 4 +- internal/sys/lazy.go | 7 +- internal/sys/stdio.go | 5 +- internal/sysfs/adapter.go | 7 +- internal/sysfs/adapter_test.go | 56 ++++++- internal/sysfs/dir_test.go | 8 +- internal/sysfs/dirfs.go | 5 +- internal/sysfs/file.go | 21 +-- internal/sysfs/file_test.go | 3 +- internal/sysfs/futimens.go | 4 +- internal/sysfs/osfile.go | 9 +- internal/sysfs/readfs.go | 9 +- internal/sysfs/readfs_test.go | 8 +- internal/sysfs/sock.go | 3 +- internal/sysfs/stat.go | 24 +-- internal/sysfs/stat_bsd.go | 51 +++---- internal/sysfs/stat_linux.go | 51 +++---- internal/sysfs/stat_test.go | 13 +- internal/sysfs/stat_unsupported.go | 36 ++--- internal/sysfs/stat_windows.go | 51 +++---- internal/sysfs/sysfs_test.go | 9 +- sys/stat.go | 103 +++++++++++++ sys/stat_bsd.go | 29 ++++ sys/stat_example_test.go | 38 +++++ sys/stat_linux.go | 32 ++++ sys/stat_test.go | 156 ++++++++++++++++++++ sys/stat_unsupported.go | 17 +++ sys/stat_windows.go | 26 ++++ 41 files changed, 707 insertions(+), 261 deletions(-) delete mode 100644 internal/fsapi/stat.go create mode 100644 sys/stat.go create mode 100644 sys/stat_bsd.go create mode 100644 sys/stat_example_test.go create mode 100644 sys/stat_linux.go create mode 100644 sys/stat_test.go create mode 100644 sys/stat_unsupported.go create mode 100644 sys/stat_windows.go diff --git a/RATIONALE.md b/RATIONALE.md index e87b1b6c..9e8a3f49 100644 --- a/RATIONALE.md +++ b/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. diff --git a/fsconfig.go b/fsconfig.go index b603d1e9..560da0b3 100644 --- a/fsconfig.go +++ b/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 } diff --git a/fsconfig_example_test.go b/fsconfig_example_test.go index 1993421e..882e152b 100644 --- a/fsconfig_example_test.go +++ b/fsconfig_example_test.go @@ -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 { diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go index c2d755b0..55a92068 100644 --- a/imports/wasi_snapshot_preview1/fs.go +++ b/imports/wasi_snapshot_preview1/fs.go @@ -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) diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go index 3de05b0a..b3eacb1c 100644 --- a/imports/wasi_snapshot_preview1/fs_test.go +++ b/imports/wasi_snapshot_preview1/fs_test.go @@ -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) diff --git a/imports/wasi_snapshot_preview1/poll_test.go b/imports/wasi_snapshot_preview1/poll_test.go index b0c0c3cb..764387d0 100644 --- a/imports/wasi_snapshot_preview1/poll_test.go +++ b/imports/wasi_snapshot_preview1/poll_test.go @@ -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 diff --git a/internal/fsapi/dir.go b/internal/fsapi/dir.go index e70cc5c8..6929e8c1 100644 --- a/internal/fsapi/dir.go +++ b/internal/fsapi/dir.go @@ -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 diff --git a/internal/fsapi/file.go b/internal/fsapi/file.go index 8f68f35f..a8531f73 100644 --- a/internal/fsapi/file.go +++ b/internal/fsapi/file.go @@ -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. diff --git a/internal/fsapi/fs.go b/internal/fsapi/fs.go index 5f325d92..391b23f9 100644 --- a/internal/fsapi/fs.go +++ b/internal/fsapi/fs.go @@ -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. // diff --git a/internal/fsapi/stat.go b/internal/fsapi/stat.go deleted file mode 100644 index 4b3cc322..00000000 --- a/internal/fsapi/stat.go +++ /dev/null @@ -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 -} diff --git a/internal/fsapi/unimplemented.go b/internal/fsapi/unimplemented.go index 560c2f0f..9d0b8b47 100644 --- a/internal/fsapi/unimplemented.go +++ b/internal/fsapi/unimplemented.go @@ -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 diff --git a/internal/fstest/times_notwindows.go b/internal/fstest/times_notwindows.go index 712cf2cc..1e7a14af 100644 --- a/internal/fstest/times_notwindows.go +++ b/internal/fstest/times_notwindows.go @@ -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") } diff --git a/internal/fstest/times_windows.go b/internal/fstest/times_windows.go index 78ffe370..2f72a64f 100644 --- a/internal/fstest/times_windows.go +++ b/internal/fstest/times_windows.go @@ -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") diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go index d6810c41..c7b25dfb 100644 --- a/internal/gojs/fs.go +++ b/internal/gojs/fs.go @@ -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 diff --git a/internal/sys/lazy.go b/internal/sys/lazy.go index 2fbbbd82..acda2236 100644 --- a/internal/sys/lazy.go +++ b/internal/sys/lazy.go @@ -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() } diff --git a/internal/sys/stdio.go b/internal/sys/stdio.go index 8b3c8815..84ed6d90 100644 --- a/internal/sys/stdio.go +++ b/internal/sys/stdio.go @@ -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 diff --git a/internal/sysfs/adapter.go b/internal/sysfs/adapter.go index c225c8dc..a052d080 100644 --- a/internal/sysfs/adapter.go +++ b/internal/sysfs/adapter.go @@ -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 diff --git a/internal/sysfs/adapter_test.go b/internal/sysfs/adapter_test.go index 00989bf0..23adf0a6 100644 --- a/internal/sysfs/adapter_test.go +++ b/internal/sysfs/adapter_test.go @@ -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). diff --git a/internal/sysfs/dir_test.go b/internal/sysfs/dir_test.go index 46a83574..42886f17 100644 --- a/internal/sysfs/dir_test.go +++ b/internal/sysfs/dir_test.go @@ -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 { diff --git a/internal/sysfs/dirfs.go b/internal/sysfs/dirfs.go index 8c89e1d9..e6a49ae2 100644 --- a/internal/sysfs/dirfs.go +++ b/internal/sysfs/dirfs.go @@ -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)) } diff --git a/internal/sysfs/file.go b/internal/sysfs/file.go index bf31a610..a1fdc3ff 100644 --- a/internal/sysfs/file.go +++ b/internal/sysfs/file.go @@ -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 } diff --git a/internal/sysfs/file_test.go b/internal/sysfs/file_test.go index df02bf5b..96de9327 100644 --- a/internal/sysfs/file_test.go +++ b/internal/sysfs/file_test.go @@ -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}, diff --git a/internal/sysfs/futimens.go b/internal/sysfs/futimens.go index 1c2bcead..af566584 100644 --- a/internal/sysfs/futimens.go +++ b/internal/sysfs/futimens.go @@ -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 } diff --git a/internal/sysfs/osfile.go b/internal/sysfs/osfile.go index f4740fe3..7c797526 100644 --- a/internal/sysfs/osfile.go +++ b/internal/sysfs/osfile.go @@ -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) diff --git a/internal/sysfs/readfs.go b/internal/sysfs/readfs.go index 125ad316..802d2729 100644 --- a/internal/sysfs/readfs.go +++ b/internal/sysfs/readfs.go @@ -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) } diff --git a/internal/sysfs/readfs_test.go b/internal/sysfs/readfs_test.go index 784d8854..d7da21fc 100644 --- a/internal/sysfs/readfs_test.go +++ b/internal/sysfs/readfs_test.go @@ -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 { diff --git a/internal/sysfs/sock.go b/internal/sysfs/sock.go index 62bef426..6f5a63b0 100644 --- a/internal/sysfs/sock.go +++ b/internal/sysfs/sock.go @@ -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 diff --git a/internal/sysfs/stat.go b/internal/sysfs/stat.go index 342271c6..1e05fdc8 100644 --- a/internal/sysfs/stat.go +++ b/internal/sysfs/stat.go @@ -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 -} diff --git a/internal/sysfs/stat_bsd.go b/internal/sysfs/stat_bsd.go index 9c2b3c60..f24b6355 100644 --- a/internal/sysfs/stat_bsd.go +++ b/internal/sysfs/stat_bsd.go @@ -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) } diff --git a/internal/sysfs/stat_linux.go b/internal/sysfs/stat_linux.go index 9dfc1b90..d8085673 100644 --- a/internal/sysfs/stat_linux.go +++ b/internal/sysfs/stat_linux.go @@ -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) } diff --git a/internal/sysfs/stat_test.go b/internal/sysfs/stat_test.go index 779bb2ed..4c279ecb 100644 --- a/internal/sysfs/stat_test.go +++ b/internal/sysfs/stat_test.go @@ -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) diff --git a/internal/sysfs/stat_unsupported.go b/internal/sysfs/stat_unsupported.go index 8e214c90..5bb3c3da 100644 --- a/internal/sysfs/stat_unsupported.go +++ b/internal/sysfs/stat_unsupported.go @@ -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 } diff --git a/internal/sysfs/stat_windows.go b/internal/sysfs/stat_windows.go index 71bc7308..575c984c 100644 --- a/internal/sysfs/stat_windows.go +++ b/internal/sysfs/stat_windows.go @@ -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) diff --git a/internal/sysfs/sysfs_test.go b/internal/sysfs/sysfs_test.go index b04c8e33..1a9402b6 100644 --- a/internal/sysfs/sysfs_test.go +++ b/internal/sysfs/sysfs_test.go @@ -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) diff --git a/sys/stat.go b/sys/stat.go new file mode 100644 index 00000000..f2cc3845 --- /dev/null +++ b/sys/stat.go @@ -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 +} diff --git a/sys/stat_bsd.go b/sys/stat_bsd.go new file mode 100644 index 00000000..3bf9b5d1 --- /dev/null +++ b/sys/stat_bsd.go @@ -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) +} diff --git a/sys/stat_example_test.go b/sys/stat_example_test.go new file mode 100644 index 00000000..9b75eb58 --- /dev/null +++ b/sys/stat_example_test.go @@ -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} +} diff --git a/sys/stat_linux.go b/sys/stat_linux.go new file mode 100644 index 00000000..9b5e20e8 --- /dev/null +++ b/sys/stat_linux.go @@ -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) +} diff --git a/sys/stat_test.go b/sys/stat_test.go new file mode 100644 index 00000000..2c5840da --- /dev/null +++ b/sys/stat_test.go @@ -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) + } + }) + } +} diff --git a/sys/stat_unsupported.go b/sys/stat_unsupported.go new file mode 100644 index 00000000..583c2adb --- /dev/null +++ b/sys/stat_unsupported.go @@ -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) +} diff --git a/sys/stat_windows.go b/sys/stat_windows.go new file mode 100644 index 00000000..1a7070f4 --- /dev/null +++ b/sys/stat_windows.go @@ -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) +}