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 decodeMayan,12 isReserved,13} from "./mayan";1415161718function matrixToImageData(19 matrix: boolean[][],20 size: number,21 modulePx: number,22): ImageData {23 const W = size * modulePx;24 const H = size * modulePx;25 const data = new Uint8ClampedArray(W * H * 4);26 for (let r = 0; r < size; r++) {27 for (let c = 0; c < size; c++) {28 const dark = matrix[r][c];29 const v = dark ? 0 : 255;30 for (let dy = 0; dy < modulePx; dy++) {31 for (let dx = 0; dx < modulePx; dx++) {32 const px = c * modulePx + dx;33 const py = r * modulePx + dy;34 const i = (py * W + px) * 4;35 data[i] = v;36 data[i + 1] = v;37 data[i + 2] = v;38 data[i + 3] = 255;39 }40 }41 }42 }43 return { data, width: W, height: H, colorSpace: "srgb" } as ImageData;44}454647function mulberry32(seed: number) {48 let state = seed >>> 0;49 return () => {50 state = (state + 0x6d2b79f5) >>> 0;51 let t = state;52 t = Math.imul(t ^ (t >>> 15), t | 1);53 t ^= t + Math.imul(t ^ (t >>> 7), t | 61);54 return ((t ^ (t >>> 14)) >>> 0) / 0x100000000;55 };56}5758function randomPayload(n: number, seed = 1): Uint8Array {59 const rng = mulberry32(seed);60 const out = new Uint8Array(n);61 for (let i = 0; i < n; i++) out[i] = (rng() * 256) | 0;62 return out;63}6465function flipRandomDataBits(66 matrix: boolean[][],67 size: number,68 fraction: number,69 seed = 1,70): boolean[][] {71 const center = Math.floor(size / 2);72 const dataPositions: { r: number; c: number }[] = [];73 for (let r = QUIET_ZONE; r < size - QUIET_ZONE; r++) {74 for (let c = QUIET_ZONE; c < size - QUIET_ZONE; c++) {75 if (!isReserved(r, c, center, size)) dataPositions.push({ r, c });76 }77 }78 const rng = mulberry32(seed);79 const out = matrix.map((row) => row.slice());80 const toFlip = Math.floor(dataPositions.length * fraction);81 for (let i = 0; i < toFlip; i++) {82 const j = i + Math.floor(rng() * (dataPositions.length - i));83 [dataPositions[i], dataPositions[j]] = [dataPositions[j], dataPositions[i]];84 const { r, c } = dataPositions[i];85 out[r][c] = !out[r][c];86 }87 return out;88}8990describe("Mayan format", () => {91 it("constants are coherent", () => {92 expect(MAYAN_MAGIC).toBe(0x4d);93 expect(MAYAN_VERSION).toBe(0x01);94 expect(RUNE_ZONE).toBe(27);95 expect(QUIET_ZONE).toBe(2);96 });9798 it("calculateSymbolSize returns >=64 for tiny payloads", () => {99 expect(calculateSymbolSize(0)).toBe(64);100 expect(calculateSymbolSize(50)).toBeGreaterThanOrEqual(64);101 });102103 it("calculateSymbolSize grows with payload", () => {104 const small = calculateSymbolSize(100);105 const big = calculateSymbolSize(5000);106 expect(big).toBeGreaterThan(small);107 });108109 it("rejects payloads larger than MAX_PAYLOAD_BYTES", () => {110 const oversize = new Uint8Array(MAX_PAYLOAD_BYTES + 1);111 expect(() => encodeMayanMatrix(oversize)).toThrow(/payload too large/);112 });113});114115describe("Mayan round-trip", () => {116 it("round-trips a 5-byte payload", () => {117 const payload = new Uint8Array([1, 2, 3, 4, 5]);118 const { matrix, size } = encodeMayanMatrix(payload);119 const decoded = decodeMatrix(matrix, size);120 expect(decoded).not.toBeNull();121 expect(Array.from(decoded!)).toEqual(Array.from(payload));122 });123124 it("round-trips a 100-byte random payload", () => {125 const payload = randomPayload(100);126 const { matrix, size } = encodeMayanMatrix(payload);127 const decoded = decodeMatrix(matrix, size);128 expect(decoded).not.toBeNull();129 expect(Array.from(decoded!)).toEqual(Array.from(payload));130 });131132 it("round-trips a 500-byte payload (multi-block RS)", () => {133 const payload = randomPayload(500);134 const { matrix, size } = encodeMayanMatrix(payload);135 const decoded = decodeMatrix(matrix, size);136 expect(decoded).not.toBeNull();137 expect(Array.from(decoded!)).toEqual(Array.from(payload));138 });139140 it("round-trips a 2000-byte payload", () => {141 const payload = randomPayload(2000);142 const { matrix, size } = encodeMayanMatrix(payload);143 const decoded = decodeMatrix(matrix, size);144 expect(decoded).not.toBeNull();145 expect(Array.from(decoded!)).toEqual(Array.from(payload));146 });147148 it("rejects a matrix with no Mayan magic byte", () => {149 const fake: boolean[][] = Array.from({ length: 64 }, () =>150 new Array<boolean>(64).fill(false),151 );152 const decoded = decodeMatrix(fake, 64);153 expect(decoded).toBeNull();154 });155});156157describe("Mayan image pipeline", () => {158 it("decodes a 5-byte payload via the full image pipeline at modulePx=3", () => {159 const payload = new Uint8Array([1, 2, 3, 4, 5]);160 const { matrix, size } = encodeMayanMatrix(payload);161 const imgData = matrixToImageData(matrix, size, 3);162 const decoded = decodeMayan(imgData);163 expect(decoded).not.toBeNull();164 expect(Array.from(decoded!)).toEqual(Array.from(payload));165 });166167 it("decodes a 100-byte payload via the full image pipeline at modulePx=3", () => {168 const payload = randomPayload(100);169 const { matrix, size } = encodeMayanMatrix(payload);170 const imgData = matrixToImageData(matrix, size, 3);171 const decoded = decodeMayan(imgData);172 expect(decoded).not.toBeNull();173 expect(Array.from(decoded!)).toEqual(Array.from(payload));174 });175176 it("decodes a 100-byte payload via the full image pipeline at modulePx=4", () => {177 const payload = randomPayload(100);178 const { matrix, size } = encodeMayanMatrix(payload);179 const imgData = matrixToImageData(matrix, size, 4);180 const decoded = decodeMayan(imgData);181 expect(decoded).not.toBeNull();182 expect(Array.from(decoded!)).toEqual(Array.from(payload));183 });184185 it("decodes a 500-byte payload via the full image pipeline at modulePx=3", () => {186 const payload = randomPayload(500);187 const { matrix, size } = encodeMayanMatrix(payload);188 const imgData = matrixToImageData(matrix, size, 3);189 const decoded = decodeMayan(imgData);190 expect(decoded).not.toBeNull();191 expect(Array.from(decoded!)).toEqual(Array.from(payload));192 });193});194195describe("Mayan RS recovery", () => {196 197 198 199 200 it("recovers from 0.5% random bit flips in data area", () => {201 const payload = randomPayload(50);202 const { matrix, size } = encodeMayanMatrix(payload);203 const corrupted = flipRandomDataBits(matrix, size, 0.005, 42);204 const decoded = decodeMatrix(corrupted, size);205 expect(decoded).not.toBeNull();206 expect(Array.from(decoded!)).toEqual(Array.from(payload));207 });208});