mayan.test.ts

4.6 KB · raw

1import { describe, it, expect } from "vitest";2import {3  MAYAN_MAGIC,4  MAYAN_VERSION,5  MAX_PAYLOAD_BYTES,6  RUNE_ZONE,7  QUIET_ZONE,8  calculateSymbolSize,9  encodeMayanMatrix,10  decodeMatrix,11  isReserved,12} from "./mayan";1314// Deterministic PRNG — Mulberry32 — so the recovery test is reproducible.15function mulberry32(seed: number) {16  let state = seed >>> 0;17  return () => {18    state = (state + 0x6d2b79f5) >>> 0;19    let t = state;20    t = Math.imul(t ^ (t >>> 15), t | 1);21    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);22    return ((t ^ (t >>> 14)) >>> 0) / 0x100000000;23  };24}2526function randomPayload(n: number, seed = 1): Uint8Array {27  const rng = mulberry32(seed);28  const out = new Uint8Array(n);29  for (let i = 0; i < n; i++) out[i] = (rng() * 256) | 0;30  return out;31}3233function flipRandomDataBits(34  matrix: boolean[][],35  size: number,36  fraction: number,37  seed = 1,38): boolean[][] {39  const center = Math.floor(size / 2);40  const dataPositions: { r: number; c: number }[] = [];41  for (let r = QUIET_ZONE; r < size - QUIET_ZONE; r++) {42    for (let c = QUIET_ZONE; c < size - QUIET_ZONE; c++) {43      if (!isReserved(r, c, center, size)) dataPositions.push({ r, c });44    }45  }46  const rng = mulberry32(seed);47  const out = matrix.map((row) => row.slice());48  const toFlip = Math.floor(dataPositions.length * fraction);49  for (let i = 0; i < toFlip; i++) {50    const j = i + Math.floor(rng() * (dataPositions.length - i));51    [dataPositions[i], dataPositions[j]] = [dataPositions[j], dataPositions[i]];52    const { r, c } = dataPositions[i];53    out[r][c] = !out[r][c];54  }55  return out;56}5758describe("Mayan format", () => {59  it("constants are coherent", () => {60    expect(MAYAN_MAGIC).toBe(0x4d);61    expect(MAYAN_VERSION).toBe(0x01);62    expect(RUNE_ZONE).toBe(27);63    expect(QUIET_ZONE).toBe(2);64  });6566  it("calculateSymbolSize returns >=64 for tiny payloads", () => {67    expect(calculateSymbolSize(0)).toBe(64);68    expect(calculateSymbolSize(50)).toBeGreaterThanOrEqual(64);69  });7071  it("calculateSymbolSize grows with payload", () => {72    const small = calculateSymbolSize(100);73    const big = calculateSymbolSize(5000);74    expect(big).toBeGreaterThan(small);75  });7677  it("rejects payloads larger than MAX_PAYLOAD_BYTES", () => {78    const oversize = new Uint8Array(MAX_PAYLOAD_BYTES + 1);79    expect(() => encodeMayanMatrix(oversize)).toThrow(/payload too large/);80  });81});8283describe("Mayan round-trip", () => {84  it("round-trips a 5-byte payload", () => {85    const payload = new Uint8Array([1, 2, 3, 4, 5]);86    const { matrix, size } = encodeMayanMatrix(payload);87    const decoded = decodeMatrix(matrix, size);88    expect(decoded).not.toBeNull();89    expect(Array.from(decoded!)).toEqual(Array.from(payload));90  });9192  it("round-trips a 100-byte random payload", () => {93    const payload = randomPayload(100);94    const { matrix, size } = encodeMayanMatrix(payload);95    const decoded = decodeMatrix(matrix, size);96    expect(decoded).not.toBeNull();97    expect(Array.from(decoded!)).toEqual(Array.from(payload));98  });99100  it("round-trips a 500-byte payload (multi-block RS)", () => {101    const payload = randomPayload(500);102    const { matrix, size } = encodeMayanMatrix(payload);103    const decoded = decodeMatrix(matrix, size);104    expect(decoded).not.toBeNull();105    expect(Array.from(decoded!)).toEqual(Array.from(payload));106  });107108  it("round-trips a 2000-byte payload", () => {109    const payload = randomPayload(2000);110    const { matrix, size } = encodeMayanMatrix(payload);111    const decoded = decodeMatrix(matrix, size);112    expect(decoded).not.toBeNull();113    expect(Array.from(decoded!)).toEqual(Array.from(payload));114  });115116  it("rejects a matrix with no Mayan magic byte", () => {117    const fake: boolean[][] = Array.from({ length: 64 }, () =>118      new Array<boolean>(64).fill(false),119    );120    const decoded = decodeMatrix(fake, 64);121    expect(decoded).toBeNull();122  });123});124125describe("Mayan RS recovery", () => {126  // Recoverable threshold for RS(255,178): up to 38 byte errors per block.127  // Each byte holds 8 bits, so the bit-flip equivalent is bounded by the128  // byte coverage P(byte has ≥1 flip) = 1 - (1-p)^8. For 30% byte errors129  // (within RS limit), p ≈ 0.043 — so 1% bit flips is comfortably inside.130  it("recovers from 0.5% random bit flips in data area", () => {131    const payload = randomPayload(50);132    const { matrix, size } = encodeMayanMatrix(payload);133    const corrupted = flipRandomDataBits(matrix, size, 0.005, 42);134    const decoded = decodeMatrix(corrupted, size);135    expect(decoded).not.toBeNull();136    expect(Array.from(decoded!)).toEqual(Array.from(payload));137  });138});