See https://github.com/WebAssembly/wasi-testsuite/issues/50 Signed-off-by: Adrian Cole <adrian@tetrate.io>
385 lines
9.9 KiB
Go
385 lines
9.9 KiB
Go
package sys
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/tetratelabs/wazero/internal/platform"
|
|
"github.com/tetratelabs/wazero/internal/sysfs"
|
|
)
|
|
|
|
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 sysfs.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, or the pre-open
|
|
// name itself when IsPreopen.
|
|
//
|
|
// Note: This can drift on rename.
|
|
Name string
|
|
|
|
// IsPreopen is a directory that is lazily opened.
|
|
IsPreopen bool
|
|
|
|
// FS is the filesystem associated with the pre-open.
|
|
FS sysfs.FS
|
|
|
|
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
|
|
|
|
openPath string
|
|
openFlag int
|
|
openPerm fs.FileMode
|
|
}
|
|
|
|
// IsDir returns true if the file is a directory.
|
|
func (f *FileEntry) IsDir() bool {
|
|
if 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 {
|
|
// rootFS is the root ("/") mount.
|
|
rootFS sysfs.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 sysfs.UnimplementedFS, it is inserted into
|
|
// the file descriptor table as FdPreopen.
|
|
func NewFSContext(stdin io.Reader, stdout, stderr io.Writer, rootFS sysfs.FS) (fsc *FSContext, err error) {
|
|
fsc = &FSContext{rootFS: rootFS}
|
|
fsc.openedFiles.Insert(stdinReader(stdin))
|
|
fsc.openedFiles.Insert(stdioWriter(stdout, noopStdoutStat))
|
|
fsc.openedFiles.Insert(stdioWriter(stderr, noopStderrStat))
|
|
|
|
if _, ok := rootFS.(sysfs.UnimplementedFS); ok {
|
|
return fsc, nil
|
|
}
|
|
|
|
if comp, ok := rootFS.(*sysfs.CompositeFS); ok {
|
|
preopens := comp.FS()
|
|
for i, p := range comp.GuestPaths() {
|
|
fsc.openedFiles.Insert(&FileEntry{
|
|
FS: preopens[i],
|
|
Name: p,
|
|
IsPreopen: true,
|
|
isDirectory: true,
|
|
File: &lazyDir{fs: rootFS},
|
|
})
|
|
}
|
|
} else {
|
|
fsc.openedFiles.Insert(&FileEntry{
|
|
FS: rootFS,
|
|
Name: "/",
|
|
IsPreopen: true,
|
|
isDirectory: true,
|
|
File: &lazyDir{fs: rootFS},
|
|
})
|
|
}
|
|
|
|
return fsc, nil
|
|
}
|
|
|
|
func stdinReader(r io.Reader) *FileEntry {
|
|
if r == nil {
|
|
r = eofReader{}
|
|
}
|
|
s := stdioStat(r, noopStdinStat)
|
|
return &FileEntry{Name: noopStdinStat.Name(), 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{Name: s.Name(), 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 }
|
|
|
|
// RootFS returns the underlying filesystem. Any files that should be added to
|
|
// the table should be inserted via InsertFile.
|
|
func (c *FSContext) RootFS() sysfs.FS {
|
|
return c.rootFS
|
|
}
|
|
|
|
// 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(fs sysfs.FS, path string, flag int, perm fs.FileMode) (uint32, error) {
|
|
if f, err := fs.OpenFile(path, flag, perm); err != nil {
|
|
return 0, err
|
|
} else {
|
|
fe := &FileEntry{openPath: path, FS: fs, File: f, openFlag: flag, openPerm: perm}
|
|
if path == "/" || path == "." {
|
|
fe.Name = ""
|
|
} else {
|
|
fe.Name = path
|
|
}
|
|
newFD := c.openedFiles.Insert(fe)
|
|
return newFD, nil
|
|
}
|
|
}
|
|
|
|
// ReOpenDir re-opens the directory while keeping the same file descriptor.
|
|
// TODO: this might not be necessary once we have our own File type.
|
|
func (c *FSContext) ReOpenDir(fd uint32) (*FileEntry, error) {
|
|
f, ok := c.openedFiles.Lookup(fd)
|
|
if !ok {
|
|
return nil, syscall.EBADF
|
|
} else if !f.IsDir() {
|
|
return nil, syscall.EISDIR
|
|
}
|
|
|
|
if err := f.File.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Re-opens with the same parameters as before.
|
|
opened, err := f.FS.OpenFile(f.openPath, f.openFlag, f.openPerm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Reset the state.
|
|
f.File = opened
|
|
f.ReadDir.CountRead, f.ReadDir.Entries = 0, nil
|
|
return f, 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
|
|
} else if f.IsPreopen {
|
|
// WASI is the only user of pre-opens and wasi-testsuite disallows this
|
|
// See https://github.com/WebAssembly/wasi-testsuite/issues/50
|
|
return syscall.ENOTSUP
|
|
}
|
|
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
|
|
}
|