From f2029c263537a0d6614ab5bc96f2f744656603f7 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 22 Jun 2022 15:36:41 -0600 Subject: [PATCH 1/4] Add more organized implementation as a "V4" module --- app/assets/javascripts/lib/utils/uuids/v4.js | 86 ++++++++++++++++++ spec/frontend/lib/utils/uuids/v4_spec.js | 92 ++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 app/assets/javascripts/lib/utils/uuids/v4.js create mode 100644 spec/frontend/lib/utils/uuids/v4_spec.js diff --git a/app/assets/javascripts/lib/utils/uuids/v4.js b/app/assets/javascripts/lib/utils/uuids/v4.js new file mode 100644 index 00000000000000..1334199406d61b --- /dev/null +++ b/app/assets/javascripts/lib/utils/uuids/v4.js @@ -0,0 +1,86 @@ +/** + * @module uuidsV4 + */ + +/** + * A string or number representing a start state for a random generator + * @typedef {(Number|String)} Seed + */ +/** + * A UUIDv4 string in the format Hex{8}-Hex{4}-4Hex{3}-[89ab]Hex{3}-Hex{12} + * @typedef {String} UUIDv4 + */ + +import { MersenneTwister } from 'fast-mersenne-twister'; +import { isString } from 'lodash'; +import stringHash from 'string-hash'; +import { v4 } from 'uuid'; + +function arrayOf(length) { + return { + using(uuidGenerator) { + return Array(length).fill(0).map(uuidGenerator); + }, + }; +} + +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) { + const usefulSeeds = seeds.filter((seed) => seed || seed === 0); + let seedNumber; + + if (usefulSeeds.length) { + seedNumber = getSeed(usefulSeeds); + } else { + throw new Error( + 'You must provide String and/or Number seed values' /* eslint-disable-line @gitlab/require-i18n-strings */, + ); + } + + return new MersenneTwister(seedNumber); +} + +function valuesForUuid(prng) { + const values = []; + + for (let i = 0; i <= 3; i += 1) { + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); + + view.setUint32(0, prng.randomNumber()); + + values.push(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3)); + } + + return values; +} + +/** + * Get an array of UUIDv4s, seeding the PRNG first + * @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|UUIDv4[]} A single UUIDv4, or an array of >=2 UUIDv4s + */ +export function seeded({ seeds = [], count = 1 } = {}) { + const prng = getPseudoRandomNumberGenerator(...seeds); + + return count === 1 + ? v4({ random: valuesForUuid(prng) }) + : arrayOf(count).using(() => v4({ random: valuesForUuid(prng) })); +} + +export { v4 as random }; diff --git a/spec/frontend/lib/utils/uuids/v4_spec.js b/spec/frontend/lib/utils/uuids/v4_spec.js new file mode 100644 index 00000000000000..fb68edf724569a --- /dev/null +++ b/spec/frontend/lib/utils/uuids/v4_spec.js @@ -0,0 +1,92 @@ +import { random, seeded } from '~/lib/utils/uuids/v4'; + +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', +); + +function arrayOf(length) { + return { + using(uuidGenerator) { + return Array(length).fill(0).map(uuidGenerator); + }, + }; +} + +describe('UUIDs Util', () => { + describe('random', () => { + it('returns a version 4 UUID', () => { + expect(random()).toMatch(UUIDV4); + }); + + describe('unseeded UUID randomness', () => { + const nonRandom = arrayOf(6).using((_, i) => seeded({ seeds: [i] })); + const randomSet = arrayOf(6).using(() => random()); + const additionalRandomSet = arrayOf(6).using(() => random()); + + it('is different from a seeded result', () => { + randomSet.forEach((id, i) => { + expect(id).not.toEqual(nonRandom[i]); + }); + }); + + it('is different from other random results', () => { + randomSet.forEach((id, i) => { + expect(id).not.toEqual(additionalRandomSet[i]); + }); + }); + + it('never produces any duplicates', () => { + expect(new Set(randomSet).size).toEqual(randomSet.length); + }); + }); + }); + describe('seeded', () => { + 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.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(seeded({ seeds })).toEqual(uuid); + }, + ); + + 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(seeded({ seeds: [seed], count: 5 })).toEqual(sequence); + }, + ); + }); +}); -- GitLab From b26e8929bbded8f116dcfc28fb3c84fd15d28c21 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 22 Jun 2022 15:39:32 -0600 Subject: [PATCH 2/4] Use new "V4" module and single return value signature --- app/assets/javascripts/actioncable_link.js | 4 ++-- app/assets/javascripts/diffs/utils/diff_file.js | 6 +++--- app/assets/javascripts/editor/source_editor.js | 4 ++-- app/assets/javascripts/lib/utils/intersection_observer.js | 4 ++-- app/assets/javascripts/lib/utils/recurrence.js | 4 ++-- spec/frontend/actioncable_link_spec.js | 4 ++-- spec/frontend/work_items/pages/create_work_item_spec.js | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/actioncable_link.js b/app/assets/javascripts/actioncable_link.js index cf53d9e21b4a75..c6d1662590738d 100644 --- a/app/assets/javascripts/actioncable_link.js +++ b/app/assets/javascripts/actioncable_link.js @@ -1,7 +1,7 @@ import { ApolloLink, Observable } from '@apollo/client/core'; import { print } from 'graphql'; import cable from '~/actioncable_consumer'; -import { uuids } from '~/lib/utils/uuids'; +import { random } from '~/lib/utils/uuids/v4'; export default class ActionCableLink extends ApolloLink { // eslint-disable-next-line class-methods-use-this @@ -13,7 +13,7 @@ export default class ActionCableLink extends ApolloLink { query: operation.query ? print(operation.query) : null, variables: operation.variables, operationName: operation.operationName, - nonce: uuids()[0], + nonce: random(), }, { received(data) { diff --git a/app/assets/javascripts/diffs/utils/diff_file.js b/app/assets/javascripts/diffs/utils/diff_file.js index bcd9fa012786be..2cff906d23d949 100644 --- a/app/assets/javascripts/diffs/utils/diff_file.js +++ b/app/assets/javascripts/diffs/utils/diff_file.js @@ -1,7 +1,7 @@ import { diffViewerModes as viewerModes } from '~/ide/constants'; import { changeInPercent, numberToHumanSize } from '~/lib/utils/number_utils'; import { truncateSha } from '~/lib/utils/text_utility'; -import { uuids } from '~/lib/utils/uuids'; +import { seeded } from '~/lib/utils/uuids/v4'; import { DIFF_FILE_SYMLINK_MODE, @@ -43,9 +43,9 @@ function identifier(file) { endpoint: file.load_collapsed_diff_url, }); - return uuids({ + return seeded({ seeds: [userOrGroup, project, id, file.file_identifier_hash, file.blob?.id], - })[0]; + }); } export const isNotDiffable = (file) => file?.viewer?.name === viewerModes.not_diffable; diff --git a/app/assets/javascripts/editor/source_editor.js b/app/assets/javascripts/editor/source_editor.js index d585dc009e6a73..67c950b66c3109 100644 --- a/app/assets/javascripts/editor/source_editor.js +++ b/app/assets/javascripts/editor/source_editor.js @@ -4,7 +4,7 @@ import { defaultEditorOptions } from '~/ide/lib/editor_options'; import languages from '~/ide/lib/languages'; import { registerLanguages } from '~/ide/utils'; import { joinPaths } from '~/lib/utils/url_utility'; -import { uuids } from '~/lib/utils/uuids'; +import { random } from '~/lib/utils/uuids/v4'; import { SOURCE_EDITOR_INSTANCE_ERROR_NO_EL, URI_PREFIX, @@ -113,7 +113,7 @@ export default class SourceEditor { blobPath = '', blobContent = '', blobOriginalContent = '', - blobGlobalId = uuids()[0], + blobGlobalId = random(), isDiff = false, ...instanceOptions } = {}) { diff --git a/app/assets/javascripts/lib/utils/intersection_observer.js b/app/assets/javascripts/lib/utils/intersection_observer.js index 0959df9a186f0c..1c7df1761f55ff 100644 --- a/app/assets/javascripts/lib/utils/intersection_observer.js +++ b/app/assets/javascripts/lib/utils/intersection_observer.js @@ -1,9 +1,9 @@ import { memoize } from 'lodash'; -import { uuids } from './uuids'; +import { random } from './uuids/v4'; export const create = memoize((options = {}) => { - const id = uuids()[0]; + const id = random(); return { id, diff --git a/app/assets/javascripts/lib/utils/recurrence.js b/app/assets/javascripts/lib/utils/recurrence.js index 8fd26f3e393abc..6d8d56b222b4c4 100644 --- a/app/assets/javascripts/lib/utils/recurrence.js +++ b/app/assets/javascripts/lib/utils/recurrence.js @@ -1,4 +1,4 @@ -import { uuids } from './uuids'; +import { random } from './uuids/v4'; /** * @module recurrence @@ -11,7 +11,7 @@ const instances = {}; * @returns {module:recurrence.RecurInstance} The newly created {@link module:recurrence~RecurInstance|RecurInstance} */ export function create() { - const id = uuids()[0]; + const id = random(); let handlers = {}; let count = 0; diff --git a/spec/frontend/actioncable_link_spec.js b/spec/frontend/actioncable_link_spec.js index b15b93cb11d910..d08002e45ed76d 100644 --- a/spec/frontend/actioncable_link_spec.js +++ b/spec/frontend/actioncable_link_spec.js @@ -4,8 +4,8 @@ import cable from '~/actioncable_consumer'; import ActionCableLink from '~/actioncable_link'; // Mock uuids module for determinism -jest.mock('~/lib/utils/uuids', () => ({ - uuids: () => ['testuuid'], +jest.mock('~/lib/utils/uuids/v4', () => ({ + random: () => 'testuuid', })); const TEST_OPERATION = { diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js index e89477ed599354..6237a322df535f 100644 --- a/spec/frontend/work_items/pages/create_work_item_spec.js +++ b/spec/frontend/work_items/pages/create_work_item_spec.js @@ -15,7 +15,7 @@ import { createWorkItemFromTaskMutationResponse, } from '../mock_data'; -jest.mock('~/lib/utils/uuids', () => ({ uuids: () => ['testuuid'] })); +jest.mock('~/lib/utils/uuids/v4', () => ({ random: () => 'testuuid' })); Vue.use(VueApollo); -- GitLab From 0c316a41fef1e964d471749cc5d3fd3aec6e65a3 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 22 Jun 2022 15:39:46 -0600 Subject: [PATCH 3/4] Remove old general "uuids" module --- app/assets/javascripts/lib/utils/uuids.js | 76 ------------------- spec/frontend/lib/utils/uuids_spec.js | 92 ----------------------- 2 files changed, 168 deletions(-) delete mode 100644 app/assets/javascripts/lib/utils/uuids.js delete mode 100644 spec/frontend/lib/utils/uuids_spec.js diff --git a/app/assets/javascripts/lib/utils/uuids.js b/app/assets/javascripts/lib/utils/uuids.js deleted file mode 100644 index 98fe4bf96646c6..00000000000000 --- a/app/assets/javascripts/lib/utils/uuids.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * @module uuids - */ - -/** - * A string or number representing a start state for a random generator - * @typedef {(Number|String)} Seed - */ -/** - * A UUIDv4 string in the format Hex{8}-Hex{4}-4Hex{3}-[89ab]Hex{3}-Hex{12} - * @typedef {String} UUIDv4 - */ - -import { MersenneTwister } from 'fast-mersenne-twister'; -import { isString } from 'lodash'; -import stringHash from 'string-hash'; -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.randomNumber()); - - 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/spec/frontend/lib/utils/uuids_spec.js b/spec/frontend/lib/utils/uuids_spec.js deleted file mode 100644 index a7770d37566f46..00000000000000 --- a/spec/frontend/lib/utils/uuids_spec.js +++ /dev/null @@ -1,92 +0,0 @@ -import { uuids } from '~/lib/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); - }, - ); - }); -}); -- GitLab From 4e9476a9c7ea2e77d65e798b01ba78b5ad357b3f Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 22 Jun 2022 15:53:59 -0600 Subject: [PATCH 4/4] Update Google Tag Manager to use UUID encapsulation module --- app/assets/javascripts/google_tag_manager/index.js | 2 +- spec/frontend/google_tag_manager/index_spec.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/google_tag_manager/index.js b/app/assets/javascripts/google_tag_manager/index.js index 2969121bf06b82..a9cfca29c7b58e 100644 --- a/app/assets/javascripts/google_tag_manager/index.js +++ b/app/assets/javascripts/google_tag_manager/index.js @@ -1,4 +1,4 @@ -import { v4 as uuidv4 } from 'uuid'; +import { random as uuidv4 } from '~/lib/utils/uuids/v4'; import { logError } from '~/lib/logger'; const SKU_PREMIUM = '2c92a00d76f0d5060176f2fb0a5029ff'; diff --git a/spec/frontend/google_tag_manager/index_spec.js b/spec/frontend/google_tag_manager/index_spec.js index 50811f43fc3556..f9b7a4eb99f722 100644 --- a/spec/frontend/google_tag_manager/index_spec.js +++ b/spec/frontend/google_tag_manager/index_spec.js @@ -1,5 +1,4 @@ import { merge } from 'lodash'; -import { v4 as uuidv4 } from 'uuid'; import { trackCombinedGroupProjectForm, trackFreeTrialAccountSubmissions, @@ -19,7 +18,9 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { logError } from '~/lib/logger'; jest.mock('~/lib/logger'); -jest.mock('uuid'); +jest.mock('~/lib/utils/uuids/v4', () => ({ + random: () => '123', +})); describe('~/google_tag_manager/index', () => { let spy; @@ -372,7 +373,6 @@ describe('~/google_tag_manager/index', () => { const { selectedPlan, revenue, name, id } = planObject; expect(spy).not.toHaveBeenCalled(); - uuidv4.mockImplementationOnce(() => '123'); const transactionDetails = { paymentOption: 'visa', -- GitLab