open-standards/wai/wai-web/demo/verify_pack.mjs
Transaction Science 2e398cde86 wai-web: pack and packMulti refuse a payload past its u32 field
SECURITY.md and the CHANGELOG named wai-web's writers among those that
refuse a payload whose length does not fit its u32 field, but they still
wrote `payload.len() as u32` and `p.len() as u32`; packMulti's checked_add
caught only a total that overflows, and the claim held only because usize
is 32 bits on wasm32. Both now convert with u32::try_from, as wai-rs's
writers do, and name the payload that does not fit.

verify_pack.mjs: the refused manifest is 2^27 + 1 bytes, one past the
limit; the comment read as if it were 2^27.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 17:14:52 -04:00

113 lines
5.9 KiB
JavaScript

// Checks the browser writers against SPEC §5, *Candidate capabilities*: `pack`
// and `packMulti` refuse a manifest that names a candidate in any place an
// envelope can name one (the WAI1 capability or fallback; a WAI2 component in
// either role), and still write an envelope that names none. The candidates are
// read from the committed export wai/registry/capabilities.json, so a newly
// registered candidate is checked without editing this script. They also refuse a
// manifest that names a member twice in one object (SPEC §2), which no reader reads,
// and one longer than SPEC §2's reader limit, while writing a long one within it.
//
// node verify_pack.mjs [path/to/wai_web.js] [path/to/capabilities.json]
// The package path is relative to this directory (as in the other verify scripts);
// the export path is relative to the working directory (default: this tree's).
// Exit 0 iff every check holds.
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
const pkg = resolve(here, process.argv[2] ?? './pkg-node/wai_web.js');
const exportPath = process.argv[3] ? resolve(process.argv[3]) : resolve(here, '../../registry/capabilities.json');
const wai = await import(pathToFileURL(pkg).href);
const candidates = JSON.parse(readFileSync(exportPath, 'utf8')).capabilities
.filter((c) => c.status === 'candidate')
.map((c) => c.capability);
let pass = true;
const fail = (msg) => { pass = false; console.log(`FAIL ${msg}`); };
const bytes = new Uint8Array([1, 2, 3]);
const v1 = (capability, fallback) => JSON.stringify({
wai: '1.0', media: 'binary', intent: 'improve',
model_requirement: fallback ? { capability, fallback } : { capability },
conditioning: { kind: 'bytes' }, target: null,
});
const v2 = (components) => JSON.stringify({
wai: '1.1', media: 'video', intent: 'improve', target: null,
renditions: components.map(([capability, base]) => base
? { capability, kind: 'bytes', role: 'companion', companion_of: base }
: { capability, kind: 'bytes' }),
});
function refuses(what, write) {
try {
write();
fail(`${what}: written, but a candidate MUST NOT be emitted`);
} catch (e) {
if (!String(e.message ?? e).includes('candidate')) fail(`${what}: refused for another reason: ${e.message ?? e}`);
}
}
if (candidates.length === 0) fail(`no candidate in ${exportPath}`);
for (const cand of candidates) {
refuses(`pack, ${cand} as the capability`, () => wai.pack(v1(cand), bytes));
refuses(`pack, ${cand} as the fallback`, () => wai.pack(v1('wai.image.png', cand), bytes));
refuses(`packMulti, ${cand} as a rendition`, () => wai.packMulti(v2([[cand]]), [bytes]));
refuses(`packMulti, ${cand} as a companion`,
() => wai.packMulti(v2([['wai.video.av1'], [cand, 'wai.video.av1']]), [bytes, bytes]));
}
// A manifest that names a member twice in one object is refused (SPEC §2): every
// reader refuses it, so a writer never emits it.
function refusesTwice(what, write) {
try {
write();
fail(`${what}: written, but no reader reads it`);
} catch (e) {
if (!String(e.message ?? e).includes('twice')) fail(`${what}: refused for another reason: ${e.message ?? e}`);
}
}
const v1Text = v1('wai.image.png');
refusesTwice('pack, a member twice at the top', () => wai.pack(v1Text.slice(0, -1) + ',"zz":1,"zz":2}', bytes));
refusesTwice('pack, a member twice in target', () => wai.pack(v1Text.replace('"target":null', '"target":{"w":1,"w":2}'), bytes));
refusesTwice('pack, a member twice in model_requirement',
() => wai.pack(v1Text.replace('"capability":"wai.image.png"', '"capability":"wai.image.png","q":1,"q":2'), bytes));
refusesTwice('packMulti, a member twice in an entry',
() => wai.packMulti(v2([['wai.video.av1']]).replace('"kind":"bytes"', '"kind":"bytes","zz":1,"zz":2'), [bytes]));
// SPEC §2, *Reader limits*: a manifest of 3 MiB is written as given; one of 2^27 + 1
// bytes, one byte past the limit, is refused (`manifest_too_large`), as every
// reference reader refuses it.
const MAX = 1 << 27;
const LONG = 3 << 20;
// `text` with a member `pad` added so that it is exactly `len` bytes (all ASCII).
const padTo = (text, len) => text.slice(0, -1) + ',"pad":"' + 'x'.repeat(len - text.length - 9) + '"}';
function refusesTooLarge(what, write) {
try {
write();
fail(`${what}: written, but it is past the reader limit`);
} catch (e) {
if (!String(e.message ?? e).includes('manifest_too_large')) fail(`${what}: refused for another reason: ${e.message ?? e}`);
}
}
const v2Text = v2([['wai.video.av1']]);
try {
for (const [what, env] of [['pack', wai.pack(padTo(v1Text, LONG), bytes)], ['packMulti', wai.packMulti(padTo(v2Text, LONG), [bytes])]]) {
if (new DataView(env.buffer, env.byteOffset, env.byteLength).getUint32(4, true) !== LONG) fail(`${what}: a 3 MiB manifest was not written as given`);
}
} catch (e) {
fail(`a manifest within the reader limit was refused: ${e.message ?? e}`);
}
refusesTooLarge('pack, a manifest one byte past the limit', () => wai.pack(padTo(v1Text, MAX + 1), bytes));
refusesTooLarge('packMulti, a manifest one byte past the limit', () => wai.packMulti(padTo(v2Text, MAX + 1), [bytes]));
// An envelope that names no candidate is still written, and parses back.
try {
const one = wai.pack(v1('wai.image.png', 'wai.image.jpeg'), bytes);
if (wai.detectVersion(one) !== 1) fail('pack: the written envelope is not WAI1');
const two = wai.packMulti(v2([['wai.video.av1'], ['wai.meta.hdr10plus', 'wai.video.av1']]), [bytes, bytes]);
if (wai.detectVersion(two) !== 2) fail('packMulti: the written envelope is not WAI2');
} catch (e) {
fail(`an envelope with no candidate was refused: ${e.message ?? e}`);
}
console.log(pass ? `PASS pack and packMulti refuse every candidate (${candidates.join(', ')}), a member named twice and a manifest past the reader limit`
: '\n✗ a browser writer emits what no reader may take');
process.exit(pass ? 0 : 1);