/** * scan-limits.test.ts — PB-26-207 resource bounds on the Codex scan path. * * Every limit here is derived from the Codex format, not from available * memory: raw scanner text ≤ 4 KiB before JSON.parse; `d` ≤ base64url * expansion of one 350-byte shard before atob; decoded chunk ≤ shard size + * explicit headroom; `c`/`h` exact lengths and integer ranges before any * integrity work; buffer bytes/chunks ≤ MAX_STRIPE_COUNT × max shards per * stripe × chunk size. The camera gate allows one parse/add in flight. */ import { beforeEach, describe, expect, it, vi } from "vitest"; import { CHUNK_SIZE, CODEX_MAX_PARITY_SHARDS, MAX_DATA_SHARDS_PER_STRIPE, MAX_STRIPE_COUNT, type ChunkEnvelope, } from "../../shared/codex-types"; import { buildChunkEnvelope, bytesToBase64Url, sha256Hex } from "./helpers"; import { _resetReedSolomonCacheForTest } from "./rs-loader"; import { CHUNK_HASH_LENGTH, ChunkBuffer, createScanGate, MAX_BUFFER_BYTES, MAX_BUFFER_CHUNKS, MAX_CHUNK_B64_CHARS, MAX_DECODED_CHUNK_BYTES, MAX_RAW_SCAN_CHARS, MAX_SHARDS_PER_STRIPE, parseRawScan, VERIFY_CODE_LENGTH, } from "./restore"; beforeEach(() => { _resetReedSolomonCacheForTest(); }); const VERIFY_CODE = "PX-TEST-ABCD"; async function makeEnv(opts: { bytes?: Uint8Array; index?: number; total?: number; verifyCode?: string; stripe?: number; stripeCount?: number; } = {}): Promise { return buildChunkEnvelope( opts.bytes ?? new Uint8Array(CHUNK_SIZE).fill(7), opts.index ?? 0, opts.total ?? 5, false, opts.verifyCode ?? VERIFY_CODE, opts.stripe ?? 0, opts.stripeCount ?? 1, ); } describe("PB-26-207 limit constants derive from the Codex format", () => { it("raw scan ceiling is 4 KiB", () => { expect(MAX_RAW_SCAN_CHARS).toBe(4096); }); it("d ceiling is the base64url expansion of one shard", () => { expect(MAX_CHUNK_B64_CHARS).toBe(Math.ceil(CHUNK_SIZE / 3) * 4); }); it("decoded chunk ceiling is one shard plus explicit headroom", () => { expect(MAX_DECODED_CHUNK_BYTES).toBeGreaterThan(CHUNK_SIZE); expect(MAX_DECODED_CHUNK_BYTES - CHUNK_SIZE).toBeLessThanOrEqual(128); }); it("aggregate ceiling is stripes × max shards per stripe × chunk size", () => { expect(MAX_SHARDS_PER_STRIPE).toBe( MAX_DATA_SHARDS_PER_STRIPE + CODEX_MAX_PARITY_SHARDS, ); expect(MAX_BUFFER_CHUNKS).toBe(MAX_STRIPE_COUNT * MAX_SHARDS_PER_STRIPE); expect(MAX_BUFFER_BYTES).toBe(MAX_BUFFER_CHUNKS * CHUNK_SIZE); }); it("c and h lengths match the documented envelope format", () => { expect(VERIFY_CODE_LENGTH).toBe("PX-XXXX-XXXX".length); expect(CHUNK_HASH_LENGTH).toBe(32); }); }); describe("PB-26-207 parseRawScan bounds", () => { it("rejects oversized raw text without calling JSON.parse", async () => { const env = await makeEnv(); const raw = JSON.stringify(env); expect(raw.length).toBeLessThan(MAX_RAW_SCAN_CHARS); const padded = raw.padEnd(MAX_RAW_SCAN_CHARS + 1, " "); const spy = vi.spyOn(JSON, "parse"); try { expect(parseRawScan(padded)).toEqual({ error: "parse_error" }); expect(spy).not.toHaveBeenCalled(); } finally { spy.mockRestore(); } }); it("accepts a valid envelope padded to exactly the raw ceiling", async () => { const env = await makeEnv(); const padded = JSON.stringify(env).padEnd(MAX_RAW_SCAN_CHARS, " "); expect(padded.length).toBe(MAX_RAW_SCAN_CHARS); expect(parseRawScan(padded)).toMatchObject({ v: 3, c: VERIFY_CODE }); }); it("rejects non-string raw input without throwing", () => { expect(parseRawScan(12345 as unknown as string)).toEqual({ error: "parse_error", }); }); it("honest envelopes sit far below the raw ceiling", async () => { const env = await makeEnv(); expect(JSON.stringify(env).length).toBeLessThan(MAX_RAW_SCAN_CHARS / 2); }); it("rejects wrong-length c and h before integrity work", async () => { const env = await makeEnv(); expect( parseRawScan(JSON.stringify({ ...env, c: "PX-SHORT" })), ).toEqual({ error: "parse_error" }); expect( parseRawScan(JSON.stringify({ ...env, c: `${VERIFY_CODE}XX` })), ).toEqual({ error: "parse_error" }); expect(parseRawScan(JSON.stringify({ ...env, h: "ABCDEF" }))).toEqual({ error: "parse_error", }); expect( parseRawScan(JSON.stringify({ ...env, h: `${env.h}FF` })), ).toEqual({ error: "parse_error" }); }); it("rejects oversized d before decoding", async () => { const env = await makeEnv(); expect( parseRawScan( JSON.stringify({ ...env, d: `${env.d}AAAA` }), ), ).toEqual({ error: "parse_error" }); }); it("rejects out-of-range stripe/chunk indices", async () => { const env = await makeEnv(); const cases: Array> = [ { S: 0 }, { S: MAX_STRIPE_COUNT + 1 }, { s: 1, S: 1 }, { s: -1 }, { i: -1 }, { i: env.t }, { t: 0 }, { t: MAX_SHARDS_PER_STRIPE + 1 }, ]; for (const patch of cases) { expect( parseRawScan(JSON.stringify({ ...env, ...patch })), JSON.stringify(patch), ).toEqual({ error: "parse_error" }); } }); it("accepts boundary indices: S=64, s=S-1, i=t-1", async () => { const env = await makeEnv({ index: 4, total: 5, stripe: 63, stripeCount: 64 }); expect(parseRawScan(JSON.stringify(env))).toMatchObject({ v: 3, S: 64, s: 63, i: 4, t: 5, }); }); }); describe("PB-26-207 ChunkBuffer.add validates before decode/hash", () => { it("rejects oversized d with an honest hash as parse_error, not bad_integrity", async () => { // 400-byte chunk: d is ~534 chars and the hash genuinely matches, so a // post-hash check would accept it. parse_error proves the length gate ran // before atob/alloc/hash. const bytes = new Uint8Array(400).fill(9); const env = await buildChunkEnvelope(bytes, 0, 5, false, VERIFY_CODE); expect(env.d.length).toBeGreaterThan(MAX_CHUNK_B64_CHARS); const buf = new ChunkBuffer(); expect(await buf.add(env)).toEqual({ accepted: false, reason: "parse_error", }); expect(buf.size()).toBe(0); expect(buf.bufferedBytes()).toBe(0); }); it("d boundary: 468 chars accepted, 469 rejected", async () => { // 351 bytes encode to exactly 468 unpadded chars and stay within the // decoded headroom; a padded 469-char twin of a 350-byte chunk is not. const bytes351 = new Uint8Array(351).fill(3); const atLimit = await buildChunkEnvelope(bytes351, 0, 5, false, VERIFY_CODE); expect(atLimit.d.length).toBe(MAX_CHUNK_B64_CHARS); expect((await new ChunkBuffer().add(atLimit)).accepted).toBe(true); const base = await makeEnv(); const over = { ...base, d: `${base.d}==` }; expect(over.d.length).toBe(MAX_CHUNK_B64_CHARS + 1); const buf = new ChunkBuffer(); expect(await buf.add(over)).toEqual({ accepted: false, reason: "parse_error", }); expect(buf.size()).toBe(0); }); it("rejects wrong-length c/h and non-boolean p without hashing", async () => { const env = await makeEnv(); for (const patch of [ { c: "PX-SHORT" }, { c: `${VERIFY_CODE}XX` }, { h: "00" }, { h: `${env.h}FF` }, { p: "yes" }, ]) { const buf = new ChunkBuffer(); expect(await buf.add({ ...env, ...patch } as ChunkEnvelope)).toEqual({ accepted: false, reason: "parse_error", }); expect(buf.getContext()).toBeNull(); } }); it("rejects an oversized stripe count before decoding", async () => { const env = await buildChunkEnvelope( new Uint8Array([1, 2, 3]), 0, 5, false, VERIFY_CODE, 0, MAX_STRIPE_COUNT + 1, ); const buf = new ChunkBuffer(); expect(await buf.add(env)).toEqual({ accepted: false, reason: "parse_error", }); expect(buf.getContext()).toBeNull(); }); it("tracks decoded bytes on accept only", async () => { const buf = new ChunkBuffer(); expect(buf.bufferedBytes()).toBe(0); const first = await makeEnv({ index: 0, bytes: new Uint8Array(100).fill(1) }); expect((await buf.add(first)).accepted).toBe(true); expect(buf.bufferedBytes()).toBe(100); // Duplicate, foreign, corrupt, and malformed inputs leave the counter flat. expect(await buf.add(first)).toEqual({ accepted: false, reason: "duplicate", }); const foreign = await makeEnv({ index: 1, verifyCode: "PX-OTHR-XXXX" }); expect(await buf.add(foreign)).toEqual({ accepted: false, reason: "foreign_codex", }); const badHash = await makeEnv({ index: 1 }); expect(await buf.add({ ...badHash, h: `FF${badHash.h.slice(2)}` })).toEqual( { accepted: false, reason: "bad_integrity", }, ); const malformed = await makeEnv({ index: 1 }); expect(await buf.add({ ...malformed, d: "x".repeat(9999) })).toEqual({ accepted: false, reason: "parse_error", }); expect(buf.bufferedBytes()).toBe(100); expect(buf.size()).toBe(1); }); it("enforces the aggregate byte/chunk ceiling: exact fits, +1 fails", async () => { // A 1-chunk, 350-byte budget keeps the enforcement path fast: the // production ceiling uses the same comparison against larger constants. const buf = new ChunkBuffer({ maxBytes: CHUNK_SIZE, maxChunks: 1 }); const first = await makeEnv({ index: 0 }); expect((await buf.add(first)).accepted).toBe(true); expect(buf.bufferedBytes()).toBe(CHUNK_SIZE); const second = await makeEnv({ index: 1 }); expect(await buf.add(second)).toEqual({ accepted: false, reason: "parse_error", }); expect(buf.size()).toBe(1); expect(buf.bufferedBytes()).toBe(CHUNK_SIZE); }); it("honest 350-byte shards count toward the real aggregate ceiling", async () => { const buf = new ChunkBuffer(); const env = await makeEnv({ index: 0 }); expect((await buf.add(env)).accepted).toBe(true); expect(buf.bufferedBytes()).toBe(CHUNK_SIZE); expect(buf.bufferedBytes()).toBeLessThanOrEqual(MAX_BUFFER_BYTES); expect(buf.size()).toBeLessThanOrEqual(MAX_BUFFER_CHUNKS); }); }); describe("PB-26-207 camera scan gate", () => { it("runs one scan at a time and drops repeats while busy", async () => { let release!: () => void; const gate = new Promise((resolve) => { release = resolve; }); let calls = 0; const scan = createScanGate(async () => { calls++; await gate; }); scan.onScan("first"); scan.onScan("second"); scan.onScan("third"); await Promise.resolve(); await Promise.resolve(); expect(calls).toBe(1); release(); await gate; await Promise.resolve(); await Promise.resolve(); // Dropped callbacks are never replayed; the next frame starts new work. scan.onScan("fourth"); await Promise.resolve(); await Promise.resolve(); expect(calls).toBe(2); }); it("a throwing scan resets the gate without an unhandled rejection", async () => { const errors: unknown[] = []; const scan = createScanGate(async () => { throw new Error("boom"); }); scan.onScan("bad"); await new Promise((resolve) => setTimeout(resolve, 10)); // Gate is usable again: a second failure would hang or throw here. let second = 0; const ok = createScanGate(async () => { second++; }); ok.onScan("good"); await new Promise((resolve) => setTimeout(resolve, 10)); expect(second).toBe(1); expect(errors).toEqual([]); }); }); describe("PB-26-207 existing fixtures remain valid", () => { it("a generator-built envelope passes every new gate", async () => { const bytes = new Uint8Array(CHUNK_SIZE).fill(11); const env = await buildChunkEnvelope(bytes, 2, 5, false, VERIFY_CODE); expect(env.d.length).toBeLessThanOrEqual(MAX_CHUNK_B64_CHARS); const parsed = parseRawScan(JSON.stringify(env)); expect("error" in parsed).toBe(false); const buf = new ChunkBuffer(); expect((await buf.add(parsed as ChunkEnvelope)).accepted).toBe(true); expect(buf.bufferedBytes()).toBe(CHUNK_SIZE); }); it("base64url helpers still round-trip the shard size", () => { const bytes = new Uint8Array(CHUNK_SIZE).fill(5); const encoded = bytesToBase64Url(bytes); expect(encoded.length).toBeLessThanOrEqual(MAX_CHUNK_B64_CHARS); }); it("chunk hash helper still emits the exact documented length", async () => { const hash = ( await sha256Hex(new Uint8Array(CHUNK_SIZE).fill(5)) ).slice(0, 32); expect(hash).toHaveLength(CHUNK_HASH_LENGTH); }); });