From 7135ea28654d0761967f57f01c782be9a7337c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Remko=20Tron=C3=A7on?= Date: Sun, 21 Jan 2024 17:45:46 +0100 Subject: [PATCH] thurtle: Add URL compression --- src/web/thurtle/thurtle.tsx | 31 ++++++++--- src/web/thurtle/ulz.ts | 108 ++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 src/web/thurtle/ulz.ts diff --git a/src/web/thurtle/thurtle.tsx b/src/web/thurtle/thurtle.tsx index 33c9e96..7bf6a34 100644 --- a/src/web/thurtle/thurtle.tsx +++ b/src/web/thurtle/thurtle.tsx @@ -1,6 +1,7 @@ ///////////////////////////////////////////////////////////////////////// // Query parameters: // `p`: Base64-encoded program +// `u`: Base64-encoded ULZ-compressed program // `pn`: Program name. If `p` is not provided, looks up builtin example // `ar`: Auto-run program // `sn`: Show navbar (default: 1) @@ -18,9 +19,12 @@ import { import Editor from "./Editor"; import { saveAs } from "file-saver"; import draw from "./draw"; +import { ulzEncode, ulzDecode } from "./ulz"; declare let bootstrap: any; +//////////////////////////////////////////////////////////////////////////////// + async function b64encode(bs: Uint8Array, forURL?: boolean) { const url = await new Promise((resolve) => { const reader = new FileReader(); @@ -46,6 +50,8 @@ function b64decode(s: string) { ); } +//////////////////////////////////////////////////////////////////////////////// + function parseQS(sqs = window.location.search) { const qs: Record = {}; const sqss = sqs @@ -65,6 +71,8 @@ function parseQS(sqs = window.location.search) { const qs = parseQS(); +//////////////////////////////////////////////////////////////////////////////// + function About() { return ( <> @@ -604,13 +612,17 @@ async function downloadPNG(ev: MouseEvent) { async function share(ev: MouseEvent) { ev.preventDefault(); shareModalTitleEl.innerText = programsEl.value; - shareModalURLEl.value = `${window.location.protocol}//${ - window.location.host - }${window.location.pathname}?pn=${encodeURIComponent( - programsEl.value - )}&p=${encodeURIComponent( - await b64encode(new TextEncoder().encode(editor.getValue()), true) - )}&ar=1`; + let url = `${window.location.protocol}//${window.location.host}${ + window.location.pathname + }?pn=${encodeURIComponent(programsEl.value)}&ar=1&`; + const program = new TextEncoder().encode(editor.getValue()); + const compressed = ulzEncode(program); + if (compressed.length < program.length) { + url += `u=${encodeURIComponent(await b64encode(compressed, true))}`; + } else { + url += `p=${encodeURIComponent(await b64encode(program, true))}`; + } + shareModalURLEl.value = url; shareModal.show(); } @@ -676,8 +688,9 @@ async function reset() { ///////////////////////////////////////////////////////////////////////// -if (qs.p) { - saveProgram(qs.pn ?? "", new TextDecoder().decode(b64decode(qs.p)), true); +if (qs.p || qs.u) { + const program = qs.p ? b64decode(qs.p) : ulzDecode(b64decode(qs.u)); + saveProgram(qs.pn ?? "", new TextDecoder().decode(program), true); loadPrograms(); loadProgram(qs.pn); } else { diff --git a/src/web/thurtle/ulz.ts b/src/web/thurtle/ulz.ts new file mode 100644 index 0000000..86de837 --- /dev/null +++ b/src/web/thurtle/ulz.ts @@ -0,0 +1,108 @@ +/* + * ULZ ( http://wiki.xxiivv.com/site/ulz_format ) encoder & decoder + */ + +export function ulzDecode(src: Uint8Array) { + const dst: Array = []; + let sp = 0; + while (sp < src.length) { + const c = src[sp++]; + if (c & 0x80) { + // CPY + let length; + if (c & 0x40) { + if (sp >= src.length) { + throw new Error(`incomplete CPY2`); + } + length = ((c & 0x3f) << 8) | src[sp++]; + } else { + length = c & 0x3f; + } + if (sp >= src.length) { + throw new Error(`incomplete CPY`); + } + let cp = dst.length - (src[sp++] + 1); + if (cp < 0) { + throw new Error(`CPY underflow`); + } + for (let i = 0; i < length + 4; i++) { + dst.push(dst[cp++]); + } + } else { + // LIT + if (sp + c >= src.length) { + throw new Error(`LIT out of bounds: ${sp} + ${c} >= ${src.length}`); + } + for (let i = 0; i < c + 1; i++) { + dst.push(src[sp++]); + } + } + } + return new Uint8Array(dst); +} + +const MIN_MAX_LENGTH = 4; + +function findBestMatch( + src: Uint8Array, + sp: number, + dlen: number, + slen: number +) { + let bmlen = 0; + let bmp = 0; + let dp = sp - dlen; + for (; dlen; dp++, dlen--) { + let i = 0; + for (; ; i++) { + if (i == slen) { + return [dp, i]; + } + if (src[sp + i] != src[dp + (i % dlen)]) { + break; + } + } + if (i > bmlen) { + bmlen = i; + bmp = dp; + } + } + return [bmp, bmlen]; +} + +export function ulzEncode(src: Uint8Array) { + const dst: Array = []; + let sp = 0; + let litp = -1; + while (sp < src.length) { + const dlen = Math.min(sp, 256); + const slen = Math.min(src.length - sp, 0x3fff + MIN_MAX_LENGTH); + const [bmp, bmlen] = findBestMatch(src, sp, dlen, slen); + if (bmlen >= MIN_MAX_LENGTH) { + // CPY + const bmctl = bmlen - MIN_MAX_LENGTH; + if (bmctl > 0x3f) { + // CPY2 + dst.push((bmctl >> 8) | 0xc0); + dst.push(bmctl & 0xff); + } else { + dst.push(bmctl | 0x80); + } + dst.push(sp - bmp - 1); + sp += bmlen; + litp = -1; + } else { + // LIT + if (litp >= 0) { + if ((dst[litp] += 1) == 127) { + litp = -1; + } + } else { + dst.push(0); + litp = dst.length - 1; + } + dst.push(src[sp++]); + } + } + return new Uint8Array(dst); +}