diff --git a/README.md b/README.md index ec723630..b9079b69 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ For timestamp UUIDs, namespace UUIDs, and other options read on ... | [`uuid.v6()`](#uuidv6options-buffer-offset) | Create a version 6 (timestamp, reordered) UUID | New in `uuid@10` | | [`uuid.v6ToV1()`](#uuidv6tov1uuid) | Create a version 1 UUID from a version 6 UUID | New in `uuid@10` | | [`uuid.v7()`](#uuidv7options-buffer-offset) | Create a version 7 (Unix Epoch time-based) UUID | New in `uuid@10` | +| ~~[`uuid.v8()`](#uuidv8)~~ | "Intentionally left blank" | | | [`uuid.validate()`](#uuidvalidatestr) | Test a string to see if it is a valid UUID | New in `uuid@8.3` | | [`uuid.version()`](#uuidversionstr) | Detect RFC version of a UUID | New in `uuid@8.3` | @@ -120,7 +121,7 @@ import { parse as uuidParse } from 'uuid'; const bytes = uuidParse('6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b'); // Convert to hex strings to show byte order (for documentation purposes) -[...bytes].map((v) => v.toString(16).padStart(2, '0')); // ⇨ +[...bytes].map((v) => v.toString(16).padStart(2, '0')); // ⇨ // [ // '6e', 'c0', 'bd', '7f', // '11', 'c0', '43', 'da', @@ -353,6 +354,14 @@ import { v7 as uuidv7 } from 'uuid'; uuidv7(); // ⇨ '01695553-c90c-722d-9b5d-b38dfbbd4bed' ``` +### ~~uuid.v8()~~ + +**_"Intentionally left blank"_** + + +> [!NOTE] +> Version 8 (experimental) UUIDs are "[for experimental or vendor-specific use cases](https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-8)". The RFC does not define a creation algorithm for them, which is why this package does not offer a `v8()` method. The `validate()` and `version()` methods do work with such UUIDs, however. + ### uuid.validate(str) Test a string to see if it is a valid UUID diff --git a/README_js.md b/README_js.md index e4265d35..fc7a1e4c 100644 --- a/README_js.md +++ b/README_js.md @@ -82,6 +82,7 @@ For timestamp UUIDs, namespace UUIDs, and other options read on ... | [`uuid.v6()`](#uuidv6options-buffer-offset) | Create a version 6 (timestamp, reordered) UUID | New in `uuid@10` | | [`uuid.v6ToV1()`](#uuidv6tov1uuid) | Create a version 1 UUID from a version 6 UUID | New in `uuid@10` | | [`uuid.v7()`](#uuidv7options-buffer-offset) | Create a version 7 (Unix Epoch time-based) UUID | New in `uuid@10` | +| ~~[`uuid.v8()`](#uuidv8)~~ | "Intentionally left blank" | | | [`uuid.validate()`](#uuidvalidatestr) | Test a string to see if it is a valid UUID | New in `uuid@8.3` | | [`uuid.version()`](#uuidversionstr) | Detect RFC version of a UUID | New in `uuid@8.3` | @@ -361,6 +362,14 @@ import { v7 as uuidv7 } from 'uuid'; uuidv7(); // RESULT ``` +### ~~uuid.v8()~~ + +**_"Intentionally left blank"_** + + +> [!NOTE] +> Version 8 (experimental) UUIDs are "[for experimental or vendor-specific use cases](https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-8)". The RFC does not define a creation algorithm for them, which is why this package does not offer a `v8()` method. The `validate()` and `version()` methods do work with such UUIDs, however. + ### uuid.validate(str) Test a string to see if it is a valid UUID diff --git a/test/unit/test_constants.js b/test/unit/test_constants.js new file mode 100644 index 00000000..26c9aebb --- /dev/null +++ b/test/unit/test_constants.js @@ -0,0 +1,111 @@ +import MAX from '../../src/max.js'; +import NIL from '../../src/nil.js'; + +// Table of [uuid value, expected validate(), [expected version()]] +export const TESTS = [ + // constants + { value: NIL, expectedValidate: true, expectedVersion: 0 }, + { value: MAX, expectedValidate: true, expectedVersion: 15 }, + + // each version, with either all 0's or all 1's in settable bits + { value: '00000000-0000-1000-8000-000000000000', expectedValidate: true, expectedVersion: 1 }, + { value: 'ffffffff-ffff-1fff-8fff-ffffffffffff', expectedValidate: true, expectedVersion: 1 }, + { value: '00000000-0000-2000-8000-000000000000', expectedValidate: true, expectedVersion: 2 }, + { value: 'ffffffff-ffff-2fff-bfff-ffffffffffff', expectedValidate: true, expectedVersion: 2 }, + { value: '00000000-0000-3000-8000-000000000000', expectedValidate: true, expectedVersion: 3 }, + { value: 'ffffffff-ffff-3fff-bfff-ffffffffffff', expectedValidate: true, expectedVersion: 3 }, + { value: '00000000-0000-4000-8000-000000000000', expectedValidate: true, expectedVersion: 4 }, + { value: 'ffffffff-ffff-4fff-bfff-ffffffffffff', expectedValidate: true, expectedVersion: 4 }, + { value: '00000000-0000-5000-8000-000000000000', expectedValidate: true, expectedVersion: 5 }, + { value: 'ffffffff-ffff-5fff-bfff-ffffffffffff', expectedValidate: true, expectedVersion: 5 }, + { value: '00000000-0000-6000-8000-000000000000', expectedValidate: true, expectedVersion: 6 }, + { value: 'ffffffff-ffff-6fff-bfff-ffffffffffff', expectedValidate: true, expectedVersion: 6 }, + { value: '00000000-0000-7000-8000-000000000000', expectedValidate: true, expectedVersion: 7 }, + { value: 'ffffffff-ffff-7fff-bfff-ffffffffffff', expectedValidate: true, expectedVersion: 7 }, + { value: '00000000-0000-8000-8000-000000000000', expectedValidate: true, expectedVersion: 8 }, + { value: 'ffffffff-ffff-8fff-bfff-ffffffffffff', expectedValidate: true, expectedVersion: 8 }, + { value: '00000000-0000-9000-8000-000000000000', expectedValidate: false }, + { value: 'ffffffff-ffff-9fff-bfff-ffffffffffff', expectedValidate: false }, + { value: '00000000-0000-a000-8000-000000000000', expectedValidate: false }, + { value: 'ffffffff-ffff-afff-bfff-ffffffffffff', expectedValidate: false }, + { value: '00000000-0000-b000-8000-000000000000', expectedValidate: false }, + { value: 'ffffffff-ffff-bfff-bfff-ffffffffffff', expectedValidate: false }, + { value: '00000000-0000-c000-8000-000000000000', expectedValidate: false }, + { value: 'ffffffff-ffff-cfff-bfff-ffffffffffff', expectedValidate: false }, + { value: '00000000-0000-d000-8000-000000000000', expectedValidate: false }, + { value: 'ffffffff-ffff-dfff-bfff-ffffffffffff', expectedValidate: false }, + { value: '00000000-0000-e000-8000-000000000000', expectedValidate: false }, + { value: 'ffffffff-ffff-efff-bfff-ffffffffffff', expectedValidate: false }, + + // selection of normal, valid UUIDs + { value: 'd9428888-122b-11e1-b85c-61cd3cbb3210', expectedValidate: true, expectedVersion: 1 }, + { value: '000003e8-2363-21ef-b200-325096b39f47', expectedValidate: true, expectedVersion: 2 }, + { value: 'a981a0c2-68b1-35dc-bcfc-296e52ab01ec', expectedValidate: true, expectedVersion: 3 }, + { value: '109156be-c4fb-41ea-b1b4-efe1671c5836', expectedValidate: true, expectedVersion: 4 }, + { value: '90123e1c-7512-523e-bb28-76fab9f2f73d', expectedValidate: true, expectedVersion: 5 }, + { value: '1ef21d2f-1207-6660-8c4f-419efbd44d48', expectedValidate: true, expectedVersion: 6 }, + { value: '017f22e2-79b0-7cc3-98c4-dc0c0c07398f', expectedValidate: true, expectedVersion: 7 }, + { value: '0d8f23a0-697f-83ae-802e-48f3756dd581', expectedValidate: true, expectedVersion: 8 }, + + // all variant octet values + { value: '00000000-0000-1000-0000-000000000000', expectedValidate: false }, + { value: '00000000-0000-1000-1000-000000000000', expectedValidate: false }, + { value: '00000000-0000-1000-2000-000000000000', expectedValidate: false }, + { value: '00000000-0000-1000-3000-000000000000', expectedValidate: false }, + { value: '00000000-0000-1000-4000-000000000000', expectedValidate: false }, + { value: '00000000-0000-1000-5000-000000000000', expectedValidate: false }, + { value: '00000000-0000-1000-6000-000000000000', expectedValidate: false }, + { value: '00000000-0000-1000-7000-000000000000', expectedValidate: false }, + { value: '00000000-0000-1000-8000-000000000000', expectedValidate: true, expectedVersion: 1 }, + { value: '00000000-0000-1000-9000-000000000000', expectedValidate: true, expectedVersion: 1 }, + { value: '00000000-0000-1000-a000-000000000000', expectedValidate: true, expectedVersion: 1 }, + { value: '00000000-0000-1000-b000-000000000000', expectedValidate: true, expectedVersion: 1 }, + { value: '00000000-0000-1000-c000-000000000000', expectedValidate: false }, + { value: '00000000-0000-1000-d000-000000000000', expectedValidate: false }, + { value: '00000000-0000-1000-e000-000000000000', expectedValidate: false }, + { value: '00000000-0000-1000-f000-000000000000', expectedValidate: false }, + + // invalid strings + { value: '00000000000000000000000000000000', expectedValidate: false }, // unhyphenated NIL + { value: '', expectedValidate: false }, + { value: 'invalid uuid string', expectedValidate: false }, + { + value: '=Y00a-f*vb*-c-d#-p00f\b-g0h-#i^-j*3&-L00k-\nl---00n-fg000-00p-00r+', + expectedValidate: false, + }, + + // invalid types + { value: undefined, expectedValidate: false }, + { value: null, expectedValidate: false }, + { value: 123, expectedValidate: false }, + { value: /regex/, expectedValidate: false }, + { value: new Date(0), expectedValidate: false }, + { value: false, expectedValidate: false }, +]; + +// Add NIL and MAX UUIDs with 1-bit flipped in each position +for (let charIndex = 0; charIndex < 36; charIndex++) { + // Skip hyphens and version char + if ( + charIndex === 8 || + charIndex === 13 || + charIndex === 14 || // version char + charIndex === 18 || + charIndex === 23 + ) { + continue; + } + + const nilChars = NIL.split(''); + const maxChars = MAX.split(''); + + for (let i = 0; i < 4; i++) { + nilChars[charIndex] = (0x0 ^ (1 << i)).toString(16); + // NIL UUIDs w/ a single 1-bit + TESTS.push({ value: nilChars.join(''), expectedValidate: false }); + + // MAX UUIDs w/ a single 0-bit + maxChars[charIndex] = (0xf ^ (1 << i)).toString(16); + TESTS.push({ value: maxChars.join(''), expectedValidate: false }); + } +} diff --git a/test/unit/validate.test.js b/test/unit/validate.test.js index d5710904..39b08bc6 100644 --- a/test/unit/validate.test.js +++ b/test/unit/validate.test.js @@ -1,72 +1,15 @@ import assert from 'assert'; -import MAX from '../../src/max.js'; -import NIL from '../../src/nil.js'; import validate from '../../src/validate.js'; +import { TESTS } from './test_constants.js'; -describe('validate', () => { - test('validate uuid', () => { - assert.strictEqual(validate(NIL), true); - assert.strictEqual(validate(MAX), true); - - // test valid UUID versions - - // v1 - assert.strictEqual(validate('d9428888-122b-11e1-b85c-61cd3cbb3210'), true); - - // v3 - assert.strictEqual(validate('a981a0c2-68b1-35dc-bcfc-296e52ab01ec'), true); - - // v4 - assert.strictEqual(validate('109156be-c4fb-41ea-b1b4-efe1671c5836'), true); - - // v5 - assert.strictEqual(validate('90123e1c-7512-523e-bb28-76fab9f2f73d'), true); - - // v6 - assert.strictEqual(validate('1ef21d2f-1207-6660-8c4f-419efbd44d48'), true); - - // v7 - assert.strictEqual(validate('017f22e2-79b0-7cc3-98c4-dc0c0c07398f'), true); - - // test invalid/unsupported UUID versions - [0, 2, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f'].forEach((v) => { +describe('validate() tests', () => { + test('TESTS cases', () => { + for (const { value, expectedValidate } of TESTS) { assert.strictEqual( - validate('12300000-0000-' + v + '000-0000-000000000000'), - false, - 'version ' + v + ' should not be valid' + validate(value), + expectedValidate, + `validate(${value}) should be ${expectedValidate}` ); - }); - - assert.strictEqual(validate(), false); - - assert.strictEqual(validate(''), false); - - assert.strictEqual(validate('invalid uuid string'), false); - - assert.strictEqual(validate('00000000000000000000000000000000'), false); - - // NIL UUIDs that have a bit set (incorrectly) should not validate - for (let charIndex = 0; charIndex < 36; charIndex++) { - if (charIndex === 14) { - continue; - } // version field - - for (let bit = 0; bit < 4; bit++) { - const chars = NIL.split(''); - if (chars[charIndex] === '-') { - continue; - } - - chars[charIndex] = (1 << bit).toString(16); - assert.strictEqual(validate(chars.join('')), false); - } } - - assert.strictEqual( - validate( - '=Y00a-f*v00b*-00c-00d#-p00f\b-00g-00h-####00i^^^-00j*1*2*3&-L00k-\n00l-/00m-----00n-fg000-00p-00r+' - ), - false - ); }); }); diff --git a/test/unit/version.test.js b/test/unit/version.test.js index 89dd3a70..3b9f47bf 100644 --- a/test/unit/version.test.js +++ b/test/unit/version.test.js @@ -1,24 +1,17 @@ import assert from 'assert'; -import MAX from '../../src/max.js'; -import NIL from '../../src/nil.js'; import version from '../../src/version.js'; +import { TESTS } from './test_constants.js'; -describe('version', () => { - test('check uuid version', () => { - assert.strictEqual(version(NIL), 0); - assert.strictEqual(version(MAX), 15); - - assert.strictEqual(version('d9428888-122b-11e1-b85c-61cd3cbb3210'), 1); - assert.strictEqual(version('a981a0c2-68b1-35dc-bcfc-296e52ab01ec'), 3); - assert.strictEqual(version('109156be-c4fb-41ea-b1b4-efe1671c5836'), 4); - assert.strictEqual(version('90123e1c-7512-523e-bb28-76fab9f2f73d'), 5); - assert.strictEqual(version('1ef21d2f-1207-6660-8c4f-419efbd44d48'), 6); - assert.strictEqual(version('017f22e2-79b0-7cc3-98c4-dc0c0c07398f'), 7); - - assert.throws(() => version()); - assert.throws(() => version('')); - assert.throws(() => version('invalid uuid string')); - assert.throws(() => version('00000000000000000000000000000000')); - assert.throws(() => version('=Y00a-f*v00b*-00c-00d#-p00f\b-00g-00h-##0p-00r+')); +describe('version() tests', () => { + test('TESTS cases', () => { + for (const { value, expectedValidate, expectedVersion } of TESTS) { + try { + const actualVersion = version(value); + assert(expectedValidate, `version(${value}) should throw`); + assert.strictEqual(actualVersion, expectedVersion); + } catch (err) { + assert(!expectedValidate, `version(${value}) threw unexpectedly`); + } + } }); });