// https://dev.to/ndesmic/building-a-minimal-wasi-polyfill-for-browsers-4nel // http://www.wasmtutor.com/webassembly-barebones-wasi // https://github.com/emscripten-core/emscripten/blob/c017fc2d6961962ee87ae387462a099242dfbbd2/src/library_wasi.js#L451 // https://github.com/emscripten-core/emscripten/blob/c017fc2d6961962ee87ae387462a099242dfbbd2/src/library_fs.js // https://github.com/WebAssembly/wasi-libc/blob/main/libc-bottom-half/sources/preopens.c // https://fossies.org/linux/wasm3/source/extra/wasi_core.h // https://wasix.org/docs/api-reference/wasi/fd_read const use_debug = true; const debug = use_debug ? console.log : () => { }; export enum FDType { UNKNOWN = 0, BLOCK_DEVICE = 1, CHARACTER_DEVICE = 2, DIRECTORY = 3, REGULAR_FILE = 4, SOCKET_DGRAM = 5, SOCKET_STREAM = 6, SYMBOLIC_LINK = 7, } export enum FDRights { FD_DATASYNC = 1, FD_READ = 2, FD_SEEK = 4, FD_FDSTAT_SET_FLAGS = 8, FD_SYNC = 16, FD_TELL = 32, FD_WRITE = 64, FD_ADVISE = 128, FD_ALLOCATE = 256, PATH_CREATE_DIRECTORY = 512, PATH_CREATE_FILE = 1024, PATH_LINK_SOURCE = 2048, PATH_LINK_TARGET = 4096, PATH_OPEN = 8192, FD_READDIR = 16384, PATH_READLINK = 32768, PATH_RENAME_SOURCE = 65536, PATH_RENAME_TARGET = 131072, PATH_FILESTAT_GET = 262144, PATH_FILESTAT_SET_SIZE = 524288, PATH_FILESTAT_SET_TIMES = 1048576, FD_FILESTAT_GET = 2097152, FD_FILESTAT_SET_SIZE = 4194304, FD_FILESTAT_SET_TIMES = 8388608, PATH_SYMLINK = 16777216, PATH_REMOVE_DIRECTORY = 33554432, PATH_UNLINK_FILE = 67108864, POLL_FD_READWRITE = 134217728, SOCK_SHUTDOWN = 268435456, FD_ALL = 536870911, // TODO? } export enum FDFlags { APPEND = 1, DSYNC = 2, NONBLOCK = 4, RSYNC = 8, SYNC = 16, } export enum FDOpenFlags { CREAT = 1, DIRECTORY = 2, EXCL = 4, TRUNC = 8, } export enum WASIErrors { SUCCESS = 0, TOOBIG = 1, ACCES = 2, ADDRINUSE = 3, ADDRNOTAVAIL = 4, AFNOSUPPORT = 5, AGAIN = 6, ALREADY = 7, BADF = 8, BADMSG = 9, BUSY = 10, CANCELED = 11, CHILD = 12, CONNABORTED = 13, CONNREFUSED = 14, CONNRESET = 15, DEADLK = 16, DESTADDRREQ = 17, DOM = 18, DQUOT = 19, EXIST = 20, FAULT = 21, FBIG = 22, HOSTUNREACH = 23, IDRM = 24, ILSEQ = 25, INPROGRESS = 26, INTR = 27, INVAL = 28, IO = 29, ISCONN = 30, ISDIR = 31, LOOP = 32, MFILE = 33, MLINK = 34, MSGSIZE = 35, MULTIHOP = 36, NAMETOOLONG = 37, NETDOWN = 38, NETRESET = 39, NETUNREACH = 40, NFILE = 41, NOBUFS = 42, NODEV = 43, NOENT = 44, NOEXEC = 45, NOLCK = 46, NOLINK = 47, NOMEM = 48, NOMSG = 49, NOPROTOOPT = 50, NOSPC = 51, NOSYS = 52, NOTCONN = 53, NOTDIR = 54, NOTEMPTY = 55, NOTRECOVERABLE = 56, NOTSOCK = 57, NOTSUP = 58, NOTTY = 59, NXIO = 60, OVERFLOW = 61, OWNERDEAD = 62, PERM = 63, PIPE = 64, PROTO = 65, PROTONOSUPPORT = 66, PROTOTYPE = 67, RANGE = 68, ROFS = 69, SPIPE = 70, SRCH = 71, STALE = 72, TIMEDOUT = 73, TXTBSY = 74, XDEV = 75, NOTCAPABLE = 76, } export class WASIFileDescriptor { fdindex: number = -1; data: Uint8Array = new Uint8Array(16); flags: number = 0; size: number = 0; offset: number = 0; constructor(public name: string, public type: FDType, public rights: number) { this.rights = -1; // TODO? } ensureCapacity(size: number) { if (this.data.byteLength < size) { const newdata = new Uint8Array(size * 2); // TODO? newdata.set(this.data); this.data = newdata; } } write(chunk: Uint8Array) { this.ensureCapacity(this.offset + chunk.byteLength); this.data.set(chunk, this.offset); this.offset += chunk.byteLength; this.size = Math.max(this.size, this.offset); } read(chunk: Uint8Array) { const len = Math.min(chunk.byteLength, this.size - this.offset); chunk.set(this.data.subarray(this.offset, this.offset + len)); this.offset += len; return len; } truncate() { this.size = 0; this.offset = 0; } llseek(offset: number, whence: number) { switch (whence) { case 0: // SEEK_SET this.offset = offset; break; case 1: // SEEK_CUR this.offset += offset; break; case 2: // SEEK_END this.offset = this.size + offset; break; } if (this.offset < 0) this.offset = 0; if (this.offset > this.size) this.offset = this.size; } getBytes() { return this.data.subarray(0, this.size); } toString() { return `FD(${this.fdindex} "${this.name}" 0x${this.type.toString(16)} 0x${this.rights.toString(16)} ${this.offset}/${this.size}/${this.data.byteLength})`; } } class WASIStreamingFileDescriptor extends WASIFileDescriptor { constructor(fdindex: number, name: string, type: FDType, rights: number, private stream: NodeJS.WritableStream) { super(name, type, rights); this.fdindex = fdindex; } write(chunk: Uint8Array) { this.stream.write(chunk); } } export class WASIFilesystem { private files: Map = new Map(); private dirs: Map = new Map(); constructor() { this.putDirectory("/"); } putDirectory(name: string, rights?: number) { if (!rights) rights = FDRights.PATH_OPEN | FDRights.PATH_CREATE_DIRECTORY | FDRights.PATH_CREATE_FILE; const dir = new WASIFileDescriptor(name, FDType.DIRECTORY, rights); this.dirs.set(name, dir); return dir; } putFile(name: string, data: string | Uint8Array, rights?: number) { if (typeof data === 'string') { data = new TextEncoder().encode(data); } if (!rights) rights = FDRights.FD_READ | FDRights.FD_WRITE; const file = new WASIFileDescriptor(name, FDType.REGULAR_FILE, rights); file.write(data); file.offset = 0; this.files.set(name, file); return file; } getFile(name: string) { return this.files.get(name); } } export class WASIRunner { #compiled: WebAssembly.Module; #instance; // TODO #memarr8: Uint8Array; #memarr32: Int32Array; #args: Uint8Array[] = []; stdin = new WASIStreamingFileDescriptor(0, '', FDType.CHARACTER_DEVICE, FDRights.FD_READ, process.stdin); stdout = new WASIStreamingFileDescriptor(1, '', FDType.CHARACTER_DEVICE, FDRights.FD_WRITE, process.stdout); stderr = new WASIStreamingFileDescriptor(2, '', FDType.CHARACTER_DEVICE, FDRights.FD_WRITE, process.stderr); fds: WASIFileDescriptor[] = [this.stdin, this.stdout, this.stderr]; exited = false; errno = -1; fs = new WASIFilesystem(); async loadAsync(wasmSource: Uint8Array) { this.#compiled = await WebAssembly.compile(wasmSource); this.#instance = await WebAssembly.instantiate(this.#compiled, this.getImportObject()); } setArgs(args: string[]) { this.#args = args.map(arg => new TextEncoder().encode(arg + '\0')); } addPreopenDirectory(name: string) { return this.openFile(name, FDOpenFlags.DIRECTORY | FDOpenFlags.CREAT); } openFile(path: string, o_flags: number, mode?: number): WASIFileDescriptor | number { let file = this.fs.getFile(path); mode = typeof mode == 'undefined' ? 438 /* 0666 */ : mode; if (o_flags & FDOpenFlags.CREAT) { if (file == null) { if (o_flags & FDOpenFlags.DIRECTORY) { file = this.fs.putDirectory(path); } else { file = this.fs.putFile(path, new Uint8Array(), FDRights.FD_ALL); } } else { if (o_flags & FDOpenFlags.TRUNC) { // truncate file.truncate(); } else return WASIErrors.INVAL; } } else { if (file == null) return WASIErrors.NOSYS; if (o_flags & FDOpenFlags.DIRECTORY) { // check type if (file.type !== FDType.DIRECTORY) return WASIErrors.NOSYS; } if (o_flags & FDOpenFlags.EXCL) return WASIErrors.INVAL; // already exists if (o_flags & FDOpenFlags.TRUNC) { // truncate file.truncate(); } } file.fdindex = this.fds.length; this.fds.push(file); return file; } mem8() { if (!this.#memarr8?.byteLength) { this.#memarr8 = new Uint8Array(this.#instance.exports.memory.buffer); } return this.#memarr8; } mem32() { if (!this.#memarr32?.byteLength) { this.#memarr32 = new Int32Array(this.#instance.exports.memory.buffer); } return this.#memarr32; } run() { try { this.#instance.exports._start(); if (!this.exited) { this.exited = true; this.errno = 0; } } catch (err) { if (!this.exited) throw err; } return this.getErrno(); } getImportObject() { return { "wasi_snapshot_preview1": this.getWASISnapshotPreview1(), "env": this.getEnv(), } } peek8(ptr: number) { return this.mem8()[ptr]; } peek16(ptr: number) { return this.mem8()[ptr] | (this.mem8()[ptr + 1] << 8); } peek32(ptr: number) { return this.mem32()[ptr >>> 2]; } poke8(ptr: number, val: number) { this.mem8()[ptr] = val; } poke16(ptr: number, val: number) { this.mem8()[ptr] = val; this.mem8()[ptr + 1] = val >> 8; } poke32(ptr: number, val: number) { this.mem32()[ptr >>> 2] = val; } poke64(ptr: number, val: number) { this.mem32()[ptr >>> 2] = val; this.mem32()[(ptr >>> 2) + 1] = 0; } pokeUTF8(str: string, ptr: number, maxlen: number) { const enc = new TextEncoder(); const bytes = enc.encode(str); const len = Math.min(bytes.length, maxlen); this.mem8().set(bytes.subarray(0, len), ptr); } peekUTF8(ptr: number, maxlen: number) { const bytes = this.mem8().subarray(ptr, ptr + maxlen); const dec = new TextDecoder(); return dec.decode(bytes); } getErrno() { return this.errno; //let errno_ptr = this.#instance.exports.__errno_location(); //return this.peek32(errno_ptr); } args_sizes_get(argcount_ptr: number, argv_buf_size_ptr: number) { debug("args_sizes_get", argcount_ptr, argv_buf_size_ptr); this.poke32(argcount_ptr, this.#args.length); this.poke32(argv_buf_size_ptr, this.#args.reduce((acc, arg) => acc + arg.length, 0)); return 0; } args_get(argv_ptr: number, argv_buf_ptr: number) { debug("args_get", argv_ptr, argv_buf_ptr); let argv = argv_ptr; let argv_buf = argv_buf_ptr; for (let arg of this.#args) { this.poke32(argv, argv_buf); argv += 4; for (let i = 0; i < arg.length; i++) { this.poke8(argv_buf, arg[i]); argv_buf++; } } return 0; } fd_write(fd, iovs, iovs_len) { debug("fd_write", fd, iovs, iovs_len); const stream = this.fds[fd]; const iovecs = this.mem32().subarray(iovs >>> 2, (iovs + iovs_len * 8) >>> 2); let total = 0; for (let i = 0; i < iovs_len; i++) { const ptr = iovecs[i * 2]; const len = iovecs[i * 2 + 1]; const chunk = this.mem8().subarray(ptr, ptr + len); total += len; stream.write(chunk); } return total; } fd_read(fd, iovs, iovs_len, nread_ptr) { const stream = this.fds[fd]; const iovecs = this.mem32().subarray(iovs >>> 2, (iovs + iovs_len * 8) >>> 2); let total = 0; for (let i = 0; i < iovs_len; i++) { const ptr = iovecs[i * 2]; const len = iovecs[i * 2 + 1]; const chunk = this.mem8().subarray(ptr, ptr + len); total += stream.read(chunk); } this.poke32(nread_ptr, total); debug("fd_read", fd, iovs, iovs_len, '->', total); return WASIErrors.SUCCESS; } fd_seek(fd: number, offset: number, whence: number, newoffset_ptr: number) { const file = this.fds[fd]; debug("fd_seek", fd, offset, whence, file); if (file != null) { file.llseek(offset, whence); this.poke64(newoffset_ptr, file.offset); return WASIErrors.SUCCESS; } return WASIErrors.BADF; } fd_close(fd: number) { debug("fd_close", fd); const file = this.fds[fd]; if (file != null) { this.fds[fd] = null; return 0; } return WASIErrors.BADF; } proc_exit(errno: number) { debug("proc_exit", errno); this.errno = errno; this.exited = true; } fd_prestat_get(fd: number, prestat_ptr: number) { const file = this.fds[fd]; debug("fd_prestat_get", fd, prestat_ptr, file?.name); if (file && file.type === FDType.DIRECTORY) { const enc_name = new TextEncoder().encode(file.name); this.poke8(prestat_ptr + 0, 0); // __WASI_PREOPENTYPE_DIR this.poke64(prestat_ptr + 8, enc_name.length); return WASIErrors.SUCCESS; } return WASIErrors.BADF; } fd_fdstat_get(fd: number, fdstat_ptr: number) { const file = this.fds[fd]; debug("fd_fdstat_get", fd, fdstat_ptr, file + ""); if (file != null) { this.poke16(fdstat_ptr + 0, file.type); // fs_filetype this.poke16(fdstat_ptr + 2, file.flags); // fs_flags this.poke64(fdstat_ptr + 8, file.rights); // fs_rights_base this.poke64(fdstat_ptr + 16, file.rights); // fs_rights_inheriting return WASIErrors.SUCCESS; } return WASIErrors.BADF; } fd_prestat_dir_name(fd: number, path_ptr: number, path_len: number) { const file = this.fds[fd]; debug("fd_prestat_dir_name", fd, path_ptr, path_len); if (file != null) { this.pokeUTF8(file.name, path_ptr, path_len); return WASIErrors.SUCCESS; } return WASIErrors.INVAL; } path_open(dirfd: number, dirflags: number, path_ptr: number, path_len: number, o_flags: number, fs_rights_base: number, fs_rights_inheriting: number, fd_flags: number, fd_ptr: number) { const dir = this.fds[dirfd]; if (dir == null) return WASIErrors.BADF; if (dir.type !== FDType.DIRECTORY) return WASIErrors.NOTDIR; const filename = this.peekUTF8(path_ptr, path_len); const path = dir.name + '/' + filename; const fd = this.openFile(path, o_flags, fd_flags); debug("path_open", path, dirfd, dirflags, o_flags, //fs_rights_base, fs_rights_inheriting, fd_flags, fd_ptr, '->', fd + ""); if (typeof fd === 'number') return fd; // error msg this.poke32(fd_ptr, fd.fdindex); return WASIErrors.SUCCESS; } random_get(ptr: number, len: number) { debug("random_get", ptr, len); for (let i=0; i