1import { describe, expect, test } from "vitest";2import {3 decodePeachScript,4 encodePeachScript,5 isPeachScript,6} from "./peachScript";7import { IMPORT_LIMITS } from "../shared/import-limits";89describe("peachScript decode size bound (PB-26-010)", () => {10 test("decode rejects input above MAX_JSON_BYTES before allocating", () => {11 12 13 const chunk = String.fromCodePoint(0xe000).repeat(1024);14 const tooLarge = chunk.repeat(15 Math.floor(IMPORT_LIMITS.MAX_JSON_BYTES / 1024) + 1,16 );17 expect(tooLarge.length).toBeGreaterThan(IMPORT_LIMITS.MAX_JSON_BYTES);18 expect(() => decodePeachScript(tooLarge)).toThrow(19 "PEACHSCRIPT_TOO_LARGE",20 );21 });2223 test("decode accepts input at the bound", () => {24 const atLimit = String.fromCodePoint(25 ...Array.from({ length: 64 }, () => 0xe041),26 );27 expect(decodePeachScript(atLimit).length).toBe(64);28 });29});3031describe("peachScript encode size bound (PB-26-113)", () => {32 33 34 35 const encodedFileBytes = (bytes: Uint8Array) =>36 new TextEncoder().encode(encodePeachScript(bytes)).length;3738 test("an envelope just above the import ceiling fails at export", () => {39 const overCeiling = Math.floor(IMPORT_LIMITS.MAX_JSON_BYTES / 3) + 1;40 const bytes = new Uint8Array(overCeiling);4142 43 expect(bytes.length).toBeLessThan(IMPORT_LIMITS.MAX_JSON_BYTES);44 expect(() => encodePeachScript(bytes)).toThrow("PEACHSCRIPT_TOO_LARGE");45 });4647 test("the encoded file costs three bytes per envelope byte", () => {48 49 50 const probe = new Uint8Array(1024);51 for (let i = 0; i < probe.length; i++) probe[i] = (i * 37 + 13) % 256;52 expect(encodedFileBytes(probe)).toBe(probe.length * 3);53 expect(decodePeachScript(encodePeachScript(probe))).toEqual(probe);54 });5556 test("an envelope at the ceiling exports to a file the importer accepts", () => {57 const atCeiling = Math.floor(IMPORT_LIMITS.MAX_JSON_BYTES / 3);58 const bytes = new Uint8Array(atCeiling);59 for (let i = 0; i < atCeiling; i++) bytes[i] = (i * 37 + 13) % 256;6061 const encoded = encodePeachScript(bytes);62 63 expect(encodedFileBytes(bytes)).toBeLessThanOrEqual(IMPORT_LIMITS.MAX_JSON_BYTES);64 65 66 expect(encoded.length).toBe(atCeiling);67 expect(decodePeachScript(encoded)[atCeiling - 1]).toBe(bytes[atCeiling - 1]);68 });6970 test("a 4 MiB envelope is refused — it would download as ~12 MiB", () => {71 const fourMiB = 4 * 1024 * 1024;72 expect(() => encodePeachScript(new Uint8Array(fourMiB))).toThrow(73 "PEACHSCRIPT_TOO_LARGE",74 );75 });76});7778describe("peachScript", () => {79 test("byte 0 maps to U+E000", () => {80 const out = encodePeachScript(new Uint8Array([0]));81 expect(out.codePointAt(0)).toBe(0xe000);82 });8384 test("byte 255 maps to U+E0FF", () => {85 const out = encodePeachScript(new Uint8Array([255]));86 expect(out.codePointAt(0)).toBe(0xe0ff);87 });8889 test("round-trip identity for all 256 byte values", () => {90 const bytes = new Uint8Array(256);91 for (let i = 0; i < 256; i++) bytes[i] = i;92 const encoded = encodePeachScript(bytes);93 const decoded = decodePeachScript(encoded);94 expect(decoded).toEqual(bytes);95 });9697 test("empty bytes round-trip to empty string and back", () => {98 expect(encodePeachScript(new Uint8Array())).toBe("");99 expect(decodePeachScript("")).toEqual(new Uint8Array());100 });101102 test("decode throws on character outside PUA range", () => {103 expect(() => decodePeachScript("hello")).toThrow("INVALID_PEACHSCRIPT_CHARACTER");104 expect(() => decodePeachScript(String.fromCodePoint(0xe100))).toThrow(105 "INVALID_PEACHSCRIPT_CHARACTER"106 );107 expect(() => decodePeachScript(String.fromCodePoint(0xdfff))).toThrow(108 "INVALID_PEACHSCRIPT_CHARACTER"109 );110 });111112 test("random-payload round-trip preserves bytes", () => {113 const payload = crypto.getRandomValues(new Uint8Array(2048));114 const decoded = decodePeachScript(encodePeachScript(payload));115 expect(decoded).toEqual(payload);116 });117118 test("isPeachScript accepts only PUA strings", () => {119 expect(isPeachScript(encodePeachScript(new Uint8Array([1, 2, 3])))).toBe(true);120 expect(isPeachScript("")).toBe(false);121 expect(isPeachScript("hello")).toBe(false);122 const mixed = encodePeachScript(new Uint8Array([1])) + "x";123 expect(isPeachScript(mixed)).toBe(false);124 });125});126127describe("peachScript fuzz corpus", () => {128 129 test("rejects U+E100 (off-by-one out of range)", () => {130 expect(() => decodePeachScript(String.fromCodePoint(0xe100))).toThrow(131 "INVALID_PEACHSCRIPT_CHARACTER"132 );133 });134135 test("rejects U+E200 (far out of range)", () => {136 expect(() => decodePeachScript(String.fromCodePoint(0xe200))).toThrow(137 "INVALID_PEACHSCRIPT_CHARACTER"138 );139 });140141 142 test("rejects lone low surrogate U+DFFF", () => {143 expect(() => decodePeachScript(String.fromCodePoint(0xdfff))).toThrow(144 "INVALID_PEACHSCRIPT_CHARACTER"145 );146 });147148 test("rejects lone high surrogate U+D800", () => {149 expect(() => decodePeachScript(String.fromCodePoint(0xd800))).toThrow(150 "INVALID_PEACHSCRIPT_CHARACTER"151 );152 });153154 155 test("rejects U+1D2E0 (real Mayan numeral — outside codec range)", () => {156 expect(() => decodePeachScript("\u{1D2E0}")).toThrow(157 "INVALID_PEACHSCRIPT_CHARACTER"158 );159 });160161 162 test("rejects BOM (U+FEFF) mid-stream", () => {163 const encoded = encodePeachScript(new Uint8Array([0x41, 0x42]));164 165 const withBOM = encoded[0] + String.fromCodePoint(0xfeff) + encoded[1];166 expect(() => decodePeachScript(withBOM)).toThrow("INVALID_PEACHSCRIPT_CHARACTER");167 });168169 test("rejects RTL mark (U+200F) mid-stream", () => {170 const encoded = encodePeachScript(new Uint8Array([0x41]));171 172 expect(() => decodePeachScript(encoded + String.fromCodePoint(0x200f))).toThrow(173 "INVALID_PEACHSCRIPT_CHARACTER"174 );175 });176177 test("rejects zero-width non-joiner (U+200C)", () => {178 const encoded = encodePeachScript(new Uint8Array([0x41]));179 180 expect(() => decodePeachScript(encoded + String.fromCodePoint(0x200c))).toThrow(181 "INVALID_PEACHSCRIPT_CHARACTER"182 );183 });184185 186 187 188 189 test("skips space between valid rune characters", () => {190 const encoded = encodePeachScript(new Uint8Array([0x41, 0x42]));191 const withSpace = encoded[0] + " " + encoded[1];192 expect(decodePeachScript(withSpace)).toEqual(new Uint8Array([0x41, 0x42]));193 });194195 test("skips newline between valid rune characters", () => {196 const encoded = encodePeachScript(new Uint8Array([0x41, 0x42]));197 const withNewline = encoded[0] + "\n" + encoded[1];198 expect(decodePeachScript(withNewline)).toEqual(new Uint8Array([0x41, 0x42]));199 });200201 202 test("byte 0x00 round-trips correctly", () => {203 const data = new Uint8Array([0x00]);204 expect(decodePeachScript(encodePeachScript(data))).toEqual(data);205 });206207 test("byte 0xFE round-trips correctly", () => {208 const data = new Uint8Array([0xfe]);209 expect(decodePeachScript(encodePeachScript(data))).toEqual(data);210 });211212 test("byte 0xFF round-trips correctly", () => {213 const data = new Uint8Array([0xff]);214 expect(decodePeachScript(encodePeachScript(data))).toEqual(data);215 });216217 218 test("1 KiB deterministic payload round-trips", () => {219 const data = new Uint8Array(1024);220 for (let i = 0; i < 1024; i++) data[i] = (i * 37 + 13) % 256;221 expect(decodePeachScript(encodePeachScript(data))).toEqual(data);222 });223224 225 226 test("rejects large all-invalid input in well under 50 ms", () => {227 const invalid = String.fromCodePoint(0xe100).repeat(10_000);228 const start = Date.now();229 expect(() => decodePeachScript(invalid)).toThrow("INVALID_PEACHSCRIPT_CHARACTER");230 expect(Date.now() - start).toBeLessThan(50);231 });232233 234 test("rejects left double quotation mark (U+201C) prefix", () => {235 const encoded = encodePeachScript(new Uint8Array([0x41]));236 expect(() => decodePeachScript(String.fromCodePoint(0x201c) + encoded)).toThrow(237 "INVALID_PEACHSCRIPT_CHARACTER"238 );239 });240});