// Canonical base64 data-URL grammar contract. // // The client always produces a canonical `data:;base64,` // URL (canvas.toDataURL). Anything else is a hand-crafted request, so ingress // must require the canonical grammar and a byte-exact round trip rather than // silently repairing missing padding, embedded newlines, or non-zero trailing // pad bits. The exact supported MIME policy is unchanged: JPEG/PNG/WebP only. import test from 'node:test'; import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { decodeImageDataUrl, validateImageIngress } from '../src/image-ingress.js'; const fixture = (name) => fileURLToPath(new URL(`../tests/fixtures/${name}`, import.meta.url)); const clean = await readFile(fixture('ingress-clean.jpg')); const canonical = clean.toString('base64'); const ingest = (imageDataUrl) => validateImageIngress({ consent: true, imageDataUrl }); test('canonical base64 data URLs round-trip byte-exactly', () => { const decoded = decodeImageDataUrl(`data:image/jpeg;base64,${canonical}`); assert.ok(decoded, 'canonical data URL must decode'); assert.equal(decoded.mime, 'image/jpeg'); assert.ok(decoded.bytes.equals(clean), 'decoded bytes must equal the source bytes exactly'); }); test('missing base64 padding is rejected', async () => { const unpadded = canonical.replace(/=+$/, ''); assert.notEqual(unpadded, canonical, 'fixture must actually require padding'); assert.equal(decodeImageDataUrl(`data:image/jpeg;base64,${unpadded}`), null); await assert.rejects(() => ingest(`data:image/jpeg;base64,${unpadded}`), /not a supported image/i); }); test('excess base64 padding is rejected', async () => { assert.equal(decodeImageDataUrl(`data:image/jpeg;base64,${canonical}===`), null); await assert.rejects(() => ingest(`data:image/jpeg;base64,${canonical}====`), /not a supported image/i); }); test('CR and LF inside the base64 payload are rejected', async () => { const withCrlf = `${canonical.slice(0, 40)}\r\n${canonical.slice(40)}`; const withLf = `${canonical.slice(0, 40)}\n${canonical.slice(40)}`; const withCr = `${canonical.slice(0, 40)}\r${canonical.slice(40)}`; for (const payload of [withCrlf, withLf, withCr]) { assert.equal(decodeImageDataUrl(`data:image/jpeg;base64,${payload}`), null); await assert.rejects(() => ingest(`data:image/jpeg;base64,${payload}`), /not a supported image/i); } }); test('non-canonical encodings are rejected: non-zero trailing pad bits, whitespace, URL-safe alphabet', async () => { const nonZeroPadBits = `${canonical.slice(0, -2)}/=`; const withSpace = `${canonical.slice(0, 20)} ${canonical.slice(20)}`; const urlSafe = canonical.replace(/\+/g, '-').replace(/\//g, '_'); for (const payload of [nonZeroPadBits, withSpace]) { assert.equal(decodeImageDataUrl(`data:image/jpeg;base64,${payload}`), null); await assert.rejects(() => ingest(`data:image/jpeg;base64,${payload}`), /not a supported image/i); } if (urlSafe !== canonical) { assert.equal(decodeImageDataUrl(`data:image/jpeg;base64,${urlSafe}`), null); } }); test('the supported MIME policy is exact and unchanged', async () => { for (const mime of ['image/jpeg', 'image/png', 'image/webp']) { assert.ok(decodeImageDataUrl(`data:${mime};base64,${canonical}`), `${mime} must remain supported`); } for (const mime of ['image/gif', 'image/svg+xml', 'text/html', 'application/octet-stream', '']) { assert.equal(decodeImageDataUrl(`data:${mime};base64,${canonical}`), null, `${mime} must not be supported`); await assert.rejects(() => ingest(`data:${mime};base64,${canonical}`), /not a supported image/i); } }); test('malformed data-URL envelopes are rejected without repair', async () => { const malformed = [ `data:image/jpeg,${canonical}`, // no base64 token `data:image/jpeg;base64;${canonical}`, // wrong separator `DATA:image/jpeg;base64,${canonical}`, // scheme casing is not canonical here `data:image/jpeg;base64,`, // empty payload ` data:image/jpeg;base64,${canonical}`, // leading whitespace `data:image/jpeg;base64,${canonical} `, // trailing whitespace `data:image/jpeg;charset=utf-8;base64,${canonical}`, // extra parameters canonical, // bare base64, no envelope ]; for (const value of malformed) { assert.equal(decodeImageDataUrl(value), null, `must reject: ${value.slice(0, 42)}`); await assert.rejects(() => ingest(value), /not a supported image/i); } }); test('a canonical supported data URL still completes ingress end to end', async () => { const result = await ingest(`data:image/jpeg;base64,${canonical}`); assert.equal(result.format, 'jpeg'); assert.equal(result.mime, 'image/jpeg'); assert.match(result.imageDataUrl, /^data:image\/jpeg;base64,[A-Za-z0-9+/]+={0,2}$/); });