From 1f241d2872e09f3f6b386d005e8851a1ad71adb1 Mon Sep 17 00:00:00 2001
From: Thomas Randolph <trandolph@gitlab.com>
Date: Thu, 4 Jun 2020 09:10:27 -0600
Subject: [PATCH] Add a seedable UUIDv4 generator

Most UUID generators assume that you want fully
random UUIDs. In most cases, this is true.

The `uuid` package allows
a consumer to pass in `random` values (an array
of 16 numbers 0-255), or a generator that outputs
16 random bytes.

This is our hook into being able to provide
"random" values. We just need a way to get
"random" values that are actually random in
most cases, but that we can control if we want
to.

Enter: the Mersenne Twister.
Mersenne Twisters can be seeded with a number
to start. They will derive all of their future twisted
states from that initial seed. So: we still get
"randomness," but we can also seed it to make
the output deterministic.

This `random.js` file outputs a single function
(for now) called `uuids` that will generate a
random UUIDv4 string or - if provided seeds -
will generate the correct resulting UUIDv4
given those seeds.

Consumers can request multiple values
to avoid having to constantly call the function
and/or constantly reconstruct the internal
Twister.
---
 app/assets/javascripts/diffs/utils/uuids.js | 79 ++++++++++++++++++
 config/dependency_decisions.yml             |  6 ++
 package.json                                |  3 +
 spec/frontend/diffs/utils/uuids_spec.js     | 92 +++++++++++++++++++++
 yarn.lock                                   | 15 ++++
 5 files changed, 195 insertions(+)
 create mode 100644 app/assets/javascripts/diffs/utils/uuids.js
 create mode 100644 spec/frontend/diffs/utils/uuids_spec.js

diff --git a/app/assets/javascripts/diffs/utils/uuids.js b/app/assets/javascripts/diffs/utils/uuids.js
new file mode 100644
index 0000000000000..1a529c07ccc35
--- /dev/null
+++ b/app/assets/javascripts/diffs/utils/uuids.js
@@ -0,0 +1,79 @@
+/**
+ * @module uuids
+ */
+
+/**
+ * A string or number representing a start state for a random generator
+ * @typedef {(Number|String)} Seed
+ */
+/**
+ * A UUIDv4 string in the format <code>Hex{8}-Hex{4}-4Hex{3}-[89ab]Hex{3}-Hex{12}</code>
+ * @typedef {String} UUIDv4
+ */
+
+// https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/20
+/* eslint-disable import/prefer-default-export */
+
+import MersenneTwister from 'mersenne-twister';
+import stringHash from 'string-hash';
+import { isString } from 'lodash';
+import { v4 } from 'uuid';
+
+function getSeed(seeds) {
+  return seeds.reduce((seedling, seed, i) => {
+    let thisSeed = 0;
+
+    if (Number.isInteger(seed)) {
+      thisSeed = seed;
+    } else if (isString(seed)) {
+      thisSeed = stringHash(seed);
+    }
+
+    return seedling + (seeds.length - i) * thisSeed;
+  }, 0);
+}
+
+function getPseudoRandomNumberGenerator(...seeds) {
+  let seedNumber;
+
+  if (seeds.length) {
+    seedNumber = getSeed(seeds);
+  } else {
+    seedNumber = Math.floor(Math.random() * 10 ** 15);
+  }
+
+  return new MersenneTwister(seedNumber);
+}
+
+function randomValuesForUuid(prng) {
+  const randomValues = [];
+
+  for (let i = 0; i <= 3; i += 1) {
+    const buffer = new ArrayBuffer(4);
+    const view = new DataView(buffer);
+
+    view.setUint32(0, prng.random_int());
+
+    randomValues.push(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3));
+  }
+
+  return randomValues;
+}
+
+/**
+ * Get an array of UUIDv4s
+ * @param {Object} [options={}]
+ * @param {Seed[]} [options.seeds=[]] - A list of mixed strings or numbers to seed the UUIDv4 generator
+ * @param {Number} [options.count=1] - A total number of UUIDv4s to generate
+ * @returns {UUIDv4[]} An array of UUIDv4s
+ */
+export function uuids({ seeds = [], count = 1 } = {}) {
+  const rng = getPseudoRandomNumberGenerator(...seeds);
+  return (
+    // Create an array the same size as the number of UUIDs requested
+    Array(count)
+      .fill(0)
+      // Replace each slot in the array with a UUID which needs 16 (pseudo)random values to generate
+      .map(() => v4({ random: randomValuesForUuid(rng) }))
+  );
+}
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 84db15d65355b..ff5ccbb3c1bb3 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -626,3 +626,9 @@
     :why: 
     :versions: []
     :when: 2019-11-08 10:03:31.787226000 Z
