[experiment] add alternative wasm sqlite3 implementation available via build-tag (#2863)

This allows for building GoToSocial with [SQLite transpiled to WASM](https://github.com/ncruces/go-sqlite3) and accessed through [Wazero](https://wazero.io/).
This commit is contained in:
kim
2024-05-27 15:46:15 +00:00
committed by GitHub
parent cce21c11cb
commit 1e7b32490d
398 changed files with 86174 additions and 684 deletions

View File

@ -0,0 +1,105 @@
package sysfs
import (
"fmt"
"io/fs"
"path"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/sys"
)
type AdaptFS struct {
FS fs.FS
}
// String implements fmt.Stringer
func (a *AdaptFS) String() string {
return fmt.Sprintf("%v", a.FS)
}
// OpenFile implements the same method as documented on sys.FS
func (a *AdaptFS) OpenFile(path string, flag experimentalsys.Oflag, perm fs.FileMode) (experimentalsys.File, experimentalsys.Errno) {
return OpenFSFile(a.FS, cleanPath(path), flag, perm)
}
// Lstat implements the same method as documented on sys.FS
func (a *AdaptFS) Lstat(path string) (sys.Stat_t, experimentalsys.Errno) {
// At this time, we make the assumption sys.FS instances do not support
// symbolic links, therefore Lstat is the same as Stat. This is obviously
// not true, but until FS.FS has a solid story for how to handle symlinks,
// we are better off not making a decision that would be difficult to
// revert later on.
//
// For further discussions on the topic, see:
// https://github.com/golang/go/issues/49580
return a.Stat(path)
}
// Stat implements the same method as documented on sys.FS
func (a *AdaptFS) Stat(path string) (sys.Stat_t, experimentalsys.Errno) {
f, errno := a.OpenFile(path, experimentalsys.O_RDONLY, 0)
if errno != 0 {
return sys.Stat_t{}, errno
}
defer f.Close()
return f.Stat()
}
// Readlink implements the same method as documented on sys.FS
func (a *AdaptFS) Readlink(string) (string, experimentalsys.Errno) {
return "", experimentalsys.ENOSYS
}
// Mkdir implements the same method as documented on sys.FS
func (a *AdaptFS) Mkdir(string, fs.FileMode) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Chmod implements the same method as documented on sys.FS
func (a *AdaptFS) Chmod(string, fs.FileMode) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Rename implements the same method as documented on sys.FS
func (a *AdaptFS) Rename(string, string) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Rmdir implements the same method as documented on sys.FS
func (a *AdaptFS) Rmdir(string) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Link implements the same method as documented on sys.FS
func (a *AdaptFS) Link(string, string) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Symlink implements the same method as documented on sys.FS
func (a *AdaptFS) Symlink(string, string) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Unlink implements the same method as documented on sys.FS
func (a *AdaptFS) Unlink(string) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Utimens implements the same method as documented on sys.FS
func (a *AdaptFS) Utimens(string, int64, int64) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
func cleanPath(name string) string {
if len(name) == 0 {
return name
}
// fs.ValidFile cannot be rooted (start with '/')
cleaned := name
if name[0] == '/' {
cleaned = name[1:]
}
cleaned = path.Clean(cleaned) // e.g. "sub/." -> "sub"
return cleaned
}

View File

@ -0,0 +1,14 @@
//go:build linux && !tinygo
package sysfs
import (
"os"
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
)
func datasync(f *os.File) sys.Errno {
return sys.UnwrapOSError(syscall.Fdatasync(int(f.Fd())))
}

View File

@ -0,0 +1,13 @@
//go:build tinygo
package sysfs
import (
"os"
"github.com/tetratelabs/wazero/experimental/sys"
)
func datasync(f *os.File) sys.Errno {
return sys.ENOSYS
}

View File

@ -0,0 +1,14 @@
//go:build !linux
package sysfs
import (
"os"
"github.com/tetratelabs/wazero/experimental/sys"
)
func datasync(f *os.File) sys.Errno {
// Attempt to sync everything, even if we only need to sync the data.
return fsync(f)
}

View File

@ -0,0 +1,24 @@
package sysfs
import (
"io"
"github.com/tetratelabs/wazero/experimental/sys"
)
func adjustReaddirErr(f sys.File, isClosed bool, err error) sys.Errno {
if err == io.EOF {
return 0 // e.g. Readdir on darwin returns io.EOF, but linux doesn't.
} else if errno := sys.UnwrapOSError(err); errno != 0 {
errno = dirError(f, isClosed, errno)
// Comply with errors allowed on sys.File Readdir
switch errno {
case sys.EINVAL: // os.File Readdir can return this
return sys.EBADF
case sys.ENOTDIR: // dirError can return this
return sys.EBADF
}
return errno
}
return 0
}

View File

@ -0,0 +1,99 @@
package sysfs
import (
"io/fs"
"os"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/sys"
)
func DirFS(dir string) experimentalsys.FS {
return &dirFS{
dir: dir,
cleanedDir: ensureTrailingPathSeparator(dir),
}
}
func ensureTrailingPathSeparator(dir string) string {
if !os.IsPathSeparator(dir[len(dir)-1]) {
return dir + string(os.PathSeparator)
}
return dir
}
// dirFS is not exported because the input fields must be maintained together.
// This is likely why os.DirFS doesn't, either!
type dirFS struct {
experimentalsys.UnimplementedFS
dir string
// cleanedDir is for easier OS-specific concatenation, as it always has
// a trailing path separator.
cleanedDir string
}
// String implements fmt.Stringer
func (d *dirFS) String() string {
return d.dir
}
// OpenFile implements the same method as documented on sys.FS
func (d *dirFS) OpenFile(path string, flag experimentalsys.Oflag, perm fs.FileMode) (experimentalsys.File, experimentalsys.Errno) {
return OpenOSFile(d.join(path), flag, perm)
}
// Lstat implements the same method as documented on sys.FS
func (d *dirFS) Lstat(path string) (sys.Stat_t, experimentalsys.Errno) {
return lstat(d.join(path))
}
// Stat implements the same method as documented on sys.FS
func (d *dirFS) Stat(path string) (sys.Stat_t, experimentalsys.Errno) {
return stat(d.join(path))
}
// Mkdir implements the same method as documented on sys.FS
func (d *dirFS) Mkdir(path string, perm fs.FileMode) (errno experimentalsys.Errno) {
err := os.Mkdir(d.join(path), perm)
if errno = experimentalsys.UnwrapOSError(err); errno == experimentalsys.ENOTDIR {
errno = experimentalsys.ENOENT
}
return
}
// Readlink implements the same method as documented on sys.FS
func (d *dirFS) Readlink(path string) (string, experimentalsys.Errno) {
// Note: do not use syscall.Readlink as that causes race on Windows.
// In any case, syscall.Readlink does almost the same logic as os.Readlink.
dst, err := os.Readlink(d.join(path))
if err != nil {
return "", experimentalsys.UnwrapOSError(err)
}
return platform.ToPosixPath(dst), 0
}
// Rmdir implements the same method as documented on sys.FS
func (d *dirFS) Rmdir(path string) experimentalsys.Errno {
return rmdir(d.join(path))
}
// Utimens implements the same method as documented on sys.FS
func (d *dirFS) Utimens(path string, atim, mtim int64) experimentalsys.Errno {
return utimens(d.join(path), atim, mtim)
}
func (d *dirFS) join(path string) string {
switch path {
case "", ".", "/":
if d.cleanedDir == "/" {
return "/"
}
// cleanedDir includes an unnecessary delimiter for the root path.
return d.cleanedDir[:len(d.cleanedDir)-1]
}
// TODO: Enforce similar to safefilepath.FromFS(path), but be careful as
// relative path inputs are allowed. e.g. dir or path == ../
return d.cleanedDir + path
}

View File

@ -0,0 +1,42 @@
//go:build !tinygo
package sysfs
import (
"io/fs"
"os"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
)
// Link implements the same method as documented on sys.FS
func (d *dirFS) Link(oldName, newName string) experimentalsys.Errno {
err := os.Link(d.join(oldName), d.join(newName))
return experimentalsys.UnwrapOSError(err)
}
// Unlink implements the same method as documented on sys.FS
func (d *dirFS) Unlink(path string) (err experimentalsys.Errno) {
return unlink(d.join(path))
}
// Rename implements the same method as documented on sys.FS
func (d *dirFS) Rename(from, to string) experimentalsys.Errno {
from, to = d.join(from), d.join(to)
return rename(from, to)
}
// Chmod implements the same method as documented on sys.FS
func (d *dirFS) Chmod(path string, perm fs.FileMode) experimentalsys.Errno {
err := os.Chmod(d.join(path), perm)
return experimentalsys.UnwrapOSError(err)
}
// Symlink implements the same method as documented on sys.FS
func (d *dirFS) Symlink(oldName, link string) experimentalsys.Errno {
// Note: do not resolve `oldName` relative to this dirFS. The link result is always resolved
// when dereference the `link` on its usage (e.g. readlink, read, etc).
// https://github.com/bytecodealliance/cap-std/blob/v1.0.4/cap-std/src/fs/dir.rs#L404-L409
err := os.Symlink(oldName, d.join(link))
return experimentalsys.UnwrapOSError(err)
}

View File

@ -0,0 +1,34 @@
//go:build tinygo
package sysfs
import (
"io/fs"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
)
// Link implements the same method as documented on sys.FS
func (d *dirFS) Link(oldName, newName string) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Unlink implements the same method as documented on sys.FS
func (d *dirFS) Unlink(path string) (err experimentalsys.Errno) {
return experimentalsys.ENOSYS
}
// Rename implements the same method as documented on sys.FS
func (d *dirFS) Rename(from, to string) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Chmod implements the same method as documented on sys.FS
func (d *dirFS) Chmod(path string, perm fs.FileMode) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Symlink implements the same method as documented on sys.FS
func (d *dirFS) Symlink(oldName, link string) experimentalsys.Errno {
return experimentalsys.ENOSYS
}

View File

@ -0,0 +1,520 @@
package sysfs
import (
"io"
"io/fs"
"os"
"time"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/sys"
)
func NewStdioFile(stdin bool, f fs.File) (fsapi.File, error) {
// Return constant stat, which has fake times, but keep the underlying
// file mode. Fake times are needed to pass wasi-testsuite.
// https://github.com/WebAssembly/wasi-testsuite/blob/af57727/tests/rust/src/bin/fd_filestat_get.rs#L1-L19
var mode fs.FileMode
if st, err := f.Stat(); err != nil {
return nil, err
} else {
mode = st.Mode()
}
var flag experimentalsys.Oflag
if stdin {
flag = experimentalsys.O_RDONLY
} else {
flag = experimentalsys.O_WRONLY
}
var file fsapi.File
if of, ok := f.(*os.File); ok {
// This is ok because functions that need path aren't used by stdioFile
file = newOsFile("", flag, 0, of)
} else {
file = &fsFile{file: f}
}
return &stdioFile{File: file, st: sys.Stat_t{Mode: mode, Nlink: 1}}, nil
}
func OpenFile(path string, flag experimentalsys.Oflag, perm fs.FileMode) (*os.File, experimentalsys.Errno) {
if flag&experimentalsys.O_DIRECTORY != 0 && flag&(experimentalsys.O_WRONLY|experimentalsys.O_RDWR) != 0 {
return nil, experimentalsys.EISDIR // invalid to open a directory writeable
}
return openFile(path, flag, perm)
}
func OpenOSFile(path string, flag experimentalsys.Oflag, perm fs.FileMode) (experimentalsys.File, experimentalsys.Errno) {
f, errno := OpenFile(path, flag, perm)
if errno != 0 {
return nil, errno
}
return newOsFile(path, flag, perm, f), 0
}
func OpenFSFile(fs fs.FS, path string, flag experimentalsys.Oflag, perm fs.FileMode) (experimentalsys.File, experimentalsys.Errno) {
if flag&experimentalsys.O_DIRECTORY != 0 && flag&(experimentalsys.O_WRONLY|experimentalsys.O_RDWR) != 0 {
return nil, experimentalsys.EISDIR // invalid to open a directory writeable
}
f, err := fs.Open(path)
if errno := experimentalsys.UnwrapOSError(err); errno != 0 {
return nil, errno
}
// Don't return an os.File because the path is not absolute. osFile needs
// the path to be real and certain FS.File impls are subrooted.
return &fsFile{fs: fs, name: path, file: f}, 0
}
type stdioFile struct {
fsapi.File
st sys.Stat_t
}
// SetAppend implements File.SetAppend
func (f *stdioFile) SetAppend(bool) experimentalsys.Errno {
// Ignore for stdio.
return 0
}
// IsAppend implements File.SetAppend
func (f *stdioFile) IsAppend() bool {
return true
}
// Stat implements File.Stat
func (f *stdioFile) Stat() (sys.Stat_t, experimentalsys.Errno) {
return f.st, 0
}
// Close implements File.Close
func (f *stdioFile) Close() experimentalsys.Errno {
return 0
}
// fsFile is used for wrapped fs.File, like os.Stdin or any fs.File
// implementation. Notably, this does not have access to the full file path.
// so certain operations can't be supported, such as inode lookups on Windows.
type fsFile struct {
experimentalsys.UnimplementedFile
// fs is the file-system that opened the file, or nil when wrapped for
// pre-opens like stdio.
fs fs.FS
// name is what was used in fs for Open, so it may not be the actual path.
name string
// file is always set, possibly an os.File like os.Stdin.
file fs.File
// reopenDir is true if reopen should be called before Readdir. This flag
// is deferred until Readdir to prevent redundant rewinds. This could
// happen if Seek(0) was called twice, or if in Windows, Seek(0) was called
// before Readdir.
reopenDir bool
// closed is true when closed was called. This ensures proper sys.EBADF
closed bool
// cachedStat includes fields that won't change while a file is open.
cachedSt *cachedStat
}
type cachedStat struct {
// dev is the same as sys.Stat_t Dev.
dev uint64
// dev is the same as sys.Stat_t Ino.
ino sys.Inode
// isDir is sys.Stat_t Mode masked with fs.ModeDir
isDir bool
}
// cachedStat returns the cacheable parts of sys.Stat_t or an error if they
// couldn't be retrieved.
func (f *fsFile) cachedStat() (dev uint64, ino sys.Inode, isDir bool, errno experimentalsys.Errno) {
if f.cachedSt == nil {
if _, errno = f.Stat(); errno != 0 {
return
}
}
return f.cachedSt.dev, f.cachedSt.ino, f.cachedSt.isDir, 0
}
// Dev implements the same method as documented on sys.File
func (f *fsFile) Dev() (uint64, experimentalsys.Errno) {
dev, _, _, errno := f.cachedStat()
return dev, errno
}
// Ino implements the same method as documented on sys.File
func (f *fsFile) Ino() (sys.Inode, experimentalsys.Errno) {
_, ino, _, errno := f.cachedStat()
return ino, errno
}
// IsDir implements the same method as documented on sys.File
func (f *fsFile) IsDir() (bool, experimentalsys.Errno) {
_, _, isDir, errno := f.cachedStat()
return isDir, errno
}
// IsAppend implements the same method as documented on sys.File
func (f *fsFile) IsAppend() bool {
return false
}
// SetAppend implements the same method as documented on sys.File
func (f *fsFile) SetAppend(bool) (errno experimentalsys.Errno) {
return fileError(f, f.closed, experimentalsys.ENOSYS)
}
// Stat implements the same method as documented on sys.File
func (f *fsFile) Stat() (sys.Stat_t, experimentalsys.Errno) {
if f.closed {
return sys.Stat_t{}, experimentalsys.EBADF
}
st, errno := statFile(f.file)
switch errno {
case 0:
f.cachedSt = &cachedStat{dev: st.Dev, ino: st.Ino, isDir: st.Mode&fs.ModeDir == fs.ModeDir}
case experimentalsys.EIO:
errno = experimentalsys.EBADF
}
return st, errno
}
// Read implements the same method as documented on sys.File
func (f *fsFile) Read(buf []byte) (n int, errno experimentalsys.Errno) {
if n, errno = read(f.file, buf); errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
}
return
}
// Pread implements the same method as documented on sys.File
func (f *fsFile) Pread(buf []byte, off int64) (n int, errno experimentalsys.Errno) {
if ra, ok := f.file.(io.ReaderAt); ok {
if n, errno = pread(ra, buf, off); errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
}
return
}
// See /RATIONALE.md "fd_pread: io.Seeker fallback when io.ReaderAt is not supported"
if rs, ok := f.file.(io.ReadSeeker); ok {
// Determine the current position in the file, as we need to revert it.
currentOffset, err := rs.Seek(0, io.SeekCurrent)
if err != nil {
return 0, fileError(f, f.closed, experimentalsys.UnwrapOSError(err))
}
// Put the read position back when complete.
defer func() { _, _ = rs.Seek(currentOffset, io.SeekStart) }()
// If the current offset isn't in sync with this reader, move it.
if off != currentOffset {
if _, err = rs.Seek(off, io.SeekStart); err != nil {
return 0, fileError(f, f.closed, experimentalsys.UnwrapOSError(err))
}
}
n, err = rs.Read(buf)
if errno = experimentalsys.UnwrapOSError(err); errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
}
} else {
errno = experimentalsys.ENOSYS // unsupported
}
return
}
// Seek implements the same method as documented on sys.File
func (f *fsFile) Seek(offset int64, whence int) (newOffset int64, errno experimentalsys.Errno) {
// If this is a directory, and we're attempting to seek to position zero,
// we have to re-open the file to ensure the directory state is reset.
var isDir bool
if offset == 0 && whence == io.SeekStart {
if isDir, errno = f.IsDir(); errno == 0 && isDir {
f.reopenDir = true
return
}
}
if s, ok := f.file.(io.Seeker); ok {
if newOffset, errno = seek(s, offset, whence); errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
}
} else {
errno = experimentalsys.ENOSYS // unsupported
}
return
}
// Readdir implements the same method as documented on sys.File
//
// Notably, this uses readdirFile or fs.ReadDirFile if available. This does not
// return inodes on windows.
func (f *fsFile) Readdir(n int) (dirents []experimentalsys.Dirent, errno experimentalsys.Errno) {
// Windows lets you Readdir after close, FS.File also may not implement
// close in a meaningful way. read our closed field to return consistent
// results.
if f.closed {
errno = experimentalsys.EBADF
return
}
if f.reopenDir { // re-open the directory if needed.
f.reopenDir = false
if errno = adjustReaddirErr(f, f.closed, f.reopen()); errno != 0 {
return
}
}
if of, ok := f.file.(readdirFile); ok {
// We can't use f.name here because it is the path up to the sys.FS,
// not necessarily the real path. For this reason, Windows may not be
// able to populate inodes. However, Darwin and Linux will.
if dirents, errno = readdir(of, "", n); errno != 0 {
errno = adjustReaddirErr(f, f.closed, errno)
}
return
}
// Try with FS.ReadDirFile which is available on api.FS implementations
// like embed:FS.
if rdf, ok := f.file.(fs.ReadDirFile); ok {
entries, e := rdf.ReadDir(n)
if errno = adjustReaddirErr(f, f.closed, e); errno != 0 {
return
}
dirents = make([]experimentalsys.Dirent, 0, len(entries))
for _, e := range entries {
// By default, we don't attempt to read inode data
dirents = append(dirents, experimentalsys.Dirent{Name: e.Name(), Type: e.Type()})
}
} else {
errno = experimentalsys.EBADF // not a directory
}
return
}
// Write implements the same method as documented on sys.File.
func (f *fsFile) Write(buf []byte) (n int, errno experimentalsys.Errno) {
if w, ok := f.file.(io.Writer); ok {
if n, errno = write(w, buf); errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
}
} else {
errno = experimentalsys.ENOSYS // unsupported
}
return
}
// Pwrite implements the same method as documented on sys.File.
func (f *fsFile) Pwrite(buf []byte, off int64) (n int, errno experimentalsys.Errno) {
if wa, ok := f.file.(io.WriterAt); ok {
if n, errno = pwrite(wa, buf, off); errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
}
} else {
errno = experimentalsys.ENOSYS // unsupported
}
return
}
// Close implements the same method as documented on sys.File.
func (f *fsFile) Close() experimentalsys.Errno {
if f.closed {
return 0
}
f.closed = true
return f.close()
}
func (f *fsFile) close() experimentalsys.Errno {
return experimentalsys.UnwrapOSError(f.file.Close())
}
// IsNonblock implements the same method as documented on fsapi.File
func (f *fsFile) IsNonblock() bool {
return false
}
// SetNonblock implements the same method as documented on fsapi.File
func (f *fsFile) SetNonblock(bool) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Poll implements the same method as documented on fsapi.File
func (f *fsFile) Poll(fsapi.Pflag, int32) (ready bool, errno experimentalsys.Errno) {
return false, experimentalsys.ENOSYS
}
// dirError is used for commands that work against a directory, but not a file.
func dirError(f experimentalsys.File, isClosed bool, errno experimentalsys.Errno) experimentalsys.Errno {
if vErrno := validate(f, isClosed, false, true); vErrno != 0 {
return vErrno
}
return errno
}
// fileError is used for commands that work against a file, but not a directory.
func fileError(f experimentalsys.File, isClosed bool, errno experimentalsys.Errno) experimentalsys.Errno {
if vErrno := validate(f, isClosed, true, false); vErrno != 0 {
return vErrno
}
return errno
}
// validate is used to making syscalls which will fail.
func validate(f experimentalsys.File, isClosed, wantFile, wantDir bool) experimentalsys.Errno {
if isClosed {
return experimentalsys.EBADF
}
isDir, errno := f.IsDir()
if errno != 0 {
return errno
}
if wantFile && isDir {
return experimentalsys.EISDIR
} else if wantDir && !isDir {
return experimentalsys.ENOTDIR
}
return 0
}
func read(r io.Reader, buf []byte) (n int, errno experimentalsys.Errno) {
if len(buf) == 0 {
return 0, 0 // less overhead on zero-length reads.
}
n, err := r.Read(buf)
return n, experimentalsys.UnwrapOSError(err)
}
func pread(ra io.ReaderAt, buf []byte, off int64) (n int, errno experimentalsys.Errno) {
if len(buf) == 0 {
return 0, 0 // less overhead on zero-length reads.
}
n, err := ra.ReadAt(buf, off)
return n, experimentalsys.UnwrapOSError(err)
}
func seek(s io.Seeker, offset int64, whence int) (int64, experimentalsys.Errno) {
if uint(whence) > io.SeekEnd {
return 0, experimentalsys.EINVAL // negative or exceeds the largest valid whence
}
newOffset, err := s.Seek(offset, whence)
return newOffset, experimentalsys.UnwrapOSError(err)
}
// reopenFile allows re-opening a file for reasons such as applying flags or
// directory iteration.
type reopenFile func() experimentalsys.Errno
// compile-time check to ensure fsFile.reopen implements reopenFile.
var _ reopenFile = (*fsFile)(nil).reopen
// reopen implements the same method as documented on reopenFile.
func (f *fsFile) reopen() experimentalsys.Errno {
_ = f.close()
var err error
f.file, err = f.fs.Open(f.name)
return experimentalsys.UnwrapOSError(err)
}
// readdirFile allows masking the `Readdir` function on os.File.
type readdirFile interface {
Readdir(n int) ([]fs.FileInfo, error)
}
// readdir uses readdirFile.Readdir, special casing windows when path !="".
func readdir(f readdirFile, path string, n int) (dirents []experimentalsys.Dirent, errno experimentalsys.Errno) {
fis, e := f.Readdir(n)
if errno = experimentalsys.UnwrapOSError(e); errno != 0 {
return
}
dirents = make([]experimentalsys.Dirent, 0, len(fis))
// linux/darwin won't have to fan out to lstat, but windows will.
var ino sys.Inode
for fi := range fis {
t := fis[fi]
// inoFromFileInfo is more efficient than sys.NewStat_t, as it gets the
// inode without allocating an instance and filling other fields.
if ino, errno = inoFromFileInfo(path, t); errno != 0 {
return
}
dirents = append(dirents, experimentalsys.Dirent{Name: t.Name(), Ino: ino, Type: t.Mode().Type()})
}
return
}
func write(w io.Writer, buf []byte) (n int, errno experimentalsys.Errno) {
if len(buf) == 0 {
return 0, 0 // less overhead on zero-length writes.
}
n, err := w.Write(buf)
return n, experimentalsys.UnwrapOSError(err)
}
func pwrite(w io.WriterAt, buf []byte, off int64) (n int, errno experimentalsys.Errno) {
if len(buf) == 0 {
return 0, 0 // less overhead on zero-length writes.
}
n, err := w.WriteAt(buf, off)
return n, experimentalsys.UnwrapOSError(err)
}
func chtimes(path string, atim, mtim int64) (errno experimentalsys.Errno) { //nolint:unused
// When both inputs are omitted, there is nothing to change.
if atim == experimentalsys.UTIME_OMIT && mtim == experimentalsys.UTIME_OMIT {
return
}
// UTIME_OMIT is expensive until progress is made in Go, as it requires a
// stat to read-back the value to re-apply.
// - https://github.com/golang/go/issues/32558.
// - https://go-review.googlesource.com/c/go/+/219638 (unmerged)
var st sys.Stat_t
if atim == experimentalsys.UTIME_OMIT || mtim == experimentalsys.UTIME_OMIT {
if st, errno = stat(path); errno != 0 {
return
}
}
var atime, mtime time.Time
if atim == experimentalsys.UTIME_OMIT {
atime = epochNanosToTime(st.Atim)
mtime = epochNanosToTime(mtim)
} else if mtim == experimentalsys.UTIME_OMIT {
atime = epochNanosToTime(atim)
mtime = epochNanosToTime(st.Mtim)
} else {
atime = epochNanosToTime(atim)
mtime = epochNanosToTime(mtim)
}
return experimentalsys.UnwrapOSError(os.Chtimes(path, atime, mtime))
}
func epochNanosToTime(epochNanos int64) time.Time { //nolint:unused
seconds := epochNanos / 1e9
nanos := epochNanos % 1e9
return time.Unix(seconds, nanos)
}

View File

@ -0,0 +1,39 @@
//go:build unix && !tinygo
package sysfs
import (
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
)
const (
nonBlockingFileReadSupported = true
nonBlockingFileWriteSupported = true
)
func rmdir(path string) sys.Errno {
err := syscall.Rmdir(path)
return sys.UnwrapOSError(err)
}
// readFd exposes syscall.Read.
func readFd(fd uintptr, buf []byte) (int, sys.Errno) {
if len(buf) == 0 {
return 0, 0 // Short-circuit 0-len reads.
}
n, err := syscall.Read(int(fd), buf)
errno := sys.UnwrapOSError(err)
return n, errno
}
// writeFd exposes syscall.Write.
func writeFd(fd uintptr, buf []byte) (int, sys.Errno) {
if len(buf) == 0 {
return 0, 0 // Short-circuit 0-len writes.
}
n, err := syscall.Write(int(fd), buf)
errno := sys.UnwrapOSError(err)
return n, errno
}

View File

@ -0,0 +1,28 @@
//go:build !(unix || windows) || tinygo
package sysfs
import (
"os"
"github.com/tetratelabs/wazero/experimental/sys"
)
const (
nonBlockingFileReadSupported = false
nonBlockingFileWriteSupported = false
)
func rmdir(path string) sys.Errno {
return sys.UnwrapOSError(os.Remove(path))
}
// readFd returns ENOSYS on unsupported platforms.
func readFd(fd uintptr, buf []byte) (int, sys.Errno) {
return -1, sys.ENOSYS
}
// writeFd returns ENOSYS on unsupported platforms.
func writeFd(fd uintptr, buf []byte) (int, sys.Errno) {
return -1, sys.ENOSYS
}

