From 38d8b753e37f1463cbe5ce2bc993c7cee1fe740f Mon Sep 17 00:00:00 2001 From: Warren James Date: Tue, 18 Feb 2025 12:11:34 -0500 Subject: [PATCH 1/7] fix incorrect bigint handling --- src/utils/number_utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/number_utils.ts b/src/utils/number_utils.ts index 02f4dbeb..3e76b2f5 100644 --- a/src/utils/number_utils.ts +++ b/src/utils/number_utils.ts @@ -90,7 +90,8 @@ export const NumberUtils: NumberUtils = { eslint-disable-next-line no-restricted-globals -- This is allowed since this helper should not be called unless bigint features are enabled */ - return (BigInt(hi) << BigInt(32)) + BigInt(lo); + const intermediate = (BigInt(hi) << BigInt(32)) + BigInt(lo); + return BigInt.asIntN(64, intermediate); }, /** Reads a little-endian 64-bit float from source */ From 9686f2a633768fadb6467b10bf35fc3bc6b85109 Mon Sep 17 00:00:00 2001 From: Warren James Date: Tue, 18 Feb 2025 13:03:49 -0500 Subject: [PATCH 2/7] update bigint tests --- test/node/bigint.test.ts | 99 ++++++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 44 deletions(-) diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index 9532859f..07cc0dc7 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -3,14 +3,28 @@ import { bufferFromHexArray } from './tools/utils'; import { expect } from 'chai'; import { BSON_DATA_LONG } from '../../src/constants'; -describe('BSON BigInt support', function () { - beforeEach(function () { +describe('BSON BigInt support', function() { + beforeEach(function() { if (__noBigInt__) { this.currentTest?.skip(); } }); - describe('BSON.deserialize()', function () { + describe('BSON roundtripping', function() { + const numbers = [-(2n ** 63n), -1n, 0n, 1n, (2n ** 63n) - 1n]; + + for (const number of numbers) { + it(`correctly roundtrips ${number}`, function() { + const inputDoc = { number }; + const serializedDoc = BSON.serialize(inputDoc); + const outputDoc = BSON.deserialize(serializedDoc, { useBigInt64: true }); + + expect(outputDoc).to.deep.equal(inputDoc); + }); + } + }); + + describe('BSON.deserialize()', function() { type DeserialzationOptions = { useBigInt64: boolean | undefined; promoteValues: boolean | undefined; @@ -65,15 +79,12 @@ describe('BSON BigInt support', function () { function generateTestDescription(entry: TestTableEntry): string { const options = entry.options; - const promoteValues = `promoteValues ${ - options.promoteValues === undefined ? 'is default' : `is ${options.promoteValues}` - }`; - const promoteLongs = `promoteLongs ${ - options.promoteLongs === undefined ? 'is default' : `is ${options.promoteLongs}` - }`; - const useBigInt64 = `useBigInt64 ${ - options.useBigInt64 === undefined ? 'is default' : `is ${options.useBigInt64}` - }`; + const promoteValues = `promoteValues ${options.promoteValues === undefined ? 'is default' : `is ${options.promoteValues}` + }`; + const promoteLongs = `promoteLongs ${options.promoteLongs === undefined ? 'is default' : `is ${options.promoteLongs}` + }`; + const useBigInt64 = `useBigInt64 ${options.useBigInt64 === undefined ? 'is default' : `is ${options.useBigInt64}` + }`; const flagString = `${useBigInt64}, ${promoteValues}, and ${promoteLongs}`; if (entry.shouldThrow) { return `throws when ${flagString}`; @@ -107,7 +118,7 @@ describe('BSON BigInt support', function () { } }); - describe('BSON.serialize()', function () { + describe('BSON.serialize()', function() { // Index for the data type byte of a BSON document with a // NOTE: These offsets only apply for documents with the shape {a : } // where n is a BigInt @@ -147,13 +158,13 @@ describe('BSON BigInt support', function () { }; } - it('serializes bigints with the correct BSON type', function () { + it('serializes bigints with the correct BSON type', function() { const testDoc = { a: 0n }; const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); expect(serializedDoc.dataType).to.equal(BSON_DATA_LONG); }); - it('serializes bigints into little-endian byte order', function () { + it('serializes bigints into little-endian byte order', function() { const testDoc = { a: 0x1234567812345678n }; const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); const expectedResult = getSerializedDocParts( @@ -167,7 +178,7 @@ describe('BSON BigInt support', function () { expect(expectedResult.value).to.equal(serializedDoc.value); }); - it('serializes a BigInt that can be safely represented as a Number', function () { + it('serializes a BigInt that can be safely represented as a Number', function() { const testDoc = { a: 0x23n }; const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); const expectedResult = getSerializedDocParts( @@ -180,7 +191,7 @@ describe('BSON BigInt support', function () { expect(serializedDoc).to.deep.equal(expectedResult); }); - it('serializes a BigInt in the valid range [-2^63, 2^63 - 1]', function () { + it('serializes a BigInt in the valid range [-2^63, 2^63 - 1]', function() { const testDoc = { a: 0xfffffffffffffff1n }; const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); const expectedResult = getSerializedDocParts( @@ -193,7 +204,7 @@ describe('BSON BigInt support', function () { expect(serializedDoc).to.deep.equal(expectedResult); }); - it('wraps to negative on a BigInt that is larger than (2^63 -1)', function () { + it('wraps to negative on a BigInt that is larger than (2^63 -1)', function() { const maxIntPlusOne = { a: 2n ** 63n }; const serializedMaxIntPlusOne = getSerializedDocParts(BSON.serialize(maxIntPlusOne)); const expectedResultForMaxIntPlusOne = getSerializedDocParts( @@ -206,7 +217,7 @@ describe('BSON BigInt support', function () { expect(serializedMaxIntPlusOne).to.deep.equal(expectedResultForMaxIntPlusOne); }); - it('serializes BigInts at the edges of the valid range [-2^63, 2^63 - 1]', function () { + it('serializes BigInts at the edges of the valid range [-2^63, 2^63 - 1]', function() { const maxPositiveInt64 = { a: 2n ** 63n - 1n }; const serializedMaxPositiveInt64 = getSerializedDocParts(BSON.serialize(maxPositiveInt64)); const expectedSerializationForMaxPositiveInt64 = getSerializedDocParts( @@ -230,7 +241,7 @@ describe('BSON BigInt support', function () { expect(serializedMinPositiveInt64).to.deep.equal(expectedSerializationForMinPositiveInt64); }); - it('truncates a BigInt that is larger than a 64-bit int', function () { + it('truncates a BigInt that is larger than a 64-bit int', function() { const testDoc = { a: 2n ** 64n + 1n }; const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); const expectedSerialization = getSerializedDocParts( @@ -243,7 +254,7 @@ describe('BSON BigInt support', function () { expect(serializedDoc).to.deep.equal(expectedSerialization); }); - it('serializes array of BigInts', function () { + it('serializes array of BigInts', function() { const testArr = { a: [1n] }; const serializedArr = BSON.serialize(testArr); const expectedSerialization = bufferFromHexArray([ @@ -258,7 +269,7 @@ describe('BSON BigInt support', function () { expect(serializedArr).to.deep.equal(expectedSerialization); }); - it('serializes Map with BigInt values', function () { + it('serializes Map with BigInt values', function() { const testMap = new Map(); testMap.set('a', 1n); const serializedMap = getSerializedDocParts(BSON.serialize(testMap)); @@ -273,7 +284,7 @@ describe('BSON BigInt support', function () { }); }); - describe('EJSON.parse()', function () { + describe('EJSON.parse()', function() { type ParseOptions = { useBigInt64: boolean | undefined; relaxed: boolean | undefined; @@ -330,13 +341,13 @@ describe('BSON BigInt support', function () { const condDescription = generateConditionDescription(entry); const behaviourDescription = generateBehaviourDescription(entry, sampleString); - describe(condDescription, function () { + describe(condDescription, function() { it(behaviourDescription, test); }); } } - describe('canonical input', function () { + describe('canonical input', function() { const canonicalInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { return relaxedValues.flatMap(relaxed => { return genTestTable( @@ -359,7 +370,7 @@ describe('BSON BigInt support', function () { createTestsFromTestTable(canonicalInputTestTable, sampleCanonicalString); }); - describe('relaxed integer input', function () { + describe('relaxed integer input', function() { const relaxedIntegerInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { return relaxedValues.flatMap(relaxed => { return genTestTable( @@ -381,7 +392,7 @@ describe('BSON BigInt support', function () { createTestsFromTestTable(relaxedIntegerInputTestTable, sampleRelaxedIntegerString); }); - describe('relaxed double input where double is outside of int32 range and useBigInt64 is true', function () { + describe('relaxed double input where double is outside of int32 range and useBigInt64 is true', function() { const relaxedDoubleInputTestTable = relaxedValues.flatMap(relaxed => { return genTestTable(true, relaxed, (_, relaxedIsSet: boolean) => relaxedIsSet ? { a: 2147483647.9 } : { a: new BSON.Double(2147483647.9) } @@ -396,15 +407,15 @@ describe('BSON BigInt support', function () { }); }); - describe('EJSON.stringify()', function () { - context('canonical mode (relaxed=false)', function () { - it('truncates bigint values when they are outside the range [BSON_INT64_MIN, BSON_INT64_MAX]', function () { + describe('EJSON.stringify()', function() { + context('canonical mode (relaxed=false)', function() { + it('truncates bigint values when they are outside the range [BSON_INT64_MIN, BSON_INT64_MAX]', function() { const numbers = { a: 2n ** 64n + 1n, b: -(2n ** 64n) - 1n }; const serialized = EJSON.stringify(numbers, { relaxed: false }); expect(serialized).to.equal('{"a":{"$numberLong":"1"},"b":{"$numberLong":"-1"}}'); }); - it('truncates bigint values in the same way as BSON.serialize', function () { + it('truncates bigint values in the same way as BSON.serialize', function() { const number = { a: 0x1234_5678_1234_5678_9999n }; const stringified = EJSON.stringify(number, { relaxed: false }); const serialized = BSON.serialize(number); @@ -424,15 +435,15 @@ describe('BSON BigInt support', function () { expect(parsed.a.$numberLong).to.equal(serializedValue.toString()); }); - it('serializes bigint values to numberLong in canonical mode', function () { + it('serializes bigint values to numberLong in canonical mode', function() { const number = { a: 2n }; const serialized = EJSON.stringify(number, { relaxed: false }); expect(serialized).to.equal('{"a":{"$numberLong":"2"}}'); }); }); - context('relaxed mode (relaxed=true)', function () { - it('truncates bigint values in the same way as BSON.serialize', function () { + context('relaxed mode (relaxed=true)', function() { + it('truncates bigint values in the same way as BSON.serialize', function() { const number = { a: 0x1234_0000_1234_5678_9999n }; // Ensure that the truncated number can be exactly represented as a JS number const stringified = EJSON.stringify(number, { relaxed: true }); const serializedDoc = BSON.serialize(number); @@ -451,23 +462,23 @@ describe('BSON BigInt support', function () { expect(parsed.a).to.equal(Number(dataView.getBigInt64(VALUE_OFFSET, true))); }); - it('serializes bigint values to Number', function () { + it('serializes bigint values to Number', function() { const number = { a: 10000n }; const serialized = EJSON.stringify(number, { relaxed: true }); expect(serialized).to.equal('{"a":10000}'); }); - it('loses precision when serializing bigint values outside of range [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]', function () { + it('loses precision when serializing bigint values outside of range [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]', function() { const numbers = { a: -(2n ** 53n) - 1n, b: 2n ** 53n + 2n }; const serialized = EJSON.stringify(numbers, { relaxed: true }); expect(serialized).to.equal('{"a":-9007199254740992,"b":9007199254740994}'); }); }); - context('when passed bigint values that are 64 bits wide or less', function () { + context('when passed bigint values that are 64 bits wide or less', function() { let parsed; - before(function () { + before(function() { if (__noBigInt__) { return; } @@ -476,20 +487,20 @@ describe('BSON BigInt support', function () { parsed = JSON.parse(serialized); }); - it('passes loose equality checks with native bigint values', function () { + it('passes loose equality checks with native bigint values', function() { // eslint-disable-next-line eqeqeq expect(parsed.a.$numberLong == 12345n).true; }); - it('equals the result of BigInt.toString', function () { + it('equals the result of BigInt.toString', function() { expect(parsed.a.$numberLong).to.equal(12345n.toString()); }); }); - context('when passed bigint values that are more than 64 bits wide', function () { + context('when passed bigint values that are more than 64 bits wide', function() { let parsed; - before(function () { + before(function() { if (__noBigInt__) { return; } @@ -498,12 +509,12 @@ describe('BSON BigInt support', function () { parsed = JSON.parse(serialized); }); - it('fails loose equality checks with native bigint values', function () { + it('fails loose equality checks with native bigint values', function() { // eslint-disable-next-line eqeqeq expect(parsed.a.$numberLong == 0x1234_5678_1234_5678_9999n).false; }); - it('not equal to results of BigInt.toString', function () { + it('not equal to results of BigInt.toString', function() { expect(parsed.a.$numberLong).to.not.equal(0x1234_5678_1234_5678_9999n.toString()); }); }); From fdeb012745697d5b0870c768ca1e311b88f5d0c1 Mon Sep 17 00:00:00 2001 From: Warren James Date: Tue, 18 Feb 2025 13:17:50 -0500 Subject: [PATCH 3/7] update format script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3cec7832..dc7ac3c5 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "build:bundle": "rollup -c rollup.config.mjs", "build": "npm run build:dts && npm run build:bundle", "check:lint": "ESLINT_USE_FLAT_CONFIG=false eslint -v && ESLINT_USE_FLAT_CONFIG=false eslint --ext '.js,.ts' --max-warnings=0 src test && npm run build:dts && npm run check:tsd", - "format": "eslint --ext '.js,.ts' src test --fix", + "format": "ESLINT_USE_FLAT_CONFIG=false eslint --ext '.js,.ts' src test --fix", "check:coverage": "nyc --check-coverage npm run check:node", "prepare": "node etc/prepare.js", "release": "standard-version -i HISTORY.md" From cdaec880dbc8ae9d7e5ec815f0ba1888b4cf1515 Mon Sep 17 00:00:00 2001 From: Warren James Date: Tue, 18 Feb 2025 13:19:03 -0500 Subject: [PATCH 4/7] format --- src/utils/number_utils.ts | 3 +- test/node/bigint.test.ts | 91 ++++++++++++++++++++------------------- 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/src/utils/number_utils.ts b/src/utils/number_utils.ts index 3e76b2f5..b6d96d3b 100644 --- a/src/utils/number_utils.ts +++ b/src/utils/number_utils.ts @@ -87,11 +87,12 @@ export const NumberUtils: NumberUtils = { const hi = NumberUtils.getUint32LE(source, offset + 4); /* - eslint-disable-next-line no-restricted-globals + eslint-disable no-restricted-globals -- This is allowed since this helper should not be called unless bigint features are enabled */ const intermediate = (BigInt(hi) << BigInt(32)) + BigInt(lo); return BigInt.asIntN(64, intermediate); + /* eslint-enable no-restricted-globals */ }, /** Reads a little-endian 64-bit float from source */ diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index 07cc0dc7..dbb09c8e 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -3,18 +3,18 @@ import { bufferFromHexArray } from './tools/utils'; import { expect } from 'chai'; import { BSON_DATA_LONG } from '../../src/constants'; -describe('BSON BigInt support', function() { - beforeEach(function() { +describe('BSON BigInt support', function () { + beforeEach(function () { if (__noBigInt__) { this.currentTest?.skip(); } }); - describe('BSON roundtripping', function() { - const numbers = [-(2n ** 63n), -1n, 0n, 1n, (2n ** 63n) - 1n]; + describe('BSON roundtripping', function () { + const numbers = [-(2n ** 63n), -1n, 0n, 1n, 2n ** 63n - 1n]; for (const number of numbers) { - it(`correctly roundtrips ${number}`, function() { + it(`correctly roundtrips ${number}`, function () { const inputDoc = { number }; const serializedDoc = BSON.serialize(inputDoc); const outputDoc = BSON.deserialize(serializedDoc, { useBigInt64: true }); @@ -24,7 +24,7 @@ describe('BSON BigInt support', function() { } }); - describe('BSON.deserialize()', function() { + describe('BSON.deserialize()', function () { type DeserialzationOptions = { useBigInt64: boolean | undefined; promoteValues: boolean | undefined; @@ -79,12 +79,15 @@ describe('BSON BigInt support', function() { function generateTestDescription(entry: TestTableEntry): string { const options = entry.options; - const promoteValues = `promoteValues ${options.promoteValues === undefined ? 'is default' : `is ${options.promoteValues}` - }`; - const promoteLongs = `promoteLongs ${options.promoteLongs === undefined ? 'is default' : `is ${options.promoteLongs}` - }`; - const useBigInt64 = `useBigInt64 ${options.useBigInt64 === undefined ? 'is default' : `is ${options.useBigInt64}` - }`; + const promoteValues = `promoteValues ${ + options.promoteValues === undefined ? 'is default' : `is ${options.promoteValues}` + }`; + const promoteLongs = `promoteLongs ${ + options.promoteLongs === undefined ? 'is default' : `is ${options.promoteLongs}` + }`; + const useBigInt64 = `useBigInt64 ${ + options.useBigInt64 === undefined ? 'is default' : `is ${options.useBigInt64}` + }`; const flagString = `${useBigInt64}, ${promoteValues}, and ${promoteLongs}`; if (entry.shouldThrow) { return `throws when ${flagString}`; @@ -118,7 +121,7 @@ describe('BSON BigInt support', function() { } }); - describe('BSON.serialize()', function() { + describe('BSON.serialize()', function () { // Index for the data type byte of a BSON document with a // NOTE: These offsets only apply for documents with the shape {a : } // where n is a BigInt @@ -158,13 +161,13 @@ describe('BSON BigInt support', function() { }; } - it('serializes bigints with the correct BSON type', function() { + it('serializes bigints with the correct BSON type', function () { const testDoc = { a: 0n }; const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); expect(serializedDoc.dataType).to.equal(BSON_DATA_LONG); }); - it('serializes bigints into little-endian byte order', function() { + it('serializes bigints into little-endian byte order', function () { const testDoc = { a: 0x1234567812345678n }; const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); const expectedResult = getSerializedDocParts( @@ -178,7 +181,7 @@ describe('BSON BigInt support', function() { expect(expectedResult.value).to.equal(serializedDoc.value); }); - it('serializes a BigInt that can be safely represented as a Number', function() { + it('serializes a BigInt that can be safely represented as a Number', function () { const testDoc = { a: 0x23n }; const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); const expectedResult = getSerializedDocParts( @@ -191,7 +194,7 @@ describe('BSON BigInt support', function() { expect(serializedDoc).to.deep.equal(expectedResult); }); - it('serializes a BigInt in the valid range [-2^63, 2^63 - 1]', function() { + it('serializes a BigInt in the valid range [-2^63, 2^63 - 1]', function () { const testDoc = { a: 0xfffffffffffffff1n }; const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); const expectedResult = getSerializedDocParts( @@ -204,7 +207,7 @@ describe('BSON BigInt support', function() { expect(serializedDoc).to.deep.equal(expectedResult); }); - it('wraps to negative on a BigInt that is larger than (2^63 -1)', function() { + it('wraps to negative on a BigInt that is larger than (2^63 -1)', function () { const maxIntPlusOne = { a: 2n ** 63n }; const serializedMaxIntPlusOne = getSerializedDocParts(BSON.serialize(maxIntPlusOne)); const expectedResultForMaxIntPlusOne = getSerializedDocParts( @@ -217,7 +220,7 @@ describe('BSON BigInt support', function() { expect(serializedMaxIntPlusOne).to.deep.equal(expectedResultForMaxIntPlusOne); }); - it('serializes BigInts at the edges of the valid range [-2^63, 2^63 - 1]', function() { + it('serializes BigInts at the edges of the valid range [-2^63, 2^63 - 1]', function () { const maxPositiveInt64 = { a: 2n ** 63n - 1n }; const serializedMaxPositiveInt64 = getSerializedDocParts(BSON.serialize(maxPositiveInt64)); const expectedSerializationForMaxPositiveInt64 = getSerializedDocParts( @@ -241,7 +244,7 @@ describe('BSON BigInt support', function() { expect(serializedMinPositiveInt64).to.deep.equal(expectedSerializationForMinPositiveInt64); }); - it('truncates a BigInt that is larger than a 64-bit int', function() { + it('truncates a BigInt that is larger than a 64-bit int', function () { const testDoc = { a: 2n ** 64n + 1n }; const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); const expectedSerialization = getSerializedDocParts( @@ -254,7 +257,7 @@ describe('BSON BigInt support', function() { expect(serializedDoc).to.deep.equal(expectedSerialization); }); - it('serializes array of BigInts', function() { + it('serializes array of BigInts', function () { const testArr = { a: [1n] }; const serializedArr = BSON.serialize(testArr); const expectedSerialization = bufferFromHexArray([ @@ -269,7 +272,7 @@ describe('BSON BigInt support', function() { expect(serializedArr).to.deep.equal(expectedSerialization); }); - it('serializes Map with BigInt values', function() { + it('serializes Map with BigInt values', function () { const testMap = new Map(); testMap.set('a', 1n); const serializedMap = getSerializedDocParts(BSON.serialize(testMap)); @@ -284,7 +287,7 @@ describe('BSON BigInt support', function() { }); }); - describe('EJSON.parse()', function() { + describe('EJSON.parse()', function () { type ParseOptions = { useBigInt64: boolean | undefined; relaxed: boolean | undefined; @@ -341,13 +344,13 @@ describe('BSON BigInt support', function() { const condDescription = generateConditionDescription(entry); const behaviourDescription = generateBehaviourDescription(entry, sampleString); - describe(condDescription, function() { + describe(condDescription, function () { it(behaviourDescription, test); }); } } - describe('canonical input', function() { + describe('canonical input', function () { const canonicalInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { return relaxedValues.flatMap(relaxed => { return genTestTable( @@ -370,7 +373,7 @@ describe('BSON BigInt support', function() { createTestsFromTestTable(canonicalInputTestTable, sampleCanonicalString); }); - describe('relaxed integer input', function() { + describe('relaxed integer input', function () { const relaxedIntegerInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { return relaxedValues.flatMap(relaxed => { return genTestTable( @@ -392,7 +395,7 @@ describe('BSON BigInt support', function() { createTestsFromTestTable(relaxedIntegerInputTestTable, sampleRelaxedIntegerString); }); - describe('relaxed double input where double is outside of int32 range and useBigInt64 is true', function() { + describe('relaxed double input where double is outside of int32 range and useBigInt64 is true', function () { const relaxedDoubleInputTestTable = relaxedValues.flatMap(relaxed => { return genTestTable(true, relaxed, (_, relaxedIsSet: boolean) => relaxedIsSet ? { a: 2147483647.9 } : { a: new BSON.Double(2147483647.9) } @@ -407,15 +410,15 @@ describe('BSON BigInt support', function() { }); }); - describe('EJSON.stringify()', function() { - context('canonical mode (relaxed=false)', function() { - it('truncates bigint values when they are outside the range [BSON_INT64_MIN, BSON_INT64_MAX]', function() { + describe('EJSON.stringify()', function () { + context('canonical mode (relaxed=false)', function () { + it('truncates bigint values when they are outside the range [BSON_INT64_MIN, BSON_INT64_MAX]', function () { const numbers = { a: 2n ** 64n + 1n, b: -(2n ** 64n) - 1n }; const serialized = EJSON.stringify(numbers, { relaxed: false }); expect(serialized).to.equal('{"a":{"$numberLong":"1"},"b":{"$numberLong":"-1"}}'); }); - it('truncates bigint values in the same way as BSON.serialize', function() { + it('truncates bigint values in the same way as BSON.serialize', function () { const number = { a: 0x1234_5678_1234_5678_9999n }; const stringified = EJSON.stringify(number, { relaxed: false }); const serialized = BSON.serialize(number); @@ -435,15 +438,15 @@ describe('BSON BigInt support', function() { expect(parsed.a.$numberLong).to.equal(serializedValue.toString()); }); - it('serializes bigint values to numberLong in canonical mode', function() { + it('serializes bigint values to numberLong in canonical mode', function () { const number = { a: 2n }; const serialized = EJSON.stringify(number, { relaxed: false }); expect(serialized).to.equal('{"a":{"$numberLong":"2"}}'); }); }); - context('relaxed mode (relaxed=true)', function() { - it('truncates bigint values in the same way as BSON.serialize', function() { + context('relaxed mode (relaxed=true)', function () { + it('truncates bigint values in the same way as BSON.serialize', function () { const number = { a: 0x1234_0000_1234_5678_9999n }; // Ensure that the truncated number can be exactly represented as a JS number const stringified = EJSON.stringify(number, { relaxed: true }); const serializedDoc = BSON.serialize(number); @@ -462,23 +465,23 @@ describe('BSON BigInt support', function() { expect(parsed.a).to.equal(Number(dataView.getBigInt64(VALUE_OFFSET, true))); }); - it('serializes bigint values to Number', function() { + it('serializes bigint values to Number', function () { const number = { a: 10000n }; const serialized = EJSON.stringify(number, { relaxed: true }); expect(serialized).to.equal('{"a":10000}'); }); - it('loses precision when serializing bigint values outside of range [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]', function() { + it('loses precision when serializing bigint values outside of range [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]', function () { const numbers = { a: -(2n ** 53n) - 1n, b: 2n ** 53n + 2n }; const serialized = EJSON.stringify(numbers, { relaxed: true }); expect(serialized).to.equal('{"a":-9007199254740992,"b":9007199254740994}'); }); }); - context('when passed bigint values that are 64 bits wide or less', function() { + context('when passed bigint values that are 64 bits wide or less', function () { let parsed; - before(function() { + before(function () { if (__noBigInt__) { return; } @@ -487,20 +490,20 @@ describe('BSON BigInt support', function() { parsed = JSON.parse(serialized); }); - it('passes loose equality checks with native bigint values', function() { + it('passes loose equality checks with native bigint values', function () { // eslint-disable-next-line eqeqeq expect(parsed.a.$numberLong == 12345n).true; }); - it('equals the result of BigInt.toString', function() { + it('equals the result of BigInt.toString', function () { expect(parsed.a.$numberLong).to.equal(12345n.toString()); }); }); - context('when passed bigint values that are more than 64 bits wide', function() { + context('when passed bigint values that are more than 64 bits wide', function () { let parsed; - before(function() { + before(function () { if (__noBigInt__) { return; } @@ -509,12 +512,12 @@ describe('BSON BigInt support', function() { parsed = JSON.parse(serialized); }); - it('fails loose equality checks with native bigint values', function() { + it('fails loose equality checks with native bigint values', function () { // eslint-disable-next-line eqeqeq expect(parsed.a.$numberLong == 0x1234_5678_1234_5678_9999n).false; }); - it('not equal to results of BigInt.toString', function() { + it('not equal to results of BigInt.toString', function () { expect(parsed.a.$numberLong).to.not.equal(0x1234_5678_1234_5678_9999n.toString()); }); }); From d64860e60df7181a741b221a04a01dfa5e882749 Mon Sep 17 00:00:00 2001 From: Warren James Date: Tue, 18 Feb 2025 15:47:11 -0500 Subject: [PATCH 5/7] move tests to deserialize --- test/node/bigint.test.ts | 47 ++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index dbb09c8e..52891760 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -10,20 +10,6 @@ describe('BSON BigInt support', function () { } }); - describe('BSON roundtripping', function () { - const numbers = [-(2n ** 63n), -1n, 0n, 1n, 2n ** 63n - 1n]; - - for (const number of numbers) { - it(`correctly roundtrips ${number}`, function () { - const inputDoc = { number }; - const serializedDoc = BSON.serialize(inputDoc); - const outputDoc = BSON.deserialize(serializedDoc, { useBigInt64: true }); - - expect(outputDoc).to.deep.equal(inputDoc); - }); - } - }); - describe('BSON.deserialize()', function () { type DeserialzationOptions = { useBigInt64: boolean | undefined; @@ -119,6 +105,39 @@ describe('BSON BigInt support', function () { it(description, test); } + + describe('edge case tests', function () { + const tests = [ + { + expectedResult: { a: -(2n ** 63n) }, + input: Buffer.from('10000000126100000000000000008000', 'hex') + }, + { + expectedResult: { a: -1n }, + input: Buffer.from('10000000126100FFFFFFFFFFFFFFFF00', 'hex') + }, + { + expectedResult: { a: 0n }, + input: Buffer.from('10000000126100000000000000000000', 'hex') + }, + { + expectedResult: { a: 1n }, + input: Buffer.from('10000000126100010000000000000000', 'hex') + }, + { + expectedResult: { a: 2n ** 63n - 1n }, + input: Buffer.from('10000000126100FFFFFFFFFFFFFF7F00', 'hex') + } + ]; + + for (const test of tests) { + it(`correctly deserializes the bson document encoded in ${test.input.toString('hex')}`, function () { + expect(BSON.deserialize(test.input, { useBigInt64: true })).to.deep.equal( + test.expectedResult + ); + }); + } + }); }); describe('BSON.serialize()', function () { From 69b73042d298d6a81ca590a67ad1964230f77a5e Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 19 Feb 2025 10:55:35 -0500 Subject: [PATCH 6/7] move tests out of loop --- test/node/bigint.test.ts | 67 ++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index 52891760..3ffca593 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -106,37 +106,44 @@ describe('BSON BigInt support', function () { it(description, test); } - describe('edge case tests', function () { - const tests = [ - { - expectedResult: { a: -(2n ** 63n) }, - input: Buffer.from('10000000126100000000000000008000', 'hex') - }, - { - expectedResult: { a: -1n }, - input: Buffer.from('10000000126100FFFFFFFFFFFFFFFF00', 'hex') - }, - { - expectedResult: { a: 0n }, - input: Buffer.from('10000000126100000000000000000000', 'hex') - }, - { - expectedResult: { a: 1n }, - input: Buffer.from('10000000126100010000000000000000', 'hex') - }, - { - expectedResult: { a: 2n ** 63n - 1n }, - input: Buffer.from('10000000126100FFFFFFFFFFFFFF7F00', 'hex') - } - ]; + it('correctly deserializes min 64 bit int (-2n**63n)', function () { + expect( + BSON.deserialize(Buffer.from('10000000126100000000000000008000', 'hex'), { + useBigInt64: true + }) + ).to.deep.equal({ a: -(2n ** 63n) }); + }); - for (const test of tests) { - it(`correctly deserializes the bson document encoded in ${test.input.toString('hex')}`, function () { - expect(BSON.deserialize(test.input, { useBigInt64: true })).to.deep.equal( - test.expectedResult - ); - }); - } + it('correctly deserializes -1n', function () { + expect( + BSON.deserialize(Buffer.from('10000000126100FFFFFFFFFFFFFFFF00', 'hex'), { + useBigInt64: true + }) + ).to.deep.equal({ a: -1n }); + }); + + it('correctly deserializes 0n', function () { + expect( + BSON.deserialize(Buffer.from('10000000126100000000000000000000', 'hex'), { + useBigInt64: true + }) + ).to.deep.equal({ a: 0n }); + }); + + it('correctly deserializes 1n', function () { + expect( + BSON.deserialize(Buffer.from('10000000126100010000000000000000', 'hex'), { + useBigInt64: true + }) + ).to.deep.equal({ a: 1n }); + }); + + it('correctly deserializes max 64 bit int (2n**63n -1n)', function () { + expect( + BSON.deserialize(Buffer.from('10000000126100FFFFFFFFFFFFFF7F00', 'hex'), { + useBigInt64: true + }) + ).to.deep.equal({ a: 2n ** 63n - 1n }); }); }); From 1a702264a11965850414e242ab7b25c993868b00 Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 19 Feb 2025 11:30:15 -0500 Subject: [PATCH 7/7] change bit math instead of using BigInt.asIntN --- src/utils/number_utils.ts | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/utils/number_utils.ts b/src/utils/number_utils.ts index b6d96d3b..5970a963 100644 --- a/src/utils/number_utils.ts +++ b/src/utils/number_utils.ts @@ -83,16 +83,23 @@ export const NumberUtils: NumberUtils = { /** Reads a little-endian 64-bit integer from source */ getBigInt64LE(source: Uint8Array, offset: number): bigint { - const lo = NumberUtils.getUint32LE(source, offset); - const hi = NumberUtils.getUint32LE(source, offset + 4); - - /* - eslint-disable no-restricted-globals - -- This is allowed since this helper should not be called unless bigint features are enabled - */ - const intermediate = (BigInt(hi) << BigInt(32)) + BigInt(lo); - return BigInt.asIntN(64, intermediate); - /* eslint-enable no-restricted-globals */ + // eslint-disable-next-line no-restricted-globals + const hi = BigInt( + source[offset + 4] + + source[offset + 5] * 256 + + source[offset + 6] * 65536 + + (source[offset + 7] << 24) + ); // Overflow + + // eslint-disable-next-line no-restricted-globals + const lo = BigInt( + source[offset] + + source[offset + 1] * 256 + + source[offset + 2] * 65536 + + source[offset + 3] * 16777216 + ); + // eslint-disable-next-line no-restricted-globals + return (hi << BigInt(32)) + lo; }, /** Reads a little-endian 64-bit float from source */