+- - :whitelist
+  - CC0-1.0
+  - :who: Thomas Randolph
+    :why: This license is public domain
+    :versions: []
+    :when: 2020-06-03 05:04:44.632875345 Z
diff --git a/package.json b/package.json
index 3c1abb083dd27..c1f20c3fc8dd4 100644
--- a/package.json
+++ b/package.json
@@ -102,6 +102,7 @@
     "lodash": "^4.17.15",
     "marked": "^0.3.12",
     "mermaid": "^8.5.1",
+    "mersenne-twister": "1.1.0",
     "mitt": "^1.2.0",
     "monaco-editor": "^0.18.1",
     "monaco-editor-webpack-plugin": "^1.7.0",
@@ -120,6 +121,7 @@
     "sortablejs": "^1.10.2",
     "sql.js": "^0.4.0",
     "stickyfilljs": "^2.1.0",
+    "string-hash": "1.1.3",
     "style-loader": "^1.1.3",
     "svg4everybody": "2.1.9",
     "swagger-ui-dist": "^3.24.3",
@@ -133,6 +135,7 @@
     "tributejs": "4.1.3",
     "unfetch": "^4.1.0",
     "url-loader": "^3.0.0",
+    "uuid": "8.1.0",
     "visibilityjs": "^1.2.4",
     "vue": "^2.6.10",
     "vue-apollo": "^3.0.3",
