From c0f5350d89b3d37424aa14e780417b3e324d4fe5 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 18 Apr 2024 14:19:23 -0400 Subject: [PATCH 01/20] initial blah --- src/long.ts | 46 ++++++++++++++++++++++++++++++++++++++++++ test/node/long.test.ts | 9 +++++++++ 2 files changed, 55 insertions(+) diff --git a/src/long.ts b/src/long.ts index f05f71e6b..5ddc2d691 100644 --- a/src/long.ts +++ b/src/long.ts @@ -245,6 +245,52 @@ export class Long extends BSONValue { return Long.fromString(value.toString(), unsigned); } + static validateStringCharacter(str: string, radix?: number): false | string { + radix = radix ?? 10; + + return false; + } + + static fromStringHelper(str: string, unsigned?: boolean, radix?: number, throwsError?: boolean): Long { + if (str.length === 0) throw new BSONError('empty string'); + if (str === 'NaN' || str === 'Infinity' || str === '+Infinity' || str === '-Infinity') + return Long.ZERO; + if (typeof unsigned === 'number') { + // For goog.math.long compatibility + (radix = unsigned), (unsigned = false); + } else { + unsigned = !!unsigned; + } + radix = radix || 10; + if (radix < 2 || 36 < radix) throw new BSONError('radix'); + + let p; + if ((p = str.indexOf('-')) > 0) throw new BSONError('interior hyphen'); + else if (p === 0) { + return Long.fromString(str.substring(1), unsigned, radix).neg(); + } + + // Do several (8) digits each time through the loop, so as to + // minimize the calls to the very expensive emulated div. + const radixToPower = Long.fromNumber(Math.pow(radix, 8)); + + let result = Long.ZERO; + for (let i = 0; i < str.length; i += 8) { + const size = Math.min(8, str.length - i), + value = parseInt(str.substring(i, i + size), radix); + if (size < 8) { + const power = Long.fromNumber(Math.pow(radix, size)); + result = result.mul(power).add(Long.fromNumber(value)); + } else { + result = result.mul(radixToPower); + result = result.add(Long.fromNumber(value)); + } + } + result.unsigned = unsigned; + return result; + } + + /** * Returns a Long representation of the given string, written using the specified radix. * @param str - The textual representation of the Long diff --git a/test/node/long.test.ts b/test/node/long.test.ts index 75611a8a5..63e623750 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -163,4 +163,13 @@ describe('Long', function () { }); }); }); + + describe.only('static validateString()', function() { + it('does not accept non-numeric inputs', () => { + console.log(Long.fromString('foo')); + console.log(Long.fromString("1234xxx5")); + console.log(Long.fromString("1234xxxx5")) + console.log(Long.fromString("1234xxxxx5")); + }); + }); }); From 272e4f13777193c0e2b60e825945144380fcc407 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 18 Apr 2024 15:03:19 -0400 Subject: [PATCH 02/20] another tempp commit --- src/long.ts | 51 +++++++++++---------------------------------------- 1 file changed, 11 insertions(+), 40 deletions(-) diff --git a/src/long.ts b/src/long.ts index 5ddc2d691..30d8ae8d8 100644 --- a/src/long.ts +++ b/src/long.ts @@ -245,13 +245,19 @@ export class Long extends BSONValue { return Long.fromString(value.toString(), unsigned); } - static validateStringCharacter(str: string, radix?: number): false | string { + static validateStringCharacters(str: string, radix?: number): false | string { radix = radix ?? 10; - - return false; + let regexInputString; + if (radix - 10 > 0) { + validCharRangeEnd = String.fromCharCode('a'.charCodeAt(0) + (radix-10)); + } else { + regexInputString = `[^-0-9.+]` + } + const regex = new RegExp(regexInputString); + return regex.test(str) ? str : false; } - static fromStringHelper(str: string, unsigned?: boolean, radix?: number, throwsError?: boolean): Long { + static fromStringHelper(str: string, throwsError?: boolean, unsigned?: boolean, radix?: number): Long { if (str.length === 0) throw new BSONError('empty string'); if (str === 'NaN' || str === 'Infinity' || str === '+Infinity' || str === '-Infinity') return Long.ZERO; @@ -299,42 +305,7 @@ export class Long extends BSONValue { * @returns The corresponding Long value */ static fromString(str: string, unsigned?: boolean, radix?: number): Long { - if (str.length === 0) throw new BSONError('empty string'); - if (str === 'NaN' || str === 'Infinity' || str === '+Infinity' || str === '-Infinity') - return Long.ZERO; - if (typeof unsigned === 'number') { - // For goog.math.long compatibility - (radix = unsigned), (unsigned = false); - } else { - unsigned = !!unsigned; - } - radix = radix || 10; - if (radix < 2 || 36 < radix) throw new BSONError('radix'); - - let p; - if ((p = str.indexOf('-')) > 0) throw new BSONError('interior hyphen'); - else if (p === 0) { - return Long.fromString(str.substring(1), unsigned, radix).neg(); - } - - // Do several (8) digits each time through the loop, so as to - // minimize the calls to the very expensive emulated div. - const radixToPower = Long.fromNumber(Math.pow(radix, 8)); - - let result = Long.ZERO; - for (let i = 0; i < str.length; i += 8) { - const size = Math.min(8, str.length - i), - value = parseInt(str.substring(i, i + size), radix); - if (size < 8) { - const power = Long.fromNumber(Math.pow(radix, size)); - result = result.mul(power).add(Long.fromNumber(value)); - } else { - result = result.mul(radixToPower); - result = result.add(Long.fromNumber(value)); - } - } - result.unsigned = unsigned; - return result; + return Long.fromStringHelper(str, false, unsigned, radix); } /** From 4483ac486ddac7097aaaa3a20d8aaa1fd2a2c973 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 18 Apr 2024 15:47:42 -0400 Subject: [PATCH 03/20] finished validateStringCharacters --- src/long.ts | 14 +++++++++----- test/node/long.test.ts | 1 + 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/long.ts b/src/long.ts index 30d8ae8d8..6086e95de 100644 --- a/src/long.ts +++ b/src/long.ts @@ -247,12 +247,11 @@ export class Long extends BSONValue { static validateStringCharacters(str: string, radix?: number): false | string { radix = radix ?? 10; - let regexInputString; + let regexInputString = `[^-0-9.+]` if (radix - 10 > 0) { - validCharRangeEnd = String.fromCharCode('a'.charCodeAt(0) + (radix-10)); - } else { - regexInputString = `[^-0-9.+]` - } + const validCharRangeEnd = String.fromCharCode('a'.charCodeAt(0) + (radix - 11)); + regexInputString = `[^-0-9.+a-${validCharRangeEnd }]`; + } const regex = new RegExp(regexInputString); return regex.test(str) ? str : false; } @@ -268,6 +267,10 @@ export class Long extends BSONValue { unsigned = !!unsigned; } radix = radix || 10; + + if (throwsError && !Long.validateStringCharacters(str, radix)) { + throw new BSONError(`Input: ${str} contains invalid characters for radix: ${radix}`); + } if (radix < 2 || 36 < radix) throw new BSONError('radix'); let p; @@ -293,6 +296,7 @@ export class Long extends BSONValue { } } result.unsigned = unsigned; + return result; } diff --git a/test/node/long.test.ts b/test/node/long.test.ts index 63e623750..23db75d29 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -170,6 +170,7 @@ describe('Long', function () { console.log(Long.fromString("1234xxx5")); console.log(Long.fromString("1234xxxx5")) console.log(Long.fromString("1234xxxxx5")); + console.log(Long.fromString("1e5")); }); }); }); From a87dade1018704ef1cbc0c04d83c6c4f519959cc Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 18 Apr 2024 16:14:25 -0400 Subject: [PATCH 04/20] feat(NODE-5648): add Long.fromStringStrict method --- src/long.ts | 57 ++++++++++++++++++++++++++++++++---------- test/node/long.test.ts | 10 -------- 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/src/long.ts b/src/long.ts index 6086e95de..54055d88e 100644 --- a/src/long.ts +++ b/src/long.ts @@ -245,18 +245,40 @@ export class Long extends BSONValue { return Long.fromString(value.toString(), unsigned); } + /** + * @internal + * Returns false for an string that contains invalid characters for its radix, else returns the original string. + * @param str - The textual representation of the Long + * @param radix - The radix in which the text is written (2-36), defaults to 10 + */ static validateStringCharacters(str: string, radix?: number): false | string { - radix = radix ?? 10; - let regexInputString = `[^-0-9.+]` - if (radix - 10 > 0) { - const validCharRangeEnd = String.fromCharCode('a'.charCodeAt(0) + (radix - 11)); - regexInputString = `[^-0-9.+a-${validCharRangeEnd }]`; - } - const regex = new RegExp(regexInputString); - return regex.test(str) ? str : false; + radix = radix ?? 10; + let regexInputString = `[^-0-9.+]`; + if (radix - 10 > 0) { + const validCharRangeEnd = String.fromCharCode('a'.charCodeAt(0) + (radix - 11)); + regexInputString = `[^-0-9.+a-${validCharRangeEnd}]`; + } + const regex = new RegExp(regexInputString); + return regex.test(str) ? str : false; } - static fromStringHelper(str: string, throwsError?: boolean, unsigned?: boolean, radix?: number): Long { + /** + * @internal + * Returns a Long representation of the given string, written using the specified radix. + * This method throws an error, if the following both of the conditions are true: + * - the string contains invalid characters for the given radix + * - throwsError is true + * @param str - The textual representation of the Long + * @param unsigned - Whether unsigned or not, defaults to signed + * @param radix - The radix in which the text is written (2-36), defaults to 10 + * @returns The corresponding Long value + */ + static fromStringHelper( + str: string, + unsigned?: boolean, + radix?: number, + throwsError?: boolean + ): Long { if (str.length === 0) throw new BSONError('empty string'); if (str === 'NaN' || str === 'Infinity' || str === '+Infinity' || str === '-Infinity') return Long.ZERO; @@ -267,7 +289,6 @@ export class Long extends BSONValue { unsigned = !!unsigned; } radix = radix || 10; - if (throwsError && !Long.validateStringCharacters(str, radix)) { throw new BSONError(`Input: ${str} contains invalid characters for radix: ${radix}`); } @@ -276,7 +297,7 @@ export class Long extends BSONValue { let p; if ((p = str.indexOf('-')) > 0) throw new BSONError('interior hyphen'); else if (p === 0) { - return Long.fromString(str.substring(1), unsigned, radix).neg(); + return Long.fromStringHelper(str.substring(1), unsigned, radix).neg(); } // Do several (8) digits each time through the loop, so as to @@ -296,10 +317,20 @@ export class Long extends BSONValue { } } result.unsigned = unsigned; - return result; } + /** + * Returns a Long representation of the given string, written using the specified radix. + * If the string contains invalid characters for the given radix, this function will throw an error. + * @param str - The textual representation of the Long + * @param unsigned - Whether unsigned or not, defaults to signed + * @param radix - The radix in which the text is written (2-36), defaults to 10 + * @returns The corresponding Long value + */ + static fromStringStrict(str: string, unsigned?: boolean, radix?: number): Long { + return Long.fromStringHelper(str, unsigned, radix, true); + } /** * Returns a Long representation of the given string, written using the specified radix. @@ -309,7 +340,7 @@ export class Long extends BSONValue { * @returns The corresponding Long value */ static fromString(str: string, unsigned?: boolean, radix?: number): Long { - return Long.fromStringHelper(str, false, unsigned, radix); + return Long.fromStringHelper(str, false, radix, unsigned); } /** diff --git a/test/node/long.test.ts b/test/node/long.test.ts index 23db75d29..75611a8a5 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -163,14 +163,4 @@ describe('Long', function () { }); }); }); - - describe.only('static validateString()', function() { - it('does not accept non-numeric inputs', () => { - console.log(Long.fromString('foo')); - console.log(Long.fromString("1234xxx5")); - console.log(Long.fromString("1234xxxx5")) - console.log(Long.fromString("1234xxxxx5")); - console.log(Long.fromString("1e5")); - }); - }); }); From 76801e883dea75aec06ee7246c7f4a5b8ebad41d Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 18 Apr 2024 18:14:09 -0400 Subject: [PATCH 05/20] EOD progress --- src/long.ts | 17 ++++++++---- test/node/long.test.ts | 62 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/src/long.ts b/src/long.ts index 54055d88e..4d1eb7b6d 100644 --- a/src/long.ts +++ b/src/long.ts @@ -253,13 +253,16 @@ export class Long extends BSONValue { */ static validateStringCharacters(str: string, radix?: number): false | string { radix = radix ?? 10; - let regexInputString = `[^-0-9.+]`; - if (radix - 10 > 0) { + + let regexInputString = ''; + if (radix <= 10) { + regexInputString = `[^-0-${radix - 1}+]`; + } else { const validCharRangeEnd = String.fromCharCode('a'.charCodeAt(0) + (radix - 11)); - regexInputString = `[^-0-9.+a-${validCharRangeEnd}]`; + regexInputString = `[^-0-9+(a-${validCharRangeEnd})]`; } - const regex = new RegExp(regexInputString); - return regex.test(str) ? str : false; + const regex = new RegExp(regexInputString, '\i'); + return regex.test(str) ? false : str; } /** @@ -271,6 +274,7 @@ export class Long extends BSONValue { * @param str - The textual representation of the Long * @param unsigned - Whether unsigned or not, defaults to signed * @param radix - The radix in which the text is written (2-36), defaults to 10 + * @param throwsError - Whether or not throwing an error is permitted * @returns The corresponding Long value */ static fromStringHelper( @@ -321,6 +325,7 @@ export class Long extends BSONValue { } /** + * @internal - TODO(NODE-XXXX): fromStrictString throws on overflow * Returns a Long representation of the given string, written using the specified radix. * If the string contains invalid characters for the given radix, this function will throw an error. * @param str - The textual representation of the Long @@ -340,7 +345,7 @@ export class Long extends BSONValue { * @returns The corresponding Long value */ static fromString(str: string, unsigned?: boolean, radix?: number): Long { - return Long.fromStringHelper(str, false, radix, unsigned); + return Long.fromStringHelper(str, unsigned, radix, false); } /** diff --git a/test/node/long.test.ts b/test/node/long.test.ts index 75611a8a5..f9565e93e 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -163,4 +163,66 @@ describe('Long', function () { }); }); }); + + describe.only('static fromStringStrict()', function () { + const successInputs = [ + ['basic no alphabet low radix', '1236', 8], + ['radix does allow given alphabet letter', 'eEe', 15], + ['hexadecimal letters', '126073efbcdADEF', 16], + ['negative hexadecimal letters', '-126073efbcdADEF', 16] + ]; + + const failureInputs = [ + ['empty string','', 2], + ['non a-z or numeric string', '~~', 36], + ['alphabet in radix < 10', 'a', 4], + ['radix does not allow all alphabet letters', 'eee', 14] + ]; + + for (const [testName, str, radix] of successInputs) { + context(`when the input is ${testName}`, () => { + it(`should return a input string`, () => { + expect(Long.fromStringStrict(str, true, radix).toString(radix)).to.equal(str.toLowerCase()); + }); + }); + } + for (const [testName, str, radix] of failureInputs) { + context(`when the input is ${testName}`, () => { + it(`should return false`, () => { + expect(() => Long.fromStringStrict(str, true, radix)).to.throw(BSONError); + }); + }); + } + }); + + describe('static validateStringCharacters()', function () { + const successInputs = [ + ['multiple decimal points', '..', 30], + ['radix does not allow given alphabet letter', 'eEe', 15], + ['empty string','', 2], + ['all possible hexadecimal characters', '12efabc689873dADCDEF', 16] + ]; + + const failureInputs = [ + ['non a-z or numeric string', '~~', 36], + ['alphabet in radix < 10', 'a', 4], + ['radix does not allow all alphabet letters', 'eee', 14] + ]; + + for (const [testName, str, radix] of successInputs) { + context(`when the input is ${testName}`, () => { + it(`should return a input string`, () => { + expect(Long.validateStringCharacters(str, radix)).to.equal(str); + }); + }); + } + + for (const [testName, str, radix] of failureInputs) { + context(`when the input is ${testName}`, () => { + it(`should return false`, () => { + expect(Long.validateStringCharacters(str, radix)).to.equal(false); + }); + }); + } + }); }); From a0015900d4bd3e1aaa25ea8dc602c5e72ab4f9b9 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Fri, 19 Apr 2024 16:28:39 -0400 Subject: [PATCH 06/20] ready for review --- src/int_32.ts | 7 ++--- src/long.ts | 33 +++++++++++++++------ src/utils/string_utils.ts | 11 +++++++ test/node/long.test.ts | 61 ++++++++++++++++++++++++++++----------- test/node/release.test.ts | 1 + 5 files changed, 82 insertions(+), 31 deletions(-) create mode 100644 src/utils/string_utils.ts diff --git a/src/int_32.ts b/src/int_32.ts index f394f7af6..2dad14a38 100644 --- a/src/int_32.ts +++ b/src/int_32.ts @@ -3,6 +3,7 @@ import { BSON_INT32_MAX, BSON_INT32_MIN } from './constants'; import { BSONError } from './error'; import type { EJSONOptions } from './extended_json'; import { type InspectFn, defaultInspect } from './parser/utils'; +import { removeLeadingZeros } from './utils/string_utils'; /** @public */ export interface Int32Extended { @@ -48,11 +49,7 @@ export class Int32 extends BSONValue { * @param value - the string we want to represent as an int32. */ static fromString(value: string): Int32 { - const cleanedValue = !/[^0]+/.test(value) - ? value.replace(/^0+/, '0') // all zeros case - : value[0] === '-' - ? value.replace(/^-0+/, '-') // negative number with leading zeros - : value.replace(/^\+?0+/, ''); // positive number with leading zeros + const cleanedValue = removeLeadingZeros(value); const coercedValue = Number(value); diff --git a/src/long.ts b/src/long.ts index 4d1eb7b6d..5e7c54861 100644 --- a/src/long.ts +++ b/src/long.ts @@ -3,6 +3,7 @@ import { BSONError } from './error'; import type { EJSONOptions } from './extended_json'; import { type InspectFn, defaultInspect } from './parser/utils'; import type { Timestamp } from './timestamp'; +import { removeLeadingZeros } from './utils/string_utils'; interface LongWASMHelpers { /** Gets the high bits of the last operation performed */ @@ -261,16 +262,16 @@ export class Long extends BSONValue { const validCharRangeEnd = String.fromCharCode('a'.charCodeAt(0) + (radix - 11)); regexInputString = `[^-0-9+(a-${validCharRangeEnd})]`; } - const regex = new RegExp(regexInputString, '\i'); + const regex = new RegExp(regexInputString, 'i'); return regex.test(str) ? false : str; } /** * @internal * Returns a Long representation of the given string, written using the specified radix. - * This method throws an error, if the following both of the conditions are true: + * Throws an error if `throwsError` is set to true and any of the following conditions are true: * - the string contains invalid characters for the given radix - * - throwsError is true + * - the string contains whitespace * @param str - The textual representation of the Long * @param unsigned - Whether unsigned or not, defaults to signed * @param radix - The radix in which the text is written (2-36), defaults to 10 @@ -293,15 +294,20 @@ export class Long extends BSONValue { unsigned = !!unsigned; } radix = radix || 10; + + if (radix < 2 || 36 < radix) throw new BSONError('radix'); + if (throwsError && !Long.validateStringCharacters(str, radix)) { - throw new BSONError(`Input: ${str} contains invalid characters for radix: ${radix}`); + throw new BSONError(`Input: '${str}' contains invalid characters for radix: ${radix}`); + } + if (throwsError && str.trim() !== str) { + throw new BSONError(`Input: '${str}' contains whitespace.`); } - if (radix < 2 || 36 < radix) throw new BSONError('radix'); let p; if ((p = str.indexOf('-')) > 0) throw new BSONError('interior hyphen'); else if (p === 0) { - return Long.fromStringHelper(str.substring(1), unsigned, radix).neg(); + return Long.fromStringHelper(str.substring(1), unsigned, radix, throwsError).neg(); } // Do several (8) digits each time through the loop, so as to @@ -325,16 +331,25 @@ export class Long extends BSONValue { } /** - * @internal - TODO(NODE-XXXX): fromStrictString throws on overflow * Returns a Long representation of the given string, written using the specified radix. - * If the string contains invalid characters for the given radix, this function will throw an error. + * Throws an error if any of the following conditions are true: + * - the string contains invalid characters for the given radix + * - the string contains whitespace + * - the value the string represents is too large or too small to be a Long * @param str - The textual representation of the Long * @param unsigned - Whether unsigned or not, defaults to signed * @param radix - The radix in which the text is written (2-36), defaults to 10 * @returns The corresponding Long value */ static fromStringStrict(str: string, unsigned?: boolean, radix?: number): Long { - return Long.fromStringHelper(str, unsigned, radix, true); + // remove leading zeros (for later string comparison and to make math faster) + const cleanedStr = removeLeadingZeros(str); + // doing this check outside of recursive function so cleanedStr value is consistent + const result = Long.fromStringHelper(cleanedStr, unsigned, radix, true); + if (result.toString(radix).toLowerCase() !== cleanedStr.toLowerCase()) { + throw new BSONError(`Input: ${str} is not representable as a Long`); + } + return result; } /** diff --git a/src/utils/string_utils.ts b/src/utils/string_utils.ts new file mode 100644 index 000000000..0e75d1d2e --- /dev/null +++ b/src/utils/string_utils.ts @@ -0,0 +1,11 @@ +/** + * @internal + * Removes leading zeros from textual representation of a number. + */ +export function removeLeadingZeros(str: string): string { + return !/[^0]+/.test(str) + ? str.replace(/^0+/, '0') // all zeros case + : str[0] === '-' + ? str.replace(/^-0+/, '-') // negative number with leading zeros + : str.replace(/^\+?0+/, ''); +} diff --git a/test/node/long.test.ts b/test/node/long.test.ts index f9565e93e..248f1f534 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -164,32 +164,59 @@ describe('Long', function () { }); }); - describe.only('static fromStringStrict()', function () { + describe('static fromStringStrict()', function () { const successInputs = [ - ['basic no alphabet low radix', '1236', 8], - ['radix does allow given alphabet letter', 'eEe', 15], - ['hexadecimal letters', '126073efbcdADEF', 16], - ['negative hexadecimal letters', '-126073efbcdADEF', 16] + ['basic no alphabet low radix', '1236', true, 8], + ['negative basic no alphabet low radix', '-1236', false, 8], + ['valid upper and lower case letters in string with radix > 10', 'eEe', true, 15], + ['hexadecimal letters', '126073efbcdADEF', true, 16], + ['negative hexadecimal letters', '-1267efbcdDEF', false, 16], + ['negative leading zeros', '-00000032', false, 15, '-32'], + ['leading zeros', '00000032', false, 15, '32'], + ['explicit positive leading zeros', '+00000032', false, 15, '32'], + ['max unsigned binary input', Long.MAX_UNSIGNED_VALUE.toString(2), true, 2], + ['max unsigned decimal input', Long.MAX_UNSIGNED_VALUE.toString(10), true, 10], + ['max unsigned hex input', Long.MAX_UNSIGNED_VALUE.toString(16), true, 16], + ['max signed binary input', Long.MAX_VALUE.toString(2), false, 2], + ['max signed decimal input', Long.MAX_VALUE.toString(10), false, 10], + ['max signed hex input', Long.MAX_VALUE.toString(16), false, 16], + ['min signed binary input', Long.MIN_VALUE.toString(2), false, 2], + ['min signed decimal input', Long.MIN_VALUE.toString(10), false, 10], + ['min signed hex input', Long.MIN_VALUE.toString(16), false, 16], + ['signed zero', '0', false, 10], + ['unsigned zero', '0', true, 10] ]; const failureInputs = [ - ['empty string','', 2], - ['non a-z or numeric string', '~~', 36], - ['alphabet in radix < 10', 'a', 4], - ['radix does not allow all alphabet letters', 'eee', 14] + ['empty string', '', true, 2], + ['invalid numbers in binary string', '234', true, 2], + ['non a-z or numeric string', '~~', true, 36], + ['alphabet in radix < 10', 'a', true, 9], + ['radix does not allow all alphabet letters', 'eee', 14], + ['over max unsigned binary input', Long.MAX_UNSIGNED_VALUE.toString(2) + '1', true, 2], + ['over max unsigned decimal input', Long.MAX_UNSIGNED_VALUE.toString(10) + '1', true, 10], + ['over max unsigned hex input', Long.MAX_UNSIGNED_VALUE.toString(16) + '1', true, 16], + ['over max signed binary input', Long.MAX_VALUE.toString(2) + '1', false, 2], + ['over max signed decimal input', Long.MAX_VALUE.toString(10) + '1', false, 10], + ['over max signed hex input', Long.MAX_VALUE.toString(16) + '1', false, 16], + ['under min signed binary input', Long.MIN_VALUE.toString(2) + '1', false, 2], + ['under min signed decimal input', Long.MIN_VALUE.toString(10) + '1', false, 10], + ['under min signed hex input', Long.MIN_VALUE.toString(16) + '1', false, 16] ]; - for (const [testName, str, radix] of successInputs) { + for (const [testName, str, unsigned, radix, expectedStr] of successInputs) { context(`when the input is ${testName}`, () => { it(`should return a input string`, () => { - expect(Long.fromStringStrict(str, true, radix).toString(radix)).to.equal(str.toLowerCase()); + expect(Long.fromStringStrict(str, unsigned, radix).toString(radix)).to.equal( + expectedStr ?? str.toLowerCase() + ); }); }); } - for (const [testName, str, radix] of failureInputs) { + for (const [testName, str, unsigned, radix] of failureInputs) { context(`when the input is ${testName}`, () => { - it(`should return false`, () => { - expect(() => Long.fromStringStrict(str, true, radix)).to.throw(BSONError); + it(`should throw BSONError`, () => { + expect(() => Long.fromStringStrict(str, unsigned, radix)).to.throw(BSONError); }); }); } @@ -197,13 +224,13 @@ describe('Long', function () { describe('static validateStringCharacters()', function () { const successInputs = [ - ['multiple decimal points', '..', 30], - ['radix does not allow given alphabet letter', 'eEe', 15], - ['empty string','', 2], + ['radix does allows given alphabet letter', 'eEe', 15], + ['empty string', '', 2], ['all possible hexadecimal characters', '12efabc689873dADCDEF', 16] ]; const failureInputs = [ + ['multiple decimal points', '..', 30], ['non a-z or numeric string', '~~', 36], ['alphabet in radix < 10', 'a', 4], ['radix does not allow all alphabet letters', 'eee', 14] diff --git a/test/node/release.test.ts b/test/node/release.test.ts index da69230df..f35018716 100644 --- a/test/node/release.test.ts +++ b/test/node/release.test.ts @@ -48,6 +48,7 @@ const REQUIRED_FILES = [ 'src/utils/byte_utils.ts', 'src/utils/node_byte_utils.ts', 'src/utils/number_utils.ts', + 'src/utils/string_utils.ts', 'src/utils/web_byte_utils.ts', 'src/utils/latin.ts', 'src/validate_utf8.ts', From c4818d93725852293b2adb2c3b99ec71391b5747 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Fri, 19 Apr 2024 16:47:09 -0400 Subject: [PATCH 07/20] make error message for overflows more accurate --- src/long.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/long.ts b/src/long.ts index 5e7c54861..29326b925 100644 --- a/src/long.ts +++ b/src/long.ts @@ -347,7 +347,9 @@ export class Long extends BSONValue { // doing this check outside of recursive function so cleanedStr value is consistent const result = Long.fromStringHelper(cleanedStr, unsigned, radix, true); if (result.toString(radix).toLowerCase() !== cleanedStr.toLowerCase()) { - throw new BSONError(`Input: ${str} is not representable as a Long`); + throw new BSONError( + `Input: ${str} is not representable as ${result.unsigned ? 'an unsigned' : 'a signed'} 64-bit Long with radix: ${radix}` + ); } return result; } From 4c7d5b577dbaa3d4770d06504cfc31920ec84ebd Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Fri, 19 Apr 2024 16:51:36 -0400 Subject: [PATCH 08/20] add whitespace test case --- test/node/long.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/node/long.test.ts b/test/node/long.test.ts index 248f1f534..f43b7abba 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -201,7 +201,8 @@ describe('Long', function () { ['over max signed hex input', Long.MAX_VALUE.toString(16) + '1', false, 16], ['under min signed binary input', Long.MIN_VALUE.toString(2) + '1', false, 2], ['under min signed decimal input', Long.MIN_VALUE.toString(10) + '1', false, 10], - ['under min signed hex input', Long.MIN_VALUE.toString(16) + '1', false, 16] + ['under min signed hex input', Long.MIN_VALUE.toString(16) + '1', false, 16], + ['string with whitespace', ' 3503a ', false, 11] ]; for (const [testName, str, unsigned, radix, expectedStr] of successInputs) { From 8939fe9f1dcf8e5c80a3b08836d7098cd821e077 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Fri, 19 Apr 2024 16:56:43 -0400 Subject: [PATCH 09/20] fix test naming --- test/node/long.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/node/long.test.ts b/test/node/long.test.ts index f43b7abba..daf13eeff 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -207,7 +207,7 @@ describe('Long', function () { for (const [testName, str, unsigned, radix, expectedStr] of successInputs) { context(`when the input is ${testName}`, () => { - it(`should return a input string`, () => { + it(`should return a Long represenation of the input`, () => { expect(Long.fromStringStrict(str, unsigned, radix).toString(radix)).to.equal( expectedStr ?? str.toLowerCase() ); From 667168e130f0ec04d2ae22ed01756a342d5aec5d Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 22 Apr 2024 17:14:56 -0400 Subject: [PATCH 10/20] requested changes --- src/long.ts | 32 +++++++++++++------------------- test/node/long.test.ts | 15 +++++++++++---- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/long.ts b/src/long.ts index 29326b925..215d1f4e3 100644 --- a/src/long.ts +++ b/src/long.ts @@ -252,17 +252,11 @@ export class Long extends BSONValue { * @param str - The textual representation of the Long * @param radix - The radix in which the text is written (2-36), defaults to 10 */ - static validateStringCharacters(str: string, radix?: number): false | string { + private static validateStringCharacters(str: string, radix?: number): false | string { radix = radix ?? 10; - - let regexInputString = ''; - if (radix <= 10) { - regexInputString = `[^-0-${radix - 1}+]`; - } else { - const validCharRangeEnd = String.fromCharCode('a'.charCodeAt(0) + (radix - 11)); - regexInputString = `[^-0-9+(a-${validCharRangeEnd})]`; - } - const regex = new RegExp(regexInputString, 'i'); + const validCharacters = '0123456789abcdefghijklmnopqrstuvwxyz'.slice(0, radix); + // regex is case insensitive and checks that each character within the string is one of the validCharacters + const regex = new RegExp(`[^-+${validCharacters}]`, 'i'); return regex.test(str) ? false : str; } @@ -273,16 +267,16 @@ export class Long extends BSONValue { * - the string contains invalid characters for the given radix * - the string contains whitespace * @param str - The textual representation of the Long + * @param validateStringCharacters - Whether or not invalid characters should throw an error * @param unsigned - Whether unsigned or not, defaults to signed * @param radix - The radix in which the text is written (2-36), defaults to 10 - * @param throwsError - Whether or not throwing an error is permitted * @returns The corresponding Long value */ - static fromStringHelper( + private static _fromString( str: string, + validateStringCharacters: boolean, unsigned?: boolean, radix?: number, - throwsError?: boolean ): Long { if (str.length === 0) throw new BSONError('empty string'); if (str === 'NaN' || str === 'Infinity' || str === '+Infinity' || str === '-Infinity') @@ -297,17 +291,17 @@ export class Long extends BSONValue { if (radix < 2 || 36 < radix) throw new BSONError('radix'); - if (throwsError && !Long.validateStringCharacters(str, radix)) { + if (validateStringCharacters && !Long.validateStringCharacters(str, radix)) { throw new BSONError(`Input: '${str}' contains invalid characters for radix: ${radix}`); } - if (throwsError && str.trim() !== str) { - throw new BSONError(`Input: '${str}' contains whitespace.`); + if (validateStringCharacters && str.trim() !== str) { + throw new BSONError(`Input: '${str}' contains leading and/or trailing whitespace.`); } let p; if ((p = str.indexOf('-')) > 0) throw new BSONError('interior hyphen'); else if (p === 0) { - return Long.fromStringHelper(str.substring(1), unsigned, radix, throwsError).neg(); + return Long._fromString(str.substring(1), validateStringCharacters, unsigned, radix).neg(); } // Do several (8) digits each time through the loop, so as to @@ -345,7 +339,7 @@ export class Long extends BSONValue { // remove leading zeros (for later string comparison and to make math faster) const cleanedStr = removeLeadingZeros(str); // doing this check outside of recursive function so cleanedStr value is consistent - const result = Long.fromStringHelper(cleanedStr, unsigned, radix, true); + const result = Long._fromString(cleanedStr, true, unsigned, radix); if (result.toString(radix).toLowerCase() !== cleanedStr.toLowerCase()) { throw new BSONError( `Input: ${str} is not representable as ${result.unsigned ? 'an unsigned' : 'a signed'} 64-bit Long with radix: ${radix}` @@ -362,7 +356,7 @@ export class Long extends BSONValue { * @returns The corresponding Long value */ static fromString(str: string, unsigned?: boolean, radix?: number): Long { - return Long.fromStringHelper(str, unsigned, radix, false); + return Long._fromString(str, false, unsigned, radix); } /** diff --git a/test/node/long.test.ts b/test/node/long.test.ts index daf13eeff..ab355e231 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -165,7 +165,13 @@ describe('Long', function () { }); describe('static fromStringStrict()', function () { - const successInputs = [ + const successInputs: [ + name: string, + input: string, + unsigned: boolean, + radix: number, + expectedStr?: string + ][] = [ ['basic no alphabet low radix', '1236', true, 8], ['negative basic no alphabet low radix', '-1236', false, 8], ['valid upper and lower case letters in string with radix > 10', 'eEe', true, 15], @@ -187,12 +193,12 @@ describe('Long', function () { ['unsigned zero', '0', true, 10] ]; - const failureInputs = [ + const failureInputs: [name: string, input: string, unsigned: boolean, radix: number][] = [ ['empty string', '', true, 2], ['invalid numbers in binary string', '234', true, 2], ['non a-z or numeric string', '~~', true, 36], ['alphabet in radix < 10', 'a', true, 9], - ['radix does not allow all alphabet letters', 'eee', 14], + ['radix does not allow all alphabet letters', 'eee', false, 14], ['over max unsigned binary input', Long.MAX_UNSIGNED_VALUE.toString(2) + '1', true, 2], ['over max unsigned decimal input', Long.MAX_UNSIGNED_VALUE.toString(10) + '1', true, 10], ['over max unsigned hex input', Long.MAX_UNSIGNED_VALUE.toString(16) + '1', true, 16], @@ -227,7 +233,8 @@ describe('Long', function () { const successInputs = [ ['radix does allows given alphabet letter', 'eEe', 15], ['empty string', '', 2], - ['all possible hexadecimal characters', '12efabc689873dADCDEF', 16] + ['all possible hexadecimal characters', '12efabc689873dADCDEF', 16], + ['leading zeros', '0000000004567e', 16] ]; const failureInputs = [ From 88fb76775297b13ee7b0bcc67d40c1ed247629c7 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 22 Apr 2024 17:15:39 -0400 Subject: [PATCH 11/20] lint fix --- src/long.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/long.ts b/src/long.ts index 215d1f4e3..7589af7f7 100644 --- a/src/long.ts +++ b/src/long.ts @@ -276,7 +276,7 @@ export class Long extends BSONValue { str: string, validateStringCharacters: boolean, unsigned?: boolean, - radix?: number, + radix?: number ): Long { if (str.length === 0) throw new BSONError('empty string'); if (str === 'NaN' || str === 'Infinity' || str === '+Infinity' || str === '-Infinity') From e616e8879491c7a2fb035eac01779463ea9144fc Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Wed, 24 Apr 2024 15:40:14 -0400 Subject: [PATCH 12/20] requested changes team review --- src/int_32.ts | 4 +-- src/long.ts | 42 +++++++++------------- src/utils/string_utils.ts | 24 ++++++++++--- test/node/int_32_tests.js | 4 ++- test/node/long.test.ts | 50 ++++++++------------------ test/node/utils/string_utils.test.ts | 54 ++++++++++++++++++++++++++++ 6 files changed, 108 insertions(+), 70 deletions(-) create mode 100644 test/node/utils/string_utils.test.ts diff --git a/src/int_32.ts b/src/int_32.ts index 2dad14a38..afcca018f 100644 --- a/src/int_32.ts +++ b/src/int_32.ts @@ -3,7 +3,7 @@ import { BSON_INT32_MAX, BSON_INT32_MIN } from './constants'; import { BSONError } from './error'; import type { EJSONOptions } from './extended_json'; import { type InspectFn, defaultInspect } from './parser/utils'; -import { removeLeadingZeros } from './utils/string_utils'; +import { removeLeadingZerosandExplicitPlus } from './utils/string_utils'; /** @public */ export interface Int32Extended { @@ -49,7 +49,7 @@ export class Int32 extends BSONValue { * @param value - the string we want to represent as an int32. */ static fromString(value: string): Int32 { - const cleanedValue = removeLeadingZeros(value); + const cleanedValue = removeLeadingZerosandExplicitPlus(value); const coercedValue = Number(value); diff --git a/src/long.ts b/src/long.ts index 7589af7f7..3e49776b5 100644 --- a/src/long.ts +++ b/src/long.ts @@ -3,7 +3,7 @@ import { BSONError } from './error'; import type { EJSONOptions } from './extended_json'; import { type InspectFn, defaultInspect } from './parser/utils'; import type { Timestamp } from './timestamp'; -import { removeLeadingZeros } from './utils/string_utils'; +import * as StringUtils from './utils/string_utils'; interface LongWASMHelpers { /** Gets the high bits of the last operation performed */ @@ -246,20 +246,6 @@ export class Long extends BSONValue { return Long.fromString(value.toString(), unsigned); } - /** - * @internal - * Returns false for an string that contains invalid characters for its radix, else returns the original string. - * @param str - The textual representation of the Long - * @param radix - The radix in which the text is written (2-36), defaults to 10 - */ - private static validateStringCharacters(str: string, radix?: number): false | string { - radix = radix ?? 10; - const validCharacters = '0123456789abcdefghijklmnopqrstuvwxyz'.slice(0, radix); - // regex is case insensitive and checks that each character within the string is one of the validCharacters - const regex = new RegExp(`[^-+${validCharacters}]`, 'i'); - return regex.test(str) ? false : str; - } - /** * @internal * Returns a Long representation of the given string, written using the specified radix. @@ -291,19 +277,19 @@ export class Long extends BSONValue { if (radix < 2 || 36 < radix) throw new BSONError('radix'); - if (validateStringCharacters && !Long.validateStringCharacters(str, radix)) { - throw new BSONError(`Input: '${str}' contains invalid characters for radix: ${radix}`); - } - if (validateStringCharacters && str.trim() !== str) { - throw new BSONError(`Input: '${str}' contains leading and/or trailing whitespace.`); - } - let p; if ((p = str.indexOf('-')) > 0) throw new BSONError('interior hyphen'); else if (p === 0) { return Long._fromString(str.substring(1), validateStringCharacters, unsigned, radix).neg(); } + if (str.trim() !== str) { + throw new BSONError(`Input: '${str}' contains leading and/or trailing whitespace`); + } + if (!StringUtils.validateStringCharacters(str, radix)) { + throw new BSONError(`Input: '${str}' contains invalid characters for radix: ${radix}`); + } + // Do several (8) digits each time through the loop, so as to // minimize the calls to the very expensive emulated div. const radixToPower = Long.fromNumber(Math.pow(radix, 8)); @@ -336,13 +322,17 @@ export class Long extends BSONValue { * @returns The corresponding Long value */ static fromStringStrict(str: string, unsigned?: boolean, radix?: number): Long { + if (str === 'NaN' || str === 'Infinity' || str === '+Infinity' || str === '-Infinity') + return Long.ZERO; + // remove leading zeros (for later string comparison and to make math faster) - const cleanedStr = removeLeadingZeros(str); - // doing this check outside of recursive function so cleanedStr value is consistent + const cleanedStr = StringUtils.removeLeadingZerosandExplicitPlus(str); + + // check roundtrip result const result = Long._fromString(cleanedStr, true, unsigned, radix); if (result.toString(radix).toLowerCase() !== cleanedStr.toLowerCase()) { throw new BSONError( - `Input: ${str} is not representable as ${result.unsigned ? 'an unsigned' : 'a signed'} 64-bit Long with radix: ${radix}` + `Input: ${str} is not representable as ${result.unsigned ? 'an unsigned' : 'a signed'} 64-bit Long ${radix != null ? `with radix: ${radix}` : ''}` ); } return result; @@ -356,7 +346,7 @@ export class Long extends BSONValue { * @returns The corresponding Long value */ static fromString(str: string, unsigned?: boolean, radix?: number): Long { - return Long._fromString(str, false, unsigned, radix); + return Long._fromString(str, true, unsigned, radix); } /** diff --git a/src/utils/string_utils.ts b/src/utils/string_utils.ts index 0e75d1d2e..0f9f9d06b 100644 --- a/src/utils/string_utils.ts +++ b/src/utils/string_utils.ts @@ -1,11 +1,25 @@ /** * @internal - * Removes leading zeros from textual representation of a number. + * Removes leading zeros and explicit plus from textual representation of a number. */ -export function removeLeadingZeros(str: string): string { - return !/[^0]+/.test(str) - ? str.replace(/^0+/, '0') // all zeros case +export function removeLeadingZerosandExplicitPlus(str: string): string { + return !/[^+?0]+/.test(str) + ? str.replace(/^\+?0+/, '0') // all zeros case (remove explicit plus if it exists) : str[0] === '-' ? str.replace(/^-0+/, '-') // negative number with leading zeros - : str.replace(/^\+?0+/, ''); + : str.replace(/^\+?0*/, ''); // remove explicit plus +} + +/** + * @internal + * Returns false for an string that contains invalid characters for its radix, else returns the original string. + * @param str - The textual representation of the Long + * @param radix - The radix in which the text is written (2-36), defaults to 10 + */ +export function validateStringCharacters(str: string, radix?: number): false | string { + radix = radix ?? 10; + const validCharacters = '0123456789abcdefghijklmnopqrstuvwxyz'.slice(0, radix); + // regex is case insensitive and checks that each character within the string is one of the validCharacters + const regex = new RegExp(`[^-+${validCharacters}]`, 'i'); + return regex.test(str) ? false : str; } diff --git a/test/node/int_32_tests.js b/test/node/int_32_tests.js index 44f2b7440..8e3fab140 100644 --- a/test/node/int_32_tests.js +++ b/test/node/int_32_tests.js @@ -108,7 +108,9 @@ describe('Int32', function () { ['a string with zero with leading zeros', '000000', 0], ['a string with positive leading zeros', '000000867', 867], ['a string with explicity positive leading zeros', '+000000867', 867], - ['a string with negative leading zeros', '-00007', -7] + ['a string with negative leading zeros', '-00007', -7], + ['a string with explicit positive zeros', '+000000', 0], + ['a string explicit positive no leading zeros', '+32', 32] ]; const errorInputs = [ ['Int32.max + 1', '2147483648', 'larger than the maximum value for Int32'], diff --git a/test/node/long.test.ts b/test/node/long.test.ts index ab355e231..488261585 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -189,8 +189,13 @@ describe('Long', function () { ['min signed binary input', Long.MIN_VALUE.toString(2), false, 2], ['min signed decimal input', Long.MIN_VALUE.toString(10), false, 10], ['min signed hex input', Long.MIN_VALUE.toString(16), false, 16], - ['signed zero', '0', false, 10], - ['unsigned zero', '0', true, 10] + ['signed zeros', '+000000', false, 10, '0'], + ['unsigned zero', '0', true, 10], + ['explicit positive no leading zeros', '+32', true, 10, '32'], + ['Infinity', 'Infinity', false, 21, '0'], + ['-Infinity', '-Infinity', false, 13, '0'], + ['+Infinity', '+Infinity', false, 13, '0'], + ['NaN', 'NaN', false, 11, '0'] ]; const failureInputs: [name: string, input: string, unsigned: boolean, radix: number][] = [ @@ -208,12 +213,17 @@ describe('Long', function () { ['under min signed binary input', Long.MIN_VALUE.toString(2) + '1', false, 2], ['under min signed decimal input', Long.MIN_VALUE.toString(10) + '1', false, 10], ['under min signed hex input', Long.MIN_VALUE.toString(16) + '1', false, 16], - ['string with whitespace', ' 3503a ', false, 11] + ['string with whitespace', ' 3503a ', false, 11], + ['negative zero unsigned', '-0', true, 9], + ['negative zero signed', '-0', false, 13], + ['radix 1', '12', false, 1], + ['negative radix', '12', false, -4], + ['radix over 36', '12', false, 37] ]; for (const [testName, str, unsigned, radix, expectedStr] of successInputs) { context(`when the input is ${testName}`, () => { - it(`should return a Long represenation of the input`, () => { + it(`should return a Long representation of the input`, () => { expect(Long.fromStringStrict(str, unsigned, radix).toString(radix)).to.equal( expectedStr ?? str.toLowerCase() ); @@ -228,36 +238,4 @@ describe('Long', function () { }); } }); - - describe('static validateStringCharacters()', function () { - const successInputs = [ - ['radix does allows given alphabet letter', 'eEe', 15], - ['empty string', '', 2], - ['all possible hexadecimal characters', '12efabc689873dADCDEF', 16], - ['leading zeros', '0000000004567e', 16] - ]; - - const failureInputs = [ - ['multiple decimal points', '..', 30], - ['non a-z or numeric string', '~~', 36], - ['alphabet in radix < 10', 'a', 4], - ['radix does not allow all alphabet letters', 'eee', 14] - ]; - - for (const [testName, str, radix] of successInputs) { - context(`when the input is ${testName}`, () => { - it(`should return a input string`, () => { - expect(Long.validateStringCharacters(str, radix)).to.equal(str); - }); - }); - } - - for (const [testName, str, radix] of failureInputs) { - context(`when the input is ${testName}`, () => { - it(`should return false`, () => { - expect(Long.validateStringCharacters(str, radix)).to.equal(false); - }); - }); - } - }); }); diff --git a/test/node/utils/string_utils.test.ts b/test/node/utils/string_utils.test.ts new file mode 100644 index 000000000..c0ae0f6ad --- /dev/null +++ b/test/node/utils/string_utils.test.ts @@ -0,0 +1,54 @@ +import * as StringUtils from '../../../src/utils/string_utils'; + +describe('removeLeadingZerosandExplicitPlus()', function () { + const inputs: [testName: string, str: string, expectedStr: string][] = [ + ['a string with zero with leading zeros', '000000', '0'], + ['a string with positive leading zeros', '000000867', '867'], + ['a string with explicity positive leading zeros', '+000000867', '867'], + ['a string with negative leading zeros', '-00007', '-7'], + ['a string with explicit positive zeros', '+000000', '0'], + ['a string explicit positive no leading zeros', '+32', '32'], + ['a string explicit positive no leading zeros and letters', '+ab00', 'ab00'] + ]; + + for (const [testName, str, expectedStr] of inputs) { + context(`when the input is ${testName}`, () => { + it(`should return a input string`, () => { + expect(StringUtils.removeLeadingZerosandExplicitPlus(str)).to.equal(expectedStr); + }); + }); + } +}); + +describe('validateStringCharacters()', function () { + const successInputs: [testName: string, str: string, radix: number][] = [ + ['radix does allows given alphabet letter', 'eEe', 15], + ['empty string', '', 2], + ['all possible hexadecimal characters', '12efabc689873dADCDEF', 16], + ['leading zeros', '0000000004567e', 16], + ['explicit positive no leading zeros', '+32', 10] + ]; + + const failureInputs = [ + ['multiple decimal points', '..', 30], + ['non a-z or numeric string', '~~', 36], + ['alphabet in radix < 10', 'a', 4], + ['radix does not allow all alphabet letters', 'eee', 14] + ]; + + for (const [testName, str, radix] of successInputs) { + context(`when the input is ${testName}`, () => { + it(`should return a input string`, () => { + expect(StringUtils.validateStringCharacters(str, radix)).to.equal(str); + }); + }); + } + + for (const [testName, str, radix] of failureInputs) { + context(`when the input is ${testName}`, () => { + it(`should return false`, () => { + expect(StringUtils.validateStringCharacters(str, radix)).to.equal(false); + }); + }); + } +}); From 70c6162faae62f30a7f71ff38a91cc1a833a6956 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Wed, 24 Apr 2024 16:26:02 -0400 Subject: [PATCH 13/20] add argument parsing check within fromStringStrict --- src/long.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/long.ts b/src/long.ts index 3e49776b5..e10b2232d 100644 --- a/src/long.ts +++ b/src/long.ts @@ -325,6 +325,13 @@ export class Long extends BSONValue { if (str === 'NaN' || str === 'Infinity' || str === '+Infinity' || str === '-Infinity') return Long.ZERO; + if (typeof unsigned === 'number') { + // For goog.math.long compatibility + (radix = unsigned), (unsigned = false); + } else { + unsigned = !!unsigned; + } + // remove leading zeros (for later string comparison and to make math faster) const cleanedStr = StringUtils.removeLeadingZerosandExplicitPlus(str); From 1d261c50c360706931f87fb9f020eb4b12050e6f Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Wed, 24 Apr 2024 16:45:55 -0400 Subject: [PATCH 14/20] removed extraneous functionality from _fromString and moved it to fromStringStrict only --- src/long.ts | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/long.ts b/src/long.ts index e10b2232d..19ba11e66 100644 --- a/src/long.ts +++ b/src/long.ts @@ -258,36 +258,17 @@ export class Long extends BSONValue { * @param radix - The radix in which the text is written (2-36), defaults to 10 * @returns The corresponding Long value */ - private static _fromString( - str: string, - validateStringCharacters: boolean, - unsigned?: boolean, - radix?: number - ): Long { + private static _fromString(str: string, unsigned: boolean, radix: number): Long { if (str.length === 0) throw new BSONError('empty string'); if (str === 'NaN' || str === 'Infinity' || str === '+Infinity' || str === '-Infinity') return Long.ZERO; - if (typeof unsigned === 'number') { - // For goog.math.long compatibility - (radix = unsigned), (unsigned = false); - } else { - unsigned = !!unsigned; - } - radix = radix || 10; if (radix < 2 || 36 < radix) throw new BSONError('radix'); let p; if ((p = str.indexOf('-')) > 0) throw new BSONError('interior hyphen'); else if (p === 0) { - return Long._fromString(str.substring(1), validateStringCharacters, unsigned, radix).neg(); - } - - if (str.trim() !== str) { - throw new BSONError(`Input: '${str}' contains leading and/or trailing whitespace`); - } - if (!StringUtils.validateStringCharacters(str, radix)) { - throw new BSONError(`Input: '${str}' contains invalid characters for radix: ${radix}`); + return Long._fromString(str.substring(1), unsigned, radix).neg(); } // Do several (8) digits each time through the loop, so as to @@ -331,12 +312,20 @@ export class Long extends BSONValue { } else { unsigned = !!unsigned; } + radix = radix || 10; + + if (str.trim() !== str) { + throw new BSONError(`Input: '${str}' contains leading and/or trailing whitespace`); + } + if (!StringUtils.validateStringCharacters(str, radix)) { + throw new BSONError(`Input: '${str}' contains invalid characters for radix: ${radix}`); + } // remove leading zeros (for later string comparison and to make math faster) const cleanedStr = StringUtils.removeLeadingZerosandExplicitPlus(str); // check roundtrip result - const result = Long._fromString(cleanedStr, true, unsigned, radix); + const result = Long._fromString(cleanedStr, unsigned, radix); if (result.toString(radix).toLowerCase() !== cleanedStr.toLowerCase()) { throw new BSONError( `Input: ${str} is not representable as ${result.unsigned ? 'an unsigned' : 'a signed'} 64-bit Long ${radix != null ? `with radix: ${radix}` : ''}` @@ -353,7 +342,14 @@ export class Long extends BSONValue { * @returns The corresponding Long value */ static fromString(str: string, unsigned?: boolean, radix?: number): Long { - return Long._fromString(str, true, unsigned, radix); + if (typeof unsigned === 'number') { + // For goog.math.long compatibility + (radix = unsigned), (unsigned = false); + } else { + unsigned = !!unsigned; + } + radix = radix || 10; + return Long._fromString(str, unsigned, radix); } /** From 60b2272e4f35165ab92f2be66444b4a565684e0b Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Wed, 24 Apr 2024 16:46:27 -0400 Subject: [PATCH 15/20] remove unneeded param from _fromString --- src/long.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/long.ts b/src/long.ts index 19ba11e66..338fb139d 100644 --- a/src/long.ts +++ b/src/long.ts @@ -253,7 +253,6 @@ export class Long extends BSONValue { * - the string contains invalid characters for the given radix * - the string contains whitespace * @param str - The textual representation of the Long - * @param validateStringCharacters - Whether or not invalid characters should throw an error * @param unsigned - Whether unsigned or not, defaults to signed * @param radix - The radix in which the text is written (2-36), defaults to 10 * @returns The corresponding Long value From 9f308aa25ba5e3fef0e235b2c9b7d00588d7ace5 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 25 Apr 2024 15:54:11 -0400 Subject: [PATCH 16/20] requested changes: alter support for infinity/nan cases and add explicit overloads with different docs --- src/long.ts | 97 +++++++++++++++++++++++++++++++++++------- test/node/long.test.ts | 61 ++++++++++++++++++++++---- 2 files changed, 134 insertions(+), 24 deletions(-) diff --git a/src/long.ts b/src/long.ts index 338fb139d..bb9ab5df7 100644 --- a/src/long.ts +++ b/src/long.ts @@ -259,9 +259,6 @@ export class Long extends BSONValue { */ private static _fromString(str: string, unsigned: boolean, radix: number): Long { if (str.length === 0) throw new BSONError('empty string'); - if (str === 'NaN' || str === 'Infinity' || str === '+Infinity' || str === '-Infinity') - return Long.ZERO; - if (radix < 2 || 36 < radix) throw new BSONError('radix'); let p; @@ -290,28 +287,67 @@ export class Long extends BSONValue { return result; } + /** + * Returns a signed Long representation of the given string, written using radix 10. + * Will throw an error if the given text is not exactly representable as a Long. + * Throws an error if any of the following conditions are true: + * - the string contains invalid characters for the radix 10 + * - the string contains whitespace + * - the value the string represents is too large or too small to be a Long + * Unlike Long.fromString, this method does not coerce '+/-Infinity' and 'NaN' to Long.Zero + * @param str - The textual representation of the Long + * @returns The corresponding Long value + */ + static fromStringStrict(str: string): Long; + /** + * Returns a Long representation of the given string, written using the radix 10. + * Will throw an error if the given parameters are not exactly representable as a Long. + * Throws an error if any of the following conditions are true: + * - the string contains invalid characters for the given radix + * - the string contains whitespace + * - the value the string represents is too large or too small to be a Long + * Unlike Long.fromString, this method does not coerce '+/-Infinity' and 'NaN' to Long.Zero + * @param str - The textual representation of the Long + * @param unsigned - Whether unsigned or not, defaults to signed + * @returns The corresponding Long value + */ + static fromStringStrict(str: string, unsigned?: boolean): Long; + /** + * Returns a signed Long representation of the given string, written using the specified radix. + * Will throw an error if the given parameters are not exactly representable as a Long. + * Throws an error if any of the following conditions are true: + * - the string contains invalid characters for the given radix + * - the string contains whitespace + * - the value the string represents is too large or too small to be a Long + * Unlike Long.fromString, this method does not coerce '+/-Infinity' and 'NaN' to Long.Zero + * @param str - The textual representation of the Long + * @param radix - The radix in which the text is written (2-36), defaults to 10 + * @returns The corresponding Long value + */ + static fromStringStrict(str: string, radix?: boolean): Long; /** * Returns a Long representation of the given string, written using the specified radix. + * Will throw an error if the given parameters are not exactly representable as a Long. * Throws an error if any of the following conditions are true: * - the string contains invalid characters for the given radix * - the string contains whitespace * - the value the string represents is too large or too small to be a Long + * Unlike Long.fromString, this method does not coerce '+/-Infinity' and 'NaN' to Long.Zero * @param str - The textual representation of the Long * @param unsigned - Whether unsigned or not, defaults to signed * @param radix - The radix in which the text is written (2-36), defaults to 10 * @returns The corresponding Long value */ - static fromStringStrict(str: string, unsigned?: boolean, radix?: number): Long { - if (str === 'NaN' || str === 'Infinity' || str === '+Infinity' || str === '-Infinity') - return Long.ZERO; - - if (typeof unsigned === 'number') { + static fromStringStrict(str: string, unsigned?: boolean, radix?: number): Long; + static fromStringStrict(str: string, unsignedOrRadix?: boolean | number, radix?: number): Long { + let unsigned = false; + if (typeof unsignedOrRadix === 'number') { // For goog.math.long compatibility - (radix = unsigned), (unsigned = false); + (radix = unsignedOrRadix), (unsignedOrRadix = false); } else { - unsigned = !!unsigned; + unsigned = !!unsignedOrRadix; } - radix = radix || 10; + radix ??= 10; if (str.trim() !== str) { throw new BSONError(`Input: '${str}' contains leading and/or trailing whitespace`); @@ -333,6 +369,26 @@ export class Long extends BSONValue { return result; } + /** + * Returns a signed Long representation of the given string, written using radix 10. + * @param str - The textual representation of the Long + * @returns The corresponding Long value + */ + static fromString(str: string): Long; + /** + * Returns a signed Long representation of the given string, written using radix 10. + * @param str - The textual representation of the Long + * @param radix - The radix in which the text is written (2-36), defaults to 10 + * @returns The corresponding Long value + */ + static fromString(str: string, radix?: number): Long; + /** + * Returns a Long representation of the given string, written using radix 10. + * @param str - The textual representation of the Long + * @param unsigned - Whether unsigned or not, defaults to signed + * @returns The corresponding Long value + */ + static fromString(str: string, unsigned?: boolean): Long; /** * Returns a Long representation of the given string, written using the specified radix. * @param str - The textual representation of the Long @@ -340,14 +396,23 @@ export class Long extends BSONValue { * @param radix - The radix in which the text is written (2-36), defaults to 10 * @returns The corresponding Long value */ - static fromString(str: string, unsigned?: boolean, radix?: number): Long { - if (typeof unsigned === 'number') { + static fromString(str: string, unsigned?: boolean, radix?: number): Long; + static fromString(str: string, unsignedOrRadix?: boolean | number, radix?: number): Long { + let unsigned = false; + if (typeof unsignedOrRadix === 'number') { // For goog.math.long compatibility - (radix = unsigned), (unsigned = false); + (radix = unsignedOrRadix), (unsignedOrRadix = false); } else { - unsigned = !!unsigned; + unsigned = !!unsignedOrRadix; + } + radix ??= 10; + if (str === 'NaN' && radix < 24) { + // radix does not support n, so coerce to zero + return Long.ZERO; + } else if ((str === 'Infinity' || str === '+Infinity' || str === '-Infinity') && radix < 35) { + // radix does not support y, so coerce to zero + return Long.ZERO; } - radix = radix || 10; return Long._fromString(str, unsigned, radix); } diff --git a/test/node/long.test.ts b/test/node/long.test.ts index 488261585..00377de60 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -164,12 +164,41 @@ describe('Long', function () { }); }); + describe('static fromString()', function () { + const successInputs: [ + name: string, + input: string, + unsigned: boolean | undefined, + radix: number | undefined, + expectedStr?: string + ][] = [ + ['radix 36 Infinity', 'Infinity', false, 36], + ['radix 36 -Infinity', '-Infinity', false, 36], + ['radix 36 +Infinity', '+Infinity', false, 36, 'infinity'], + ['radix < 35 Infinity', 'Infinity', false, 34, '0'], + ['radix < 35 -Infinity', '-Infinity', false, 23, '0'], + ['radix < 35 +Infinity', '+Infinity', false, 12, '0'], + ['radix < 24 NaN', 'NaN', false, 16, '0'], + ['radix > 24 NaN', 'NaN', false, 25] + ]; + + for (const [testName, str, unsigned, radix, expectedStr] of successInputs) { + context(`when the input is ${testName}`, () => { + it(`should return a Long representation of the input`, () => { + expect(Long.fromString(str, unsigned, radix).toString(radix)).to.equal( + expectedStr ?? str.toLowerCase() + ); + }); + }); + } + }); + describe('static fromStringStrict()', function () { const successInputs: [ name: string, input: string, - unsigned: boolean, - radix: number, + unsigned: boolean | undefined, + radix: number | undefined, expectedStr?: string ][] = [ ['basic no alphabet low radix', '1236', true, 8], @@ -192,13 +221,22 @@ describe('Long', function () { ['signed zeros', '+000000', false, 10, '0'], ['unsigned zero', '0', true, 10], ['explicit positive no leading zeros', '+32', true, 10, '32'], - ['Infinity', 'Infinity', false, 21, '0'], - ['-Infinity', '-Infinity', false, 13, '0'], - ['+Infinity', '+Infinity', false, 13, '0'], - ['NaN', 'NaN', false, 11, '0'] + // the following inputs are valid radix 36 inputs, but will not represent NaN or +/- Infinity + ['radix 36 Infinity', 'Infinity', false, 36], + ['radix 36 -Infinity', '-Infinity', false, 36], + ['radix 36 +Infinity', '+Infinity', false, 36, 'infinity'], + ['radix 36 NaN', 'NaN', false, 36], + ['overload no unsigned and no radix parameter', '-32', undefined, undefined], + ['overload no unsigned parameter', '-32', undefined, 12], + ['overload no radix parameter', '32', true, undefined] ]; - const failureInputs: [name: string, input: string, unsigned: boolean, radix: number][] = [ + const failureInputs: [ + name: string, + input: string, + unsigned: boolean | undefined, + radix: number | undefined + ][] = [ ['empty string', '', true, 2], ['invalid numbers in binary string', '234', true, 2], ['non a-z or numeric string', '~~', true, 36], @@ -218,7 +256,14 @@ describe('Long', function () { ['negative zero signed', '-0', false, 13], ['radix 1', '12', false, 1], ['negative radix', '12', false, -4], - ['radix over 36', '12', false, 37] + ['radix over 36', '12', false, 37], + // the following inputs are invalid radix 16 inputs + // this is because of the characters, not because of the values they commonly represent + ['radix 10 Infinity', 'Infinity', false, 10], + ['radix 10 -Infinity', '-Infinity', false, 10], + ['radix 10 +Infinity', '+Infinity', false, 10], + ['radix 10 NaN', 'NaN', false, 10], + ['overload no radix parameter and invalid sign', '-32', true, undefined] ]; for (const [testName, str, unsigned, radix, expectedStr] of successInputs) { From 63beeeacddf7e71c9fc815c1045de15127e3a2cf Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 25 Apr 2024 17:52:22 -0400 Subject: [PATCH 17/20] optimize removeLeadingZerosAndExplicitPlus function and fix name typo --- src/int_32.ts | 4 ++-- src/long.ts | 2 +- src/utils/string_utils.ts | 32 ++++++++++++++++++++++------ test/node/utils/string_utils.test.ts | 5 +++-- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/int_32.ts b/src/int_32.ts index afcca018f..7c95027ce 100644 --- a/src/int_32.ts +++ b/src/int_32.ts @@ -3,7 +3,7 @@ import { BSON_INT32_MAX, BSON_INT32_MIN } from './constants'; import { BSONError } from './error'; import type { EJSONOptions } from './extended_json'; import { type InspectFn, defaultInspect } from './parser/utils'; -import { removeLeadingZerosandExplicitPlus } from './utils/string_utils'; +import { removeLeadingZerosAndExplicitPlus } from './utils/string_utils'; /** @public */ export interface Int32Extended { @@ -49,7 +49,7 @@ export class Int32 extends BSONValue { * @param value - the string we want to represent as an int32. */ static fromString(value: string): Int32 { - const cleanedValue = removeLeadingZerosandExplicitPlus(value); + const cleanedValue = removeLeadingZerosAndExplicitPlus(value); const coercedValue = Number(value); diff --git a/src/long.ts b/src/long.ts index bb9ab5df7..da2506289 100644 --- a/src/long.ts +++ b/src/long.ts @@ -357,7 +357,7 @@ export class Long extends BSONValue { } // remove leading zeros (for later string comparison and to make math faster) - const cleanedStr = StringUtils.removeLeadingZerosandExplicitPlus(str); + const cleanedStr = StringUtils.removeLeadingZerosAndExplicitPlus(str); // check roundtrip result const result = Long._fromString(cleanedStr, unsigned, radix); diff --git a/src/utils/string_utils.ts b/src/utils/string_utils.ts index 0f9f9d06b..4d77bf3ba 100644 --- a/src/utils/string_utils.ts +++ b/src/utils/string_utils.ts @@ -2,12 +2,32 @@ * @internal * Removes leading zeros and explicit plus from textual representation of a number. */ -export function removeLeadingZerosandExplicitPlus(str: string): string { - return !/[^+?0]+/.test(str) - ? str.replace(/^\+?0+/, '0') // all zeros case (remove explicit plus if it exists) - : str[0] === '-' - ? str.replace(/^-0+/, '-') // negative number with leading zeros - : str.replace(/^\+?0*/, ''); // remove explicit plus +export function removeLeadingZerosAndExplicitPlus(str: string): string { + if (str === '') { + return str; + } + + let startIndex = 0; + + const isNegative = str[startIndex] === '-'; + const isExplicitlyPositive = str[startIndex] === '+'; + + if (isExplicitlyPositive || isNegative) { + startIndex += 1; + } + + let foundInsignificantZero = false; + + while (str[startIndex] === '0') { + foundInsignificantZero = true; + startIndex += 1; + } + + if (!foundInsignificantZero) { + return isExplicitlyPositive ? str.slice(1) : str; + } + + return `${isNegative ? '-' : ''}${str.length === startIndex ? '0' : str.slice(startIndex)}`; } /** diff --git a/test/node/utils/string_utils.test.ts b/test/node/utils/string_utils.test.ts index c0ae0f6ad..73b05c7aa 100644 --- a/test/node/utils/string_utils.test.ts +++ b/test/node/utils/string_utils.test.ts @@ -1,6 +1,7 @@ +import { expect } from 'chai'; import * as StringUtils from '../../../src/utils/string_utils'; -describe('removeLeadingZerosandExplicitPlus()', function () { +describe('removeLeadingZerosAndExplicitPlus()', function () { const inputs: [testName: string, str: string, expectedStr: string][] = [ ['a string with zero with leading zeros', '000000', '0'], ['a string with positive leading zeros', '000000867', '867'], @@ -14,7 +15,7 @@ describe('removeLeadingZerosandExplicitPlus()', function () { for (const [testName, str, expectedStr] of inputs) { context(`when the input is ${testName}`, () => { it(`should return a input string`, () => { - expect(StringUtils.removeLeadingZerosandExplicitPlus(str)).to.equal(expectedStr); + expect(StringUtils.removeLeadingZerosAndExplicitPlus(str)).to.equal(expectedStr); }); }); } From d212f7e879f6033135e65cfcbb345608ce2f706e Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 29 Apr 2024 15:51:53 -0400 Subject: [PATCH 18/20] requested changes - check str length in removeLeadingZerosAndExplicitPlus --- src/utils/string_utils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/utils/string_utils.ts b/src/utils/string_utils.ts index 4d77bf3ba..1ffb118e9 100644 --- a/src/utils/string_utils.ts +++ b/src/utils/string_utils.ts @@ -18,9 +18,8 @@ export function removeLeadingZerosAndExplicitPlus(str: string): string { let foundInsignificantZero = false; - while (str[startIndex] === '0') { + for (; startIndex < str.length && str[startIndex] === '0'; ++startIndex) { foundInsignificantZero = true; - startIndex += 1; } if (!foundInsignificantZero) { From c98b513d79e0bb08ad000142f7bfadf10882e184 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 29 Apr 2024 17:00:20 -0400 Subject: [PATCH 19/20] remove fromString fix --- src/long.ts | 6 +----- test/node/long.test.ts | 12 ++++-------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/long.ts b/src/long.ts index da2506289..fc3c69460 100644 --- a/src/long.ts +++ b/src/long.ts @@ -406,11 +406,7 @@ export class Long extends BSONValue { unsigned = !!unsignedOrRadix; } radix ??= 10; - if (str === 'NaN' && radix < 24) { - // radix does not support n, so coerce to zero - return Long.ZERO; - } else if ((str === 'Infinity' || str === '+Infinity' || str === '-Infinity') && radix < 35) { - // radix does not support y, so coerce to zero + if (str === 'NaN' || str === 'Infinity' || str === '+Infinity' || str === '-Infinity') { return Long.ZERO; } return Long._fromString(str, unsigned, radix); diff --git a/test/node/long.test.ts b/test/node/long.test.ts index 00377de60..f8b52f53b 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -172,14 +172,10 @@ describe('Long', function () { radix: number | undefined, expectedStr?: string ][] = [ - ['radix 36 Infinity', 'Infinity', false, 36], - ['radix 36 -Infinity', '-Infinity', false, 36], - ['radix 36 +Infinity', '+Infinity', false, 36, 'infinity'], - ['radix < 35 Infinity', 'Infinity', false, 34, '0'], - ['radix < 35 -Infinity', '-Infinity', false, 23, '0'], - ['radix < 35 +Infinity', '+Infinity', false, 12, '0'], - ['radix < 24 NaN', 'NaN', false, 16, '0'], - ['radix > 24 NaN', 'NaN', false, 25] + ['Infinity', 'Infinity', false, 34, '0'], + ['-Infinity', '-Infinity', false, 23, '0'], + ['+Infinity', '+Infinity', false, 12, '0'], + ['NaN', 'NaN', false, 16, '0'], ]; for (const [testName, str, unsigned, radix, expectedStr] of successInputs) { From 152db8fe9aee5e15a60f2529ef15cd520c769ebc Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 29 Apr 2024 17:03:48 -0400 Subject: [PATCH 20/20] lint fix --- test/node/long.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/node/long.test.ts b/test/node/long.test.ts index f8b52f53b..32bf7ebb9 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -175,7 +175,7 @@ describe('Long', function () { ['Infinity', 'Infinity', false, 34, '0'], ['-Infinity', '-Infinity', false, 23, '0'], ['+Infinity', '+Infinity', false, 12, '0'], - ['NaN', 'NaN', false, 16, '0'], + ['NaN', 'NaN', false, 16, '0'] ]; for (const [testName, str, unsigned, radix, expectedStr] of successInputs) {