peachScript.ts
1/**2 * peachScript.ts — PeachScript encoding/decoding.3 *4 * PeachScript is the device-to-device export format. Each byte of an5 * encrypted vault payload maps to one Unicode Private Use Area code point6 * in U+E000..U+E0FF. The bundled `PeachScript-Regular.otf` renders those7 * 256 code points as the visual script. Outside Peach the string renders8 * as boxes — that is the desired property: PeachScript is meant to be9 * read by Peach, not by humans.10 *11 * The encoding is trivially reversible: byte 0 → U+E000, byte 255 →12 * U+E0FF. No padding, no base conversion, no alignment fix-ups. The font13 * does the visual work; this module is the codec.14 */1516const BASE = 0xe000;1718// PB-26-010: decode allocates one byte per code point, so bound the input at19// the same 10 MiB ceiling parseImportSource applies to CSV/JSON — otherwise a20// pasted or loaded string allocates without bound before any validation.21import { IMPORT_LIMITS } from "../shared/import-limits";2223/** Encode a binary payload to a PeachScript string (one byte per code point). */24export function encodePeachScript(bytes: Uint8Array): string {25 const out: string[] = [];26 for (let i = 0; i < bytes.length; i++) {27 out.push(String.fromCodePoint(BASE + bytes[i]));28 }29 return out.join("");30}3132/**33 * Decode a PeachScript string back to bytes. Throws34 * `INVALID_PEACHSCRIPT_CHARACTER` when any character is outside the35 * allotted PUA range — i.e. the string was edited, truncated mid-pair,36 * or is not PeachScript at all.37 */38export function decodePeachScript(str: string): Uint8Array {39 if (str.length > IMPORT_LIMITS.MAX_JSON_BYTES) {40 throw new Error("PEACHSCRIPT_TOO_LARGE");41 }42 const codePoints = Array.from(str);43 const bytes = new Uint8Array(codePoints.length);44 for (let i = 0; i < codePoints.length; i++) {45 const cp = codePoints[i].codePointAt(0)!;46 if (cp < BASE || cp > BASE + 0xff) {47 throw new Error("INVALID_PEACHSCRIPT_CHARACTER");48 }49 bytes[i] = cp - BASE;50 }51 return bytes;52}5354/** Strip whitespace from a PeachScript string before decoding. */55export function normalisePeachScript(str: string): string {56 return str.replace(/\s+/g, "");57}5859/**60 * Cheap pre-flight: every non-whitespace char must be in the PUA range and61 * the string must be non-empty. Used by the import UI to gate the decode62 * button without throwing.63 */64export function isPeachScript(str: string): boolean {65 const trimmed = normalisePeachScript(str);66 if (trimmed.length === 0) return false;67 for (const ch of trimmed) {68 const cp = ch.codePointAt(0)!;69 if (cp < BASE || cp > BASE + 0xff) return false;70 }71 return true;72}