peachScript.ts

2.6 KB · raw

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}