wasi/platform: supports inode and dev on Windows (#1132)
Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io> Signed-off-by: Adrian Cole <adrian@tetrate.io> Co-authored-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
@@ -386,10 +386,9 @@ func getWasiFiletype(fileMode fs.FileMode) uint8 {
|
||||
}
|
||||
|
||||
func writeFilestat(buf []byte, f fs.File, stat fs.FileInfo) (err error) {
|
||||
device, inode := platform.StatDeviceInode(stat)
|
||||
filetype := getWasiFiletype(stat.Mode())
|
||||
filesize := uint64(stat.Size())
|
||||
atimeNsec, mtimeNsec, ctimeNsec, nlink, err := platform.Stat(f, stat)
|
||||
atimeNsec, mtimeNsec, ctimeNsec, nlink, device, inode, err := platform.Stat(f, stat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3223,7 +3223,7 @@ func Test_pathLink(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.False(t, st.Mode()&os.ModeSymlink == os.ModeSymlink)
|
||||
|
||||
_, _, _, nlink, err := platform.Stat(f, st)
|
||||
_, _, _, nlink, _, _, err := platform.Stat(f, st)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(2), nlink)
|
||||
})
|
||||
|
||||
@@ -125,10 +125,15 @@ func (jsfsStat) invoke(ctx context.Context, mod api.Module, args ...interface{})
|
||||
func syscallStat(mod api.Module, path string) (*jsSt, error) {
|
||||
fsc := mod.(*wasm.CallContext).Sys.FS()
|
||||
|
||||
if stat, err := sysfs.StatPath(fsc.RootFS(), path); err != nil {
|
||||
f, err := fsc.RootFS().OpenFile(path, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
if stat, err := sysfs.StatFile(f); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return newJsSt(stat), nil
|
||||
return newJsSt(stat, f), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,13 +192,13 @@ func syscallFstat(fsc *internalsys.FSContext, fd uint32) (*jsSt, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newJsSt(stat), nil
|
||||
return newJsSt(stat, f.File), nil
|
||||
}
|
||||
|
||||
func newJsSt(stat fs.FileInfo) *jsSt {
|
||||
func newJsSt(stat fs.FileInfo, f fs.File) *jsSt {
|
||||
ret := &jsSt{}
|
||||
ret.isDir = stat.IsDir()
|
||||
ret.dev, ret.ino = platform.StatDeviceInode(stat)
|
||||
_, _, _, _, ret.dev, ret.ino, _ = platform.Stat(f, stat)
|
||||
ret.mode = getJsMode(stat.Mode())
|
||||
ret.size = stat.Size()
|
||||
atimeNsec, mtimeNsec, ctimeNsec := platform.StatTimes(stat)
|
||||
|
||||
@@ -68,12 +68,12 @@ func Test_writefs(t *testing.T) {
|
||||
// Note: as of Go 1.19, only the Sec field is set on update in fs_js.go.
|
||||
require.Equal(t, `/tmp/dir mode drwx------
|
||||
/tmp/dir/file mode -rw-------
|
||||
times: 123000000000 567000000000
|
||||
dir times: 123000000000 567000000000
|
||||
`, stdout)
|
||||
} else { // only mtimes will return.
|
||||
} else { // only mtimes will return on a plarform we don't support in sysfs
|
||||
require.Equal(t, `/tmp/dir mode drwx------
|
||||
/tmp/dir/file mode -rw-------
|
||||
times: 567000000000 567000000000
|
||||
dir times: 567000000000 567000000000
|
||||
`, stdout)
|
||||
}
|
||||
}
|
||||
|
||||
43
internal/gojs/testdata/writefs/main.go
vendored
43
internal/gojs/testdata/writefs/main.go
vendored
@@ -111,21 +111,18 @@ func Main() {
|
||||
}
|
||||
|
||||
// Ensure the times translated properly.
|
||||
if st, err := os.Stat(dir); err != nil {
|
||||
log.Panicln("unexpected error", err)
|
||||
} else {
|
||||
atimeNsec, mtimeNsec, _ := statTimes(st)
|
||||
fmt.Println("times:", atimeNsec, mtimeNsec)
|
||||
dirAtimeNsec, dirMtimeNsec, dirDev, dirInode := statFields(dir)
|
||||
fmt.Println("dir times:", dirAtimeNsec, dirMtimeNsec)
|
||||
|
||||
// statDeviceInode cannot be tested against real device values because
|
||||
// the size of d.Dev (32-bit) in js is smaller than linux (64-bit).
|
||||
//
|
||||
// We can't test the real inode of dir, though we could /tmp as that
|
||||
// file is visible on the host. However, we haven't yet implemented
|
||||
// platform.StatDeviceInode on windows, so we couldn't run that test
|
||||
// in CI. For now, this only tests there is no compilation problem or
|
||||
// runtime panic.
|
||||
_, _ = statDeviceInode(st)
|
||||
// Ensure we were able to read the dev and inode.
|
||||
//
|
||||
// Note: The size of syscall.Stat_t.Dev (32-bit) in js is smaller than
|
||||
// linux (64-bit), so we can't compare its real value against the host.
|
||||
if dirDev == 0 {
|
||||
log.Panicln("expected dir dev != 0", dirDev)
|
||||
}
|
||||
if dirInode == 0 {
|
||||
log.Panicln("expected dir inode != 0", dirInode)
|
||||
}
|
||||
|
||||
// Test renaming a file, noting we can't verify error numbers as they
|
||||
@@ -145,6 +142,24 @@ func Main() {
|
||||
log.Panicln("unexpected error", err)
|
||||
}
|
||||
|
||||
// Compare stat after renaming.
|
||||
atimeNsec, mtimeNsec, dev, inode := statFields(dir1)
|
||||
// atime shouldn't change as we didn't access (re-open) the directory.
|
||||
if atimeNsec != dirAtimeNsec {
|
||||
log.Panicln("expected dir atimeNsec = previous value", atimeNsec, dirAtimeNsec)
|
||||
}
|
||||
// mtime should change because we renamed the directory.
|
||||
if mtimeNsec <= dirMtimeNsec {
|
||||
log.Panicln("expected dir mtimeNsec > previous value", mtimeNsec, dirMtimeNsec)
|
||||
}
|
||||
// dev/inode shouldn't change during rename.
|
||||
if dev != dirDev {
|
||||
log.Panicln("expected dir dev = previous value", dev, dirDev)
|
||||
}
|
||||
if inode != dirInode {
|
||||
log.Panicln("expected dir inode = previous value", dev, dirInode)
|
||||
}
|
||||
|
||||
// Test unlinking a file
|
||||
if err = syscall.Rmdir(file1); err != syscall.ENOTDIR {
|
||||
log.Panicln("unexpected error", err)
|
||||
|
||||
12
internal/gojs/testdata/writefs/stat.go
vendored
Normal file
12
internal/gojs/testdata/writefs/stat.go
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build !js
|
||||
|
||||
package writefs
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// statFields isn't used outside JS, it is only for compilation
|
||||
func statFields(string) (atimeNsec, mtimeNsec int64, dev, inode uint64) {
|
||||
panic(syscall.ENOSYS)
|
||||
}
|
||||
16
internal/gojs/testdata/writefs/stat_js.go
vendored
Normal file
16
internal/gojs/testdata/writefs/stat_js.go
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
package writefs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func statFields(path string) (atimeNsec, mtimeNsec int64, dev, inode uint64) {
|
||||
if t, err := os.Stat(path); err != nil {
|
||||
panic(fmt.Errorf("failed to stat path %s: %v", path, err))
|
||||
} else {
|
||||
d := t.Sys().(*syscall.Stat_t)
|
||||
return d.Atime*1e9 + d.AtimeNsec, d.Mtime*1e9 + d.MtimeNsec, uint64(d.Dev), uint64(d.Ino)
|
||||
}
|
||||
}
|
||||
17
internal/gojs/testdata/writefs/times.go
vendored
17
internal/gojs/testdata/writefs/times.go
vendored
@@ -1,17 +0,0 @@
|
||||
//go:build !js
|
||||
|
||||
package writefs
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
)
|
||||
|
||||
func statTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) {
|
||||
return platform.StatTimes(t) // allow the file to compile and run outside JS
|
||||
}
|
||||
|
||||
func statDeviceInode(t os.FileInfo) (dev, inode uint64) {
|
||||
return platform.StatDeviceInode(t)
|
||||
}
|
||||
16
internal/gojs/testdata/writefs/times_js.go
vendored
16
internal/gojs/testdata/writefs/times_js.go
vendored
@@ -1,16 +0,0 @@
|
||||
package writefs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func statTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) {
|
||||
d := t.Sys().(*syscall.Stat_t)
|
||||
return d.Atime*1e9 + d.AtimeNsec, d.Mtime*1e9 + d.MtimeNsec, d.Ctime*1e9 + d.CtimeNsec
|
||||
}
|
||||
|
||||
func statDeviceInode(t os.FileInfo) (dev, inode uint64) {
|
||||
d := t.Sys().(*syscall.Stat_t)
|
||||
return uint64(d.Dev), uint64(d.Ino)
|
||||
}
|
||||
@@ -16,7 +16,7 @@ func StatTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) {
|
||||
}
|
||||
|
||||
// Stat returns platform-specific values if os.FileInfo Sys is available.
|
||||
func Stat(f fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlink uint64, err error) {
|
||||
func Stat(f fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlink, dev, inode uint64, err error) {
|
||||
if t.Sys() == nil { // possibly fake filesystem
|
||||
atimeNsec, mtimeNsec, ctimeNsec = mtimes(t)
|
||||
nlink = 1
|
||||
@@ -25,21 +25,6 @@ func Stat(f fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlin
|
||||
return stat(f, t)
|
||||
}
|
||||
|
||||
// StatDeviceInode returns platform-specific values if os.FileInfo Sys is
|
||||
// available. Otherwise, it returns zero which makes file identity comparison
|
||||
// unsupported.
|
||||
//
|
||||
// Returning zero for now works in most cases, except notably wasi-libc
|
||||
// code that needs to compare file identity via the underlying data as
|
||||
// opposed to a host function similar to os.SameFile.
|
||||
// See https://github.com/WebAssembly/wasi-filesystem/issues/65
|
||||
func StatDeviceInode(t os.FileInfo) (dev, inode uint64) {
|
||||
if t.Sys() == nil { // possibly fake filesystem
|
||||
return
|
||||
}
|
||||
return statDeviceInode(t)
|
||||
}
|
||||
|
||||
func mtimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) {
|
||||
mtimeNsec = t.ModTime().UnixNano()
|
||||
atimeNsec = mtimeNsec
|
||||
|
||||
@@ -16,17 +16,11 @@ func statTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) {
|
||||
return atime.Sec*1e9 + atime.Nsec, mtime.Sec*1e9 + mtime.Nsec, ctime.Sec*1e9 + ctime.Nsec
|
||||
}
|
||||
|
||||
func stat(_ fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlink uint64, err error) {
|
||||
func stat(_ fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlink, dev, inode uint64, err error) {
|
||||
d := t.Sys().(*syscall.Stat_t)
|
||||
atime := d.Atimespec
|
||||
mtime := d.Mtimespec
|
||||
ctime := d.Ctimespec
|
||||
return atime.Sec*1e9 + atime.Nsec, mtime.Sec*1e9 + mtime.Nsec, ctime.Sec*1e9 + ctime.Nsec, uint64(d.Nlink), nil
|
||||
}
|
||||
|
||||
func statDeviceInode(t os.FileInfo) (dev, inode uint64) {
|
||||
d := t.Sys().(*syscall.Stat_t)
|
||||
dev = uint64(d.Dev)
|
||||
inode = d.Ino
|
||||
return
|
||||
return atime.Sec*1e9 + atime.Nsec, mtime.Sec*1e9 + mtime.Nsec, ctime.Sec*1e9 + ctime.Nsec,
|
||||
uint64(d.Nlink), uint64(d.Dev), uint64(d.Ino), nil
|
||||
}
|
||||
|
||||
@@ -19,17 +19,10 @@ func statTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) {
|
||||
return atime.Sec*1e9 + atime.Nsec, mtime.Sec*1e9 + mtime.Nsec, ctime.Sec*1e9 + ctime.Nsec
|
||||
}
|
||||
|
||||
func stat(_ fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlink uint64, err error) {
|
||||
func stat(_ fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlink, dev, inode uint64, err error) {
|
||||
d := t.Sys().(*syscall.Stat_t)
|
||||
atime := d.Atim
|
||||
mtime := d.Mtim
|
||||
ctime := d.Ctim
|
||||
return atime.Sec*1e9 + atime.Nsec, mtime.Sec*1e9 + mtime.Nsec, ctime.Sec*1e9 + ctime.Nsec, uint64(d.Nlink), nil
|
||||
}
|
||||
|
||||
func statDeviceInode(t os.FileInfo) (dev, inode uint64) {
|
||||
d := t.Sys().(*syscall.Stat_t)
|
||||
dev = d.Dev
|
||||
inode = d.Ino
|
||||
return
|
||||
return atime.Sec*1e9 + atime.Nsec, mtime.Sec*1e9 + mtime.Nsec, ctime.Sec*1e9 + ctime.Nsec, uint64(d.Nlink), uint64(d.Dev), uint64(d.Ino), nil
|
||||
}
|
||||
|
||||
@@ -59,30 +59,26 @@ func Test_Stat(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatDeviceInode(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("platform.StatDeviceInode not yet implemented on windows")
|
||||
}
|
||||
|
||||
func TestStat_dev_inode(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
path1 := path.Join(tmpDir, "1")
|
||||
fa, err := os.Create(path1)
|
||||
require.NoError(t, err)
|
||||
defer fa.Close()
|
||||
|
||||
path2 := path.Join(tmpDir, "2")
|
||||
fb, err := os.Create(path2)
|
||||
require.NoError(t, err)
|
||||
defer fb.Close()
|
||||
|
||||
stat1, err := fa.Stat()
|
||||
require.NoError(t, err)
|
||||
device1, inode1 := StatDeviceInode(stat1)
|
||||
_, _, _, _, device1, inode1, err := Stat(fa, stat1)
|
||||
require.NoError(t, err)
|
||||
|
||||
stat2, err := fb.Stat()
|
||||
require.NoError(t, err)
|
||||
device2, inode2 := StatDeviceInode(stat2)
|
||||
_, _, _, _, device2, inode2, err := Stat(fb, stat2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The files should be on the same device, but different inodes
|
||||
require.Equal(t, device1, device2)
|
||||
@@ -91,15 +87,25 @@ func TestStatDeviceInode(t *testing.T) {
|
||||
// Redoing stat should result in the same inodes
|
||||
stat1Again, err := os.Stat(path1)
|
||||
require.NoError(t, err)
|
||||
device1Again, inode1Again := StatDeviceInode(stat1Again)
|
||||
_, _, _, _, device1Again, inode1Again, err := Stat(fa, stat1Again)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, device1, device1Again)
|
||||
require.Equal(t, inode1, inode1Again)
|
||||
|
||||
// Renaming a file shouldn't change its inodes
|
||||
require.NoError(t, os.Rename(path1, path2))
|
||||
// On Windows, we cannot rename while opening.
|
||||
// So we manually close here before renaming.
|
||||
require.NoError(t, fa.Close())
|
||||
require.NoError(t, fb.Close())
|
||||
|
||||
// Renaming a file shouldn't change its inodes.
|
||||
require.NoError(t, Rename(path1, path2))
|
||||
fa, err = os.Open(path2)
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, fa.Close()) }()
|
||||
stat1Again, err = os.Stat(path2)
|
||||
require.NoError(t, err)
|
||||
device1Again, inode1Again = StatDeviceInode(stat1Again)
|
||||
_, _, _, _, device1Again, inode1Again, err = Stat(fa, stat1Again)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, device1, device1Again)
|
||||
require.Equal(t, inode1, inode1Again)
|
||||
}
|
||||
|
||||
@@ -12,11 +12,7 @@ func statTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) {
|
||||
return
|
||||
}
|
||||
|
||||
func stat(_ fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlink uint64, err error) {
|
||||
func stat(_ fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlink, dev, inode uint64, err error) {
|
||||
atimeNsec, mtimeNsec, ctimeNsec = mtimes(t)
|
||||
return
|
||||
}
|
||||
|
||||
func statDeviceInode(t os.FileInfo) (dev, inode uint64) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func statTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) {
|
||||
return
|
||||
}
|
||||
|
||||
func stat(f fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlink uint64, err error) {
|
||||
func stat(f fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlink, dev, inode uint64, err error) {
|
||||
d := t.Sys().(*syscall.Win32FileAttributeData)
|
||||
atimeNsec = d.LastAccessTime.Nanoseconds()
|
||||
mtimeNsec = d.LastWriteTime.Nanoseconds()
|
||||
@@ -47,14 +47,9 @@ func stat(f fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlin
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
nlink = uint64(info.NumberOfLinks)
|
||||
nlink, dev = uint64(info.NumberOfLinks), uint64(info.VolumeSerialNumber)
|
||||
// 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
|
||||
inode = (uint64(info.FileIndexHigh) << 32) | uint64(info.FileIndexLow)
|
||||
return
|
||||
}
|
||||
|
||||
func statDeviceInode(t os.FileInfo) (dev, inode uint64) {
|
||||
// TODO: VolumeSerialNumber, FileIndexHigh and FileIndexLow are used in
|
||||
// os.SameFile, but the fields aren't exported or accessible in os.FileInfo
|
||||
// When we make our file type, get these from GetFileInformationByHandle.
|
||||
// Note that this requires access to the underlying FD number.
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user