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:
Crypt Keeper
2023-01-16 22:22:39 -06:00
committed by GitHub
parent c222e73847
commit 3609d74c92
12 changed files with 129 additions and 31 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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