diff --git a/spec/frontend/diffs/utils/uuids_spec.js b/spec/frontend/diffs/utils/uuids_spec.js
new file mode 100644
index 0000000000000..79d3ebadd4fb9
--- /dev/null
+++ b/spec/frontend/diffs/utils/uuids_spec.js
@@ -0,0 +1,92 @@
+import { uuids } from '~/diffs/utils/uuids';
+
+const HEX = /[a-f0-9]/i;
+const HEX_RE = HEX.source;
+const UUIDV4 = new RegExp(
+  `${HEX_RE}{8}-${HEX_RE}{4}-4${HEX_RE}{3}-[89ab]${HEX_RE}{3}-${HEX_RE}{12}`,
+  'i',
+);
+
+describe('UUIDs Util', () => {
+  describe('uuids', () => {
+    const SEQUENCE_FOR_GITLAB_SEED = [
+      'a1826a44-316c-480e-a93d-8cdfeb36617c',
+      'e049db1f-a4cf-4cba-aa60-6d95e3b547dc',
+      '6e3c737c-13a7-4380-b17d-601f187d7e69',
+      'bee5cc7f-c486-45c0-8ad3-d1ac5402632d',
+      'af248c9f-a3a6-4d4f-a311-fe151ffab25a',
+    ];
+    const SEQUENCE_FOR_12345_SEED = [
+      'edfb51e2-e3e1-4de5-90fd-fd1d21760881',
+      '2f154da4-0a2d-4da9-b45e-0ffed391517e',
+      '91566d65-8836-4222-9875-9e1df4d0bb01',
+      'f6ea6c76-7640-4d71-a736-9d3bec7a1a8e',
+      'bfb85869-5fb9-4c5b-a750-5af727ac5576',
+    ];
+
+    it('returns version 4 UUIDs', () => {
+      expect(uuids()[0]).toMatch(UUIDV4);
+    });
+
+    it('outputs an array of UUIDs', () => {
+      const ids = uuids({ count: 11 });
+
+      expect(ids.length).toEqual(11);
+      expect(ids.every(id => UUIDV4.test(id))).toEqual(true);
+    });
+
+    it.each`
+      seeds                          | uuid
+      ${['some', 'special', 'seed']} | ${'6fa53e51-0f70-4072-9c84-1c1eee1b9934'}
+      ${['magic']}                   | ${'fafae8cd-7083-44f3-b82d-43b30bd27486'}
+      ${['seeded']}                  | ${'e06ed291-46c5-4e42-836b-e7c772d48b49'}
+      ${['GitLab']}                  | ${'a1826a44-316c-480e-a93d-8cdfeb36617c'}
+      ${['JavaScript']}              | ${'12dfb297-1560-4c38-9775-7178ef8472fb'}
+      ${[99, 169834, 2619]}          | ${'3ecc8ad6-5b7c-4c9b-94a8-c7271c2fa083'}
+      ${[12]}                        | ${'2777374b-723b-469b-bd73-e586df964cfd'}
+      ${[9876, 'mixed!', 7654]}      | ${'865212e0-4a16-4934-96f9-103cf36a6931'}
+      ${[123, 1234, 12345, 6]}       | ${'40aa2ee6-0a11-4e67-8f09-72f5eba04244'}
+      ${[0]}                         | ${'8c7f0aac-97c4-4a2f-b716-a675d821ccc0'}
+    `(
+      'should always output the UUID $uuid when the options.seeds argument is $seeds',
+      ({ uuid, seeds }) => {
+        expect(uuids({ seeds })[0]).toEqual(uuid);
+      },
+    );
+
+    describe('unseeded UUID randomness', () => {
+      const nonRandom = Array(6)
+        .fill(0)
+        .map((_, i) => uuids({ seeds: [i] })[0]);
+      const random = uuids({ count: 6 });
+      const moreRandom = uuids({ count: 6 });
+
+      it('is different from a seeded result', () => {
+        random.forEach((id, i) => {
+          expect(id).not.toEqual(nonRandom[i]);
+        });
+      });
+
+      it('is different from other random results', () => {
+        random.forEach((id, i) => {
+          expect(id).not.toEqual(moreRandom[i]);
+        });
+      });
+
+      it('never produces any duplicates', () => {
+        expect(new Set(random).size).toEqual(random.length);
+      });
+    });
+
+    it.each`
+      seed        | sequence
+      ${'GitLab'} | ${SEQUENCE_FOR_GITLAB_SEED}
+      ${12345}    | ${SEQUENCE_FOR_12345_SEED}
+    `(
+      'should output the same sequence of UUIDs for the given seed "$seed"',
+      ({ seed, sequence }) => {
+        expect(uuids({ seeds: [seed], count: 5 })).toEqual(sequence);
+      },
+    );
+  });
+});
diff --git a/yarn.lock b/yarn.lock
index 542bd369c146b..ef03db616ce3f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7774,6 +7774,11 @@ mermaid@^8.5.1:
     moment-mini "^2.22.1"
     scope-css "^1.2.1"
 
+mersenne-twister@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/mersenne-twister/-/mersenne-twister-1.1.0.tgz#f916618ee43d7179efcf641bec4531eb9670978a"
+  integrity sha1-+RZhjuQ9cXnvz2Qb7EUx65Zwl4o=
+
 methods@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
@@ -10629,6 +10634,11 @@ streamroller@^1.0.6:
     fs-extra "^7.0.1"
     lodash "^4.17.14"
 
+string-hash@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b"
+  integrity sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=
+
 string-length@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
@@ -11687,6 +11697,11 @@ uuid@3.3.2, uuid@^3.0.1, uuid@^3.3.2:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
   integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
 
+uuid@8.1.0:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d"
+  integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==
+
 v8-compile-cache@2.0.3, v8-compile-cache@^2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"
-- 
GitLab