Skip to content

Commit

Permalink
feat: handle JSON decoding of large integers as BigInt
Browse files Browse the repository at this point in the history
For numbers that have a decimal point, always return as a plain
`number` type. Otherwise, for integers that are larger than
Number.MAX_SAFE_INTEGER or smaller than Number.MIN_SAFE_INTEGER,
return them as a `BigInt`.
  • Loading branch information
rvagg committed Aug 5, 2021
1 parent ed03df7 commit dc87eb4
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 4 deletions.
19 changes: 16 additions & 3 deletions lib/json/decode.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class Tokenizer {
parseNumber () {
const startPos = this.pos
let negative = false
let float = false

/**
* @param {number[]} chars
Expand All @@ -95,6 +96,7 @@ class Tokenizer {
this.pos++
if (this.ch() === 46) { // '.'
this.pos++
float = true
} else {
return new Token(Type.uint, 0, this.pos - startPos)
}
Expand All @@ -104,20 +106,31 @@ class Tokenizer {
throw new Error(`${decodeErrPrefix} unexpected token at position ${this.pos}`)
}
if (!this.done() && this.ch() === 46) { // '.'
if (float) {
throw new Error(`${decodeErrPrefix} unexpected token at position ${this.pos}`)
}
float = true
this.pos++
swallow([48, 49, 50, 51, 52, 53, 54, 55, 56, 57]) // DIGIT
}
if (!this.done() && (this.ch() === 101 || this.ch() === 69)) { // '[eE]'
float = true
this.pos++
if (!this.done() && (this.ch() === 43 || this.ch() === 45)) { // '+', '-'
this.pos++
}
swallow([48, 49, 50, 51, 52, 53, 54, 55, 56, 57]) // DIGIT
}
// TODO: check canonical form of this number?
// @ts-ignore
const float = parseFloat(String.fromCharCode.apply(null, this.data.subarray(startPos, this.pos)))
return new Token(Number.isInteger(float) ? float >= 0 ? Type.uint : Type.negint : Type.float, float, this.pos - startPos)
const numStr = String.fromCharCode.apply(null, this.data.subarray(startPos, this.pos))
const num = parseFloat(numStr)
if (float) {
return new Token(Type.float, num, this.pos - startPos)
}
if (Number.isSafeInteger(num)) {
return new Token(num >= 0 ? Type.uint : Type.negint, num, this.pos - startPos)
}
return new Token(num >= 0 ? Type.uint : Type.negint, BigInt(numStr), this.pos - startPos)
}

/**
Expand Down
3 changes: 2 additions & 1 deletion test/test-1negint.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const fixtures = [
// kind of hard to assert on these (TODO: improve bignum handling)
{ data: '3b001fffffffffffff', expected: BigInt('-9007199254740992') /* Number.MIN_SAFE_INTEGER - 1 */, type: 'negint64' },
{ data: '3b0020000000000000', expected: BigInt('-9007199254740993') /* Number.MIN_SAFE_INTEGER - 2 */, type: 'negint64' },
{ data: '3ba5f702b3a5f702b3', expected: BigInt('-11959030306112471732'), type: 'negint64' }
{ data: '3ba5f702b3a5f702b3', expected: BigInt('-11959030306112471732'), type: 'negint64' },
{ data: '3bffffffffffffffff', expected: BigInt('-18446744073709551616'), type: 'negint64' }
]

describe('negint', () => {
Expand Down
27 changes: 27 additions & 0 deletions test/test-json.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,32 @@ describe('json basics', () => {
verifyRoundTrip(1.0011111e-18)
})

it('handles large integers as BigInt', () => {
const verify = (inp, str) => {
if (str === undefined) {
str = String(inp)
}
assert.strictEqual(decode(toBytes(str)), inp)
}
verify(Number.MAX_SAFE_INTEGER)
verify(-Number.MAX_SAFE_INTEGER)
verify(BigInt('9007199254740992')) // Number.MAX_SAFE_INTEGER+1
verify(BigInt('9007199254740993'))
verify(BigInt('11959030306112471731'))
verify(BigInt('18446744073709551615')) // max uint64
verify(BigInt('9223372036854775807')) // max int64
verify(BigInt('-9007199254740992'))
verify(BigInt('-9007199254740993'))
verify(BigInt('-9223372036854776000')) // min int64
verify(BigInt('-11959030306112471732'))
verify(BigInt('-18446744073709551616')) // min -uint64

// these are "floats", distinct from "ints" which wouldn't have the `.` in them
verify(-9007199254740992, '-9007199254740992.0')
verify(-9223372036854776000, '-9223372036854776000.0')
verify(-18446744073709551616, '-18446744073709551616.0')
})

it('can round-trip string literals', () => {
const testCases = [
JSON.stringify(''),
Expand Down Expand Up @@ -154,5 +180,6 @@ describe('json basics', () => {
assert.throws(() => decode(toBytes('[tr]')), 'CBOR decode error: unexpected end of input at position 1')
assert.throws(() => decode(toBytes('{"v":flase}')), 'CBOR decode error: unexpected token at position 7, expected to find \'false\'')
assert.throws(() => decode(toBytes('[fa]')), 'CBOR decode error: unexpected end of input at position 1')
assert.throws(() => decode(toBytes('-0..1')), 'CBOR decode error: unexpected token at position 3')
})
})

0 comments on commit dc87eb4

Please sign in to comment.