213 lines
6.8 KiB
Svelte
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>
|