diff --git a/src/v1.js b/src/v1.js index ca9a70ec..8a0bf3a1 100644 --- a/src/v1.js +++ b/src/v1.js @@ -19,36 +19,46 @@ function v1(options, buf, offset) { const b = buf || new Array(16); options = options || {}; - let node = options.node || _nodeId; - let clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; + let node = options.node; + let clockseq = options.clockseq; - // node and clockseq need to be initialized to random values if they're not - // specified. We do this lazily to minimize issues related to insufficient - // system entropy. See #189 + // v1 only: Use cached `node` and `clockseq` values + if (!options._v6) { + if (!node) node = _nodeId; + if (clockseq == null) clockseq = _clockseq; + } + + // Handle cases where we need entropy. We do this lazily to minimize issues + // related to insufficient system entropy. See #189 if (node == null || clockseq == null) { const seedBytes = options.random || (options.rng || rng)(); + // Randomize node if (node == null) { - // Per 4.5, create a 48-bit node id, (47 random bits + multicast bit = 1) - node = _nodeId = [ - seedBytes[0] | 0x01, - seedBytes[1], - seedBytes[2], - seedBytes[3], - seedBytes[4], - seedBytes[5], - ]; + node = [seedBytes[0], seedBytes[1], seedBytes[2], seedBytes[3], seedBytes[4], seedBytes[5]]; + + // v1 only: cache node value for reuse + if (!_nodeId && !options._v6) { + // per RFC4122 4.5: Set MAC multicast bit (v1 only) + node[0] |= 0x01; // Set multicast bit + + _nodeId = node; + } } + // Randomize clockseq if (clockseq == null) { // Per 4.2.2, randomize (14 bit) clockseq - clockseq = _clockseq = ((seedBytes[6] << 8) | seedBytes[7]) & 0x3fff; + clockseq = ((seedBytes[6] << 8) | seedBytes[7]) & 0x3fff; + if (_clockseq === undefined && !options._v6) { + _clockseq = clockseq; + } } } - // UUID timestamps are 100 nano-second units since the Gregorian epoch, - // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so - // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' + // v1 & v6 timestamps are 100 nano-second units since the Gregorian epoch, + // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so time is + // handled internally as 'msecs' (integer milliseconds) and 'nsecs' // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. let msecs = options.msecs !== undefined ? options.msecs : Date.now(); diff --git a/src/v1ToV6.js b/src/v1ToV6.js index d7e865c4..60664a14 100644 --- a/src/v1ToV6.js +++ b/src/v1ToV6.js @@ -1,47 +1,18 @@ import parse from './parse.js'; -import rng from './rng.js'; import { unsafeStringify } from './stringify.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 set to random values for each new v6 UUID - * to aid in collision resistance and security. This function does not do this - * by default, however, 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 needing the RFC-recommended randomization can enable this by passing - * `true` for the `randomize` argument. - * * @param {string|Uint8Array} uuid - The v1 UUID to convert to v6 - * @param {boolean} [randomize=false] - Whether to randomize the clock_seq and - * node fields - * @returns {string|Uint8Array} The v6 UUID as a string or Uint8Array + * @returns {string|Uint8Array} The v6 UUID as the same type as the `uuid` arg + * (string or Uint8Array) */ -export default function v1ToV6(uuid, randomize = false) { - // Non-string UUIDs are documented as being Uint8Arrays, but we don't enforce - // that. They just need to be "array-like". And some day when we port this - // to TypeScript we'll have to take an actual stance on this. +export default function v1ToV6(uuid) { const v1Bytes = typeof uuid === 'string' ? parse(uuid) : uuid; const v6Bytes = _v1ToV6(v1Bytes); - if (randomize) { - const rnds = rng(); - v6Bytes[8] = (rnds[0] & 0x3f) | 0x80; - v6Bytes[9] = rnds[1]; - v6Bytes[10] = rnds[2]; - v6Bytes[11] = rnds[3]; - v6Bytes[12] = rnds[4]; - v6Bytes[13] = rnds[5]; - v6Bytes[14] = rnds[6]; - v6Bytes[15] = rnds[7]; - } return typeof uuid === 'string' ? unsafeStringify(v6Bytes) : v6Bytes; } diff --git a/src/v6.js b/src/v6.js index 9b85fe42..550b9708 100644 --- a/src/v6.js +++ b/src/v6.js @@ -9,30 +9,15 @@ import v1ToV6 from './v1ToV6.js'; * @param {number=} offset * @returns */ -export default function v6(options, buf, offset = 0) { - // v6 is just a v1 UUID with different field layout. - let bytes = v1(options, new Uint8Array(16)); +export default function v6(options = {}, buf, offset = 0) { + // v6 is v1 with different field layout, so we start with a v1 UUID. + let bytes = v1({ ...options, _v6: true }, new Uint8Array(16)); // Reorder the fields to v6 layout. Per RFC9562, we randomize the clock_seq // and node fields for each UUID. Randomize node and clock_seq here to get // the RFC-recommended behavior (we restore these below if they're provided in // options.) - bytes = v1ToV6(bytes, true); - - // Apply options.clockseq? - const clockseq = options?.clockseq; - if (clockseq !== undefined) { - bytes[8] = ((clockseq >> 8) & 0x3f) | 0x80; - bytes[9] = clockseq & 0xff; - } - - // Apply options.node? - const node = options?.node; - if (node !== undefined) { - for (let i = 0; i < 6; i++) { - bytes[10 + i] = node[i]; - } - } + bytes = v1ToV6(bytes); if (buf) { // Copy to buffer diff --git a/src/v6ToV1.js b/src/v6ToV1.js index 7fcb44de..349c1afe 100644 --- a/src/v6ToV1.js +++ b/src/v6ToV1.js @@ -4,16 +4,11 @@ import { unsafeStringify } from './stringify.js'; /** * Convert a v6 UUID to a v1 UUID * - * @param {string|Uint8Array} uuid - The v1 UUID to convert to v6 - * @param {boolean} [randomize=false] - Whether to randomize the clock_seq and - * node fields - * @returns {string|Uint8Array} The v6 UUID as the same type as the `uuid` arg + * @param {string|Uint8Array} uuid - The v6 UUID to convert to v6 + * @returns {string|Uint8Array} The v1 UUID as the same type as the `uuid` arg * (string or Uint8Array) */ export default function v6ToV1(uuid) { - // Non-string UUIDs are documented as being Uint8Arrays, but we don't enforce - // that. They just need to be "array-like". And some day when we port this - // to TypeScript we'll have to take an actual stance on this. const v6Bytes = typeof uuid === 'string' ? parse(uuid) : uuid; const v1Bytes = _v6ToV1(v6Bytes); diff --git a/test/unit/v6.test.js b/test/unit/v6.test.js index b5e9b1c1..ae6c4294 100644 --- a/test/unit/v6.test.js +++ b/test/unit/v6.test.js @@ -14,26 +14,44 @@ describe('v6', () => { node: [0x61, 0xcd, 0x3c, 0xbb, 0x32, 0x10], }; - test('v6 (full options)', () => { + const EXPECTED_BYTES = [30, 17, 34, 189, 148, 40, 104, 136, 184, 92, 97, 205, 60, 187, 50, 16]; + + test('default behavior', () => { // Verify explicit options produce expected id - const id = v6(fullOptions); - assert.equal(id, '1e1122bd-9428-6888-b85c-61cd3cbb3210'); + const id = v6(); + assert( + /[0-9a-f]{8}-[0-9a-f]{4}-6[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/.test(id), + 'id is valid v6 UUID' + ); }); - const expectedBytes = [30, 17, 34, 189, 148, 40, 104, 136, 184, 92, 97, 205, 60, 187, 50, 16]; - - test('v6 (binary)', () => { + test('default behavior (binary type)', () => { const buffer = []; const result = v6(fullOptions, buffer); - assert.deepEqual(buffer, expectedBytes); + assert.deepEqual(buffer, EXPECTED_BYTES); assert.strictEqual(buffer, result); }); - test('v6 (binary w/ buffer offset)', () => { + test('all options', () => { + // Verify explicit options produce expected id + const id = v6(fullOptions); + assert.equal(id, '1e1122bd-9428-6888-b85c-61cd3cbb3210'); + }); + + test('sort by creation time', () => { + // Verify ids sort by creation time + const ids = []; + for (let i = 0; i < 5; i++) { + ids.push(v6({ msecs: i * 1000 })); + } + assert.deepEqual(ids, ids.slice().sort()); + }); + + test('creating at array offset', () => { const buffer = []; v6(fullOptions, buffer, 0); v6(fullOptions, buffer, 16); - assert.deepEqual(buffer, expectedBytes.concat(expectedBytes)); + assert.deepEqual(buffer, EXPECTED_BYTES.concat(EXPECTED_BYTES)); }); test('v1 -> v6 conversion', () => { @@ -41,16 +59,6 @@ describe('v6', () => { assert.equal(id, V6_ID); }); - test('v1 -> v6 conversion (randomized0', () => { - const id = v1ToV6(V1_ID, true); - - // clock_seq and node fields should be randomized - assert.notEqual(id, V6_ID); - - // timestamp field should not change - assert.equal(id.slice(0, 19), V6_ID.slice(0, 19)); - }); - test('v6 -> v1 conversion', () => { const id = v6ToV1(V6_ID); assert.equal(id, V1_ID);