Zum Inhalt

src/share/encodePreset.ts

// src/share/encodePreset.ts
// Round-trip a preset through msgpack + base64url so it can live in a URL
// fragment (#p=…). The decoder validates against the canonical schema so an
// outdated or hand-crafted link can never poison the running engine.

import { decode, encode } from '@msgpack/msgpack';
import type { Preset } from '~/engine/types';
import { PresetSchema } from '~/presets/schema';

function b64urlFromBytes(bytes: Uint8Array): string {
  let bin = '';
  for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i] ?? 0);
  return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function bytesFromB64url(s: string): Uint8Array {
  const pad = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4));
  const bin = atob(s.replace(/-/g, '+').replace(/_/g, '/') + pad);
  const out = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
  return out;
}

export function encodePreset(preset: Preset): string {
  // Shareable copy: always demote builtIn so the recipient treats it as custom.
  const payload = { ...preset, builtIn: false };
  return b64urlFromBytes(encode(payload));
}

export function decodePreset(token: string): Preset | null {
  try {
    const decoded = decode(bytesFromB64url(token));
    const result = PresetSchema.safeParse(decoded);
    if (!result.success) return null;
    return { ...(result.data as Preset), builtIn: false };
  } catch {
    return null;
  }
}

/** Builds a full https://… URL for sharing a preset. */
export function shareUrl(preset: Preset): string {
  const { origin, pathname } = window.location;
  return `${origin}${pathname}#p=${encodePreset(preset)}`;
}

/** Reads `#p=…` from the current URL and returns the decoded preset, if any. */
export function readPresetFromHash(): Preset | null {
  const hash = window.location.hash;
  if (!hash.startsWith('#p=')) return null;
  return decodePreset(hash.slice(3));
}

/** Removes the `#p=…` fragment without triggering a navigation. */
export function clearPresetHash(): void {
  if (!window.location.hash.startsWith('#p=')) return;
  const { origin, pathname, search } = window.location;
  window.history.replaceState(null, '', `${origin}${pathname}${search}`);
}