Skip to content

Commit

Permalink
gojs: implements lstat
Browse files Browse the repository at this point in the history
This implements platform.Lstat and uses it in GOOS=js. Notably,
directory listings need to run lstat on their entries to get the correct
inodes back. In GOOS=js, directories are a fan-out of names, then lstat.

Signed-off-by: Adrian Cole <[email protected]>
  • Loading branch information
Adrian Cole committed Feb 26, 2023
1 parent 70924aa commit 2f9ae52
Show file tree
Hide file tree
Showing 20 changed files with 569 additions and 126 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ defaults:
shell: bash

env: # Update this prior to requiring a higher minor version in go.mod
GO_VERSION: "1.19" # 1.xx == latest patch of 1.xx
GO_VERSION: "1.20" # 1.xx == latest patch of 1.xx
ZIG_VERSION: "0.11.0-dev.1797+d3c9bfada"
TINYGO_VERSION: "0.27.0"

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ main_sources := $(wildcard $(filter-out %_test.go $(all_testdata) $(all_testing
main_packages := $(sort $(foreach f,$(dir $(main_sources)),$(if $(findstring ./,$(f)),./,./$(f))))

# By default, we don't run with -race as it's costly to run on every PR.
go_test_options ?= -timeout 120s
go_test_options ?= -timeout 1200s

ensureCompilerFastest := -ldflags '-X github.com/tetratelabs/wazero/internal/integration_test/vs.ensureCompilerFastest=true'
.PHONY: bench
Expand Down
22 changes: 16 additions & 6 deletions internal/gojs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,22 @@ func (jsfsLstat) invoke(ctx context.Context, mod api.Module, args ...interface{}
path := args[0].(string)
callback := args[1].(funcWrapper)

lstat, err := syscallStat(mod, path) // TODO switch to lstat syscall
lstat, err := syscallLstat(mod, path)

return callback.invoke(ctx, mod, goos.RefJsfs, err, lstat) // note: error first
}

// syscallLstat is like syscall.Lstat
func syscallLstat(mod api.Module, path string) (*jsSt, error) {
fsc := mod.(*wasm.CallContext).Sys.FS()

var stat platform.Stat_t
if err := fsc.RootFS().Lstat(path, &stat); err != nil {
return nil, err
}
return newJsSt(&stat), nil
}

// jsfsFstat implements jsFn for syscall.Open
//
// stat, err := fsCall("fstat", fd); err == nil && stat.Call("isDirectory").Bool()
Expand Down Expand Up @@ -211,6 +222,8 @@ func newJsSt(stat *platform.Stat_t) *jsSt {
func getJsMode(mode fs.FileMode) (jsMode uint32) {
jsMode = uint32(mode & fs.ModePerm)
switch mode & fs.ModeType {
case 0:
jsMode |= S_IFREG
case fs.ModeDir:
jsMode |= S_IFDIR
case fs.ModeSymlink:
Expand All @@ -227,9 +240,6 @@ func getJsMode(mode fs.FileMode) (jsMode uint32) {
// unmapped to js
}

if mode&fs.ModeType == 0 {
jsMode |= S_IFREG
}
if mode&fs.ModeSetgid != 0 {
jsMode |= S_ISGID
}
Expand Down Expand Up @@ -687,8 +697,8 @@ func (jsfsSymlink) invoke(ctx context.Context, mod api.Module, args ...interface
link := args[1].(string)
callback := args[2].(funcWrapper)

_, _ = path, link // TODO
var err error = syscall.ENOSYS
fsc := mod.(*wasm.CallContext).Sys.FS()
err := fsc.RootFS().Symlink(path, link)

return jsfsInvoke(ctx, mod, callback, err)
}
Expand Down
32 changes: 29 additions & 3 deletions internal/gojs/testdata/writefs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,18 @@ func Main() {
if err = syscall.Chmod(file1, 0o600); err != nil {
log.Panicln(err)
}
if stat, err := os.Stat(file1); err != nil {

// Test stat
stat, err := os.Stat(file1)
if err != nil {
log.Panicln(err)
} else if mode := stat.Mode() & fs.ModePerm; mode != 0o600 {
log.Panicln("expected mode = 0o600", mode)
}

if stat.Mode().Type() != 0 {
log.Panicln("expected type = 0", stat.Mode().Type())
}
if stat.Mode().Perm() != 0o600 {
log.Panicln("expected perm = 0o600", stat.Mode().Perm())
}

// Check the file was truncated.
Expand All @@ -114,6 +122,24 @@ func Main() {
log.Panicln("unexpected contents:", string(bytes))
}

// Test lstat which should be about the link not its target.
link := file1 + "-link"
if err = os.Symlink(file1, link); err != nil {
log.Panicln(err)
}

lstat, err := os.Lstat(link)
if err != nil {
log.Panicln(err)
}

if lstat.Mode().Type() != fs.ModeSymlink {
log.Panicln("expected type = symlink", lstat.Mode().Type())
}
if size := int64(len(file1)); lstat.Size() != size {
log.Panicln("unexpected symlink size", lstat.Size(), size)
}

// Test removing a non-empty empty directory
if err = syscall.Rmdir(dir); err != syscall.ENOTEMPTY {
log.Panicln("unexpected error", err)
Expand Down
2 changes: 2 additions & 0 deletions internal/platform/open_file_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ func OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
}

switch err {
// To match expectations of WASI, e.g. TinyGo TestStatBadDir, return
// ENOENT, not ENOTDIR.
case syscall.ENOTDIR:
err = syscall.ENOENT
case syscall.ENOENT:
Expand Down
60 changes: 33 additions & 27 deletions internal/platform/stat.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type Stat_t struct {
Ino uint64

// Mode is the same as Mode on fs.FileInfo containing bits to identify the
// type of the file and its permissions (fs.ModePerm).
// type of the file (fs.ModeType) and its permissions (fs.ModePerm).
Mode fs.FileMode

/// Nlink is the number of hard links to the file.
Expand All @@ -42,47 +42,53 @@ type Stat_t struct {
Ctim int64
}

// Lstat is like syscall.Lstat. This returns syscall.ENOENT if the path doesn't
// exist.
//
// # Notes
//
// The primary difference between this and Stat is, when the path is a
// symbolic link, the stat is about the link, not its target, such as directory
// listings.
func Lstat(path string, st *Stat_t) error {
err := lstat(path, st) // extracted to override more expensively in windows
return UnwrapOSError(err)
}

// Stat is like syscall.Stat. This returns syscall.ENOENT if the path doesn't
// exist.
func Stat(path string, st *Stat_t) error {
return stat(path, st) // extracted to override more expensively in windows
err := stat(path, st) // extracted to override more expensively in windows
return UnwrapOSError(err)
}

// StatFile is like syscall.Fstat, but for fs.File instead of a file
// descriptor. This returns syscall.EBADF if the file or directory was closed.
// Note: windows allows you to stat a closed directory.
func StatFile(f fs.File, st *Stat_t) (err error) {
t, err := f.Stat()
if err = UnwrapOSError(err); err != nil {
if err == syscall.EIO { // linux/darwin returns this on a closed file.
err = syscall.EBADF // windows returns this, which is better.
}
return
err = statFile(f, st)
if err = UnwrapOSError(err); err == syscall.EIO {
err = syscall.EBADF
}
return fillStatFile(st, f, t)
return
}

// fdFile is implemented by os.File in file_unix.go and file_windows.go
// Note: we use this until we finalize our own FD-scoped file.
type fdFile interface{ Fd() (fd uintptr) }

func fillStatFile(stat *Stat_t, f fs.File, t fs.FileInfo) (err error) {
if of, ok := f.(fdFile); !ok { // possibly fake filesystem
fillStatFromFileInfo(stat, t)
} else {
err = fillStatFromOpenFile(stat, of.Fd(), t)
func defaultStatFile(f fs.File, st *Stat_t) (err error) {
var t fs.FileInfo
if t, err = f.Stat(); err == nil {
fillStatFromFileInfo(st, t)
}
return
}

func fillStatFromFileInfo(stat *Stat_t, t fs.FileInfo) {
stat.Ino = 0
stat.Dev = 0
stat.Mode = t.Mode()
stat.Nlink = 1
stat.Size = t.Size()
func fillStatFromDefaultFileInfo(st *Stat_t, t fs.FileInfo) {
st.Ino = 0
st.Dev = 0
st.Mode = t.Mode()
st.Nlink = 1
st.Size = t.Size()
mtim := t.ModTime().UnixNano() // Set all times to the mod time
stat.Atim = mtim
stat.Mtim = mtim
stat.Ctim = mtim
st.Atim = mtim
st.Mtim = mtim
st.Ctim = mtim
}
49 changes: 30 additions & 19 deletions internal/platform/stat_bsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,45 @@
package platform

import (
"io/fs"
"os"
"syscall"
)

func stat(path string, st *Stat_t) (err error) {
t, err := os.Stat(path)
if err = UnwrapOSError(err); err == nil {
fillStatFromSys(st, t)
func lstat(path string, st *Stat_t) (err error) {
var t fs.FileInfo
if t, err = os.Lstat(path); err == nil {
fillStatFromFileInfo(st, t)
}
return
}

func fillStatFromOpenFile(stat *Stat_t, fd uintptr, t os.FileInfo) (err error) {
fillStatFromSys(stat, t)
func stat(path string, st *Stat_t) (err error) {
var t fs.FileInfo
if t, err = os.Stat(path); err == nil {
fillStatFromFileInfo(st, t)
}
return
}

func fillStatFromSys(stat *Stat_t, t os.FileInfo) {
d := t.Sys().(*syscall.Stat_t)
stat.Ino = d.Ino
stat.Dev = uint64(d.Dev)
stat.Mode = t.Mode()
stat.Nlink = uint64(d.Nlink)
stat.Size = d.Size
atime := d.Atimespec
stat.Atim = atime.Sec*1e9 + atime.Nsec
mtime := d.Mtimespec
stat.Mtim = mtime.Sec*1e9 + mtime.Nsec
ctime := d.Ctimespec
stat.Ctim = ctime.Sec*1e9 + ctime.Nsec
func statFile(f fs.File, st *Stat_t) error {
return defaultStatFile(f, st)
}

func fillStatFromFileInfo(st *Stat_t, t fs.FileInfo) {
if d, ok := t.Sys().(*syscall.Stat_t); ok {
st.Ino = d.Ino
st.Dev = uint64(d.Dev)
st.Mode = t.Mode()
st.Nlink = uint64(d.Nlink)
st.Size = d.Size
atime := d.Atimespec
st.Atim = atime.Sec*1e9 + atime.Nsec
mtime := d.Mtimespec
st.Mtim = mtime.Sec*1e9 + mtime.Nsec
ctime := d.Ctimespec
st.Ctim = ctime.Sec*1e9 + ctime.Nsec
} else {
fillStatFromDefaultFileInfo(st, t)
}
}
49 changes: 30 additions & 19 deletions internal/platform/stat_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,45 @@
package platform

import (
"io/fs"
"os"
"syscall"
)

func stat(path string, st *Stat_t) (err error) {
t, err := os.Stat(path)
if err = UnwrapOSError(err); err == nil {
fillStatFromSys(st, t)
func lstat(path string, st *Stat_t) (err error) {
var t fs.FileInfo
if t, err = os.Lstat(path); err == nil {
fillStatFromFileInfo(st, t)
}
return
}

func fillStatFromOpenFile(stat *Stat_t, fd uintptr, t os.FileInfo) (err error) {
fillStatFromSys(stat, t)
func stat(path string, st *Stat_t) (err error) {
var t fs.FileInfo
if t, err = os.Stat(path); err == nil {
fillStatFromFileInfo(st, t)
}
return
}

func fillStatFromSys(stat *Stat_t, t os.FileInfo) {
d := t.Sys().(*syscall.Stat_t)
stat.Ino = uint64(d.Ino)
stat.Dev = uint64(d.Dev)
stat.Mode = t.Mode()
stat.Nlink = uint64(d.Nlink)
stat.Size = d.Size
atime := d.Atim
stat.Atim = atime.Sec*1e9 + atime.Nsec
mtime := d.Mtim
stat.Mtim = mtime.Sec*1e9 + mtime.Nsec
ctime := d.Ctim
stat.Ctim = ctime.Sec*1e9 + ctime.Nsec
func statFile(f fs.File, st *Stat_t) error {
return defaultStatFile(f, st)
}

func fillStatFromFileInfo(st *Stat_t, t fs.FileInfo) {
if d, ok := t.Sys().(*syscall.Stat_t); ok {
st.Ino = uint64(d.Ino)
st.Dev = uint64(d.Dev)
st.Mode = t.Mode()
st.Nlink = uint64(d.Nlink)
st.Size = d.Size
atime := d.Atim
st.Atim = atime.Sec*1e9 + atime.Nsec
mtime := d.Mtim
st.Mtim = mtime.Sec*1e9 + mtime.Nsec
ctime := d.Ctim
st.Ctim = ctime.Sec*1e9 + ctime.Nsec
} else {
fillStatFromDefaultFileInfo(st, t)
}
}
Loading

0 comments on commit 2f9ae52

Please sign in to comment.