Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements stat device/inode on WASI and GOOS=js #1041

Merged
merged 1 commit into from
Jan 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 9 additions & 18 deletions imports/wasi_snapshot_preview1/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion internal/gojs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions internal/gojs/fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 12 additions & 2 deletions internal/gojs/testdata/writefs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion internal/gojs/testdata/writefs/times.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 7 additions & 2 deletions internal/gojs/testdata/writefs/times_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
15 changes: 15 additions & 0 deletions internal/platform/stat.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions internal/platform/stat_bsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
12 changes: 11 additions & 1 deletion internal/platform/stat_linux.go
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
}
49 changes: 46 additions & 3 deletions internal/platform/stat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
6 changes: 5 additions & 1 deletion internal/platform/stat_unsupported.go
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
}
8 changes: 8 additions & 0 deletions internal/platform/stat_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}