1ad3a3572756

add peachScript.ts — PUA U+E000..U+E0FF byte-to-codepoint codec with full round-trip tests

time
2026-04-27 15:55 UTC · 6 months ago
machine
koh2
parent
3cd9c3a61b67
3 files public/fonts/PeachScript-Regular.otfbinarysrc/crypto/peachScript.test.ts+62 −0src/crypto/peachScript.ts+64 −0
public/fonts/PeachScript-Regular.otfbinary

binary file, not shown

view file

src/crypto/peachScript.test.ts+62 −0
@@ -0,0 +1,62 @@1+import { describe, expect, test } from "vitest";2+import {3+  decodePeachScript,4+  encodePeachScript,5+  isPeachScript,6+  normalisePeachScript,7+} from "./peachScript";8+9+describe("peachScript", () => {10+  test("byte 0 maps to U+E000", () => {11+    const out = encodePeachScript(new Uint8Array([0]));12+    expect(out.codePointAt(0)).toBe(0xe000);13+  });14+15+  test("byte 255 maps to U+E0FF", () => {16+    const out = encodePeachScript(new Uint8Array([255]));17+    expect(out.codePointAt(0)).toBe(0xe0ff);18+  });19+20+  test("round-trip identity for all 256 byte values", () => {21+    const bytes = new Uint8Array(256);22+    for (let i = 0; i < 256; i++) bytes[i] = i;23+    const encoded = encodePeachScript(bytes);24+    const decoded = decodePeachScript(encoded);25+    expect(decoded).toEqual(bytes);26+  });27+28+  test("empty bytes round-trip to empty string and back", () => {29+    expect(encodePeachScript(new Uint8Array())).toBe("");30+    expect(decodePeachScript("")).toEqual(new Uint8Array());31+  });32+33+  test("decode throws on character outside PUA range", () => {34+    expect(() => decodePeachScript("hello")).toThrow("INVALID_PEACHSCRIPT_CHARACTER");35+    expect(() => decodePeachScript(String.fromCodePoint(0xe100))).toThrow(36+      "INVALID_PEACHSCRIPT_CHARACTER"37+    );38+    expect(() => decodePeachScript(String.fromCodePoint(0xdfff))).toThrow(39+      "INVALID_PEACHSCRIPT_CHARACTER"40+    );41+  });42+43+  test("random-payload round-trip preserves bytes", () => {44+    const payload = crypto.getRandomValues(new Uint8Array(2048));45+    const decoded = decodePeachScript(encodePeachScript(payload));46+    expect(decoded).toEqual(payload);47+  });48+49+  test("normalisePeachScript strips whitespace", () => {50+    const encoded = encodePeachScript(new Uint8Array([1, 2, 3, 4]));51+    const wrapped = encoded.slice(0, 2) + "\n  " + encoded.slice(2);52+    expect(normalisePeachScript(wrapped)).toBe(encoded);53+  });54+55+  test("isPeachScript accepts only PUA strings", () => {56+    expect(isPeachScript(encodePeachScript(new Uint8Array([1, 2, 3])))).toBe(true);57+    expect(isPeachScript("")).toBe(false);58+    expect(isPeachScript("hello")).toBe(false);59+    const mixed = encodePeachScript(new Uint8Array([1])) + "x";60+    expect(isPeachScript(mixed)).toBe(false);61+  });62+});

view file

src/crypto/peachScript.ts+64 −0
@@ -0,0 +1,64 @@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+ */15+16+const BASE = 0xe000;17+18+/** Encode a binary payload to a PeachScript string (one byte per code point). */19+export function encodePeachScript(bytes: Uint8Array): string {20+  const out: string[] = [];21+  for (let i = 0; i < bytes.length; i++) {22+    out.push(String.fromCodePoint(BASE + bytes[i]));23+  }24+  return out.join("");25+}26+27+/**28+ * Decode a PeachScript string back to bytes. Throws29+ * `INVALID_PEACHSCRIPT_CHARACTER` when any character is outside the30+ * allotted PUA range — i.e. the string was edited, truncated mid-pair,31+ * or is not PeachScript at all.32+ */33+export function decodePeachScript(str: string): Uint8Array {34+  const codePoints = Array.from(str);35+  const bytes = new Uint8Array(codePoints.length);36+  for (let i = 0; i < codePoints.length; i++) {37+    const cp = codePoints[i].codePointAt(0)!;38+    if (cp < BASE || cp > BASE + 0xff) {39+      throw new Error("INVALID_PEACHSCRIPT_CHARACTER");40+    }41+    bytes[i] = cp - BASE;42+  }43+  return bytes;44+}45+46+/** Strip whitespace from a PeachScript string before decoding. */47+export function normalisePeachScript(str: string): string {48+  return str.replace(/\s+/g, "");49+}50+51+/**52+ * Cheap pre-flight: every non-whitespace char must be in the PUA range and53+ * the string must be non-empty. Used by the import UI to gate the decode54+ * button without throwing.55+ */56+export function isPeachScript(str: string): boolean {57+  const trimmed = normalisePeachScript(str);58+  if (trimmed.length === 0) return false;59+  for (const ch of trimmed) {60+    const cp = ch.codePointAt(0)!;61+    if (cp < BASE || cp > BASE + 0xff) return false;62+  }63+  return true;64+}

view file