Skip to content

Add stricter string validation to neo4j.int (#985) #987

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 46 additions & 6 deletions packages/core/src/integer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
Expand Down Expand Up @@ -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))
Expand All @@ -882,18 +890,20 @@ 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
}
if (typeof val === 'number') {
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())
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
122 changes: 122 additions & 0 deletions packages/core/test/integer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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))
Expand Down Expand Up @@ -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<I, O> {
input: I
expectedOutput: O
Expand Down