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,34 @@ 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 = ''", + ); + + const validByteUpperBound = + availableSigns.length * + Math.floor(numberOfPossibleByteValues / availableSigns.length); + const drawBytes = + length * (1 + 2 * (1 - 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,42 @@ +// @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, 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(); + }); +});