wa-lang.org/wazero@v1.0.2/internal/gojs/fs.go (about) 1 package gojs 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "io/fs" 8 "os" 9 "syscall" 10 11 "wa-lang.org/wazero/api" 12 "wa-lang.org/wazero/internal/wasm" 13 ) 14 15 var ( 16 // jsfs = js.Global().Get("fs") // fs_js.go init 17 // 18 // js.fsCall conventions: 19 // * funcWrapper callback is the last parameter 20 // * arg0 is error and up to one result in arg1 21 jsfs = newJsVal(refJsfs, "fs"). 22 addProperties(map[string]interface{}{ 23 "constants": jsfsConstants, // = jsfs.Get("constants") // init 24 }). 25 addFunction("open", &jsfsOpen{}). 26 addFunction("stat", &jsfsStat{}). 27 addFunction("fstat", &jsfsFstat{}). 28 addFunction("lstat", &jsfsStat{}). // because fs.FS doesn't support symlink 29 addFunction("close", &jsfsClose{}). 30 addFunction("read", &jsfsRead{}). 31 addFunction("write", &jsfsWrite{}). 32 addFunction("readdir", &jsfsReaddir{}) 33 34 // TODO: stub all these with syscall.ENOSYS 35 // * _, err := fsCall("mkdir", path, perm) // syscall.Mkdir 36 // * _, err := fsCall("unlink", path) // syscall.Unlink 37 // * _, err := fsCall("rmdir", path) // syscall.Rmdir 38 // * _, err := fsCall("chmod", path, mode) // syscall.Chmod 39 // * _, err := fsCall("fchmod", fd, mode) // syscall.Fchmod 40 // * _, err := fsCall("chown", path, uint32(uid), uint32(gid)) // syscall.Chown 41 // * _, err := fsCall("fchown", fd, uint32(uid), uint32(gid)) // syscall.Fchown 42 // * _, err := fsCall("lchown", path, uint32(uid), uint32(gid)) // syscall.Lchown 43 // * _, err := fsCall("utimes", path, atime, mtime) // syscall.UtimesNano 44 // * _, err := fsCall("rename", from, to) // syscall.Rename 45 // * _, err := fsCall("truncate", path, length) // syscall.Truncate 46 // * _, err := fsCall("ftruncate", fd, length) // syscall.Ftruncate 47 // * dst, err := fsCall("readlink", path) // syscall.Readlink 48 // * _, err := fsCall("link", path, link) // syscall.Link 49 // * _, err := fsCall("symlink", path, link) // syscall.Symlink 50 // * _, err := fsCall("fsync", fd) // syscall.Fsync 51 52 // jsfsConstants = jsfs Get("constants") // fs_js.go init 53 jsfsConstants = newJsVal(refJsfsConstants, "constants"). 54 addProperties(map[string]interface{}{ 55 "O_WRONLY": oWRONLY, 56 "O_RDWR": oRDWR, 57 "O_CREAT": oCREAT, 58 "O_TRUNC": oTRUNC, 59 "O_APPEND": oAPPEND, 60 "O_EXCL": oEXCL, 61 }) 62 63 // oWRONLY = jsfsConstants Get("O_WRONLY").Int() // fs_js.go init 64 oWRONLY = api.EncodeF64(float64(os.O_WRONLY)) 65 66 // oRDWR = jsfsConstants Get("O_RDWR").Int() // fs_js.go init 67 oRDWR = api.EncodeF64(float64(os.O_RDWR)) 68 69 // o CREAT = jsfsConstants Get("O_CREAT").Int() // fs_js.go init 70 oCREAT = api.EncodeF64(float64(os.O_CREATE)) 71 72 // oTRUNC = jsfsConstants Get("O_TRUNC").Int() // fs_js.go init 73 oTRUNC = api.EncodeF64(float64(os.O_TRUNC)) 74 75 // oAPPEND = jsfsConstants Get("O_APPEND").Int() // fs_js.go init 76 oAPPEND = api.EncodeF64(float64(os.O_APPEND)) 77 78 // oEXCL = jsfsConstants Get("O_EXCL").Int() // fs_js.go init 79 oEXCL = api.EncodeF64(float64(os.O_EXCL)) 80 ) 81 82 // jsfsOpen implements fs.Open 83 // 84 // jsFD /* Int */, err := fsCall("open", path, flags, perm) 85 type jsfsOpen struct{} 86 87 // invoke implements jsFn.invoke 88 func (*jsfsOpen) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) { 89 name := args[0].(string) 90 flags := toUint32(args[1]) // flags are derived from constants like oWRONLY 91 perm := toUint32(args[2]) 92 callback := args[3].(funcWrapper) 93 94 fd, err := syscallOpen(ctx, mod, name, flags, perm) 95 return callback.invoke(ctx, mod, refJsfs, err, fd) // note: error first 96 } 97 98 // jsfsStat is used for syscall.Stat 99 // 100 // jsSt, err := fsCall("stat", path) 101 type jsfsStat struct{} 102 103 // invoke implements jsFn.invoke 104 func (*jsfsStat) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) { 105 name := args[0].(string) 106 callback := args[1].(funcWrapper) 107 108 stat, err := syscallStat(ctx, mod, name) 109 return callback.invoke(ctx, mod, refJsfs, err, stat) // note: error first 110 } 111 112 // syscallStat is like syscall.Stat 113 func syscallStat(ctx context.Context, mod api.Module, name string) (*jsSt, error) { 114 fsc := mod.(*wasm.CallContext).Sys.FS(ctx) 115 if fd, err := fsc.OpenFile(ctx, name); err != nil { 116 return nil, err 117 } else { 118 defer fsc.CloseFile(ctx, fd) 119 return syscallFstat(ctx, mod, fd) 120 } 121 } 122 123 // jsfsStat is used for syscall.Open 124 // 125 // stat, err := fsCall("fstat", fd); err == nil && stat.Call("isDirectory").Bool() 126 type jsfsFstat struct{} 127 128 // invoke implements jsFn.invoke 129 func (*jsfsFstat) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) { 130 fd := toUint32(args[0]) 131 callback := args[1].(funcWrapper) 132 133 fstat, err := syscallFstat(ctx, mod, fd) 134 return callback.invoke(ctx, mod, refJsfs, err, fstat) // note: error first 135 } 136 137 // syscallFstat is like syscall.Fstat 138 func syscallFstat(ctx context.Context, mod api.Module, fd uint32) (*jsSt, error) { 139 fsc := mod.(*wasm.CallContext).Sys.FS(ctx) 140 if f, ok := fsc.OpenedFile(ctx, fd); !ok { 141 return nil, syscall.EBADF 142 } else if stat, err := f.File.Stat(); err != nil { 143 return nil, err 144 } else { 145 ret := &jsSt{} 146 ret.isDir = stat.IsDir() 147 // TODO ret.dev=stat.Sys 148 ret.mode = uint32(stat.Mode()) 149 ret.size = uint32(stat.Size()) 150 ret.mtimeMs = uint32(stat.ModTime().UnixMilli()) 151 return ret, nil 152 } 153 } 154 155 // jsfsClose is used for syscall.Close 156 // 157 // _, err := fsCall("close", fd) 158 type jsfsClose struct{} 159 160 // invoke implements jsFn.invoke 161 func (*jsfsClose) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) { 162 fd := toUint32(args[0]) 163 callback := args[1].(funcWrapper) 164 165 err := syscallClose(ctx, mod, fd) 166 return callback.invoke(ctx, mod, refJsfs, err, true) // note: error first 167 } 168 169 // syscallClose is like syscall.Close 170 func syscallClose(ctx context.Context, mod api.Module, fd uint32) (err error) { 171 fsc := mod.(*wasm.CallContext).Sys.FS(ctx) 172 if ok := fsc.CloseFile(ctx, fd); !ok { 173 err = syscall.EBADF // already closed 174 } 175 return 176 } 177 178 // jsfsRead is used in syscall.Read and syscall.Pread, called by 179 // src/internal/poll/fd_unix.go poll.Read. 180 // 181 // n, err := fsCall("read", fd, buf, 0, len(b), nil) 182 type jsfsRead struct{} 183 184 // invoke implements jsFn.invoke 185 func (*jsfsRead) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) { 186 fd := toUint32(args[0]) 187 buf, ok := args[1].(*byteArray) 188 if !ok { 189 return nil, fmt.Errorf("arg[1] is %v not a []byte", args[1]) 190 } 191 offset := toUint32(args[2]) 192 byteCount := toUint32(args[3]) 193 fOffset := args[4] // nil unless Pread 194 callback := args[5].(funcWrapper) 195 196 n, err := syscallRead(ctx, mod, fd, fOffset, buf.slice[offset:offset+byteCount]) 197 return callback.invoke(ctx, mod, refJsfs, err, n) // note: error first 198 } 199 200 // syscallRead is like syscall.Read 201 func syscallRead(ctx context.Context, mod api.Module, fd uint32, offset interface{}, p []byte) (n uint32, err error) { 202 r := fdReader(ctx, mod, fd) 203 if r == nil { 204 err = syscall.EBADF 205 } 206 207 if offset != nil { 208 if s, ok := r.(io.Seeker); ok { 209 if _, err := s.Seek(toInt64(offset), io.SeekStart); err != nil { 210 return 0, err 211 } 212 } else { 213 return 0, syscall.ENOTSUP 214 } 215 } 216 217 if nRead, e := r.Read(p); e == nil || e == io.EOF { 218 // fs_js.go cannot parse io.EOF so coerce it to nil. 219 // See https://github.com/golang/go/issues/43913 220 n = uint32(nRead) 221 } else { 222 err = e 223 } 224 return 225 } 226 227 // jsfsWrite is used in syscall.Write and syscall.Pwrite. 228 // 229 // Notably, offset is non-nil in Pwrite. 230 // 231 // n, err := fsCall("write", fd, buf, 0, len(b), nil) 232 type jsfsWrite struct{} 233 234 // invoke implements jsFn.invoke 235 func (*jsfsWrite) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) { 236 fd := toUint32(args[0]) 237 buf, ok := args[1].(*byteArray) 238 if !ok { 239 return nil, fmt.Errorf("arg[1] is %v not a []byte", args[1]) 240 } 241 offset := toUint32(args[2]) 242 byteCount := toUint32(args[3]) 243 fOffset := args[4] // nil unless Pread 244 callback := args[5].(funcWrapper) 245 246 if byteCount > 0 { // empty is possible on EOF 247 n, err := syscallWrite(ctx, mod, fd, fOffset, buf.slice[offset:offset+byteCount]) 248 return callback.invoke(ctx, mod, refJsfs, err, n) // note: error first 249 } 250 return callback.invoke(ctx, mod, refJsfs, nil, refValueZero) 251 } 252 253 // syscallWrite is like syscall.Write 254 func syscallWrite(ctx context.Context, mod api.Module, fd uint32, offset interface{}, p []byte) (n uint32, err error) { 255 if writer := fdWriter(ctx, mod, fd); writer == nil { 256 err = syscall.EBADF 257 } else if nWritten, e := writer.Write(p); e == nil || e == io.EOF { 258 // fs_js.go cannot parse io.EOF so coerce it to nil. 259 // See https://github.com/golang/go/issues/43913 260 n = uint32(nWritten) 261 } else { 262 err = e 263 } 264 return 265 } 266 267 // jsfsReaddir is used in syscall.Open 268 // 269 // dir, err := fsCall("readdir", path) 270 // dir.Length(), dir.Index(i).String() 271 type jsfsReaddir struct{} 272 273 // invoke implements jsFn.invoke 274 func (*jsfsReaddir) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) { 275 name := args[0].(string) 276 callback := args[1].(funcWrapper) 277 278 stat, err := syscallReaddir(ctx, mod, name) 279 return callback.invoke(ctx, mod, refJsfs, err, stat) // note: error first 280 } 281 282 func syscallReaddir(ctx context.Context, mod api.Module, name string) (*objectArray, error) { 283 fsc := mod.(*wasm.CallContext).Sys.FS(ctx) 284 fd, err := fsc.OpenFile(ctx, name) 285 if err != nil { 286 return nil, err 287 } 288 defer fsc.CloseFile(ctx, fd) 289 290 if f, ok := fsc.OpenedFile(ctx, fd); !ok { 291 return nil, syscall.EBADF 292 } else if d, ok := f.File.(fs.ReadDirFile); !ok { 293 return nil, syscall.ENOTDIR 294 } else if l, err := d.ReadDir(-1); err != nil { 295 return nil, err 296 } else { 297 entries := make([]interface{}, 0, len(l)) 298 for _, e := range l { 299 entries = append(entries, e.Name()) 300 } 301 return &objectArray{entries}, nil 302 } 303 } 304 305 type returnZero struct{} 306 307 // invoke implements jsFn.invoke 308 func (*returnZero) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) { 309 return refValueZero, nil 310 } 311 312 type returnSliceOfZero struct{} 313 314 // invoke implements jsFn.invoke 315 func (*returnSliceOfZero) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) { 316 return &objectArray{slice: []interface{}{refValueZero}}, nil 317 } 318 319 type returnArg0 struct{} 320 321 // invoke implements jsFn.invoke 322 func (*returnArg0) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) { 323 return args[0], nil 324 } 325 326 // cwd for fs.Open syscall.Getcwd in fs_js.go 327 type cwd struct{} 328 329 // invoke implements jsFn.invoke 330 func (*cwd) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) { 331 return getState(ctx).cwd, nil 332 } 333 334 // chdir for fs.Open syscall.Chdir in fs_js.go 335 type chdir struct{} 336 337 // invoke implements jsFn.invoke 338 func (*chdir) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) { 339 path := args[0].(string) 340 341 // TODO: refactor so that sys has path-based ops, also needed in WASI. 342 fsc := mod.(*wasm.CallContext).Sys.FS(ctx) 343 if fd, err := fsc.OpenFile(ctx, path); err != nil { 344 return nil, syscall.ENOENT 345 } else if f, ok := fsc.OpenedFile(ctx, fd); !ok { 346 return nil, syscall.ENOENT 347 } else if s, err := f.File.Stat(); err != nil { 348 fsc.CloseFile(ctx, fd) 349 return nil, syscall.ENOENT 350 } else if !s.IsDir() { 351 fsc.CloseFile(ctx, fd) 352 return nil, syscall.ENOTDIR 353 } else { 354 getState(ctx).cwd = path 355 return nil, nil 356 } 357 } 358 359 // jsSt is pre-parsed from fs_js.go setStat to avoid thrashing 360 type jsSt struct { 361 isDir bool 362 363 dev uint32 364 ino uint32 365 mode uint32 366 nlink uint32 367 uid uint32 368 gid uint32 369 rdev uint32 370 size uint32 371 blksize uint32 372 blocks uint32 373 atimeMs uint32 374 mtimeMs uint32 375 ctimeMs uint32 376 } 377 378 // get implements jsGet.get 379 func (s *jsSt) get(_ context.Context, propertyKey string) interface{} { 380 switch propertyKey { 381 case "dev": 382 return s.dev 383 case "ino": 384 return s.ino 385 case "mode": 386 return s.mode 387 case "nlink": 388 return s.nlink 389 case "uid": 390 return s.uid 391 case "gid": 392 return s.gid 393 case "rdev": 394 return s.rdev 395 case "size": 396 return s.size 397 case "blksize": 398 return s.blksize 399 case "blocks": 400 return s.blocks 401 case "atimeMs": 402 return s.atimeMs 403 case "mtimeMs": 404 return s.mtimeMs 405 case "ctimeMs": 406 return s.ctimeMs 407 } 408 panic(fmt.Sprintf("TODO: stat.%s", propertyKey)) 409 } 410 411 // call implements jsCall.call 412 func (s *jsSt) call(ctx context.Context, mod api.Module, this ref, method string, args ...interface{}) (interface{}, error) { 413 if method == "isDirectory" { 414 return s.isDir, nil 415 } 416 panic(fmt.Sprintf("TODO: stat.%s", method)) 417 }