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]>
113 lines
5.9 KiB
JavaScript
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);
|