62 lines
2.1 KiB
TypeScript
62 lines
2.1 KiB
TypeScript
import { migrateSave, type SaveDocument, type SaveParse } from "@deadhead/schema";
|
|
|
|
/**
|
|
* Save export/import (decision 0.5.92): one text code holding the whole save,
|
|
* gzip-compressed and base64url-encoded behind a short prefix, so it survives
|
|
* being pasted into a chat, a note or a file.
|
|
*/
|
|
export const SAVE_CODE_PREFIX = "DEADHEAD1.";
|
|
|
|
async function pipe(
|
|
bytes: Uint8Array,
|
|
stream: CompressionStream | DecompressionStream,
|
|
): Promise<Uint8Array> {
|
|
const piped = new Response(bytes as BodyInit).body!.pipeThrough(stream);
|
|
return new Uint8Array(await new Response(piped).arrayBuffer());
|
|
}
|
|
|
|
function toBase64Url(bytes: Uint8Array): string {
|
|
let binary = "";
|
|
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
}
|
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
}
|
|
|
|
function fromBase64Url(text: string): Uint8Array {
|
|
const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
|
|
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
}
|
|
|
|
/** The save as an export code. */
|
|
export async function encodeSave(save: SaveDocument): Promise<string> {
|
|
const json = new TextEncoder().encode(JSON.stringify(save));
|
|
return SAVE_CODE_PREFIX + toBase64Url(await pipe(json, new CompressionStream("gzip")));
|
|
}
|
|
|
|
/**
|
|
* Reads an export code: decodes, migrates an older `saveVersion` and validates.
|
|
* Never throws; anything that isn't a good save gives a readable error.
|
|
*/
|
|
export async function decodeSave(
|
|
code: string,
|
|
migrate: (raw: unknown) => SaveParse = migrateSave,
|
|
): Promise<SaveParse> {
|
|
// Pasting often adds spaces or line breaks.
|
|
const text = code.replace(/\s+/g, "");
|
|
if (!text.startsWith(SAVE_CODE_PREFIX)) {
|
|
return { ok: false, error: "That isn't a Deadhead save code." };
|
|
}
|
|
let raw: unknown;
|
|
try {
|
|
const bytes = await pipe(
|
|
fromBase64Url(text.slice(SAVE_CODE_PREFIX.length)),
|
|
new DecompressionStream("gzip"),
|
|
);
|
|
raw = JSON.parse(new TextDecoder().decode(bytes));
|
|
} catch {
|
|
return { ok: false, error: "The save code is incomplete or damaged." };
|
|
}
|
|
return migrate(raw);
|
|
}
|