github.com/taubyte/vm-wasm-utils@v1.0.2/sys/fs.go (about)

     1  package sys
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"io/fs"
     7  	"math"
     8  	"sync/atomic"
     9  	"syscall"
    10  )
    11  
    12  const (
    13  	FdStdin = iota
    14  	FdStdout
    15  	FdStderr
    16  )
    17  
    18  // FSKey is a context.Context Value key. It allows overriding fs.FS for WASI.
    19  //
    20  // See https://github.com/tetratelabs/wazero/issues/491
    21  type FSKey struct{}
    22  
    23  // EmptyFS is exported to special-case an empty file system.
    24  var EmptyFS = &emptyFS{}
    25  
    26  type emptyFS struct{}
    27  
    28  // compile-time check to ensure emptyFS implements fs.FS
    29  var _ fs.FS = &emptyFS{}
    30  
    31  // Open implements the same method as documented on fs.FS.
    32  func (f *emptyFS) Open(name string) (fs.File, error) {
    33  	if !fs.ValidPath(name) {
    34  		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
    35  	}
    36  	return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
    37  }
    38  
    39  // FileEntry maps a path to an open file in a file system.
    40  type FileEntry struct {
    41  	Path string
    42  	// File when nil this is the root "/" (fd=3)
    43  	File fs.File
    44  }
    45  
    46  type FSContext struct {
    47  	// fs is the root ("/") mount.
    48  	fs fs.FS
    49  
    50  	// openedFiles is a map of file descriptor numbers (>=3) to open files (or directories) and defaults to empty.
    51  	// TODO: This is unguarded, so not goroutine-safe!
    52  	openedFiles map[uint32]*FileEntry
    53  
    54  	// lastFD is not meant to be read directly. Rather by nextFD.
    55  	lastFD uint32
    56  }
    57  
    58  // emptyFSContext is the context associated with EmptyFS.
    59  //
    60  // Note: This is not mutable as operations functions do not affect field state.
    61  var emptyFSContext = &FSContext{
    62  	fs:          EmptyFS,
    63  	openedFiles: map[uint32]*FileEntry{},
    64  	lastFD:      2,
    65  }
    66  
    67  // NewFSContext returns a mutable context if the fs is not EmptyFS.
    68  func NewFSContext(fs fs.FS) *FSContext {
    69  	if fs == EmptyFS {
    70  		return emptyFSContext
    71  	}
    72  	return &FSContext{
    73  		fs: fs,
    74  		openedFiles: map[uint32]*FileEntry{
    75  			3: {Path: "/"}, // after STDERR
    76  		},
    77  		lastFD: 3,
    78  	}
    79  }
    80  
    81  // nextFD gets the next file descriptor number in a goroutine safe way (monotonically) or zero if we ran out.
    82  // TODO: openedFiles is still not goroutine safe!
    83  // TODO: This can return zero if we ran out of file descriptors. A future change can optimize by re-using an FD pool.
    84  func (c *FSContext) nextFD() uint32 {
    85  	if c.lastFD == math.MaxUint32 {
    86  		return 0
    87  	}
    88  	return atomic.AddUint32(&c.lastFD, 1)
    89  }
    90  
    91  // OpenedFile returns a file and true if it was opened or nil and false, if syscall.EBADF.
    92  func (c *FSContext) OpenedFile(_ context.Context, fd uint32) (*FileEntry, bool) {
    93  	f, ok := c.openedFiles[fd]
    94  	return f, ok
    95  }
    96  
    97  // OpenFile is like syscall.Open and returns the file descriptor of the new file or an error.
    98  //
    99  // TODO: Consider dirflags and oflags. Also, allow non-read-only open based on config about the mount.
   100  // Ex. allow os.O_RDONLY, os.O_WRONLY, or os.O_RDWR either by config flag or pattern on filename
   101  // See #390
   102  func (c *FSContext) OpenFile(_ context.Context, name string /* TODO: flags int, perm int */) (uint32, error) {
   103  	// fs.ValidFile cannot start with '/'
   104  	fsOpenPath := name
   105  	if name[0] == '/' {
   106  		fsOpenPath = name[1:]
   107  	}
   108  
   109  	f, err := c.fs.Open(fsOpenPath)
   110  	if err != nil {
   111  		return 0, err // Don't wrap the underlying error which is already a PathError!
   112  	}
   113  
   114  	newFD := c.nextFD()
   115  	if newFD == 0 {
   116  		_ = f.Close()
   117  		return 0, syscall.EBADF
   118  	}
   119  	c.openedFiles[newFD] = &FileEntry{Path: name, File: f}
   120  	return newFD, nil
   121  }
   122  
   123  // CloseFile returns true if a file was opened and closed without error, or false if syscall.EBADF.
   124  func (c *FSContext) CloseFile(_ context.Context, fd uint32) bool {
   125  	f, ok := c.openedFiles[fd]
   126  	if !ok {
   127  		return false
   128  	}
   129  	delete(c.openedFiles, fd)
   130  
   131  	if f.File == nil { // The root entry
   132  		return true
   133  	}
   134  	if err := f.File.Close(); err != nil {
   135  		return false
   136  	}
   137  	return true
   138  }
   139  
   140  // Close implements io.Closer
   141  func (c *FSContext) Close(_ context.Context) (err error) {
   142  	// Close any files opened in this context
   143  	for fd, entry := range c.openedFiles {
   144  		delete(c.openedFiles, fd)
   145  		if entry.File != nil { // File is nil for the root filesystem
   146  			if e := entry.File.Close(); e != nil {
   147  				err = e // This means the err returned == the last non-nil error.
   148  			}
   149  		}
   150  	}
   151  	return
   152  }
   153  
   154  // FdWriter returns a valid writer for the given file descriptor or nil if syscall.EBADF.
   155  func FdWriter(ctx context.Context, sysCtx *Context, fd uint32) io.Writer {
   156  	switch fd {
   157  	case FdStdout:
   158  		return sysCtx.Stdout()
   159  	case FdStderr:
   160  		return sysCtx.Stderr()
   161  	default:
   162  		// Check to see if the file descriptor is available
   163  		if f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok || f.File == nil {
   164  			return nil
   165  		} else if writer, ok := f.File.(io.Writer); !ok {
   166  			// Go's syscall.Write also returns EBADF if the FD is present, but not writeable
   167  			return nil
   168  		} else {
   169  			return writer
   170  		}
   171  	}
   172  }
   173  
   174  // FdReader returns a valid reader for the given file descriptor or nil if syscall.EBADF.
   175  func FdReader(ctx context.Context, sysCtx *Context, fd uint32) io.Reader {
   176  	if fd == FdStdin {
   177  		return sysCtx.Stdin()
   178  	} else if f, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd); !ok {
   179  		return nil
   180  	} else {
   181  		return f.File
   182  	}
   183  }