implements lstat and fixes inode stat on windows go 1.20 (#1168)

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 <takeshi@tetrate.io>
This commit is contained in:
Crypt Keeper
2023-02-28 07:20:31 +08:00
committed by GitHub
parent d955cd7a13
commit 3d5b6d609a
18 changed files with 578 additions and 131 deletions

View File

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

View File

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

View File

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

View File

@@ -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.
@@ -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
}
return fillStatFile(st, f, t)
}
// 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)
err = statFile(f, st)
if err = UnwrapOSError(err); err == syscall.EIO {
err = syscall.EBADF
}
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()
mtim := t.ModTime().UnixNano() // Set all times to the mod time
stat.Atim = mtim
stat.Mtim = mtim
stat.Ctim = mtim
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 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
st.Atim = mtim
st.Mtim = mtim
st.Ctim = mtim
}

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
package platform
import (
"io/fs"
"os"
"path"
pathutil "path"
"runtime"
"strings"
"syscall"
"testing"
"time"
@@ -11,33 +13,125 @@ import (
"github.com/tetratelabs/wazero/internal/testing/require"
)
func TestLstat(t *testing.T) {
tmpDir := t.TempDir()
var stat Stat_t
require.EqualErrno(t, syscall.ENOENT, Lstat(pathutil.Join(tmpDir, "cat"), &stat))
require.EqualErrno(t, syscall.ENOENT, Lstat(pathutil.Join(tmpDir, "sub/cat"), &stat))
t.Run("dir", func(t *testing.T) {
err := Lstat(tmpDir, &stat)
require.NoError(t, err)
require.True(t, stat.Mode.IsDir())
require.NotEqual(t, uint64(0), stat.Ino)
})
file := pathutil.Join(tmpDir, "file")
var statFile Stat_t
t.Run("file", func(t *testing.T) {
require.NoError(t, os.WriteFile(file, []byte{1, 2}, 0o400))
require.NoError(t, Lstat(file, &statFile))
require.Zero(t, statFile.Mode.Type())
require.Equal(t, int64(2), statFile.Size)
require.NotEqual(t, uint64(0), statFile.Ino)
})
t.Run("link to file", func(t *testing.T) {
requireLinkStat(t, file, &statFile)
})
subdir := pathutil.Join(tmpDir, "sub")
var statSubdir Stat_t
t.Run("subdir", func(t *testing.T) {
require.NoError(t, os.Mkdir(subdir, 0o500))
require.NoError(t, Lstat(subdir, &statSubdir))
require.True(t, statSubdir.Mode.IsDir())
require.NotEqual(t, uint64(0), statSubdir.Ino)
})
t.Run("link to dir", func(t *testing.T) {
requireLinkStat(t, subdir, &statSubdir)
})
t.Run("link to dir link", func(t *testing.T) {
pathLink := subdir + "-link"
var statLink Stat_t
require.NoError(t, Lstat(pathLink, &statLink))
requireLinkStat(t, pathLink, &statLink)
})
}
func requireLinkStat(t *testing.T, path string, stat *Stat_t) {
link := path + "-link"
var linkStat Stat_t
require.NoError(t, os.Symlink(path, link))
require.NoError(t, Lstat(link, &linkStat))
require.NotEqual(t, uint64(0), linkStat.Ino)
require.NotEqual(t, stat.Ino, linkStat.Ino) // inodes are not equal
require.Equal(t, fs.ModeSymlink, linkStat.Mode.Type())
// From https://linux.die.net/man/2/lstat:
// The size of a symbolic link is the length of the pathname it
// contains, without a terminating null byte.
if runtime.GOOS == "windows" { // size is zero, not the path length
require.Zero(t, linkStat.Size)
} else {
require.Equal(t, int64(len(path)), linkStat.Size)
}
}
func TestStat(t *testing.T) {
tmpDir := t.TempDir()
var stat Stat_t
require.EqualErrno(t, syscall.ENOENT, Stat(path.Join(tmpDir, "cat"), &stat))
require.EqualErrno(t, syscall.ENOENT, Stat(path.Join(tmpDir, "sub/cat"), &stat))
require.EqualErrno(t, syscall.ENOENT, Stat(pathutil.Join(tmpDir, "cat"), &stat))
require.EqualErrno(t, syscall.ENOENT, Stat(pathutil.Join(tmpDir, "sub/cat"), &stat))
t.Run("dir", func(t *testing.T) {
err := Stat(tmpDir, &stat)
require.NoError(t, err)
require.True(t, stat.Mode.IsDir())
require.NotEqual(t, uint64(0), stat.Ino)
})
file := pathutil.Join(tmpDir, "file")
var statFile Stat_t
t.Run("file", func(t *testing.T) {
file := path.Join(tmpDir, "file")
require.NoError(t, os.WriteFile(file, nil, 0o400))
require.NoError(t, Stat(file, &stat))
require.False(t, stat.Mode.IsDir())
require.NoError(t, Stat(file, &statFile))
require.False(t, statFile.Mode.IsDir())
require.NotEqual(t, uint64(0), stat.Ino)
})
t.Run("link to file", func(t *testing.T) {
link := pathutil.Join(tmpDir, "file-link")
require.NoError(t, os.Symlink(file, link))
require.NoError(t, Stat(link, &stat))
require.Equal(t, statFile, stat) // resolves to the file
})
subdir := pathutil.Join(tmpDir, "sub")
var statSubdir Stat_t
t.Run("subdir", func(t *testing.T) {
subdir := path.Join(tmpDir, "sub")
require.NoError(t, os.Mkdir(subdir, 0o500))
require.NoError(t, Stat(subdir, &stat))
require.True(t, stat.Mode.IsDir())
require.NoError(t, Stat(subdir, &statSubdir))
require.True(t, statSubdir.Mode.IsDir())
require.NotEqual(t, uint64(0), stat.Ino)
})
t.Run("link to dir", func(t *testing.T) {
link := pathutil.Join(tmpDir, "dir-link")
require.NoError(t, os.Symlink(subdir, link))
require.NoError(t, Stat(link, &stat))
require.Equal(t, statSubdir, stat) // resolves to the dir
})
}
@@ -54,16 +148,19 @@ func TestStatFile(t *testing.T) {
err = StatFile(tmpDirF, &stat)
require.NoError(t, err)
require.True(t, stat.Mode.IsDir())
requireDirectoryDevIno(t, stat)
})
if runtime.GOOS != "windows" { // windows allows you to stat a closed dir
// Windows allows you to stat a closed dir because it is accessed by path,
// not by file descriptor.
if runtime.GOOS != "windows" {
t.Run("closed dir", func(t *testing.T) {
require.NoError(t, tmpDirF.Close())
require.EqualErrno(t, syscall.EBADF, StatFile(tmpDirF, &stat))
})
}
file := path.Join(tmpDir, "file")
file := pathutil.Join(tmpDir, "file")
require.NoError(t, os.WriteFile(file, nil, 0o400))
fileF, err := OpenFile(file, syscall.O_RDONLY, 0)
require.NoError(t, err)
@@ -73,14 +170,16 @@ func TestStatFile(t *testing.T) {
err = StatFile(fileF, &stat)
require.NoError(t, err)
require.False(t, stat.Mode.IsDir())
require.NotEqual(t, uint64(0), stat.Ino)
})
t.Run("closed file", func(t *testing.T) {
require.NoError(t, fileF.Close())
require.EqualErrno(t, syscall.EBADF, StatFile(fileF, &stat))
require.NotEqual(t, uint64(0), stat.Ino)
})
subdir := path.Join(tmpDir, "sub")
subdir := pathutil.Join(tmpDir, "sub")
require.NoError(t, os.Mkdir(subdir, 0o500))
subdirF, err := OpenFile(subdir, syscall.O_RDONLY, 0)
require.NoError(t, err)
@@ -90,6 +189,7 @@ func TestStatFile(t *testing.T) {
err = StatFile(subdirF, &stat)
require.NoError(t, err)
require.True(t, stat.Mode.IsDir())
requireDirectoryDevIno(t, stat)
})
if runtime.GOOS != "windows" { // windows allows you to stat a closed dir
@@ -103,7 +203,7 @@ func TestStatFile(t *testing.T) {
func Test_StatFile_times(t *testing.T) {
tmpDir := t.TempDir()
file := path.Join(tmpDir, "file")
file := pathutil.Join(tmpDir, "file")
err := os.WriteFile(file, []byte{}, 0o700)
require.NoError(t, err)
@@ -153,36 +253,56 @@ func Test_StatFile_times(t *testing.T) {
func TestStatFile_dev_inode(t *testing.T) {
tmpDir := t.TempDir()
d, err := os.Open(tmpDir)
require.NoError(t, err)
defer d.Close()
path1 := path.Join(tmpDir, "1")
path1 := pathutil.Join(tmpDir, "1")
f1, err := os.Create(path1)
require.NoError(t, err)
defer f1.Close()
path2 := path.Join(tmpDir, "2")
path2 := pathutil.Join(tmpDir, "2")
f2, err := os.Create(path2)
require.NoError(t, err)
defer f2.Close()
pathLink2 := pathutil.Join(tmpDir, "link2")
err = os.Symlink(path2, pathLink2)
require.NoError(t, err)
l2, err := os.Open(pathLink2)
require.NoError(t, err)
defer l2.Close()
// First, stat the directory
var stat1 Stat_t
require.NoError(t, StatFile(d, &stat1))
requireDirectoryDevIno(t, stat1)
// Now, stat the files in it
require.NoError(t, StatFile(f1, &stat1))
var stat2 Stat_t
require.NoError(t, StatFile(f2, &stat2))
var stat3 Stat_t
require.NoError(t, StatFile(l2, &stat3))
// The files should be on the same device, but different inodes
require.Equal(t, stat1.Dev, stat2.Dev)
require.NotEqual(t, stat1.Ino, stat2.Ino)
require.Equal(t, stat2, stat3) // stat on a link is for its target
// Redoing stat should result in the same inodes
var stat1Again Stat_t
require.NoError(t, StatFile(f1, &stat1Again))
require.Equal(t, stat1.Dev, stat1Again.Dev)
require.Equal(t, stat1.Ino, stat1Again.Ino)
// On Windows, we cannot rename while opening.
// So we manually close here before renaming.
require.NoError(t, f1.Close())
require.NoError(t, f2.Close())
require.NoError(t, l2.Close())
// Renaming a file shouldn't change its inodes.
require.NoError(t, Rename(path1, path2))
@@ -194,3 +314,14 @@ func TestStatFile_dev_inode(t *testing.T) {
require.Equal(t, stat1.Dev, stat1Again.Dev)
require.Equal(t, stat1.Ino, stat1Again.Ino)
}
func requireDirectoryDevIno(t *testing.T, st Stat_t) {
// windows before go 1.20 has trouble reading the inode information on directories.
if runtime.GOOS != "windows" || strings.HasPrefix(runtime.Version(), "go1.20") {
require.NotEqual(t, uint64(0), st.Dev)
require.NotEqual(t, uint64(0), st.Ino)
} else {
require.Zero(t, st.Dev)
require.Zero(t, st.Ino)
}
}

View File

@@ -2,7 +2,18 @@
package platform
import "os"
import (
"io/fs"
"os"
)
func lstat(path string, st *Stat_t) (err error) {
t, err := os.Lstat(path)
if err = UnwrapOSError(err); err == nil {
fillStatFromFileInfo(st, t)
}
return
}
func stat(path string, st *Stat_t) (err error) {
t, err := os.Stat(path)
@@ -12,7 +23,15 @@ func stat(path string, st *Stat_t) (err error) {
return
}
func fillStatFromOpenFile(stat *Stat_t, fd uintptr, t os.FileInfo) (err error) {
fillStatFromFileInfo(stat, t)
func statFile(f fs.File, st *Stat_t) error {
return defaultStatFile(f, st)
}
func fillStatFromFileInfo(st *Stat_t, t fs.FileInfo) {
fillStatFromDefaultFileInfo(st, t)
}
func fillStatFromOpenFile(st *Stat_t, fd uintptr, t os.FileInfo) (err error) {
fillStatFromFileInfo(st, t)
return
}

View File

@@ -3,46 +3,119 @@
package platform
import (
"os"
"io/fs"
"syscall"
)
func stat(path string, st *Stat_t) (err error) {
// TODO: See if we can refactor to avoid opening a file first.
f, err := OpenFile(path, syscall.O_RDONLY, 0)
if err != nil {
return
}
defer f.Close()
return StatFile(f, st)
func lstat(path string, st *Stat_t) error {
attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS)
// Use FILE_FLAG_OPEN_REPARSE_POINT, otherwise CreateFile will follow symlink.
// See https://docs.microsoft.com/en-us/windows/desktop/FileIO/symbolic-link-effects-on-file-systems-functions#createfile-and-createfiletransacted
attrs |= syscall.FILE_FLAG_OPEN_REPARSE_POINT
return statPath(attrs, path, st)
}
func fillStatFromOpenFile(stat *Stat_t, fd uintptr, t os.FileInfo) (err error) {
d := t.Sys().(*syscall.Win32FileAttributeData)
handle := syscall.Handle(fd)
var info syscall.ByHandleFileInformation
if err = syscall.GetFileInformationByHandle(handle, &info); err != nil {
// If the file descriptor is already closed, we have to re-open just like
// os.Stat does to allow the results on the closed files.
// https://github.com/golang/go/blob/go1.20/src/os/stat_windows.go#L86
//
// TODO: once we have our File/Stat type, this shouldn't be necessary.
// But for now, ignore the error to pass the std library test for bad file descriptor.
// https://github.com/ziglang/zig/blob/master/lib/std/os/test.zig#L167-L170
if err == syscall.Errno(6) {
err = nil
func stat(path string, st *Stat_t) error {
attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS)
return statPath(attrs, path, st)
}
func statPath(createFileAttrs uint32, path string, st *Stat_t) (err error) {
if len(path) == 0 {
return syscall.ENOENT
}
pathp, err := syscall.UTF16PtrFromString(path)
if err != nil {
return syscall.EINVAL
}
// open the file handle
h, err := syscall.CreateFile(pathp, 0, 0, nil,
syscall.OPEN_EXISTING, createFileAttrs, 0)
if err != nil {
// To match expectations of WASI, e.g. TinyGo TestStatBadDir, return
// ENOENT, not ENOTDIR.
if err == syscall.ENOTDIR {
err = syscall.ENOENT
}
return err
}
defer syscall.CloseHandle(h)
return statHandle(h, st)
}
// 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 statFile(f fs.File, st *Stat_t) (err error) {
if of, ok := f.(fdFile); ok {
// Attempt to get the stat by handle, which works for normal files
err = statHandle(syscall.Handle(of.Fd()), st)
// ERROR_INVALID_HANDLE happens before Go 1.20. Don't fail as we only
// use that approach to fill in inode data, which is not critical.
if err != ERROR_INVALID_HANDLE {
return
}
}
return defaultStatFile(f, st)
}
func fillStatFromFileInfo(st *Stat_t, t fs.FileInfo) {
if d, ok := t.Sys().(*syscall.Win32FileAttributeData); ok {
st.Ino = 0 // not in Win32FileAttributeData
st.Dev = 0 // not in Win32FileAttributeData
st.Mode = t.Mode()
st.Nlink = 1 // not in Win32FileAttributeData
st.Size = t.Size()
st.Atim = d.LastAccessTime.Nanoseconds()
st.Mtim = d.LastWriteTime.Nanoseconds()
st.Ctim = d.CreationTime.Nanoseconds()
} else {
fillStatFromDefaultFileInfo(st, t)
}
}
func statHandle(h syscall.Handle, st *Stat_t) (err error) {
winFt, err := syscall.GetFileType(h)
if err != nil {
return err
}
var fi syscall.ByHandleFileInformation
if err = syscall.GetFileInformationByHandle(h, &fi); err != nil {
return err
}
var m fs.FileMode
if fi.FileAttributes&syscall.FILE_ATTRIBUTE_READONLY != 0 {
m |= 0o444
} else {
m |= 0o666
}
switch { // check whether this is a symlink first
case fi.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0:
m |= fs.ModeSymlink
case winFt == syscall.FILE_TYPE_PIPE:
m |= fs.ModeNamedPipe
case winFt == syscall.FILE_TYPE_CHAR:
m |= fs.ModeDevice | fs.ModeCharDevice
case fi.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0:
m |= fs.ModeDir | 0o111 // e.g. 0o444 -> 0o555
}
// FileIndex{High,Low} can be combined and used as a unique identifier like inode.
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/ns-fileapi-by_handle_file_information
stat.Ino = (uint64(info.FileIndexHigh) << 32) | uint64(info.FileIndexLow)
stat.Dev = uint64(info.VolumeSerialNumber)
stat.Mode = t.Mode()
stat.Nlink = uint64(info.NumberOfLinks)
stat.Size = t.Size()
stat.Atim = d.LastAccessTime.Nanoseconds()
stat.Mtim = d.LastWriteTime.Nanoseconds()
stat.Ctim = d.CreationTime.Nanoseconds()
st.Dev = uint64(fi.VolumeSerialNumber)
st.Ino = (uint64(fi.FileIndexHigh) << 32) | uint64(fi.FileIndexLow)
st.Mode = m
st.Nlink = uint64(fi.NumberOfLinks)
st.Size = int64(fi.FileSizeHigh)<<32 + int64(fi.FileSizeLow)
st.Atim = fi.LastAccessTime.Nanoseconds()
st.Mtim = fi.LastWriteTime.Nanoseconds()
st.Ctim = fi.CreationTime.Nanoseconds()
return
}

View File

@@ -3,6 +3,8 @@ package sys
import (
"bytes"
"io/fs"
"runtime"
"strings"
"testing"
"time"
@@ -110,12 +112,23 @@ func TestFileEntry_cachedStat(t *testing.T) {
ino, ft, err := f.CachedStat()
require.NoError(t, err)
require.Equal(t, fs.ModeDir, ft)
if !canReadDirInode() {
tc.expectedIno = 0
}
require.Equal(t, tc.expectedIno, ino)
require.Equal(t, &cachedStat{Ino: tc.expectedIno, Type: fs.ModeDir}, f.cachedStat)
})
}
}
func canReadDirInode() bool {
if runtime.GOOS != "windows" {
return true
} else {
return strings.HasPrefix(runtime.Version(), "go1.20")
}
}
func TestNewContext_Args(t *testing.T) {
tests := []struct {
name string

View File

@@ -9,6 +9,7 @@ import (
"testing"
"github.com/tetratelabs/wazero/internal/fstest"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/testing/require"
)
@@ -104,6 +105,22 @@ func TestAdapt_Open_Read(t *testing.T) {
})
}
// TestAdapt_Lstat is unsupported because the Lstat() function is not implemented
// on os.File.
func TestAdapt_Lstat(t *testing.T) {
tmpDir := t.TempDir()
require.NoError(t, fstest.WriteTestFiles(tmpDir))
testFS := Adapt(os.DirFS(tmpDir))
for _, path := range []string{"animals.txt", "sub", "sub-link"} {
fullPath := pathutil.Join(tmpDir, path)
linkPath := pathutil.Join(tmpDir, path+"-link")
require.NoError(t, os.Symlink(fullPath, linkPath))
var stat platform.Stat_t
require.EqualErrno(t, syscall.ENOSYS, testFS.Lstat(linkPath, &stat))
}
}
func TestAdapt_Stat(t *testing.T) {
tmpDir := t.TempDir()
require.NoError(t, fstest.WriteTestFiles(tmpDir))

View File

@@ -49,6 +49,11 @@ func (d *dirFS) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, erro
}
}
// Lstat implements FS.Lstat
func (d *dirFS) Lstat(path string, stat *platform.Stat_t) error {
return platform.Lstat(d.join(path), stat)
}
// Stat implements FS.Stat
func (d *dirFS) Stat(path string, stat *platform.Stat_t) error {
return platform.Stat(d.join(path), stat)

View File

@@ -45,6 +45,18 @@ func TestDirFS_String(t *testing.T) {
require.Equal(t, ".", testFS.String())
}
func TestDirFS_Lstat(t *testing.T) {
tmpDir := t.TempDir()
require.NoError(t, fstest.WriteTestFiles(tmpDir))
testFS := NewDirFS(tmpDir)
for _, path := range []string{"animals.txt", "sub", "sub-link"} {
require.NoError(t, testFS.Symlink(path, path+"-link"))
}
testLstat(t, testFS)
}
func TestDirFS_MkDir(t *testing.T) {
tmpDir := t.TempDir()
testFS := NewDirFS(tmpDir)

View File

@@ -128,6 +128,11 @@ func maskForReads(f fs.File) fs.File {
}
}
// Lstat implements FS.Lstat
func (r *readFS) Lstat(path string, lstat *platform.Stat_t) error {
return r.fs.Lstat(path, lstat)
}
// Stat implements FS.Stat
func (r *readFS) Stat(path string, stat *platform.Stat_t) error {
return r.fs.Stat(path, stat)

View File

@@ -33,6 +33,20 @@ func TestReadFS_String(t *testing.T) {
require.Equal(t, "/tmp", readFS.String())
}
func TestReadFS_Lstat(t *testing.T) {
tmpDir := t.TempDir()
require.NoError(t, fstest.WriteTestFiles(tmpDir))
writeable := NewDirFS(tmpDir)
for _, path := range []string{"animals.txt", "sub", "sub-link"} {
require.NoError(t, writeable.Symlink(path, path+"-link"))
}
testFS := NewReadFS(writeable)
testLstat(t, testFS)
}
func TestReadFS_MkDir(t *testing.T) {
writeable := NewDirFS(t.TempDir())
testFS := NewReadFS(writeable)

View File

@@ -61,6 +61,22 @@ type FS interface {
// ^^ TODO: Consider syscall.Open, though this implies defining and
// coercing flags and perms similar to what is done in os.OpenFile.
// Lstat is similar to syscall.Lstat, except the path is relative to this
// file system.
//
// # Errors
//
// The following errors are expected:
// - syscall.ENOENT: `path` doesn't exist.
//
// # Notes
//
// - An fs.FileInfo backed implementation sets atim, mtim and ctim to the
// same value.
// - When the path is a symbolic link, the stat returned is for the link,
// not the file it refers to.
Lstat(path string, stat *platform.Stat_t) error
// Stat is similar to syscall.Stat, except the path is relative to this
// file system.
//
@@ -73,6 +89,8 @@ type FS interface {
//
// - An fs.FileInfo backed implementation sets atim, mtim and ctim to the
// same value.
// - When the path is a symbolic link, the stat returned is for the file
// it refers to.
Stat(path string, stat *platform.Stat_t) error
// Mkdir is similar to os.Mkdir, except the path is relative to this file

View File

@@ -11,6 +11,7 @@ import (
"path"
"runtime"
"sort"
"strings"
"syscall"
"testing"
gofstest "testing/fstest"
@@ -64,7 +65,7 @@ func testOpen_O_RDWR(t *testing.T, tmpDir string, testFS FS) {
// Verify stat on the file
stat, err := f.Stat()
require.NoError(t, err)
require.Equal(t, fs.FileMode(0o444), stat.Mode()&fs.ModePerm)
require.Equal(t, fs.FileMode(0o444), stat.Mode().Perm())
}
func testOpen_Read(t *testing.T, tmpDir string, testFS FS) {
@@ -176,6 +177,67 @@ func testOpen_Read(t *testing.T, tmpDir string, testFS FS) {
})
}
func testLstat(t *testing.T, testFS FS) {
var stat platform.Stat_t
require.EqualErrno(t, syscall.ENOENT, testFS.Lstat("cat", &stat))
require.EqualErrno(t, syscall.ENOENT, testFS.Lstat("sub/cat", &stat))
t.Run("dir", func(t *testing.T) {
err := testFS.Lstat(".", &stat)
require.NoError(t, err)
require.True(t, stat.Mode.IsDir())
require.NotEqual(t, uint64(0), stat.Ino)
})
var statFile platform.Stat_t
t.Run("file", func(t *testing.T) {
require.NoError(t, testFS.Lstat("animals.txt", &statFile))
require.Zero(t, statFile.Mode.Type())
require.Equal(t, int64(30), statFile.Size)
require.NotEqual(t, uint64(0), stat.Ino)
})
t.Run("link to file", func(t *testing.T) {
requireLinkStat(t, testFS, "animals.txt", &statFile)
})
var statSubdir platform.Stat_t
t.Run("subdir", func(t *testing.T) {
require.NoError(t, testFS.Lstat("sub", &statSubdir))
require.True(t, statSubdir.Mode.IsDir())
require.NotEqual(t, uint64(0), stat.Ino)
})
t.Run("link to dir", func(t *testing.T) {
requireLinkStat(t, testFS, "sub", &statSubdir)
})
t.Run("link to dir link", func(t *testing.T) {
pathLink := "sub-link"
var statLink platform.Stat_t
require.NoError(t, testFS.Lstat(pathLink, &statLink))
requireLinkStat(t, testFS, pathLink, &statLink)
})
}
func requireLinkStat(t *testing.T, testFS FS, path string, stat *platform.Stat_t) {
link := path + "-link"
var linkStat platform.Stat_t
require.NoError(t, testFS.Lstat(link, &linkStat))
require.NotEqual(t, stat.Ino, linkStat.Ino) // inodes are not equal
require.Equal(t, fs.ModeSymlink, linkStat.Mode.Type())
// From https://linux.die.net/man/2/lstat:
// The size of a symbolic link is the length of the pathname it
// contains, without a terminating null byte.
if runtime.GOOS == "windows" { // size is zero, not the path length
require.Zero(t, linkStat.Size)
} else {
require.Equal(t, int64(len(path)), linkStat.Size)
}
}
func testStat(t *testing.T, testFS FS) {
var stat platform.Stat_t
require.EqualErrno(t, syscall.ENOENT, testFS.Stat("cat", &stat))
@@ -184,10 +246,17 @@ func testStat(t *testing.T, testFS FS) {
err := testFS.Stat("sub/test.txt", &stat)
require.NoError(t, err)
require.False(t, stat.Mode.IsDir())
require.NotEqual(t, uint64(0), stat.Dev)
require.NotEqual(t, uint64(0), stat.Ino)
err = testFS.Stat("sub", &stat)
require.NoError(t, err)
require.True(t, stat.Mode.IsDir())
// windows before go 1.20 has trouble reading the inode information on directories.
if runtime.GOOS != "windows" || strings.HasPrefix(runtime.Version(), "go1.20") {
require.NotEqual(t, uint64(0), stat.Dev)
require.NotEqual(t, uint64(0), stat.Ino)
}
}
// requireReadDir ensures the input file is a directory, and returns its

View File

@@ -26,6 +26,11 @@ func (UnimplementedFS) OpenFile(path string, flag int, perm fs.FileMode) (fs.Fil
return nil, syscall.ENOSYS
}
// Lstat implements FS.Lstat
func (UnimplementedFS) Lstat(path string, stat *platform.Stat_t) error {
return syscall.ENOSYS
}
// Stat implements FS.Stat
func (UnimplementedFS) Stat(path string, stat *platform.Stat_t) error {
return syscall.ENOSYS