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
↓ peach-browser-1ad3a357.tar.gzatlas at this save
files changed3 files
public/fonts/PeachScript-Regular.otfbinarysrc/crypto/peachScript.test.ts+62 −0src/crypto/peachScript.ts+64 −0src/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+});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+}