diff --git a/web/utils/text-utils.js b/web/utils/text-utils.js --- a/web/utils/text-utils.js +++ b/web/utils/text-utils.js @@ -1,4 +1,6 @@ // @flow +import crypto from 'crypto'; +import invariant from 'invariant'; let canvas; @@ -16,4 +18,44 @@ return Math.max(...widths); } -export { calculateMaxTextWidth }; +const numberOfPossibleByteValues = 256; + +function generateRandomString(length: number, availableSigns: string): string { + invariant(length >= 0, 'length must be non-negative'); + invariant( + availableSigns !== '' || length === 0, + "cannot create a random string of non-zero length from availableSigns = ''", + ); + invariant( + numberOfPossibleByteValues >= availableSigns.length, + `The number of available signs must not exceed ${numberOfPossibleByteValues}`, + ); + + const validByteUpperBound = + availableSigns.length * + Math.floor(numberOfPossibleByteValues / availableSigns.length); + // Generating more bytes than the required length, + // proportionally to how many values will be omitted + // due to uniformness requirement, + // to lower the chances of having to draw again + const drawBytes = + length * + (1 + + 2 * (1 - Math.floor(validByteUpperBound / numberOfPossibleByteValues))); + + let str = ''; + + while (str.length < length) { + const rand = crypto.randomBytes(drawBytes); + + for (let i = 0; str.length < length && i < drawBytes; i++) { + if (rand[i] < validByteUpperBound) { + const index = rand[i] % availableSigns.length; + str += availableSigns.charAt(index); + } + } + } + return str; +} + +export { calculateMaxTextWidth, generateRandomString }; diff --git a/web/utils/text-utils.test.js b/web/utils/text-utils.test.js new file mode 100644 --- /dev/null +++ b/web/utils/text-utils.test.js @@ -0,0 +1,55 @@ +// @flow + +import { generateRandomString } from './text-utils'; + +describe('generateRandomString', () => { + it('should return an empty string when passed length = 0', () => { + expect(generateRandomString(0, 'abcde')).toMatch(/^$/); + expect(generateRandomString(0, '')).toMatch(/^$/); + }); + + it( + 'should return a random string of length equal to length argument, ' + + 'containig only characters given in availableSigns', + () => { + const length1 = 100; + const availableSigns1 = 'abcde'; + expect(generateRandomString(length1, availableSigns1)).toMatch( + new RegExp(`^[${availableSigns1}]{${length1.toString()}}$`), + ); + + const length2 = 10; + const availableSigns2 = 'abcde0123456789!@#$%^&*'; + expect(generateRandomString(length2, availableSigns2)).toMatch( + new RegExp(`^[${availableSigns2}]{${length2.toString()}}$`), + ); + + const length3 = 10; + const availableSigns3 = 'a'; + expect(generateRandomString(length3, availableSigns3)).toMatch( + new RegExp(`^[${availableSigns3}]{${length3.toString()}}$`), + ); + }, + ); + + it('should throw an error when length is negative', () => { + expect(() => generateRandomString(-1, 'abc')).toThrow(); + expect(() => generateRandomString(-1, '')).toThrow(); + expect(() => generateRandomString(-123, 'abc')).toThrow(); + expect(() => generateRandomString(-123, '')).toThrow(); + }); + + it( + 'should throw an error when availableSigns is an empty string, ' + + 'and length is positive', + () => { + expect(() => generateRandomString(1, '')).toThrow(); + expect(() => generateRandomString(10, '')).toThrow(); + }, + ); + + it('should throw an error when availableSigns length exceeds 256', () => { + const longString = 'a'.repeat(257); + expect(() => generateRandomString(1, longString)).toThrow(); + }); +});