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 }