toolkit/src/routes/uuid/+page.svelte
2026-03-15 16:34:31 -03:00

213 lines
6.8 KiB
Svelte

<svelte:head>
<title>UUID Tools</title>
</svelte:head>
<script lang="js">
import { copy } from '$lib/clipboard.js';
// 100-ns intervals from 1582-10-15 to 1970-01-01
const GREGORIAN_OFFSET = 122192928000000000n;
/** @param {Uint8Array} bytes @returns {string} */
function bytesToUUID(bytes) {
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;
}
function uuidv1() {
const bytes = crypto.getRandomValues(new Uint8Array(16));
const t = BigInt(Date.now()) * 10000n + GREGORIAN_OFFSET;
const tLow = Number(t & 0xffffffffn);
const tMid = Number((t >> 32n) & 0xffffn);
const tHi = Number((t >> 48n) & 0x0fffn);
bytes[0] = (tLow >> 24) & 0xff;
bytes[1] = (tLow >> 16) & 0xff;
bytes[2] = (tLow >> 8) & 0xff;
bytes[3] = tLow & 0xff;
bytes[4] = (tMid >> 8) & 0xff;
bytes[5] = tMid & 0xff;
bytes[6] = ((tHi >> 8) & 0x0f) | 0x10;
bytes[7] = tHi & 0xff;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
bytes[10] |= 0x01; // multicast bit → random node
return bytesToUUID(bytes);
}
function uuidv4() {
return crypto.randomUUID();
}
function uuidv6() {
const bytes = crypto.getRandomValues(new Uint8Array(16));
const t = BigInt(Date.now()) * 10000n + GREGORIAN_OFFSET;
const tTop = t >> 12n;
bytes[0] = Number((tTop >> 40n) & 0xffn);
bytes[1] = Number((tTop >> 32n) & 0xffn);
bytes[2] = Number((tTop >> 24n) & 0xffn);
bytes[3] = Number((tTop >> 16n) & 0xffn);
bytes[4] = Number((tTop >> 8n) & 0xffn);
bytes[5] = Number( tTop & 0xffn);
bytes[6] = 0x60 | Number((t >> 8n) & 0x0fn);
bytes[7] = Number(t & 0xffn);
bytes[8] = (bytes[8] & 0x3f) | 0x80;
bytes[10] |= 0x01;
return bytesToUUID(bytes);
}
function uuidv7() {
const bytes = crypto.getRandomValues(new Uint8Array(16));
const ms = Date.now();
bytes[0] = (ms / 2**40) & 0xff;
bytes[1] = (ms / 2**32) & 0xff;
bytes[2] = (ms / 2**24) & 0xff;
bytes[3] = (ms / 2**16) & 0xff;
bytes[4] = (ms / 2**8) & 0xff;
bytes[5] = ms & 0xff;
bytes[6] = (bytes[6] & 0x0f) | 0x70;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
return bytesToUUID(bytes);
}
/**
* @typedef {{
* version: number,
* variant: string,
* timestamp?: number,
* date?: string,
* dateLocal?: string,
* randA?: string,
* randB?: string,
* clockSeq?: string,
* node?: string
* }} ParsedUUID
*/
/** @param {string} input @returns {ParsedUUID | { error: string }} */
function parseUUID(input) {
const s = input.trim().toLowerCase();
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(s)) {
return { error: 'Invalid UUID format (expected xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)' };
}
const hex = s.replace(/-/g, '');
const version = parseInt(hex[12], 16);
const variantNibble = parseInt(hex[16], 16);
let variant;
if ((variantNibble & 0x8) === 0) variant = 'NCS (0xxx)';
else if ((variantNibble & 0xc) === 0x8) variant = 'RFC 4122 (10xx)';
else if ((variantNibble & 0xe) === 0xc) variant = 'Microsoft (110x)';
else variant = 'Reserved (111x)';
/** @type {ParsedUUID} */
const result = { version, variant };
if (version === 7) {
const tsMs = parseInt(hex.slice(0, 12), 16);
const date = new Date(tsMs);
result.timestamp = tsMs;
result.date = date.toISOString();
result.dateLocal = date.toLocaleString();
result.randA = hex.slice(13, 16);
result.randB = hex.slice(16);
} else if (version === 1) {
const tHi = hex.slice(13, 16);
const tMid = hex.slice(8, 12);
const tLow = hex.slice(0, 8);
const t100ns = BigInt('0x' + tHi + tMid + tLow);
const unixMs = (t100ns - GREGORIAN_OFFSET) / 10000n;
const date = new Date(Number(unixMs));
result.timestamp = Number(unixMs);
result.date = date.toISOString();
result.dateLocal = date.toLocaleString();
result.clockSeq = hex.slice(16, 20);
result.node = hex.slice(20);
} else if (version === 6) {
const tHigh48 = hex.slice(0, 12);
const tLow12 = hex.slice(13, 16);
const t100ns = (BigInt('0x' + tHigh48) << 12n) | BigInt('0x' + tLow12);
const unixMs = (t100ns - GREGORIAN_OFFSET) / 10000n;
const date = new Date(Number(unixMs));
result.timestamp = Number(unixMs);
result.date = date.toISOString();
result.dateLocal = date.toLocaleString();
result.clockSeq = hex.slice(16, 20);
result.node = hex.slice(20);
}
return result;
}
// --- State ---
/** @type {{ label: string, uuid: string, gen: () => string }[]} */
let generators = $state([
{ label: 'v1', uuid: uuidv1(), gen: uuidv1 },
{ label: 'v4', uuid: uuidv4(), gen: uuidv4 },
{ label: 'v6', uuid: uuidv6(), gen: uuidv6 },
{ label: 'v7', uuid: uuidv7(), gen: uuidv7 },
]);
let parseInput = $state('');
const parseResult = $derived(parseInput.trim().length === 36 ? parseUUID(parseInput) : null);
const parsed = $derived(parseResult && !('error' in parseResult) ? parseResult : null);
const parseError = $derived(parseResult && 'error' in parseResult ? parseResult.error : '');
</script>
<h1>UUID</h1>
<h2>Generate</h2>
<table>
<tbody>
{#each generators as g}
<tr>
<td>{g.label}</td>
<td><code>{g.uuid}</code></td>
<td><button onclick={() => { g.uuid = g.gen(); }}>new</button></td>
<td><button onclick={() => copy(g.uuid)}>copy</button></td>
</tr>
{/each}
</tbody>
</table>
<hr>
<h2>Parse / Inspect</h2>
<p>
<input
type="text"
bind:value={parseInput}
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
size="38"
spellcheck="false"
>
</p>
{#if parseError}
<p class="error">{parseError}</p>
{/if}
{#if parsed}
<table>
<tbody>
<tr><td>Version</td><td><b>{parsed.version}</b></td><td></td></tr>
<tr><td>Variant</td><td>{parsed.variant}</td><td></td></tr>
{#if parsed.date}
<tr><td>Timestamp (ms)</td><td><code>{parsed.timestamp}</code></td><td><button onclick={() => copy(String(parsed.timestamp))}>copy</button></td></tr>
<tr><td>Date (UTC)</td><td><code>{parsed.date}</code></td><td><button onclick={() => copy(parsed.date ?? '')}>copy</button></td></tr>
<tr><td>Date (local)</td><td><code>{parsed.dateLocal}</code></td><td><button onclick={() => copy(parsed.dateLocal ?? '')}>copy</button></td></tr>
{/if}
{#if parsed.randA}
<tr><td>rand_a (12 bits)</td><td><code>{parsed.randA}</code></td><td></td></tr>
<tr><td>rand_b (62 bits)</td><td><code>{parsed.randB}</code></td><td></td></tr>
{/if}
{#if parsed.clockSeq}
<tr><td>Clock seq</td><td><code>{parsed.clockSeq}</code></td><td></td></tr>
<tr><td>Node</td><td><code>{parsed.node}</code></td><td><button onclick={() => copy(parsed.node ?? '')}>copy</button></td></tr>
{/if}
</tbody>
</table>
{/if}
<style>
.error { color: var(--err); }
</style>