Skip to content

Commit

Permalink
feat: add parse/stringify/validate/version/NIL APIs (#479)
Browse files Browse the repository at this point in the history
Add the following new APIs:
- uuid.NIL The nil UUID string (all zeros)
- uuid.parse() Convert UUID string to array of bytes
- uuid.stringify() Convert array of bytes to UUID string
- uuid.validate() Test a string to see if it is a valid UUID
- uuid.version() Detect RFC version of a UUID

This commit also adds more accurate validation when stringifying UUIDs and improves UUID parsing performance (for v3/v5 UUIDs) significantly (Thanks @awwit!).

Co-authored-by: Christoph Tavan <[email protected]>
Co-authored-by: Ignat Prokopovich <[email protected]>
Co-authored-by: Robert Kieffer <[email protected]>
Co-authored-by: Linus Unnebäck <[email protected]>
  • Loading branch information
4 people authored Jul 22, 2020
1 parent cba367a commit 0e6c10b
Show file tree
Hide file tree
Showing 25 changed files with 819 additions and 420 deletions.
346 changes: 186 additions & 160 deletions README.md

Large diffs are not rendered by default.

337 changes: 190 additions & 147 deletions README_js.md

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions bundlewatch.config.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"files": [
{ "path": "./examples/browser-rollup/dist/v1-size.js", "maxSize": "0.8 kB" },
{ "path": "./examples/browser-rollup/dist/v3-size.js", "maxSize": "1.8 kB" },
{ "path": "./examples/browser-rollup/dist/v4-size.js", "maxSize": "0.5 kB" },
{ "path": "./examples/browser-rollup/dist/v5-size.js", "maxSize": "1.2 kB" },
{ "path": "./examples/browser-rollup/dist/v1-size.js", "maxSize": "1.0 kB" },
{ "path": "./examples/browser-rollup/dist/v3-size.js", "maxSize": "2.1 kB" },
{ "path": "./examples/browser-rollup/dist/v4-size.js", "maxSize": "0.7 kB" },
{ "path": "./examples/browser-rollup/dist/v5-size.js", "maxSize": "1.5 kB" },

{ "path": "./examples/browser-webpack/dist/v1-size.js", "maxSize": "1.0 kB" },
{ "path": "./examples/browser-webpack/dist/v3-size.js", "maxSize": "2.0 kB" },
{ "path": "./examples/browser-webpack/dist/v4-size.js", "maxSize": "0.7 kB" },
{ "path": "./examples/browser-webpack/dist/v5-size.js", "maxSize": "1.4 kB" }
{ "path": "./examples/browser-webpack/dist/v1-size.js", "maxSize": "1.3 kB" },
{ "path": "./examples/browser-webpack/dist/v3-size.js", "maxSize": "2.5 kB" },
{ "path": "./examples/browser-webpack/dist/v4-size.js", "maxSize": "1.0 kB" },
{ "path": "./examples/browser-webpack/dist/v5-size.js", "maxSize": "1.9 kB" }
]
}
2 changes: 2 additions & 0 deletions examples/benchmark/benchmark.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<script src="./node_modules/uuid/dist/umd/uuidv3.min.js"></script>
<script src="./node_modules/uuid/dist/umd/uuidv4.min.js"></script>
<script src="./node_modules/uuid/dist/umd/uuidv5.min.js"></script>
<script src="./node_modules/uuid/dist/umd/uuidParse.min.js"></script>
<script src="./node_modules/uuid/dist/umd/uuidStringify.min.js"></script>
<script src="./node_modules/lodash/lodash.js"></script>
<script src="./node_modules/benchmark/benchmark.js"></script>
<script src="./benchmark.js"></script>
130 changes: 90 additions & 40 deletions examples/benchmark/benchmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,96 @@ const uuidv1 = (typeof window !== 'undefined' && window.uuidv1) || require('uuid
const uuidv4 = (typeof window !== 'undefined' && window.uuidv4) || require('uuid').v4;
const uuidv3 = (typeof window !== 'undefined' && window.uuidv3) || require('uuid').v3;
const uuidv5 = (typeof window !== 'undefined' && window.uuidv5) || require('uuid').v5;
const uuidParse = (typeof window !== 'undefined' && window.uuidParse) || require('uuid').parse;
const uuidStringify =
(typeof window !== 'undefined' && window.uuidStringify) || require('uuid').stringify;

console.log('Starting. Tests take ~1 minute to run ...');

const array = new Array(16);

const suite = new Benchmark.Suite({
onError(event) {
console.error(event.target.error);
},
});

suite
.add('uuidv1()', function () {
uuidv1();
})
.add('uuidv1() fill existing array', function () {
try {
uuidv1(null, array, 0);
} catch (err) {
// The spec (https://tools.ietf.org/html/rfc4122#section-4.2.1.2) defines that only 10M/s v1
// UUIDs can be generated on a single node. This library throws an error if we hit that limit
// (which can happen on modern hardware and modern Node.js versions).
}
})
.add('uuidv4()', function () {
uuidv4();
})
.add('uuidv4() fill existing array', function () {
uuidv4(null, array, 0);
})
.add('uuidv3()', function () {
uuidv3('hello.example.com', uuidv3.DNS);
})
.add('uuidv5()', function () {
uuidv5('hello.example.com', uuidv5.DNS);
})
.on('cycle', function (event) {
console.log(event.target.toString());
})
.on('complete', function () {
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
.run();
function testParseAndStringify() {
const suite = new Benchmark.Suite({
onError(event) {
console.error(event.target.error);
},
});

const BYTES = [
0x0f,
0x5a,
0xbc,
0xd1,
0xc1,
0x94,
0x47,
0xf3,
0x90,
0x5b,
0x2d,
0xf7,
0x26,
0x3a,
0x08,
0x4b,
];

suite
.add('uuidStringify()', function () {
uuidStringify(BYTES);
})
.add('uuidParse()', function () {
uuidParse('0f5abcd1-c194-47f3-905b-2df7263a084b');
})
.on('cycle', function (event) {
console.log(event.target.toString());
})
.on('complete', function () {
console.log('---\n');
})
.run();
}

function testGeneration() {
const array = new Array(16);

const suite = new Benchmark.Suite({
onError(event) {
console.error(event.target.error);
},
});

suite
.add('uuidv1()', function () {
uuidv1();
})
.add('uuidv1() fill existing array', function () {
try {
uuidv1(null, array, 0);
} catch (err) {
// The spec (https://tools.ietf.org/html/rfc4122#section-4.2.1.2) defines that only 10M/s v1
// UUIDs can be generated on a single node. This library throws an error if we hit that limit
// (which can happen on modern hardware and modern Node.js versions).
}
})
.add('uuidv4()', function () {
uuidv4();
})
.add('uuidv4() fill existing array', function () {
uuidv4(null, array, 0);
})
.add('uuidv3()', function () {
uuidv3('hello.example.com', uuidv3.DNS);
})
.add('uuidv5()', function () {
uuidv5('hello.example.com', uuidv5.DNS);
})
.on('cycle', function (event) {
console.log(event.target.toString());
})
.on('complete', function () {
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
.run();
}

testParseAndStringify();
testGeneration();
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"lint-staged": "10.2.11",
"npm-run-all": "4.1.5",
"prettier": "2.0.5",
"random-seed": "0.3.0",
"rollup": "2.18.0",
"rollup-plugin-terser": "6.1.0",
"runmd": "1.3.2",
Expand Down
7 changes: 7 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,11 @@ export default [
chunk('v3', 'uuidv3'),
chunk('v4', 'uuidv4'),
chunk('v5', 'uuidv5'),

chunk('nil', 'uuidNIL'),

chunk('version', 'uuidVersion'),
chunk('validate', 'uuidValidate'),
chunk('parse', 'uuidParse'),
chunk('stringify', 'uuidStringify'),
];
40 changes: 0 additions & 40 deletions src/bytesToUuid.js

This file was deleted.

5 changes: 5 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ export { default as v1 } from './v1.js';
export { default as v3 } from './v3.js';
export { default as v4 } from './v4.js';
export { default as v5 } from './v5.js';
export { default as NIL } from './nil.js';
export { default as version } from './version.js';
export { default as validate } from './validate.js';
export { default as stringify } from './stringify.js';
export { default as parse } from './parse.js';
1 change: 1 addition & 0 deletions src/nil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default '00000000-0000-0000-0000-000000000000';
41 changes: 41 additions & 0 deletions src/parse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import validate from './validate.js';

function parse(uuid) {
if (!validate(uuid)) {
throw TypeError('Invalid UUID');
}

let v;
const arr = new Uint8Array(16);

// Parse ########-....-....-....-............
arr[0] = (v = parseInt(uuid.slice(0, 8), 16)) >>> 24;
arr[1] = (v >>> 16) & 0xff;
arr[2] = (v >>> 8) & 0xff;
arr[3] = v & 0xff;

// Parse ........-####-....-....-............
arr[4] = (v = parseInt(uuid.slice(9, 13), 16)) >>> 8;
arr[5] = v & 0xff;

// Parse ........-....-####-....-............
arr[6] = (v = parseInt(uuid.slice(14, 18), 16)) >>> 8;
arr[7] = v & 0xff;

// Parse ........-....-....-####-............
arr[8] = (v = parseInt(uuid.slice(19, 23), 16)) >>> 8;
arr[9] = v & 0xff;

// Parse ........-....-....-....-############
// (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes)
arr[10] = ((v = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000) & 0xff;
arr[11] = (v / 0x100000000) & 0xff;
arr[12] = (v >>> 24) & 0xff;
arr[13] = (v >>> 16) & 0xff;
arr[14] = (v >>> 8) & 0xff;
arr[15] = v & 0xff;

return arr;
}

export default parse;
1 change: 1 addition & 0 deletions src/regex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
3 changes: 3 additions & 0 deletions src/sha1-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ function sha1(bytes) {
for (let i = 0; i < msg.length; ++i) {
bytes.push(msg.charCodeAt(i));
}
} else if (!Array.isArray(bytes)) {
// Convert Array-like to Array
bytes = Array.prototype.slice.call(bytes);
}

bytes.push(0x80);
Expand Down
51 changes: 51 additions & 0 deletions src/stringify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import validate from './validate.js';

/**
* Convert array of 16 byte values to UUID string format of the form:
* XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
*/
const byteToHex = [];

for (let i = 0; i < 256; ++i) {
byteToHex.push((i + 0x100).toString(16).substr(1));
}

function stringify(arr, offset = 0) {
// Note: Be careful editing this code! It's been tuned for performance
// and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434
const uuid = (
byteToHex[arr[offset + 0]] +
byteToHex[arr[offset + 1]] +
byteToHex[arr[offset + 2]] +
byteToHex[arr[offset + 3]] +
'-' +
byteToHex[arr[offset + 4]] +
byteToHex[arr[offset + 5]] +
'-' +
byteToHex[arr[offset + 6]] +
byteToHex[arr[offset + 7]] +
'-' +
byteToHex[arr[offset + 8]] +
byteToHex[arr[offset + 9]] +
'-' +
byteToHex[arr[offset + 10]] +
byteToHex[arr[offset + 11]] +
byteToHex[arr[offset + 12]] +
byteToHex[arr[offset + 13]] +
byteToHex[arr[offset + 14]] +
byteToHex[arr[offset + 15]]
).toLowerCase();

// Consistency check for valid UUID. If this throws, it's likely due to one
// of the following:
// - One or more input array values don't map to a hex octet (leading to
// "undefined" in the uuid)
// - Invalid input values for the RFC `version` or `variant` fields
if (!validate(uuid)) {
throw TypeError('Stringified UUID is invalid');
}

return uuid;
}

export default stringify;
4 changes: 2 additions & 2 deletions src/v1.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import rng from './rng.js';
import bytesToUuid from './bytesToUuid.js';
import stringify from './stringify.js';

// **`v1()` - Generate time-based UUID**
//
Expand Down Expand Up @@ -109,7 +109,7 @@ function v1(options, buf, offset) {
b[i + n] = node[n];
}

return buf || bytesToUuid(b);
return buf || stringify(b);
}

export default v1;
Loading

0 comments on commit 0e6c10b

Please sign in to comment.