View File

@ -0,0 +1,175 @@
package sysfs
import (
"errors"
"syscall"
"unsafe"
"github.com/tetratelabs/wazero/experimental/sys"
)
const (
nonBlockingFileReadSupported = true
nonBlockingFileWriteSupported = false
_ERROR_IO_INCOMPLETE = syscall.Errno(996)
)
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
// procPeekNamedPipe is the syscall.LazyProc in kernel32 for PeekNamedPipe
var (
// procPeekNamedPipe is the syscall.LazyProc in kernel32 for PeekNamedPipe
procPeekNamedPipe = kernel32.NewProc("PeekNamedPipe")
// procGetOverlappedResult is the syscall.LazyProc in kernel32 for GetOverlappedResult
procGetOverlappedResult = kernel32.NewProc("GetOverlappedResult")
// procCreateEventW is the syscall.LazyProc in kernel32 for CreateEventW
procCreateEventW = kernel32.NewProc("CreateEventW")
)
// readFd returns ENOSYS on unsupported platforms.
//
// PeekNamedPipe: https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe
// "GetFileType can assist in determining what device type the handle refers to. A console handle presents as FILE_TYPE_CHAR."
// https://learn.microsoft.com/en-us/windows/console/console-handles
func readFd(fd uintptr, buf []byte) (int, sys.Errno) {
handle := syscall.Handle(fd)
fileType, err := syscall.GetFileType(handle)
if err != nil {
return 0, sys.UnwrapOSError(err)
}
if fileType&syscall.FILE_TYPE_CHAR == 0 {
return -1, sys.ENOSYS
}
n, errno := peekNamedPipe(handle)
if errno == syscall.ERROR_BROKEN_PIPE {
return 0, 0
}
if n == 0 {
return -1, sys.EAGAIN
}
un, err := syscall.Read(handle, buf[0:n])
return un, sys.UnwrapOSError(err)
}
func writeFd(fd uintptr, buf []byte) (int, sys.Errno) {
return -1, sys.ENOSYS
}
func readSocket(h uintptr, buf []byte) (int, sys.Errno) {
// Poll the socket to ensure that we never perform a blocking/overlapped Read.
//
// When the socket is closed by the remote peer, wsaPoll will return n=1 and
// errno=0, and syscall.ReadFile will return n=0 and errno=0 -- which indicates
// io.EOF.
if n, errno := wsaPoll(
[]pollFd{newPollFd(h, _POLLIN, 0)}, 0); !errors.Is(errno, sys.Errno(0)) {
return 0, sys.UnwrapOSError(errno)
} else if n <= 0 {
return 0, sys.EAGAIN
}
// Properly use overlapped result.
//
// If hFile was opened with FILE_FLAG_OVERLAPPED, the following conditions are in effect:
// - The lpOverlapped parameter must point to a valid and unique OVERLAPPED structure,
// otherwise the function can incorrectly report that the read operation is complete.
// - The lpNumberOfBytesRead parameter should be set to NULL. Use the GetOverlappedResult
// function to get the actual number of bytes read. If the hFile parameter is associated
// with an I/O completion port, you can also get the number of bytes read by calling the
// GetQueuedCompletionStatus function.
//
// We are currently skipping checking if hFile was opened with FILE_FLAG_OVERLAPPED but using
// both lpOverlapped and lpNumberOfBytesRead.
var overlapped syscall.Overlapped
// Create an event to wait on.
if hEvent, err := createEventW(nil, true, false, nil); err != 0 {
return 0, sys.UnwrapOSError(err)
} else {
overlapped.HEvent = syscall.Handle(hEvent)
}
var done uint32
errno := syscall.ReadFile(syscall.Handle(h), buf, &done, &overlapped)
if errors.Is(errno, syscall.ERROR_IO_PENDING) {
errno = syscall.CancelIo(syscall.Handle(h))
if errno != nil {
return 0, sys.UnwrapOSError(errno) // This is a fatal error. CancelIo failed.
}
done, errno = getOverlappedResult(syscall.Handle(h), &overlapped, true) // wait for I/O to complete(cancel or finish). Overwrite done and errno.
if errors.Is(errno, syscall.ERROR_OPERATION_ABORTED) {
return int(done), sys.EAGAIN // This is one of the expected behavior, I/O was cancelled(completed) before finished.
}
}
return int(done), sys.UnwrapOSError(errno)
}
func writeSocket(fd uintptr, buf []byte) (int, sys.Errno) {
var done uint32
var overlapped syscall.Overlapped
errno := syscall.WriteFile(syscall.Handle(fd), buf, &done, &overlapped)
if errors.Is(errno, syscall.ERROR_IO_PENDING) {
errno = syscall.EAGAIN
}
return int(done), sys.UnwrapOSError(errno)
}
// peekNamedPipe partially exposes PeekNamedPipe from the Win32 API
// see https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe
func peekNamedPipe(handle syscall.Handle) (uint32, syscall.Errno) {
var totalBytesAvail uint32
totalBytesPtr := unsafe.Pointer(&totalBytesAvail)
_, _, errno := syscall.SyscallN(
procPeekNamedPipe.Addr(),
uintptr(handle), // [in] HANDLE hNamedPipe,
0, // [out, optional] LPVOID lpBuffer,
0, // [in] DWORD nBufferSize,
0, // [out, optional] LPDWORD lpBytesRead
uintptr(totalBytesPtr), // [out, optional] LPDWORD lpTotalBytesAvail,
0) // [out, optional] LPDWORD lpBytesLeftThisMessage
return totalBytesAvail, errno
}
func rmdir(path string) sys.Errno {
err := syscall.Rmdir(path)
return sys.UnwrapOSError(err)
}
func getOverlappedResult(handle syscall.Handle, overlapped *syscall.Overlapped, wait bool) (uint32, syscall.Errno) {
var totalBytesAvail uint32
var bwait uintptr
if wait {
bwait = 0xFFFFFFFF
}
totalBytesPtr := unsafe.Pointer(&totalBytesAvail)
_, _, errno := syscall.SyscallN(
procGetOverlappedResult.Addr(),
uintptr(handle), // [in] HANDLE hFile,
uintptr(unsafe.Pointer(overlapped)), // [in] LPOVERLAPPED lpOverlapped,
uintptr(totalBytesPtr), // [out] LPDWORD lpNumberOfBytesTransferred,
bwait) // [in] BOOL bWait
return totalBytesAvail, errno
}
func createEventW(lpEventAttributes *syscall.SecurityAttributes, bManualReset bool, bInitialState bool, lpName *uint16) (uintptr, syscall.Errno) {
var manualReset uintptr
var initialState uintptr
if bManualReset {
manualReset = 1
}
if bInitialState {
initialState = 1
}
handle, _, errno := syscall.SyscallN(
procCreateEventW.Addr(),
uintptr(unsafe.Pointer(lpEventAttributes)), // [in] LPSECURITY_ATTRIBUTES lpEventAttributes,
manualReset, // [in] BOOL bManualReset,
initialState, // [in] BOOL bInitialState,
uintptr(unsafe.Pointer(lpName)), // [in, opt]LPCWSTR lpName,
)
return handle, errno
}

View File

@ -0,0 +1,37 @@
//go:build (linux || darwin) && !tinygo
package sysfs
import (
"syscall"
"unsafe"
"github.com/tetratelabs/wazero/experimental/sys"
)
func timesToPtr(times *[2]syscall.Timespec) unsafe.Pointer { //nolint:unused
if times != nil {
return unsafe.Pointer(&times[0])
}
return unsafe.Pointer(nil)
}
func timesToTimespecs(atim int64, mtim int64) (times *[2]syscall.Timespec) {
// When both inputs are omitted, there is nothing to change.
if atim == sys.UTIME_OMIT && mtim == sys.UTIME_OMIT {
return
}
times = &[2]syscall.Timespec{}
if atim == sys.UTIME_OMIT {
times[0] = syscall.Timespec{Nsec: _UTIME_OMIT}
times[1] = syscall.NsecToTimespec(mtim)
} else if mtim == sys.UTIME_OMIT {
times[0] = syscall.NsecToTimespec(atim)
times[1] = syscall.Timespec{Nsec: _UTIME_OMIT}
} else {
times[0] = syscall.NsecToTimespec(atim)
times[1] = syscall.NsecToTimespec(mtim)
}
return
}

View File

@ -0,0 +1,51 @@
package sysfs
import (
"syscall"
_ "unsafe"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
)
const (
_AT_FDCWD = -0x2
_AT_SYMLINK_NOFOLLOW = 0x0020
_UTIME_OMIT = -2
)
//go:noescape
//go:linkname utimensat syscall.utimensat
func utimensat(dirfd int, path string, times *[2]syscall.Timespec, flags int) error
func utimens(path string, atim, mtim int64) experimentalsys.Errno {
times := timesToTimespecs(atim, mtim)
if times == nil {
return 0
}
var flags int
return experimentalsys.UnwrapOSError(utimensat(_AT_FDCWD, path, times, flags))
}
func futimens(fd uintptr, atim, mtim int64) experimentalsys.Errno {
times := timesToTimespecs(atim, mtim)
if times == nil {
return 0
}
_p0 := timesToPtr(times)
// Warning: futimens only exists since High Sierra (10.13).
_, _, e1 := syscall_syscall6(libc_futimens_trampoline_addr, fd, uintptr(_p0), 0, 0, 0, 0)
return experimentalsys.UnwrapOSError(e1)
}
// libc_futimens_trampoline_addr is the address of the
// `libc_futimens_trampoline` symbol, defined in `futimens_darwin.s`.
//
// We use this to invoke the syscall through syscall_syscall6 imported below.
var libc_futimens_trampoline_addr uintptr
// Imports the futimens symbol from libc as `libc_futimens`.
//
// Note: CGO mechanisms are used in darwin regardless of the CGO_ENABLED value
// or the "cgo" build flag. See /RATIONALE.md for why.
//go:cgo_import_dynamic libc_futimens futimens "/usr/lib/libSystem.B.dylib"

View File

@ -0,0 +1,8 @@
// lifted from golang.org/x/sys unix
#include "textflag.h"
TEXT libc_futimens_trampoline<>(SB), NOSPLIT, $0-0
JMP libc_futimens(SB)
GLOBL ·libc_futimens_trampoline_addr(SB), RODATA, $8
DATA ·libc_futimens_trampoline_addr(SB)/8, $libc_futimens_trampoline<>(SB)

View File

@ -0,0 +1,49 @@
//go:build !tinygo
package sysfs
import (
"syscall"
"unsafe"
_ "unsafe"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
)
const (
_AT_FDCWD = -0x64
_UTIME_OMIT = (1 << 30) - 2
)
func utimens(path string, atim, mtim int64) experimentalsys.Errno {
times := timesToTimespecs(atim, mtim)
if times == nil {
return 0
}
var flags int
var _p0 *byte
_p0, err := syscall.BytePtrFromString(path)
if err == nil {
err = utimensat(_AT_FDCWD, uintptr(unsafe.Pointer(_p0)), times, flags)
}
return experimentalsys.UnwrapOSError(err)
}
// On linux, implement futimens via utimensat with the NUL path.
func futimens(fd uintptr, atim, mtim int64) experimentalsys.Errno {
times := timesToTimespecs(atim, mtim)
if times == nil {
return 0
}
return experimentalsys.UnwrapOSError(utimensat(int(fd), 0 /* NUL */, times, 0))
}
// utimensat is like syscall.utimensat special-cased to accept a NUL string for the path value.
func utimensat(dirfd int, strPtr uintptr, times *[2]syscall.Timespec, flags int) (err error) {
_, _, e1 := syscall.Syscall6(syscall.SYS_UTIMENSAT, uintptr(dirfd), strPtr, uintptr(unsafe.Pointer(times)), uintptr(flags), 0, 0)
if e1 != 0 {
err = e1
}
return
}

View File

