Files
wazero/internal/sys/fs.go
Crypt Keeper 7fec94b2e1 experimental: consolidates context patterns (#579)
This consolidates the pattern used for context overrides, notably
replacing clock overrides via experimental.WithTimeNowUnixNano
and making all context keys internal.

This also makes sure experimental example tests are handled the same
way, notably backfilling one for WithFS
2022-05-26 18:22:29 -07:00

173 lines
4.6 KiB
Go

package sys
import (
"context"
"fmt"
"io/fs"
"math"
"sync/atomic"
)
// FSKey is a context.Context Value key. It allows overriding fs.FS for WASI.
//
// See https://github.com/tetratelabs/wazero/issues/491
type FSKey struct{}
// FileEntry maps a path to an open file in a file system.
//
// Note: This does not introduce cycles because the types here are in the package "wasi" not "internalwasi".
type FileEntry struct {
Path string
FS fs.FS
// File when nil this is a mount like "." or "/".
File fs.File
}
type FSContext struct {
// openedFiles is a map of file descriptor numbers (>=3) to open files (or directories) and defaults to empty.
// TODO: This is unguarded, so not goroutine-safe!
openedFiles map[uint32]*FileEntry
// lastFD is not meant to be read directly. Rather by nextFD.
lastFD uint32
}
func NewFSContext(openedFiles map[uint32]*FileEntry) *FSContext {
var fsCtx FSContext
if openedFiles == nil {
fsCtx.openedFiles = map[uint32]*FileEntry{}
fsCtx.lastFD = 2 // STDERR
} else {
fsCtx.openedFiles = openedFiles
fsCtx.lastFD = 2 // STDERR
for fd := range openedFiles {
if fd > fsCtx.lastFD {
fsCtx.lastFD = fd
}
}
}
return &fsCtx
}
// nextFD gets the next file descriptor number in a goroutine safe way (monotonically) or zero if we ran out.
// TODO: opendFiles is still not goroutine safe!
// TODO: This can return zero if we ran out of file descriptors. A future change can optimize by re-using an FD pool.
func (c *FSContext) nextFD() uint32 {
if c.lastFD == math.MaxUint32 {
return 0
}
return atomic.AddUint32(&c.lastFD, 1)
}
// Close implements io.Closer
func (c *FSContext) Close(_ context.Context) (err error) {
// Close any files opened in this context
for fd, entry := range c.openedFiles {
delete(c.openedFiles, fd)
if entry.File != nil { // File is nil for a mount like "." or "/"
if e := entry.File.Close(); e != nil {
err = e // This means the err returned == the last non-nil error.
}
}
}
return
}
// CloseFile returns true if a file was opened and closed without error, or false if not.
func (c *FSContext) CloseFile(fd uint32) (bool, error) {
f, ok := c.openedFiles[fd]
if !ok {
return false, nil
}
delete(c.openedFiles, fd)
if f.File == nil { // TODO: currently, this means it is a pre-opened filesystem, but this may change later.
return true, nil
}
if err := f.File.Close(); err != nil {
return false, err
}
return true, nil
}
// OpenedFile returns a file and true if it was opened or nil and false, if not.
func (c *FSContext) OpenedFile(fd uint32) (*FileEntry, bool) {
f, ok := c.openedFiles[fd]
return f, ok
}
// OpenFile returns the file descriptor of the new file or false if we ran out of file descriptors
func (c *FSContext) OpenFile(f *FileEntry) (uint32, bool) {
newFD := c.nextFD()
if newFD == 0 {
return 0, false
}
c.openedFiles[newFD] = f
return newFD, true
}
type FSConfig struct {
// preopenFD has the next FD number to use
preopenFD uint32
// preopens are keyed on file descriptor and only include the Path and FS fields.
preopens map[uint32]*FileEntry
// preopenPaths allow overwriting of existing paths.
preopenPaths map[string]uint32
}
func NewFSConfig() *FSConfig {
return &FSConfig{
preopenFD: uint32(3), // after stdin/stdout/stderr
preopens: map[uint32]*FileEntry{},
preopenPaths: map[string]uint32{},
}
}
// setFS maps a path to a file-system. This is only used for base paths: "/" and ".".
func (c *FSConfig) setFS(path string, fs fs.FS) {
// Check to see if this key already exists and update it.
entry := &FileEntry{Path: path, FS: fs}
if fd, ok := c.preopenPaths[path]; ok {
c.preopens[fd] = entry
} else {
c.preopens[c.preopenFD] = entry
c.preopenPaths[path] = c.preopenFD
c.preopenFD++
}
}
func (c *FSConfig) WithFS(fs fs.FS) *FSConfig {
ret := *c // copy
ret.setFS("/", fs)
return &ret
}
func (c *FSConfig) WithWorkDirFS(fs fs.FS) *FSConfig {
ret := *c // copy
ret.setFS(".", fs)
return &ret
}
func (c *FSConfig) Preopens() (map[uint32]*FileEntry, error) {
// Ensure no-one set a nil FD. We do this here instead of at the call site to allow chaining as nil is unexpected.
rootFD := uint32(0) // zero is invalid
setWorkDirFS := false
preopens := c.preopens
for fd, entry := range preopens {
if entry.FS == nil {
return nil, fmt.Errorf("FS for %s is nil", entry.Path)
} else if entry.Path == "/" {
rootFD = fd
} else if entry.Path == "." {
setWorkDirFS = true
}
}
// Default the working directory to the root FS if it exists.
if rootFD != 0 && !setWorkDirFS {
preopens[c.preopenFD] = &FileEntry{Path: ".", FS: preopens[rootFD].FS}
}
return preopens, nil
}