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 }