Skip to content

Commit

Permalink
chore: revise v6 implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
broofa committed Jun 5, 2024
1 parent f5c6018 commit 18cff2e
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 95 deletions.
46 changes: 28 additions & 18 deletions src/v1.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
35 changes: 3 additions & 32 deletions src/v1ToV6.js
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down
23 changes: 4 additions & 19 deletions src/v6.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 2 additions & 7 deletions src/v6ToV1.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
46 changes: 27 additions & 19 deletions test/unit/v6.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,51 @@ 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', () => {
const id = v1ToV6(V1_ID);
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);
Expand Down

0 comments on commit 18cff2e

Please sign in to comment.