import { describe, it, expect } from "vitest"; import { MAYAN_MAGIC, MAYAN_VERSION, MAX_PAYLOAD_BYTES, RUNE_ZONE, QUIET_ZONE, calculateSymbolSize, encodeMayanMatrix, decodeMatrix, isReserved, } from "./mayan"; // Deterministic PRNG — Mulberry32 — so the recovery test is 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 center = Math.floor(size / 2); 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, center, 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 format", () => { it("constants are coherent", () => { expect(MAYAN_MAGIC).toBe(0x4d); expect(MAYAN_VERSION).toBe(0x01); expect(RUNE_ZONE).toBe(27); expect(QUIET_ZONE).toBe(2); }); it("calculateSymbolSize returns >=64 for tiny payloads", () => { expect(calculateSymbolSize(0)).toBe(64); expect(calculateSymbolSize(50)).toBeGreaterThanOrEqual(64); }); it("calculateSymbolSize grows with payload", () => { const small = calculateSymbolSize(100); const big = calculateSymbolSize(5000); 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/); }); }); describe("Mayan round-trip", () => { 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("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 RS recovery", () => { // Recoverable threshold for RS(255,178): up to 38 byte errors per block. // Each byte holds 8 bits, so the bit-flip equivalent is bounded by the // byte coverage P(byte has ≥1 flip) = 1 - (1-p)^8. For 30% byte errors // (within RS limit), p ≈ 0.043 — so 1% bit flips is comfortably inside. it("recovers from 0.5% random bit flips in 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)); }); });