wa-lang.org/wazero@v1.0.2/internal/sys/fs.go (about)

     1  package sys
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"io/fs"
     7  	"math"
     8  	"path"
     9  	"sync/atomic"
    10  	"syscall"
    11  )
    12  
    13  const (
    14  	FdStdin = iota
    15  	FdStdout
    16  	FdStderr
    17  )
    18  
    19  // FSKey is a context.Context Value key. It allows overriding fs.FS for WASI.
    20  //
    21  // See https://github.com/tetratelabs/wazero/issues/491
    22  type FSKey struct{}
    23  
    24  // EmptyFS is exported to special-case an empty file system.
    25  var EmptyFS = &emptyFS{}
    26  
    27  type emptyFS struct{}
    28  
    29  // compile-time check to ensure emptyFS implements fs.FS
    30  var _ fs.FS = &emptyFS{}
    31  
    32  // Open implements the same method as documented on fs.FS.
    33  func (f *emptyFS) Open(name string) (fs.File, error) {
    34  	if !fs.ValidPath(name) {
    35  		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
    36  	}
    37  	return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
    38  }
    39  
    40  // FileEntry maps a path to an open file in a file system.
    41  type FileEntry struct {
    42  	// Path was the argument to FSContext.OpenFile
    43  	Path string
    44  
    45  	// File when nil this is the root "/" (fd=3)
    46  	File fs.File
    47  
    48  	// ReadDir is present when this File is a fs.ReadDirFile and `ReadDir`
    49  	// was called.
    50  	ReadDir *ReadDir
    51  }
    52  
    53  // ReadDir is the status of a prior fs.ReadDirFile call.
    54  type ReadDir struct {
    55  	// CountRead is the total count of files read including Entries.
    56  	CountRead uint64
    57  
    58  	// Entries is the contents of the last fs.ReadDirFile call. Notably,
    59  	// directory listing are not rewindable, so we keep entries around in case
    60  	// the caller mis-estimated their buffer and needs a few still cached.
    61  	Entries []fs.DirEntry
    62  }
    63  
    64  type FSContext struct {
    65  	// fs is the root ("/") mount.
    66  	fs fs.FS
    67  
    68  	// openedFiles is a map of file descriptor numbers (>=3) to open files (or directories) and defaults to empty.
    69  	// TODO: This is unguarded, so not goroutine-safe!
    70  	openedFiles map[uint32]*FileEntry
    71  
    72  	// lastFD is not meant to be read directly. Rather by nextFD.
    73  	lastFD uint32
    74  }
    75  
    76  // emptyFSContext is the context associated with EmptyFS.
    77  //
    78  // Note: This is not mutable as operations functions do not affect field state.
    79  var emptyFSContext = &FSContext{
    80  	fs:          EmptyFS,
    81  	openedFiles: map[uint32]*FileEntry{},
    82  	lastFD:      2,
    83  }
    84  
    85  // NewFSContext creates a FSContext, using the `root` parameter for any paths
    86  // beginning at "/". If the input is EmptyFS, there is no root filesystem.
    87  // Otherwise, `root` is assigned file descriptor 3 and the returned context
    88  // can open files in that file system.
    89  //
    90  // Why file descriptor 3?
    91  //
    92  // While not specified, the most common WASI implementation, wasi-libc, expects
    93  // POSIX style file descriptor allocation, where the lowest available number is
    94  // used to open the next file. Since 1 and 2 are taken by stdout and stderr,
    95  // `root` is assigned 3.
    96  //   - https://github.com/WebAssembly/WASI/issues/122
    97  //   - https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_14
    98  //   - https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-16/libc-bottom-half/sources/preopens.c#L215
    99  func NewFSContext(root fs.FS) *FSContext {
   100  	if root == EmptyFS {
   101  		return emptyFSContext
   102  	}
   103  	return &FSContext{
   104  		fs: root,
   105  		openedFiles: map[uint32]*FileEntry{
   106  			3: {Path: "/"},
   107  		},
   108  		lastFD: 3,
   109  	}
   110  }
   111  
   112  // nextFD gets the next file descriptor number in a goroutine safe way (monotonically) or zero if we ran out.
   113  // TODO: openedFiles is still not goroutine safe!
   114  // TODO: This can return zero if we ran out of file descriptors. A future change can optimize by re-using an FD pool.
   115  func (c *FSContext) nextFD() uint32 {
   116  	if c.lastFD == math.MaxUint32 {
   117  		return 0
   118  	}
   119  	return atomic.AddUint32(&c.lastFD, 1)
   120  }
   121  
   122  // OpenedFile returns a file and true if it was opened or nil and false, if syscall.EBADF.
   123  func (c *FSContext) OpenedFile(_ context.Context, fd uint32) (*FileEntry, bool) {
   124  	f, ok := c.openedFiles[fd]
   125  	return f, ok
   126  }
   127  
   128  // OpenFile is like syscall.Open and returns the file descriptor of the new file or an error.
   129  //
   130  // TODO: Consider dirflags and oflags. Also, allow non-read-only open based on config about the mount.
   131  // e.g. allow os.O_RDONLY, os.O_WRONLY, or os.O_RDWR either by config flag or pattern on filename
   132  // See #390
   133  func (c *FSContext) OpenFile(_ context.Context, name string /* TODO: flags int, perm int */) (uint32, error) {
   134  	// fs.ValidFile cannot be rooted (start with '/')
   135  	fsOpenPath := name
   136  	if name[0] == '/' {
   137  		fsOpenPath = name[1:]
   138  	}
   139  	fsOpenPath = path.Clean(fsOpenPath) // e.g. "sub/." -> "sub"
   140  
   141  	f, err := c.fs.Open(fsOpenPath)
   142  	if err != nil {
   143  		return 0, err
   144  	}
   145  
   146  	newFD := c.nextFD()
   147  	if newFD == 0 { // TODO: out of file descriptors
   148  		_ = f.Close()
   149  		return 0, syscall.EBADF
   150  	}
   151  	c.openedFiles[newFD] = &FileEntry{Path: name, File: f}
   152  	return newFD, nil
   153  }
   154  
   155  // CloseFile returns true if a file was opened and closed without error, or false if syscall.EBADF.
   156  func (c *FSContext) CloseFile(_ context.Context, fd uint32) bool {
   157  	f, ok := c.openedFiles[fd]
   158  	if !ok {
   159  		return false
   160  	}
   161  	delete(c.openedFiles, fd)
   162  
   163  	if f.File == nil { // The root entry
   164  		return true
   165  	}
   166  	if err := f.File.Close(); err != nil {
   167  		return false
   168  	}
   169  	return true
   170  }
   171  
   172  // Close implements io.Closer
   173  func (c *FSContext) Close(context.Context) (err error) {
   174  	// Close any files opened in this context
   175  	for fd, entry := range c.openedFiles {
   176  		delete(c.openedFiles, fd)
   177  		if entry.File != nil { // File is nil for the root filesystem
   178  			if e := entry.File.Close(); e != nil {
   179  				err = e // This means err returned == the last non-nil error.
   180  			}
   181  		}
   182  	}
   183  	return
   184  }
   185  
   186  // FdWriter returns a valid writer for the given file descriptor or nil if syscall.EBADF.
   187  func FdWriter(ctx context.Context, sysCtx *Context, fd uint32) io.Writer {
   188  	switch fd {
   189  	case FdStdout:
   190  		return sysCtx.Stdout()
   191  	case FdStderr:
   192  		return sysCtx.Stderr()
   193  	default:
   194  		// Check to see if the file descriptor is available
   195  		if f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok || f.File == nil {
   196  			return nil
   197  		} else if writer, ok := f.File.(io.Writer); !ok {
   198  			// Go's syscall.Write also returns EBADF if the FD is present, but not writeable
   199  			return nil
   200  		} else {
   201  			return writer
   202  		}
   203  	}
   204  }
   205  
   206  // FdReader returns a valid reader for the given file descriptor or nil if syscall.EBADF.
   207  func FdReader(ctx context.Context, sysCtx *Context, fd uint32) io.Reader {
   208  	if fd == FdStdin {
   209  		return sysCtx.Stdin()
   210  	} else if f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok {
   211  		return nil
   212  	} else {
   213  		return f.File
   214  	}
   215  }