peachScript.test.ts

7.4 KB · raw

1import { describe, expect, test } from "vitest";2import {3  decodePeachScript,4  encodePeachScript,5  isPeachScript,6  normalisePeachScript,7} from "./peachScript";8import { IMPORT_LIMITS } from "../shared/import-limits";910describe("peachScript decode size bound (PB-26-010)", () => {11  test("decode rejects input above MAX_JSON_BYTES before allocating", () => {12    // Build an oversized valid PeachScript string without the spread-arg13    // limit: repeat a 1K-char chunk past the 10 MiB bound.14    const chunk = String.fromCodePoint(0xe000).repeat(1024);15    const tooLarge = chunk.repeat(16      Math.floor(IMPORT_LIMITS.MAX_JSON_BYTES / 1024) + 1,17    );18    expect(tooLarge.length).toBeGreaterThan(IMPORT_LIMITS.MAX_JSON_BYTES);19    expect(() => decodePeachScript(tooLarge)).toThrow(20      "PEACHSCRIPT_TOO_LARGE",21    );22  });2324  test("decode accepts input at the bound", () => {25    const atLimit = String.fromCodePoint(26      ...Array.from({ length: 64 }, () => 0xe041),27    );28    expect(decodePeachScript(atLimit).length).toBe(64);29  });30});3132describe("peachScript", () => {33  test("byte 0 maps to U+E000", () => {34    const out = encodePeachScript(new Uint8Array([0]));35    expect(out.codePointAt(0)).toBe(0xe000);36  });3738  test("byte 255 maps to U+E0FF", () => {39    const out = encodePeachScript(new Uint8Array([255]));40    expect(out.codePointAt(0)).toBe(0xe0ff);41  });4243  test("round-trip identity for all 256 byte values", () => {44    const bytes = new Uint8Array(256);45    for (let i = 0; i < 256; i++) bytes[i] = i;46    const encoded = encodePeachScript(bytes);47    const decoded = decodePeachScript(encoded);48    expect(decoded).toEqual(bytes);49  });5051  test("empty bytes round-trip to empty string and back", () => {52    expect(encodePeachScript(new Uint8Array())).toBe("");53    expect(decodePeachScript("")).toEqual(new Uint8Array());54  });5556  test("decode throws on character outside PUA range", () => {57    expect(() => decodePeachScript("hello")).toThrow("INVALID_PEACHSCRIPT_CHARACTER");58    expect(() => decodePeachScript(String.fromCodePoint(0xe100))).toThrow(59      "INVALID_PEACHSCRIPT_CHARACTER"60    );61    expect(() => decodePeachScript(String.fromCodePoint(0xdfff))).toThrow(62      "INVALID_PEACHSCRIPT_CHARACTER"63    );64  });6566  test("random-payload round-trip preserves bytes", () => {67    const payload = crypto.getRandomValues(new Uint8Array(2048));68    const decoded = decodePeachScript(encodePeachScript(payload));69    expect(decoded).toEqual(payload);70  });7172  test("normalisePeachScript strips whitespace", () => {73    const encoded = encodePeachScript(new Uint8Array([1, 2, 3, 4]));74    const wrapped = encoded.slice(0, 2) + "\n  " + encoded.slice(2);75    expect(normalisePeachScript(wrapped)).toBe(encoded);76  });7778  test("isPeachScript accepts only PUA strings", () => {79    expect(isPeachScript(encodePeachScript(new Uint8Array([1, 2, 3])))).toBe(true);80    expect(isPeachScript("")).toBe(false);81    expect(isPeachScript("hello")).toBe(false);82    const mixed = encodePeachScript(new Uint8Array([1])) + "x";83    expect(isPeachScript(mixed)).toBe(false);84  });85});8687describe("peachScript fuzz corpus", () => {88  // Off-by-one boundary — U+E100 is one above the valid ceiling U+E0FF89  test("rejects U+E100 (off-by-one out of range)", () => {90    expect(() => decodePeachScript(String.fromCodePoint(0xe100))).toThrow(91      "INVALID_PEACHSCRIPT_CHARACTER"92    );93  });9495  test("rejects U+E200 (far out of range)", () => {96    expect(() => decodePeachScript(String.fromCodePoint(0xe200))).toThrow(97      "INVALID_PEACHSCRIPT_CHARACTER"98    );99  });100101  // Surrogate code points — below the valid PUA range U+E000..U+E0FF102  test("rejects lone low surrogate U+DFFF", () => {103    expect(() => decodePeachScript(String.fromCodePoint(0xdfff))).toThrow(104      "INVALID_PEACHSCRIPT_CHARACTER"105    );106  });107108  test("rejects lone high surrogate U+D800", () => {109    expect(() => decodePeachScript(String.fromCodePoint(0xd800))).toThrow(110      "INVALID_PEACHSCRIPT_CHARACTER"111    );112  });113114  // Real Mayan numerals are U+1D2E0..U+1D2F3 — outside PUA codec range115  test("rejects U+1D2E0 (real Mayan numeral — outside codec range)", () => {116    expect(() => decodePeachScript("\u{1D2E0}")).toThrow(117      "INVALID_PEACHSCRIPT_CHARACTER"118    );119  });120121  // BOM and Unicode direction/zero-width marks mid-stream122  test("rejects BOM (U+FEFF) mid-stream", () => {123    const encoded = encodePeachScript(new Uint8Array([0x41, 0x42]));124    // U+FEFF = BOM, inject between two valid runes125    const withBOM = encoded[0] + String.fromCodePoint(0xfeff) + encoded[1];126    expect(() => decodePeachScript(withBOM)).toThrow("INVALID_PEACHSCRIPT_CHARACTER");127  });128129  test("rejects RTL mark (U+200F) mid-stream", () => {130    const encoded = encodePeachScript(new Uint8Array([0x41]));131    // U+200F = RIGHT-TO-LEFT MARK132    expect(() => decodePeachScript(encoded + String.fromCodePoint(0x200f))).toThrow(133      "INVALID_PEACHSCRIPT_CHARACTER"134    );135  });136137  test("rejects zero-width non-joiner (U+200C)", () => {138    const encoded = encodePeachScript(new Uint8Array([0x41]));139    // U+200C = ZERO WIDTH NON-JOINER140    expect(() => decodePeachScript(encoded + String.fromCodePoint(0x200c))).toThrow(141      "INVALID_PEACHSCRIPT_CHARACTER"142    );143  });144145  // Whitespace — the raw decoder is strict; normalisation is a separate step146  test("rejects space between valid rune characters", () => {147    const encoded = encodePeachScript(new Uint8Array([0x41, 0x42]));148    const withSpace = encoded[0] + " " + encoded[1];149    expect(() => decodePeachScript(withSpace)).toThrow("INVALID_PEACHSCRIPT_CHARACTER");150  });151152  test("rejects newline between valid rune characters", () => {153    const encoded = encodePeachScript(new Uint8Array([0x41, 0x42]));154    const withNewline = encoded[0] + "\n" + encoded[1];155    expect(() => decodePeachScript(withNewline)).toThrow("INVALID_PEACHSCRIPT_CHARACTER");156  });157158  // Boundary bytes must round-trip correctly159  test("byte 0x00 round-trips correctly", () => {160    const data = new Uint8Array([0x00]);161    expect(decodePeachScript(encodePeachScript(data))).toEqual(data);162  });163164  test("byte 0xFE round-trips correctly", () => {165    const data = new Uint8Array([0xfe]);166    expect(decodePeachScript(encodePeachScript(data))).toEqual(data);167  });168169  test("byte 0xFF round-trips correctly", () => {170    const data = new Uint8Array([0xff]);171    expect(decodePeachScript(encodePeachScript(data))).toEqual(data);172  });173174  // 1 KiB pseudo-random round-trip175  test("1 KiB deterministic payload round-trips", () => {176    const data = new Uint8Array(1024);177    for (let i = 0; i < 1024; i++) data[i] = (i * 37 + 13) % 256;178    expect(decodePeachScript(encodePeachScript(data))).toEqual(data);179  });180181  // DoS resistance — the decoder must fail on the first invalid character,182  // not after scanning the whole input. U+E100 > 0xE0FF fails on char 0.183  test("rejects large all-invalid input in well under 50 ms", () => {184    const invalid = String.fromCodePoint(0xe100).repeat(10_000);185    const start = Date.now();186    expect(() => decodePeachScript(invalid)).toThrow("INVALID_PEACHSCRIPT_CHARACTER");187    expect(Date.now() - start).toBeLessThan(50);188  });189190  // Copy-paste hostility — smart quote prefix (U+201C = LEFT DOUBLE QUOTATION MARK)191  test("rejects left double quotation mark (U+201C) prefix", () => {192    const encoded = encodePeachScript(new Uint8Array([0x41]));193    expect(() => decodePeachScript(String.fromCodePoint(0x201c) + encoded)).toThrow(194      "INVALID_PEACHSCRIPT_CHARACTER"195    );196  });197});