-
Notifications
You must be signed in to change notification settings - Fork 915
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implements a JSON.parse that transforms unsafe numbers to bigints
- Loading branch information
1 parent
04942c9
commit addd8e9
Showing
6 changed files
with
124 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
packages/rpc-transport-http/src/__tests__/json-parse-with-bigint-test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { jsonParseWithLargeIntegersAsBigInts, wrapLargeIntegers } from '../json-parse-with-bigint'; | ||
|
||
describe('jsonParseWithLargeNumbersAsBigInts', () => { | ||
it('parsed large integers as bigints', () => { | ||
const value = BigInt(Number.MAX_SAFE_INTEGER) + 1n; | ||
const result = jsonParseWithLargeIntegersAsBigInts(`{"value":${value}}`) as { value: unknown }; | ||
expect(result).toEqual({ value }); | ||
expect(typeof result.value).toBe('bigint'); | ||
}); | ||
it('keeps safe integers as numbers', () => { | ||
const value = Number.MAX_SAFE_INTEGER; | ||
const result = jsonParseWithLargeIntegersAsBigInts(`{"value":${value}}`) as { value: unknown }; | ||
expect(result).toEqual({ value }); | ||
expect(typeof result.value).toBe('number'); | ||
}); | ||
it('does not affect numbers in strings', () => { | ||
expect(jsonParseWithLargeIntegersAsBigInts(`{"value":"Hello 123 World"}`)).toEqual({ | ||
value: 'Hello 123 World', | ||
}); | ||
}); | ||
it('does not affect numbers in strings with escaped double quotes', () => { | ||
expect(jsonParseWithLargeIntegersAsBigInts(`{"value":"Hello\\" 123 World"}`)).toEqual({ | ||
value: 'Hello" 123 World', | ||
}); | ||
}); | ||
}); | ||
|
||
describe('wrapLargeIntegers', () => { | ||
it('wraps large integers as bigint strings', () => { | ||
const value = BigInt(Number.MAX_SAFE_INTEGER) + 1n; | ||
expect(wrapLargeIntegers(`{"value":${value}}`)).toBe(`{"value":{"__brand":"bigint","value":"${value}"}}`); | ||
}); | ||
it('keeps safe integers as numbers', () => { | ||
const value = Number.MAX_SAFE_INTEGER; | ||
expect(wrapLargeIntegers(`{"value":${value}}`)).toBe(`{"value":${value}}`); | ||
}); | ||
it('does not wrap numbers in strings', () => { | ||
expect(wrapLargeIntegers(`{"value":"Hello 123 World"}`)).toBe(`{"value":"Hello 123 World"}`); | ||
}); | ||
it('does not wrap numbers in strings with escaped double quotes', () => { | ||
expect(wrapLargeIntegers(`{"value":"Hello\\" 123 World"}`)).toBe(`{"value":"Hello\\" 123 World"}`); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
/** | ||
* This function is a replacement for `JSON.parse` that can handle large integers by | ||
* parsing them as BigInts. It will only transform integers that are too large to be | ||
* represented as a regular JavaScript number. | ||
*/ | ||
export function jsonParseWithLargeIntegersAsBigInts(json: string): unknown { | ||
return JSON.parse(wrapLargeIntegers(json), (_, value) => { | ||
return isWrappedBigInt(value) ? unwrapBigInt(value) : value; | ||
}); | ||
} | ||
|
||
/** | ||
* This function takes a JSON string and wraps any large, unsafe integers in a special | ||
* format that can be later recognized and unwrapped by a `JSON.parse` reviver. | ||
*/ | ||
export function wrapLargeIntegers(json: string): string { | ||
return transformUnquotedSegments(json, unquotedSegment => { | ||
return unquotedSegment.replaceAll(/(-?\d+)/g, (_, value: string) => { | ||
return Number.isSafeInteger(Number(value)) ? value : wrapBigInt(value); | ||
}); | ||
}); | ||
} | ||
|
||
/** | ||
* This function takes a JSON string and transforms any unquoted segments using the provided | ||
* `transform` function. This means we can be sure that our transformations will never occur | ||
* inside a double quoted string — even if it contains escaped double quotes. | ||
*/ | ||
function transformUnquotedSegments(json: string, transform: (value: string) => string): string { | ||
/** | ||
* This regex matches any part of a JSON string that isn't wrapped in double quotes. | ||
* | ||
* For instance, in the string `{"age":42,"name":"Alice \"The\" 2nd"}`, it would the | ||
* following parts: `{`, `:42,`, `:`, `}`. Notice the whole "Alice \"The\" 2nd" string | ||
* is not matched as it is wrapped in double quotes and contains escaped double quotes. | ||
* | ||
* The regex is composed of two parts: | ||
* | ||
* 1. The first part `^([^"]+)` matches any character until we reach the first double quote. | ||
* 2. The second part `("(?:\\"|[^"])+")([^"]+)` matches any double quoted string that may | ||
* and any unquoted segment that follows it. To match a double quoted string, we use the | ||
* `(?:\\"|[^"])` regex to match any character that isn't a double quote whilst allowing | ||
* escaped double quotes. | ||
*/ | ||
const unquotedSegmentsRegex = /^([^"]+)|("(?:\\"|[^"])+")([^"]+)/g; | ||
|
||
return json.replaceAll(unquotedSegmentsRegex, (_, firstGroup, secondGroup, thirdGroup) => { | ||
// If the first group is matched, it means we are at the | ||
// beginning of the JSON string and we have an unquoted segment. | ||
if (firstGroup) return transform(firstGroup); | ||
|
||
// Otherwise, we have a double quoted string followed by an unquoted segment. | ||
return `${secondGroup}${transform(thirdGroup)}`; | ||
}); | ||
} | ||
|
||
type WrappedBigInt = { __brand: 'bigint'; value: string }; | ||
|
||
function wrapBigInt(value: string): string { | ||
return `{"__brand":"bigint","value":"${value}"}`; | ||
} | ||
|
||
function unwrapBigInt({ value }: WrappedBigInt): bigint { | ||
return BigInt(value); | ||
} | ||
|
||
function isWrappedBigInt(value: unknown): value is WrappedBigInt { | ||
return ( | ||
!!value && | ||
typeof value === 'object' && | ||
'__brand' in value && | ||
value.__brand === 'bigint' && | ||
'value' in value && | ||
typeof value.value === 'string' | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters