Files
wazero/internal/sys/fs.go
Crypt Keeper 319d6cca62 fs: adds base UnimplementedFS type and unwraps PathError (#1046)
This reduces some boilerplate by extracting UnimplementedFS from the
existing FS implementations, such that it returns ENOSYS. This also
removes inconsistency where some methods on FS returned syscall.Errno
and others PathError.

Note: this doesn't get rid of all PathError, yet. We still need to
create a syscallfs.File type which would be able to do that. This is
just one preliminary cleanup before refactoring out the `fs.FS`
embedding from `syscallfs.DS`.

P.S. naming convention is arbitrary, so I took UnimplementedXXX from
grpc. This pattern is used a lot of places, also proxy-wasm-go-sdk, e.g.
`DefaultVMContext`.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
2023-01-18 09:37:12 -06:00

327 lines
8.5 KiB
Go

package sys
import (
"context"
"fmt"
"io"
"io/fs"
"os"
"syscall"
"time"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/syscallfs"
)
const (
FdStdin uint32 = iota
FdStdout
FdStderr
// FdPreopen is the file descriptor of the first pre-opened directory.
//
// # Why file descriptor 3?
//
// While not specified, the most common WASI implementation, wasi-libc,
// expects POSIX style file descriptor allocation, where the lowest
// available number is used to open the next file. Since 1 and 2 are taken
// by stdout and stderr, the next is 3.
// - https://github.com/WebAssembly/WASI/issues/122
// - https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_14
// - https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-16/libc-bottom-half/sources/preopens.c#L215
FdPreopen
)
const (
modeDevice = uint32(fs.ModeDevice | 0o640)
modeCharDevice = uint32(fs.ModeCharDevice | 0o640)
)
type stdioFileWriter struct {
w io.Writer
s fs.FileInfo
}
// Stat implements fs.File
func (w *stdioFileWriter) Stat() (fs.FileInfo, error) { return w.s, nil }
// Read implements fs.File
func (w *stdioFileWriter) Read([]byte) (n int, err error) {
return // emulate os.Stdout which returns zero
}
// Write implements io.Writer
func (w *stdioFileWriter) Write(p []byte) (n int, err error) {
return w.w.Write(p)
}
// Close implements fs.File
func (w *stdioFileWriter) Close() error {
// Don't actually close the underlying file, as we didn't open it!
return nil
}
type stdioFileReader struct {
r io.Reader
s fs.FileInfo
}
// Stat implements fs.File
func (r *stdioFileReader) Stat() (fs.FileInfo, error) { return r.s, nil }
// Read implements fs.File
func (r *stdioFileReader) Read(p []byte) (n int, err error) {
return r.r.Read(p)
}
// Close implements fs.File
func (r *stdioFileReader) Close() error {
// Don't actually close the underlying file, as we didn't open it!
return nil
}
var (
noopStdinStat = stdioFileInfo{FdStdin, modeDevice}
noopStdoutStat = stdioFileInfo{FdStdout, modeDevice}
noopStderrStat = stdioFileInfo{FdStderr, modeDevice}
)
// stdioFileInfo implements fs.FileInfo where index zero is the FD and one is the mode.
type stdioFileInfo [2]uint32
func (s stdioFileInfo) Name() string {
switch s[0] {
case FdStdin:
return "stdin"
case FdStdout:
return "stdout"
case FdStderr:
return "stderr"
default:
panic(fmt.Errorf("BUG: incorrect FD %d", s[0]))
}
}
func (stdioFileInfo) Size() int64 { return 0 }
func (s stdioFileInfo) Mode() fs.FileMode { return fs.FileMode(s[1]) }
func (stdioFileInfo) ModTime() time.Time { return time.Unix(0, 0) }
func (stdioFileInfo) IsDir() bool { return false }
func (stdioFileInfo) Sys() interface{} { return nil }
type lazyDir struct {
fs syscallfs.FS
f fs.File
}
// Stat implements fs.File
func (r *lazyDir) Stat() (fs.FileInfo, error) {
if f, err := r.file(); err != nil {
return nil, err
} else {
return f.Stat()
}
}
func (r *lazyDir) file() (f fs.File, err error) {
if f = r.f; r.f != nil {
return
}
r.f, err = r.fs.OpenFile(".", os.O_RDONLY, 0)
f = r.f
return
}
// Read implements fs.File
func (r *lazyDir) Read(p []byte) (n int, err error) {
if f, err := r.file(); err != nil {
return 0, err
} else {
return f.Read(p)
}
}
// Close implements fs.File
func (r *lazyDir) Close() error {
if f, err := r.file(); err != nil {
return nil
} else {
return f.Close()
}
}
// FileEntry maps a path to an open file in a file system.
type FileEntry struct {
// Name is the name of the directory up to its pre-open.
//
// Note: This is empty when a pre-open and can drift on rename.
Name string
// IsPreopen is a directory that is lazily opened.
IsPreopen bool
isDirectory bool
// File is always non-nil.
File fs.File
// ReadDir is present when this File is a fs.ReadDirFile and `ReadDir`
// was called.
ReadDir *ReadDir
}
// IsDir returns true if the file is a directory.
func (f *FileEntry) IsDir() bool {
if f.IsPreopen || f.isDirectory {
return true
}
_, _ = f.Stat() // Maybe the file hasn't had stat yet.
return f.isDirectory
}
// Stat returns the underlying stat of this file.
func (f *FileEntry) Stat() (stat fs.FileInfo, err error) {
stat, err = f.File.Stat()
if err == nil && stat.IsDir() {
f.isDirectory = true
}
return stat, err
}
// ReadDir is the status of a prior fs.ReadDirFile call.
type ReadDir struct {
// CountRead is the total count of files read including Entries.
CountRead uint64
// Entries is the contents of the last fs.ReadDirFile call. Notably,
// directory listing are not rewindable, so we keep entries around in case
// the caller mis-estimated their buffer and needs a few still cached.
Entries []fs.DirEntry
}
type FSContext struct {
// fs is the root ("/") mount.
fs syscallfs.FS
// openedFiles is a map of file descriptor numbers (>=FdPreopen) to open files
// (or directories) and defaults to empty.
// TODO: This is unguarded, so not goroutine-safe!
openedFiles FileTable
}
// NewFSContext creates a FSContext with stdio streams and an optional
// pre-opened filesystem.
//
// If `preopened` is not syscallfs.UnimplementedFS, it is inserted into
// the file descriptor table as FdPreopen.
func NewFSContext(stdin io.Reader, stdout, stderr io.Writer, preopened syscallfs.FS) (fsc *FSContext, err error) {
fsc = &FSContext{fs: preopened}
fsc.openedFiles.Insert(stdinReader(stdin))
fsc.openedFiles.Insert(stdioWriter(stdout, noopStdoutStat))
fsc.openedFiles.Insert(stdioWriter(stderr, noopStderrStat))
if _, ok := preopened.(syscallfs.UnimplementedFS); ok {
return fsc, nil
}
fsc.openedFiles.Insert(&FileEntry{
IsPreopen: true,
File: &lazyDir{fs: preopened},
})
return fsc, nil
}
func stdinReader(r io.Reader) *FileEntry {
if r == nil {
r = eofReader{}
}
s := stdioStat(r, noopStdinStat)
return &FileEntry{File: &stdioFileReader{r: r, s: s}}
}
func stdioWriter(w io.Writer, defaultStat stdioFileInfo) *FileEntry {
if w == nil {
w = io.Discard
}
s := stdioStat(w, defaultStat)
return &FileEntry{File: &stdioFileWriter{w: w, s: s}}
}
func stdioStat(f interface{}, defaultStat stdioFileInfo) fs.FileInfo {
if f, ok := f.(*os.File); ok && platform.IsTerminal(f.Fd()) {
return stdioFileInfo{defaultStat[0], modeCharDevice}
}
return defaultStat
}
// fileModeStat is a fake fs.FileInfo which only returns its mode.
// This is used for character devices.
type fileModeStat fs.FileMode
var _ fs.FileInfo = fileModeStat(0)
func (s fileModeStat) Size() int64 { return 0 }
func (s fileModeStat) Mode() fs.FileMode { return fs.FileMode(s) }
func (s fileModeStat) ModTime() time.Time { return time.Unix(0, 0) }
func (s fileModeStat) Sys() interface{} { return nil }
func (s fileModeStat) Name() string { return "" }
func (s fileModeStat) IsDir() bool { return false }
// FS returns the underlying filesystem. Any files that should be added to the
// table should be inserted via InsertFile.
func (c *FSContext) FS() syscallfs.FS {
return c.fs
}
// OpenFile opens the file into the table and returns its file descriptor.
// The result must be closed by CloseFile or Close.
func (c *FSContext) OpenFile(path string, flag int, perm fs.FileMode) (uint32, error) {
if f, err := c.fs.OpenFile(path, flag, perm); err != nil {
return 0, err
} else {
if path == "/" || path == "." {
path = ""
}
newFD := c.openedFiles.Insert(&FileEntry{Name: path, File: f})
return newFD, nil
}
}
// LookupFile returns a file if it is in the table.
func (c *FSContext) LookupFile(fd uint32) (*FileEntry, bool) {
f, ok := c.openedFiles.Lookup(fd)
return f, ok
}
// CloseFile returns any error closing the existing file.
func (c *FSContext) CloseFile(fd uint32) error {
f, ok := c.openedFiles.Lookup(fd)
if !ok {
return syscall.EBADF
}
c.openedFiles.Delete(fd)
return f.File.Close()
}
// Close implements api.Closer
func (c *FSContext) Close(context.Context) (err error) {
// Close any files opened in this context
c.openedFiles.Range(func(fd uint32, entry *FileEntry) bool {
if e := entry.File.Close(); e != nil {
err = e // This means err returned == the last non-nil error.
}
return true
})
// A closed FSContext cannot be reused so clear the state instead of
// using Reset.
c.openedFiles = FileTable{}
return
}
// WriterForFile returns a writer for the given file descriptor or nil if not
// opened or not writeable (e.g. a directory or a file not opened for writes).
func WriterForFile(fsc *FSContext, fd uint32) (writer io.Writer) {
if f, ok := fsc.LookupFile(fd); ok {
writer = f.File.(io.Writer)
}
return
}