From 9b365da73562a482252858421b73aef15775ae6c Mon Sep 17 00:00:00 2001 From: Robert Kieffer Date: Mon, 3 Jun 2024 12:02:36 -0700 Subject: [PATCH] feat: support v6 uuids --- src/v1tov6.js | 48 ++++++++++++++++++++++++++++++++++++++++++ src/v6tov1.js | 50 ++++++++++++++++++++++++++++++++++++++++++++ test/unit/v6.test.js | 26 +++++++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 src/v1tov6.js create mode 100644 src/v6tov1.js create mode 100644 test/unit/v6.test.js diff --git a/src/v1tov6.js b/src/v1tov6.js new file mode 100644 index 00000000..03715cf5 --- /dev/null +++ b/src/v1tov6.js @@ -0,0 +1,48 @@ +import parse from './parse.js'; +import rng from './rng.js'; +import stringify from './stringify.js'; +import version from './version.js'; + +export default function v1tov6(uuid, randomize = false) { + if (version(uuid) !== 1) { + throw new Error('id is not a valid v1 UUID'); + } + + const v1Bytes = parse(uuid); + + if (randomize) { + const rnds = rng(); + v1Bytes[8] = (rnds[0] & 0x3f) | 0x80; + v1Bytes[9] = rnds[1]; + v1Bytes[10] = rnds[2]; + v1Bytes[11] = rnds[3]; + v1Bytes[12] = rnds[4]; + v1Bytes[13] = rnds[5]; + v1Bytes[14] = rnds[6]; + v1Bytes[15] = rnds[7]; + } + + const v6Bytes = Uint8Array.from([ + ((v1Bytes[6] & 0x0f) << 4) | ((v1Bytes[7] >> 4) & 0x0f), + ((v1Bytes[7] & 0x0f) << 4) | ((v1Bytes[4] & 0xf0) >> 4), + ((v1Bytes[4] & 0x0f) << 4) | ((v1Bytes[5] & 0xf0) >> 4), + ((v1Bytes[5] & 0x0f) << 4) | ((v1Bytes[0] & 0xf0) >> 4), + + ((v1Bytes[0] & 0x0f) << 4) | ((v1Bytes[1] & 0xf0) >> 4), + ((v1Bytes[1] & 0x0f) << 4) | ((v1Bytes[2] & 0xf0) >> 4), + + 0x60 | (v1Bytes[2] & 0x0f), + v1Bytes[3], + + v1Bytes[8], + v1Bytes[9], + v1Bytes[10], + v1Bytes[11], + v1Bytes[12], + v1Bytes[13], + v1Bytes[14], + v1Bytes[15], + ]); + + return stringify(v6Bytes); +} diff --git a/src/v6tov1.js b/src/v6tov1.js new file mode 100644 index 00000000..3a76cba4 --- /dev/null +++ b/src/v6tov1.js @@ -0,0 +1,50 @@ +import parse from './parse.js'; +import stringify from './stringify.js'; +import version from './version.js'; + +/** + * Convert a v1 UUID to a v6 UUID. + * + * Note: Per https://www.rfc-editor.org/rfc/rfc9562.html#section-5.6-4, the + * clock_seq and node fields SHOULD be randomized to aid in collision resistance + * and security. However. this behavior is not enabled by default for two reasons: + * + * 1. Doing so makes the conversion non-reversible. I.e. `v6tov1(v1tov6(uuid)) + * !== uuid`. + * 2. Doing so makes the conversion non-deterministic. I.e. `v1tov6(uuid) !== + * v1tov6(uuid)` + * + * Callers wishing to enable the RFC-recommended randomization can do so by + * passing `true` for the second parameter. + */ +export default function v6tov1(uuid, randomize = false) { + if (version(uuid) !== 6) { + throw new Error('id is not a valid v6 UUID'); + } + + const v1Bytes = parse(uuid); + + const v6Bytes = Uint8Array.from([ + ((v1Bytes[3] & 0x0f) << 4) | ((v1Bytes[4] >> 4) & 0x0f), + ((v1Bytes[4] & 0x0f) << 4) | ((v1Bytes[5] & 0xf0) >> 4), + ((v1Bytes[5] & 0x0f) << 4) | (v1Bytes[6] & 0x0f), + v1Bytes[7], + + ((v1Bytes[1] & 0x0f) << 4) | ((v1Bytes[2] & 0xf0) >> 4), + ((v1Bytes[2] & 0x0f) << 4) | ((v1Bytes[3] & 0xf0) >> 4), + + 0x10 | ((v1Bytes[0] & 0xf0) >> 4), + ((v1Bytes[0] & 0x0f) << 4) | ((v1Bytes[1] & 0xf0) >> 4), + + v1Bytes[8], + v1Bytes[9], + v1Bytes[10], + v1Bytes[11], + v1Bytes[12], + v1Bytes[13], + v1Bytes[14], + v1Bytes[15], + ]); + + return stringify(v6Bytes); +} diff --git a/test/unit/v6.test.js b/test/unit/v6.test.js new file mode 100644 index 00000000..d7ef639a --- /dev/null +++ b/test/unit/v6.test.js @@ -0,0 +1,26 @@ +import assert from 'assert'; +import v1tov6 from '../../src/v1tov6.js'; +import v6tov1 from '../../src/v6tov1.js'; + +describe('v1 <-> v6 conversion', () => { + const v1Fixture = 'f1207660-21d2-11ef-8c4f-419efbd44d48'; + const v6Fixture = '1ef21d2f-1207-6660-8c4f-419efbd44d48'; + + test('v1 -> v6 conversion', () => { + const id = v1tov6(v1Fixture); + assert.equal(id, v6Fixture); + }); + + test('v1 -> v6 conversion (w', () => { + const id = v1tov6(v1Fixture, true); + + // + assert.notEqual(id, v6Fixture); + assert.equal(id.slice(0, 24), v6Fixture.slice(0, 24)); + }); + + test('v6 -> v1 conversion', () => { + const id = v6tov1(v6Fixture); + assert.equal(id, v1Fixture); + }); +});