diff --git a/examples/rustc.html b/examples/rustc.html index e760820..902e2ec 100644 --- a/examples/rustc.html +++ b/examples/rustc.html @@ -47,15 +47,11 @@ super(); this.term = term; } - fd_write(view8/*: Uint8Array*/, iovs/*: [wasi.Iovec]*/)/*: {ret: number, nwritten: number}*/ { + fd_write(data/*: Uint8Array*/)/*: {ret: number, nwritten: number}*/ { let nwritten = 0; - for (let iovec of iovs) { - console.log(iovec.buf_len, iovec.buf_len, view8.slice(iovec.buf, iovec.buf + iovec.buf_len)); - let buffer = view8.slice(iovec.buf, iovec.buf + iovec.buf_len); - this.term.writeUtf8(buffer); - nwritten += iovec.buf_len; - } - return { ret: 0, nwritten }; + console.log(data); + this.term.writeUtf8(data); + return { ret: 0, nwritten: data.byteLength }; } } @@ -75,16 +71,16 @@ new XtermStdio(term), new XtermStdio(term), new XtermStdio(term), - new PreopenDirectory("/tmp", {}), - new PreopenDirectory("/sysroot", { - "lib": new Directory({ - "rustlib": new Directory({ - "wasm32-wasi": new Directory({ - "lib": new Directory({}), - }), - "x86_64-unknown-linux-gnu": new Directory({ - "lib": new Directory(await (async function () { - let dir = {}; + new PreopenDirectory("/tmp", []), + new PreopenDirectory("/sysroot", [ + ["lib", new Directory([ + ["rustlib", new Directory([ + ["wasm32-wasi", new Directory([ + ["lib", new Directory([])], + ])], + ["x86_64-unknown-linux-gnu", new Directory([ + ["lib", new Directory(await (async function () { + let dir = new Map(); for (let file of [ "libaddr2line-3368a2ecf632bfc6.rlib", "libadler-16845f650eeea12c.rlib", @@ -115,17 +111,17 @@ "libunicode_width-d55ce9c674fbd422.rlib", "libunwind-8ca3e01a84805f9e.rlib" ]) { - dir[file] = await load_external_file("/examples/wasm-rustc/lib/rustlib/x86_64-unknown-linux-gnu/lib/" + file); + dir.set(file, await load_external_file("/examples/wasm-rustc/lib/rustlib/x86_64-unknown-linux-gnu/lib/" + file)); } return dir; - })()), - }), - }), - }), - }), - new PreopenDirectory("/", { - "hello.rs": new File(new TextEncoder("utf-8").encode(`fn main() { println!("Hello World!"); }`)), - }), + })())], + ])], + ])], + ])], + ]), + new PreopenDirectory("/", [ + ["hello.rs", new File(new TextEncoder("utf-8").encode(`fn main() { println!("Hello World!"); }`))], + ]), ]; let w = new WASI(args, env, fds, { debug: true }); @@ -135,14 +131,14 @@ }); term.writeln("\x1B[93mExecuting\x1B[0m"); console.log(inst.exports); - try { w.start(inst); } catch(e) { term.writeln("Exception: " + e.message); term.writeln("backtrace:"); term.writeln(e.stack); throw e; } + try { w.start(inst); } catch(e) { term.writeln("Exception: " + e.message); /*term.writeln("backtrace:"); term.writeln(e.stack);*/ } term.writeln("\x1B[92mDone\x1B[0m"); console.log(fds); console.log(fds[5].dir); - console.log(fds[5].dir.contents["hello.hello.2490b9cce2492134-cgu.0.rcgu.o"].data); - document.querySelector("#downloads").innerHTML += "
Download object"; - document.querySelector("#downloads").innerHTML += "
Download allocator shim"; + console.log(fds[5].dir.contents.get("hello.hello.2490b9cce2492134-cgu.0.rcgu.o").data); + document.querySelector("#downloads").innerHTML += "
Download object"; + document.querySelector("#downloads").innerHTML += "
Download allocator shim"; })(); diff --git a/package-lock.json b/package-lock.json index 7cb8f6d..6891208 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bjorn3/browser_wasi_shim", - "version": "0.2.21", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bjorn3/browser_wasi_shim", - "version": "0.2.21", + "version": "0.3.0", "license": "MIT OR Apache-2.0", "devDependencies": { "@swc/cli": "^0.1.62", diff --git a/package.json b/package.json index 433fcda..bab05a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bjorn3/browser_wasi_shim", - "version": "0.2.21", + "version": "0.3.0", "license": "MIT OR Apache-2.0", "description": "A pure javascript shim for WASI", "type": "module", diff --git a/src/fd.ts b/src/fd.ts index 435685a..4e3b6f5 100644 --- a/src/fd.ts +++ b/src/fd.ts @@ -2,18 +2,12 @@ import * as wasi from "./wasi_defs.js"; export abstract class Fd { - fd_advise(offset: bigint, len: bigint, advice: number): number { - return wasi.ERRNO_SUCCESS; - } fd_allocate(offset: bigint, len: bigint): number { return wasi.ERRNO_NOTSUP; } fd_close(): number { return 0; } - fd_datasync(): number { - return wasi.ERRNO_NOTSUP; - } fd_fdstat_get(): { ret: number; fdstat: wasi.Fdstat | null } { return { ret: wasi.ERRNO_NOTSUP, fdstat: null }; } @@ -35,31 +29,20 @@ export abstract class Fd { fd_filestat_set_times(atim: bigint, mtim: bigint, fst_flags: number): number { return wasi.ERRNO_NOTSUP; } - fd_pread( - view8: Uint8Array, - iovs: Array, - offset: bigint, - ): { ret: number; nread: number } { - return { ret: wasi.ERRNO_NOTSUP, nread: 0 }; + fd_pread(size: number, offset: bigint): { ret: number; data: Uint8Array } { + return { ret: wasi.ERRNO_NOTSUP, data: new Uint8Array() }; } fd_prestat_get(): { ret: number; prestat: wasi.Prestat | null } { return { ret: wasi.ERRNO_NOTSUP, prestat: null }; } - fd_prestat_dir_name(): { ret: number; prestat_dir_name: Uint8Array | null } { - return { ret: wasi.ERRNO_NOTSUP, prestat_dir_name: null }; - } fd_pwrite( - view8: Uint8Array, - iovs: Array, + data: Uint8Array, offset: bigint, ): { ret: number; nwritten: number } { return { ret: wasi.ERRNO_NOTSUP, nwritten: 0 }; } - fd_read( - view8: Uint8Array, - iovs: Array, - ): { ret: number; nread: number } { - return { ret: wasi.ERRNO_NOTSUP, nread: 0 }; + fd_read(size: number): { ret: number; data: Uint8Array } { + return { ret: wasi.ERRNO_NOTSUP, data: new Uint8Array() }; } fd_readdir_single(cookie: bigint): { ret: number; @@ -76,10 +59,7 @@ export abstract class Fd { fd_tell(): { ret: number; offset: bigint } { return { ret: wasi.ERRNO_NOTSUP, offset: 0n }; } - fd_write( - view8: Uint8Array, - iovs: Array, - ): { ret: number; nwritten: number } { + fd_write(data: Uint8Array): { ret: number; nwritten: number } { return { ret: wasi.ERRNO_NOTSUP, nwritten: 0 }; } path_create_directory(path: string): number { @@ -100,14 +80,18 @@ export abstract class Fd { ): number { return wasi.ERRNO_NOTSUP; } - path_link( - old_fd: number, - old_flags: number, - old_path: string, - new_path: string, - ): number { + path_link(path: string, inode: Inode, allow_dir: boolean): number { return wasi.ERRNO_NOTSUP; } + path_unlink(path: string): { ret: number; inode_obj: Inode | null } { + return { ret: wasi.ERRNO_NOTSUP, inode_obj: null }; + } + path_lookup( + path: string, + dirflags: number, + ): { ret: number; inode_obj: Inode | null } { + return { ret: wasi.ERRNO_NOTSUP, inode_obj: null }; + } path_open( dirflags: number, path: string, @@ -116,7 +100,7 @@ export abstract class Fd { fs_rights_inheriting: bigint, fd_flags: number, ): { ret: number; fd_obj: Fd | null } { - return { ret: wasi.ERRNO_NOTSUP, fd_obj: null }; + return { ret: wasi.ERRNO_NOTDIR, fd_obj: null }; } path_readlink(path: string): { ret: number; data: string | null } { return { ret: wasi.ERRNO_NOTSUP, data: null }; @@ -127,10 +111,17 @@ export abstract class Fd { path_rename(old_path: string, new_fd: number, new_path: string): number { return wasi.ERRNO_NOTSUP; } - path_symlink(old_path: string, new_path: string): number { - return wasi.ERRNO_NOTSUP; - } path_unlink_file(path: string): number { return wasi.ERRNO_NOTSUP; } } + +export abstract class Inode { + abstract path_open( + oflags: number, + fs_rights_base: bigint, + fd_flags: number, + ): { ret: number; fd_obj: Fd | null }; + + abstract stat(): wasi.Filestat; +} diff --git a/src/fs_core.ts b/src/fs_core.ts deleted file mode 100644 index ad0f17d..0000000 --- a/src/fs_core.ts +++ /dev/null @@ -1,689 +0,0 @@ -import { debug } from "./debug.js"; -import * as wasi from "./wasi_defs.js"; -import { Fd } from "./fd.js"; - -export class OpenFile extends Fd { - file: File; - file_pos: bigint = 0n; - - constructor(file: File) { - super(); - this.file = file; - } - - fd_allocate(offset: bigint, len: bigint): number { - if (this.file.size > offset + len) { - // already big enough - } else { - // extend - const new_data = new Uint8Array(Number(offset + len)); - new_data.set(this.file.data, 0); - this.file.data = new_data; - } - return wasi.ERRNO_SUCCESS; - } - - fd_fdstat_get(): { ret: number; fdstat: wasi.Fdstat | null } { - return { ret: 0, fdstat: new wasi.Fdstat(wasi.FILETYPE_REGULAR_FILE, 0) }; - } - - fd_filestat_set_size(size: bigint): number { - if (this.file.size > size) { - // truncate - this.file.data = new Uint8Array( - this.file.data.buffer.slice(0, Number(size)), - ); - } else { - // extend - const new_data = new Uint8Array(Number(size)); - new_data.set(this.file.data, 0); - this.file.data = new_data; - } - return wasi.ERRNO_SUCCESS; - } - - fd_read( - view8: Uint8Array, - iovs: Array, - ): { ret: number; nread: number } { - let nread = 0; - for (const iovec of iovs) { - if (this.file_pos < this.file.data.byteLength) { - const slice = this.file.data.slice( - Number(this.file_pos), - Number(this.file_pos + BigInt(iovec.buf_len)), - ); - view8.set(slice, iovec.buf); - this.file_pos += BigInt(slice.length); - nread += slice.length; - } else { - break; - } - } - return { ret: 0, nread }; - } - - fd_pread( - view8: Uint8Array, - iovs: Array, - offset: bigint, - ): { ret: number; nread: number } { - let nread = 0; - for (const iovec of iovs) { - if (offset < this.file.data.byteLength) { - const slice = this.file.data.slice( - Number(offset), - Number(offset + BigInt(iovec.buf_len)), - ); - view8.set(slice, iovec.buf); - offset += BigInt(slice.length); - nread += slice.length; - } else { - break; - } - } - return { ret: 0, nread }; - } - - fd_seek(offset: bigint, whence: number): { ret: number; offset: bigint } { - let calculated_offset: bigint; - switch (whence) { - case wasi.WHENCE_SET: - calculated_offset = offset; - break; - case wasi.WHENCE_CUR: - calculated_offset = this.file_pos + offset; - break; - case wasi.WHENCE_END: - calculated_offset = BigInt(this.file.data.byteLength) + offset; - break; - default: - return { ret: wasi.ERRNO_INVAL, offset: 0n }; - } - - if (calculated_offset < 0) { - return { ret: wasi.ERRNO_INVAL, offset: 0n }; - } - - this.file_pos = calculated_offset; - return { ret: 0, offset: this.file_pos }; - } - - fd_tell(): { ret: number; offset: bigint } { - return { ret: 0, offset: this.file_pos }; - } - - fd_write( - view8: Uint8Array, - iovs: Array, - ): { ret: number; nwritten: number } { - let nwritten = 0; - if (this.file.readonly) return { ret: wasi.ERRNO_BADF, nwritten }; - for (const iovec of iovs) { - const buffer = view8.slice(iovec.buf, iovec.buf + iovec.buf_len); - if (this.file_pos + BigInt(buffer.byteLength) > this.file.size) { - const old = this.file.data; - this.file.data = new Uint8Array( - Number(this.file_pos + BigInt(buffer.byteLength)), - ); - this.file.data.set(old); - } - this.file.data.set( - buffer.slice(0, Number(this.file.size - this.file_pos)), - Number(this.file_pos), - ); - this.file_pos += BigInt(buffer.byteLength); - nwritten += iovec.buf_len; - } - return { ret: 0, nwritten }; - } - - fd_pwrite(view8: Uint8Array, iovs: Array, offset: bigint) { - let nwritten = 0; - if (this.file.readonly) return { ret: wasi.ERRNO_BADF, nwritten }; - for (const iovec of iovs) { - const buffer = view8.slice(iovec.buf, iovec.buf + iovec.buf_len); - if (offset + BigInt(buffer.byteLength) > this.file.size) { - const old = this.file.data; - this.file.data = new Uint8Array( - Number(offset + BigInt(buffer.byteLength)), - ); - this.file.data.set(old); - } - this.file.data.set( - buffer.slice(0, Number(this.file.size - offset)), - Number(offset), - ); - offset += BigInt(buffer.byteLength); - nwritten += iovec.buf_len; - } - return { ret: 0, nwritten }; - } - - fd_filestat_get(): { ret: number; filestat: wasi.Filestat } { - return { ret: 0, filestat: this.file.stat() }; - } -} - -export class OpenSyncOPFSFile extends Fd { - file: SyncOPFSFile; - position: bigint = 0n; - - constructor(file: SyncOPFSFile) { - super(); - this.file = file; - } - - fd_allocate(offset: bigint, len: bigint): number { - if (BigInt(this.file.handle.getSize()) > offset + len) { - // already big enough - } else { - // extend - this.file.handle.truncate(Number(offset + len)); - } - return wasi.ERRNO_SUCCESS; - } - - fd_fdstat_get(): { ret: number; fdstat: wasi.Fdstat | null } { - return { ret: 0, fdstat: new wasi.Fdstat(wasi.FILETYPE_REGULAR_FILE, 0) }; - } - - fd_filestat_get(): { ret: number; filestat: wasi.Filestat } { - return { - ret: 0, - filestat: new wasi.Filestat( - wasi.FILETYPE_REGULAR_FILE, - BigInt(this.file.handle.getSize()), - ), - }; - } - - fd_filestat_set_size(size: bigint): number { - this.file.handle.truncate(Number(size)); - return wasi.ERRNO_SUCCESS; - } - - fd_read( - view8: Uint8Array, - iovs: Array, - ): { ret: number; nread: number } { - let nread = 0; - for (const iovec of iovs) { - if (this.position < this.file.handle.getSize()) { - const buf = new Uint8Array(view8.buffer, iovec.buf, iovec.buf_len); - const n = this.file.handle.read(buf, { at: Number(this.position) }); - this.position += BigInt(n); - nread += n; - } else { - break; - } - } - return { ret: 0, nread }; - } - - fd_seek( - offset: number | bigint, - whence: number, - ): { ret: number; offset: bigint } { - let calculated_offset: bigint; - switch (whence) { - case wasi.WHENCE_SET: - calculated_offset = BigInt(offset); - break; - case wasi.WHENCE_CUR: - calculated_offset = this.position + BigInt(offset); - break; - case wasi.WHENCE_END: - calculated_offset = BigInt(this.file.handle.getSize()) + BigInt(offset); - break; - default: - return { ret: wasi.ERRNO_INVAL, offset: 0n }; - } - if (calculated_offset < 0) { - return { ret: wasi.ERRNO_INVAL, offset: 0n }; - } - this.position = calculated_offset; - return { ret: wasi.ERRNO_SUCCESS, offset: this.position }; - } - - fd_write( - view8: Uint8Array, - iovs: Array, - ): { ret: number; nwritten: number } { - let nwritten = 0; - if (this.file.readonly) return { ret: wasi.ERRNO_BADF, nwritten }; - for (const iovec of iovs) { - const buf = new Uint8Array(view8.buffer, iovec.buf, iovec.buf_len); - // don't need to extend file manually, just write - const n = this.file.handle.write(buf, { at: Number(this.position) }); - this.position += BigInt(n); - nwritten += n; - } - return { ret: wasi.ERRNO_SUCCESS, nwritten }; - } - - fd_datasync(): number { - this.file.handle.flush(); - return wasi.ERRNO_SUCCESS; - } - - fd_sync(): number { - return this.fd_datasync(); - } - - fd_close(): number { - return wasi.ERRNO_SUCCESS; - } -} - -export class OpenDirectory extends Fd { - dir: Directory; - - constructor(dir: Directory) { - super(); - this.dir = dir; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - fd_seek(offset: bigint, whence: number): { ret: number; offset: bigint } { - return { ret: wasi.ERRNO_ISDIR, offset: 0n }; - } - - fd_fdstat_get(): { ret: number; fdstat: wasi.Fdstat | null } { - return { ret: 0, fdstat: new wasi.Fdstat(wasi.FILETYPE_DIRECTORY, 0) }; - } - - fd_readdir_single(cookie: bigint): { - ret: number; - dirent: wasi.Dirent | null; - } { - if (debug.enabled) { - debug.log("readdir_single", cookie); - debug.log(cookie, Object.keys(this.dir.contents)); - } - debug.log(cookie, Object.keys(this.dir.contents).slice(Number(cookie))); - if (cookie >= BigInt(Object.keys(this.dir.contents).length)) { - return { ret: 0, dirent: null }; - } - - const name = Object.keys(this.dir.contents)[Number(cookie)]; - const entry = this.dir.contents[name]; - - return { - ret: 0, - dirent: new wasi.Dirent(cookie + 1n, name, entry.stat().filetype), - }; - } - - path_filestat_get( - flags: number, - path: string, - ): { ret: number; filestat: wasi.Filestat | null } { - const entry = this.dir.get_entry_for_path(path); - if (entry == null) { - return { ret: wasi.ERRNO_NOENT, filestat: null }; - } - return { ret: 0, filestat: entry.stat() }; - } - - path_open( - dirflags: number, - path: string, - oflags: number, - fs_rights_base: bigint, - fs_rights_inheriting: bigint, - fd_flags: number, - ): { ret: number; fd_obj: Fd | null } { - let entry = this.dir.get_entry_for_path(path); - if (entry == null) { - if ((oflags & wasi.OFLAGS_CREAT) == wasi.OFLAGS_CREAT) { - // doesn't exist, but shall be created - entry = this.dir.create_entry_for_path( - path, - (oflags & wasi.OFLAGS_DIRECTORY) == wasi.OFLAGS_DIRECTORY, - ); - } else { - // doesn't exist, no such file - return { ret: wasi.ERRNO_NOENT, fd_obj: null }; - } - } else if ((oflags & wasi.OFLAGS_EXCL) == wasi.OFLAGS_EXCL) { - // was supposed to be created exclusively, but exists already - return { ret: wasi.ERRNO_EXIST, fd_obj: null }; - } - if ( - (oflags & wasi.OFLAGS_DIRECTORY) == wasi.OFLAGS_DIRECTORY && - entry.stat().filetype !== wasi.FILETYPE_DIRECTORY - ) { - // expected a directory but the file is not a directory - return { ret: wasi.ERRNO_NOTDIR, fd_obj: null }; - } - if ( - entry.readonly && - (fs_rights_base & BigInt(wasi.RIGHTS_FD_WRITE)) == - BigInt(wasi.RIGHTS_FD_WRITE) - ) { - // no write permission to file - return { ret: wasi.ERRNO_PERM, fd_obj: null }; - } - if ( - !(entry instanceof Directory) && - (oflags & wasi.OFLAGS_TRUNC) == wasi.OFLAGS_TRUNC - ) { - // truncate existing file first - const ret = entry.truncate(); - if (ret != wasi.ERRNO_SUCCESS) return { ret, fd_obj: null }; - } - return { ret: wasi.ERRNO_SUCCESS, fd_obj: entry.open(fd_flags) }; - } - - path_create_directory(path: string): number { - return this.path_open( - 0, - path, - wasi.OFLAGS_CREAT | wasi.OFLAGS_DIRECTORY, - 0n, - 0n, - 0, - ).ret; - } - - path_unlink_file(path: string): number { - path = this.clean_path(path); - const parentDirEntry = this.dir.get_parent_dir_for_path(path); - const pathComponents = path.split("/"); - const fileName = pathComponents[pathComponents.length - 1]; - const entry = this.dir.get_entry_for_path(path); - if (entry === null) { - return wasi.ERRNO_NOENT; - } - if (entry.stat().filetype === wasi.FILETYPE_DIRECTORY) { - return wasi.ERRNO_ISDIR; - } - delete parentDirEntry.contents[fileName]; - return wasi.ERRNO_SUCCESS; - } - - path_remove_directory(path: string): number { - path = this.clean_path(path); - const parentDirEntry = this.dir.get_parent_dir_for_path(path); - const pathComponents = path.split("/"); - const fileName = pathComponents[pathComponents.length - 1]; - - const entry = this.dir.get_entry_for_path(path); - - if (entry === null) { - return wasi.ERRNO_NOENT; - } - if ( - !(entry instanceof Directory) || - entry.stat().filetype !== wasi.FILETYPE_DIRECTORY - ) { - return wasi.ERRNO_NOTDIR; - } - if (Object.keys(entry.contents).length !== 0) { - return wasi.ERRNO_NOTEMPTY; - } - if (parentDirEntry.contents[fileName] === undefined) { - return wasi.ERRNO_NOENT; - } - delete parentDirEntry.contents[fileName]; - return wasi.ERRNO_SUCCESS; - } - - clean_path(path: string): string { - while (path.length > 0 && path[path.length - 1] === "/") { - path = path.slice(0, path.length - 1); - } - return path; - } -} - -export class PreopenDirectory extends OpenDirectory { - prestat_name: Uint8Array; - - constructor( - name: string, - contents: { [key: string]: File | Directory | SyncOPFSFile }, - ) { - super(new Directory(contents)); - this.prestat_name = new TextEncoder().encode(name); - } - - fd_prestat_get(): { ret: number; prestat: wasi.Prestat | null } { - return { - ret: 0, - prestat: wasi.Prestat.dir(this.prestat_name.length), - }; - } - - fd_prestat_dir_name(): { ret: number; prestat_dir_name: Uint8Array } { - return { - ret: 0, - prestat_dir_name: this.prestat_name, - }; - } -} - -// options that can be passed to Files and SyncOPFSFiles -type FileOptions = Partial<{ - readonly: boolean; -}>; - -export class File { - data: Uint8Array; - readonly: boolean; - - constructor( - data: ArrayBuffer | SharedArrayBuffer | Uint8Array | Array, - options?: FileOptions, - ) { - this.data = new Uint8Array(data); - this.readonly = !!options?.readonly; - } - - open(fd_flags: number) { - const file = new OpenFile(this); - if (fd_flags & wasi.FDFLAGS_APPEND) file.fd_seek(0n, wasi.WHENCE_END); - return file; - } - - get size(): bigint { - return BigInt(this.data.byteLength); - } - - stat(): wasi.Filestat { - return new wasi.Filestat(wasi.FILETYPE_REGULAR_FILE, this.size); - } - - truncate(): number { - if (this.readonly) return wasi.ERRNO_PERM; - this.data = new Uint8Array([]); - return wasi.ERRNO_SUCCESS; - } -} - -// Shim for https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle -// This is not part of the public interface. -export interface FileSystemSyncAccessHandle { - close(): void; - flush(): void; - getSize(): number; - read(buffer: ArrayBuffer | ArrayBufferView, options?: { at: number }): number; - truncate(to: number): void; - write( - buffer: ArrayBuffer | ArrayBufferView, - options?: { at: number }, - ): number; -} - -// Synchronous access to an individual file in the origin private file system. -// Only allowed inside a WebWorker. -export class SyncOPFSFile { - handle: FileSystemSyncAccessHandle; - readonly: boolean; - - // FIXME needs a close() method to be called after start() to release the underlying handle - constructor(handle: FileSystemSyncAccessHandle, options?: FileOptions) { - this.handle = handle; - this.readonly = !!options?.readonly; - } - - open(fd_flags: number) { - const file = new OpenSyncOPFSFile(this); - if (fd_flags & wasi.FDFLAGS_APPEND) file.fd_seek(0n, wasi.WHENCE_END); - return file; - } - - get size(): bigint { - return BigInt(this.handle.getSize()); - } - - stat(): wasi.Filestat { - return new wasi.Filestat(wasi.FILETYPE_REGULAR_FILE, this.size); - } - - truncate(): number { - if (this.readonly) return wasi.ERRNO_PERM; - this.handle.truncate(0); - return wasi.ERRNO_SUCCESS; - } -} - -export class Directory { - contents: { [key: string]: File | Directory | SyncOPFSFile }; - readonly = false; // FIXME implement, like marking all files within readonly? - - constructor(contents: { [key: string]: File | Directory | SyncOPFSFile }) { - this.contents = contents; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - open(fd_flags: number) { - return new OpenDirectory(this); - } - - stat(): wasi.Filestat { - return new wasi.Filestat(wasi.FILETYPE_DIRECTORY, 0n); - } - - get_entry_for_path(path: string): File | Directory | SyncOPFSFile | null { - let entry: File | Directory | SyncOPFSFile = this; - for (const component of path.split("/")) { - if (component == "") break; - if (component == ".") continue; - if (!(entry instanceof Directory)) { - return null; - } - if (entry.contents[component] != undefined) { - entry = entry.contents[component]; - } else { - debug.log(component); - return null; - } - } - return entry; - } - - get_parent_dir_for_path(path: string): Directory | null { - if (path === "") return null; - let entry: File | Directory | SyncOPFSFile = this; - let parentEntry: File | Directory | SyncOPFSFile = entry; - for (const component of path.split("/")) { - if (component === "") break; - if (component === ".") continue; - if (!(entry instanceof Directory)) { - debug.log(entry); - return null; - } - if (entry.contents[component] === undefined) { - debug.log(component); - return null; - } - parentEntry = entry; - entry = entry.contents[component]; - } - return parentEntry; - } - - create_entry_for_path( - path: string, - is_dir: boolean, - ): File | Directory | SyncOPFSFile { - let entry: File | Directory | SyncOPFSFile = this; - - const components: Array = path - .split("/") - .filter((component) => component != "/"); - for (let i = 0; i < components.length; i++) { - const component = components[i]; - if (!(entry instanceof Directory)) { - break; - } - if (entry.contents[component] != undefined) { - entry = entry.contents[component]; - } else { - debug.log("create", component); - if (i == components.length - 1 && !is_dir) { - entry.contents[component] = new File(new ArrayBuffer(0)); - } else { - entry.contents[component] = new Directory({}); - } - entry = entry.contents[component]; - } - } - - return entry; - } -} - -export class ConsoleStdout extends Fd { - write: (buffer: Uint8Array) => void; - - constructor(write: (buffer: Uint8Array) => void) { - super(); - this.write = write; - } - - fd_filestat_get(): { ret: number; filestat: wasi.Filestat } { - const filestat = new wasi.Filestat( - wasi.FILETYPE_CHARACTER_DEVICE, - BigInt(0), - ); - return { ret: 0, filestat }; - } - - fd_fdstat_get(): { ret: number; fdstat: wasi.Fdstat | null } { - const fdstat = new wasi.Fdstat(wasi.FILETYPE_CHARACTER_DEVICE, 0); - fdstat.fs_rights_base = BigInt(wasi.RIGHTS_FD_WRITE); - return { ret: 0, fdstat }; - } - - fd_write( - view8: Uint8Array, - iovs: Array, - ): { ret: number; nwritten: number } { - let nwritten = 0; - for (const iovec of iovs) { - const buffer = view8.slice(iovec.buf, iovec.buf + iovec.buf_len); - this.write(buffer); - nwritten += iovec.buf_len; - } - return { ret: 0, nwritten }; - } - - static lineBuffered(write: (line: string) => void): ConsoleStdout { - const dec = new TextDecoder("utf-8", { fatal: false }); - let line_buf = ""; - return new ConsoleStdout((buffer) => { - line_buf += dec.decode(buffer, { stream: true }); - const lines = line_buf.split("\n"); - for (const [i, line] of lines.entries()) { - if (i < lines.length - 1) { - write(line); - } else { - line_buf = line; - } - } - }); - } -} diff --git a/src/fs_mem.ts b/src/fs_mem.ts new file mode 100644 index 0000000..6f540ee --- /dev/null +++ b/src/fs_mem.ts @@ -0,0 +1,741 @@ +import { debug } from "./debug.js"; +import * as wasi from "./wasi_defs.js"; +import { Fd, Inode } from "./fd.js"; + +export class OpenFile extends Fd { + file: File; + file_pos: bigint = 0n; + + constructor(file: File) { + super(); + this.file = file; + } + + fd_allocate(offset: bigint, len: bigint): number { + if (this.file.size > offset + len) { + // already big enough + } else { + // extend + const new_data = new Uint8Array(Number(offset + len)); + new_data.set(this.file.data, 0); + this.file.data = new_data; + } + return wasi.ERRNO_SUCCESS; + } + + fd_fdstat_get(): { ret: number; fdstat: wasi.Fdstat | null } { + return { ret: 0, fdstat: new wasi.Fdstat(wasi.FILETYPE_REGULAR_FILE, 0) }; + } + + fd_filestat_set_size(size: bigint): number { + if (this.file.size > size) { + // truncate + this.file.data = new Uint8Array( + this.file.data.buffer.slice(0, Number(size)), + ); + } else { + // extend + const new_data = new Uint8Array(Number(size)); + new_data.set(this.file.data, 0); + this.file.data = new_data; + } + return wasi.ERRNO_SUCCESS; + } + + fd_read(size: number): { ret: number; data: Uint8Array } { + const slice = this.file.data.slice( + Number(this.file_pos), + Number(this.file_pos + BigInt(size)), + ); + this.file_pos += BigInt(slice.length); + return { ret: 0, data: slice }; + } + + fd_pread(size: number, offset: bigint): { ret: number; data: Uint8Array } { + const slice = this.file.data.slice( + Number(offset), + Number(offset + BigInt(size)), + ); + return { ret: 0, data: slice }; + } + + fd_seek(offset: bigint, whence: number): { ret: number; offset: bigint } { + let calculated_offset: bigint; + switch (whence) { + case wasi.WHENCE_SET: + calculated_offset = offset; + break; + case wasi.WHENCE_CUR: + calculated_offset = this.file_pos + offset; + break; + case wasi.WHENCE_END: + calculated_offset = BigInt(this.file.data.byteLength) + offset; + break; + default: + return { ret: wasi.ERRNO_INVAL, offset: 0n }; + } + + if (calculated_offset < 0) { + return { ret: wasi.ERRNO_INVAL, offset: 0n }; + } + + this.file_pos = calculated_offset; + return { ret: 0, offset: this.file_pos }; + } + + fd_tell(): { ret: number; offset: bigint } { + return { ret: 0, offset: this.file_pos }; + } + + fd_write(data: Uint8Array): { ret: number; nwritten: number } { + if (this.file.readonly) return { ret: wasi.ERRNO_BADF, nwritten: 0 }; + + if (this.file_pos + BigInt(data.byteLength) > this.file.size) { + const old = this.file.data; + this.file.data = new Uint8Array( + Number(this.file_pos + BigInt(data.byteLength)), + ); + this.file.data.set(old); + } + + this.file.data.set(data, Number(this.file_pos)); + this.file_pos += BigInt(data.byteLength); + return { ret: 0, nwritten: data.byteLength }; + } + + fd_pwrite(data: Uint8Array, offset: bigint) { + if (this.file.readonly) return { ret: wasi.ERRNO_BADF, nwritten: 0 }; + + if (offset + BigInt(data.byteLength) > this.file.size) { + const old = this.file.data; + this.file.data = new Uint8Array(Number(offset + BigInt(data.byteLength))); + this.file.data.set(old); + } + + this.file.data.set(data, Number(offset)); + return { ret: 0, nwritten: data.byteLength }; + } + + fd_filestat_get(): { ret: number; filestat: wasi.Filestat } { + return { ret: 0, filestat: this.file.stat() }; + } +} + +export class OpenDirectory extends Fd { + dir: Directory; + + constructor(dir: Directory) { + super(); + this.dir = dir; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + fd_seek(offset: bigint, whence: number): { ret: number; offset: bigint } { + return { ret: wasi.ERRNO_BADF, offset: 0n }; + } + + fd_tell(): { ret: number; offset: bigint } { + return { ret: wasi.ERRNO_BADF, offset: 0n }; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + fd_allocate(offset: bigint, len: bigint): number { + return wasi.ERRNO_BADF; + } + + fd_fdstat_get(): { ret: number; fdstat: wasi.Fdstat | null } { + return { ret: 0, fdstat: new wasi.Fdstat(wasi.FILETYPE_DIRECTORY, 0) }; + } + + fd_readdir_single(cookie: bigint): { + ret: number; + dirent: wasi.Dirent | null; + } { + if (debug.enabled) { + debug.log("readdir_single", cookie); + debug.log(cookie, this.dir.contents.keys()); + } + + if (cookie == 0n) { + return { + ret: wasi.ERRNO_SUCCESS, + dirent: new wasi.Dirent(1n, ".", wasi.FILETYPE_DIRECTORY), + }; + } else if (cookie == 1n) { + return { + ret: wasi.ERRNO_SUCCESS, + dirent: new wasi.Dirent(2n, "..", wasi.FILETYPE_DIRECTORY), + }; + } + + if (cookie >= BigInt(this.dir.contents.size) + 2n) { + return { ret: 0, dirent: null }; + } + + const [name, entry] = Array.from(this.dir.contents.entries())[ + Number(cookie - 2n) + ]; + + return { + ret: 0, + dirent: new wasi.Dirent(cookie + 1n, name, entry.stat().filetype), + }; + } + + path_filestat_get( + flags: number, + path_str: string, + ): { ret: number; filestat: wasi.Filestat | null } { + const { ret: path_err, path } = Path.from(path_str); + if (path == null) { + return { ret: path_err, filestat: null }; + } + + const { ret, entry } = this.dir.get_entry_for_path(path); + if (entry == null) { + return { ret, filestat: null }; + } + + return { ret: 0, filestat: entry.stat() }; + } + + path_lookup( + path_str: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + dirflags: number, + ): { ret: number; inode_obj: Inode | null } { + const { ret: path_ret, path } = Path.from(path_str); + if (path == null) { + return { ret: path_ret, inode_obj: null }; + } + + const { ret, entry } = this.dir.get_entry_for_path(path); + if (entry == null) { + return { ret, inode_obj: null }; + } + + return { ret: wasi.ERRNO_SUCCESS, inode_obj: entry }; + } + + path_open( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + dirflags: number, + path_str: string, + oflags: number, + fs_rights_base: bigint, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + fs_rights_inheriting: bigint, + fd_flags: number, + ): { ret: number; fd_obj: Fd | null } { + const { ret: path_ret, path } = Path.from(path_str); + if (path == null) { + return { ret: path_ret, fd_obj: null }; + } + + // eslint-disable-next-line prefer-const + let { ret, entry } = this.dir.get_entry_for_path(path); + if (entry == null) { + if (ret != wasi.ERRNO_NOENT) { + return { ret, fd_obj: null }; + } + if ((oflags & wasi.OFLAGS_CREAT) == wasi.OFLAGS_CREAT) { + // doesn't exist, but shall be created + const { ret, entry: new_entry } = this.dir.create_entry_for_path( + path_str, + (oflags & wasi.OFLAGS_DIRECTORY) == wasi.OFLAGS_DIRECTORY, + ); + if (new_entry == null) { + return { ret, fd_obj: null }; + } + entry = new_entry; + } else { + // doesn't exist, no such file + return { ret: wasi.ERRNO_NOENT, fd_obj: null }; + } + } else if ((oflags & wasi.OFLAGS_EXCL) == wasi.OFLAGS_EXCL) { + // was supposed to be created exclusively, but exists already + return { ret: wasi.ERRNO_EXIST, fd_obj: null }; + } + if ( + (oflags & wasi.OFLAGS_DIRECTORY) == wasi.OFLAGS_DIRECTORY && + entry.stat().filetype !== wasi.FILETYPE_DIRECTORY + ) { + // expected a directory but the file is not a directory + return { ret: wasi.ERRNO_NOTDIR, fd_obj: null }; + } + return entry.path_open(oflags, fs_rights_base, fd_flags); + } + + path_create_directory(path: string): number { + return this.path_open( + 0, + path, + wasi.OFLAGS_CREAT | wasi.OFLAGS_DIRECTORY, + 0n, + 0n, + 0, + ).ret; + } + + path_link(path_str: string, inode: Inode, allow_dir: boolean): number { + const { ret: path_ret, path } = Path.from(path_str); + if (path == null) { + return path_ret; + } + + if (path.is_dir) { + return wasi.ERRNO_NOENT; + } + + const { + ret: parent_ret, + parent_entry, + filename, + entry, + } = this.dir.get_parent_dir_and_entry_for_path(path, true); + if (parent_entry == null || filename == null) { + return parent_ret; + } + + if (entry != null) { + const source_is_dir = inode.stat().filetype == wasi.FILETYPE_DIRECTORY; + const target_is_dir = entry.stat().filetype == wasi.FILETYPE_DIRECTORY; + if (source_is_dir && target_is_dir) { + if (allow_dir && entry instanceof Directory) { + if (entry.contents.size == 0) { + // Allow overwriting empty directories + } else { + return wasi.ERRNO_NOTEMPTY; + } + } else { + return wasi.ERRNO_EXIST; + } + } else if (source_is_dir && !target_is_dir) { + return wasi.ERRNO_NOTDIR; + } else if (!source_is_dir && target_is_dir) { + return wasi.ERRNO_ISDIR; + } else if ( + inode.stat().filetype == wasi.FILETYPE_REGULAR_FILE && + entry.stat().filetype == wasi.FILETYPE_REGULAR_FILE + ) { + // Overwriting regular files is fine + } else { + return wasi.ERRNO_EXIST; + } + } + + if (!allow_dir && inode.stat().filetype == wasi.FILETYPE_DIRECTORY) { + return wasi.ERRNO_PERM; + } + + parent_entry.contents.set(filename, inode); + + return wasi.ERRNO_SUCCESS; + } + + path_unlink(path_str: string): { ret: number; inode_obj: Inode | null } { + const { ret: path_ret, path } = Path.from(path_str); + if (path == null) { + return { ret: path_ret, inode_obj: null }; + } + + const { + ret: parent_ret, + parent_entry, + filename, + entry, + } = this.dir.get_parent_dir_and_entry_for_path(path, true); + if (parent_entry == null || filename == null) { + return { ret: parent_ret, inode_obj: null }; + } + + if (entry == null) { + return { ret: wasi.ERRNO_NOENT, inode_obj: null }; + } + + parent_entry.contents.delete(filename); + + return { ret: wasi.ERRNO_SUCCESS, inode_obj: entry }; + } + + path_unlink_file(path_str: string): number { + const { ret: path_ret, path } = Path.from(path_str); + if (path == null) { + return path_ret; + } + + const { + ret: parent_ret, + parent_entry, + filename, + entry, + } = this.dir.get_parent_dir_and_entry_for_path(path, false); + if (parent_entry == null || filename == null || entry == null) { + return parent_ret; + } + if (entry.stat().filetype === wasi.FILETYPE_DIRECTORY) { + return wasi.ERRNO_ISDIR; + } + parent_entry.contents.delete(filename); + return wasi.ERRNO_SUCCESS; + } + + path_remove_directory(path_str: string): number { + const { ret: path_ret, path } = Path.from(path_str); + if (path == null) { + return path_ret; + } + + const { + ret: parent_ret, + parent_entry, + filename, + entry, + } = this.dir.get_parent_dir_and_entry_for_path(path, false); + if (parent_entry == null || filename == null || entry == null) { + return parent_ret; + } + + if ( + !(entry instanceof Directory) || + entry.stat().filetype !== wasi.FILETYPE_DIRECTORY + ) { + return wasi.ERRNO_NOTDIR; + } + if (entry.contents.size !== 0) { + return wasi.ERRNO_NOTEMPTY; + } + if (!parent_entry.contents.delete(filename)) { + return wasi.ERRNO_NOENT; + } + return wasi.ERRNO_SUCCESS; + } + + fd_filestat_get(): { ret: number; filestat: wasi.Filestat } { + return { ret: 0, filestat: this.dir.stat() }; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + fd_filestat_set_size(size: bigint): number { + return wasi.ERRNO_BADF; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + fd_read(size: number): { ret: number; data: Uint8Array } { + return { ret: wasi.ERRNO_BADF, data: new Uint8Array() }; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + fd_pread(size: number, offset: bigint): { ret: number; data: Uint8Array } { + return { ret: wasi.ERRNO_BADF, data: new Uint8Array() }; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + fd_write(data: Uint8Array): { ret: number; nwritten: number } { + return { ret: wasi.ERRNO_BADF, nwritten: 0 }; + } + + fd_pwrite( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + data: Uint8Array, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + offset: bigint, + ): { ret: number; nwritten: number } { + return { ret: wasi.ERRNO_BADF, nwritten: 0 }; + } +} + +export class PreopenDirectory extends OpenDirectory { + prestat_name: string; + + constructor(name: string, contents: Map) { + super(new Directory(contents)); + this.prestat_name = name; + } + + fd_prestat_get(): { ret: number; prestat: wasi.Prestat | null } { + return { + ret: 0, + prestat: wasi.Prestat.dir(this.prestat_name), + }; + } +} + +export class File extends Inode { + data: Uint8Array; + readonly: boolean; + + constructor( + data: ArrayBuffer | SharedArrayBuffer | Uint8Array | Array, + options?: Partial<{ + readonly: boolean; + }>, + ) { + super(); + this.data = new Uint8Array(data); + this.readonly = !!options?.readonly; + } + + path_open(oflags: number, fs_rights_base: bigint, fd_flags: number) { + if ( + this.readonly && + (fs_rights_base & BigInt(wasi.RIGHTS_FD_WRITE)) == + BigInt(wasi.RIGHTS_FD_WRITE) + ) { + // no write permission to file + return { ret: wasi.ERRNO_PERM, fd_obj: null }; + } + + if ((oflags & wasi.OFLAGS_TRUNC) == wasi.OFLAGS_TRUNC) { + if (this.readonly) return { ret: wasi.ERRNO_PERM, fd_obj: null }; + this.data = new Uint8Array([]); + } + + const file = new OpenFile(this); + if (fd_flags & wasi.FDFLAGS_APPEND) file.fd_seek(0n, wasi.WHENCE_END); + return { ret: wasi.ERRNO_SUCCESS, fd_obj: file }; + } + + get size(): bigint { + return BigInt(this.data.byteLength); + } + + stat(): wasi.Filestat { + return new wasi.Filestat(wasi.FILETYPE_REGULAR_FILE, this.size); + } +} + +class Path { + parts: string[] = []; + is_dir: boolean = false; + + static from(path: string): { ret: number; path: Path | null } { + const self = new Path(); + self.is_dir = path.endsWith("/"); + + if (path.startsWith("/")) { + return { ret: wasi.ERRNO_NOTCAPABLE, path: null }; + } + if (path.includes("\0")) { + return { ret: wasi.ERRNO_INVAL, path: null }; + } + + for (const component of path.split("/")) { + if (component === "" || component === ".") { + continue; + } + if (component === "..") { + if (self.parts.pop() == undefined) { + return { ret: wasi.ERRNO_NOTCAPABLE, path: null }; + } + continue; + } + self.parts.push(component); + } + + return { ret: wasi.ERRNO_SUCCESS, path: self }; + } + + to_path_string(): string { + let s = this.parts.join("/"); + if (this.is_dir) { + s += "/"; + } + return s; + } +} + +export class Directory extends Inode { + contents: Map; + + constructor(contents: Map | [string, Inode][]) { + super(); + if (contents instanceof Array) { + this.contents = new Map(contents); + } else { + this.contents = contents; + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + path_open(oflags: number, fs_rights_base: bigint, fd_flags: number) { + return { ret: wasi.ERRNO_SUCCESS, fd_obj: new OpenDirectory(this) }; + } + + stat(): wasi.Filestat { + return new wasi.Filestat(wasi.FILETYPE_DIRECTORY, 0n); + } + + get_entry_for_path(path: Path): { ret: number; entry: Inode | null } { + let entry: Inode = this; + for (const component of path.parts) { + if (!(entry instanceof Directory)) { + return { ret: wasi.ERRNO_NOTDIR, entry: null }; + } + const child = entry.contents.get(component); + if (child !== undefined) { + entry = child; + } else { + debug.log(component); + return { ret: wasi.ERRNO_NOENT, entry: null }; + } + } + + if (path.is_dir) { + if (entry.stat().filetype != wasi.FILETYPE_DIRECTORY) { + return { ret: wasi.ERRNO_NOTDIR, entry: null }; + } + } + + return { ret: wasi.ERRNO_SUCCESS, entry }; + } + + get_parent_dir_and_entry_for_path( + path: Path, + allow_undefined: boolean, + ): { + ret: number; + parent_entry: Directory | null; + filename: string | null; + entry: Inode | null; + } { + const filename = path.parts.pop(); + + if (filename === undefined) { + return { + ret: wasi.ERRNO_INVAL, + parent_entry: null, + filename: null, + entry: null, + }; + } + + const { ret: entry_ret, entry: parent_entry } = + this.get_entry_for_path(path); + if (parent_entry == null) { + return { + ret: entry_ret, + parent_entry: null, + filename: null, + entry: null, + }; + } + if (!(parent_entry instanceof Directory)) { + return { + ret: wasi.ERRNO_NOTDIR, + parent_entry: null, + filename: null, + entry: null, + }; + } + const entry: Inode | undefined | null = parent_entry.contents.get(filename); + if (entry === undefined) { + if (!allow_undefined) { + return { + ret: wasi.ERRNO_NOENT, + parent_entry: null, + filename: null, + entry: null, + }; + } else { + return { ret: wasi.ERRNO_SUCCESS, parent_entry, filename, entry: null }; + } + } + + if (path.is_dir) { + if (entry.stat().filetype != wasi.FILETYPE_DIRECTORY) { + return { + ret: wasi.ERRNO_NOTDIR, + parent_entry: null, + filename: null, + entry: null, + }; + } + } + + return { ret: wasi.ERRNO_SUCCESS, parent_entry, filename, entry }; + } + + create_entry_for_path( + path_str: string, + is_dir: boolean, + ): { ret: number; entry: Inode | null } { + const { ret: path_ret, path } = Path.from(path_str); + if (path == null) { + return { ret: path_ret, entry: null }; + } + + let { + // eslint-disable-next-line prefer-const + ret: parent_ret, + // eslint-disable-next-line prefer-const + parent_entry, + // eslint-disable-next-line prefer-const + filename, + entry, + } = this.get_parent_dir_and_entry_for_path(path, true); + if (parent_entry == null || filename == null) { + return { ret: parent_ret, entry: null }; + } + + if (entry != null) { + return { ret: wasi.ERRNO_EXIST, entry: null }; + } + + debug.log("create", path); + let new_child; + if (!is_dir) { + new_child = new File(new ArrayBuffer(0)); + } else { + new_child = new Directory(new Map()); + } + parent_entry.contents.set(filename, new_child); + entry = new_child; + + return { ret: wasi.ERRNO_SUCCESS, entry }; + } +} + +export class ConsoleStdout extends Fd { + write: (buffer: Uint8Array) => void; + + constructor(write: (buffer: Uint8Array) => void) { + super(); + this.write = write; + } + + fd_filestat_get(): { ret: number; filestat: wasi.Filestat } { + const filestat = new wasi.Filestat( + wasi.FILETYPE_CHARACTER_DEVICE, + BigInt(0), + ); + return { ret: 0, filestat }; + } + + fd_fdstat_get(): { ret: number; fdstat: wasi.Fdstat | null } { + const fdstat = new wasi.Fdstat(wasi.FILETYPE_CHARACTER_DEVICE, 0); + fdstat.fs_rights_base = BigInt(wasi.RIGHTS_FD_WRITE); + return { ret: 0, fdstat }; + } + + fd_write(data: Uint8Array): { ret: number; nwritten: number } { + this.write(data); + return { ret: 0, nwritten: data.byteLength }; + } + + static lineBuffered(write: (line: string) => void): ConsoleStdout { + const dec = new TextDecoder("utf-8", { fatal: false }); + let line_buf = ""; + return new ConsoleStdout((buffer) => { + line_buf += dec.decode(buffer, { stream: true }); + const lines = line_buf.split("\n"); + for (const [i, line] of lines.entries()) { + if (i < lines.length - 1) { + write(line); + } else { + line_buf = line; + } + } + }); + } +} diff --git a/src/fs_opfs.ts b/src/fs_opfs.ts new file mode 100644 index 0000000..7cdde8d --- /dev/null +++ b/src/fs_opfs.ts @@ -0,0 +1,149 @@ +import * as wasi from "./wasi_defs.js"; +import { Fd, Inode } from "./fd.js"; + +// Shim for https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle +// This is not part of the public interface. +export interface FileSystemSyncAccessHandle { + close(): void; + flush(): void; + getSize(): number; + read(buffer: ArrayBuffer | ArrayBufferView, options?: { at: number }): number; + truncate(to: number): void; + write( + buffer: ArrayBuffer | ArrayBufferView, + options?: { at: number }, + ): number; +} + +// Synchronous access to an individual file in the origin private file system. +// Only allowed inside a WebWorker. +export class SyncOPFSFile extends Inode { + handle: FileSystemSyncAccessHandle; + readonly: boolean; + + // FIXME needs a close() method to be called after start() to release the underlying handle + constructor( + handle: FileSystemSyncAccessHandle, + options?: Partial<{ + readonly: boolean; + }>, + ) { + super(); + this.handle = handle; + this.readonly = !!options?.readonly; + } + + path_open(oflags: number, fs_rights_base: bigint, fd_flags: number) { + if ( + this.readonly && + (fs_rights_base & BigInt(wasi.RIGHTS_FD_WRITE)) == + BigInt(wasi.RIGHTS_FD_WRITE) + ) { + // no write permission to file + return { ret: wasi.ERRNO_PERM, fd_obj: null }; + } + + if ((oflags & wasi.OFLAGS_TRUNC) == wasi.OFLAGS_TRUNC) { + if (this.readonly) return { ret: wasi.ERRNO_PERM, fd_obj: null }; + this.handle.truncate(0); + } + + const file = new OpenSyncOPFSFile(this); + if (fd_flags & wasi.FDFLAGS_APPEND) file.fd_seek(0n, wasi.WHENCE_END); + return { ret: wasi.ERRNO_SUCCESS, fd_obj: file }; + } + + get size(): bigint { + return BigInt(this.handle.getSize()); + } + + stat(): wasi.Filestat { + return new wasi.Filestat(wasi.FILETYPE_REGULAR_FILE, this.size); + } +} + +export class OpenSyncOPFSFile extends Fd { + file: SyncOPFSFile; + position: bigint = 0n; + + constructor(file: SyncOPFSFile) { + super(); + this.file = file; + } + + fd_allocate(offset: bigint, len: bigint): number { + if (BigInt(this.file.handle.getSize()) > offset + len) { + // already big enough + } else { + // extend + this.file.handle.truncate(Number(offset + len)); + } + return wasi.ERRNO_SUCCESS; + } + + fd_fdstat_get(): { ret: number; fdstat: wasi.Fdstat | null } { + return { ret: 0, fdstat: new wasi.Fdstat(wasi.FILETYPE_REGULAR_FILE, 0) }; + } + + fd_filestat_get(): { ret: number; filestat: wasi.Filestat } { + return { + ret: 0, + filestat: new wasi.Filestat( + wasi.FILETYPE_REGULAR_FILE, + BigInt(this.file.handle.getSize()), + ), + }; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + fd_filestat_set_size(size: bigint): number { + this.file.handle.truncate(Number(size)); + return wasi.ERRNO_SUCCESS; + } + + fd_read(size: number): { ret: number; data: Uint8Array } { + const buf = new Uint8Array(size); + const n = this.file.handle.read(buf, { at: Number(this.position) }); + this.position += BigInt(n); + return { ret: 0, data: buf.slice(0, n) }; + } + + fd_seek( + offset: number | bigint, + whence: number, + ): { ret: number; offset: bigint } { + let calculated_offset: bigint; + switch (whence) { + case wasi.WHENCE_SET: + calculated_offset = BigInt(offset); + break; + case wasi.WHENCE_CUR: + calculated_offset = this.position + BigInt(offset); + break; + case wasi.WHENCE_END: + calculated_offset = BigInt(this.file.handle.getSize()) + BigInt(offset); + break; + default: + return { ret: wasi.ERRNO_INVAL, offset: 0n }; + } + if (calculated_offset < 0) { + return { ret: wasi.ERRNO_INVAL, offset: 0n }; + } + this.position = calculated_offset; + return { ret: wasi.ERRNO_SUCCESS, offset: this.position }; + } + + fd_write(data: Uint8Array): { ret: number; nwritten: number } { + if (this.file.readonly) return { ret: wasi.ERRNO_BADF, nwritten: 0 }; + + // don't need to extend file manually, just write + const n = this.file.handle.write(data, { at: Number(this.position) }); + this.position += BigInt(n); + return { ret: wasi.ERRNO_SUCCESS, nwritten: n }; + } + + fd_sync(): number { + this.file.handle.flush(); + return wasi.ERRNO_SUCCESS; + } +} diff --git a/src/index.ts b/src/index.ts index bd4236f..5f80030 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,15 @@ import WASI, { WASIProcExit } from "./wasi.js"; export { WASI, WASIProcExit }; -export { Fd } from "./fd.js"; +export { Fd, Inode } from "./fd.js"; export { File, - SyncOPFSFile, Directory, OpenFile, OpenDirectory, - OpenSyncOPFSFile, PreopenDirectory, ConsoleStdout, -} from "./fs_core.js"; +} from "./fs_mem.js"; +export { SyncOPFSFile, OpenSyncOPFSFile } from "./fs_opfs.js"; export { strace } from "./strace.js"; export * as wasi from "./wasi_defs.js"; diff --git a/src/wasi.ts b/src/wasi.ts index abb4e33..a3e4439 100644 --- a/src/wasi.ts +++ b/src/wasi.ts @@ -184,12 +184,15 @@ export default class WASI { fd_advise( fd: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars offset: bigint, + // eslint-disable-next-line @typescript-eslint/no-unused-vars len: bigint, + // eslint-disable-next-line @typescript-eslint/no-unused-vars advice: number, ): number { if (self.fds[fd] != undefined) { - return self.fds[fd].fd_advise(offset, len, advice); + return wasi.ERRNO_SUCCESS; } else { return wasi.ERRNO_BADF; } @@ -212,7 +215,7 @@ export default class WASI { }, fd_datasync(fd: number): number { if (self.fds[fd] != undefined) { - return self.fds[fd].fd_datasync(); + return self.fds[fd].fd_sync(); } else { return wasi.ERRNO_BADF; } @@ -300,9 +303,22 @@ export default class WASI { iovs_ptr, iovs_len, ); - const { ret, nread } = self.fds[fd].fd_pread(buffer8, iovecs, offset); + let nread = 0; + for (const iovec of iovecs) { + const { ret, data } = self.fds[fd].fd_pread(iovec.buf_len, offset); + if (ret != wasi.ERRNO_SUCCESS) { + buffer.setUint32(nread_ptr, nread, true); + return ret; + } + buffer8.set(data, iovec.buf); + nread += data.length; + offset += BigInt(data.length); + if (data.length != iovec.buf_len) { + break; + } + } buffer.setUint32(nread_ptr, nread, true); - return ret; + return wasi.ERRNO_SUCCESS; } else { return wasi.ERRNO_BADF; } @@ -328,12 +344,18 @@ export default class WASI { ): number { // FIXME don't ignore path_len if (self.fds[fd] != undefined) { - const { ret, prestat_dir_name } = self.fds[fd].fd_prestat_dir_name(); - if (prestat_dir_name != null) { - const buffer8 = new Uint8Array(self.inst.exports.memory.buffer); - buffer8.set(prestat_dir_name, path_ptr); + const { ret, prestat } = self.fds[fd].fd_prestat_get(); + if (prestat == null) { + return ret; } - return ret; + const prestat_dir_name = prestat.inner.pr_name; + + const buffer8 = new Uint8Array(self.inst.exports.memory.buffer); + buffer8.set(prestat_dir_name.slice(0, path_len), path_ptr); + + return prestat_dir_name.byteLength > path_len + ? wasi.ERRNO_NAMETOOLONG + : wasi.ERRNO_SUCCESS; } else { return wasi.ERRNO_BADF; } @@ -353,13 +375,25 @@ export default class WASI { iovs_ptr, iovs_len, ); - const { ret, nwritten } = self.fds[fd].fd_pwrite( - buffer8, - iovecs, - offset, - ); + let nwritten = 0; + for (const iovec of iovecs) { + const data = buffer8.slice(iovec.buf, iovec.buf + iovec.buf_len); + const { ret, nwritten: nwritten_part } = self.fds[fd].fd_pwrite( + data, + offset, + ); + if (ret != wasi.ERRNO_SUCCESS) { + buffer.setUint32(nwritten_ptr, nwritten, true); + return ret; + } + nwritten += nwritten_part; + offset += BigInt(nwritten_part); + if (nwritten_part != data.byteLength) { + break; + } + } buffer.setUint32(nwritten_ptr, nwritten, true); - return ret; + return wasi.ERRNO_SUCCESS; } else { return wasi.ERRNO_BADF; } @@ -378,9 +412,21 @@ export default class WASI { iovs_ptr, iovs_len, ); - const { ret, nread } = self.fds[fd].fd_read(buffer8, iovecs); + let nread = 0; + for (const iovec of iovecs) { + const { ret, data } = self.fds[fd].fd_read(iovec.buf_len); + if (ret != wasi.ERRNO_SUCCESS) { + buffer.setUint32(nread_ptr, nread, true); + return ret; + } + buffer8.set(data, iovec.buf); + nread += data.length; + if (data.length != iovec.buf_len) { + break; + } + } buffer.setUint32(nread_ptr, nread, true); - return ret; + return wasi.ERRNO_SUCCESS; } else { return wasi.ERRNO_BADF; } @@ -505,9 +551,22 @@ export default class WASI { iovs_ptr, iovs_len, ); - const { ret, nwritten } = self.fds[fd].fd_write(buffer8, iovecs); + let nwritten = 0; + for (const iovec of iovecs) { + const data = buffer8.slice(iovec.buf, iovec.buf + iovec.buf_len); + const { ret, nwritten: nwritten_part } = + self.fds[fd].fd_write(data); + if (ret != wasi.ERRNO_SUCCESS) { + buffer.setUint32(nwritten_ptr, nwritten, true); + return ret; + } + nwritten += nwritten_part; + if (nwritten_part != data.byteLength) { + break; + } + } buffer.setUint32(nwritten_ptr, nwritten, true); - return ret; + return wasi.ERRNO_SUCCESS; } else { return wasi.ERRNO_BADF; } @@ -523,6 +582,8 @@ export default class WASI { buffer8.slice(path_ptr, path_ptr + path_len), ); return self.fds[fd].path_create_directory(path); + } else { + return wasi.ERRNO_BADF; } }, path_filestat_get( @@ -589,12 +650,14 @@ export default class WASI { const new_path = new TextDecoder("utf-8").decode( buffer8.slice(new_path_ptr, new_path_ptr + new_path_len), ); - return self.fds[new_fd].path_link( - old_fd, - old_flags, + const { ret, inode_obj } = self.fds[old_fd].path_lookup( old_path, - new_path, + old_flags, ); + if (inode_obj == null) { + return ret; + } + return self.fds[new_fd].path_link(new_path, inode_obj, false); } else { return wasi.ERRNO_BADF; } @@ -683,20 +746,39 @@ export default class WASI { } }, path_rename( - // eslint-disable-next-line @typescript-eslint/no-unused-vars fd: number, - // eslint-disable-next-line @typescript-eslint/no-unused-vars old_path_ptr: number, - // eslint-disable-next-line @typescript-eslint/no-unused-vars old_path_len: number, - // eslint-disable-next-line @typescript-eslint/no-unused-vars new_fd: number, - // eslint-disable-next-line @typescript-eslint/no-unused-vars new_path_ptr: number, - // eslint-disable-next-line @typescript-eslint/no-unused-vars new_path_len: number, ): number { - throw "FIXME what is the best abstraction for this?"; + const buffer8 = new Uint8Array(self.inst.exports.memory.buffer); + if (self.fds[fd] != undefined && self.fds[new_fd] != undefined) { + const old_path = new TextDecoder("utf-8").decode( + buffer8.slice(old_path_ptr, old_path_ptr + old_path_len), + ); + const new_path = new TextDecoder("utf-8").decode( + buffer8.slice(new_path_ptr, new_path_ptr + new_path_len), + ); + // eslint-disable-next-line prefer-const + let { ret, inode_obj } = self.fds[fd].path_unlink(old_path); + if (inode_obj == null) { + return ret; + } + ret = self.fds[new_fd].path_link(new_path, inode_obj, true); + if (ret != wasi.ERRNO_SUCCESS) { + if ( + self.fds[fd].path_link(old_path, inode_obj, true) != + wasi.ERRNO_SUCCESS + ) { + throw "path_link should always return success when relinking an inode back to the original place"; + } + } + return ret; + } else { + return wasi.ERRNO_BADF; + } }, path_symlink( old_path_ptr: number, @@ -707,13 +789,15 @@ export default class WASI { ): number { const buffer8 = new Uint8Array(self.inst.exports.memory.buffer); if (self.fds[fd] != undefined) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const old_path = new TextDecoder("utf-8").decode( buffer8.slice(old_path_ptr, old_path_ptr + old_path_len), ); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const new_path = new TextDecoder("utf-8").decode( buffer8.slice(new_path_ptr, new_path_ptr + new_path_len), ); - return self.fds[fd].path_symlink(old_path, new_path); + return wasi.ERRNO_NOTSUP; } else { return wasi.ERRNO_BADF; } diff --git a/src/wasi_defs.ts b/src/wasi_defs.ts index 52f4b5a..db0b37c 100644 --- a/src/wasi_defs.ts +++ b/src/wasi_defs.ts @@ -184,7 +184,7 @@ export const FILETYPE_SYMBOLIC_LINK = 7; export class Dirent { d_next: bigint; - d_ino: bigint = 1n; + d_ino: bigint = 0n; d_namlen: number; d_type: number; dir_name: Uint8Array; @@ -341,14 +341,14 @@ export const SDFLAGS_WR = 1 << 1; export const PREOPENTYPE_DIR = 0; export class PrestatDir { - pr_name_len: number; + pr_name: Uint8Array; - constructor(name_len: number) { - this.pr_name_len = name_len; + constructor(name: string) { + this.pr_name = new TextEncoder().encode(name); } write_bytes(view: DataView, ptr: number) { - view.setUint32(ptr, this.pr_name_len, true); + view.setUint32(ptr, this.pr_name.byteLength, true); } } @@ -358,10 +358,10 @@ export class Prestat { //@ts-ignore strictPropertyInitialization inner: PrestatDir; - static dir(name_len: number): Prestat { + static dir(name: string): Prestat { const prestat = new Prestat(); prestat.tag = PREOPENTYPE_DIR; - prestat.inner = new PrestatDir(name_len); + prestat.inner = new PrestatDir(name); return prestat; } diff --git a/test/adapters/browser/run-test.html b/test/adapters/browser/run-test.html index 1c87557..34e36da 100644 --- a/test/adapters/browser/run-test.html +++ b/test/adapters/browser/run-test.html @@ -6,9 +6,9 @@ const rawPreopens = await window.bindingDerivePreopens(dirs) function transform(entry) { if (entry.kind === "dir") { - const contents = {}; + const contents = new Map(); for (const [name, child] of Object.entries(entry.contents)) { - contents[name] = transform(child); + contents.set(name, transform(child)); } return new Directory(contents); } else if (entry.kind === "file") { @@ -20,9 +20,9 @@ const preopens = [] for (const preopen of rawPreopens) { const { dir, contents } = preopen; - const newContents = {}; + const newContents = new Map(); for (const [name, child] of Object.entries(contents)) { - newContents[name] = transform(child); + newContents.set(name, transform(child)); } preopens.push(new PreopenDirectory(dir, newContents)); } diff --git a/test/adapters/browser/run-wasi.mjs b/test/adapters/browser/run-wasi.mjs index e35845b..b66cb92 100644 --- a/test/adapters/browser/run-wasi.mjs +++ b/test/adapters/browser/run-wasi.mjs @@ -15,7 +15,7 @@ async function derivePreopens(dirs) { entry.buffer = Array.from(entry.buffer); } return { ...out, [name]: entry }; - }, {}); + }, () => {}); preopens.push({ dir, contents }); } return preopens; @@ -24,8 +24,8 @@ async function derivePreopens(dirs) { /** * Configure routes for the browser harness. * - * @param {import('playwright').BrowserContext} context - * @param {string} harnessURL + * @param {import('playwright').BrowserContext} context + * @param {string} harnessURL */ async function configureRoutes(context, harnessURL) { diff --git a/test/adapters/node/run-wasi.mjs b/test/adapters/node/run-wasi.mjs index ab9fd3f..4a82d0d 100644 --- a/test/adapters/node/run-wasi.mjs +++ b/test/adapters/node/run-wasi.mjs @@ -25,14 +25,9 @@ class NodeStdout extends Fd { return { ret: 0, fdstat }; } - fd_write(view8, iovs) { - let nwritten = 0; - for (let iovec of iovs) { - let buffer = view8.slice(iovec.buf, iovec.buf + iovec.buf_len); - this.out.write(buffer); - nwritten += iovec.buf_len; - } - return { ret: 0, nwritten }; + fd_write(data) { + this.out.write(data); + return { ret: 0, nwritten: data.byteLength }; } } @@ -50,8 +45,9 @@ async function derivePreopens(dirs) { default: throw new Error(`Unexpected entry kind: ${entry.kind}`); } - return { ...out, [name]: entry} - }, {}) + out.set(name, entry); + return out; + }, () => new Map()) const preopen = new PreopenDirectory(dir, contents); preopens.push(preopen); } diff --git a/test/adapters/shared/walkFs.mjs b/test/adapters/shared/walkFs.mjs index 16fc548..8d625e0 100644 --- a/test/adapters/shared/walkFs.mjs +++ b/test/adapters/shared/walkFs.mjs @@ -6,12 +6,12 @@ import path from 'path'; * using the given reducer function. * * @typedef {{ kind: "dir", contents: any } | { kind: "file", buffer: Buffer }} Entry - * @param {string} dir + * @param {string} dir * @param {(name: string, entry: Entry, out: any) => any} nextPartialResult - * @param {any} initial + * @param {() => any} initial */ export async function walkFs(dir, nextPartialResult, initial) { - let result = { ...initial } + let result = initial(); const srcContents = await fs.readdir(dir, { withFileTypes: true }); for (let entry of srcContents) { const entryPath = path.join(dir, entry.name); diff --git a/test/skip.json b/test/skip.json index 5bf1aa6..5b75508 100644 --- a/test/skip.json +++ b/test/skip.json @@ -8,13 +8,9 @@ "fdopendir-with-access": "fail" }, "WASI Rust tests": { - "sched_yield": "not implemented yet", - "path_rename": "fail", "path_exists": "fail", - "path_open_dirfd_not_dir": "fail", "fd_filestat_set": "fail", "symlink_create": "fail", - "overwrite_preopen": "fail", "path_open_read_write": "fail", "path_rename_dir_trailing_slashes": "fail", "fd_flags_set": "fail", @@ -22,17 +18,12 @@ "path_link": "fail", "fd_fdstat_set_rights": "fail", "readlink": "fail", - "unlink_file_trailing_slashes": "fail", "path_symlink_trailing_slashes": "fail", "poll_oneoff_stdio": "fail", "dangling_symlink": "fail", - "dir_fd_op_failures": "fail", - "file_allocate": "fail", "nofollow_errors": "fail", "path_open_preopen": "fail", - "fd_readdir": "fail", "symlink_filestat": "fail", - "symlink_loop": "fail", - "interesting_paths": "fail" + "symlink_loop": "fail" } }