@ -0,0 +1,18 @@
//go:build (!windows && !linux && !darwin) || tinygo
package sysfs
import (
"github.com/tetratelabs/wazero/experimental/sys"
)
func utimens(path string, atim, mtim int64) sys.Errno {
return chtimes(path, atim, mtim)
}
func futimens(fd uintptr, atim, mtim int64) error {
// Go exports syscall.Futimes, which is microsecond granularity, and
// WASI tests expect nanosecond. We don't yet have a way to invoke the
// futimens syscall portably.
return sys.ENOSYS
}

View File

@ -0,0 +1,42 @@
package sysfs
import (
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
)
func utimens(path string, atim, mtim int64) sys.Errno {
return chtimes(path, atim, mtim)
}
func futimens(fd uintptr, atim, mtim int64) error {
// Per docs, zero isn't a valid timestamp as it cannot be differentiated
// from nil. In both cases, it is a marker like sys.UTIME_OMIT.
// See https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfiletime
a, w := timespecToFiletime(atim, mtim)
if a == nil && w == nil {
return nil // both omitted, so nothing to change
}
// Attempt to get the stat by handle, which works for normal files
h := syscall.Handle(fd)
// Note: This returns ERROR_ACCESS_DENIED when the input is a directory.
return syscall.SetFileTime(h, nil, a, w)
}
func timespecToFiletime(atim, mtim int64) (a, w *syscall.Filetime) {
a = timespecToFileTime(atim)
w = timespecToFileTime(mtim)
return
}
func timespecToFileTime(tim int64) *syscall.Filetime {
if tim == sys.UTIME_OMIT {
return nil
}
ft := syscall.NsecToFiletime(tim)
return &ft
}

View File

@ -0,0 +1,22 @@
//go:build !windows && !plan9 && !tinygo
package sysfs
import (
"io/fs"
"syscall"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/sys"
)
func inoFromFileInfo(_ string, info fs.FileInfo) (sys.Inode, experimentalsys.Errno) {
switch v := info.Sys().(type) {
case *sys.Stat_t:
return v.Ino, 0
case *syscall.Stat_t:
return v.Ino, 0
default:
return 0, 0
}
}

View File

@ -0,0 +1,15 @@
package sysfs
import (
"io/fs"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/sys"
)
func inoFromFileInfo(_ string, info fs.FileInfo) (sys.Inode, experimentalsys.Errno) {
if v, ok := info.Sys().(*sys.Stat_t); ok {
return v.Ino, 0
}
return 0, 0
}

View File

@ -0,0 +1,14 @@
//go:build tinygo
package sysfs
import (
"io/fs"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/sys"
)
func inoFromFileInfo(_ string, info fs.FileInfo) (sys.Inode, experimentalsys.Errno) {
return 0, experimentalsys.ENOTSUP
}

View File

@ -0,0 +1,28 @@
package sysfs
import (
"io/fs"
"path"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/sys"
)
// inoFromFileInfo uses stat to get the inode information of the file.
func inoFromFileInfo(dirPath string, info fs.FileInfo) (ino sys.Inode, errno experimentalsys.Errno) {
if v, ok := info.Sys().(*sys.Stat_t); ok {
return v.Ino, 0
}
if dirPath == "" {
// This is a FS.File backed implementation which doesn't have access to
// the original file path.
return
}
// Ino is no not in Win32FileAttributeData
inoPath := path.Clean(path.Join(dirPath, info.Name()))
var st sys.Stat_t
if st, errno = lstat(inoPath); errno == 0 {
ino = st.Ino
}
return
}

View File

@ -0,0 +1,17 @@
//go:build !windows && !plan9 && !tinygo
package sysfs
import (
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
)
func setNonblock(fd uintptr, enable bool) sys.Errno {
return sys.UnwrapOSError(syscall.SetNonblock(int(fd), enable))
}
func isNonblock(f *osFile) bool {
return f.flag&sys.O_NONBLOCK == sys.O_NONBLOCK
}

View File

@ -0,0 +1,13 @@
//go:build plan9 || tinygo
package sysfs
import "github.com/tetratelabs/wazero/experimental/sys"
func setNonblock(fd uintptr, enable bool) sys.Errno {
return sys.ENOSYS
}
func isNonblock(f *osFile) bool {
return false
}

View File

@ -0,0 +1,23 @@
package sysfs
import (
"io/fs"
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
)
func setNonblock(fd uintptr, enable bool) sys.Errno {
// We invoke the syscall, but this is currently no-op.
return sys.UnwrapOSError(syscall.SetNonblock(syscall.Handle(fd), enable))
}
func isNonblock(f *osFile) bool {
// On Windows, we support non-blocking reads only on named pipes.
isValid := false
st, errno := f.Stat()
if errno == 0 {
isValid = st.Mode&fs.ModeNamedPipe != 0
}
return isValid && f.flag&sys.O_NONBLOCK == sys.O_NONBLOCK
}

View File

@ -0,0 +1,38 @@
package sysfs
import (
"os"
"github.com/tetratelabs/wazero/experimental/sys"
)
// toOsOpenFlag converts the input to the flag parameter of os.OpenFile
func toOsOpenFlag(oflag sys.Oflag) (flag int) {
// First flags are exclusive
switch oflag & (sys.O_RDONLY | sys.O_RDWR | sys.O_WRONLY) {
case sys.O_RDONLY:
flag |= os.O_RDONLY
case sys.O_RDWR:
flag |= os.O_RDWR
case sys.O_WRONLY:
flag |= os.O_WRONLY
}
// Run down the flags defined in the os package
if oflag&sys.O_APPEND != 0 {
flag |= os.O_APPEND
}
if oflag&sys.O_CREAT != 0 {
flag |= os.O_CREATE
}
if oflag&sys.O_EXCL != 0 {
flag |= os.O_EXCL
}
if oflag&sys.O_SYNC != 0 {
flag |= os.O_SYNC
}
if oflag&sys.O_TRUNC != 0 {
flag |= os.O_TRUNC
}
return withSyscallOflag(oflag, flag)
}

View File

@ -0,0 +1,26 @@
package sysfs
import (
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
)
const supportedSyscallOflag = sys.O_DIRECTORY | sys.O_DSYNC | sys.O_NOFOLLOW | sys.O_NONBLOCK
func withSyscallOflag(oflag sys.Oflag, flag int) int {
if oflag&sys.O_DIRECTORY != 0 {
flag |= syscall.O_DIRECTORY
}
if oflag&sys.O_DSYNC != 0 {
flag |= syscall.O_DSYNC
}
if oflag&sys.O_NOFOLLOW != 0 {
flag |= syscall.O_NOFOLLOW
}
if oflag&sys.O_NONBLOCK != 0 {
flag |= syscall.O_NONBLOCK
}
// syscall.O_RSYNC not defined on darwin
return flag
}

View File

@ -0,0 +1,24 @@
package sysfs
import (
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
)
const supportedSyscallOflag = sys.O_DIRECTORY | sys.O_NOFOLLOW | sys.O_NONBLOCK
func withSyscallOflag(oflag sys.Oflag, flag int) int {
if oflag&sys.O_DIRECTORY != 0 {
flag |= syscall.O_DIRECTORY
}
// syscall.O_DSYNC not defined on darwin
if oflag&sys.O_NOFOLLOW != 0 {
flag |= syscall.O_NOFOLLOW
}
if oflag&sys.O_NONBLOCK != 0 {
flag |= syscall.O_NONBLOCK
}
// syscall.O_RSYNC not defined on darwin
return flag
}

View File

@ -0,0 +1,30 @@
//go:build !tinygo
package sysfs
import (
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
)
const supportedSyscallOflag = sys.O_DIRECTORY | sys.O_DSYNC | sys.O_NOFOLLOW | sys.O_NONBLOCK | sys.O_RSYNC
func withSyscallOflag(oflag sys.Oflag, flag int) int {
if oflag&sys.O_DIRECTORY != 0 {
flag |= syscall.O_DIRECTORY
}
if oflag&sys.O_DSYNC != 0 {
flag |= syscall.O_DSYNC
}
if oflag&sys.O_NOFOLLOW != 0 {
flag |= syscall.O_NOFOLLOW
}
if oflag&sys.O_NONBLOCK != 0 {
flag |= syscall.O_NONBLOCK
}
if oflag&sys.O_RSYNC != 0 {
flag |= syscall.O_RSYNC
}
return flag
}

View File

@ -0,0 +1,20 @@
//go:build !windows && !tinygo
package sysfs
import (
"io/fs"
"os"
"github.com/tetratelabs/wazero/experimental/sys"
)
// openFile is like os.OpenFile except it accepts a sys.Oflag and returns
// sys.Errno. A zero sys.Errno is success.
func openFile(path string, oflag sys.Oflag, perm fs.FileMode) (*os.File, sys.Errno) {
f, err := os.OpenFile(path, toOsOpenFlag(oflag), perm)
// Note: This does not return a sys.File because sys.FS that returns
// one may want to hide the real OS path. For example, this is needed for
// pre-opens.
return f, sys.UnwrapOSError(err)
}

View File

@ -0,0 +1,31 @@
//go:build illumos || solaris
package sysfs
import (
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
)
const supportedSyscallOflag = sys.O_DIRECTORY | sys.O_DSYNC | sys.O_NOFOLLOW | sys.O_NONBLOCK | sys.O_RSYNC
func withSyscallOflag(oflag sys.Oflag, flag int) int {
if oflag&sys.O_DIRECTORY != 0 {
// See https://github.com/illumos/illumos-gate/blob/edd580643f2cf1434e252cd7779e83182ea84945/usr/src/uts/common/sys/fcntl.h#L90
flag |= 0x1000000
}
if oflag&sys.O_DSYNC != 0 {
flag |= syscall.O_DSYNC
}
if oflag&sys.O_NOFOLLOW != 0 {
flag |= syscall.O_NOFOLLOW
}
if oflag&sys.O_NONBLOCK != 0 {
flag |= syscall.O_NONBLOCK
}
if oflag&sys.O_RSYNC != 0 {
flag |= syscall.O_RSYNC
}
return flag
}

View File

@ -0,0 +1,25 @@
//go:build tinygo
package sysfs
import (
"io/fs"
"os"
"github.com/tetratelabs/wazero/experimental/sys"
)
const supportedSyscallOflag = sys.Oflag(0)
func withSyscallOflag(oflag sys.Oflag, flag int) int {
// O_DIRECTORY not defined
// O_DSYNC not defined
// O_NOFOLLOW not defined
// O_NONBLOCK not defined
// O_RSYNC not defined
return flag
}
func openFile(path string, oflag sys.Oflag, perm fs.FileMode) (*os.File, sys.Errno) {
return nil, sys.ENOSYS
}

View File

@ -0,0 +1,18 @@
//go:build !darwin && !linux && !windows && !illumos && !solaris && !freebsd
package sysfs
import (
"github.com/tetratelabs/wazero/experimental/sys"
)
const supportedSyscallOflag = sys.Oflag(0)
func withSyscallOflag(oflag sys.Oflag, flag int) int {
// O_DIRECTORY not defined
// O_DSYNC not defined
// O_NOFOLLOW not defined
// O_NONBLOCK not defined
// O_RSYNC not defined
return flag
}

View File

