import { describe, it, expect } from "vitest"; import { MAYAN_MAGIC, MAYAN_V2, MAX_PAYLOAD_BYTES, RUNE_ZONE, QUIET_ZONE, ANCHOR_FOOTPRINT, RS_DATA, RS_BLOCK, HEADER_BYTES, calculateSymbolSize, encodeMayanMatrix, encodeMayan, decodeMatrix, decodeMayan, isReserved, } from "./mayan"; // Synthesize an ImageData from a boolean module matrix at a given module pixel // size. Pure black/white, no border padding. function matrixToImageData( matrix: boolean[][], size: number, modulePx: number, ): ImageData { const W = size * modulePx; const H = size * modulePx; const data = new Uint8ClampedArray(W * H * 4); for (let r = 0; r < size; r++) { for (let c = 0; c < size; c++) { const dark = matrix[r][c]; const v = dark ? 0 : 255; for (let dy = 0; dy < modulePx; dy++) { for (let dx = 0; dx < modulePx; dx++) { const px = c * modulePx + dx; const py = r * modulePx + dy; const i = (py * W + px) * 4; data[i] = v; data[i + 1] = v; data[i + 2] = v; data[i + 3] = 255; } } } } return { data, width: W, height: H, colorSpace: "srgb" } as ImageData; } // Deterministic PRNG — Mulberry32 — so tests are reproducible. function mulberry32(seed: number) { let state = seed >>> 0; return () => { state = (state + 0x6d2b79f5) >>> 0; let t = state; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 0x100000000; }; } function randomPayload(n: number, seed = 1): Uint8Array { const rng = mulberry32(seed); const out = new Uint8Array(n); for (let i = 0; i < n; i++) out[i] = (rng() * 256) | 0; return out; } function flipRandomDataBits( matrix: boolean[][], size: number, fraction: number, seed = 1, ): boolean[][] { const dataPositions: { r: number; c: number }[] = []; for (let r = QUIET_ZONE; r < size - QUIET_ZONE; r++) { for (let c = QUIET_ZONE; c < size - QUIET_ZONE; c++) { if (!isReserved(r, c, size)) dataPositions.push({ r, c }); } } const rng = mulberry32(seed); const out = matrix.map((row) => row.slice()); const toFlip = Math.floor(dataPositions.length * fraction); for (let i = 0; i < toFlip; i++) { const j = i + Math.floor(rng() * (dataPositions.length - i)); [dataPositions[i], dataPositions[j]] = [dataPositions[j], dataPositions[i]]; const { r, c } = dataPositions[i]; out[r][c] = !out[r][c]; } return out; } describe("Mayan v2 format", () => { it("constants are coherent", () => { expect(MAYAN_MAGIC).toBe(0x4d); expect(MAYAN_V2).toBe(0x02); expect(RUNE_ZONE).toBe(27); expect(QUIET_ZONE).toBe(2); expect(ANCHOR_FOOTPRINT).toBe(9); expect(RS_DATA).toBe(204); expect(RS_BLOCK).toBe(255); expect(HEADER_BYTES).toBe(6); }); it("calculateSymbolSize returns >=64 for tiny payloads", () => { expect(calculateSymbolSize(0)).toBeGreaterThanOrEqual(64); expect(calculateSymbolSize(50)).toBeGreaterThanOrEqual(64); }); it("calculateSymbolSize grows with payload", () => { const small = calculateSymbolSize(100); const big = calculateSymbolSize(50_000); expect(big).toBeGreaterThan(small); }); it("rejects payloads larger than MAX_PAYLOAD_BYTES", () => { const oversize = new Uint8Array(MAX_PAYLOAD_BYTES + 1); expect(() => encodeMayanMatrix(oversize)).toThrow(/payload too large/); }); it("isReserved marks anchor footprints as reserved", () => { const size = 128; // Top-left anchor footprint: rows/cols [QUIET_ZONE..QUIET_ZONE+8] expect(isReserved(QUIET_ZONE, QUIET_ZONE, size)).toBe(true); expect(isReserved(QUIET_ZONE + 5, QUIET_ZONE + 5, size)).toBe(true); expect(isReserved(QUIET_ZONE + 8, QUIET_ZONE + 8, size)).toBe(true); // Just outside the anchor footprint should be data area (assuming the // timing strip and rune zone don't claim it) const safeR = QUIET_ZONE + ANCHOR_FOOTPRINT + 1; const safeC = QUIET_ZONE + ANCHOR_FOOTPRINT + 1; // The corner (anchorEnd, anchorEnd) lies at the timing-strip intersection, // so step one further in expect(isReserved(safeR, safeC, size)).toBe(false); }); it("isReserved marks size-bit row positions as reserved", () => { const size = 128; const row = QUIET_ZONE; const colStart = QUIET_ZONE + ANCHOR_FOOTPRINT + 1; for (let i = 0; i < 16; i++) { expect(isReserved(row, colStart + i, size)).toBe(true); } }); it("encodes the symbol size bits round-trippably", () => { const payload = randomPayload(50); const { matrix, size } = encodeMayanMatrix(payload); const row = QUIET_ZONE; const colStart = QUIET_ZONE + ANCHOR_FOOTPRINT + 1; let v = 0; for (let i = 0; i < 16; i++) { if (matrix[row][colStart + i]) v |= 1 << (15 - i); } expect(v).toBe(size); }); // encodeMayan() requires DOM canvas (only available in browser/jsdom). // Paper-size tagging is exercised end-to-end via the Export view. }); describe("Mayan v2 round-trip (matrix)", () => { it("round-trips a 5-byte payload", () => { const payload = new Uint8Array([1, 2, 3, 4, 5]); const { matrix, size } = encodeMayanMatrix(payload); const decoded = decodeMatrix(matrix, size); expect(decoded).not.toBeNull(); expect(Array.from(decoded!)).toEqual(Array.from(payload)); }); it("round-trips a 100-byte random payload", () => { const payload = randomPayload(100); const { matrix, size } = encodeMayanMatrix(payload); const decoded = decodeMatrix(matrix, size); expect(decoded).not.toBeNull(); expect(Array.from(decoded!)).toEqual(Array.from(payload)); }); it("round-trips a 500-byte payload (multi-block RS)", () => { const payload = randomPayload(500); const { matrix, size } = encodeMayanMatrix(payload); const decoded = decodeMatrix(matrix, size); expect(decoded).not.toBeNull(); expect(Array.from(decoded!)).toEqual(Array.from(payload)); }); it("round-trips a 2000-byte payload", () => { const payload = randomPayload(2000); const { matrix, size } = encodeMayanMatrix(payload); const decoded = decodeMatrix(matrix, size); expect(decoded).not.toBeNull(); expect(Array.from(decoded!)).toEqual(Array.from(payload)); }); it("round-trips a 20000-byte payload (realistic vault size)", () => { const payload = randomPayload(20000); const { matrix, size } = encodeMayanMatrix(payload); const decoded = decodeMatrix(matrix, size); expect(decoded).not.toBeNull(); expect(Array.from(decoded!)).toEqual(Array.from(payload)); }); it("rejects a matrix with no Mayan magic byte", () => { const fake: boolean[][] = Array.from({ length: 64 }, () => new Array(64).fill(false), ); const decoded = decodeMatrix(fake, 64); expect(decoded).toBeNull(); }); }); describe("Mayan v2 image pipeline", () => { it("decodes a 5-byte payload at modulePx=3", () => { const payload = new Uint8Array([1, 2, 3, 4, 5]); const { matrix, size } = encodeMayanMatrix(payload); const imgData = matrixToImageData(matrix, size, 3); const decoded = decodeMayan(imgData); expect(decoded).not.toBeNull(); expect(Array.from(decoded!)).toEqual(Array.from(payload)); }); it("decodes a 100-byte payload at modulePx=3", () => { const payload = randomPayload(100); const { matrix, size } = encodeMayanMatrix(payload); const imgData = matrixToImageData(matrix, size, 3); const decoded = decodeMayan(imgData); expect(decoded).not.toBeNull(); expect(Array.from(decoded!)).toEqual(Array.from(payload)); }); it("decodes a 100-byte payload at modulePx=4", () => { const payload = randomPayload(100); const { matrix, size } = encodeMayanMatrix(payload); const imgData = matrixToImageData(matrix, size, 4); const decoded = decodeMayan(imgData); expect(decoded).not.toBeNull(); expect(Array.from(decoded!)).toEqual(Array.from(payload)); }); it("decodes a 500-byte payload at modulePx=3", () => { const payload = randomPayload(500); const { matrix, size } = encodeMayanMatrix(payload); const imgData = matrixToImageData(matrix, size, 3); const decoded = decodeMayan(imgData); expect(decoded).not.toBeNull(); expect(Array.from(decoded!)).toEqual(Array.from(payload)); }); it("decodes a 5000-byte payload at modulePx=4", () => { const payload = randomPayload(5000); const { matrix, size } = encodeMayanMatrix(payload); const imgData = matrixToImageData(matrix, size, 4); const decoded = decodeMayan(imgData); expect(decoded).not.toBeNull(); expect(Array.from(decoded!)).toEqual(Array.from(payload)); }); }); describe("Mayan v2 RS recovery", () => { it("recovers from 0.5% random bit flips in the data area", () => { const payload = randomPayload(50); const { matrix, size } = encodeMayanMatrix(payload); const corrupted = flipRandomDataBits(matrix, size, 0.005, 42); const decoded = decodeMatrix(corrupted, size); expect(decoded).not.toBeNull(); expect(Array.from(decoded!)).toEqual(Array.from(payload)); }); it("recovers from 1% random bit flips on a multi-block payload", () => { // RS(255,204) corrects up to 25 byte errors per block. With interleaving // a 1% bit flip rate translates to ~7 byte errors per block — comfortably // within capacity. Higher rates approach the per-block limit and are // exercised in stress benchmarks separately. const payload = randomPayload(500); const { matrix, size } = encodeMayanMatrix(payload); const corrupted = flipRandomDataBits(matrix, size, 0.01, 7); const decoded = decodeMatrix(corrupted, size); expect(decoded).not.toBeNull(); expect(Array.from(decoded!)).toEqual(Array.from(payload)); }); });