From a354b248ea8ecb7fb10742322d7a36ad71eddf0a Mon Sep 17 00:00:00 2001 From: Casper Beyer Date: Wed, 24 Jun 2020 21:27:31 +0800 Subject: [PATCH] feat(std/wasi): add wasi_snapshot_preview1 (#6441) --- .github/workflows/ci.yml | 6 + .gitignore | 3 + std/wasi/README.md | 42 + std/wasi/snapshot_preview1.ts | 1344 +++++++++++++++++++++++++ std/wasi/snapshot_preview1_test.ts | 126 +++ std/wasi/testdata/std_env_args.rs | 6 + std/wasi/testdata/std_env_vars.rs | 6 + std/wasi/testdata/std_io_stderr.rs | 7 + std/wasi/testdata/std_io_stdin.rs | 9 + std/wasi/testdata/std_io_stdout.rs | 7 + std/wasi/testdata/std_process_exit.rs | 5 + 11 files changed, 1561 insertions(+) create mode 100644 std/wasi/README.md create mode 100644 std/wasi/snapshot_preview1.ts create mode 100644 std/wasi/snapshot_preview1_test.ts create mode 100644 std/wasi/testdata/std_env_args.rs create mode 100644 std/wasi/testdata/std_env_vars.rs create mode 100644 std/wasi/testdata/std_io_stderr.rs create mode 100644 std/wasi/testdata/std_io_stdin.rs create mode 100644 std/wasi/testdata/std_io_stdout.rs create mode 100644 std/wasi/testdata/std_process_exit.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 532a7c1e74..e06cdda7db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,12 @@ jobs: with: rust-version: 1.44.0 + - name: Install rust targets + run: | + # Necessary for the std/wasi tests + rustup target add wasm32-unknown-unknown + rustup target add wasm32-wasi + - name: Install clippy and rustfmt if: matrix.config.kind == 'lint' run: | diff --git a/.gitignore b/.gitignore index eff476d86e..cec330c486 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ yarn.lock # yarn creates this in error. tools/node_modules/ +# compiled wasm files +std/wasi/testdata/snapshot_preview1/ + # MacOS generated files .DS_Store .DS_Store? diff --git a/std/wasi/README.md b/std/wasi/README.md new file mode 100644 index 0000000000..27a7fdbefb --- /dev/null +++ b/std/wasi/README.md @@ -0,0 +1,42 @@ +# wasi + +This module provides an implementation of the WebAssembly System Interface + +## Supported Syscalls + +## Usage + +```typescript +import WASI from "https://deno.land/std/wasi/snapshot_preview1.ts"; + +const wasi = new WASI({ + args: Deno.args, + env: Deno.env, +}); + +const binary = Deno.readAll("path/to/your/module.wasm"); +const module = await WebAssembly.compile(binary); +const instance = await WebAssembly.instantiate(module, { + wasi_snapshot_preview1: wasi.exports, +}); + +wasi.memory = module.exports.memory; + +if (module.exports._start) { + instance.exports._start(); +} else if (module.exports._initialize) { + instance.exports._initialize(); +} else { + throw new Error("No entry point found"); +} +``` + +## Testing + +The test suite for this module spawns rustc processes to compile various example +Rust programs. You must have wasm targets enabled: + +``` +rustup target add wasm32-wasi +rustup target add wasm32-unknown-unknown +``` diff --git a/std/wasi/snapshot_preview1.ts b/std/wasi/snapshot_preview1.ts new file mode 100644 index 0000000000..a1ae5c8b90 --- /dev/null +++ b/std/wasi/snapshot_preview1.ts @@ -0,0 +1,1344 @@ +/* eslint-disable */ + +import { resolve } from "../path/mod.ts"; + +const CLOCKID_REALTIME = 0; +const CLOCKID_MONOTONIC = 1; +const CLOCKID_PROCESS_CPUTIME_ID = 2; +const CLOCKID_THREAD_CPUTIME_ID = 3; + +const ERRNO_SUCCESS = 0; +const ERRNO_2BIG = 1; +const ERRNO_ACCES = 2; +const ERRNO_ADDRINUSE = 3; +const ERRNO_ADDRNOTAVAIL = 4; +const ERRNO_AFNOSUPPORT = 5; +const ERRNO_AGAIN = 6; +const ERRNO_ALREADY = 7; +const ERRNO_BADF = 8; +const ERRNO_BADMSG = 9; +const ERRNO_BUSY = 10; +const ERRNO_CANCELED = 11; +const ERRNO_CHILD = 12; +const ERRNO_CONNABORTED = 13; +const ERRNO_CONNREFUSED = 14; +const ERRNO_CONNRESET = 15; +const ERRNO_DEADLK = 16; +const ERRNO_DESTADDRREQ = 17; +const ERRNO_DOM = 18; +const ERRNO_DQUOT = 19; +const ERRNO_EXIST = 20; +const ERRNO_FAULT = 21; +const ERRNO_FBIG = 22; +const ERRNO_HOSTUNREACH = 23; +const ERRNO_IDRM = 24; +const ERRNO_ILSEQ = 25; +const ERRNO_INPROGRESS = 26; +const ERRNO_INTR = 27; +const ERRNO_INVAL = 28; +const ERRNO_IO = 29; +const ERRNO_ISCONN = 30; +const ERRNO_ISDIR = 31; +const ERRNO_LOOP = 32; +const ERRNO_MFILE = 33; +const ERRNO_MLINK = 34; +const ERRNO_MSGSIZE = 35; +const ERRNO_MULTIHOP = 36; +const ERRNO_NAMETOOLONG = 37; +const ERRNO_NETDOWN = 38; +const ERRNO_NETRESET = 39; +const ERRNO_NETUNREACH = 40; +const ERRNO_NFILE = 41; +const ERRNO_NOBUFS = 42; +const ERRNO_NODEV = 43; +const ERRNO_NOENT = 44; +const ERRNO_NOEXEC = 45; +const ERRNO_NOLCK = 46; +const ERRNO_NOLINK = 47; +const ERRNO_NOMEM = 48; +const ERRNO_NOMSG = 49; +const ERRNO_NOPROTOOPT = 50; +const ERRNO_NOSPC = 51; +const ERRNO_NOSYS = 52; +const ERRNO_NOTCONN = 53; +const ERRNO_NOTDIR = 54; +const ERRNO_NOTEMPTY = 55; +const ERRNO_NOTRECOVERABLE = 56; +const ERRNO_NOTSOCK = 57; +const ERRNO_NOTSUP = 58; +const ERRNO_NOTTY = 59; +const ERRNO_NXIO = 60; +const ERRNO_OVERFLOW = 61; +const ERRNO_OWNERDEAD = 62; +const ERRNO_PERM = 63; +const ERRNO_PIPE = 64; +const ERRNO_PROTO = 65; +const ERRNO_PROTONOSUPPORT = 66; +const ERRNO_PROTOTYPE = 67; +const ERRNO_RANGE = 68; +const ERRNO_ROFS = 69; +const ERRNO_SPIPE = 70; +const ERRNO_SRCH = 71; +const ERRNO_STALE = 72; +const ERRNO_TIMEDOUT = 73; +const ERRNO_TXTBSY = 74; +const ERRNO_XDEV = 75; +const ERRNO_NOTCAPABLE = 76; + +const RIGHTS_FD_DATASYNC = 0x0000000000000001n; +const RIGHTS_FD_READ = 0x0000000000000002n; +const RIGHTS_FD_SEEK = 0x0000000000000004n; +const RIGHTS_FD_FDSTAT_SET_FLAGS = 0x0000000000000008n; +const RIGHTS_FD_SYNC = 0x0000000000000010n; +const RIGHTS_FD_TELL = 0x0000000000000020n; +const RIGHTS_FD_WRITE = 0x0000000000000040n; +const RIGHTS_FD_ADVISE = 0x0000000000000080n; +const RIGHTS_FD_ALLOCATE = 0x0000000000000100n; +const RIGHTS_PATH_CREATE_DIRECTORY = 0x0000000000000200n; +const RIGHTS_PATH_CREATE_FILE = 0x0000000000000400n; +const RIGHTS_PATH_LINK_SOURCE = 0x0000000000000800n; +const RIGHTS_PATH_LINK_TARGET = 0x0000000000001000n; +const RIGHTS_PATH_OPEN = 0x0000000000002000n; +const RIGHTS_FD_READDIR = 0x0000000000004000n; +const RIGHTS_PATH_READLINK = 0x0000000000008000n; +const RIGHTS_PATH_RENAME_SOURCE = 0x0000000000010000n; +const RIGHTS_PATH_RENAME_TARGET = 0x0000000000020000n; +const RIGHTS_PATH_FILESTAT_GET = 0x0000000000040000n; +const RIGHTS_PATH_FILESTAT_SET_SIZE = 0x0000000000080000n; +const RIGHTS_PATH_FILESTAT_SET_TIMES = 0x0000000000100000n; +const RIGHTS_FD_FILESTAT_GET = 0x0000000000200000n; +const RIGHTS_FD_FILESTAT_SET_SIZE = 0x0000000000400000n; +const RIGHTS_FD_FILESTAT_SET_TIMES = 0x0000000000800000n; +const RIGHTS_PATH_SYMLINK = 0x0000000001000000n; +const RIGHTS_PATH_REMOVE_DIRECTORY = 0x0000000002000000n; +const RIGHTS_PATH_UNLINK_FILE = 0x0000000004000000n; +const RIGHTS_POLL_FD_READWRITE = 0x0000000008000000n; +const RIGHTS_SOCK_SHUTDOWN = 0x0000000010000000n; + +const WHENCE_SET = 0; +const WHENCE_CUR = 1; +const WHENCE_END = 2; + +const FILETYPE_UNKNOWN = 0; +const FILETYPE_BLOCK_DEVICE = 1; +const FILETYPE_CHARACTER_DEVICE = 2; +const FILETYPE_DIRECTORY = 3; +const FILETYPE_REGULAR_FILE = 4; +const FILETYPE_SOCKET_DGRAM = 5; +const FILETYPE_SOCKET_STREAM = 6; +const FILETYPE_SYMBOLIC_LINK = 7; + +const ADVICE_NORMAL = 0; +const ADVICE_SEQUENTIAL = 1; +const ADVICE_RANDOM = 2; +const ADVICE_WILLNEED = 3; +const ADVICE_DONTNEED = 4; +const ADVICE_NOREUSE = 5; + +const FDFLAGS_APPEND = 0x0001; +const FDFLAGS_DSYNC = 0x0002; +const FDFLAGS_NONBLOCK = 0x0004; +const FDFLAGS_RSYNC = 0x0008; +const FDFLAGS_SYNC = 0x0010; + +const FSTFLAGS_ATIM = 0x0001; +const FSTFLAGS_ATIM_NOW = 0x0002; +const FSTFLAGS_MTIM = 0x0004; +const FSTFLAGS_MTIM_NOW = 0x0008; + +const LOOKUPFLAGS_SYMLINK_FOLLOW = 0x0001; + +const OFLAGS_CREAT = 0x0001; +const OFLAGS_DIRECTORY = 0x0002; +const OFLAGS_EXCL = 0x0004; +const OFLAGS_TRUNC = 0x0008; + +const EVENTTYPE_CLOCK = 0; +const EVENTTYPE_FD_READ = 1; +const EVENTTYPE_FD_WRITE = 2; + +const EVENTRWFLAGS_FD_READWRITE_HANGUP = 1; +const SUBCLOCKFLAGS_SUBSCRIPTION_CLOCK_ABSTIME = 1; + +const SIGNAL_NONE = 0; +const SIGNAL_HUP = 1; +const SIGNAL_INT = 2; +const SIGNAL_QUIT = 3; +const SIGNAL_ILL = 4; +const SIGNAL_TRAP = 5; +const SIGNAL_ABRT = 6; +const SIGNAL_BUS = 7; +const SIGNAL_FPE = 8; +const SIGNAL_KILL = 9; +const SIGNAL_USR1 = 10; +const SIGNAL_SEGV = 11; +const SIGNAL_USR2 = 12; +const SIGNAL_PIPE = 13; +const SIGNAL_ALRM = 14; +const SIGNAL_TERM = 15; +const SIGNAL_CHLD = 16; +const SIGNAL_CONT = 17; +const SIGNAL_STOP = 18; +const SIGNAL_TSTP = 19; +const SIGNAL_TTIN = 20; +const SIGNAL_TTOU = 21; +const SIGNAL_URG = 22; +const SIGNAL_XCPU = 23; +const SIGNAL_XFSZ = 24; +const SIGNAL_VTALRM = 25; +const SIGNAL_PROF = 26; +const SIGNAL_WINCH = 27; +const SIGNAL_POLL = 28; +const SIGNAL_PWR = 29; +const SIGNAL_SYS = 30; + +const RIFLAGS_RECV_PEEK = 0x0001; +const RIFLAGS_RECV_WAITALL = 0x0002; + +const ROFLAGS_RECV_DATA_TRUNCATED = 0x0001; + +const SDFLAGS_RD = 0x0001; +const SDFLAGS_WR = 0x0002; + +const PREOPENTYPE_DIR = 0; + +const clock_res_realtime = function (): bigint { + return BigInt(1e6); +}; + +const clock_res_monotonic = function (): bigint { + return BigInt(1e3); +}; + +const clock_res_process = clock_res_monotonic; +const clock_res_thread = clock_res_monotonic; + +const clock_time_realtime = function (): bigint { + return BigInt(Date.now()) * BigInt(1e6); +}; + +const clock_time_monotonic = function (): bigint { + const t = performance.now(); + const s = Math.trunc(t); + const ms = Math.floor((t - s) * 1e3); + + return BigInt(s) * BigInt(1e9) + BigInt(ms) * BigInt(1e6); +}; + +const clock_time_process = clock_time_monotonic; +const clock_time_thread = clock_time_monotonic; + +function errno(err: Error) { + switch (err.name) { + case "NotFound": + return ERRNO_NOENT; + + case "PermissionDenied": + return ERRNO_ACCES; + + case "ConnectionRefused": + return ERRNO_CONNREFUSED; + + case "ConnectionReset": + return ERRNO_CONNRESET; + + case "ConnectionAborted": + return ERRNO_CONNABORTED; + + case "NotConnected": + return ERRNO_NOTCONN; + + case "AddrInUse": + return ERRNO_ADDRINUSE; + + case "AddrNotAvailable": + return ERRNO_ADDRNOTAVAIL; + + case "BrokenPipe": + return ERRNO_PIPE; + + case "InvalidData": + return ERRNO_INVAL; + + case "TimedOut": + return ERRNO_TIMEDOUT; + + case "Interrupted": + return ERRNO_INTR; + + case "BadResource": + return ERRNO_BADF; + + case "Busy": + return ERRNO_BUSY; + + default: + return ERRNO_INVAL; + } +} + +export interface ModuleOptions { + args?: string[]; + env?: { [key: string]: string | undefined }; + preopens?: { [key: string]: string }; + memory?: WebAssembly.Memory; +} + +export default class Module { + args: string[]; + env: { [key: string]: string | undefined }; + memory: WebAssembly.Memory; + + // deno-lint-ignore no-explicit-any + fds: any[]; + + // deno-lint-ignore no-explicit-any + exports: { [key: string]: any }; + + constructor(options: ModuleOptions) { + this.args = options.args ? options.args : []; + this.env = options.env ? options.env : {}; + this.memory = options.memory!; + + this.fds = [ + { + type: FILETYPE_CHARACTER_DEVICE, + handle: Deno.stdin, + }, + { + type: FILETYPE_CHARACTER_DEVICE, + handle: Deno.stdout, + }, + { + type: FILETYPE_CHARACTER_DEVICE, + handle: Deno.stderr, + }, + ]; + + if (options.preopens) { + for (const [vpath, path] of Object.entries(options.preopens)) { + const info = Deno.statSync(path); + if (!info.isDirectory) { + throw new TypeError(`${path} is not a directory`); + } + + const type = FILETYPE_DIRECTORY; + + const entry = { + type, + path, + vpath, + }; + + this.fds.push(entry); + } + } + + this.exports = { + args_get: (argv_ptr: number, argv_buf_ptr: number): number => { + const args = this.args; + const text = new TextEncoder(); + const heap = new Uint8Array(this.memory.buffer); + const view = new DataView(this.memory.buffer); + + for (let arg of args) { + view.setUint32(argv_ptr, argv_buf_ptr, true); + argv_ptr += 4; + + const data = text.encode(`${arg}\0`); + heap.set(data, argv_buf_ptr); + argv_buf_ptr += data.length; + } + + return ERRNO_SUCCESS; + }, + + args_sizes_get: (argc_out: number, argv_buf_size_out: number): number => { + const args = this.args; + const text = new TextEncoder(); + const view = new DataView(this.memory.buffer); + + view.setUint32(argc_out, args.length, true); + view.setUint32( + argv_buf_size_out, + args.reduce(function (acc, arg) { + return acc + text.encode(`${arg}\0`).length; + }, 0), + true + ); + + return ERRNO_SUCCESS; + }, + + environ_get: (environ_ptr: number, environ_buf_ptr: number): number => { + const entries = Object.entries(this.env); + const text = new TextEncoder(); + const heap = new Uint8Array(this.memory.buffer); + const view = new DataView(this.memory.buffer); + + for (let [key, value] of entries) { + view.setUint32(environ_ptr, environ_buf_ptr, true); + environ_ptr += 4; + + const data = text.encode(`${key}=${value}\0`); + heap.set(data, environ_buf_ptr); + environ_buf_ptr += data.length; + } + + return ERRNO_SUCCESS; + }, + + environ_sizes_get: ( + environc_out: number, + environ_buf_size_out: number + ): number => { + const entries = Object.entries(this.env); + const text = new TextEncoder(); + const view = new DataView(this.memory.buffer); + + view.setUint32(environc_out, entries.length, true); + view.setUint32( + environ_buf_size_out, + entries.reduce(function (acc, [key, value]) { + return acc + text.encode(`${key}=${value}\0`).length; + }, 0), + true + ); + + return ERRNO_SUCCESS; + }, + + clock_res_get: (id: number, resolution_out: number): number => { + const view = new DataView(this.memory.buffer); + + switch (id) { + case CLOCKID_REALTIME: + view.setBigUint64(resolution_out, clock_res_realtime(), true); + break; + + case CLOCKID_MONOTONIC: + view.setBigUint64(resolution_out, clock_res_monotonic(), true); + break; + + case CLOCKID_PROCESS_CPUTIME_ID: + view.setBigUint64(resolution_out, clock_res_process(), true); + break; + + case CLOCKID_THREAD_CPUTIME_ID: + view.setBigUint64(resolution_out, clock_res_thread(), true); + break; + + default: + return ERRNO_INVAL; + } + + return ERRNO_SUCCESS; + }, + + clock_time_get: ( + id: number, + precision: bigint, + time_out: number + ): number => { + const view = new DataView(this.memory.buffer); + + switch (id) { + case CLOCKID_REALTIME: + view.setBigUint64(time_out, clock_time_realtime(), true); + break; + + case CLOCKID_MONOTONIC: + view.setBigUint64(time_out, clock_time_monotonic(), true); + break; + + case CLOCKID_PROCESS_CPUTIME_ID: + view.setBigUint64(time_out, clock_time_process(), true); + break; + + case CLOCKID_THREAD_CPUTIME_ID: + view.setBigUint64(time_out, clock_time_thread(), true); + break; + + default: + return ERRNO_INVAL; + } + + return ERRNO_SUCCESS; + }, + + fd_advise: ( + fd: number, + offset: bigint, + len: bigint, + advice: number + ): number => { + return ERRNO_NOSYS; + }, + + fd_allocate: (fd: number, offset: bigint, len: bigint): number => { + return ERRNO_NOSYS; + }, + + fd_close: (fd: number): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + entry.handle.close(); + delete this.fds[fd]; + + return ERRNO_SUCCESS; + }, + + fd_datasync: (fd: number): number => { + return ERRNO_NOSYS; + }, + + fd_fdstat_get: (fd: number, stat_out: number): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + const view = new DataView(this.memory.buffer); + view.setUint8(stat_out, entry.type); + view.setUint16(stat_out + 4, 0, true); // TODO + view.setBigUint64(stat_out + 8, 0n, true); // TODO + view.setBigUint64(stat_out + 16, 0n, true); // TODO + + return ERRNO_SUCCESS; + }, + + fd_fdstat_set_flags: (fd: number, flags: number): number => { + return ERRNO_NOSYS; + }, + + fd_fdstat_set_rights: ( + fd: number, + fs_rights_base: bigint, + fs_rights_inheriting: bigint + ): number => { + return ERRNO_NOSYS; + }, + + fd_filestat_get: (fd: number, buf_out: number): number => { + return ERRNO_NOSYS; + }, + + fd_filestat_set_size: (fd: number, size: bigint): number => { + return ERRNO_NOSYS; + }, + + fd_filestat_set_times: ( + fd: number, + atim: bigint, + mtim: bigint, + fst_flags: number + ): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + if (!entry.path) { + return ERRNO_INVAL; + } + + if ((fst_flags & FSTFLAGS_ATIM_NOW) == FSTFLAGS_ATIM_NOW) { + atim = BigInt(Date.now() * 1e6); + } + + if ((fst_flags & FSTFLAGS_MTIM_NOW) == FSTFLAGS_MTIM_NOW) { + mtim = BigInt(Date.now() * 1e6); + } + + try { + Deno.utimeSync(entry.path, Number(atim), Number(mtim)); + } catch (err) { + return errno(err); + } + + return ERRNO_SUCCESS; + }, + + fd_pread: ( + fd: number, + iovs_ptr: number, + iovs_len: number, + offset: bigint, + nread_out: number + ): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + const seek = entry.handle.seekSync(0, Deno.SeekMode.Current); + const view = new DataView(this.memory.buffer); + + let nread = 0; + for (let i = 0; i < iovs_len; i++) { + const data_ptr = view.getUint32(iovs_ptr, true); + iovs_ptr += 4; + + const data_len = view.getUint32(iovs_ptr, true); + iovs_ptr += 4; + + const data = new Uint8Array(this.memory.buffer, data_ptr, data_len); + nread += entry.handle.readSync(data); + } + + entry.handle.seekSync(seek, Deno.SeekMode.Start); + view.setUint32(nread_out, nread, true); + + return ERRNO_SUCCESS; + }, + + fd_prestat_get: (fd: number, buf_out: number): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + if (!entry.vpath) { + return ERRNO_BADF; + } + + const view = new DataView(this.memory.buffer); + view.setUint8(buf_out, PREOPENTYPE_DIR); + view.setUint32( + buf_out + 4, + new TextEncoder().encode(entry.vpath).byteLength, + true + ); + + return ERRNO_SUCCESS; + }, + + fd_prestat_dir_name: ( + fd: number, + path_ptr: number, + path_len: number + ): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + if (!entry.vpath) { + return ERRNO_BADF; + } + + const data = new Uint8Array(this.memory.buffer, path_ptr, path_len); + data.set(new TextEncoder().encode(entry.vpath)); + + return ERRNO_SUCCESS; + }, + + fd_pwrite: ( + fd: number, + iovs_ptr: number, + iovs_len: number, + offset: bigint, + nwritten_out: number + ): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + const seek = entry.handle.seekSync(0, Deno.SeekMode.Current); + const view = new DataView(this.memory.buffer); + + let nwritten = 0; + for (let i = 0; i < iovs_len; i++) { + const data_ptr = view.getUint32(iovs_ptr, true); + iovs_ptr += 4; + + const data_len = view.getUint32(iovs_ptr, true); + iovs_ptr += 4; + + const data = new Uint8Array(this.memory.buffer, data_ptr, data_len); + nwritten += entry.handle.writeSync(data); + } + + entry.handle.seekSync(seek, Deno.SeekMode.Start); + view.setUint32(nwritten_out, nwritten, true); + + return ERRNO_SUCCESS; + }, + + fd_read: ( + fd: number, + iovs_ptr: number, + iovs_len: number, + nread_out: number + ): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + const view = new DataView(this.memory.buffer); + + let nread = 0; + for (let i = 0; i < iovs_len; i++) { + const data_ptr = view.getUint32(iovs_ptr, true); + iovs_ptr += 4; + + const data_len = view.getUint32(iovs_ptr, true); + iovs_ptr += 4; + + const data = new Uint8Array(this.memory.buffer, data_ptr, data_len); + nread += entry.handle.readSync(data); + } + + view.setUint32(nread_out, nread, true); + + return ERRNO_SUCCESS; + }, + + fd_readdir: ( + fd: number, + buf_ptr: number, + buf_len: number, + cookie: bigint, + bufused_out: number + ): number => { + return ERRNO_NOSYS; + }, + + fd_renumber: (fd: number, to: number): number => { + if (!this.fds[fd]) { + return ERRNO_BADF; + } + + if (!this.fds[to]) { + return ERRNO_BADF; + } + + this.fds[to].handle.close(); + this.fds[to] = this.fds[fd]; + delete this.fds[fd]; + + return ERRNO_SUCCESS; + }, + + fd_seek: ( + fd: number, + offset: bigint, + whence: number, + newoffset_out: number + ): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + const view = new DataView(this.memory.buffer); + + try { + // FIXME Deno does not support seeking with big integers + + const newoffset = entry.handle.seekSync(Number(offset), whence); + view.setBigUint64(newoffset_out, BigInt(newoffset), true); + } catch (err) { + return ERRNO_INVAL; + } + + return ERRNO_SUCCESS; + }, + + fd_sync: (fd: number): number => { + return ERRNO_NOSYS; + }, + + fd_tell: (fd: number, offset_out: number): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + const view = new DataView(this.memory.buffer); + + try { + const offset = entry.handle.seekSync(0, Deno.SeekMode.Current); + view.setBigUint64(offset_out, offset, true); + } catch (err) { + return ERRNO_INVAL; + } + + return ERRNO_NOSYS; + }, + + fd_write: ( + fd: number, + iovs_ptr: number, + iovs_len: number, + nwritten_out: number + ): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + const view = new DataView(this.memory.buffer); + + let nwritten = 0; + for (let i = 0; i < iovs_len; i++) { + const data_ptr = view.getUint32(iovs_ptr, true); + iovs_ptr += 4; + + const data_len = view.getUint32(iovs_ptr, true); + iovs_ptr += 4; + + nwritten += entry.handle.writeSync( + new Uint8Array(this.memory.buffer, data_ptr, data_len) + ); + } + + view.setUint32(nwritten_out, nwritten, true); + + return ERRNO_SUCCESS; + }, + + path_create_directory: ( + fd: number, + path_ptr: number, + path_len: number + ): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + if (!entry.path) { + return ERRNO_INVAL; + } + + const text = new TextDecoder(); + const data = new Uint8Array(this.memory.buffer, path_ptr, path_len); + const path = resolve(entry.path, text.decode(data)); + + try { + Deno.mkdirSync(path); + } catch (err) { + return errno(err); + } + + return ERRNO_SUCCESS; + }, + + path_filestat_get: ( + fd: number, + flags: number, + path_ptr: number, + path_len: number, + buf_out: number + ): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + if (!entry.path) { + return ERRNO_INVAL; + } + + const text = new TextDecoder(); + const data = new Uint8Array(this.memory.buffer, path_ptr, path_len); + const path = resolve(entry.path, text.decode(data)); + + const view = new DataView(this.memory.buffer); + + try { + const info = Deno.statSync(path); + + view.setBigUint64(buf_out, BigInt(info.dev ? info.dev : 0), true); + buf_out += 8; + + view.setBigUint64(buf_out, BigInt(info.ino ? info.ino : 0), true); + buf_out += 8; + + switch (true) { + case info.isFile: + view.setUint8(buf_out, FILETYPE_REGULAR_FILE); + buf_out += 4; + break; + + case info.isDirectory: + view.setUint8(buf_out, FILETYPE_DIRECTORY); + buf_out += 4; + break; + + case info.isSymlink: + view.setUint8(buf_out, FILETYPE_SYMBOLIC_LINK); + buf_out += 4; + break; + + default: + view.setUint8(buf_out, FILETYPE_UNKNOWN); + buf_out += 4; + break; + } + + view.setUint32(buf_out, Number(info.nlink), true); + buf_out += 4; + + view.setBigUint64(buf_out, BigInt(info.size), true); + buf_out += 8; + + view.setBigUint64( + buf_out, + BigInt(info.atime ? info.atime.getTime() * 1e6 : 0), + true + ); + buf_out += 8; + + view.setBigUint64( + buf_out, + BigInt(info.mtime ? info.mtime.getTime() * 1e6 : 0), + true + ); + buf_out += 8; + + view.setBigUint64( + buf_out, + BigInt(info.birthtime ? info.birthtime.getTime() * 1e6 : 0), + true + ); + buf_out += 8; + } catch (err) { + return errno(err); + } + + return ERRNO_SUCCESS; + }, + + path_filestat_set_times: ( + fd: number, + flags: number, + path_ptr: number, + path_len: number, + atim: bigint, + mtim: bigint, + fst_flags: number + ): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + if (!entry.path) { + return ERRNO_INVAL; + } + + const text = new TextDecoder(); + const data = new Uint8Array(this.memory.buffer, path_ptr, path_len); + const path = resolve(entry.path, text.decode(data)); + + if ((fst_flags & FSTFLAGS_ATIM_NOW) == FSTFLAGS_ATIM_NOW) { + atim = BigInt(Date.now()) * BigInt(1e6); + } + + if ((fst_flags & FSTFLAGS_MTIM_NOW) == FSTFLAGS_MTIM_NOW) { + mtim = BigInt(Date.now()) * BigInt(1e6); + } + + try { + Deno.utimeSync(path, Number(atim), Number(mtim)); + } catch (err) { + return errno(err); + } + + return ERRNO_SUCCESS; + }, + + path_link: ( + old_fd: number, + old_flags: number, + old_path_ptr: number, + old_path_len: number, + new_fd: number, + new_path_ptr: number, + new_path_len: number + ): number => { + const old_entry = this.fds[old_fd]; + const new_entry = this.fds[new_fd]; + if (!old_entry || !new_entry) { + return ERRNO_BADF; + } + + if (!old_entry.path || !new_entry.path) { + return ERRNO_INVAL; + } + + const text = new TextDecoder(); + const old_data = new Uint8Array( + this.memory.buffer, + old_path_ptr, + old_path_len + ); + const old_path = resolve(old_entry.path, text.decode(old_data)); + const new_data = new Uint8Array( + this.memory.buffer, + new_path_ptr, + new_path_len + ); + const new_path = resolve(new_entry.path, text.decode(new_data)); + + try { + Deno.linkSync(old_path, new_path); + } catch (err) { + return errno(err); + } + + return ERRNO_SUCCESS; + }, + + path_open: ( + fd: number, + dirflags: number, + path_ptr: number, + path_len: number, + oflags: number, + fs_rights_base: number | bigint, + fs_rights_inherting: number | bigint, + fdflags: number, + opened_fd_out: number + ): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + if (!entry.path) { + return ERRNO_INVAL; + } + + const text = new TextDecoder(); + const data = new Uint8Array(this.memory.buffer, path_ptr, path_len); + const path = resolve(entry.path, text.decode(data)); + + const options = { + read: false, + write: false, + append: false, + truncate: false, + create: false, + createNew: false, + }; + + if ((oflags & OFLAGS_CREAT) !== 0) { + options.create = true; + options.write = true; + } + + if ((oflags & OFLAGS_DIRECTORY) !== 0) { + // TODO (caspervonb) review if we can + // emulate this; unix supports opening + // directories, windows does not. + } + + if ((oflags & OFLAGS_EXCL) !== 0) { + options.createNew = true; + } + + if ((oflags & OFLAGS_TRUNC) !== 0) { + options.truncate = true; + options.write = true; + } + + if ( + (BigInt(fs_rights_base) & + BigInt(RIGHTS_FD_READ | RIGHTS_FD_READDIR)) != + 0n + ) { + options.read = true; + } + + if ( + (BigInt(fs_rights_base) & + BigInt( + RIGHTS_FD_DATASYNC | + RIGHTS_FD_WRITE | + RIGHTS_FD_ALLOCATE | + RIGHTS_FD_FILESTAT_SET_SIZE + )) != + 0n + ) { + options.write = true; + } + + if ((fdflags & FDFLAGS_APPEND) != 0) { + options.append = true; + } + + if ((fdflags & FDFLAGS_DSYNC) != 0) { + // TODO (caspervonb) review if we can emulate this. + } + + if ((fdflags & FDFLAGS_NONBLOCK) != 0) { + // TODO (caspervonb) review if we can emulate this. + } + + if ((fdflags & FDFLAGS_RSYNC) != 0) { + // TODO (caspervonb) review if we can emulate this. + } + + if ((fdflags & FDFLAGS_SYNC) != 0) { + // TODO (caspervonb) review if we can emulate this. + } + + if (!options.read && !options.write && !options.truncate) { + options.read = true; + } + + try { + const handle = Deno.openSync(path, options); + const opened_fd = + this.fds.push({ + handle, + path, + }) - 1; + + const view = new DataView(this.memory.buffer); + view.setUint32(opened_fd_out, opened_fd, true); + } catch (err) { + return errno(err); + } + + return ERRNO_SUCCESS; + }, + + path_readlink: ( + fd: number, + path_ptr: number, + path_len: number, + buf_ptr: number, + buf_len: number, + bufused_out: number + ): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + if (!entry.path) { + return ERRNO_INVAL; + } + + const view = new DataView(this.memory.buffer); + const heap = new Uint8Array(this.memory.buffer); + + const data = new Uint8Array(this.memory.buffer, path_ptr, path_len); + const path = resolve(entry.path, new TextDecoder().decode(data)); + + try { + const link = Deno.readLinkSync(path); + const data = new TextEncoder().encode(link); + heap.set(new Uint8Array(data, 0, buf_len), buf_ptr); + + const bufused = Math.min(data.byteLength, buf_len); + view.setUint32(bufused_out, bufused, true); + } catch (err) { + return errno(err); + } + + return ERRNO_SUCCESS; + }, + + path_remove_directory: ( + fd: number, + path_ptr: number, + path_len: number + ): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + if (!entry.path) { + return ERRNO_INVAL; + } + + const text = new TextDecoder(); + const data = new Uint8Array(this.memory.buffer, path_ptr, path_len); + const path = resolve(entry.path, text.decode(data)); + + try { + if (!Deno.statSync(path).isDirectory) { + return ERRNO_NOTDIR; + } + + Deno.removeSync(path); + } catch (err) { + return errno(err); + } + + return ERRNO_SUCCESS; + }, + + path_rename: ( + fd: number, + old_path_ptr: number, + old_path_len: number, + new_fd: number, + new_path_ptr: number, + new_path_len: number + ): number => { + const old_entry = this.fds[fd]; + const new_entry = this.fds[new_fd]; + if (!old_entry || !new_entry) { + return ERRNO_BADF; + } + + if (!old_entry.path || !new_entry.path) { + return ERRNO_INVAL; + } + + const text = new TextDecoder(); + const old_data = new Uint8Array( + this.memory.buffer, + old_path_ptr, + old_path_len + ); + const old_path = resolve(old_entry.path, text.decode(old_data)); + const new_data = new Uint8Array( + this.memory.buffer, + new_path_ptr, + new_path_len + ); + const new_path = resolve(new_entry.path, text.decode(new_data)); + + try { + Deno.renameSync(old_path, new_path); + } catch (err) { + return errno(err); + } + + return ERRNO_SUCCESS; + }, + + path_symlink: ( + old_path_ptr: number, + old_path_len: number, + fd: number, + new_path_ptr: number, + new_path_len: number + ): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + if (!entry.path) { + return ERRNO_INVAL; + } + + const text = new TextDecoder(); + const old_data = new Uint8Array( + this.memory.buffer, + old_path_ptr, + old_path_len + ); + const old_path = text.decode(old_data); + const new_data = new Uint8Array( + this.memory.buffer, + new_path_ptr, + new_path_len + ); + const new_path = resolve(entry.path, text.decode(new_data)); + + try { + Deno.symlinkSync(old_path, new_path); + } catch (err) { + return errno(err); + } + + return ERRNO_SUCCESS; + }, + + path_unlink_file: ( + fd: number, + path_ptr: number, + path_len: number + ): number => { + const entry = this.fds[fd]; + if (!entry) { + return ERRNO_BADF; + } + + if (!entry.path) { + return ERRNO_INVAL; + } + + const text = new TextDecoder(); + const data = new Uint8Array(this.memory.buffer, path_ptr, path_len); + const path = resolve(entry.path, text.decode(data)); + + try { + Deno.removeSync(path); + } catch (err) { + return errno(err); + } + + return ERRNO_SUCCESS; + }, + + poll_oneoff: ( + in_ptr: number, + out_ptr: number, + nsubscriptions: number, + nevents_out: number + ): number => { + return ERRNO_NOSYS; + }, + + proc_exit: (rval: number): never => { + Deno.exit(rval); + }, + + proc_raise: (sig: number): number => { + return ERRNO_NOSYS; + }, + + sched_yield: (): number => { + return ERRNO_SUCCESS; + }, + + random_get: (buf_ptr: number, buf_len: number): number => { + const buffer = new Uint8Array(this.memory.buffer, buf_ptr, buf_len); + crypto.getRandomValues(buffer); + + return ERRNO_SUCCESS; + }, + + sock_recv: ( + fd: number, + ri_data_ptr: number, + ri_data_len: number, + ri_flags: number, + ro_datalen_out: number, + ro_flags_out: number + ): number => { + return ERRNO_NOSYS; + }, + + sock_send: ( + fd: number, + si_data_ptr: number, + si_data_len: number, + si_flags: number, + so_datalen_out: number + ): number => { + return ERRNO_NOSYS; + }, + + sock_shutdown: (fd: number, how: number): number => { + return ERRNO_NOSYS; + }, + }; + } +} diff --git a/std/wasi/snapshot_preview1_test.ts b/std/wasi/snapshot_preview1_test.ts new file mode 100644 index 0000000000..9d8892f9cd --- /dev/null +++ b/std/wasi/snapshot_preview1_test.ts @@ -0,0 +1,126 @@ +/* eslint-disable */ + +import { assert, assertEquals } from "../testing/asserts.ts"; +import * as path from "../path/mod.ts"; +import WASI from "./snapshot_preview1.ts"; + +if (import.meta.main) { + const options = JSON.parse(Deno.args[0]); + const binary = await Deno.readFile(Deno.args[1]); + const module = await WebAssembly.compile(binary); + + const wasi = new WASI({ + env: options.env, + args: options.args, + preopens: options.preopens, + }); + + const instance = new WebAssembly.Instance(module, { + wasi_snapshot_preview1: wasi.exports, + }); + + wasi.memory = instance.exports.memory; + + instance.exports._start(); +} else { + const rootdir = path.dirname(path.fromFileUrl(import.meta.url)); + const testdir = path.join(rootdir, "testdata"); + const outdir = path.join(testdir, "snapshot_preview1"); + + for await (const entry of Deno.readDir(testdir)) { + if (!entry.name.endsWith(".rs")) { + continue; + } + + const process = Deno.run({ + cmd: [ + "rustc", + "--target", + "wasm32-wasi", + "--out-dir", + outdir, + path.join(testdir, entry.name), + ], + stdout: "inherit", + stderr: "inherit", + }); + + const status = await process.status(); + assert(status.success); + + process.close(); + + // TODO(caspervonb) allow the prelude to span multiple lines + const source = await Deno.readTextFile(path.join(testdir, entry.name)); + const prelude = source.match(/^\/\/\s*\{.*/); + if (prelude) { + const basename = entry.name.replace(/.rs$/, ".json"); + await Deno.writeTextFile( + path.join(outdir, basename), + prelude[0].slice(2) + ); + } + } + + for await (const entry of Deno.readDir(outdir)) { + if (!entry.name.endsWith(".wasm")) { + continue; + } + + Deno.test(entry.name, async function () { + const basename = entry.name.replace(/\.wasm$/, ".json"); + const prelude = await Deno.readTextFile(path.resolve(outdir, basename)); + const options = JSON.parse(prelude); + + const process = await Deno.run({ + cwd: testdir, + cmd: [ + `${Deno.execPath()}`, + "run", + "--quiet", + "--unstable", + "--v8-flags=--experimental-wasm-bigint", + "--allow-all", + import.meta.url, + prelude, + path.resolve(outdir, entry.name), + ], + stdin: "piped", + stdout: "piped", + stderr: "piped", + }); + + if (options.stdin) { + const stdin = new TextEncoder().encode(options.stdin); + await Deno.writeAll(process.stdin, stdin); + } + + process.stdin.close(); + + const stdout = await Deno.readAll(process.stdout); + + if (options.stdout) { + assertEquals(new TextDecoder().decode(stdout), options.stdout); + } else { + await Deno.writeAll(Deno.stdout, stdout); + } + + process.stdout.close(); + + const stderr = await Deno.readAll(process.stderr); + + if (options.stderr) { + assertEquals(new TextDecoder().decode(stderr), options.stderr); + } else { + await Deno.writeAll(Deno.stderr, stderr); + } + + process.stderr.close(); + + const status = await process.status(); + assertEquals(status.code, options.exitCode ? +options.exitCode : 0); + + process.close(); + }); + } +} diff --git a/std/wasi/testdata/std_env_args.rs b/std/wasi/testdata/std_env_args.rs new file mode 100644 index 0000000000..09151e5492 --- /dev/null +++ b/std/wasi/testdata/std_env_args.rs @@ -0,0 +1,6 @@ +// { "args": ["one", "two", "three" ]} + +fn main() { + let args = std::env::args(); + assert_eq!(args.len(), 3); +} diff --git a/std/wasi/testdata/std_env_vars.rs b/std/wasi/testdata/std_env_vars.rs new file mode 100644 index 0000000000..5c088a5faf --- /dev/null +++ b/std/wasi/testdata/std_env_vars.rs @@ -0,0 +1,6 @@ +// { "env": { "one": "1", "two": "2", "three": "3" } } + +fn main() { + let vars = std::env::vars(); + assert_eq!(vars.count(), 3); +} diff --git a/std/wasi/testdata/std_io_stderr.rs b/std/wasi/testdata/std_io_stderr.rs new file mode 100644 index 0000000000..8e1dee30ee --- /dev/null +++ b/std/wasi/testdata/std_io_stderr.rs @@ -0,0 +1,7 @@ +// { "stderr": "Hello, stderr!" } + +use std::io::Write; + +fn main() { + assert!(std::io::stderr().write_all(b"Hello, stderr!").is_ok()) +} diff --git a/std/wasi/testdata/std_io_stdin.rs b/std/wasi/testdata/std_io_stdin.rs new file mode 100644 index 0000000000..170d575c55 --- /dev/null +++ b/std/wasi/testdata/std_io_stdin.rs @@ -0,0 +1,9 @@ +// { "stdin": "Hello, stdin!" } + +use std::io::Read; + +fn main() { + let mut buffer = String::new(); + assert!(std::io::stdin().read_to_string(&mut buffer).is_ok()); + assert_eq!(buffer, "Hello, stdin!") +} diff --git a/std/wasi/testdata/std_io_stdout.rs b/std/wasi/testdata/std_io_stdout.rs new file mode 100644 index 0000000000..3c9058a0dd --- /dev/null +++ b/std/wasi/testdata/std_io_stdout.rs @@ -0,0 +1,7 @@ +// { "stdout": "Hello, stdout!" } + +use std::io::Write; + +fn main() { + assert!(std::io::stdout().write_all(b"Hello, stdout!").is_ok()) +} diff --git a/std/wasi/testdata/std_process_exit.rs b/std/wasi/testdata/std_process_exit.rs new file mode 100644 index 0000000000..0a0287c545 --- /dev/null +++ b/std/wasi/testdata/std_process_exit.rs @@ -0,0 +1,5 @@ +// { "exitCode": "120" } + +fn main() { + std::process::exit(120); +}