@ -0,0 +1,161 @@
package sysfs
import (
"io/fs"
"os"
"strings"
"syscall"
"unsafe"
"github.com/tetratelabs/wazero/experimental/sys"
)
func openFile(path string, oflag sys.Oflag, perm fs.FileMode) (*os.File, sys.Errno) {
isDir := oflag&sys.O_DIRECTORY > 0
flag := toOsOpenFlag(oflag)
// TODO: document why we are opening twice
fd, err := open(path, flag|syscall.O_CLOEXEC, uint32(perm))
if err == nil {
return os.NewFile(uintptr(fd), path), 0
}
// TODO: Set FILE_SHARE_DELETE for directory as well.
f, err := os.OpenFile(path, flag, perm)
errno := sys.UnwrapOSError(err)
if errno == 0 {
return f, 0
}
switch errno {
case sys.EINVAL:
// WASI expects ENOTDIR for a file path with a trailing slash.
if strings.HasSuffix(path, "/") {
errno = sys.ENOTDIR
}
// To match expectations of WASI, e.g. TinyGo TestStatBadDir, return
// ENOENT, not ENOTDIR.
case sys.ENOTDIR:
errno = sys.ENOENT
case sys.ENOENT:
if isSymlink(path) {
// Either symlink or hard link not found. We change the returned
// errno depending on if it is symlink or not to have consistent
// behavior across OSes.
if isDir {
// Dangling symlink dir must raise ENOTDIR.
errno = sys.ENOTDIR
} else {
errno = sys.ELOOP
}
}
}
return f, errno
}
const supportedSyscallOflag = sys.O_NONBLOCK
// Map to synthetic values here https://github.com/golang/go/blob/go1.20/src/syscall/types_windows.go#L34-L48
func withSyscallOflag(oflag sys.Oflag, flag int) int {
// O_DIRECTORY not defined in windows
// O_DSYNC not defined in windows
// O_NOFOLLOW not defined in windows
if oflag&sys.O_NONBLOCK != 0 {
flag |= syscall.O_NONBLOCK
}
// O_RSYNC not defined in windows
return flag
}
func isSymlink(path string) bool {
if st, e := os.Lstat(path); e == nil && st.Mode()&os.ModeSymlink != 0 {
return true
}
return false
}
// # Differences from syscall.Open
//
// This code is based on syscall.Open from the below link with some differences
// https://github.com/golang/go/blame/go1.20/src/syscall/syscall_windows.go#L308-L379
//
// - syscall.O_CREAT doesn't imply syscall.GENERIC_WRITE as that breaks
// flag expectations in wasi.
// - add support for setting FILE_SHARE_DELETE.
func open(path string, mode int, perm uint32) (fd syscall.Handle, err error) {
if len(path) == 0 {
return syscall.InvalidHandle, syscall.ERROR_FILE_NOT_FOUND
}
pathp, err := syscall.UTF16PtrFromString(path)
if err != nil {
return syscall.InvalidHandle, err
}
var access uint32
switch mode & (syscall.O_RDONLY | syscall.O_WRONLY | syscall.O_RDWR) {
case syscall.O_RDONLY:
access = syscall.GENERIC_READ
case syscall.O_WRONLY:
access = syscall.GENERIC_WRITE
case syscall.O_RDWR:
access = syscall.GENERIC_READ | syscall.GENERIC_WRITE
}
if mode&syscall.O_APPEND != 0 {
access &^= syscall.GENERIC_WRITE
access |= syscall.FILE_APPEND_DATA
}
sharemode := uint32(syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE | syscall.FILE_SHARE_DELETE)
var sa *syscall.SecurityAttributes
if mode&syscall.O_CLOEXEC == 0 {
var _sa syscall.SecurityAttributes
_sa.Length = uint32(unsafe.Sizeof(sa))
_sa.InheritHandle = 1
sa = &_sa
}
var createmode uint32
switch {
case mode&(syscall.O_CREAT|syscall.O_EXCL) == (syscall.O_CREAT | syscall.O_EXCL):
createmode = syscall.CREATE_NEW
case mode&(syscall.O_CREAT|syscall.O_TRUNC) == (syscall.O_CREAT | syscall.O_TRUNC):
createmode = syscall.CREATE_ALWAYS
case mode&syscall.O_CREAT == syscall.O_CREAT:
createmode = syscall.OPEN_ALWAYS
case mode&syscall.O_TRUNC == syscall.O_TRUNC:
createmode = syscall.TRUNCATE_EXISTING
default:
createmode = syscall.OPEN_EXISTING
}
var attrs uint32 = syscall.FILE_ATTRIBUTE_NORMAL
if perm&syscall.S_IWRITE == 0 {
attrs = syscall.FILE_ATTRIBUTE_READONLY
if createmode == syscall.CREATE_ALWAYS {
// We have been asked to create a read-only file.
// If the file already exists, the semantics of
// the Unix open system call is to preserve the
// existing permissions. If we pass CREATE_ALWAYS
// and FILE_ATTRIBUTE_READONLY to CreateFile,
// and the file already exists, CreateFile will
// change the file permissions.
// Avoid that to preserve the Unix semantics.
h, e := syscall.CreateFile(pathp, access, sharemode, sa, syscall.TRUNCATE_EXISTING, syscall.FILE_ATTRIBUTE_NORMAL, 0)
switch e {
case syscall.ERROR_FILE_NOT_FOUND, syscall.ERROR_PATH_NOT_FOUND:
// File does not exist. These are the same
// errors as Errno.Is checks for ErrNotExist.
// Carry on to create the file.
default:
// Success or some different error.
return h, e
}
}
}
// This shouldn't be included before 1.20 to have consistent behavior.
// https://github.com/golang/go/commit/0f0aa5d8a6a0253627d58b3aa083b24a1091933f
if createmode == syscall.OPEN_EXISTING && access == syscall.GENERIC_READ {
// Necessary for opening directory handles.
attrs |= syscall.FILE_FLAG_BACKUP_SEMANTICS
}
h, e := syscall.CreateFile(pathp, access, sharemode, sa, createmode, attrs, 0)
return h, e
}

View File

@ -0,0 +1,295 @@
package sysfs
import (
"io"
"io/fs"
"os"
"runtime"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/sys"
)
func newOsFile(path string, flag experimentalsys.Oflag, perm fs.FileMode, f *os.File) fsapi.File {
// Windows cannot read files written to a directory after it was opened.
// This was noticed in #1087 in zig tests. Use a flag instead of a
// different type.
reopenDir := runtime.GOOS == "windows"
return &osFile{path: path, flag: flag, perm: perm, reopenDir: reopenDir, file: f, fd: f.Fd()}
}
// osFile is a file opened with this package, and uses os.File or syscalls to
// implement api.File.
type osFile struct {
path string
flag experimentalsys.Oflag
perm fs.FileMode
file *os.File
fd uintptr
// reopenDir is true if reopen should be called before Readdir. This flag
// is deferred until Readdir to prevent redundant rewinds. This could
// happen if Seek(0) was called twice, or if in Windows, Seek(0) was called
// before Readdir.
reopenDir bool
// closed is true when closed was called. This ensures proper sys.EBADF
closed bool
// cachedStat includes fields that won't change while a file is open.
cachedSt *cachedStat
}
// cachedStat returns the cacheable parts of sys.Stat_t or an error if they
// couldn't be retrieved.
func (f *osFile) cachedStat() (dev uint64, ino sys.Inode, isDir bool, errno experimentalsys.Errno) {
if f.cachedSt == nil {
if _, errno = f.Stat(); errno != 0 {
return
}
}
return f.cachedSt.dev, f.cachedSt.ino, f.cachedSt.isDir, 0
}
// Dev implements the same method as documented on sys.File
func (f *osFile) Dev() (uint64, experimentalsys.Errno) {
dev, _, _, errno := f.cachedStat()
return dev, errno
}
// Ino implements the same method as documented on sys.File
func (f *osFile) Ino() (sys.Inode, experimentalsys.Errno) {
_, ino, _, errno := f.cachedStat()
return ino, errno
}
// IsDir implements the same method as documented on sys.File
func (f *osFile) IsDir() (bool, experimentalsys.Errno) {
_, _, isDir, errno := f.cachedStat()
return isDir, errno
}
// IsAppend implements File.IsAppend
func (f *osFile) IsAppend() bool {
return f.flag&experimentalsys.O_APPEND == experimentalsys.O_APPEND
}
// SetAppend implements the same method as documented on sys.File
func (f *osFile) SetAppend(enable bool) (errno experimentalsys.Errno) {
if enable {
f.flag |= experimentalsys.O_APPEND
} else {
f.flag &= ^experimentalsys.O_APPEND
}
// Clear any create or trunc flag, as we are re-opening, not re-creating.
f.flag &= ^(experimentalsys.O_CREAT | experimentalsys.O_TRUNC)
// appendMode (bool) cannot be changed later, so we have to re-open the
// file. https://github.com/golang/go/blob/go1.20/src/os/file_unix.go#L60
return fileError(f, f.closed, f.reopen())
}
// compile-time check to ensure osFile.reopen implements reopenFile.
var _ reopenFile = (*osFile)(nil).reopen
func (f *osFile) reopen() (errno experimentalsys.Errno) {
// Clear any create flag, as we are re-opening, not re-creating.
f.flag &= ^experimentalsys.O_CREAT
var (
isDir bool
offset int64
err error
)
isDir, errno = f.IsDir()
if errno != 0 {
return errno
}
if !isDir {
offset, err = f.file.Seek(0, io.SeekCurrent)
if err != nil {
return experimentalsys.UnwrapOSError(err)
}
}
_ = f.close()
f.file, errno = OpenFile(f.path, f.flag, f.perm)
if errno != 0 {
return errno
}
if !isDir {
_, err = f.file.Seek(offset, io.SeekStart)
if err != nil {
return experimentalsys.UnwrapOSError(err)
}
}
return 0
}
// IsNonblock implements the same method as documented on fsapi.File
func (f *osFile) IsNonblock() bool {
return isNonblock(f)
}
// SetNonblock implements the same method as documented on fsapi.File
func (f *osFile) SetNonblock(enable bool) (errno experimentalsys.Errno) {
if enable {
f.flag |= experimentalsys.O_NONBLOCK
} else {
f.flag &= ^experimentalsys.O_NONBLOCK
}
if errno = setNonblock(f.fd, enable); errno != 0 {
return fileError(f, f.closed, errno)
}
return 0
}
// Stat implements the same method as documented on sys.File
func (f *osFile) Stat() (sys.Stat_t, experimentalsys.Errno) {
if f.closed {
return sys.Stat_t{}, experimentalsys.EBADF
}
st, errno := statFile(f.file)
switch errno {
case 0:
f.cachedSt = &cachedStat{dev: st.Dev, ino: st.Ino, isDir: st.Mode&fs.ModeDir == fs.ModeDir}
case experimentalsys.EIO:
errno = experimentalsys.EBADF
}
return st, errno
}
// Read implements the same method as documented on sys.File
func (f *osFile) Read(buf []byte) (n int, errno experimentalsys.Errno) {
if len(buf) == 0 {
return 0, 0 // Short-circuit 0-len reads.
}
if nonBlockingFileReadSupported && f.IsNonblock() {
n, errno = readFd(f.fd, buf)
} else {
n, errno = read(f.file, buf)
}
if errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
}
return
}
// Pread implements the same method as documented on sys.File
func (f *osFile) Pread(buf []byte, off int64) (n int, errno experimentalsys.Errno) {
if n, errno = pread(f.file, buf, off); errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
}
return
}
// Seek implements the same method as documented on sys.File
func (f *osFile) Seek(offset int64, whence int) (newOffset int64, errno experimentalsys.Errno) {
if newOffset, errno = seek(f.file, offset, whence); errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
// If the error was trying to rewind a directory, re-open it. Notably,
// seeking to zero on a directory doesn't work on Windows with Go 1.19.
if errno == experimentalsys.EISDIR && offset == 0 && whence == io.SeekStart {
errno = 0
f.reopenDir = true
}
}
return
}
// Poll implements the same method as documented on fsapi.File
func (f *osFile) Poll(flag fsapi.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
return poll(f.fd, flag, timeoutMillis)
}
// Readdir implements File.Readdir. Notably, this uses "Readdir", not
// "ReadDir", from os.File.
func (f *osFile) Readdir(n int) (dirents []experimentalsys.Dirent, errno experimentalsys.Errno) {
if f.reopenDir { // re-open the directory if needed.
f.reopenDir = false
if errno = adjustReaddirErr(f, f.closed, f.reopen()); errno != 0 {
return
}
}
if dirents, errno = readdir(f.file, f.path, n); errno != 0 {
errno = adjustReaddirErr(f, f.closed, errno)
}
return
}
// Write implements the same method as documented on sys.File
func (f *osFile) Write(buf []byte) (n int, errno experimentalsys.Errno) {
if len(buf) == 0 {
return 0, 0 // Short-circuit 0-len writes.
}
if nonBlockingFileWriteSupported && f.IsNonblock() {
n, errno = writeFd(f.fd, buf)
} else if n, errno = write(f.file, buf); errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
}
return
}
// Pwrite implements the same method as documented on sys.File
func (f *osFile) Pwrite(buf []byte, off int64) (n int, errno experimentalsys.Errno) {
if n, errno = pwrite(f.file, buf, off); errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
}
return
}
// Truncate implements the same method as documented on sys.File
func (f *osFile) Truncate(size int64) (errno experimentalsys.Errno) {
if errno = experimentalsys.UnwrapOSError(f.file.Truncate(size)); errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
}
return
}
// Sync implements the same method as documented on sys.File
func (f *osFile) Sync() experimentalsys.Errno {
return fsync(f.file)
}
// Datasync implements the same method as documented on sys.File
func (f *osFile) Datasync() experimentalsys.Errno {
return datasync(f.file)
}
// Utimens implements the same method as documented on sys.File
func (f *osFile) Utimens(atim, mtim int64) experimentalsys.Errno {
if f.closed {
return experimentalsys.EBADF
}
err := futimens(f.fd, atim, mtim)
return experimentalsys.UnwrapOSError(err)
}
// Close implements the same method as documented on sys.File
func (f *osFile) Close() experimentalsys.Errno {
if f.closed {
return 0
}
f.closed = true
return f.close()
}
func (f *osFile) close() experimentalsys.Errno {
return experimentalsys.UnwrapOSError(f.file.Close())
}

View File

@ -0,0 +1,18 @@
//go:build windows || (linux && !tinygo) || darwin
package sysfs
import (
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
)
// poll implements `Poll` as documented on sys.File via a file descriptor.
func poll(fd uintptr, flag fsapi.Pflag, timeoutMillis int32) (ready bool, errno sys.Errno) {
if flag != fsapi.POLLIN {
return false, sys.ENOTSUP
}
fds := []pollFd{newPollFd(fd, _POLLIN, 0)}
count, errno := _poll(fds, timeoutMillis)
return count > 0, errno
}

View File

