// sink.mjs: one object decoded through a WAISM module, as waism §5 and §6 checks 11 to 13 // state, in any JavaScript engine: Node, or a browser's page or worker. No dependency. // // decode(module, cap, payload, set) → { output } | { refused } | { sink, detail } // // `module` is a compiled WebAssembly.Module; `cap` the capability's term (a string), // `payload` and `set` Uint8Arrays. The module is instantiated with an empty import object // for this one object and discarded after it. `output` is the module's output (a // Uint8Array) where wai_run returned 0; `refused` its refusal code where it returned 1; // `sink` the sink's own refusal of the run (waism §8): module_resources, module_trap or // module_result_invalid. The wall-clock bound of waism §7 is the caller's: a worker that // runs this, ended from outside when a run passes its limit (module_timeout). const ENCODER = new TextEncoder(); const DECODER = new TextDecoder(); function sinkRefusal(sink, detail) { return { sink, detail: String(detail) }; } /** A refusal code a module returns: ASCII lowercase letters, digits and `_`. */ function isCode(b) { if (b.length === 0) return false; for (const c of b) { if (!((c >= 0x61 && c <= 0x7a) || (c >= 0x30 && c <= 0x39) || c === 0x5f)) return false; } return true; } export async function decode(module, cap, payload, set) { let inst; try { inst = await WebAssembly.instantiate(module, {}); } catch (e) { return sinkRefusal('module_resources', e); } const x = inst.exports; const call = (name, ...args) => x[name](...args); const view = () => new Uint8Array(x.memory.buffer); try { // 1. The interface's version. if (call('wai_abi') !== 1) return sinkRefusal('module_result_invalid', 'wai_abi is not 1'); // 2. The capability, the payload and the set, each at an offset from wai_alloc. An empty // input passes 0, 0, with no call. const args = [0, 0, 0, 0, 0, 0]; const inputs = [ENCODER.encode(cap), payload, set]; for (let i = 0; i < 3; i++) { const b = inputs[i]; if (b.length === 0) continue; const at = call('wai_alloc', b.length) >>> 0; if (at === 0) return sinkRefusal('module_resources', 'wai_alloc gave 0'); const mem = view(); if (at + b.length > mem.length) return sinkRefusal('module_result_invalid', 'wai_alloc gave a region outside the memory'); mem.set(b, at); args[2 * i] = at; args[2 * i + 1] = b.length; } // 3. The run. const status = call('wai_run', ...args); // 4. The output. const ptr = call('wai_out_ptr') >>> 0; const len = call('wai_out_len') >>> 0; const mem = view(); if (ptr + len > mem.length) return sinkRefusal('module_result_invalid', 'the output lies outside the memory'); const out = mem.slice(ptr, ptr + len); // 5. What it is. if (status === 0) return { output: out }; if (status === 1) return isCode(out) ? { refused: DECODER.decode(out) } : sinkRefusal('module_result_invalid', 'a refusal that is not a code'); return sinkRefusal('module_result_invalid', `wai_run gave ${status}`); } catch (e) { if (e instanceof WebAssembly.RuntimeError) return sinkRefusal('module_trap', e.message); return sinkRefusal('module_trap', e); } }