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:
Takeshi Yoneda
2023-02-16 10:01:24 -08:00
committed by GitHub
parent 3d72f2cb90
commit 2309db9057
15 changed files with 103 additions and 120 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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