@ -0,0 +1,55 @@
package sysfs
import (
"unsafe"
"github.com/tetratelabs/wazero/experimental/sys"
)
// pollFd is the struct to query for file descriptor events using poll.
type pollFd struct {
// fd is the file descriptor.
fd int32
// events is a bitmap containing the requested events.
events int16
// revents is a bitmap containing the returned events.
revents int16
}
// newPollFd is a constructor for pollFd that abstracts the platform-specific type of file descriptors.
func newPollFd(fd uintptr, events, revents int16) pollFd {
return pollFd{fd: int32(fd), events: events, revents: revents}
}
// _POLLIN subscribes a notification when any readable data is available.
const _POLLIN = 0x0001
// _poll implements poll on Darwin via the corresponding libc function.
func _poll(fds []pollFd, timeoutMillis int32) (n int, errno sys.Errno) {
var fdptr *pollFd
nfds := len(fds)
if nfds > 0 {
fdptr = &fds[0]
}
n1, _, err := syscall_syscall6(
libc_poll_trampoline_addr,
uintptr(unsafe.Pointer(fdptr)),
uintptr(nfds),
uintptr(int(timeoutMillis)),
uintptr(unsafe.Pointer(nil)),
uintptr(unsafe.Pointer(nil)),
uintptr(unsafe.Pointer(nil)))
return int(n1), sys.UnwrapOSError(err)
}
// libc_poll_trampoline_addr is the address of the
// `libc_poll_trampoline` symbol, defined in `poll_darwin.s`.
//
// We use this to invoke the syscall through syscall_syscall6 imported below.
var libc_poll_trampoline_addr uintptr
// Imports the select symbol from libc as `libc_poll`.
//
// Note: CGO mechanisms are used in darwin regardless of the CGO_ENABLED value
// or the "cgo" build flag. See /RATIONALE.md for why.
//go:cgo_import_dynamic libc_poll poll "/usr/lib/libSystem.B.dylib"

View File

@ -0,0 +1,8 @@
// lifted from golang.org/x/sys unix
#include "textflag.h"
TEXT libc_poll_trampoline<>(SB), NOSPLIT, $0-0
JMP libc_poll(SB)
GLOBL ·libc_poll_trampoline_addr(SB), RODATA, $8
DATA ·libc_poll_trampoline_addr(SB)/8, $libc_poll_trampoline<>(SB)

View File

@ -0,0 +1,59 @@
//go:build !tinygo
package sysfs
import (
"syscall"
"time"
"unsafe"
"github.com/tetratelabs/wazero/experimental/sys"
)
// pollFd is the struct to query for file descriptor events using poll.
type pollFd struct {
// fd is the file descriptor.
fd int32
// events is a bitmap containing the requested events.
events int16
// revents is a bitmap containing the returned events.
revents int16
}
// newPollFd is a constructor for pollFd that abstracts the platform-specific type of file descriptors.
func newPollFd(fd uintptr, events, revents int16) pollFd {
return pollFd{fd: int32(fd), events: events, revents: revents}
}
// _POLLIN subscribes a notification when any readable data is available.
const _POLLIN = 0x0001
// _poll implements poll on Linux via ppoll.
func _poll(fds []pollFd, timeoutMillis int32) (n int, errno sys.Errno) {
var ts syscall.Timespec
if timeoutMillis >= 0 {
ts = syscall.NsecToTimespec(int64(time.Duration(timeoutMillis) * time.Millisecond))
}
return ppoll(fds, &ts)
}
// ppoll is a poll variant that allows to subscribe to a mask of signals.
// However, we do not need such mask, so the corresponding argument is always nil.
func ppoll(fds []pollFd, timespec *syscall.Timespec) (n int, err sys.Errno) {
var fdptr *pollFd
nfd := len(fds)
if nfd != 0 {
fdptr = &fds[0]
}
n1, _, errno := syscall.Syscall6(
uintptr(syscall.SYS_PPOLL),
uintptr(unsafe.Pointer(fdptr)),
uintptr(nfd),
uintptr(unsafe.Pointer(timespec)),
uintptr(unsafe.Pointer(nil)), // sigmask is currently always ignored
uintptr(unsafe.Pointer(nil)),
uintptr(unsafe.Pointer(nil)))
return int(n1), sys.UnwrapOSError(errno)
}

View File

@ -0,0 +1,13 @@
//go:build !(linux || darwin || windows) || tinygo
package sysfs
import (
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
)
// poll implements `Poll` as documented on fsapi.File via a file descriptor.
func poll(uintptr, fsapi.Pflag, int32) (bool, sys.Errno) {
return false, sys.ENOSYS
}

View File

@ -0,0 +1,224 @@
package sysfs
import (
"syscall"
"time"
"unsafe"
"github.com/tetratelabs/wazero/experimental/sys"
)
var (
procWSAPoll = modws2_32.NewProc("WSAPoll")
procGetNamedPipeInfo = kernel32.NewProc("GetNamedPipeInfo")
)
const (
// _POLLRDNORM subscribes to normal data for read.
_POLLRDNORM = 0x0100
// _POLLRDBAND subscribes to priority band (out-of-band) data for read.
_POLLRDBAND = 0x0200
// _POLLIN subscribes a notification when any readable data is available.
_POLLIN = (_POLLRDNORM | _POLLRDBAND)
)
// pollFd is the struct to query for file descriptor events using poll.
type pollFd struct {
// fd is the file descriptor.
fd uintptr
// events is a bitmap containing the requested events.
events int16
// revents is a bitmap containing the returned events.
revents int16
}
// newPollFd is a constructor for pollFd that abstracts the platform-specific type of file descriptors.
func newPollFd(fd uintptr, events, revents int16) pollFd {
return pollFd{fd: fd, events: events, revents: revents}
}
// pollInterval is the interval between each calls to peekNamedPipe in selectAllHandles
const pollInterval = 100 * time.Millisecond
// _poll implements poll on Windows, for a subset of cases.
//
// fds may contain any number of file handles, but regular files and pipes are only processed for _POLLIN.
// Stdin is a pipe, thus it is checked for readiness when present. Pipes are checked using PeekNamedPipe.
// Regular files always immediately reported as ready, regardless their actual state and timeouts.
//
// If n==0 it will wait for the given timeout duration, but it will return sys.ENOSYS if timeout is nil,
// i.e. it won't block indefinitely. The given ctx is used to allow for cancellation,
// and it is currently used only in tests.
//
// The implementation actually polls every 100 milliseconds (pollInterval) until it reaches the
// given timeout (in millis).
//
// The duration may be negative, in which case it will wait indefinitely. The given ctx is
// used to allow for cancellation, and it is currently used only in tests.
func _poll(fds []pollFd, timeoutMillis int32) (n int, errno sys.Errno) {
if fds == nil {
return -1, sys.ENOSYS
}
regular, pipes, sockets, errno := partionByFtype(fds)
nregular := len(regular)
if errno != 0 {
return -1, errno
}
// Ticker that emits at every pollInterval.
tick := time.NewTicker(pollInterval)
tickCh := tick.C
defer tick.Stop()
// Timer that expires after the given duration.
// Initialize afterCh as nil: the select below will wait forever.
var afterCh <-chan time.Time
if timeoutMillis >= 0 {
// If duration is not nil, instantiate the timer.
after := time.NewTimer(time.Duration(timeoutMillis) * time.Millisecond)
defer after.Stop()
afterCh = after.C
}
npipes, nsockets, errno := peekAll(pipes, sockets)
if errno != 0 {
return -1, errno
}
count := nregular + npipes + nsockets
if count > 0 {
return count, 0
}
for {
select {
case <-afterCh:
return 0, 0
case <-tickCh:
npipes, nsockets, errno := peekAll(pipes, sockets)
if errno != 0 {
return -1, errno
}
count = nregular + npipes + nsockets
if count > 0 {
return count, 0
}
}
}
}
func peekAll(pipes, sockets []pollFd) (npipes, nsockets int, errno sys.Errno) {
npipes, errno = peekPipes(pipes)
if errno != 0 {
return
}
// Invoke wsaPoll with a 0-timeout to avoid blocking.
// Timeouts are handled in pollWithContext instead.
nsockets, errno = wsaPoll(sockets, 0)
if errno != 0 {
return
}
count := npipes + nsockets
if count > 0 {
return
}
return
}
func peekPipes(fds []pollFd) (n int, errno sys.Errno) {
for _, fd := range fds {
bytes, errno := peekNamedPipe(syscall.Handle(fd.fd))
if errno != 0 {
return -1, sys.UnwrapOSError(errno)
}
if bytes > 0 {
n++
}
}
return
}
// wsaPoll is the WSAPoll function from winsock2.
//
// See https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsapoll
func wsaPoll(fds []pollFd, timeout int) (n int, errno sys.Errno) {
if len(fds) > 0 {
sockptr := &fds[0]
ns, _, e := syscall.SyscallN(
procWSAPoll.Addr(),
uintptr(unsafe.Pointer(sockptr)),
uintptr(len(fds)),
uintptr(timeout))
if e != 0 {
return -1, sys.UnwrapOSError(e)
}
n = int(ns)
}
return
}
// ftype is a type of file that can be handled by poll.
type ftype uint8
const (
ftype_regular ftype = iota
ftype_pipe
ftype_socket
)
// partionByFtype checks the type of each fd in fds and returns 3 distinct partitions
// for regular files, named pipes and sockets.
func partionByFtype(fds []pollFd) (regular, pipe, socket []pollFd, errno sys.Errno) {
for _, pfd := range fds {
t, errno := ftypeOf(pfd.fd)
if errno != 0 {
return nil, nil, nil, errno
}
switch t {
case ftype_regular:
regular = append(regular, pfd)
case ftype_pipe:
pipe = append(pipe, pfd)
case ftype_socket:
socket = append(socket, pfd)
}
}
return
}
// ftypeOf checks the type of fd and return the corresponding ftype.
func ftypeOf(fd uintptr) (ftype, sys.Errno) {
h := syscall.Handle(fd)
t, err := syscall.GetFileType(h)
if err != nil {
return 0, sys.UnwrapOSError(err)
}
switch t {
case syscall.FILE_TYPE_CHAR, syscall.FILE_TYPE_DISK:
return ftype_regular, 0
case syscall.FILE_TYPE_PIPE:
if isSocket(h) {
return ftype_socket, 0
} else {
return ftype_pipe, 0
}
default:
return ftype_regular, 0
}
}
// isSocket returns true if the given file handle
// is a pipe.
func isSocket(fd syscall.Handle) bool {
r, _, errno := syscall.SyscallN(
procGetNamedPipeInfo.Addr(),
uintptr(fd),
uintptr(unsafe.Pointer(nil)),
uintptr(unsafe.Pointer(nil)),
uintptr(unsafe.Pointer(nil)),
uintptr(unsafe.Pointer(nil)))
return r == 0 || errno != 0
}

View File

@ -0,0 +1,117 @@
package sysfs
import (
"io/fs"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
)
type ReadFS struct {
experimentalsys.FS
}
// OpenFile implements the same method as documented on sys.FS
func (r *ReadFS) OpenFile(path string, flag experimentalsys.Oflag, perm fs.FileMode) (experimentalsys.File, experimentalsys.Errno) {
// Mask the mutually exclusive bits as they determine write mode.
switch flag & (experimentalsys.O_RDONLY | experimentalsys.O_WRONLY | experimentalsys.O_RDWR) {
case experimentalsys.O_WRONLY, experimentalsys.O_RDWR:
// Return the correct error if a directory was opened for write.
if flag&experimentalsys.O_DIRECTORY != 0 {
return nil, experimentalsys.EISDIR
}
return nil, experimentalsys.ENOSYS
default: // sys.O_RDONLY (integer zero) so we are ok!
}
f, errno := r.FS.OpenFile(path, flag, perm)
if errno != 0 {
return nil, errno
}
return &readFile{f}, 0
}
// Mkdir implements the same method as documented on sys.FS
func (r *ReadFS) Mkdir(path string, perm fs.FileMode) experimentalsys.Errno {
return experimentalsys.EROFS
}
// Chmod implements the same method as documented on sys.FS
func (r *ReadFS) Chmod(path string, perm fs.FileMode) experimentalsys.Errno {
return experimentalsys.EROFS
}
// Rename implements the same method as documented on sys.FS
func (r *ReadFS) Rename(from, to string) experimentalsys.Errno {
return experimentalsys.EROFS
}
// Rmdir implements the same method as documented on sys.FS
func (r *ReadFS) Rmdir(path string) experimentalsys.Errno {
return experimentalsys.EROFS
}
// Link implements the same method as documented on sys.FS
func (r *ReadFS) Link(_, _ string) experimentalsys.Errno {
return experimentalsys.EROFS
}
// Symlink implements the same method as documented on sys.FS
func (r *ReadFS) Symlink(_, _ string) experimentalsys.Errno {
return experimentalsys.EROFS
}
// Unlink implements the same method as documented on sys.FS
func (r *ReadFS) Unlink(path string) experimentalsys.Errno {
return experimentalsys.EROFS
}
// Utimens implements the same method as documented on sys.FS
func (r *ReadFS) Utimens(path string, atim, mtim int64) experimentalsys.Errno {
return experimentalsys.EROFS
}
// compile-time check to ensure readFile implements api.File.
var _ experimentalsys.File = (*readFile)(nil)
type readFile struct {
experimentalsys.File
}
// Write implements the same method as documented on sys.File.
func (r *readFile) Write([]byte) (int, experimentalsys.Errno) {
return 0, r.writeErr()
}
// Pwrite implements the same method as documented on sys.File.
func (r *readFile) Pwrite([]byte, int64) (n int, errno experimentalsys.Errno) {
return 0, r.writeErr()
}
// Truncate implements the same method as documented on sys.File.
func (r *readFile) Truncate(int64) experimentalsys.Errno {
return r.writeErr()
}
// Sync implements the same method as documented on sys.File.
func (r *readFile) Sync() experimentalsys.Errno {
return experimentalsys.EBADF
}
// Datasync implements the same method as documented on sys.File.
func (r *readFile) Datasync() experimentalsys.Errno {
return experimentalsys.EBADF
}
// Utimens implements the same method as documented on sys.File.
func (r *readFile) Utimens(int64, int64) experimentalsys.Errno {
return experimentalsys.EBADF
}
func (r *readFile) writeErr() experimentalsys.Errno {
if isDir, errno := r.IsDir(); errno != 0 {
return errno
} else if isDir {
return experimentalsys.EISDIR
}
return experimentalsys.EBADF
}

