Skip to content

Commit

Permalink
feat: v8 support (#759)
Browse files Browse the repository at this point in the history
  • Loading branch information
broofa authored Jun 7, 2024
1 parent c4ed13e commit 35a5342
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 84 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `[email protected]` |
| [`uuid.version()`](#uuidversionstr) | Detect RFC version of a UUID | New in `[email protected]` |

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -353,6 +354,14 @@ import { v7 as uuidv7 } from 'uuid';
uuidv7(); // ⇨ '01695553-c90c-722d-9b5d-b38dfbbd4bed'
```

### ~~uuid.v8()~~

**_"Intentionally left blank"_**

<!-- prettier-ignore -->
> [!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
Expand Down
9 changes: 9 additions & 0 deletions README_js.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `[email protected]` |
| [`uuid.version()`](#uuidversionstr) | Detect RFC version of a UUID | New in `[email protected]` |

Expand Down Expand Up @@ -361,6 +362,14 @@ import { v7 as uuidv7 } from 'uuid';
uuidv7(); // RESULT
```

### ~~uuid.v8()~~

**_"Intentionally left blank"_**

<!-- prettier-ignore -->
> [!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
Expand Down
111 changes: 111 additions & 0 deletions test/unit/test_constants.js
Original file line number Diff line number Diff line change
@@ -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 });
}
}
71 changes: 7 additions & 64 deletions test/unit/validate.test.js
Original file line number Diff line number Diff line change
@@ -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
);
});
});
31 changes: 12 additions & 19 deletions test/unit/version.test.js
Original file line number Diff line number Diff line change
@@ -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`);
}
}
});
});

0 comments on commit 35a5342

Please sign in to comment.