Implements stat device/inode on WASI and GOOS=js (#1041)
This implements stat device and inode for WASI and GOOS=js, though it does not implement the host side for windows, yet. Doing windows requires plumbing as the values needed aren't exposed in Go. When we re-do the syscallfs file type to have a stat method, we can address that glitch. Meanwhile, I can find no Go sourcebase that does any better, though the closest is the implementation details of os.SameFile. I verified this with wasi-testsuite which now passes all but 1 case which is unrelated (we haven't yet implemented `lseek`). Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
@@ -264,29 +264,20 @@ func getWasiFiletype(fileMode fs.FileMode) uint8 {
|
||||
return wasiFileType
|
||||
}
|
||||
|
||||
var blockFilestat = []byte{
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // device
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // inode
|
||||
FILETYPE_BLOCK_DEVICE, 0, 0, 0, 0, 0, 0, 0, // filetype
|
||||
1, 0, 0, 0, 0, 0, 0, 0, // nlink
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // filesize
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // atim
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // mtim
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // ctim
|
||||
}
|
||||
|
||||
func writeFilestat(buf []byte, stat fs.FileInfo) {
|
||||
device, inode := platform.StatDeviceInode(stat)
|
||||
filetype := getWasiFiletype(stat.Mode())
|
||||
filesize := uint64(stat.Size())
|
||||
atimeNsec, mtimeNsec, ctimeNsec := platform.StatTimes(stat)
|
||||
|
||||
// memory is re-used, so ensure the result is defaulted.
|
||||
copy(buf, blockFilestat[:32])
|
||||
buf[16] = filetype
|
||||
le.PutUint64(buf[32:], filesize) // filesize
|
||||
le.PutUint64(buf[40:], uint64(atimeNsec)) // atim
|
||||
le.PutUint64(buf[48:], uint64(mtimeNsec)) // mtim
|
||||
le.PutUint64(buf[56:], uint64(ctimeNsec)) // ctim
|
||||
le.PutUint64(buf, device)
|
||||
le.PutUint64(buf[8:], inode)
|
||||
le.PutUint64(buf[16:], uint64(filetype))
|
||||
le.PutUint64(buf[24:], 1) // nlink
|
||||
le.PutUint64(buf[32:], filesize)
|
||||
le.PutUint64(buf[40:], uint64(atimeNsec))
|
||||
le.PutUint64(buf[48:], uint64(mtimeNsec))
|
||||
le.PutUint64(buf[56:], uint64(ctimeNsec))
|
||||
}
|
||||
|
||||
// fdFilestatSetSize is the WASI function named FdFilestatSetSizeName which
|
||||
|
||||
@@ -185,6 +185,7 @@ func syscallFstat(fsc *internalsys.FSContext, fd uint32) (*jsSt, error) {
|
||||
func newJsSt(stat fs.FileInfo) *jsSt {
|
||||
ret := &jsSt{}
|
||||
ret.isDir = stat.IsDir()
|
||||
ret.dev, ret.ino = platform.StatDeviceInode(stat)
|
||||
ret.mode = getJsMode(stat.Mode())
|
||||
ret.size = stat.Size()
|
||||
atimeNsec, mtimeNsec, ctimeNsec := platform.StatTimes(stat)
|
||||
@@ -684,7 +685,7 @@ func (jsfsFsync) invoke(ctx context.Context, mod api.Module, args ...interface{}
|
||||
// jsSt is pre-parsed from fs_js.go setStat to avoid thrashing
|
||||
type jsSt struct {
|
||||
isDir bool
|
||||
dev int64
|
||||
dev uint64
|
||||
ino uint64
|
||||
mode uint32
|
||||
nlink uint32
|
||||
|
||||
@@ -73,12 +73,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: 123 0 567 0
|
||||
times: 123000000000 567000000000
|
||||
`, stdout)
|
||||
} else { // only mtimes will return.
|
||||
require.Equal(t, `/tmp/dir mode drwx------
|
||||
/tmp/dir/file mode -rw-------
|
||||
times: 567 0 567 0
|
||||
times: 567000000000 567000000000
|
||||
`, stdout)
|
||||
}
|
||||
}
|
||||
|
||||
14
internal/gojs/testdata/writefs/main.go
vendored
14
internal/gojs/testdata/writefs/main.go
vendored
@@ -88,8 +88,18 @@ func Main() {
|
||||
if stat, err := os.Stat(dir); err != nil {
|
||||
log.Panicln("unexpected error", err)
|
||||
} else {
|
||||
atimeSec, atimeNsec, mtimeSec, mtimeNsec, _, _ := statTimes(stat)
|
||||
fmt.Println("times:", atimeSec, atimeNsec, mtimeSec, mtimeNsec)
|
||||
atimeNsec, mtimeNsec, _ := statTimes(stat)
|
||||
fmt.Println("times:", atimeNsec, mtimeNsec)
|
||||
|
||||
// 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(stat)
|
||||
}
|
||||
|
||||
// Test renaming a file, noting we can't verify error numbers as they
|
||||
|
||||
6
internal/gojs/testdata/writefs/times.go
vendored
6
internal/gojs/testdata/writefs/times.go
vendored
@@ -8,6 +8,10 @@ import (
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
)
|
||||
|
||||
func statTimes(t os.FileInfo) (atimeSec, atimeNsec, mtimeSec, mtimeNsec, ctimeSec, ctimeNsec int64) {
|
||||
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)
|
||||
}
|
||||
|
||||
9
internal/gojs/testdata/writefs/times_js.go
vendored
9
internal/gojs/testdata/writefs/times_js.go
vendored
@@ -5,7 +5,12 @@ import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func statTimes(t os.FileInfo) (atimeSec, atimeNsec, mtimeSec, mtimeNsec, ctimeSec, ctimeNsec int64) {
|
||||
func statTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) {
|
||||
d := t.Sys().(*syscall.Stat_t)
|
||||
return d.Atime, d.AtimeNsec, d.Mtime, d.MtimeNsec, d.Ctime, d.CtimeNsec
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,21 @@ func StatTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) {
|
||||
return statTimes(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
|
||||
|
||||
@@ -14,3 +14,10 @@ func statTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) {
|
||||
ctime := d.Ctimespec
|
||||
return atime.Sec*1e9 + atime.Nsec, mtime.Sec*1e9 + mtime.Nsec, ctime.Sec*1e9 + ctime.Nsec
|
||||
}
|
||||
|
||||
func statDeviceInode(t os.FileInfo) (dev, inode uint64) {
|
||||
d := t.Sys().(*syscall.Stat_t)
|
||||
dev = uint64(d.Dev)
|
||||
inode = d.Ino
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
//go:build (amd64 || arm64) && linux
|
||||
//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 platform
|
||||
|
||||
@@ -14,3 +17,10 @@ func statTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) {
|
||||
ctime := d.Ctim
|
||||
return atime.Sec*1e9 + atime.Nsec, mtime.Sec*1e9 + mtime.Nsec, ctime.Sec*1e9 + ctime.Nsec
|
||||
}
|
||||
|
||||
func statDeviceInode(t os.FileInfo) (dev, inode uint64) {
|
||||
d := t.Sys().(*syscall.Stat_t)
|
||||
dev = d.Dev
|
||||
inode = d.Ino
|
||||
return
|
||||
}
|
||||
|
||||
@@ -53,10 +53,53 @@ func Test_StatTimes(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
atimeNsec, mtimeNsec, _ := StatTimes(stat)
|
||||
if CompilerSupported() {
|
||||
require.Equal(t, atimeNsec, tc.atimeNsec)
|
||||
} // else only mtimes will return.
|
||||
require.Equal(t, atimeNsec, tc.atimeNsec)
|
||||
require.Equal(t, mtimeNsec, tc.mtimeNsec)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatDeviceInode(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("platform.StatDeviceInode not yet implemented on windows")
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
stat2, err := fb.Stat()
|
||||
require.NoError(t, err)
|
||||
device2, inode2 := StatDeviceInode(stat2)
|
||||
|
||||
// The files should be on the same device, but different inodes
|
||||
require.Equal(t, device1, device2)
|
||||
require.NotEqual(t, inode1, inode2)
|
||||
|
||||
// Redoing stat should result in the same inodes
|
||||
stat1Again, err := os.Stat(path1)
|
||||
require.NoError(t, err)
|
||||
device1Again, inode1Again := StatDeviceInode(stat1Again)
|
||||
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))
|
||||
stat1Again, err = os.Stat(path2)
|
||||
require.NoError(t, err)
|
||||
device1Again, inode1Again = StatDeviceInode(stat1Again)
|
||||
require.Equal(t, device1, device1Again)
|
||||
require.Equal(t, inode1, inode1Again)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !(amd64 || arm64) || !(darwin || linux || freebsd || windows)
|
||||
//go:build !((amd64 || arm64 || riscv64) && linux) && !((amd64 || arm64) && (darwin || freebsd)) && !((amd64 || arm64) && windows)
|
||||
|
||||
package platform
|
||||
|
||||
@@ -7,3 +7,7 @@ import "os"
|
||||
func statTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) {
|
||||
return mtimes(t)
|
||||
}
|
||||
|
||||
func statDeviceInode(t os.FileInfo) (dev, inode uint64) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -14,3 +14,11 @@ func statTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) {
|
||||
ctimeNsec = d.CreationTime.Nanoseconds()
|
||||
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