Skip to content

Commit

Permalink
implements lstat and fixes inode stat on windows go 1.20 (#1168)
Browse files Browse the repository at this point in the history
gojs: implements lstat

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.

This also fixes stat for inodes on directories. We were missing a test
so we didn't know it was broken on windows. The approach used now is
reliable on go 1.20, and we should suggest anyone using windows to
compile with go 1.20.

Signed-off-by: Takeshi Yoneda <[email protected]>
  • Loading branch information
codefromthecrypt authored Feb 27, 2023
1 parent d955cd7 commit 3d5b6d6
Show file tree
Hide file tree
Showing 18 changed files with 571 additions and 124 deletions.
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 @@ -46,6 +46,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 3d5b6d6

Please sign in to comment.