1import { describe, it, expect } from "vitest";2import {3 MAYAN_MAGIC,4 MAYAN_V2,5 MAX_PAYLOAD_BYTES,6 RUNE_ZONE,7 QUIET_ZONE,8 ANCHOR_FOOTPRINT,9 RS_DATA,10 RS_BLOCK,11 HEADER_BYTES,12 calculateSymbolSize,13 encodeMayanMatrix,14 encodeMayan,15 decodeMatrix,16 decodeMayan,17 isReserved,18} from "./mayan";19202122function matrixToImageData(23 matrix: boolean[][],24 size: number,25 modulePx: number,26): ImageData {27 const W = size * modulePx;28 const H = size * modulePx;29 const data = new Uint8ClampedArray(W * H * 4);30 for (let r = 0; r < size; r++) {31 for (let c = 0; c < size; c++) {32 const dark = matrix[r][c];33 const v = dark ? 0 : 255;34 for (let dy = 0; dy < modulePx; dy++) {35 for (let dx = 0; dx < modulePx; dx++) {36 const px = c * modulePx + dx;37 const py = r * modulePx + dy;38 const i = (py * W + px) * 4;39 data[i] = v;40 data[i + 1] = v;41 data[i + 2] = v;42 data[i + 3] = 255;43 }44 }45 }46 }47 return { data, width: W, height: H, colorSpace: "srgb" } as ImageData;48}495051function mulberry32(seed: number) {52 let state = seed >>> 0;53 return () => {54 state = (state + 0x6d2b79f5) >>> 0;55 let t = state;56 t = Math.imul(t ^ (t >>> 15), t | 1);57 t ^= t + Math.imul(t ^ (t >>> 7), t | 61);58 return ((t ^ (t >>> 14)) >>> 0) / 0x100000000;59 };60}6162function randomPayload(n: number, seed = 1): Uint8Array {63 const rng = mulberry32(seed);64 const out = new Uint8Array(n);65 for (let i = 0; i < n; i++) out[i] = (rng() * 256) | 0;66 return out;67}6869function flipRandomDataBits(70 matrix: boolean[][],71 size: number,72 fraction: number,73 seed = 1,74): boolean[][] {75 const dataPositions: { r: number; c: number }[] = [];76 for (let r = QUIET_ZONE; r < size - QUIET_ZONE; r++) {77 for (let c = QUIET_ZONE; c < size - QUIET_ZONE; c++) {78 if (!isReserved(r, c, size)) dataPositions.push({ r, c });79 }80 }81 const rng = mulberry32(seed);82 const out = matrix.map((row) => row.slice());83 const toFlip = Math.floor(dataPositions.length * fraction);84 for (let i = 0; i < toFlip; i++) {85 const j = i + Math.floor(rng() * (dataPositions.length - i));86 [dataPositions[i], dataPositions[j]] = [dataPositions[j], dataPositions[i]];87 const { r, c } = dataPositions[i];88 out[r][c] = !out[r][c];89 }90 return out;91}9293describe("Mayan v2 format", () => {94 it("constants are coherent", () => {95 expect(MAYAN_MAGIC).toBe(0x4d);96 expect(MAYAN_V2).toBe(0x02);97 expect(RUNE_ZONE).toBe(27);98 expect(QUIET_ZONE).toBe(2);99 expect(ANCHOR_FOOTPRINT).toBe(9);100 expect(RS_DATA).toBe(204);101 expect(RS_BLOCK).toBe(255);102 expect(HEADER_BYTES).toBe(6);103 });104105 it("calculateSymbolSize returns >=64 for tiny payloads", () => {106 expect(calculateSymbolSize(0)).toBeGreaterThanOrEqual(64);107 expect(calculateSymbolSize(50)).toBeGreaterThanOrEqual(64);108 });109110 it("calculateSymbolSize grows with payload", () => {111 const small = calculateSymbolSize(100);112 const big = calculateSymbolSize(50_000);113 expect(big).toBeGreaterThan(small);114 });115116 it("rejects payloads larger than MAX_PAYLOAD_BYTES", () => {117 const oversize = new Uint8Array(MAX_PAYLOAD_BYTES + 1);118 expect(() => encodeMayanMatrix(oversize)).toThrow(/payload too large/);119 });120121 it("isReserved marks anchor footprints as reserved", () => {122 const size = 128;123 124 expect(isReserved(QUIET_ZONE, QUIET_ZONE, size)).toBe(true);125 expect(isReserved(QUIET_ZONE + 5, QUIET_ZONE + 5, size)).toBe(true);126 expect(isReserved(QUIET_ZONE + 8, QUIET_ZONE + 8, size)).toBe(true);127 128 129 const safeR = QUIET_ZONE + ANCHOR_FOOTPRINT + 1;130 const safeC = QUIET_ZONE + ANCHOR_FOOTPRINT + 1;131 132 133 expect(isReserved(safeR, safeC, size)).toBe(false);134 });135136 it("isReserved marks size-bit row positions as reserved", () => {137 const size = 128;138 const row = QUIET_ZONE;139 const colStart = QUIET_ZONE + ANCHOR_FOOTPRINT + 1;140 for (let i = 0; i < 16; i++) {141 expect(isReserved(row, colStart + i, size)).toBe(true);142 }143 });144145 it("encodes the symbol size bits round-trippably", () => {146 const payload = randomPayload(50);147 const { matrix, size } = encodeMayanMatrix(payload);148 const row = QUIET_ZONE;149 const colStart = QUIET_ZONE + ANCHOR_FOOTPRINT + 1;150 let v = 0;151 for (let i = 0; i < 16; i++) {152 if (matrix[row][colStart + i]) v |= 1 << (15 - i);153 }154 expect(v).toBe(size);155 });156157 158 159});160161describe("Mayan v2 round-trip (matrix)", () => {162 it("round-trips a 5-byte payload", () => {163 const payload = new Uint8Array([1, 2, 3, 4, 5]);164 const { matrix, size } = encodeMayanMatrix(payload);165 const decoded = decodeMatrix(matrix, size);166 expect(decoded).not.toBeNull();167 expect(Array.from(decoded!)).toEqual(Array.from(payload));168 });169170 it("round-trips a 100-byte random payload", () => {171 const payload = randomPayload(100);172 const { matrix, size } = encodeMayanMatrix(payload);173 const decoded = decodeMatrix(matrix, size);174 expect(decoded).not.toBeNull();175 expect(Array.from(decoded!)).toEqual(Array.from(payload));176 });177178 it("round-trips a 500-byte payload (multi-block RS)", () => {179 const payload = randomPayload(500);180 const { matrix, size } = encodeMayanMatrix(payload);181 const decoded = decodeMatrix(matrix, size);182 expect(decoded).not.toBeNull();183 expect(Array.from(decoded!)).toEqual(Array.from(payload));184 });185186 it("round-trips a 2000-byte payload", () => {187 const payload = randomPayload(2000);188 const { matrix, size } = encodeMayanMatrix(payload);189 const decoded = decodeMatrix(matrix, size);190 expect(decoded).not.toBeNull();191 expect(Array.from(decoded!)).toEqual(Array.from(payload));192 });193194 it("round-trips a 20000-byte payload (realistic vault size)", () => {195 const payload = randomPayload(20000);196 const { matrix, size } = encodeMayanMatrix(payload);197 const decoded = decodeMatrix(matrix, size);198 expect(decoded).not.toBeNull();199 expect(Array.from(decoded!)).toEqual(Array.from(payload));200 });201202 it("rejects a matrix with no Mayan magic byte", () => {203 const fake: boolean[][] = Array.from({ length: 64 }, () =>204 new Array<boolean>(64).fill(false),205 );206 const decoded = decodeMatrix(fake, 64);207 expect(decoded).toBeNull();208 });209});210211describe("Mayan v2 image pipeline", () => {212 it("decodes a 5-byte payload at modulePx=3", () => {213 const payload = new Uint8Array([1, 2, 3, 4, 5]);214 const { matrix, size } = encodeMayanMatrix(payload);215 const imgData = matrixToImageData(matrix, size, 3);216 const decoded = decodeMayan(imgData);217 expect(decoded).not.toBeNull();218 expect(Array.from(decoded!)).toEqual(Array.from(payload));219 });220221 it("decodes a 100-byte payload at modulePx=3", () => {222 const payload = randomPayload(100);223 const { matrix, size } = encodeMayanMatrix(payload);224 const imgData = matrixToImageData(matrix, size, 3);225 const decoded = decodeMayan(imgData);226 expect(decoded).not.toBeNull();227 expect(Array.from(decoded!)).toEqual(Array.from(payload));228 });229230 it("decodes a 100-byte payload at modulePx=4", () => {231 const payload = randomPayload(100);232 const { matrix, size } = encodeMayanMatrix(payload);233 const imgData = matrixToImageData(matrix, size, 4);234 const decoded = decodeMayan(imgData);235 expect(decoded).not.toBeNull();236 expect(Array.from(decoded!)).toEqual(Array.from(payload));237 });238239 it("decodes a 500-byte payload at modulePx=3", () => {240 const payload = randomPayload(500);241 const { matrix, size } = encodeMayanMatrix(payload);242 const imgData = matrixToImageData(matrix, size, 3);243 const decoded = decodeMayan(imgData);244 expect(decoded).not.toBeNull();245 expect(Array.from(decoded!)).toEqual(Array.from(payload));246 });247248 it("decodes a 5000-byte payload at modulePx=4", () => {249 const payload = randomPayload(5000);250 const { matrix, size } = encodeMayanMatrix(payload);251 const imgData = matrixToImageData(matrix, size, 4);252 const decoded = decodeMayan(imgData);253 expect(decoded).not.toBeNull();254 expect(Array.from(decoded!)).toEqual(Array.from(payload));255 });256});257258describe("Mayan v2 RS recovery", () => {259 it("recovers from 0.5% random bit flips in the data area", () => {260 const payload = randomPayload(50);261 const { matrix, size } = encodeMayanMatrix(payload);262 const corrupted = flipRandomDataBits(matrix, size, 0.005, 42);263 const decoded = decodeMatrix(corrupted, size);264 expect(decoded).not.toBeNull();265 expect(Array.from(decoded!)).toEqual(Array.from(payload));266 });267268 it("recovers from 1% random bit flips on a multi-block payload", () => {269 270 271 272 273 const payload = randomPayload(500);274 const { matrix, size } = encodeMayanMatrix(payload);275 const corrupted = flipRandomDataBits(matrix, size, 0.01, 7);276 const decoded = decodeMatrix(corrupted, size);277 expect(decoded).not.toBeNull();278 expect(Array.from(decoded!)).toEqual(Array.from(payload));279 });280});281