// cases.mjs: the module corpus (corpus.json) read into the bytes a sink passes, and the // checks every runner makes of what came back. For Node; no dependency. // // A case's payload is a path below wai/, or {"hex"}; its set is null, one file, {"external"} // (a file the corpus names by digest and does not hold, read from the --external // directory), or a list of [name, file] pairs passed as waism ยง5's file sequence: for each // file in order, the name's length in one byte, the name, the file's length in four bytes // little-endian, and the file. import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; export function sha256(b) { return createHash('sha256').update(b).digest('hex'); } function file(v, wai, external) { if (typeof v === 'string') return new Uint8Array(readFileSync(join(wai, v))); if (v && typeof v.hex === 'string') return new Uint8Array(Buffer.from(v.hex, 'hex')); if (v && typeof v.file === 'string') return new Uint8Array(readFileSync(join(wai, v.file))); if (v && typeof v.external === 'string') { if (!external) throw new Error(`${v.external} is an external file, and no --external directory was given`); return new Uint8Array(readFileSync(join(external, v.external))); } throw new Error(`not a file: ${JSON.stringify(v)}`); } function set(v, wai, external) { if (v === null) return new Uint8Array(0); if (!Array.isArray(v)) return file(v, wai, external); const parts = []; for (const [name, what] of v) { const b = file(what, wai, external); const n = Buffer.from(name, 'utf8'); if (n.length < 1 || n.length > 255) throw new Error(`a name of ${n.length} bytes`); const len = Buffer.alloc(4); len.writeUInt32LE(b.length); parts.push(Buffer.from([n.length]), n, len, b); } return new Uint8Array(Buffer.concat(parts)); } /** The cases of a corpus.json, with their bytes. */ export function loadCases(corpusPath, { external = null, only = null } = {}) { const doc = JSON.parse(readFileSync(corpusPath, 'utf8')); if (doc.format !== 'waism-module-corpus/1') throw new Error(`${corpusPath}: not a waism-module-corpus/1`); const wai = dirname(dirname(resolve(corpusPath))); const cases = []; for (const c of doc.cases) { if (only && !c.name.startsWith(only)) continue; try { cases.push({ name: c.name, cap: c.capability, payload: file(c.payload, wai, external), set: set(c.set, wai, external), expect: c.expect }); } catch (e) { throw new Error(`${c.name}: ${e.message}`); } } return cases; } /** A result as one line of the run digest: sha256:, refuse: or sink:. */ export function outcomeLine(r) { if (r.output) return `sha256:${sha256(r.output)}`; if (r.refused !== undefined) return `refuse:${r.refused}`; return `sink:${r.sink}`; } /** Whether a result is the one the case states. */ export function holds(c, r) { if (c.expect.sha256 !== undefined) return !!r.output && sha256(r.output) === c.expect.sha256; return r.refused === c.expect.refuse; } /** The check of one run: each case its corpus outcome, and the run digest. */ export function judge(cases, results) { const lines = []; let wrong = 0; const report = []; cases.forEach((c, i) => { const r = results[i]; const line = outcomeLine(r); lines.push(`${c.name} ${line}\n`); if (!holds(c, r)) { wrong++; const want = c.expect.sha256 !== undefined ? `sha256:${c.expect.sha256}` : `refuse:${c.expect.refuse}`; report.push(` ${c.name}: ${line}${r.detail ? ` (${r.detail})` : ''}, not ${want}`); } }); return { wrong, report, digest: sha256(Buffer.from(lines.join(''), 'utf8')) }; }