View File

@ -0,0 +1,16 @@
//go:build !windows && !plan9 && !tinygo
package sysfs
import (
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
)
func rename(from, to string) sys.Errno {
if from == to {
return 0
}
return sys.UnwrapOSError(syscall.Rename(from, to))
}

View File

@ -0,0 +1,14 @@
package sysfs
import (
"os"
"github.com/tetratelabs/wazero/experimental/sys"
)
func rename(from, to string) sys.Errno {
if from == to {
return 0
}
return sys.UnwrapOSError(os.Rename(from, to))
}

View File

@ -0,0 +1,55 @@
package sysfs
import (
"os"
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
)
func rename(from, to string) sys.Errno {
if from == to {
return 0
}
var fromIsDir, toIsDir bool
if fromStat, errno := stat(from); errno != 0 {
return errno // failed to stat from
} else {
fromIsDir = fromStat.Mode.IsDir()
}
if toStat, errno := stat(to); errno == sys.ENOENT {
return syscallRename(from, to) // file or dir to not-exist is ok
} else if errno != 0 {
return errno // failed to stat to
} else {
toIsDir = toStat.Mode.IsDir()
}
// Now, handle known cases
switch {
case !fromIsDir && toIsDir: // file to dir
return sys.EISDIR
case !fromIsDir && !toIsDir: // file to file
// Use os.Rename instead of syscall.Rename to overwrite a file.
// This uses MoveFileEx instead of MoveFile (used by syscall.Rename).
return sys.UnwrapOSError(os.Rename(from, to))
case fromIsDir && !toIsDir: // dir to file
return sys.ENOTDIR
default: // dir to dir
// We can't tell if a directory is empty or not, via stat information.
// Reading the directory is expensive, as it can buffer large amounts
// of data on fail. Instead, speculatively try to remove the directory.
// This is only one syscall and won't buffer anything.
if errno := rmdir(to); errno == 0 || errno == sys.ENOENT {
return syscallRename(from, to)
} else {
return errno
}
}
}
func syscallRename(from string, to string) sys.Errno {
return sys.UnwrapOSError(syscall.Rename(from, to))
}

View File

@ -0,0 +1,187 @@
package sysfs
import (
"net"
"os"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
socketapi "github.com/tetratelabs/wazero/internal/sock"
"github.com/tetratelabs/wazero/sys"
)
// NewTCPListenerFile creates a socketapi.TCPSock for a given *net.TCPListener.
func NewTCPListenerFile(tl *net.TCPListener) socketapi.TCPSock {
return newTCPListenerFile(tl)
}
// baseSockFile implements base behavior for all TCPSock, TCPConn files,
// regardless the platform.
type baseSockFile struct {
experimentalsys.UnimplementedFile
}
var _ experimentalsys.File = (*baseSockFile)(nil)
// IsDir implements the same method as documented on File.IsDir
func (*baseSockFile) IsDir() (bool, experimentalsys.Errno) {
// We need to override this method because WASI-libc prestats the FD
// and the default impl returns ENOSYS otherwise.
return false, 0
}
// Stat implements the same method as documented on File.Stat
func (f *baseSockFile) Stat() (fs sys.Stat_t, errno experimentalsys.Errno) {
// The mode is not really important, but it should be neither a regular file nor a directory.
fs.Mode = os.ModeIrregular
return
}
var _ socketapi.TCPSock = (*tcpListenerFile)(nil)
type tcpListenerFile struct {
baseSockFile
tl *net.TCPListener
closed bool
nonblock bool
}
// newTCPListenerFile is a constructor for a socketapi.TCPSock.
//
// The current strategy is to wrap a net.TCPListener
// and invoking raw syscalls using syscallConnControl:
// this internal calls RawConn.Control(func(fd)), making sure
// that the underlying file descriptor is valid throughout
// the duration of the syscall.
func newDefaultTCPListenerFile(tl *net.TCPListener) socketapi.TCPSock {
return &tcpListenerFile{tl: tl}
}
// Close implements the same method as documented on experimentalsys.File
func (f *tcpListenerFile) Close() experimentalsys.Errno {
if !f.closed {
return experimentalsys.UnwrapOSError(f.tl.Close())
}
return 0
}
// Addr is exposed for testing.
func (f *tcpListenerFile) Addr() *net.TCPAddr {
return f.tl.Addr().(*net.TCPAddr)
}
// IsNonblock implements the same method as documented on fsapi.File
func (f *tcpListenerFile) IsNonblock() bool {
return f.nonblock
}
// Poll implements the same method as documented on fsapi.File
func (f *tcpListenerFile) Poll(flag fsapi.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
return false, experimentalsys.ENOSYS
}
var _ socketapi.TCPConn = (*tcpConnFile)(nil)
type tcpConnFile struct {
baseSockFile
tc *net.TCPConn
// nonblock is true when the underlying connection is flagged as non-blocking.
// This ensures that reads and writes return experimentalsys.EAGAIN without blocking the caller.
nonblock bool
// closed is true when closed was called. This ensures proper experimentalsys.EBADF
closed bool
}
func newTcpConn(tc *net.TCPConn) socketapi.TCPConn {
return &tcpConnFile{tc: tc}
}
// Read implements the same method as documented on experimentalsys.File
func (f *tcpConnFile) Read(buf []byte) (n int, errno experimentalsys.Errno) {
if len(buf) == 0 {
return 0, 0 // Short-circuit 0-len reads.
}
if nonBlockingFileReadSupported && f.IsNonblock() {
n, errno = syscallConnControl(f.tc, func(fd uintptr) (int, experimentalsys.Errno) {
n, err := readSocket(fd, buf)
errno = experimentalsys.UnwrapOSError(err)
errno = fileError(f, f.closed, errno)
return n, errno
})
} else {
n, errno = read(f.tc, buf)
}
if errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
}
return
}
// Write implements the same method as documented on experimentalsys.File
func (f *tcpConnFile) Write(buf []byte) (n int, errno experimentalsys.Errno) {
if nonBlockingFileWriteSupported && f.IsNonblock() {
return syscallConnControl(f.tc, func(fd uintptr) (int, experimentalsys.Errno) {
n, err := writeSocket(fd, buf)
errno = experimentalsys.UnwrapOSError(err)
errno = fileError(f, f.closed, errno)
return n, errno
})
} else {
n, errno = write(f.tc, buf)
}
if errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
}
return
}
// Recvfrom implements the same method as documented on socketapi.TCPConn
func (f *tcpConnFile) Recvfrom(p []byte, flags int) (n int, errno experimentalsys.Errno) {
if flags != MSG_PEEK {
errno = experimentalsys.EINVAL
return
}
return syscallConnControl(f.tc, func(fd uintptr) (int, experimentalsys.Errno) {
n, err := recvfrom(fd, p, MSG_PEEK)
errno = experimentalsys.UnwrapOSError(err)
errno = fileError(f, f.closed, errno)
return n, errno
})
}
// Close implements the same method as documented on experimentalsys.File
func (f *tcpConnFile) Close() experimentalsys.Errno {
return f.close()
}
func (f *tcpConnFile) close() experimentalsys.Errno {
if f.closed {
return 0
}
f.closed = true
return f.Shutdown(socketapi.SHUT_RDWR)
}
// SetNonblock implements the same method as documented on fsapi.File
func (f *tcpConnFile) SetNonblock(enabled bool) (errno experimentalsys.Errno) {
f.nonblock = enabled
_, errno = syscallConnControl(f.tc, func(fd uintptr) (int, experimentalsys.Errno) {
return 0, experimentalsys.UnwrapOSError(setNonblockSocket(fd, enabled))
})
return
}
// IsNonblock implements the same method as documented on fsapi.File
func (f *tcpConnFile) IsNonblock() bool {
return f.nonblock
}
// Poll implements the same method as documented on fsapi.File
func (f *tcpConnFile) Poll(flag fsapi.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
return false, experimentalsys.ENOSYS
}

View File

@ -0,0 +1,77 @@
//go:build (linux || darwin || windows) && !tinygo
package sysfs
import (
"net"
"syscall"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
socketapi "github.com/tetratelabs/wazero/internal/sock"
)
// Accept implements the same method as documented on socketapi.TCPSock
func (f *tcpListenerFile) Accept() (socketapi.TCPConn, experimentalsys.Errno) {
// Ensure we have an incoming connection, otherwise return immediately.
if f.nonblock {
if ready, errno := _pollSock(f.tl, fsapi.POLLIN, 0); !ready || errno != 0 {
return nil, experimentalsys.EAGAIN
}
}
// Accept normally blocks goroutines, but we
// made sure that we have an incoming connection,
// so we should be safe.
if conn, err := f.tl.Accept(); err != nil {
return nil, experimentalsys.UnwrapOSError(err)
} else {
return newTcpConn(conn.(*net.TCPConn)), 0
}
}
// SetNonblock implements the same method as documented on fsapi.File
func (f *tcpListenerFile) SetNonblock(enabled bool) (errno experimentalsys.Errno) {
f.nonblock = enabled
_, errno = syscallConnControl(f.tl, func(fd uintptr) (int, experimentalsys.Errno) {
return 0, setNonblockSocket(fd, enabled)
})
return
}
// Shutdown implements the same method as documented on experimentalsys.Conn
func (f *tcpConnFile) Shutdown(how int) experimentalsys.Errno {
// FIXME: can userland shutdown listeners?
var err error
switch how {
case socketapi.SHUT_RD:
err = f.tc.CloseRead()
case socketapi.SHUT_WR:
err = f.tc.CloseWrite()
case socketapi.SHUT_RDWR:
return f.close()
default:
return experimentalsys.EINVAL
}
return experimentalsys.UnwrapOSError(err)
}
// syscallConnControl extracts a syscall.RawConn from the given syscall.Conn and applies
// the given fn to a file descriptor, returning an integer or a nonzero syscall.Errno on failure.
//
// syscallConnControl streamlines the pattern of extracting the syscall.Rawconn,
// invoking its syscall.RawConn.Control method, then handling properly the errors that may occur
// within fn or returned by syscall.RawConn.Control itself.
func syscallConnControl(conn syscall.Conn, fn func(fd uintptr) (int, experimentalsys.Errno)) (n int, errno experimentalsys.Errno) {
syscallConn, err := conn.SyscallConn()
if err != nil {
return 0, experimentalsys.UnwrapOSError(err)
}
// Prioritize the inner errno over Control
if controlErr := syscallConn.Control(func(fd uintptr) {
n, errno = fn(fd)
}); errno == 0 {
errno = experimentalsys.UnwrapOSError(controlErr)
}
return
}

View File

@ -0,0 +1,49 @@
//go:build (linux || darwin) && !tinygo
package sysfs
import (
"net"
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
socketapi "github.com/tetratelabs/wazero/internal/sock"
)
// MSG_PEEK is the constant syscall.MSG_PEEK
const MSG_PEEK = syscall.MSG_PEEK
func newTCPListenerFile(tl *net.TCPListener) socketapi.TCPSock {
return newDefaultTCPListenerFile(tl)
}
func _pollSock(conn syscall.Conn, flag fsapi.Pflag, timeoutMillis int32) (bool, sys.Errno) {
n, errno := syscallConnControl(conn, func(fd uintptr) (int, sys.Errno) {
if ready, errno := poll(fd, fsapi.POLLIN, 0); !ready || errno != 0 {
return -1, errno
} else {
return 0, errno
}
})
return n >= 0, errno
}
func setNonblockSocket(fd uintptr, enabled bool) sys.Errno {
return sys.UnwrapOSError(setNonblock(fd, enabled))
}
func readSocket(fd uintptr, buf []byte) (int, sys.Errno) {
n, err := syscall.Read(int(fd), buf)
return n, sys.UnwrapOSError(err)
}
func writeSocket(fd uintptr, buf []byte) (int, sys.Errno) {
n, err := syscall.Write(int(fd), buf)
return n, sys.UnwrapOSError(err)
}
func recvfrom(fd uintptr, buf []byte, flags int32) (n int, errno sys.Errno) {
n, _, err := syscall.Recvfrom(int(fd), buf, int(flags))
return n, sys.UnwrapOSError(err)
}

View File

