Line data Source code
1 : import { BASE64URL_CODEC } from '../utils/base64.js';
2 :
3 : /// The hash parameter carrying a shared script.
4 : /// The hash never reaches the server and does not collide with the ?data= setup links of the app.
5 1 : export const SHARE_SCRIPT_HASH_PARAMETER = 'script';
6 :
7 : /// Pipe bytes through a (de)compression stream.
8 : /// Reads the stream directly, Firefox logs corrupt input as console error when reading it through a Response.
9 1 : async function pipeBytes(bytes: Uint8Array, transform: CompressionStream | DecompressionStream): Promise<ArrayBuffer> {
10 1 : const writer = transform.writable.getWriter();
11 1 : const written = writer.write(bytes as Uint8Array<ArrayBuffer>).then(() => writer.close());
12 : // A failed write also fails the read below, report the read error
13 1 : written.catch(() => {});
14 1 : const reader = transform.readable.getReader();
15 1 : const chunks: Uint8Array[] = [];
16 1 : let length = 0;
17 1 : for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
18 1 : chunks.push(chunk.value);
19 1 : length += chunk.value.byteLength;
20 1 : }
21 1 : await written;
22 1 : const out = new Uint8Array(length);
23 1 : let offset = 0;
24 1 : for (const chunk of chunks) {
25 1 : out.set(chunk, offset);
26 1 : offset += chunk.byteLength;
27 1 : }
28 1 : return out.buffer;
29 1 : }
30 :
31 : /// Encode a script as base64url of its deflated UTF-8 bytes
32 1 : export async function encodeShareScript(script: string): Promise<string> {
33 1 : const deflated = await pipeBytes(new TextEncoder().encode(script), new CompressionStream('deflate-raw'));
34 1 : return BASE64URL_CODEC.encode(deflated);
35 1 : }
36 :
37 : /// Decode a script encoded with encodeShareScript
38 1 : export async function decodeShareScript(payload: string): Promise<string> {
39 1 : if (payload.length === 0 || !BASE64URL_CODEC.isValidBase64(payload)) {
40 1 : throw new Error('shared script is not valid base64url');
41 : }
42 1 : const deflated = new Uint8Array(BASE64URL_CODEC.decode(payload));
43 1 : const inflated = await pipeBytes(deflated, new DecompressionStream('deflate-raw'));
44 1 : return new TextDecoder('utf-8', { fatal: true }).decode(inflated);
45 1 : }
46 :
47 : /// Build the URL that opens the shell with a shared script
48 1 : export function buildShareUrl(payload: string, location: Pick<Location, 'origin' | 'pathname' | 'search'> = window.location): string {
49 1 : return `${location.origin}${location.pathname}${location.search}#${SHARE_SCRIPT_HASH_PARAMETER}=${payload}`;
50 1 : }
51 :
52 : /// Read a shared script payload from the hash of the current URL and remove it from the address bar
53 1 : export function readShareScriptFromLocation(): string | null {
54 1 : const params = new URLSearchParams(window.location.hash.slice(1));
55 1 : const payload = params.get(SHARE_SCRIPT_HASH_PARAMETER);
56 1 : if (payload == null) return null;
57 1 : params.delete(SHARE_SCRIPT_HASH_PARAMETER);
58 1 : const hash = params.toString();
59 1 : window.history.replaceState(window.history.state, '', `${window.location.pathname}${window.location.search}${hash.length > 0 ? `#${hash}` : ''}`);
60 1 : return payload;
61 1 : }
|