diff --git a/bundlewatch.config.json b/bundlewatch.config.json
index 4a3fb794..f3d7de16 100644
--- a/bundlewatch.config.json
+++ b/bundlewatch.config.json
@@ -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": "0.9 kB" },
+ { "path": "./examples/browser-rollup/dist/v3-size.js", "maxSize": "2.1 kB" },
+ { "path": "./examples/browser-rollup/dist/v4-size.js", "maxSize": "0.6 kB" },
+ { "path": "./examples/browser-rollup/dist/v5-size.js", "maxSize": "1.5 kB" },
{ "path": "./examples/browser-webpack/dist/v1-size.js", "maxSize": "1.3 kB" },
- { "path": "./examples/browser-webpack/dist/v3-size.js", "maxSize": "2.2 kB" },
- { "path": "./examples/browser-webpack/dist/v4-size.js", "maxSize": "0.9 kB" },
- { "path": "./examples/browser-webpack/dist/v5-size.js", "maxSize": "1.6 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" }
]
}
diff --git a/examples/benchmark/benchmark.html b/examples/benchmark/benchmark.html
index 54be63f8..2f3bf78f 100644
--- a/examples/benchmark/benchmark.html
+++ b/examples/benchmark/benchmark.html
@@ -5,6 +5,8 @@
+
+
diff --git a/examples/benchmark/benchmark.js b/examples/benchmark/benchmark.js
index dc3ff19f..cc36adcb 100644
--- a/examples/benchmark/benchmark.js
+++ b/examples/benchmark/benchmark.js
@@ -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();
diff --git a/package-lock.json b/package-lock.json
index 0debcb87..0896f995 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12437,6 +12437,15 @@
"integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=",
"dev": true
},
+ "random-seed": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/random-seed/-/random-seed-0.3.0.tgz",
+ "integrity": "sha1-2UXy4fOPSejViRNDG4v2u5N1Vs0=",
+ "dev": true,
+ "requires": {
+ "json-stringify-safe": "^5.0.1"
+ }
+ },
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
diff --git a/package.json b/package.json
index 6dcccba7..cb150307 100644
--- a/package.json
+++ b/package.json
@@ -69,6 +69,7 @@
"lint-staged": "10.1.3",
"npm-run-all": "4.1.5",
"prettier": "2.0.4",
+ "random-seed": "0.3.0",
"rollup": "2.6.1",
"rollup-plugin-terser": "5.3.0",
"runmd": "1.3.2",
diff --git a/rollup.config.js b/rollup.config.js
index 34544dfd..920d599f 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -20,4 +20,9 @@ export default [
chunk('v3', 'uuidv3'),
chunk('v4', 'uuidv4'),
chunk('v5', 'uuidv5'),
+
+ chunk('version', 'uuidVersion'),
+ chunk('validate', 'uuidValidate'),
+ chunk('parse', 'uuidParse'),
+ chunk('stringify', 'uuidStringify'),
];
diff --git a/src/bytesToUuid.js b/src/bytesToUuid.js
deleted file mode 100644
index 0f57c69f..00000000
--- a/src/bytesToUuid.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * 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 bytesToUuid(buf, offset_) {
- const offset = 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
- return (
- byteToHex[buf[offset + 0]] +
- byteToHex[buf[offset + 1]] +
- byteToHex[buf[offset + 2]] +
- byteToHex[buf[offset + 3]] +
- '-' +
- byteToHex[buf[offset + 4]] +
- byteToHex[buf[offset + 5]] +
- '-' +
- byteToHex[buf[offset + 6]] +
- byteToHex[buf[offset + 7]] +
- '-' +
- byteToHex[buf[offset + 8]] +
- byteToHex[buf[offset + 9]] +
- '-' +
- byteToHex[buf[offset + 10]] +
- byteToHex[buf[offset + 11]] +
- byteToHex[buf[offset + 12]] +
- byteToHex[buf[offset + 13]] +
- byteToHex[buf[offset + 14]] +
- byteToHex[buf[offset + 15]]
- ).toLowerCase();
-}
-
-export default bytesToUuid;
diff --git a/src/index.js b/src/index.js
index 7702f92a..9586a544 100644
--- a/src/index.js
+++ b/src/index.js
@@ -5,3 +5,5 @@ export { default as v5 } from './v5.js';
export { default as REGEX } from './regex.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';
diff --git a/src/parse.js b/src/parse.js
new file mode 100644
index 00000000..85edd846
--- /dev/null
+++ b/src/parse.js
@@ -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;
diff --git a/src/sha1-browser.js b/src/sha1-browser.js
index 2bfa2cb0..377dc24c 100644
--- a/src/sha1-browser.js
+++ b/src/sha1-browser.js
@@ -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);
diff --git a/src/stringify.js b/src/stringify.js
new file mode 100644
index 00000000..17e2b0df
--- /dev/null
+++ b/src/stringify.js
@@ -0,0 +1,52 @@
+/**
+ * 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();
+
+ // Sanity check for valid UUID. This works because if any of
+ // the input array values don't map to a defined hex octet, the string length
+ // will get blown out (e.g. "74af23d8-85undefined2c44-...")
+ //
+ // This is a somewhat crude check, but avoids having to check each value
+ // individually.
+ if (uuid.length !== 36) {
+ throw new TypeError(
+ 'Invalid result UUID. Please ensure input is array-like, and contains 16 integer values 0-255',
+ );
+ }
+
+ return uuid;
+}
+
+export default stringify;
diff --git a/src/v1.js b/src/v1.js
index dbf4f5ca..0643675e 100644
--- a/src/v1.js
+++ b/src/v1.js
@@ -1,5 +1,5 @@
import rng from './rng.js';
-import bytesToUuid from './bytesToUuid.js';
+import stringify from './stringify.js';
// **`v1()` - Generate time-based UUID**
//
@@ -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;
diff --git a/src/v35.js b/src/v35.js
index dc7775fe..e8706ff0 100644
--- a/src/v35.js
+++ b/src/v35.js
@@ -1,30 +1,5 @@
-import bytesToUuid from './bytesToUuid.js';
-import validate from './validate.js';
-
-// Int32 to 4 bytes https://stackoverflow.com/a/12965194/3684944
-function numberToBytes(num, bytes, offset) {
- for (let i = 0; i < 4; ++i) {
- const byte = num & 0xff;
- // Fill the 4 bytes right-to-left.
- bytes[offset + 3 - i] = byte;
- num = (num - byte) / 256;
- }
-}
-
-function uuidToBytes(uuid) {
- if (!validate(uuid)) {
- return [];
- }
-
- const bytes = new Array(16);
-
- numberToBytes(parseInt(uuid.slice(0, 8), 16), bytes, 0);
- numberToBytes(parseInt(uuid.slice(9, 13) + uuid.slice(14, 18), 16), bytes, 4);
- numberToBytes(parseInt(uuid.slice(19, 23) + uuid.slice(24, 28), 16), bytes, 8);
- numberToBytes(parseInt(uuid.slice(28), 16), bytes, 12);
-
- return bytes;
-}
+import stringify from './stringify.js';
+import parse from './parse.js';
function stringToBytes(str) {
str = unescape(encodeURIComponent(str)); // UTF8 escape
@@ -48,19 +23,21 @@ export default function (name, version, hashfunc) {
}
if (typeof namespace === 'string') {
- namespace = uuidToBytes(namespace);
+ namespace = parse(namespace);
}
- if (!Array.isArray(value)) {
- throw TypeError('value must be an array of bytes');
+ if (namespace.length !== 16) {
+ throw TypeError('Namespace must be array-like (16 iterable integer values, 0-255)');
}
- if (!Array.isArray(namespace) || namespace.length !== 16) {
- throw TypeError('namespace must be uuid string or an Array of 16 byte values');
- }
+ // Compute hash of namespace and value, Per 4.3
+ // Future: Use spread syntax when supported on all platforms, e.g. `bytes =
+ // hashfunc([...namespace, ... value])`
+ let bytes = new Uint8Array(16 + value.length);
+ bytes.set(namespace);
+ bytes.set(value, namespace.length);
+ bytes = hashfunc(bytes);
- // Per 4.3
- const bytes = hashfunc(namespace.concat(value));
bytes[6] = (bytes[6] & 0x0f) | version;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
@@ -74,7 +51,7 @@ export default function (name, version, hashfunc) {
return buf;
}
- return bytesToUuid(bytes);
+ return stringify(bytes);
}
// Function#name is not settable on some platforms (#270)
diff --git a/src/v4.js b/src/v4.js
index 16765828..520613a4 100644
--- a/src/v4.js
+++ b/src/v4.js
@@ -1,5 +1,5 @@
import rng from './rng.js';
-import bytesToUuid from './bytesToUuid.js';
+import stringify from './stringify.js';
function v4(options, buf, offset) {
options = options || {};
@@ -21,7 +21,7 @@ function v4(options, buf, offset) {
return buf;
}
- return bytesToUuid(rnds);
+ return stringify(rnds);
}
export default v4;
diff --git a/test/unit/parse.test.js b/test/unit/parse.test.js
new file mode 100644
index 00000000..7137d953
--- /dev/null
+++ b/test/unit/parse.test.js
@@ -0,0 +1,68 @@
+import assert from 'assert';
+import uuidv4 from '../../src/v4.js';
+import parse from '../../src/parse.js';
+import stringify from '../../src/stringify.js';
+import gen from 'random-seed';
+
+// Use deterministic PRNG for reproducable tests
+const rand = gen.create('He who wonders discovers that this in itself is wonder.');
+function rng(bytes = []) {
+ for (let i = 0; i < 16; i++) {
+ bytes[i] = rand(256);
+ }
+ return bytes;
+}
+
+describe('parse', () => {
+ test('String -> bytes parsing', () => {
+ assert.deepStrictEqual(
+ parse('0f5abcd1-c194-47f3-905b-2df7263a084b'),
+ Uint8Array.from([
+ 0x0f,
+ 0x5a,
+ 0xbc,
+ 0xd1,
+ 0xc1,
+ 0x94,
+ 0x47,
+ 0xf3,
+ 0x90,
+ 0x5b,
+ 0x2d,
+ 0xf7,
+ 0x26,
+ 0x3a,
+ 0x08,
+ 0x4b,
+ ]),
+ );
+ });
+
+ test('String -> bytes -> string symmetry for assorted uuids', () => {
+ for (let i = 0; i < 1000; i++) {
+ const uuid = uuidv4({ rng });
+ assert.equal(stringify(parse(uuid)), uuid);
+ }
+ });
+
+ test('Case neutrality', () => {
+ // Verify upper/lower case neutrality
+ assert.deepStrictEqual(
+ parse('0f5abcd1-c194-47f3-905b-2df7263a084b'),
+ parse('0f5abcd1-c194-47f3-905b-2df7263a084b'.toUpperCase()),
+ );
+ });
+
+ test('Null UUID case', () => {
+ assert.deepStrictEqual(
+ parse('00000000-0000-0000-0000-000000000000'),
+ Uint8Array.from(new Array(16).fill(0)),
+ );
+ });
+
+ test('UUID validation', () => {
+ assert.throws(() => parse());
+ assert.throws(() => parse('invalid uuid'));
+ assert.throws(() => parse('zyxwvuts-rqpo-nmlk-jihg-fedcba000000'));
+ });
+});
diff --git a/test/unit/stringify.test.js b/test/unit/stringify.test.js
new file mode 100644
index 00000000..94de77d1
--- /dev/null
+++ b/test/unit/stringify.test.js
@@ -0,0 +1,55 @@
+import assert from 'assert';
+import stringify from '../../src/stringify.js';
+
+const BYTES = [
+ 0x0f,
+ 0x5a,
+ 0xbc,
+ 0xd1,
+ 0xc1,
+ 0x94,
+ 0x47,
+ 0xf3,
+ 0x90,
+ 0x5b,
+ 0x2d,
+ 0xf7,
+ 0x26,
+ 0x3a,
+ 0x08,
+ 0x4b,
+];
+
+describe('stringify', () => {
+ test('Stringify Array', () => {
+ assert.equal(stringify(BYTES), '0f5abcd1-c194-47f3-905b-2df7263a084b');
+ });
+
+ test('Stringify TypedArray', () => {
+ assert.equal(stringify(Uint8Array.from(BYTES)), '0f5abcd1-c194-47f3-905b-2df7263a084b');
+ assert.equal(stringify(Int32Array.from(BYTES)), '0f5abcd1-c194-47f3-905b-2df7263a084b');
+ });
+
+ test('Stringify w/ offset', () => {
+ assert.equal(stringify([0, 0, 0, ...BYTES], 3), '0f5abcd1-c194-47f3-905b-2df7263a084b');
+ });
+
+ test('Throws on not enough values', () => {
+ const bytes = [...BYTES];
+ bytes.length = 15;
+ assert.throws(() => stringify(bytes));
+ });
+
+ test('Throws on undefined value', () => {
+ const bytes = [...BYTES];
+ delete bytes[3];
+ bytes.length = 15;
+ assert.throws(() => stringify(bytes));
+ });
+
+ test('Throws on invalid value', () => {
+ const bytes = [...BYTES];
+ bytes[3] = 256;
+ assert.throws(() => stringify(bytes));
+ });
+});
diff --git a/wdio.conf.js b/wdio.conf.js
index 4be214bf..a7cea646 100644
--- a/wdio.conf.js
+++ b/wdio.conf.js
@@ -12,6 +12,7 @@ const commonCapabilities = {
name: 'browser test',
'browserstack.local': true,
'browserstack.debug': false,
+ 'browserstack.console': 'errors',
resolution: '1024x768',
};