@ -0,0 +1,81 @@
//go:build (!linux && !darwin && !windows) || tinygo
package sysfs
import (
"net"
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
socketapi "github.com/tetratelabs/wazero/internal/sock"
)
// MSG_PEEK is a filler value.
const MSG_PEEK = 0x2
func newTCPListenerFile(tl *net.TCPListener) socketapi.TCPSock {
return &unsupportedSockFile{}
}
type unsupportedSockFile struct {
baseSockFile
}
// Accept implements the same method as documented on socketapi.TCPSock
func (f *unsupportedSockFile) Accept() (socketapi.TCPConn, sys.Errno) {
return nil, sys.ENOSYS
}
func _pollSock(conn syscall.Conn, flag fsapi.Pflag, timeoutMillis int32) (bool, sys.Errno) {
return false, sys.ENOTSUP
}
func setNonblockSocket(fd uintptr, enabled bool) sys.Errno {
return sys.ENOTSUP
}
func readSocket(fd uintptr, buf []byte) (int, sys.Errno) {
return -1, sys.ENOTSUP
}
func writeSocket(fd uintptr, buf []byte) (int, sys.Errno) {
return -1, sys.ENOTSUP
}
func recvfrom(fd uintptr, buf []byte, flags int32) (n int, errno sys.Errno) {
return -1, sys.ENOTSUP
}
// syscallConnControl extracts a syscall.RawConn from the given syscall.Conn and applies
// the given fn to a file descriptor, returning an integer or a nonzero syscall.Errno on failure.
//
// syscallConnControl streamlines the pattern of extracting the syscall.Rawconn,
// invoking its syscall.RawConn.Control method, then handling properly the errors that may occur
// within fn or returned by syscall.RawConn.Control itself.
func syscallConnControl(conn syscall.Conn, fn func(fd uintptr) (int, experimentalsys.Errno)) (n int, errno sys.Errno) {
return -1, sys.ENOTSUP
}
// Accept implements the same method as documented on socketapi.TCPSock
func (f *tcpListenerFile) Accept() (socketapi.TCPConn, experimentalsys.Errno) {
return nil, experimentalsys.ENOSYS
}
// Shutdown implements the same method as documented on experimentalsys.Conn
func (f *tcpConnFile) Shutdown(how int) experimentalsys.Errno {
// FIXME: can userland shutdown listeners?
var err error
switch how {
case socketapi.SHUT_RD:
err = f.tc.Close()
case socketapi.SHUT_WR:
err = f.tc.Close()
case socketapi.SHUT_RDWR:
return f.close()
default:
return experimentalsys.EINVAL
}
return experimentalsys.UnwrapOSError(err)
}

View File

@ -0,0 +1,80 @@
//go:build windows
package sysfs
import (
"net"
"syscall"
"unsafe"
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
socketapi "github.com/tetratelabs/wazero/internal/sock"
)
const (
// MSG_PEEK is the flag PEEK for syscall.Recvfrom on Windows.
// This constant is not exported on this platform.
MSG_PEEK = 0x2
// _FIONBIO is the flag to set the O_NONBLOCK flag on socket handles using ioctlsocket.
_FIONBIO = 0x8004667e
)
var (
// modws2_32 is WinSock.
modws2_32 = syscall.NewLazyDLL("ws2_32.dll")
// procrecvfrom exposes recvfrom from WinSock.
procrecvfrom = modws2_32.NewProc("recvfrom")
// procioctlsocket exposes ioctlsocket from WinSock.
procioctlsocket = modws2_32.NewProc("ioctlsocket")
)
func newTCPListenerFile(tl *net.TCPListener) socketapi.TCPSock {
return newDefaultTCPListenerFile(tl)
}
// recvfrom exposes the underlying syscall in Windows.
//
// Note: since we are only using this to expose MSG_PEEK,
// we do not need really need all the parameters that are actually
// allowed in WinSock.
// We ignore `from *sockaddr` and `fromlen *int`.
func recvfrom(s uintptr, buf []byte, flags int32) (n int, errno sys.Errno) {
var _p0 *byte
if len(buf) > 0 {
_p0 = &buf[0]
}
r0, _, e1 := syscall.SyscallN(
procrecvfrom.Addr(),
s,
uintptr(unsafe.Pointer(_p0)),
uintptr(len(buf)),
uintptr(flags),
0, // from *sockaddr (optional)
0) // fromlen *int (optional)
return int(r0), sys.UnwrapOSError(e1)
}
func setNonblockSocket(fd uintptr, enabled bool) sys.Errno {
opt := uint64(0)
if enabled {
opt = 1
}
// ioctlsocket(fd, FIONBIO, &opt)
_, _, errno := syscall.SyscallN(
procioctlsocket.Addr(),
uintptr(fd),
uintptr(_FIONBIO),
uintptr(unsafe.Pointer(&opt)))
return sys.UnwrapOSError(errno)
}
func _pollSock(conn syscall.Conn, flag fsapi.Pflag, timeoutMillis int32) (bool, sys.Errno) {
if flag != fsapi.POLLIN {
return false, sys.ENOTSUP
}
n, errno := syscallConnControl(conn, func(fd uintptr) (int, sys.Errno) {
return _poll([]pollFd{newPollFd(fd, _POLLIN, 0)}, timeoutMillis)
})
return n > 0, errno
}

View File

@ -0,0 +1,16 @@
package sysfs
import (
"io/fs"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/sys"
)
func defaultStatFile(f fs.File) (sys.Stat_t, experimentalsys.Errno) {
if info, err := f.Stat(); err != nil {
return sys.Stat_t{}, experimentalsys.UnwrapOSError(err)
} else {
return sys.NewStat_t(info), 0
}
}

View File

@ -0,0 +1,37 @@
//go:build (amd64 || arm64) && (darwin || freebsd)
package sysfs
import (
"io/fs"
"os"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/sys"
)
// dirNlinkIncludesDot is true because even though os.File filters out dot
// entries, the underlying syscall.Stat includes them.
//
// Note: this is only used in tests
const dirNlinkIncludesDot = true
func lstat(path string) (sys.Stat_t, experimentalsys.Errno) {
if info, err := os.Lstat(path); err != nil {
return sys.Stat_t{}, experimentalsys.UnwrapOSError(err)
} else {
return sys.NewStat_t(info), 0
}
}
func stat(path string) (sys.Stat_t, experimentalsys.Errno) {
if info, err := os.Stat(path); err != nil {
return sys.Stat_t{}, experimentalsys.UnwrapOSError(err)
} else {
return sys.NewStat_t(info), 0
}
}
func statFile(f fs.File) (sys.Stat_t, experimentalsys.Errno) {
return defaultStatFile(f)
}

View File

@ -0,0 +1,40 @@
//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 sysfs
import (
"io/fs"
"os"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/sys"
)
// dirNlinkIncludesDot is true because even though os.File filters out dot
// entries, the underlying syscall.Stat includes them.
//
// Note: this is only used in tests
const dirNlinkIncludesDot = true
func lstat(path string) (sys.Stat_t, experimentalsys.Errno) {
if info, err := os.Lstat(path); err != nil {
return sys.Stat_t{}, experimentalsys.UnwrapOSError(err)
} else {
return sys.NewStat_t(info), 0
}
}
func stat(path string) (sys.Stat_t, experimentalsys.Errno) {
if info, err := os.Stat(path); err != nil {
return sys.Stat_t{}, experimentalsys.UnwrapOSError(err)
} else {
return sys.NewStat_t(info), 0
}
}
func statFile(f fs.File) (sys.Stat_t, experimentalsys.Errno) {
return defaultStatFile(f)
}

View File

@ -0,0 +1,40 @@
//go:build (!((amd64 || arm64 || riscv64) && linux) && !((amd64 || arm64) && (darwin || freebsd)) && !((amd64 || arm64) && windows)) || js
package sysfs
import (
"io/fs"
"os"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/sys"
)
// Note: go:build constraints must be the same as /sys.stat_unsupported.go for
// the same reasons.
// dirNlinkIncludesDot might be true for some operating systems, which can have
// new stat_XX.go files as necessary.
//
// Note: this is only used in tests
const dirNlinkIncludesDot = false
func lstat(path string) (sys.Stat_t, experimentalsys.Errno) {
if info, err := os.Lstat(path); err != nil {
return sys.Stat_t{}, experimentalsys.UnwrapOSError(err)
} else {
return sys.NewStat_t(info), 0
}
}
func stat(path string) (sys.Stat_t, experimentalsys.Errno) {
if info, err := os.Stat(path); err != nil {
return sys.Stat_t{}, experimentalsys.UnwrapOSError(err)
} else {
return sys.NewStat_t(info), 0
}
}
func statFile(f fs.File) (sys.Stat_t, experimentalsys.Errno) {
return defaultStatFile(f)
}

View File

@ -0,0 +1,120 @@
//go:build (amd64 || arm64) && windows
package sysfs
import (
"io/fs"
"syscall"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/sys"
)
// dirNlinkIncludesDot is false because Windows does not return dot entries.
//
// Note: this is only used in tests
const dirNlinkIncludesDot = false
func lstat(path string) (sys.Stat_t, experimentalsys.Errno) {
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)
}
func stat(path string) (sys.Stat_t, experimentalsys.Errno) {
attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS)
return statPath(attrs, path)
}
func statPath(createFileAttrs uint32, path string) (sys.Stat_t, experimentalsys.Errno) {
if len(path) == 0 {
return sys.Stat_t{}, experimentalsys.ENOENT
}
pathp, err := syscall.UTF16PtrFromString(path)
if err != nil {
return sys.Stat_t{}, experimentalsys.EINVAL
}
// open the file handle
h, err := syscall.CreateFile(pathp, 0, 0, nil,
syscall.OPEN_EXISTING, createFileAttrs, 0)
if err != nil {
errno := experimentalsys.UnwrapOSError(err)
// To match expectations of WASI, e.g. TinyGo TestStatBadDir, return
// ENOENT, not ENOTDIR.
if errno == experimentalsys.ENOTDIR {
errno = experimentalsys.ENOENT
}
return sys.Stat_t{}, errno
}
defer syscall.CloseHandle(h)
return statHandle(h)
}
// fdFile allows masking the `Fd` function on os.File.
type fdFile interface {
Fd() uintptr
}
func statFile(f fs.File) (sys.Stat_t, experimentalsys.Errno) {
if osF, ok := f.(fdFile); ok {
// Attempt to get the stat by handle, which works for normal files
st, err := statHandle(syscall.Handle(osF.Fd()))
// 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.
//
// Note: statHandle uses UnwrapOSError which coerces
// ERROR_INVALID_HANDLE to EBADF.
if err != experimentalsys.EBADF {
return st, err
}
}
return defaultStatFile(f)
}
func statHandle(h syscall.Handle) (sys.Stat_t, experimentalsys.Errno) {
winFt, err := syscall.GetFileType(h)
if err != nil {
return sys.Stat_t{}, experimentalsys.UnwrapOSError(err)
}
var fi syscall.ByHandleFileInformation
if err = syscall.GetFileInformationByHandle(h, &fi); err != nil {
return sys.Stat_t{}, experimentalsys.UnwrapOSError(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
}
st := sys.Stat_t{}
// 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
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 st, 0
}

View File

@ -0,0 +1,13 @@
//go:build !windows
package sysfs
import (
"os"
"github.com/tetratelabs/wazero/experimental/sys"
)
func fsync(f *os.File) sys.Errno {
return sys.UnwrapOSError(f.Sync())
}

View File

@ -0,0 +1,20 @@
package sysfs
import (
"os"
"github.com/tetratelabs/wazero/experimental/sys"
)
func fsync(f *os.File) sys.Errno {
errno := sys.UnwrapOSError(f.Sync())
// Coerce error performing stat on a directory to 0, as it won't work
// on Windows.
switch errno {
case sys.EACCES /* Go 1.20 */, sys.EBADF /* Go 1.19 */ :
if st, err := f.Stat(); err == nil && st.IsDir() {
errno = 0
}
}
return errno
}

View File

@ -0,0 +1,13 @@
package sysfs
import (
"syscall"
_ "unsafe"
)
// syscall_syscall6 is a private symbol that we link below. We need to use this
// instead of syscall.Syscall6 because the public syscall.Syscall6 won't work
// when fn is an address.
//
//go:linkname syscall_syscall6 syscall.syscall6
func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)

View File

@ -0,0 +1,6 @@
// Package sysfs includes a low-level filesystem interface and utilities needed
// for WebAssembly host functions (ABI) such as WASI.
//
// The name sysfs was chosen because wazero's public API has a "sys" package,
// which was named after https://github.com/golang/sys.
package sysfs

View File

@ -0,0 +1,17 @@
//go:build !windows && !plan9 && !tinygo
package sysfs
import (
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
)
func unlink(name string) (errno sys.Errno) {
err := syscall.Unlink(name)
if errno = sys.UnwrapOSError(err); errno == sys.EPERM {
errno = sys.EISDIR
}
return errno
}

View File

@ -0,0 +1,12 @@
package sysfs
import (
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
)
func unlink(name string) sys.Errno {
err := syscall.Remove(name)
return sys.UnwrapOSError(err)
}

View File

@ -0,0 +1,25 @@
package sysfs
import (
"os"
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
)
func unlink(name string) sys.Errno {
err := syscall.Unlink(name)
if err == nil {
return 0
}
errno := sys.UnwrapOSError(err)
if errno == sys.EBADF {
lstat, errLstat := os.Lstat(name)
if errLstat == nil && lstat.Mode()&os.ModeSymlink != 0 {
errno = sys.UnwrapOSError(os.Remove(name))
} else {
errno = sys.EISDIR
}
}
return errno
}