diff --git a/packages/core/src/integer.ts b/packages/core/src/integer.ts index 9c859d046..718011638 100644 --- a/packages/core/src/integer.ts +++ b/packages/core/src/integer.ts @@ -832,10 +832,12 @@ class Integer { * @access private * @param {string} str The textual representation of the Integer * @param {number=} radix The radix in which the text is written (2-36), defaults to 10 + * @param {Object} [opts={}] Configuration options + * @param {boolean} [opts.strictStringValidation=false] Enable strict validation generated Integer. * @returns {!Integer} The corresponding Integer value * @expose */ - static fromString(str: string, radix?: number): Integer { + static fromString (str: string, radix?: number, { strictStringValidation }: { strictStringValidation?: boolean} = {}): Integer { if (str.length === 0) { throw newError('number format error: empty string') } @@ -864,9 +866,15 @@ class Integer { const radixToPower = Integer.fromNumber(Math.pow(radix, 8)) let result = Integer.ZERO - for (var i = 0; i < str.length; i += 8) { - var size = Math.min(8, str.length - i) - var value = parseInt(str.substring(i, i + size), radix) + for (let i = 0; i < str.length; i += 8) { + const size = Math.min(8, str.length - i) + const valueString = str.substring(i, i + size) + const value = parseInt(valueString, radix) + + if (strictStringValidation === true && !_isValidNumberFromString(valueString, value, radix)) { + throw newError(`number format error: "${valueString}" is NaN in radix ${radix}: ${str}`) + } + if (size < 8) { var power = Integer.fromNumber(Math.pow(radix, size)) result = result.multiply(power).add(Integer.fromNumber(value)) @@ -882,10 +890,12 @@ class Integer { * Converts the specified value to a Integer. * @access private * @param {!Integer|number|string|bigint|!{low: number, high: number}} val Value + * @param {Object} [opts={}] Configuration options + * @param {boolean} [opts.strictStringValidation=false] Enable strict validation generated Integer. * @returns {!Integer} * @expose */ - static fromValue(val: Integerable): Integer { + static fromValue (val: Integerable, opts: { strictStringValidation?: boolean} = {}): Integer { if (val /* is compatible */ instanceof Integer) { return val } @@ -893,7 +903,7 @@ class Integer { return Integer.fromNumber(val) } if (typeof val === 'string') { - return Integer.fromString(val) + return Integer.fromString(val, undefined, opts) } if (typeof val === 'bigint') { return Integer.fromString(val.toString()) @@ -945,6 +955,34 @@ class Integer { } } +/** + * @private + * @param num + * @param radix + * @param minSize + * @returns {string} + */ +function _convertNumberToString (num: number, radix: number, minSize: number): string { + const theNumberString = num.toString(radix) + const paddingLength = Math.max(minSize - theNumberString.length, 0) + const padding = '0'.repeat(paddingLength) + return `${padding}${theNumberString}` +} + +/** + * + * @private + * @param theString + * @param theNumber + * @param radix + * @return {boolean} True if valid + */ +function _isValidNumberFromString (theString: string, theNumber: number, radix: number): boolean { + return !Number.isNaN(theString) && + !Number.isNaN(theNumber) && + _convertNumberToString(theNumber, radix, theString.length) === theString.toLowerCase() +} + type Integerable = | number | string @@ -1010,6 +1048,8 @@ var TWO_PWR_24 = Integer.fromInt(TWO_PWR_24_DBL) * Cast value to Integer type. * @access public * @param {Mixed} value - The value to use. + * @param {Object} [opts={}] Configuration options + * @param {boolean} [opts.strictStringValidation=false] Enable strict validation generated Integer. * @return {Integer} - An object of type Integer. */ const int = Integer.fromValue diff --git a/packages/core/test/integer.test.ts b/packages/core/test/integer.test.ts index 32447e192..adfd76e50 100644 --- a/packages/core/test/integer.test.ts +++ b/packages/core/test/integer.test.ts @@ -255,6 +255,26 @@ describe('Integer', () => { newError('number format error: interior "-" character: 123-2') )) + test('Integer.fromString("7891a", undefined, { strictStringValidation: true }) toThrow invalid character', () => + expect(() => Integer.fromString('7891a', undefined, { strictStringValidation: true })).toThrow( + newError('number format error: "7891a" is NaN in radix 10: 7891a') + )) + + test('Integer.fromString("78a91", undefined, { strictStringValidation: true }) toThrow invalid character', () => + expect(() => Integer.fromString('78a91', undefined, { strictStringValidation: true })).toThrow( + newError('number format error: "78a91" is NaN in radix 10: 78a91') + )) + + test('Integer.fromString("a7891", undefined, { strictStringValidation: true }) toThrow invalid character', () => + expect(() => Integer.fromString('a7891', undefined, { strictStringValidation: true })).toThrow( + newError('number format error: "a7891" is NaN in radix 10: a7891') + )) + + test('Integer.fromString("7010", 2, { strictStringValidation: true }) toThrow invalid character', () => + expect(() => Integer.fromString('7010', 2, { strictStringValidation: true })).toThrow( + newError('number format error: "7010" is NaN in radix 2: 7010') + )) + forEachFromValueScenarios(({ input, expectedOutput }) => test(`Integer.fromValue(${input}) toEqual ${expectedOutput}`, () => expect(Integer.fromValue(input)).toEqual(expectedOutput)) @@ -265,6 +285,35 @@ describe('Integer', () => { expect(int(input)).toEqual(expectedOutput)) ) + test('int("7891a", { strictStringValidation: true }) toThrow invalid character', () => + expect(() => int('7891a', { strictStringValidation: true })).toThrow( + newError('number format error: "7891a" is NaN in radix 10: 7891a') + )) + + test('int("78a91", { strictStringValidation: true }) toThrow invalid character', () => + expect(() => int('78a91', { strictStringValidation: true })).toThrow( + newError('number format error: "78a91" is NaN in radix 10: 78a91') + )) + + test('int("a7891", { strictStringValidation: true }) toThrow invalid character', () => + expect(() => int('a7891', { strictStringValidation: true })).toThrow( + newError('number format error: "a7891" is NaN in radix 10: a7891') + )) + + test('int("7891123456789876a", { strictStringValidation: true }) toThrow invalid character', () => + expect(() => int('7891123456789876a', { strictStringValidation: true })).toThrow( + newError('number format error: "a" is NaN in radix 10: 7891123456789876a') + )) + + test('int("7891123456789876a") not toThrow invalid character', () => + expect(() => int('7891123456789876a')).not.toThrow()) + + test.each(malformedNumbers())('int("%s", { strictStringValidation: true }) toThrow invalid character', (theNumberString) => + expect(() => int(theNumberString, { strictStringValidation: true })).toThrow()) + + test.each(wellFormedNumbersAndRadix())('Integer.fromString("%s", %n, { strictStringValidation: true }) not toThrown', (theNumberString, radix) => + expect(() => Integer.fromString(theNumberString, radix, { strictStringValidation: true })).not.toThrow()) + forEachStaticToNumberScenarios(({ input, expectedOutput }) => test(`Integer.toNumber(${input}) toEqual ${expectedOutput}`, () => expect(Integer.toNumber(input)).toEqual(expectedOutput)) @@ -1045,6 +1094,79 @@ function forEachStaticInSafeRangeScenarios( ].forEach(func) } +function malformedNumbers (): string[] { + return [ + '7a', + '7891123a', + '78911234a', + '789112345a', + '7891123456a', + '7891123456789876a', + '78911234567898765a', + '789112345678987654a', + '78911234567898765a2', + '7891123456789876a25', + '789112345678987a256', + '78911234567898a2567', + '7891123456789a25678', + '789112345678a256789', + '78911234567a2567898', + '7891123456a25678987', + '789112345a256789876', + '78911234a2567898765', + '7891123a25678987654', + '7891123ab2567898765', + '78911234ab256789876', + '789112345ab25678987', + '7891123456ab2567898', + '78911234567ab256789', + '78911234567abc25678', + '78911234567abcd2567', + '78911234567abcde256', + '78911234567abcdef25', + '78911234567abcdefg2', + '7891123456abcdefgh1', + '789112345abcdefgh12', + '78911234abcdefgh123', + '7891123abcdefgh1234', + '789112abcdefghij123', + '7kkkkabcdefghijklmn', + '7kkkkabcdefg12345mn', + '7kkkkabcdefg123456n', + '7kkkkab22efg123456n', + '7kkkkab22efg12345mn', + '7kkkkab223fg12345mn', + 'kkkkk11223fg12345mn', + 'kkkkk11223fg123456n', + 'kkkkk11223fg1234567', + 'kkkkk11223451234567', + 'kkk111gkk3451234567', + 'kkk111gkkkk51234567', + 'kkk111gkkkkk123kk67', + 'kkkk234', + 'kkkk2345', + 'kkkk23456', + 'kkkk234567', + 'kkkk2345679kk', + 'kkkk2345679kkkkkk', + 'kkk234567', + 'kkk2345679', + 'kk2345679', + 'kkkkkkkkkkkkkkkkkkk', + ] +} + +function wellFormedNumbersAndRadix (): [string, number][] { + return [ + ['01', 2], + ['012', 3], + ['0123', 4], + ['0123456789', 10], + ['0123456789ab', 12], + ['0123456789abcde', 16], + ] +} + interface AssertionPair { input: I expectedOutput: O