Files
wazero/internal/sysfs/sysfs.go
Crypt Keeper 2a584a8937 fs: renames internal syscallfs package to sysfs and notes RATIONALE (#1056)
It will help for us to rename earlier vs later, and syscallfs will be
laborious, especially after we introduce an FSConfig type and need to
declare a method name that differentiates from normal fs.FS. e.g. WithFS
vs WithSysFS reads nicer than WithSyscallFS, and meanwhile sys is
already a public package.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
2023-01-23 11:11:35 +08:00

315 lines
9.5 KiB
Go

// Package sysfs includes a low-level filesystem interface and utilities needed
// for WebAssembly host functions (ABI) such as WASI and runtime.GOOS=js.
//
// 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
import (
"io"
"io/fs"
"os"
"syscall"
)
// FS is a writeable fs.FS bridge backed by syscall functions needed for ABI
// including WASI and runtime.GOOS=js.
//
// Implementations should embed UnimplementedFS for forward compatability. Any
// unsupported method or parameter should return syscall.ENOSYS.
//
// See https://github.com/golang/go/issues/45757
type FS interface {
// String should return a human-readable format of the filesystem:
// - If read-only, $host:$guestDir:ro
// - If read-write, $host:$guestDir
//
// For example, if this filesystem is backed by the real directory
// "/tmp/wasm" and the GuestDir is "/", the expected value is
// "/var/tmp:/tmp".
//
// When the host filesystem isn't a real filesystem, substitute a symbolic,
// human-readable name. e.g. "virtual:/"
String() string
// GuestDir is the name of the path the guest should use this filesystem
// for, or root ("/") for any files.
//
// This value allows the guest to avoid making file-system calls when they
// won't succeed. e.g. if "/tmp" is returned and the guest requests
// "/etc/passwd". This approach is used in compilers that use WASI
// pre-opens.
//
// # Notes
// - Implementations must always return the same value.
// - Go compiled with runtime.GOOS=js do not pay attention to this value.
// Hence, you need to normalize the filesystem with NewRootFS to ensure
// paths requested resolve as expected.
// - Working directories are typically tracked in wasm, though possible
// some relative paths are requested. For example, TinyGo may attempt
// to resolve a path "../.." in unit tests.
// - Zig uses the first path name it sees as the initial working
// directory of the process.
GuestDir() string
// Open is only defined to match the signature of fs.FS until we remove it.
// Once we are done bridging, we will remove this function. Meanwhile,
// using it will panic to ensure internal code doesn't depend on it.
Open(name string) (fs.File, error)
// OpenFile is similar to os.OpenFile, except the path is relative to this
// file system, and syscall.Errno are returned instead of a os.PathError.
//
// # Errors
//
// The following errors are expected:
// - syscall.EINVAL: `path` or `flag` is invalid.
// - syscall.ENOENT: `path` doesn't exist and `flag` doesn't contain
// os.O_CREATE.
//
// # Constraints on the returned file
//
// Implementations that can read flags should enforce them regardless of
// the type returned. For example, while os.File implements io.Writer,
// attempts to write to a directory or a file opened with os.O_RDONLY fail
// with a syscall.EBADF.
//
// Some implementations choose whether to enforce read-only opens, namely
// fs.FS. While fs.FS is supported (Adapt), wazero cannot runtime enforce
// open flags. Instead, we encourage good behavior and test our built-in
// implementations.
OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error)
// ^^ TODO: Consider syscall.Open, though this implies defining and
// coercing flags and perms similar to what is done in os.OpenFile.
// Mkdir is similar to os.Mkdir, except the path is relative to this file
// system, and syscall.Errno are returned instead of a os.PathError.
//
// # Errors
//
// The following errors are expected:
// - syscall.EINVAL: `path` is invalid.
// - syscall.EEXIST: `path` exists and is a directory.
// - syscall.ENOTDIR: `path` exists and is a file.
//
Mkdir(path string, perm fs.FileMode) error
// ^^ TODO: Consider syscall.Mkdir, though this implies defining and
// coercing flags and perms similar to what is done in os.Mkdir.
// Rename is similar to syscall.Rename, except the path is relative to this
// file system.
//
// # Errors
//
// The following errors are expected:
// - syscall.EINVAL: `from` or `to` is invalid.
// - syscall.ENOENT: `from` or `to` don't exist.
// - syscall.ENOTDIR: `from` is a directory and `to` exists, but is a file.
// - syscall.EISDIR: `from` is a file and `to` exists, but is a directory.
//
// # Notes
//
// - Windows doesn't let you overwrite an existing directory.
Rename(from, to string) error
// Rmdir is similar to syscall.Rmdir, except the path is relative to this
// file system.
//
// # Errors
//
// The following errors are expected:
// - syscall.EINVAL: `path` is invalid.
// - syscall.ENOENT: `path` doesn't exist.
// - syscall.ENOTDIR: `path` exists, but isn't a directory.
// - syscall.ENOTEMPTY: `path` exists, but isn't empty.
//
// # Notes
//
// - As of Go 1.19, Windows maps syscall.ENOTDIR to syscall.ENOENT.
Rmdir(path string) error
// Unlink is similar to syscall.Unlink, except the path is relative to this
// file system.
//
// The following errors are expected:
// - syscall.EINVAL: `path` is invalid.
// - syscall.ENOENT: `path` doesn't exist.
// - syscall.EISDIR: `path` exists, but is a directory.
Unlink(path string) error
// Utimes is similar to syscall.UtimesNano, except the path is relative to
// this file system.
//
// # Errors
//
// The following errors are expected:
// - syscall.EINVAL: `path` is invalid.
// - syscall.ENOENT: `path` doesn't exist
//
// # Notes
//
// - To set wall clock time, retrieve it first from sys.Walltime.
// - syscall.UtimesNano cannot change the ctime. Also, neither WASI nor
// runtime.GOOS=js support changing it. Hence, ctime it is absent here.
Utimes(path string, atimeNsec, mtimeNsec int64) error
}
// StatPath is a convenience that calls FS.OpenFile until there is a stat
// method.
func StatPath(fs FS, path string) (fs.FileInfo, error) {
f, err := fs.OpenFile(path, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
defer f.Close()
return f.Stat()
}
// readFile declares all read interfaces defined on os.File used by wazero.
type readFile interface {
fs.ReadDirFile
io.ReaderAt // for pread
io.Seeker // fallback for ReaderAt for embed:fs
}
// file declares all interfaces defined on os.File used by wazero.
type file interface {
readFile
io.Writer
io.WriterAt // for pwrite
syncer
}
type syncer interface{ Sync() error }
// ReaderAtOffset gets an io.Reader from a fs.File that reads from an offset,
// yet doesn't affect the underlying position. This is used to implement
// syscall.Pread.
//
// Note: The file accessed shouldn't be used concurrently, but wasm isn't safe
// to use concurrently anyway. Hence, we don't do any locking against parallel
// reads.
func ReaderAtOffset(f fs.File, offset int64) io.Reader {
if ret, ok := f.(io.ReaderAt); ok {
return &readerAtOffset{ret, offset}
} else if ret, ok := f.(io.ReadSeeker); ok {
return &seekToOffsetReader{ret, offset}
} else {
return enosysReader{}
}
}
type enosysReader struct{}
// enosysReader implements io.Reader
func (rs enosysReader) Read([]byte) (n int, err error) {
return 0, syscall.ENOSYS
}
type readerAtOffset struct {
r io.ReaderAt
offset int64
}
// Read implements io.Reader
func (r *readerAtOffset) Read(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil // less overhead on zero-length reads.
}
n, err := r.r.ReadAt(p, r.offset)
r.offset += int64(n)
return n, err
}
// seekToOffsetReader implements io.Reader that seeks to an offset and reverts
// to its initial offset after each call to Read.
//
// See /RATIONALE.md "fd_pread: io.Seeker fallback when io.ReaderAt is not supported"
type seekToOffsetReader struct {
s io.ReadSeeker
offset int64
}
// Read implements io.Reader
func (rs *seekToOffsetReader) Read(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil // less overhead on zero-length reads.
}
// Determine the current position in the file, as we need to revert it.
currentOffset, err := rs.s.Seek(0, io.SeekCurrent)
if err != nil {
return 0, err
}
// Put the read position back when complete.
defer func() { _, _ = rs.s.Seek(currentOffset, io.SeekStart) }()
// If the current offset isn't in sync with this reader, move it.
if rs.offset != currentOffset {
_, err := rs.s.Seek(rs.offset, io.SeekStart)
if err != nil {
return 0, err
}
}
// Perform the read, updating the offset.
n, err := rs.s.Read(p)
rs.offset += int64(n)
return n, err
}
// WriterAtOffset gets an io.Writer from a fs.File that writes to an offset,
// yet doesn't affect the underlying position. This is used to implement
// syscall.Pwrite.
func WriterAtOffset(f fs.File, offset int64) io.Writer {
if ret, ok := f.(io.WriterAt); ok {
return &writerAtOffset{ret, offset}
} else {
return enosysWriter{}
}
}
type enosysWriter struct{}
// enosysWriter implements io.Writer
func (rs enosysWriter) Write([]byte) (n int, err error) {
return 0, syscall.ENOSYS
}
type writerAtOffset struct {
r io.WriterAt
offset int64
}
// Write implements io.Writer
func (r *writerAtOffset) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil // less overhead on zero-length writes.
}
n, err := r.r.WriteAt(p, r.offset)
r.offset += int64(n)
return n, err
}
func unwrapPathError(err error) error {
if pe, ok := err.(*fs.PathError); ok {
err = pe.Err
}
switch err {
case fs.ErrInvalid:
return syscall.EINVAL
case fs.ErrPermission:
return syscall.EPERM
case fs.ErrExist:
return syscall.EEXIST
case fs.ErrNotExist:
return syscall.ENOENT
case fs.ErrClosed:
return syscall.EBADF
}
return err
}