From 75a6207cc7c1b34017a830cc39ad94cebe695640 Mon Sep 17 00:00:00 2001 From: Warren James Date: Thu, 5 Jan 2023 16:58:21 -0500 Subject: [PATCH 01/36] feat(NODE-4874): WIP --- src/extended_json.ts | 20 ++++++++++++++++---- src/long.ts | 9 ++++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/extended_json.ts b/src/extended_json.ts index d9b5f8f6..5114752f 100644 --- a/src/extended_json.ts +++ b/src/extended_json.ts @@ -22,6 +22,8 @@ export type EJSONOptions = { legacy?: boolean; /** Enable Extended JSON's `relaxed` mode, which attempts to return native JS types where possible, rather than BSON types */ relaxed?: boolean; + /** Enable native bigint support */ + useBigInt64?: boolean; }; /** @internal */ @@ -81,7 +83,10 @@ function deserializeValue(value: any, options: EJSONOptions = {}) { } if (value >= BSON_INT64_MIN && value <= BSON_INT64_MAX) { // TODO(NODE-4377): EJSON js number handling diverges from BSON - return Long.fromNumber(value); + if (!options.useBigInt64) { + return Long.fromNumber(value); + } + return BigInt(value); } } @@ -89,6 +94,13 @@ function deserializeValue(value: any, options: EJSONOptions = {}) { return new Double(value); } + if (typeof value === 'bigint') { + if (options.useBigInt64) { + return value; + } + return Long.fromBigInt(value); + } + // from here on out we're looking for bson types, so bail if its not an object if (value == null || typeof value !== 'object') return value; @@ -194,8 +206,8 @@ function serializeValue(value: any, options: EJSONSerializeOptions): any { throw new BSONError( 'Converting circular structure to EJSON:\n' + - ` ${leadingPart}${alreadySeen}${circularPart}${current}\n` + - ` ${leadingSpace}\\${dashes}/` + ` ${leadingPart}${alreadySeen}${circularPart}${current}\n` + + ` ${leadingSpace}\\${dashes}/` ); } options.seenObjects[options.seenObjects.length - 1].obj = value; @@ -368,7 +380,7 @@ function parse(text: string, options?: EJSONOptions): any { `BSON Document field names cannot contain null bytes, found: ${JSON.stringify(key)}` ); } - return deserializeValue(value, { relaxed: true, legacy: false, ...options }); + return deserializeValue(value, { relaxed: true, legacy: false, useBigInt64: false, ...options }); }); } diff --git a/src/long.ts b/src/long.ts index 65f61f2b..3f3f4f31 100644 --- a/src/long.ts +++ b/src/long.ts @@ -1021,9 +1021,12 @@ export class Long { if (options && options.relaxed) return this.toNumber(); return { $numberLong: this.toString() }; } - static fromExtendedJSON(doc: { $numberLong: string }, options?: EJSONOptions): number | Long { - const result = Long.fromString(doc.$numberLong); - return options && options.relaxed ? result.toNumber() : result; + static fromExtendedJSON(doc: { $numberLong: string }, options?: EJSONOptions): number | Long | bigint { + const longResult = Long.fromString(doc.$numberLong); + if (options && options.relaxed) { + return options && options.useBigInt64 ? BigInt(doc.$numberLong) : longResult.toNumber(); + } + return longResult; } /** @internal */ From 7cd7f6cbbf3b62ba7374c02182876e0af665a91e Mon Sep 17 00:00:00 2001 From: Warren James Date: Thu, 5 Jan 2023 16:58:37 -0500 Subject: [PATCH 02/36] test(NODE-4874): Start table test --- test/node/bigint.test.ts | 140 ++++++++++++++++++++++++++++++++------- 1 file changed, 117 insertions(+), 23 deletions(-) diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index fcc95790..c0ffa408 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -1,11 +1,11 @@ -import { BSON, BSONError } from '../register-bson'; +import { BSON, BSONError, EJSON } from '../register-bson'; import { bufferFromHexArray } from './tools/utils'; -import { expect } from 'chai'; +import { expect, use } from 'chai'; import { BSON_DATA_LONG } from '../../src/constants'; import { BSONDataView } from '../../src/utils/byte_utils'; -describe('BSON BigInt support', function () { - describe('BSON.deserialize()', function () { +describe('BSON BigInt support', function() { + describe('BSON.deserialize()', function() { type DeserialzationOptions = { useBigInt64: boolean | undefined; promoteValues: boolean | undefined; @@ -60,15 +60,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}`; @@ -102,7 +99,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 @@ -138,13 +135,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( @@ -158,7 +155,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( @@ -171,7 +168,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( @@ -184,7 +181,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( @@ -197,7 +194,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( @@ -221,7 +218,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( @@ -234,7 +231,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([ @@ -249,7 +246,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)); @@ -263,4 +260,101 @@ describe('BSON BigInt support', function () { expect(serializedMap).to.deep.equal(expectedSerialization); }); }); + + describe('EJSON.parse()', function() { + type ParseOptions = { + useBigInt64: boolean | undefined; + relaxed: boolean | undefined; + }; + type TestTableEntry = { + options: ParseOptions; + expectedResult: BSON.Document; + }; + const useBigInt64Values = [true, false, undefined]; + const relaxedValues = [true, false, undefined]; + const sampleCanonicalString = '{"a":{"$numberLong":"23"}}'; + const sampleRelaxedString = '{"a":2147483648}'; + + function genTestTable(useBigInt64, relaxed, expectedResultGenerator): [TestTableEntry] { + const useBigInt64IsSet = useBigInt64 ?? false; + const relaxedIsSet = relaxed ?? true; + + let expectedResult: BSON.Document; + expectedResult = expectedResultGenerator(useBigInt64IsSet, relaxedIsSet); + + return [{ options: { useBigInt64, relaxed }, expectedResult }]; + }; + + function generateTestDescription(entry: TestTableEntry, canonical: boolean) { + const options = entry.options; + if (canonical) { + return `parses '${sampleCanonicalString}' to '${entry.expectedResult}' when useBigInt64 is ${options.useBigInt64} and relaxed is ${options.relaxed}`; + } else { + return `parses '${sampleRelaxedString}' to '${entry.expectedResult}' when useBigInt64 is ${options.useBigInt64} and relaxed is ${options.relaxed}`; + } + } + + function generateTest(entry: TestTableEntry, sampleString: string) { + const options = entry.options; + const parse = () => { + return EJSON.parse(sampleString, { useBigInt64: options.useBigInt64, relaxed: options.relaxed }); + } + + return () => { + const parsed = parse(); + expect(parsed).to.deep.equal(entry.expectedResult); + }; + } + + + + describe.only('canonical input', function() { + const canonicalInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { + return relaxedValues.flatMap(relaxed => { + return genTestTable(useBigInt64, relaxed, (useBigInt64IsSet: boolean, relaxedIsSet: boolean) => + useBigInt64IsSet + ? { a: 23n } + : relaxedIsSet + ? { a: 23 } + : { a: new BSON.Long(23) } + ); + }); + }); + + it('meta test: generates 9 tests', () => { + expect(canonicalInputTestTable).to.have.lengthOf(9); + }); + + for (const entry of canonicalInputTestTable) { + const test = generateTest(entry, sampleCanonicalString); + const description = generateTestDescription(entry, true); + + it(description, test); + } + }); + + describe('relaxed input', function() { + const relaxedInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { + return relaxedValues.flatMap(relaxed => { + return genTestTable(useBigInt64, relaxed, (useBigInt64IsSet: boolean, relaxedIsSet: boolean) => useBigInt64IsSet + ? { a: 2147483648n } + : relaxedIsSet + ? { a: 2147483648 } + : { a: new BSON.Long(2147483648) } + ); + }); + }); + it('meta test: generates 9 tests', () => { + expect(relaxedInputTestTable).to.have.lengthOf(9); + }); + + for (const entry of relaxedInputTestTable) { + const test = generateTest(entry, sampleRelaxedString); + const description = generateTestDescription(entry, false); + + it(description, test); + } + }); + + }); }); From df9755288daaa4383e22b0ec80852de8b0bb1ec5 Mon Sep 17 00:00:00 2001 From: Warren James Date: Fri, 6 Jan 2023 10:46:40 -0500 Subject: [PATCH 03/36] fix(NODE-4874): Get canonical mode parsing to work --- src/long.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/long.ts b/src/long.ts index 3f3f4f31..e4f71b47 100644 --- a/src/long.ts +++ b/src/long.ts @@ -1021,10 +1021,16 @@ export class Long { if (options && options.relaxed) return this.toNumber(); return { $numberLong: this.toString() }; } + // NOTE: We handle the defaults for the EJSON options very poorly here. static fromExtendedJSON(doc: { $numberLong: string }, options?: EJSONOptions): number | Long | bigint { const longResult = Long.fromString(doc.$numberLong); - if (options && options.relaxed) { - return options && options.useBigInt64 ? BigInt(doc.$numberLong) : longResult.toNumber(); + const defaults = { useBigInt64: false, relaxed: true, legacy: false }; + options = { ...defaults, ...options }; + if (options.useBigInt64 ?? false) { + return BigInt(doc.$numberLong); + } + if (options.relaxed ?? true) { + return longResult.toNumber(); } return longResult; } From 07904f97586f3157c45c9a61a6d65aca9457963c Mon Sep 17 00:00:00 2001 From: Warren James Date: Fri, 6 Jan 2023 11:04:15 -0500 Subject: [PATCH 04/36] style(NODE-4874): Style --- test/node/bigint.test.ts | 106 +++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 49 deletions(-) diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index c0ffa408..388a1ee6 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -1,11 +1,11 @@ import { BSON, BSONError, EJSON } from '../register-bson'; import { bufferFromHexArray } from './tools/utils'; -import { expect, use } from 'chai'; +import { expect } from 'chai'; import { BSON_DATA_LONG } from '../../src/constants'; import { BSONDataView } from '../../src/utils/byte_utils'; -describe('BSON BigInt support', function() { - describe('BSON.deserialize()', function() { +describe('BSON BigInt support', function () { + describe('BSON.deserialize()', function () { type DeserialzationOptions = { useBigInt64: boolean | undefined; promoteValues: boolean | undefined; @@ -60,12 +60,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}`; @@ -99,7 +102,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 @@ -135,13 +138,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( @@ -155,7 +158,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( @@ -168,7 +171,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( @@ -181,7 +184,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( @@ -194,7 +197,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( @@ -218,7 +221,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( @@ -231,7 +234,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([ @@ -246,7 +249,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)); @@ -261,7 +264,7 @@ describe('BSON BigInt support', function() { }); }); - describe('EJSON.parse()', function() { + describe('EJSON.parse()', function () { type ParseOptions = { useBigInt64: boolean | undefined; relaxed: boolean | undefined; @@ -270,53 +273,55 @@ describe('BSON BigInt support', function() { options: ParseOptions; expectedResult: BSON.Document; }; + const useBigInt64Values = [true, false, undefined]; const relaxedValues = [true, false, undefined]; const sampleCanonicalString = '{"a":{"$numberLong":"23"}}'; - const sampleRelaxedString = '{"a":2147483648}'; + const sampleRelaxedString = '{"a":4294967296}'; - function genTestTable(useBigInt64, relaxed, expectedResultGenerator): [TestTableEntry] { + function genTestTable( + useBigInt64: boolean | undefined, + relaxed: boolean | undefined, + getExpectedResult: (boolean, boolean) => BSON.Document + ): [TestTableEntry] { const useBigInt64IsSet = useBigInt64 ?? false; const relaxedIsSet = relaxed ?? true; - let expectedResult: BSON.Document; - expectedResult = expectedResultGenerator(useBigInt64IsSet, relaxedIsSet); + const expectedResult = getExpectedResult(useBigInt64IsSet, relaxedIsSet); return [{ options: { useBigInt64, relaxed }, expectedResult }]; - }; + } - function generateTestDescription(entry: TestTableEntry, canonical: boolean) { + function generateTestDescription(entry: TestTableEntry, canonical: boolean): string { + // TODO(NODE-4874): When NODE-4873 is merged in, replace this with EJSON.stringify const options = entry.options; if (canonical) { - return `parses '${sampleCanonicalString}' to '${entry.expectedResult}' when useBigInt64 is ${options.useBigInt64} and relaxed is ${options.relaxed}`; + return `parses field 'a' of '${sampleCanonicalString}' to '${entry.expectedResult.a.constructor.name}' when useBigInt64 is ${options.useBigInt64} and relaxed is ${options.relaxed}`; } else { - return `parses '${sampleRelaxedString}' to '${entry.expectedResult}' when useBigInt64 is ${options.useBigInt64} and relaxed is ${options.relaxed}`; + return `parses field 'a' of' ${sampleRelaxedString}' to '${entry.expectedResult.a.constructor.name}' when useBigInt64 is ${options.useBigInt64} and relaxed is ${options.relaxed}`; } } - function generateTest(entry: TestTableEntry, sampleString: string) { + function generateTest(entry: TestTableEntry, sampleString: string): () => void { const options = entry.options; - const parse = () => { - return EJSON.parse(sampleString, { useBigInt64: options.useBigInt64, relaxed: options.relaxed }); - } return () => { - const parsed = parse(); + const parsed = EJSON.parse(sampleString, { + useBigInt64: options.useBigInt64, + relaxed: options.relaxed + }); expect(parsed).to.deep.equal(entry.expectedResult); }; } - - - describe.only('canonical input', function() { + describe('canonical input', function () { const canonicalInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { return relaxedValues.flatMap(relaxed => { - return genTestTable(useBigInt64, relaxed, (useBigInt64IsSet: boolean, relaxedIsSet: boolean) => - useBigInt64IsSet - ? { a: 23n } - : relaxedIsSet - ? { a: 23 } - : { a: new BSON.Long(23) } + return genTestTable( + useBigInt64, + relaxed, + (useBigInt64IsSet: boolean, relaxedIsSet: boolean) => + useBigInt64IsSet ? { a: 23n } : relaxedIsSet ? { a: 23 } : { a: new BSON.Long(23) } ); }); }); @@ -333,14 +338,18 @@ describe('BSON BigInt support', function() { } }); - describe('relaxed input', function() { + describe.skip('relaxed input', function () { const relaxedInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { return relaxedValues.flatMap(relaxed => { - return genTestTable(useBigInt64, relaxed, (useBigInt64IsSet: boolean, relaxedIsSet: boolean) => useBigInt64IsSet - ? { a: 2147483648n } - : relaxedIsSet - ? { a: 2147483648 } - : { a: new BSON.Long(2147483648) } + return genTestTable( + useBigInt64, + relaxed, + (useBigInt64IsSet: boolean, relaxedIsSet: boolean) => + useBigInt64IsSet + ? { a: 4294967296n } + : relaxedIsSet + ? { a: 4294967296 } + : { a: new BSON.Long(4294967296) } ); }); }); @@ -355,6 +364,5 @@ describe('BSON BigInt support', function() { it(description, test); } }); - }); }); From d0dd23dd110aa98110962a61e31d3338468a47a0 Mon Sep 17 00:00:00 2001 From: Warren James Date: Fri, 6 Jan 2023 11:04:53 -0500 Subject: [PATCH 05/36] style(NODE-4874): eslint --- src/extended_json.ts | 11 ++++++++--- src/long.ts | 5 ++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/extended_json.ts b/src/extended_json.ts index 5114752f..52bb57c1 100644 --- a/src/extended_json.ts +++ b/src/extended_json.ts @@ -206,8 +206,8 @@ function serializeValue(value: any, options: EJSONSerializeOptions): any { throw new BSONError( 'Converting circular structure to EJSON:\n' + - ` ${leadingPart}${alreadySeen}${circularPart}${current}\n` + - ` ${leadingSpace}\\${dashes}/` + ` ${leadingPart}${alreadySeen}${circularPart}${current}\n` + + ` ${leadingSpace}\\${dashes}/` ); } options.seenObjects[options.seenObjects.length - 1].obj = value; @@ -380,7 +380,12 @@ function parse(text: string, options?: EJSONOptions): any { `BSON Document field names cannot contain null bytes, found: ${JSON.stringify(key)}` ); } - return deserializeValue(value, { relaxed: true, legacy: false, useBigInt64: false, ...options }); + return deserializeValue(value, { + relaxed: true, + legacy: false, + useBigInt64: false, + ...options + }); }); } diff --git a/src/long.ts b/src/long.ts index e4f71b47..c136cb1e 100644 --- a/src/long.ts +++ b/src/long.ts @@ -1022,7 +1022,10 @@ export class Long { return { $numberLong: this.toString() }; } // NOTE: We handle the defaults for the EJSON options very poorly here. - static fromExtendedJSON(doc: { $numberLong: string }, options?: EJSONOptions): number | Long | bigint { + static fromExtendedJSON( + doc: { $numberLong: string }, + options?: EJSONOptions + ): number | Long | bigint { const longResult = Long.fromString(doc.$numberLong); const defaults = { useBigInt64: false, relaxed: true, legacy: false }; options = { ...defaults, ...options }; From 5120ca54d5642a3b49bb95a4b38899eba61b3494 Mon Sep 17 00:00:00 2001 From: Warren James Date: Fri, 6 Jan 2023 16:05:53 -0500 Subject: [PATCH 06/36] fix(NODE-4874): Fix handling of flags --- src/extended_json.ts | 29 +++++++++++++++++------------ src/long.ts | 1 - 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/extended_json.ts b/src/extended_json.ts index 52bb57c1..f7bb0ed9 100644 --- a/src/extended_json.ts +++ b/src/extended_json.ts @@ -72,21 +72,26 @@ const keysToCodecs = { // eslint-disable-next-line @typescript-eslint/no-explicit-any function deserializeValue(value: any, options: EJSONOptions = {}) { if (typeof value === 'number') { + const in32BitRange = value <= BSON_INT32_MAX && value >= BSON_INT32_MIN; + const in64BitRange = value <= BSON_INT64_MAX && value >= BSON_INT64_MIN; + if (options.useBigInt64) { + if (!in32BitRange && in64BitRange) { + return BigInt(value); + } + } + if (options.relaxed || options.legacy) { return value; } if (Number.isInteger(value) && !Object.is(value, -0)) { // interpret as being of the smallest BSON integer type that can represent the number exactly - if (value >= BSON_INT32_MIN && value <= BSON_INT32_MAX) { + if (in32BitRange) { return new Int32(value); } - if (value >= BSON_INT64_MIN && value <= BSON_INT64_MAX) { + if (in64BitRange) { // TODO(NODE-4377): EJSON js number handling diverges from BSON - if (!options.useBigInt64) { - return Long.fromNumber(value); - } - return BigInt(value); + return Long.fromNumber(value); } } @@ -374,18 +379,18 @@ function serializeDocument(doc: any, options: EJSONSerializeOptions) { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function parse(text: string, options?: EJSONOptions): any { + options = { + useBigInt64: options?.useBigInt64 ?? false, + relaxed: options?.relaxed ?? true, + legacy: options?.legacy ?? false + }; return JSON.parse(text, (key, value) => { if (key.indexOf('\x00') !== -1) { throw new BSONError( `BSON Document field names cannot contain null bytes, found: ${JSON.stringify(key)}` ); } - return deserializeValue(value, { - relaxed: true, - legacy: false, - useBigInt64: false, - ...options - }); + return deserializeValue(value, options); }); } diff --git a/src/long.ts b/src/long.ts index c136cb1e..fc3865e0 100644 --- a/src/long.ts +++ b/src/long.ts @@ -1021,7 +1021,6 @@ export class Long { if (options && options.relaxed) return this.toNumber(); return { $numberLong: this.toString() }; } - // NOTE: We handle the defaults for the EJSON options very poorly here. static fromExtendedJSON( doc: { $numberLong: string }, options?: EJSONOptions From d67757d5a8966226a8dcd34a4d64ef6d549d2a9d Mon Sep 17 00:00:00 2001 From: Warren James Date: Fri, 6 Jan 2023 16:06:12 -0500 Subject: [PATCH 07/36] test(NODE-4874): Finish tests --- test/node/bigint.test.ts | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index 388a1ee6..ee88ac9b 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -292,14 +292,10 @@ describe('BSON BigInt support', function () { return [{ options: { useBigInt64, relaxed }, expectedResult }]; } - function generateTestDescription(entry: TestTableEntry, canonical: boolean): string { + function generateTestDescription(entry: TestTableEntry, inputString: string): string { // TODO(NODE-4874): When NODE-4873 is merged in, replace this with EJSON.stringify const options = entry.options; - if (canonical) { - return `parses field 'a' of '${sampleCanonicalString}' to '${entry.expectedResult.a.constructor.name}' when useBigInt64 is ${options.useBigInt64} and relaxed is ${options.relaxed}`; - } else { - return `parses field 'a' of' ${sampleRelaxedString}' to '${entry.expectedResult.a.constructor.name}' when useBigInt64 is ${options.useBigInt64} and relaxed is ${options.relaxed}`; - } + return `parses field 'a' of '${inputString}' to '${entry.expectedResult.a.constructor.name}' when useBigInt64 is ${options.useBigInt64} and relaxed is ${options.relaxed}`; } function generateTest(entry: TestTableEntry, sampleString: string): () => void { @@ -321,7 +317,11 @@ describe('BSON BigInt support', function () { useBigInt64, relaxed, (useBigInt64IsSet: boolean, relaxedIsSet: boolean) => - useBigInt64IsSet ? { a: 23n } : relaxedIsSet ? { a: 23 } : { a: new BSON.Long(23) } + useBigInt64IsSet + ? { a: 23n } + : relaxedIsSet + ? { a: 23 } + : { a: BSON.Long.fromNumber(23) } ); }); }); @@ -332,13 +332,13 @@ describe('BSON BigInt support', function () { for (const entry of canonicalInputTestTable) { const test = generateTest(entry, sampleCanonicalString); - const description = generateTestDescription(entry, true); + const description = generateTestDescription(entry, sampleCanonicalString); it(description, test); } }); - describe.skip('relaxed input', function () { + describe('relaxed input', function () { const relaxedInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { return relaxedValues.flatMap(relaxed => { return genTestTable( @@ -349,7 +349,7 @@ describe('BSON BigInt support', function () { ? { a: 4294967296n } : relaxedIsSet ? { a: 4294967296 } - : { a: new BSON.Long(4294967296) } + : { a: BSON.Long.fromNumber(4294967296) } ); }); }); @@ -357,9 +357,16 @@ describe('BSON BigInt support', function () { expect(relaxedInputTestTable).to.have.lengthOf(9); }); + /* + const entry = relaxedInputTestTable[5]; + const test = generateTest(entry, sampleRelaxedString); + const description = generateTestDescription(entry, sampleRelaxedString); + it.only(description, test); + */ + for (const entry of relaxedInputTestTable) { const test = generateTest(entry, sampleRelaxedString); - const description = generateTestDescription(entry, false); + const description = generateTestDescription(entry, sampleRelaxedString); it(description, test); } From 4f15e42f0e76680b311d473b496f8c0fa5dd0848 Mon Sep 17 00:00:00 2001 From: Warren James Date: Fri, 6 Jan 2023 16:08:39 -0500 Subject: [PATCH 08/36] style(NODE-4874): fixup condition --- src/extended_json.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/extended_json.ts b/src/extended_json.ts index f7bb0ed9..6dd97df5 100644 --- a/src/extended_json.ts +++ b/src/extended_json.ts @@ -74,10 +74,8 @@ function deserializeValue(value: any, options: EJSONOptions = {}) { if (typeof value === 'number') { const in32BitRange = value <= BSON_INT32_MAX && value >= BSON_INT32_MIN; const in64BitRange = value <= BSON_INT64_MAX && value >= BSON_INT64_MIN; - if (options.useBigInt64) { - if (!in32BitRange && in64BitRange) { - return BigInt(value); - } + if (options.useBigInt64 && !in32BitRange && in64BitRange) { + return BigInt(value); } if (options.relaxed || options.legacy) { @@ -211,8 +209,8 @@ function serializeValue(value: any, options: EJSONSerializeOptions): any { throw new BSONError( 'Converting circular structure to EJSON:\n' + - ` ${leadingPart}${alreadySeen}${circularPart}${current}\n` + - ` ${leadingSpace}\\${dashes}/` + ` ${leadingPart}${alreadySeen}${circularPart}${current}\n` + + ` ${leadingSpace}\\${dashes}/` ); } options.seenObjects[options.seenObjects.length - 1].obj = value; From 2f5db0515e4d74fce4a985b0731ef8f56aff3915 Mon Sep 17 00:00:00 2001 From: Warren James Date: Fri, 6 Jan 2023 16:14:57 -0500 Subject: [PATCH 09/36] fix(NODE-4874): Remove dead code --- src/extended_json.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/extended_json.ts b/src/extended_json.ts index 6dd97df5..01dadd2a 100644 --- a/src/extended_json.ts +++ b/src/extended_json.ts @@ -97,13 +97,6 @@ function deserializeValue(value: any, options: EJSONOptions = {}) { return new Double(value); } - if (typeof value === 'bigint') { - if (options.useBigInt64) { - return value; - } - return Long.fromBigInt(value); - } - // from here on out we're looking for bson types, so bail if its not an object if (value == null || typeof value !== 'object') return value; @@ -209,8 +202,8 @@ function serializeValue(value: any, options: EJSONSerializeOptions): any { throw new BSONError( 'Converting circular structure to EJSON:\n' + - ` ${leadingPart}${alreadySeen}${circularPart}${current}\n` + - ` ${leadingSpace}\\${dashes}/` + ` ${leadingPart}${alreadySeen}${circularPart}${current}\n` + + ` ${leadingSpace}\\${dashes}/` ); } options.seenObjects[options.seenObjects.length - 1].obj = value; From 822dff8fb5e22ea92a7e01dbd70f75eacd0458ea Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 11 Jan 2023 10:51:46 -0500 Subject: [PATCH 10/36] fix(NODE-4874): Add fix and test for double bug --- src/extended_json.ts | 8 ++++---- test/node/bigint.test.ts | 7 +++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/extended_json.ts b/src/extended_json.ts index 01dadd2a..e03bd431 100644 --- a/src/extended_json.ts +++ b/src/extended_json.ts @@ -74,12 +74,9 @@ function deserializeValue(value: any, options: EJSONOptions = {}) { if (typeof value === 'number') { const in32BitRange = value <= BSON_INT32_MAX && value >= BSON_INT32_MIN; const in64BitRange = value <= BSON_INT64_MAX && value >= BSON_INT64_MIN; - if (options.useBigInt64 && !in32BitRange && in64BitRange) { - return BigInt(value); - } if (options.relaxed || options.legacy) { - return value; + return Number.isInteger(value) && options.useBigInt64 ? BigInt(value) : value; } if (Number.isInteger(value) && !Object.is(value, -0)) { @@ -89,6 +86,9 @@ function deserializeValue(value: any, options: EJSONOptions = {}) { } if (in64BitRange) { // TODO(NODE-4377): EJSON js number handling diverges from BSON + if (options.useBigInt64) { + return BigInt(value); + } return Long.fromNumber(value); } } diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index ee88ac9b..7be9514e 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -336,6 +336,7 @@ describe('BSON BigInt support', function () { it(description, test); } + }); describe('relaxed input', function () { @@ -370,6 +371,12 @@ describe('BSON BigInt support', function () { it(description, test); } + + it('returns a double when passed in a double outside int32 range and when useBigInt64 is true', function() { + const inputString= '{"a" : 2147483647.9}'; + const output = EJSON.parse(inputString, {useBigInt64: true, relaxed: true}); + expect(typeof output.a).to.equal('number'); + }); }); }); }); From 5666255ada915286b6cc89917edddd843ae42f29 Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 11 Jan 2023 11:24:34 -0500 Subject: [PATCH 11/36] fix(NODE-4874): Implement requested fixes --- src/extended_json.ts | 4 ++-- src/long.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/extended_json.ts b/src/extended_json.ts index e03bd431..7df9f475 100644 --- a/src/extended_json.ts +++ b/src/extended_json.ts @@ -370,7 +370,7 @@ function serializeDocument(doc: any, options: EJSONSerializeOptions) { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function parse(text: string, options?: EJSONOptions): any { - options = { + const ejsonOptions = { useBigInt64: options?.useBigInt64 ?? false, relaxed: options?.relaxed ?? true, legacy: options?.legacy ?? false @@ -381,7 +381,7 @@ function parse(text: string, options?: EJSONOptions): any { `BSON Document field names cannot contain null bytes, found: ${JSON.stringify(key)}` ); } - return deserializeValue(value, options); + return deserializeValue(value, ejsonOptions); }); } diff --git a/src/long.ts b/src/long.ts index fc3865e0..fe67dd1d 100644 --- a/src/long.ts +++ b/src/long.ts @@ -1026,12 +1026,12 @@ export class Long { options?: EJSONOptions ): number | Long | bigint { const longResult = Long.fromString(doc.$numberLong); - const defaults = { useBigInt64: false, relaxed: true, legacy: false }; - options = { ...defaults, ...options }; - if (options.useBigInt64 ?? false) { + const defaults = { useBigInt64: false, relaxed: true }; + const ejsonOptions = { ...defaults, ...options }; + if (ejsonOptions.useBigInt64) { return BigInt(doc.$numberLong); } - if (options.relaxed ?? true) { + if (ejsonOptions.relaxed) { return longResult.toNumber(); } return longResult; From 79f0c8145a2e286817b38424c9a7950607314bba Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 11 Jan 2023 11:25:26 -0500 Subject: [PATCH 12/36] test(NODE-4874): Add new table tests --- test/node/bigint.test.ts | 44 +++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index 7be9514e..8a4b6638 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -277,7 +277,8 @@ describe('BSON BigInt support', function () { const useBigInt64Values = [true, false, undefined]; const relaxedValues = [true, false, undefined]; const sampleCanonicalString = '{"a":{"$numberLong":"23"}}'; - const sampleRelaxedString = '{"a":4294967296}'; + const sampleRelaxedIntegerString = '{"a":4294967296}'; + const sampleRelaxedDoubleString = '{"a": 2147483647.9}'; function genTestTable( useBigInt64: boolean | undefined, @@ -336,11 +337,10 @@ describe('BSON BigInt support', function () { it(description, test); } - }); - describe('relaxed input', function () { - const relaxedInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { + describe('relaxed integer input', function () { + const relaxedIntegerInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { return relaxedValues.flatMap(relaxed => { return genTestTable( useBigInt64, @@ -355,28 +355,34 @@ describe('BSON BigInt support', function () { }); }); it('meta test: generates 9 tests', () => { - expect(relaxedInputTestTable).to.have.lengthOf(9); + expect(relaxedIntegerInputTestTable).to.have.lengthOf(9); }); - /* - const entry = relaxedInputTestTable[5]; - const test = generateTest(entry, sampleRelaxedString); - const description = generateTestDescription(entry, sampleRelaxedString); - it.only(description, test); - */ - - for (const entry of relaxedInputTestTable) { - const test = generateTest(entry, sampleRelaxedString); - const description = generateTestDescription(entry, sampleRelaxedString); + for (const entry of relaxedIntegerInputTestTable) { + const test = generateTest(entry, sampleRelaxedIntegerString); + const description = generateTestDescription(entry, sampleRelaxedIntegerString); it(description, test); } + }); + + 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) } + ); + }); - it('returns a double when passed in a double outside int32 range and when useBigInt64 is true', function() { - const inputString= '{"a" : 2147483647.9}'; - const output = EJSON.parse(inputString, {useBigInt64: true, relaxed: true}); - expect(typeof output.a).to.equal('number'); + it('meta test: generates 3 tests', () => { + expect(relaxedDoubleInputTestTable).to.have.lengthOf(3); }); + + for (const entry of relaxedDoubleInputTestTable) { + const test = generateTest(entry, sampleRelaxedDoubleString); + const description = generateTestDescription(entry, sampleRelaxedDoubleString); + + it(description, test); + } }); }); }); From 260b6672d876ee5ccafb86e7e490e99d23aff5f2 Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 11 Jan 2023 11:45:23 -0500 Subject: [PATCH 13/36] test(NODE-4874): Add test to check that legacy flag doesn't affect output of Long.fromExtendedJSON --- test/node/bigint.test.ts | 1 + test/node/long.test.ts | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index 8a4b6638..e7fdf708 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -274,6 +274,7 @@ describe('BSON BigInt support', function () { expectedResult: BSON.Document; }; + // NOTE: legacy is not changed here as it does not affect the output of parsing a Long/Number const useBigInt64Values = [true, false, undefined]; const relaxedValues = [true, false, undefined]; const sampleCanonicalString = '{"a":{"$numberLong":"23"}}'; diff --git a/test/node/long.test.ts b/test/node/long.test.ts index 9a73aedc..54200c3f 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -1,7 +1,8 @@ +import { expect } from 'chai'; import { Long } from '../register-bson'; -describe('Long', function () { - it('accepts strings in the constructor', function () { +describe('Long', function() { + it('accepts strings in the constructor', function() { expect(new Long('0').toString()).to.equal('0'); expect(new Long('00').toString()).to.equal('0'); expect(new Long('-1').toString()).to.equal('-1'); @@ -12,7 +13,7 @@ describe('Long', function () { expect(new Long('13835058055282163712', true).toString()).to.equal('13835058055282163712'); }); - it('accepts BigInts in Long constructor', function () { + it('accepts BigInts in Long constructor', function() { expect(new Long(0n).toString()).to.equal('0'); expect(new Long(-1n).toString()).to.equal('-1'); expect(new Long(-1n, true).toString()).to.equal('18446744073709551615'); @@ -21,4 +22,14 @@ describe('Long', function () { expect(new Long(13835058055282163712n).toString()).to.equal('-4611686018427387904'); expect(new Long(13835058055282163712n, true).toString()).to.equal('13835058055282163712'); }); + + describe('static fromExtendedJSON()', function() { + it('is not affected by the legacy flag', function() { + const ejsonDoc = { $numberLong: "123456789123456789" }; + const longLegacy = Long.fromExtendedJSON(ejsonDoc, { legacy: true }); + const longNonLegacy = Long.fromExtendedJSON(ejsonDoc, { legacy: false }); + + expect(longLegacy).to.deep.equal(longNonLegacy); + }); + }); }); From 6a5364a752a370ec5d90e126b092974915b7267e Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 11 Jan 2023 11:48:09 -0500 Subject: [PATCH 14/36] test(NODE-4874): Add another assertion --- test/node/long.test.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/node/long.test.ts b/test/node/long.test.ts index 54200c3f..990cb61f 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -23,13 +23,16 @@ describe('Long', function() { expect(new Long(13835058055282163712n, true).toString()).to.equal('13835058055282163712'); }); - describe('static fromExtendedJSON()', function() { + describe.only('static fromExtendedJSON()', function() { it('is not affected by the legacy flag', function() { const ejsonDoc = { $numberLong: "123456789123456789" }; - const longLegacy = Long.fromExtendedJSON(ejsonDoc, { legacy: true }); - const longNonLegacy = Long.fromExtendedJSON(ejsonDoc, { legacy: false }); + const longRelaxedLegacy = Long.fromExtendedJSON(ejsonDoc, { legacy: true, relaxed: true }); + const longRelaxedNonLegacy = Long.fromExtendedJSON(ejsonDoc, { legacy: false, relaxed: true }); + const longCanonicalLegacy = Long.fromExtendedJSON(ejsonDoc, { legacy: true, relaxed: false}); + const longCanonicalNonLegacy = Long.fromExtendedJSON(ejsonDoc, { legacy: false, relaxed: false}); - expect(longLegacy).to.deep.equal(longNonLegacy); + expect(longRelaxedLegacy).to.deep.equal(longRelaxedNonLegacy); + expect(longCanonicalLegacy).to.deep.equal(longCanonicalNonLegacy); }); }); }); From 7eca3eadf7efb00d9b3152c2425d9970d7f0a323 Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 11 Jan 2023 11:49:00 -0500 Subject: [PATCH 15/36] style(NODE-4874): Update comment --- test/node/bigint.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index e7fdf708..0f388c96 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -274,7 +274,7 @@ describe('BSON BigInt support', function () { expectedResult: BSON.Document; }; - // NOTE: legacy is not changed here as it does not affect the output of parsing a Long/Number + // NOTE: legacy is not changed here as it does not affect the output of parsing a Long const useBigInt64Values = [true, false, undefined]; const relaxedValues = [true, false, undefined]; const sampleCanonicalString = '{"a":{"$numberLong":"23"}}'; From f6aed3de8feff135c3f2e3c5dbeeee2a209a791e Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 11 Jan 2023 12:49:17 -0500 Subject: [PATCH 16/36] test(NODE-4874): Remove 'only' annotation --- test/node/long.test.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/test/node/long.test.ts b/test/node/long.test.ts index 990cb61f..b741ddf3 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -1,8 +1,8 @@ import { expect } from 'chai'; import { Long } from '../register-bson'; -describe('Long', function() { - it('accepts strings in the constructor', function() { +describe('Long', function () { + it('accepts strings in the constructor', function () { expect(new Long('0').toString()).to.equal('0'); expect(new Long('00').toString()).to.equal('0'); expect(new Long('-1').toString()).to.equal('-1'); @@ -13,7 +13,7 @@ describe('Long', function() { expect(new Long('13835058055282163712', true).toString()).to.equal('13835058055282163712'); }); - it('accepts BigInts in Long constructor', function() { + it('accepts BigInts in Long constructor', function () { expect(new Long(0n).toString()).to.equal('0'); expect(new Long(-1n).toString()).to.equal('-1'); expect(new Long(-1n, true).toString()).to.equal('18446744073709551615'); @@ -23,13 +23,19 @@ describe('Long', function() { expect(new Long(13835058055282163712n, true).toString()).to.equal('13835058055282163712'); }); - describe.only('static fromExtendedJSON()', function() { - it('is not affected by the legacy flag', function() { - const ejsonDoc = { $numberLong: "123456789123456789" }; + describe('static fromExtendedJSON()', function () { + it('is not affected by the legacy flag', function () { + const ejsonDoc = { $numberLong: '123456789123456789' }; const longRelaxedLegacy = Long.fromExtendedJSON(ejsonDoc, { legacy: true, relaxed: true }); - const longRelaxedNonLegacy = Long.fromExtendedJSON(ejsonDoc, { legacy: false, relaxed: true }); - const longCanonicalLegacy = Long.fromExtendedJSON(ejsonDoc, { legacy: true, relaxed: false}); - const longCanonicalNonLegacy = Long.fromExtendedJSON(ejsonDoc, { legacy: false, relaxed: false}); + const longRelaxedNonLegacy = Long.fromExtendedJSON(ejsonDoc, { + legacy: false, + relaxed: true + }); + const longCanonicalLegacy = Long.fromExtendedJSON(ejsonDoc, { legacy: true, relaxed: false }); + const longCanonicalNonLegacy = Long.fromExtendedJSON(ejsonDoc, { + legacy: false, + relaxed: false + }); expect(longRelaxedLegacy).to.deep.equal(longRelaxedNonLegacy); expect(longCanonicalLegacy).to.deep.equal(longCanonicalNonLegacy); From b0e67aaeecf2b82bdc7d570684cbf89622f78c83 Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 11 Jan 2023 17:01:38 -0500 Subject: [PATCH 17/36] fix(NODE-4874): Update Long.fromExtendedJSON --- src/constants.ts | 5 +++++ src/extended_json.ts | 6 +++--- src/long.ts | 28 ++++++++++++++++++++++++++-- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 17fd8a55..c21eab1b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,6 +7,11 @@ export const BSON_INT64_MAX = Math.pow(2, 63) - 1; /** @internal */ export const BSON_INT64_MIN = -Math.pow(2, 63); +/** @internal */ +export const BSON_INT64_MAX_N = 2n ** 63n -1n; +/** @internal */ +export const BSON_INT64_MIN_N = 2n ** 63n -1n; + /** * Any integer up to 2^53 can be precisely represented by a double. * @internal diff --git a/src/extended_json.ts b/src/extended_json.ts index 7df9f475..75dafb60 100644 --- a/src/extended_json.ts +++ b/src/extended_json.ts @@ -76,7 +76,7 @@ function deserializeValue(value: any, options: EJSONOptions = {}) { const in64BitRange = value <= BSON_INT64_MAX && value >= BSON_INT64_MIN; if (options.relaxed || options.legacy) { - return Number.isInteger(value) && options.useBigInt64 ? BigInt(value) : value; + return value; } if (Number.isInteger(value) && !Object.is(value, -0)) { @@ -202,8 +202,8 @@ function serializeValue(value: any, options: EJSONSerializeOptions): any { throw new BSONError( 'Converting circular structure to EJSON:\n' + - ` ${leadingPart}${alreadySeen}${circularPart}${current}\n` + - ` ${leadingSpace}\\${dashes}/` + ` ${leadingPart}${alreadySeen}${circularPart}${current}\n` + + ` ${leadingSpace}\\${dashes}/` ); } options.seenObjects[options.seenObjects.length - 1].obj = value; diff --git a/src/long.ts b/src/long.ts index fe67dd1d..0839864b 100644 --- a/src/long.ts +++ b/src/long.ts @@ -1,6 +1,7 @@ import { BSONError } from './error'; import type { EJSONOptions } from './extended_json'; import type { Timestamp } from './timestamp'; +import { BSON_INT64_MAX, BSON_INT64_MIN } from './constants'; interface LongWASMHelpers { /** Gets the high bits of the last operation performed */ @@ -75,6 +76,18 @@ const INT_CACHE: { [key: number]: Long } = {}; /** A cache of the Long representations of small unsigned integer values. */ const UINT_CACHE: { [key: number]: Long } = {}; +/** + * Validate an int64 string. + * + * Fails on strings longer than 22 characters + * Fails on non-decimal strings */ +function validateInt64String(input: string): boolean { + if (input.length > 22) { + return false; + } + return /^(\+|\-)?[1-9][0-9]+$/.test(input); +} + /** @public */ export interface LongExtended { $numberLong: string; @@ -1025,11 +1038,22 @@ export class Long { doc: { $numberLong: string }, options?: EJSONOptions ): number | Long | bigint { - const longResult = Long.fromString(doc.$numberLong); const defaults = { useBigInt64: false, relaxed: true }; const ejsonOptions = { ...defaults, ...options }; + + if (!validateInt64String(doc.$numberLong)) { + throw new BSONError('Invalid int64 string'); + } + const longResult = Long.fromString(doc.$numberLong); + if (ejsonOptions.useBigInt64) { - return BigInt(doc.$numberLong); + const INT64_MAX = BigInt('0x7fffffffffffffff'); + const INT64_MIN = BigInt('0xffffffffffffffff'); + const result = BigInt(doc.$numberLong); + if (result > INT64_MAX || result < INT64_MIN) { + throw new BSONError('EJSON numberLong must be in int64 range'); + } + return result; } if (ejsonOptions.relaxed) { return longResult.toNumber(); From 43618719a282e3a5ef24090a84186c4e03b386b3 Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 11 Jan 2023 17:01:58 -0500 Subject: [PATCH 18/36] test(NODE-4874): Add new tests --- test/node/bigint.test.ts | 59 +++++++++++++++++++--------------------- test/node/long.test.ts | 51 ++++++++++++++++++++++++++++++---- test/register-bson.js | 2 ++ 3 files changed, 75 insertions(+), 37 deletions(-) diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index 0f388c96..600d764a 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -4,8 +4,8 @@ import { expect } from 'chai'; import { BSON_DATA_LONG } from '../../src/constants'; import { BSONDataView } from '../../src/utils/byte_utils'; -describe('BSON BigInt support', function () { - describe('BSON.deserialize()', function () { +describe('BSON BigInt support', function() { + describe('BSON.deserialize()', function() { type DeserialzationOptions = { useBigInt64: boolean | undefined; promoteValues: boolean | undefined; @@ -60,15 +60,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}`; @@ -102,7 +99,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 @@ -138,13 +135,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( @@ -158,7 +155,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( @@ -171,7 +168,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( @@ -184,7 +181,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( @@ -197,7 +194,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( @@ -221,7 +218,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( @@ -234,7 +231,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([ @@ -249,7 +246,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)); @@ -264,7 +261,7 @@ describe('BSON BigInt support', function () { }); }); - describe('EJSON.parse()', function () { + describe('EJSON.parse()', function() { type ParseOptions = { useBigInt64: boolean | undefined; relaxed: boolean | undefined; @@ -312,7 +309,7 @@ describe('BSON BigInt support', function () { }; } - describe('canonical input', function () { + describe('canonical input', function() { const canonicalInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { return relaxedValues.flatMap(relaxed => { return genTestTable( @@ -322,8 +319,8 @@ describe('BSON BigInt support', function () { useBigInt64IsSet ? { a: 23n } : relaxedIsSet - ? { a: 23 } - : { a: BSON.Long.fromNumber(23) } + ? { a: 23 } + : { a: BSON.Long.fromNumber(23) } ); }); }); @@ -340,18 +337,18 @@ describe('BSON BigInt support', function () { } }); - describe('relaxed integer input', function () { + describe('relaxed integer input', function() { const relaxedIntegerInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { return relaxedValues.flatMap(relaxed => { return genTestTable( useBigInt64, relaxed, (useBigInt64IsSet: boolean, relaxedIsSet: boolean) => - useBigInt64IsSet - ? { a: 4294967296n } - : relaxedIsSet + relaxedIsSet ? { a: 4294967296 } - : { a: BSON.Long.fromNumber(4294967296) } + : useBigInt64IsSet + ? { a: 4294967296n } + : { a: BSON.Long.fromNumber(4294967296) } ); }); }); @@ -367,7 +364,7 @@ describe('BSON BigInt support', function () { } }); - 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) } diff --git a/test/node/long.test.ts b/test/node/long.test.ts index b741ddf3..f3fa3462 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -1,8 +1,8 @@ import { expect } from 'chai'; -import { Long } from '../register-bson'; +import { Long, BSONError } from '../register-bson'; -describe('Long', function () { - it('accepts strings in the constructor', function () { +describe('Long', function() { + it('accepts strings in the constructor', function() { expect(new Long('0').toString()).to.equal('0'); expect(new Long('00').toString()).to.equal('0'); expect(new Long('-1').toString()).to.equal('-1'); @@ -13,7 +13,7 @@ describe('Long', function () { expect(new Long('13835058055282163712', true).toString()).to.equal('13835058055282163712'); }); - it('accepts BigInts in Long constructor', function () { + it('accepts BigInts in Long constructor', function() { expect(new Long(0n).toString()).to.equal('0'); expect(new Long(-1n).toString()).to.equal('-1'); expect(new Long(-1n, true).toString()).to.equal('18446744073709551615'); @@ -23,8 +23,8 @@ describe('Long', function () { expect(new Long(13835058055282163712n, true).toString()).to.equal('13835058055282163712'); }); - describe('static fromExtendedJSON()', function () { - it('is not affected by the legacy flag', function () { + describe('static fromExtendedJSON()', function() { + it('is not affected by the legacy flag', function() { const ejsonDoc = { $numberLong: '123456789123456789' }; const longRelaxedLegacy = Long.fromExtendedJSON(ejsonDoc, { legacy: true, relaxed: true }); const longRelaxedNonLegacy = Long.fromExtendedJSON(ejsonDoc, { @@ -40,5 +40,44 @@ describe('Long', function () { expect(longRelaxedLegacy).to.deep.equal(longRelaxedNonLegacy); expect(longCanonicalLegacy).to.deep.equal(longCanonicalNonLegacy); }); + + describe('rejects with BSONError', function() { + it('hex strings', function() { + expect(() => { + const ejsonDoc = { $numberLong: '0xffffffff' }; + return Long.fromExtendedJSON(ejsonDoc); + }).to.throw(BSONError); + }); + it('octal strings', function() { + expect(() => { + const ejsonDoc = { $numberLong: '0o1234567' }; + return Long.fromExtendedJSON(ejsonDoc); + }).to.throw(BSONError); + }); + it('strings longer than 22 characters', function() { + expect(() => { + const ejsonDoc = { $numberLong: '99999999999999999999999' } + return Long.fromExtendedJSON(ejsonDoc); + }).to.throw(BSONError); + }); + it('strings with leading zeros', function() { + expect(() => { + const ejsonDoc = { $numberLong: '000123456' } + return Long.fromExtendedJSON(ejsonDoc); + }).to.throw(BSONError); + }); + it('non-numeric strings', function() { + expect(() => { + const ejsonDoc = { $numberLong: 'hello world' } + return Long.fromExtendedJSON(ejsonDoc); + }).to.throw(BSONError); + }) + it('strings encoding numbers larger than 64 bits wide when useBigInt64 is true', function() { + expect(() => { + const ejsonDoc = { $numberLong: 0xf_ffff_ffff_ffff_ffffn.toString() } + return Long.fromExtendedJSON(ejsonDoc, {useBigInt64: true}); + }).to.throw(BSONError); + }); + }); }); }); diff --git a/test/register-bson.js b/test/register-bson.js index 37fda7e0..9d89afe2 100644 --- a/test/register-bson.js +++ b/test/register-bson.js @@ -31,6 +31,8 @@ chai.use(function (chai) { try { throwsAssertion.call(this, ...args); } catch (assertionError) { + console.log(JSON.stringify(assertionError), 2); + // FIXME(NODE-4874) if (assertionError.actual?.name === assertionError.expected) { return; } From 92ae3572e8e2db887f17dd8c57aa8edc3a8e88ae Mon Sep 17 00:00:00 2001 From: Warren James Date: Thu, 12 Jan 2023 14:38:51 -0500 Subject: [PATCH 19/36] fix(NODE-4874): Add fixes --- src/constants.ts | 5 ----- src/extended_json.ts | 4 ++-- src/long.ts | 18 ++++++++++-------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index c21eab1b..17fd8a55 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,11 +7,6 @@ export const BSON_INT64_MAX = Math.pow(2, 63) - 1; /** @internal */ export const BSON_INT64_MIN = -Math.pow(2, 63); -/** @internal */ -export const BSON_INT64_MAX_N = 2n ** 63n -1n; -/** @internal */ -export const BSON_INT64_MIN_N = 2n ** 63n -1n; - /** * Any integer up to 2^53 can be precisely represented by a double. * @internal diff --git a/src/extended_json.ts b/src/extended_json.ts index 75dafb60..263177d4 100644 --- a/src/extended_json.ts +++ b/src/extended_json.ts @@ -202,8 +202,8 @@ function serializeValue(value: any, options: EJSONSerializeOptions): any { throw new BSONError( 'Converting circular structure to EJSON:\n' + - ` ${leadingPart}${alreadySeen}${circularPart}${current}\n` + - ` ${leadingSpace}\\${dashes}/` + ` ${leadingPart}${alreadySeen}${circularPart}${current}\n` + + ` ${leadingSpace}\\${dashes}/` ); } options.seenObjects[options.seenObjects.length - 1].obj = value; diff --git a/src/long.ts b/src/long.ts index 0839864b..c1594ad1 100644 --- a/src/long.ts +++ b/src/long.ts @@ -1,7 +1,6 @@ import { BSONError } from './error'; import type { EJSONOptions } from './extended_json'; import type { Timestamp } from './timestamp'; -import { BSON_INT64_MAX, BSON_INT64_MIN } from './constants'; interface LongWASMHelpers { /** Gets the high bits of the last operation performed */ @@ -76,16 +75,19 @@ const INT_CACHE: { [key: number]: Long } = {}; /** A cache of the Long representations of small unsigned integer values. */ const UINT_CACHE: { [key: number]: Long } = {}; +const MAX_INT64_STRING_LENGTH = 22; + /** - * Validate an int64 string. - * - * Fails on strings longer than 22 characters - * Fails on non-decimal strings */ + * @internal + * Validate an int64 string. + * + * Fails on strings longer than 22 characters + * Fails on non-decimal strings */ function validateInt64String(input: string): boolean { - if (input.length > 22) { + if (input.length > MAX_INT64_STRING_LENGTH) { return false; } - return /^(\+|\-)?[1-9][0-9]+$/.test(input); + return /^(\+|-)?(0|[1-9][0-9]*)$/.test(input); } /** @public */ @@ -1048,7 +1050,7 @@ export class Long { if (ejsonOptions.useBigInt64) { const INT64_MAX = BigInt('0x7fffffffffffffff'); - const INT64_MIN = BigInt('0xffffffffffffffff'); + const INT64_MIN = -BigInt('0x8000000000000000'); const result = BigInt(doc.$numberLong); if (result > INT64_MAX || result < INT64_MIN) { throw new BSONError('EJSON numberLong must be in int64 range'); From fff3d327b8db93abaf9ab2f7767c6f8dda0298e7 Mon Sep 17 00:00:00 2001 From: Warren James Date: Thu, 12 Jan 2023 14:39:24 -0500 Subject: [PATCH 20/36] test(NODE-4874): Update tests --- test/node/bigint.test.ts | 55 ++++++++++++++++++---------------- test/node/long.test.ts | 65 +++++++++++++++++----------------------- test/register-bson.js | 2 -- 3 files changed, 57 insertions(+), 65 deletions(-) diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index 600d764a..49867d1a 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -4,8 +4,8 @@ import { expect } from 'chai'; import { BSON_DATA_LONG } from '../../src/constants'; import { BSONDataView } from '../../src/utils/byte_utils'; -describe('BSON BigInt support', function() { - describe('BSON.deserialize()', function() { +describe('BSON BigInt support', function () { + describe('BSON.deserialize()', function () { type DeserialzationOptions = { useBigInt64: boolean | undefined; promoteValues: boolean | undefined; @@ -60,12 +60,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}`; @@ -99,7 +102,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 @@ -135,13 +138,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( @@ -155,7 +158,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( @@ -168,7 +171,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( @@ -181,7 +184,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( @@ -194,7 +197,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( @@ -218,7 +221,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( @@ -231,7 +234,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([ @@ -246,7 +249,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)); @@ -261,7 +264,7 @@ describe('BSON BigInt support', function() { }); }); - describe('EJSON.parse()', function() { + describe('EJSON.parse()', function () { type ParseOptions = { useBigInt64: boolean | undefined; relaxed: boolean | undefined; @@ -309,7 +312,7 @@ describe('BSON BigInt support', function() { }; } - describe('canonical input', function() { + describe('canonical input', function () { const canonicalInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { return relaxedValues.flatMap(relaxed => { return genTestTable( @@ -319,8 +322,8 @@ describe('BSON BigInt support', function() { useBigInt64IsSet ? { a: 23n } : relaxedIsSet - ? { a: 23 } - : { a: BSON.Long.fromNumber(23) } + ? { a: 23 } + : { a: BSON.Long.fromNumber(23) } ); }); }); @@ -337,7 +340,7 @@ describe('BSON BigInt support', function() { } }); - describe('relaxed integer input', function() { + describe('relaxed integer input', function () { const relaxedIntegerInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { return relaxedValues.flatMap(relaxed => { return genTestTable( @@ -347,8 +350,8 @@ describe('BSON BigInt support', function() { relaxedIsSet ? { a: 4294967296 } : useBigInt64IsSet - ? { a: 4294967296n } - : { a: BSON.Long.fromNumber(4294967296) } + ? { a: 4294967296n } + : { a: BSON.Long.fromNumber(4294967296) } ); }); }); @@ -364,7 +367,7 @@ describe('BSON BigInt support', function() { } }); - 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) } diff --git a/test/node/long.test.ts b/test/node/long.test.ts index f3fa3462..3c4f8fb6 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -1,8 +1,8 @@ import { expect } from 'chai'; import { Long, BSONError } from '../register-bson'; -describe('Long', function() { - it('accepts strings in the constructor', function() { +describe('Long', function () { + it('accepts strings in the constructor', function () { expect(new Long('0').toString()).to.equal('0'); expect(new Long('00').toString()).to.equal('0'); expect(new Long('-1').toString()).to.equal('-1'); @@ -13,7 +13,7 @@ describe('Long', function() { expect(new Long('13835058055282163712', true).toString()).to.equal('13835058055282163712'); }); - it('accepts BigInts in Long constructor', function() { + it('accepts BigInts in Long constructor', function () { expect(new Long(0n).toString()).to.equal('0'); expect(new Long(-1n).toString()).to.equal('-1'); expect(new Long(-1n, true).toString()).to.equal('18446744073709551615'); @@ -23,8 +23,8 @@ describe('Long', function() { expect(new Long(13835058055282163712n, true).toString()).to.equal('13835058055282163712'); }); - describe('static fromExtendedJSON()', function() { - it('is not affected by the legacy flag', function() { + describe('static fromExtendedJSON()', function () { + it('is not affected by the legacy flag', function () { const ejsonDoc = { $numberLong: '123456789123456789' }; const longRelaxedLegacy = Long.fromExtendedJSON(ejsonDoc, { legacy: true, relaxed: true }); const longRelaxedNonLegacy = Long.fromExtendedJSON(ejsonDoc, { @@ -41,42 +41,33 @@ describe('Long', function() { expect(longCanonicalLegacy).to.deep.equal(longCanonicalNonLegacy); }); - describe('rejects with BSONError', function() { - it('hex strings', function() { - expect(() => { - const ejsonDoc = { $numberLong: '0xffffffff' }; - return Long.fromExtendedJSON(ejsonDoc); - }).to.throw(BSONError); + describe('rejects with BSONError', function () { + it('hex strings', function () { + const ejsonDoc = { $numberLong: '0xffffffff' }; + expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw(BSONError); }); - it('octal strings', function() { - expect(() => { - const ejsonDoc = { $numberLong: '0o1234567' }; - return Long.fromExtendedJSON(ejsonDoc); - }).to.throw(BSONError); + + it('octal strings', function () { + const ejsonDoc = { $numberLong: '0o1234567' }; + expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw(BSONError); + }); + + it('strings longer than 22 characters', function () { + const ejsonDoc = { $numberLong: '99999999999999999999999' }; + expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw(BSONError); }); - it('strings longer than 22 characters', function() { - expect(() => { - const ejsonDoc = { $numberLong: '99999999999999999999999' } - return Long.fromExtendedJSON(ejsonDoc); - }).to.throw(BSONError); + + it('strings with leading zeros', function () { + const ejsonDoc = { $numberLong: '000123456' }; + expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw(BSONError); }); - it('strings with leading zeros', function() { - expect(() => { - const ejsonDoc = { $numberLong: '000123456' } - return Long.fromExtendedJSON(ejsonDoc); - }).to.throw(BSONError); + it('non-numeric strings', function () { + const ejsonDoc = { $numberLong: 'hello world' }; + expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw(BSONError); }); - it('non-numeric strings', function() { - expect(() => { - const ejsonDoc = { $numberLong: 'hello world' } - return Long.fromExtendedJSON(ejsonDoc); - }).to.throw(BSONError); - }) - it('strings encoding numbers larger than 64 bits wide when useBigInt64 is true', function() { - expect(() => { - const ejsonDoc = { $numberLong: 0xf_ffff_ffff_ffff_ffffn.toString() } - return Long.fromExtendedJSON(ejsonDoc, {useBigInt64: true}); - }).to.throw(BSONError); + it('strings encoding numbers larger than 64 bits wide when useBigInt64 is true', function () { + const ejsonDoc = { $numberLong: 0xf_ffff_ffff_ffff_ffffn.toString() }; + expect(() => Long.fromExtendedJSON(ejsonDoc, { useBigInt64: true })).to.throw(BSONError); }); }); }); diff --git a/test/register-bson.js b/test/register-bson.js index 9d89afe2..37fda7e0 100644 --- a/test/register-bson.js +++ b/test/register-bson.js @@ -31,8 +31,6 @@ chai.use(function (chai) { try { throwsAssertion.call(this, ...args); } catch (assertionError) { - console.log(JSON.stringify(assertionError), 2); - // FIXME(NODE-4874) if (assertionError.actual?.name === assertionError.expected) { return; } From a86a942e9b7ece3068adde7b3518cfe294d75710 Mon Sep 17 00:00:00 2001 From: Warren James Date: Thu, 12 Jan 2023 14:47:38 -0500 Subject: [PATCH 21/36] style(NODE-4874): Change variable name --- src/long.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/long.ts b/src/long.ts index c1594ad1..a04b12df 100644 --- a/src/long.ts +++ b/src/long.ts @@ -1051,11 +1051,11 @@ export class Long { if (ejsonOptions.useBigInt64) { const INT64_MAX = BigInt('0x7fffffffffffffff'); const INT64_MIN = -BigInt('0x8000000000000000'); - const result = BigInt(doc.$numberLong); - if (result > INT64_MAX || result < INT64_MIN) { + const bigIntResult = BigInt(doc.$numberLong); + if (bigIntResult > INT64_MAX || bigIntResult < INT64_MIN) { throw new BSONError('EJSON numberLong must be in int64 range'); } - return result; + return bigIntResult; } if (ejsonOptions.relaxed) { return longResult.toNumber(); From d9f539fba45c00d6f77c153bae22021248f899bb Mon Sep 17 00:00:00 2001 From: Warren James Date: Fri, 13 Jan 2023 14:16:38 -0500 Subject: [PATCH 22/36] fix(NODE-4874): Fix MAX_INT64_STRING_LENGTH constant --- src/long.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/long.ts b/src/long.ts index a04b12df..198cc49b 100644 --- a/src/long.ts +++ b/src/long.ts @@ -75,15 +75,16 @@ const INT_CACHE: { [key: number]: Long } = {}; /** A cache of the Long representations of small unsigned integer values. */ const UINT_CACHE: { [key: number]: Long } = {}; -const MAX_INT64_STRING_LENGTH = 22; +const MAX_INT64_STRING_LENGTH = 20; /** * @internal - * Validate an int64 string. + * Checks that an int64 string is of the correct length and is represented in + * base 10 * - * Fails on strings longer than 22 characters + * Fails on strings longer than MAX_INT64_STRING_LENGTH characters * Fails on non-decimal strings */ -function validateInt64String(input: string): boolean { +function isInt64StrDecimalWithCorrectLength(input: string): boolean { if (input.length > MAX_INT64_STRING_LENGTH) { return false; } @@ -1043,10 +1044,9 @@ export class Long { const defaults = { useBigInt64: false, relaxed: true }; const ejsonOptions = { ...defaults, ...options }; - if (!validateInt64String(doc.$numberLong)) { + if (!isInt64StrDecimalWithCorrectLength(doc.$numberLong)) { throw new BSONError('Invalid int64 string'); } - const longResult = Long.fromString(doc.$numberLong); if (ejsonOptions.useBigInt64) { const INT64_MAX = BigInt('0x7fffffffffffffff'); @@ -1057,6 +1057,8 @@ export class Long { } return bigIntResult; } + + const longResult = Long.fromString(doc.$numberLong); if (ejsonOptions.relaxed) { return longResult.toNumber(); } From 0acf248a58a8645f6154657cc2afa17e7b314577 Mon Sep 17 00:00:00 2001 From: Warren James Date: Tue, 17 Jan 2023 14:02:39 -0500 Subject: [PATCH 23/36] test(NODE-4874): Test fixups --- test/node/bigint.test.ts | 41 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index 49867d1a..7e170848 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -294,10 +294,14 @@ describe('BSON BigInt support', function () { return [{ options: { useBigInt64, relaxed }, expectedResult }]; } - function generateTestDescription(entry: TestTableEntry, inputString: string): string { + function generateBehaviourDescription(entry: TestTableEntry, inputString: string): string { // TODO(NODE-4874): When NODE-4873 is merged in, replace this with EJSON.stringify + return `parses field 'a' of '${inputString}' to '${entry.expectedResult.a.constructor.name}' `; + } + + function generateConditionDescription(entry: TestTableEntry): string { const options = entry.options; - return `parses field 'a' of '${inputString}' to '${entry.expectedResult.a.constructor.name}' when useBigInt64 is ${options.useBigInt64} and relaxed is ${options.relaxed}`; + return `when useBigInt64 is ${options.useBigInt64} and relaxed is ${options.relaxed}`; } function generateTest(entry: TestTableEntry, sampleString: string): () => void { @@ -312,6 +316,18 @@ describe('BSON BigInt support', function () { }; } + function createTestsFromTestTable(table: TestTableEntry[], sampleString: string) { + for (const entry of table) { + const test = generateTest(entry, sampleString); + const condDescription = generateConditionDescription(entry); + const behaviourDescription = generateBehaviourDescription(entry, sampleString); + + describe(condDescription, function () { + it(behaviourDescription, test); + }); + } + } + describe('canonical input', function () { const canonicalInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { return relaxedValues.flatMap(relaxed => { @@ -332,12 +348,7 @@ describe('BSON BigInt support', function () { expect(canonicalInputTestTable).to.have.lengthOf(9); }); - for (const entry of canonicalInputTestTable) { - const test = generateTest(entry, sampleCanonicalString); - const description = generateTestDescription(entry, sampleCanonicalString); - - it(description, test); - } + createTestsFromTestTable(canonicalInputTestTable, sampleCanonicalString); }); describe('relaxed integer input', function () { @@ -359,12 +370,7 @@ describe('BSON BigInt support', function () { expect(relaxedIntegerInputTestTable).to.have.lengthOf(9); }); - for (const entry of relaxedIntegerInputTestTable) { - const test = generateTest(entry, sampleRelaxedIntegerString); - const description = generateTestDescription(entry, sampleRelaxedIntegerString); - - it(description, test); - } + createTestsFromTestTable(relaxedIntegerInputTestTable, sampleRelaxedIntegerString); }); describe('relaxed double input where double is outside of int32 range and useBigInt64 is true', function () { @@ -378,12 +384,7 @@ describe('BSON BigInt support', function () { expect(relaxedDoubleInputTestTable).to.have.lengthOf(3); }); - for (const entry of relaxedDoubleInputTestTable) { - const test = generateTest(entry, sampleRelaxedDoubleString); - const description = generateTestDescription(entry, sampleRelaxedDoubleString); - - it(description, test); - } + createTestsFromTestTable(relaxedDoubleInputTestTable, sampleRelaxedDoubleString); }); }); }); From cdf03341b141a0f7379552666637f3567df4e67d Mon Sep 17 00:00:00 2001 From: Warren James Date: Tue, 17 Jan 2023 15:00:59 -0500 Subject: [PATCH 24/36] fix(NODE-4874): Fix fromExtendedJSON checks and errors --- src/long.ts | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/long.ts b/src/long.ts index 198cc49b..29745adc 100644 --- a/src/long.ts +++ b/src/long.ts @@ -77,20 +77,6 @@ const UINT_CACHE: { [key: number]: Long } = {}; const MAX_INT64_STRING_LENGTH = 20; -/** - * @internal - * Checks that an int64 string is of the correct length and is represented in - * base 10 - * - * Fails on strings longer than MAX_INT64_STRING_LENGTH characters - * Fails on non-decimal strings */ -function isInt64StrDecimalWithCorrectLength(input: string): boolean { - if (input.length > MAX_INT64_STRING_LENGTH) { - return false; - } - return /^(\+|-)?(0|[1-9][0-9]*)$/.test(input); -} - /** @public */ export interface LongExtended { $numberLong: string; @@ -1043,9 +1029,14 @@ export class Long { ): number | Long | bigint { const defaults = { useBigInt64: false, relaxed: true }; const ejsonOptions = { ...defaults, ...options }; + const decimalRegEx = /^(\+?0|(\+|-)?[1-9][0-9]*)$/; + + if (doc.$numberLong.length > MAX_INT64_STRING_LENGTH) { + throw new BSONError('int64 string is too long'); + } - if (!isInt64StrDecimalWithCorrectLength(doc.$numberLong)) { - throw new BSONError('Invalid int64 string'); + if (!decimalRegEx.test(doc.$numberLong)) { + throw new BSONError('int64 string is not in decimal'); } if (ejsonOptions.useBigInt64) { From 027b5df81a82e31a7a7f91d23a17450ceb40b3d8 Mon Sep 17 00:00:00 2001 From: Warren James Date: Tue, 17 Jan 2023 16:54:22 -0500 Subject: [PATCH 25/36] fix(NODE-4874): Update fromExtendedJSON --- src/extended_json.ts | 2 +- src/long.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/extended_json.ts b/src/extended_json.ts index 263177d4..b4fec7fb 100644 --- a/src/extended_json.ts +++ b/src/extended_json.ts @@ -72,6 +72,7 @@ const keysToCodecs = { // eslint-disable-next-line @typescript-eslint/no-explicit-any function deserializeValue(value: any, options: EJSONOptions = {}) { if (typeof value === 'number') { + // TODO(NODE-4377): EJSON js number handling diverges from BSON const in32BitRange = value <= BSON_INT32_MAX && value >= BSON_INT32_MIN; const in64BitRange = value <= BSON_INT64_MAX && value >= BSON_INT64_MIN; @@ -85,7 +86,6 @@ function deserializeValue(value: any, options: EJSONOptions = {}) { return new Int32(value); } if (in64BitRange) { - // TODO(NODE-4377): EJSON js number handling diverges from BSON if (options.useBigInt64) { return BigInt(value); } diff --git a/src/long.ts b/src/long.ts index 29745adc..02d6b0e2 100644 --- a/src/long.ts +++ b/src/long.ts @@ -77,6 +77,8 @@ const UINT_CACHE: { [key: number]: Long } = {}; const MAX_INT64_STRING_LENGTH = 20; +const DECIMAL_REG_EX = /^(\+?0|(\+|-)?[1-9][0-9]*)$/; + /** @public */ export interface LongExtended { $numberLong: string; @@ -1029,14 +1031,13 @@ export class Long { ): number | Long | bigint { const defaults = { useBigInt64: false, relaxed: true }; const ejsonOptions = { ...defaults, ...options }; - const decimalRegEx = /^(\+?0|(\+|-)?[1-9][0-9]*)$/; if (doc.$numberLong.length > MAX_INT64_STRING_LENGTH) { throw new BSONError('int64 string is too long'); } - if (!decimalRegEx.test(doc.$numberLong)) { - throw new BSONError('int64 string is not in decimal'); + if (!DECIMAL_REG_EX.test(doc.$numberLong)) { + throw new BSONError('int64 string is not a valid decimal integer'); } if (ejsonOptions.useBigInt64) { From a02236f351254c238fb05e640e00b8b5c3607545 Mon Sep 17 00:00:00 2001 From: Warren James Date: Tue, 17 Jan 2023 16:55:12 -0500 Subject: [PATCH 26/36] test(NODE-4874): New tests --- test/node/long.test.ts | 106 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 96 insertions(+), 10 deletions(-) diff --git a/test/node/long.test.ts b/test/node/long.test.ts index 3c4f8fb6..25b53b2a 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -23,7 +23,7 @@ describe('Long', function () { expect(new Long(13835058055282163712n, true).toString()).to.equal('13835058055282163712'); }); - describe('static fromExtendedJSON()', function () { + describe.only('static fromExtendedJSON()', function () { it('is not affected by the legacy flag', function () { const ejsonDoc = { $numberLong: '123456789123456789' }; const longRelaxedLegacy = Long.fromExtendedJSON(ejsonDoc, { legacy: true, relaxed: true }); @@ -41,33 +41,119 @@ describe('Long', function () { expect(longCanonicalLegacy).to.deep.equal(longCanonicalNonLegacy); }); + describe.only('accepts', function () { + it('+0', function () { + const ejsonDoc = { $numberLong: '+0' }; + expect(Long.fromExtendedJSON(ejsonDoc, { relaxed: false })).to.deep.equal( + Long.fromNumber(0) + ); + }); + + it('negative integers within int64 range', function () { + const ejsonDoc = { $numberLong: '-1235498139' }; + expect(Long.fromExtendedJSON(ejsonDoc, { relaxed: false })).to.deep.equal( + Long.fromNumber(-1235498139) + ); + }); + + it('positive numbers within int64 range', function () { + const ejsonDoc = { $numberLong: '1234567129' }; + expect(Long.fromExtendedJSON(ejsonDoc, { relaxed: false })).to.deep.equal( + Long.fromNumber(1234567129) + ); + }); + }); + describe('rejects with BSONError', function () { it('hex strings', function () { const ejsonDoc = { $numberLong: '0xffffffff' }; - expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw(BSONError); + expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw( + BSONError, + 'int64 string is not a valid decimal integer' + ); }); it('octal strings', function () { const ejsonDoc = { $numberLong: '0o1234567' }; - expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw(BSONError); + expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw( + BSONError, + 'int64 string is not a valid decimal integer' + ); + }); + + it('binary strings', function () { + const ejsonDoc = { $numberLong: '0b010101101011' }; + expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw( + BSONError, + 'int64 string is not a valid decimal integer' + ); }); - it('strings longer than 22 characters', function () { + it('strings longer than 20 characters', function () { const ejsonDoc = { $numberLong: '99999999999999999999999' }; - expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw(BSONError); + expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw( + BSONError, + 'int64 string is too long' + ); }); it('strings with leading zeros', function () { const ejsonDoc = { $numberLong: '000123456' }; - expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw(BSONError); + expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw( + BSONError, + 'int64 string is not a valid decimal integer' + ); }); + it('non-numeric strings', function () { const ejsonDoc = { $numberLong: 'hello world' }; - expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw(BSONError); + expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw( + BSONError, + 'int64 string is not a valid decimal integer' + ); }); - it('strings encoding numbers larger than 64 bits wide when useBigInt64 is true', function () { - const ejsonDoc = { $numberLong: 0xf_ffff_ffff_ffff_ffffn.toString() }; - expect(() => Long.fromExtendedJSON(ejsonDoc, { useBigInt64: true })).to.throw(BSONError); + + it('-0', function () { + const ejsonDoc = { $numberLong: '-0' }; + expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw( + BSONError, + 'int64 string is not a valid decimal integer' + ); + }); + }); + + describe.only('when useBigInt64=true', function () { + it('rejects strings encoding positive numbers larger than 64 bits wide', function () { + const ejsonDoc = { $numberLong: 0xffff_ffff_ffff_ffffn.toString() }; + expect(() => Long.fromExtendedJSON(ejsonDoc, { useBigInt64: true })).to.throw( + BSONError, + 'EJSON numberLong must be in int64 range' + ); + }); + + it('strings encoding negative numbers larger than 64 bits wide', function () { + const ejsonDoc = { $numberLong: '-' + 0xbfff_ffff_ffff_ffffn.toString() }; + expect(() => Long.fromExtendedJSON(ejsonDoc, { useBigInt64: true })).to.throw( + BSONError, + 'EJSON numberLong must be in int64 range' + ); + }); + }); + + describe.only('when useBigInt64=false', function () { + it('truncates strings encoding positive numbers larger than 64 bits wide', function () { + const ejsonDoc = { $numberLong: 0xffff_ffff_ffff_ffffn.toString() }; + expect( + Long.fromExtendedJSON(ejsonDoc, { useBigInt64: false, relaxed: false }) + ).to.deep.equal(Long.fromNumber(-1)); + }); + + it('truncates strings encoding negative numbers larger than 64 bits wide', function () { + const ejsonDoc = { $numberLong: '-' + 0xffff_ffff_ffff_0000n.toString() }; + expect(() => Long.fromExtendedJSON(ejsonDoc, { useBigInt64: false, relaxed: false })).to.throw( + BSONError, + 'EJSON numberLong must be in int64 range' + ); }); }); }); From 31f8bf77d5f936e5c7190286bfec67962737e558 Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 18 Jan 2023 11:12:54 -0500 Subject: [PATCH 27/36] test(NODE-4874): Add tests for current validation behaviour --- test/node/long.test.ts | 65 ++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/test/node/long.test.ts b/test/node/long.test.ts index 25b53b2a..fdfe20ec 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -23,7 +23,7 @@ describe('Long', function () { expect(new Long(13835058055282163712n, true).toString()).to.equal('13835058055282163712'); }); - describe.only('static fromExtendedJSON()', function () { + describe('static fromExtendedJSON()', function () { it('is not affected by the legacy flag', function () { const ejsonDoc = { $numberLong: '123456789123456789' }; const longRelaxedLegacy = Long.fromExtendedJSON(ejsonDoc, { legacy: true, relaxed: true }); @@ -41,7 +41,7 @@ describe('Long', function () { expect(longCanonicalLegacy).to.deep.equal(longCanonicalNonLegacy); }); - describe.only('accepts', function () { + describe('accepts', function () { it('+0', function () { const ejsonDoc = { $numberLong: '+0' }; expect(Long.fromExtendedJSON(ejsonDoc, { relaxed: false })).to.deep.equal( @@ -122,38 +122,41 @@ describe('Long', function () { }); }); - describe.only('when useBigInt64=true', function () { - it('rejects strings encoding positive numbers larger than 64 bits wide', function () { - const ejsonDoc = { $numberLong: 0xffff_ffff_ffff_ffffn.toString() }; - expect(() => Long.fromExtendedJSON(ejsonDoc, { useBigInt64: true })).to.throw( - BSONError, - 'EJSON numberLong must be in int64 range' - ); - }); - - it('strings encoding negative numbers larger than 64 bits wide', function () { - const ejsonDoc = { $numberLong: '-' + 0xbfff_ffff_ffff_ffffn.toString() }; - expect(() => Long.fromExtendedJSON(ejsonDoc, { useBigInt64: true })).to.throw( - BSONError, - 'EJSON numberLong must be in int64 range' - ); + describe('when useBigInt64=true', function () { + describe('rejects', function () { + it('strings encoding positive numbers larger than 64 bits wide', function () { + const ejsonDoc = { $numberLong: '9223372036854775808' }; + expect(() => Long.fromExtendedJSON(ejsonDoc, { useBigInt64: true })).to.throw( + BSONError, + 'EJSON numberLong must be in int64 range' + ); + }); + + it('strings encoding negative numbers larger than 64 bits wide', function () { + const ejsonDoc = { $numberLong: '9223372036854775808' }; + expect(() => Long.fromExtendedJSON(ejsonDoc, { useBigInt64: true })).to.throw( + BSONError, + 'EJSON numberLong must be in int64 range' + ); + }); }); }); - describe.only('when useBigInt64=false', function () { - it('truncates strings encoding positive numbers larger than 64 bits wide', function () { - const ejsonDoc = { $numberLong: 0xffff_ffff_ffff_ffffn.toString() }; - expect( - Long.fromExtendedJSON(ejsonDoc, { useBigInt64: false, relaxed: false }) - ).to.deep.equal(Long.fromNumber(-1)); - }); - - it('truncates strings encoding negative numbers larger than 64 bits wide', function () { - const ejsonDoc = { $numberLong: '-' + 0xffff_ffff_ffff_0000n.toString() }; - expect(() => Long.fromExtendedJSON(ejsonDoc, { useBigInt64: false, relaxed: false })).to.throw( - BSONError, - 'EJSON numberLong must be in int64 range' - ); + describe('when useBigInt64=false', function () { + describe('truncates', function () { + it('strings encoding positive numbers larger than 64 bits wide', function () { + const ejsonDoc = { $numberLong: '9223372036854775808' }; + expect( + Long.fromExtendedJSON(ejsonDoc, { useBigInt64: false, relaxed: false }) + ).to.deep.equal(Long.fromBigInt(-9223372036854775808n)); + }); + + it('strings encoding negative numbers larger than 64 bits wide', function () { + const ejsonDoc = { $numberLong: '-9223372036854775809' }; + expect( + Long.fromExtendedJSON(ejsonDoc, { useBigInt64: false, relaxed: false }) + ).to.deep.equal(Long.fromBigInt(9223372036854775807n)); + }); }); }); }); From f22ce030800a96b2fe9a44537cc65955492342bd Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 18 Jan 2023 11:16:53 -0500 Subject: [PATCH 28/36] fix(NODE-4874): Small style fix --- src/long.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/long.ts b/src/long.ts index 02d6b0e2..cad7ea5c 100644 --- a/src/long.ts +++ b/src/long.ts @@ -1029,8 +1029,7 @@ export class Long { doc: { $numberLong: string }, options?: EJSONOptions ): number | Long | bigint { - const defaults = { useBigInt64: false, relaxed: true }; - const ejsonOptions = { ...defaults, ...options }; + const { useBigInt64 = false, relaxed = true } = { ...options }; if (doc.$numberLong.length > MAX_INT64_STRING_LENGTH) { throw new BSONError('int64 string is too long'); @@ -1040,7 +1039,7 @@ export class Long { throw new BSONError('int64 string is not a valid decimal integer'); } - if (ejsonOptions.useBigInt64) { + if (useBigInt64) { const INT64_MAX = BigInt('0x7fffffffffffffff'); const INT64_MIN = -BigInt('0x8000000000000000'); const bigIntResult = BigInt(doc.$numberLong); @@ -1051,7 +1050,7 @@ export class Long { } const longResult = Long.fromString(doc.$numberLong); - if (ejsonOptions.relaxed) { + if (relaxed) { return longResult.toNumber(); } return longResult; From 8b0a7411b286864126d5eb748562014583e9ac82 Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 18 Jan 2023 11:27:28 -0500 Subject: [PATCH 29/36] test(NODE-4874): Add comments and fix test value --- test/node/long.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/node/long.test.ts b/test/node/long.test.ts index fdfe20ec..dcd38936 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -124,16 +124,16 @@ describe('Long', function () { describe('when useBigInt64=true', function () { describe('rejects', function () { - it('strings encoding positive numbers larger than 64 bits wide', function () { - const ejsonDoc = { $numberLong: '9223372036854775808' }; + it('positive numbers outside int64 range', function () { + const ejsonDoc = { $numberLong: '9223372036854775808' }; // 2^63 expect(() => Long.fromExtendedJSON(ejsonDoc, { useBigInt64: true })).to.throw( BSONError, 'EJSON numberLong must be in int64 range' ); }); - it('strings encoding negative numbers larger than 64 bits wide', function () { - const ejsonDoc = { $numberLong: '9223372036854775808' }; + it('negative numbers outside int64 range', function () { + const ejsonDoc = { $numberLong: '-9223372036854775809' }; // -2^63 - 1 expect(() => Long.fromExtendedJSON(ejsonDoc, { useBigInt64: true })).to.throw( BSONError, 'EJSON numberLong must be in int64 range' @@ -144,15 +144,15 @@ describe('Long', function () { describe('when useBigInt64=false', function () { describe('truncates', function () { - it('strings encoding positive numbers larger than 64 bits wide', function () { - const ejsonDoc = { $numberLong: '9223372036854775808' }; + it('positive numbers outside int64 range', function () { + const ejsonDoc = { $numberLong: '9223372036854775808' }; // 2^63 expect( Long.fromExtendedJSON(ejsonDoc, { useBigInt64: false, relaxed: false }) ).to.deep.equal(Long.fromBigInt(-9223372036854775808n)); }); - it('strings encoding negative numbers larger than 64 bits wide', function () { - const ejsonDoc = { $numberLong: '-9223372036854775809' }; + it('negative numbers outside int64 range', function () { + const ejsonDoc = { $numberLong: '-9223372036854775809' }; // -2^63 - 1 expect( Long.fromExtendedJSON(ejsonDoc, { useBigInt64: false, relaxed: false }) ).to.deep.equal(Long.fromBigInt(9223372036854775807n)); From e0049b3c7b46d324d8d9fcbe002df62391a70a7b Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 18 Jan 2023 14:56:32 -0500 Subject: [PATCH 30/36] fix(NODE-4874): Make error messages more verbose --- src/long.ts | 11 ++++++++--- test/node/long.test.ts | 21 +++++++++------------ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/long.ts b/src/long.ts index cad7ea5c..0ec64b61 100644 --- a/src/long.ts +++ b/src/long.ts @@ -1032,11 +1032,11 @@ export class Long { const { useBigInt64 = false, relaxed = true } = { ...options }; if (doc.$numberLong.length > MAX_INT64_STRING_LENGTH) { - throw new BSONError('int64 string is too long'); + throw new BSONError(`$numberLong string "${doc.$numberLong}"is too long`); } if (!DECIMAL_REG_EX.test(doc.$numberLong)) { - throw new BSONError('int64 string is not a valid decimal integer'); + throw new BSONError(`$numberLong string "${doc.$numberLong}" is in an invalid format`); } if (useBigInt64) { @@ -1044,7 +1044,12 @@ export class Long { const INT64_MIN = -BigInt('0x8000000000000000'); const bigIntResult = BigInt(doc.$numberLong); if (bigIntResult > INT64_MAX || bigIntResult < INT64_MIN) { - throw new BSONError('EJSON numberLong must be in int64 range'); + throw new BSONError( + `EJSON numberLong must be in int64 range; got ${BigInt.asIntN( + 64, + bigIntResult + ).toString()}` + ); } return bigIntResult; } diff --git a/test/node/long.test.ts b/test/node/long.test.ts index dcd38936..f8e58f64 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -69,7 +69,7 @@ describe('Long', function () { const ejsonDoc = { $numberLong: '0xffffffff' }; expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw( BSONError, - 'int64 string is not a valid decimal integer' + /is in an invalid format/ ); }); @@ -77,7 +77,7 @@ describe('Long', function () { const ejsonDoc = { $numberLong: '0o1234567' }; expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw( BSONError, - 'int64 string is not a valid decimal integer' + /is in an invalid format/ ); }); @@ -85,23 +85,20 @@ describe('Long', function () { const ejsonDoc = { $numberLong: '0b010101101011' }; expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw( BSONError, - 'int64 string is not a valid decimal integer' + /is in an invalid format/ ); }); it('strings longer than 20 characters', function () { const ejsonDoc = { $numberLong: '99999999999999999999999' }; - expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw( - BSONError, - 'int64 string is too long' - ); + expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw(BSONError, /is too long/); }); it('strings with leading zeros', function () { const ejsonDoc = { $numberLong: '000123456' }; expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw( BSONError, - 'int64 string is not a valid decimal integer' + /is in an invalid format/ ); }); @@ -109,7 +106,7 @@ describe('Long', function () { const ejsonDoc = { $numberLong: 'hello world' }; expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw( BSONError, - 'int64 string is not a valid decimal integer' + /is in an invalid format/ ); }); @@ -117,7 +114,7 @@ describe('Long', function () { const ejsonDoc = { $numberLong: '-0' }; expect(() => Long.fromExtendedJSON(ejsonDoc)).to.throw( BSONError, - 'int64 string is not a valid decimal integer' + /is in an invalid format/ ); }); }); @@ -128,7 +125,7 @@ describe('Long', function () { const ejsonDoc = { $numberLong: '9223372036854775808' }; // 2^63 expect(() => Long.fromExtendedJSON(ejsonDoc, { useBigInt64: true })).to.throw( BSONError, - 'EJSON numberLong must be in int64 range' + /EJSON numberLong must be in int64 range; got/ ); }); @@ -136,7 +133,7 @@ describe('Long', function () { const ejsonDoc = { $numberLong: '-9223372036854775809' }; // -2^63 - 1 expect(() => Long.fromExtendedJSON(ejsonDoc, { useBigInt64: true })).to.throw( BSONError, - 'EJSON numberLong must be in int64 range' + /EJSON numberLong must be in int64 range; got/ ); }); }); From 47cdb658611e3acd5aead83c2ac069792f3f9c02 Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 18 Jan 2023 15:35:37 -0500 Subject: [PATCH 31/36] fix(NODE-4874): Fix error messages --- src/long.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/long.ts b/src/long.ts index 0ec64b61..291181ba 100644 --- a/src/long.ts +++ b/src/long.ts @@ -1032,7 +1032,7 @@ export class Long { const { useBigInt64 = false, relaxed = true } = { ...options }; if (doc.$numberLong.length > MAX_INT64_STRING_LENGTH) { - throw new BSONError(`$numberLong string "${doc.$numberLong}"is too long`); + throw new BSONError('$numberLong string is too long'); } if (!DECIMAL_REG_EX.test(doc.$numberLong)) { @@ -1044,12 +1044,7 @@ export class Long { const INT64_MIN = -BigInt('0x8000000000000000'); const bigIntResult = BigInt(doc.$numberLong); if (bigIntResult > INT64_MAX || bigIntResult < INT64_MIN) { - throw new BSONError( - `EJSON numberLong must be in int64 range; got ${BigInt.asIntN( - 64, - bigIntResult - ).toString()}` - ); + throw new BSONError(`EJSON numberLong must be in int64 range; got ${bigIntResult}`); } return bigIntResult; } From f80b7079cbb497866376f437a4805deabe6e707e Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 18 Jan 2023 15:54:16 -0500 Subject: [PATCH 32/36] fix(NODE-4874): merge conflict fix --- test/node/bigint.test.ts | 364 ++++++++++++++++++++------------------- 1 file changed, 183 insertions(+), 181 deletions(-) diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index bcaac73e..fe71da57 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -263,223 +263,225 @@ describe('BSON BigInt support', function () { expect(serializedMap).to.deep.equal(expectedSerialization); }); }); +}); - describe('EJSON.parse()', function () { - type ParseOptions = { - useBigInt64: boolean | undefined; - relaxed: boolean | undefined; - }; - type TestTableEntry = { - options: ParseOptions; - expectedResult: BSON.Document; +describe('EJSON.parse()', function () { + type ParseOptions = { + useBigInt64: boolean | undefined; + relaxed: boolean | undefined; + }; + type TestTableEntry = { + options: ParseOptions; + expectedResult: BSON.Document; + }; + + // NOTE: legacy is not changed here as it does not affect the output of parsing a Long + const useBigInt64Values = [true, false, undefined]; + const relaxedValues = [true, false, undefined]; + const sampleCanonicalString = '{"a":{"$numberLong":"23"}}'; + const sampleRelaxedIntegerString = '{"a":4294967296}'; + const sampleRelaxedDoubleString = '{"a": 2147483647.9}'; + + function genTestTable( + useBigInt64: boolean | undefined, + relaxed: boolean | undefined, + getExpectedResult: (boolean, boolean) => BSON.Document + ): [TestTableEntry] { + const useBigInt64IsSet = useBigInt64 ?? false; + const relaxedIsSet = relaxed ?? true; + + const expectedResult = getExpectedResult(useBigInt64IsSet, relaxedIsSet); + + return [{ options: { useBigInt64, relaxed }, expectedResult }]; + } + + function generateBehaviourDescription(entry: TestTableEntry, inputString: string): string { + // TODO(NODE-4874): When NODE-4873 is merged in, replace this with EJSON.stringify + return `parses field 'a' of '${inputString}' to '${entry.expectedResult.a.constructor.name}' `; + } + + function generateConditionDescription(entry: TestTableEntry): string { + const options = entry.options; + return `when useBigInt64 is ${options.useBigInt64} and relaxed is ${options.relaxed}`; + } + + function generateTest(entry: TestTableEntry, sampleString: string): () => void { + const options = entry.options; + + return () => { + const parsed = EJSON.parse(sampleString, { + useBigInt64: options.useBigInt64, + relaxed: options.relaxed + }); + expect(parsed).to.deep.equal(entry.expectedResult); }; + } - // NOTE: legacy is not changed here as it does not affect the output of parsing a Long - const useBigInt64Values = [true, false, undefined]; - const relaxedValues = [true, false, undefined]; - const sampleCanonicalString = '{"a":{"$numberLong":"23"}}'; - const sampleRelaxedIntegerString = '{"a":4294967296}'; - const sampleRelaxedDoubleString = '{"a": 2147483647.9}'; - - function genTestTable( - useBigInt64: boolean | undefined, - relaxed: boolean | undefined, - getExpectedResult: (boolean, boolean) => BSON.Document - ): [TestTableEntry] { - const useBigInt64IsSet = useBigInt64 ?? false; - const relaxedIsSet = relaxed ?? true; - - const expectedResult = getExpectedResult(useBigInt64IsSet, relaxedIsSet); - - return [{ options: { useBigInt64, relaxed }, expectedResult }]; - } + function createTestsFromTestTable(table: TestTableEntry[], sampleString: string) { + for (const entry of table) { + const test = generateTest(entry, sampleString); + const condDescription = generateConditionDescription(entry); + const behaviourDescription = generateBehaviourDescription(entry, sampleString); - function generateBehaviourDescription(entry: TestTableEntry, inputString: string): string { - // TODO(NODE-4874): When NODE-4873 is merged in, replace this with EJSON.stringify - return `parses field 'a' of '${inputString}' to '${entry.expectedResult.a.constructor.name}' `; - } - - function generateConditionDescription(entry: TestTableEntry): string { - const options = entry.options; - return `when useBigInt64 is ${options.useBigInt64} and relaxed is ${options.relaxed}`; - } - - function generateTest(entry: TestTableEntry, sampleString: string): () => void { - const options = entry.options; - - return () => { - const parsed = EJSON.parse(sampleString, { - useBigInt64: options.useBigInt64, - relaxed: options.relaxed - }); - expect(parsed).to.deep.equal(entry.expectedResult); - }; + describe(condDescription, function () { + it(behaviourDescription, test); + }); } + } + + describe('canonical input', function () { + const canonicalInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { + return relaxedValues.flatMap(relaxed => { + return genTestTable( + useBigInt64, + relaxed, + (useBigInt64IsSet: boolean, relaxedIsSet: boolean) => + useBigInt64IsSet + ? { a: 23n } + : relaxedIsSet + ? { a: 23 } + : { a: BSON.Long.fromNumber(23) } + ); + }); + }); - function createTestsFromTestTable(table: TestTableEntry[], sampleString: string) { - for (const entry of table) { - const test = generateTest(entry, sampleString); - const condDescription = generateConditionDescription(entry); - const behaviourDescription = generateBehaviourDescription(entry, sampleString); - - describe(condDescription, function () { - it(behaviourDescription, test); - }); - } - } + it('meta test: generates 9 tests', () => { + expect(canonicalInputTestTable).to.have.lengthOf(9); + }); - describe('canonical input', function () { - const canonicalInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { - return relaxedValues.flatMap(relaxed => { - return genTestTable( - useBigInt64, - relaxed, - (useBigInt64IsSet: boolean, relaxedIsSet: boolean) => - useBigInt64IsSet - ? { a: 23n } - : relaxedIsSet - ? { a: 23 } - : { a: BSON.Long.fromNumber(23) } - ); - }); - }); + createTestsFromTestTable(canonicalInputTestTable, sampleCanonicalString); + }); - it('meta test: generates 9 tests', () => { - expect(canonicalInputTestTable).to.have.lengthOf(9); + describe('relaxed integer input', function () { + const relaxedIntegerInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { + return relaxedValues.flatMap(relaxed => { + return genTestTable( + useBigInt64, + relaxed, + (useBigInt64IsSet: boolean, relaxedIsSet: boolean) => + relaxedIsSet + ? { a: 4294967296 } + : useBigInt64IsSet + ? { a: 4294967296n } + : { a: BSON.Long.fromNumber(4294967296) } + ); }); - - createTestsFromTestTable(canonicalInputTestTable, sampleCanonicalString); + }); + it('meta test: generates 9 tests', () => { + expect(relaxedIntegerInputTestTable).to.have.lengthOf(9); }); - describe('relaxed integer input', function () { - const relaxedIntegerInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { - return relaxedValues.flatMap(relaxed => { - return genTestTable( - useBigInt64, - relaxed, - (useBigInt64IsSet: boolean, relaxedIsSet: boolean) => - relaxedIsSet - ? { a: 4294967296 } - : useBigInt64IsSet - ? { a: 4294967296n } - : { a: BSON.Long.fromNumber(4294967296) } - ); - }); - }); - it('meta test: generates 9 tests', () => { - expect(relaxedIntegerInputTestTable).to.have.lengthOf(9); - }); + createTestsFromTestTable(relaxedIntegerInputTestTable, sampleRelaxedIntegerString); + }); - createTestsFromTestTable(relaxedIntegerInputTestTable, sampleRelaxedIntegerString); + 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) } + ); }); - 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) } - ); - }); - - it('meta test: generates 3 tests', () => { - expect(relaxedDoubleInputTestTable).to.have.lengthOf(3); - }); + it('meta test: generates 3 tests', () => { + expect(relaxedDoubleInputTestTable).to.have.lengthOf(3); + }); - createTestsFromTestTable(relaxedDoubleInputTestTable, sampleRelaxedDoubleString); - 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"}}'); - }); + createTestsFromTestTable(relaxedDoubleInputTestTable, sampleRelaxedDoubleString); + 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 () { - const number = { a: 0x1234_5678_1234_5678_9999n }; - const stringified = EJSON.stringify(number, { relaxed: false }); - const serialized = BSON.serialize(number); + 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); - const VALUE_OFFSET = 7; - const dataView = BSONDataView.fromUint8Array(serialized); - const serializedValue = dataView.getBigInt64(VALUE_OFFSET, true); - const parsed = JSON.parse(stringified); + const VALUE_OFFSET = 7; + const dataView = BSONDataView.fromUint8Array(serialized); + const serializedValue = dataView.getBigInt64(VALUE_OFFSET, true); + const parsed = JSON.parse(stringified); - expect(parsed).to.have.property('a'); - expect(parsed['a']).to.have.property('$numberLong'); - expect(parsed.a.$numberLong).to.equal(0x5678_1234_5678_9999n.toString()); + expect(parsed).to.have.property('a'); + expect(parsed['a']).to.have.property('$numberLong'); + expect(parsed.a.$numberLong).to.equal(0x5678_1234_5678_9999n.toString()); - expect(parsed.a.$numberLong).to.equal(serializedValue.toString()); - }); - 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"}}'); + expect(parsed.a.$numberLong).to.equal(serializedValue.toString()); + }); + 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 () { - 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); + 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); - const VALUE_OFFSET = 7; - const dataView = BSONDataView.fromUint8Array(serializedDoc); - const parsed = JSON.parse(stringified); + const VALUE_OFFSET = 7; + const dataView = BSONDataView.fromUint8Array(serializedDoc); + const parsed = JSON.parse(stringified); - expect(parsed).to.have.property('a'); - expect(parsed.a).to.equal(0x0000_1234_5678_9999); + expect(parsed).to.have.property('a'); + expect(parsed.a).to.equal(0x0000_1234_5678_9999); - expect(parsed.a).to.equal(Number(dataView.getBigInt64(VALUE_OFFSET, true))); - }); + expect(parsed.a).to.equal(Number(dataView.getBigInt64(VALUE_OFFSET, true))); + }); - 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('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 () { - 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}'); + 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 () { - let parsed; + context('when passed bigint values that are 64 bits wide or less', function () { + let parsed; - before(function () { - const number = { a: 12345n }; - const serialized = EJSON.stringify(number, { relaxed: false }); - parsed = JSON.parse(serialized); - }); + before(function () { + const number = { a: 12345n }; + const serialized = EJSON.stringify(number, { relaxed: false }); + parsed = JSON.parse(serialized); + }); - it('passes loose equality checks with native bigint values', function () { - // eslint-disable-next-line eqeqeq - expect(parsed.a.$numberLong == 12345n).true; - }); + 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 () { - expect(parsed.a.$numberLong).to.equal(12345n.toString()); + 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 () { - let parsed; + context('when passed bigint values that are more than 64 bits wide', function () { + let parsed; - before(function () { - const number = { a: 0x1234_5678_1234_5678_9999n }; - const serialized = EJSON.stringify(number, { relaxed: false }); - parsed = JSON.parse(serialized); - }); + before(function () { + const number = { a: 0x1234_5678_1234_5678_9999n }; + const serialized = EJSON.stringify(number, { relaxed: false }); + parsed = JSON.parse(serialized); + }); - 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('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 () { - expect(parsed.a.$numberLong).to.not.equal(0x1234_5678_1234_5678_9999n.toString()); + it('not equal to results of BigInt.toString', function () { + expect(parsed.a.$numberLong).to.not.equal(0x1234_5678_1234_5678_9999n.toString()); + }); }); }); }); From c90b7833a3cd3b8fcb4ca58911f77d42fad972d5 Mon Sep 17 00:00:00 2001 From: Warren James Date: Thu, 19 Jan 2023 14:07:32 -0500 Subject: [PATCH 33/36] fix(NODE-4874): Update fromExtendedJSON behaviour to be more consistent with fromString --- src/long.ts | 7 +------ test/node/long.test.ts | 12 +++++------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/long.ts b/src/long.ts index 6582f554..609f9392 100644 --- a/src/long.ts +++ b/src/long.ts @@ -1042,13 +1042,8 @@ export class Long extends BSONValue { } if (useBigInt64) { - const INT64_MAX = BigInt('0x7fffffffffffffff'); - const INT64_MIN = -BigInt('0x8000000000000000'); const bigIntResult = BigInt(doc.$numberLong); - if (bigIntResult > INT64_MAX || bigIntResult < INT64_MIN) { - throw new BSONError(`EJSON numberLong must be in int64 range; got ${bigIntResult}`); - } - return bigIntResult; + return BigInt.asIntN(64, bigIntResult); } const longResult = Long.fromString(doc.$numberLong); diff --git a/test/node/long.test.ts b/test/node/long.test.ts index f8e58f64..66f3fd2f 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -120,20 +120,18 @@ describe('Long', function () { }); describe('when useBigInt64=true', function () { - describe('rejects', function () { + describe('truncates', function () { it('positive numbers outside int64 range', function () { const ejsonDoc = { $numberLong: '9223372036854775808' }; // 2^63 - expect(() => Long.fromExtendedJSON(ejsonDoc, { useBigInt64: true })).to.throw( - BSONError, - /EJSON numberLong must be in int64 range; got/ + expect(Long.fromExtendedJSON(ejsonDoc, { useBigInt64: true })).to.deep.equal( + -9223372036854775808n ); }); it('negative numbers outside int64 range', function () { const ejsonDoc = { $numberLong: '-9223372036854775809' }; // -2^63 - 1 - expect(() => Long.fromExtendedJSON(ejsonDoc, { useBigInt64: true })).to.throw( - BSONError, - /EJSON numberLong must be in int64 range; got/ + expect(Long.fromExtendedJSON(ejsonDoc, { useBigInt64: true })).to.deep.equal( + 9223372036854775807n ); }); }); From 43b39c128a7128fc1166f7d539898ef96d460b9b Mon Sep 17 00:00:00 2001 From: Warren James Date: Thu, 19 Jan 2023 14:54:53 -0500 Subject: [PATCH 34/36] test(NODE-4874): move EJSON.parse tests back under the correct describe block --- test/node/bigint.test.ts | 370 +++++++++++++++++++-------------------- 1 file changed, 185 insertions(+), 185 deletions(-) diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index fe71da57..e0ed18d2 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -263,224 +263,224 @@ describe('BSON BigInt support', function () { expect(serializedMap).to.deep.equal(expectedSerialization); }); }); -}); -describe('EJSON.parse()', function () { - type ParseOptions = { - useBigInt64: boolean | undefined; - relaxed: boolean | undefined; - }; - type TestTableEntry = { - options: ParseOptions; - expectedResult: BSON.Document; - }; - - // NOTE: legacy is not changed here as it does not affect the output of parsing a Long - const useBigInt64Values = [true, false, undefined]; - const relaxedValues = [true, false, undefined]; - const sampleCanonicalString = '{"a":{"$numberLong":"23"}}'; - const sampleRelaxedIntegerString = '{"a":4294967296}'; - const sampleRelaxedDoubleString = '{"a": 2147483647.9}'; - - function genTestTable( - useBigInt64: boolean | undefined, - relaxed: boolean | undefined, - getExpectedResult: (boolean, boolean) => BSON.Document - ): [TestTableEntry] { - const useBigInt64IsSet = useBigInt64 ?? false; - const relaxedIsSet = relaxed ?? true; - - const expectedResult = getExpectedResult(useBigInt64IsSet, relaxedIsSet); - - return [{ options: { useBigInt64, relaxed }, expectedResult }]; - } - - function generateBehaviourDescription(entry: TestTableEntry, inputString: string): string { - // TODO(NODE-4874): When NODE-4873 is merged in, replace this with EJSON.stringify - return `parses field 'a' of '${inputString}' to '${entry.expectedResult.a.constructor.name}' `; - } - - function generateConditionDescription(entry: TestTableEntry): string { - const options = entry.options; - return `when useBigInt64 is ${options.useBigInt64} and relaxed is ${options.relaxed}`; - } - - function generateTest(entry: TestTableEntry, sampleString: string): () => void { - const options = entry.options; - - return () => { - const parsed = EJSON.parse(sampleString, { - useBigInt64: options.useBigInt64, - relaxed: options.relaxed - }); - expect(parsed).to.deep.equal(entry.expectedResult); + describe('EJSON.parse()', function () { + type ParseOptions = { + useBigInt64: boolean | undefined; + relaxed: boolean | undefined; + }; + type TestTableEntry = { + options: ParseOptions; + expectedResult: BSON.Document; }; - } - - function createTestsFromTestTable(table: TestTableEntry[], sampleString: string) { - for (const entry of table) { - const test = generateTest(entry, sampleString); - const condDescription = generateConditionDescription(entry); - const behaviourDescription = generateBehaviourDescription(entry, sampleString); - describe(condDescription, function () { - it(behaviourDescription, test); - }); + // NOTE: legacy is not changed here as it does not affect the output of parsing a Long + const useBigInt64Values = [true, false, undefined]; + const relaxedValues = [true, false, undefined]; + const sampleCanonicalString = '{"a":{"$numberLong":"23"}}'; + const sampleRelaxedIntegerString = '{"a":4294967296}'; + const sampleRelaxedDoubleString = '{"a": 2147483647.9}'; + + function genTestTable( + useBigInt64: boolean | undefined, + relaxed: boolean | undefined, + getExpectedResult: (boolean, boolean) => BSON.Document + ): [TestTableEntry] { + const useBigInt64IsSet = useBigInt64 ?? false; + const relaxedIsSet = relaxed ?? true; + + const expectedResult = getExpectedResult(useBigInt64IsSet, relaxedIsSet); + + return [{ options: { useBigInt64, relaxed }, expectedResult }]; } - } - - describe('canonical input', function () { - const canonicalInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { - return relaxedValues.flatMap(relaxed => { - return genTestTable( - useBigInt64, - relaxed, - (useBigInt64IsSet: boolean, relaxedIsSet: boolean) => - useBigInt64IsSet - ? { a: 23n } - : relaxedIsSet - ? { a: 23 } - : { a: BSON.Long.fromNumber(23) } - ); - }); - }); - it('meta test: generates 9 tests', () => { - expect(canonicalInputTestTable).to.have.lengthOf(9); - }); - - createTestsFromTestTable(canonicalInputTestTable, sampleCanonicalString); - }); + function generateBehaviourDescription(entry: TestTableEntry, inputString: string): string { + // TODO(NODE-4874): When NODE-4873 is merged in, replace this with EJSON.stringify + return `parses field 'a' of '${inputString}' to '${entry.expectedResult.a.constructor.name}' `; + } - describe('relaxed integer input', function () { - const relaxedIntegerInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { - return relaxedValues.flatMap(relaxed => { - return genTestTable( - useBigInt64, - relaxed, - (useBigInt64IsSet: boolean, relaxedIsSet: boolean) => - relaxedIsSet - ? { a: 4294967296 } - : useBigInt64IsSet - ? { a: 4294967296n } - : { a: BSON.Long.fromNumber(4294967296) } - ); - }); - }); - it('meta test: generates 9 tests', () => { - expect(relaxedIntegerInputTestTable).to.have.lengthOf(9); - }); + function generateConditionDescription(entry: TestTableEntry): string { + const options = entry.options; + return `when useBigInt64 is ${options.useBigInt64} and relaxed is ${options.relaxed}`; + } - createTestsFromTestTable(relaxedIntegerInputTestTable, sampleRelaxedIntegerString); - }); + function generateTest(entry: TestTableEntry, sampleString: string): () => void { + const options = entry.options; - 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) } - ); - }); + return () => { + const parsed = EJSON.parse(sampleString, { + useBigInt64: options.useBigInt64, + relaxed: options.relaxed + }); + expect(parsed).to.deep.equal(entry.expectedResult); + }; + } - it('meta test: generates 3 tests', () => { - expect(relaxedDoubleInputTestTable).to.have.lengthOf(3); - }); + function createTestsFromTestTable(table: TestTableEntry[], sampleString: string) { + for (const entry of table) { + const test = generateTest(entry, sampleString); + const condDescription = generateConditionDescription(entry); + const behaviourDescription = generateBehaviourDescription(entry, sampleString); - createTestsFromTestTable(relaxedDoubleInputTestTable, sampleRelaxedDoubleString); - 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"}}'); + describe(condDescription, function () { + it(behaviourDescription, test); }); + } + } - 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); + describe('canonical input', function () { + const canonicalInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { + return relaxedValues.flatMap(relaxed => { + return genTestTable( + useBigInt64, + relaxed, + (useBigInt64IsSet: boolean, relaxedIsSet: boolean) => + useBigInt64IsSet + ? { a: 23n } + : relaxedIsSet + ? { a: 23 } + : { a: BSON.Long.fromNumber(23) } + ); + }); + }); - const VALUE_OFFSET = 7; - const dataView = BSONDataView.fromUint8Array(serialized); - const serializedValue = dataView.getBigInt64(VALUE_OFFSET, true); - const parsed = JSON.parse(stringified); + it('meta test: generates 9 tests', () => { + expect(canonicalInputTestTable).to.have.lengthOf(9); + }); - expect(parsed).to.have.property('a'); - expect(parsed['a']).to.have.property('$numberLong'); - expect(parsed.a.$numberLong).to.equal(0x5678_1234_5678_9999n.toString()); + createTestsFromTestTable(canonicalInputTestTable, sampleCanonicalString); + }); - expect(parsed.a.$numberLong).to.equal(serializedValue.toString()); - }); - 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"}}'); + describe('relaxed integer input', function () { + const relaxedIntegerInputTestTable = useBigInt64Values.flatMap(useBigInt64 => { + return relaxedValues.flatMap(relaxed => { + return genTestTable( + useBigInt64, + relaxed, + (useBigInt64IsSet: boolean, relaxedIsSet: boolean) => + relaxedIsSet + ? { a: 4294967296 } + : useBigInt64IsSet + ? { a: 4294967296n } + : { a: BSON.Long.fromNumber(4294967296) } + ); }); }); + it('meta test: generates 9 tests', () => { + expect(relaxedIntegerInputTestTable).to.have.lengthOf(9); + }); - 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); - - const VALUE_OFFSET = 7; - const dataView = BSONDataView.fromUint8Array(serializedDoc); - const parsed = JSON.parse(stringified); + createTestsFromTestTable(relaxedIntegerInputTestTable, sampleRelaxedIntegerString); + }); - expect(parsed).to.have.property('a'); - expect(parsed.a).to.equal(0x0000_1234_5678_9999); + 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) } + ); + }); - expect(parsed.a).to.equal(Number(dataView.getBigInt64(VALUE_OFFSET, true))); - }); + it('meta test: generates 3 tests', () => { + expect(relaxedDoubleInputTestTable).to.have.lengthOf(3); + }); - it('serializes bigint values to Number', function () { - const number = { a: 10000n }; - const serialized = EJSON.stringify(number, { relaxed: true }); - expect(serialized).to.equal('{"a":10000}'); + createTestsFromTestTable(relaxedDoubleInputTestTable, sampleRelaxedDoubleString); + 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 () { + const number = { a: 0x1234_5678_1234_5678_9999n }; + const stringified = EJSON.stringify(number, { relaxed: false }); + const serialized = BSON.serialize(number); + + const VALUE_OFFSET = 7; + const dataView = BSONDataView.fromUint8Array(serialized); + const serializedValue = dataView.getBigInt64(VALUE_OFFSET, true); + const parsed = JSON.parse(stringified); + + expect(parsed).to.have.property('a'); + expect(parsed['a']).to.have.property('$numberLong'); + expect(parsed.a.$numberLong).to.equal(0x5678_1234_5678_9999n.toString()); + + expect(parsed.a.$numberLong).to.equal(serializedValue.toString()); + }); + 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"}}'); + }); }); - 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('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); + + const VALUE_OFFSET = 7; + const dataView = BSONDataView.fromUint8Array(serializedDoc); + const parsed = JSON.parse(stringified); + + expect(parsed).to.have.property('a'); + expect(parsed.a).to.equal(0x0000_1234_5678_9999); + + expect(parsed.a).to.equal(Number(dataView.getBigInt64(VALUE_OFFSET, true))); + }); + + 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 () { + 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 () { - let parsed; + context('when passed bigint values that are 64 bits wide or less', function () { + let parsed; - before(function () { - const number = { a: 12345n }; - const serialized = EJSON.stringify(number, { relaxed: false }); - parsed = JSON.parse(serialized); - }); + before(function () { + const number = { a: 12345n }; + const serialized = EJSON.stringify(number, { relaxed: false }); + parsed = JSON.parse(serialized); + }); - it('passes loose equality checks with native bigint values', function () { - // eslint-disable-next-line eqeqeq - expect(parsed.a.$numberLong == 12345n).true; - }); + 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 () { - expect(parsed.a.$numberLong).to.equal(12345n.toString()); + 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 () { - let parsed; + context('when passed bigint values that are more than 64 bits wide', function () { + let parsed; - before(function () { - const number = { a: 0x1234_5678_1234_5678_9999n }; - const serialized = EJSON.stringify(number, { relaxed: false }); - parsed = JSON.parse(serialized); - }); + before(function () { + const number = { a: 0x1234_5678_1234_5678_9999n }; + const serialized = EJSON.stringify(number, { relaxed: false }); + parsed = JSON.parse(serialized); + }); - 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('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 () { - expect(parsed.a.$numberLong).to.not.equal(0x1234_5678_1234_5678_9999n.toString()); + it('not equal to results of BigInt.toString', function () { + expect(parsed.a.$numberLong).to.not.equal(0x1234_5678_1234_5678_9999n.toString()); + }); }); }); }); From b99a141142ab08de4a45bafadbc0c0b1ce9005ce Mon Sep 17 00:00:00 2001 From: Warren James Date: Thu, 19 Jan 2023 15:07:59 -0500 Subject: [PATCH 35/36] test(NODE-4874): Unnest EJSON.stringify fron EJSON.parse --- test/node/bigint.test.ts | 177 ++++++++++++++++++++------------------- 1 file changed, 89 insertions(+), 88 deletions(-) diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index e0ed18d2..fca8bf66 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -385,103 +385,104 @@ describe('BSON BigInt support', function () { }); createTestsFromTestTable(relaxedDoubleInputTestTable, sampleRelaxedDoubleString); - 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 () { - const number = { a: 0x1234_5678_1234_5678_9999n }; - const stringified = EJSON.stringify(number, { relaxed: false }); - const serialized = BSON.serialize(number); - - const VALUE_OFFSET = 7; - const dataView = BSONDataView.fromUint8Array(serialized); - const serializedValue = dataView.getBigInt64(VALUE_OFFSET, true); - const parsed = JSON.parse(stringified); - - expect(parsed).to.have.property('a'); - expect(parsed['a']).to.have.property('$numberLong'); - expect(parsed.a.$numberLong).to.equal(0x5678_1234_5678_9999n.toString()); - - expect(parsed.a.$numberLong).to.equal(serializedValue.toString()); - }); - 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 () { - 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); - - const VALUE_OFFSET = 7; - const dataView = BSONDataView.fromUint8Array(serializedDoc); - const parsed = JSON.parse(stringified); - - expect(parsed).to.have.property('a'); - expect(parsed.a).to.equal(0x0000_1234_5678_9999); - - expect(parsed.a).to.equal(Number(dataView.getBigInt64(VALUE_OFFSET, true))); - }); - - 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 () { - 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}'); - }); - }); + 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"}}'); + }); - context('when passed bigint values that are 64 bits wide or less', function () { - let parsed; + 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); - before(function () { - const number = { a: 12345n }; - const serialized = EJSON.stringify(number, { relaxed: false }); - parsed = JSON.parse(serialized); - }); + const VALUE_OFFSET = 7; + const dataView = BSONDataView.fromUint8Array(serialized); + const serializedValue = dataView.getBigInt64(VALUE_OFFSET, true); + const parsed = JSON.parse(stringified); - it('passes loose equality checks with native bigint values', function () { - // eslint-disable-next-line eqeqeq - expect(parsed.a.$numberLong == 12345n).true; - }); + expect(parsed).to.have.property('a'); + expect(parsed['a']).to.have.property('$numberLong'); + expect(parsed.a.$numberLong).to.equal(0x5678_1234_5678_9999n.toString()); - it('equals the result of BigInt.toString', function () { - expect(parsed.a.$numberLong).to.equal(12345n.toString()); - }); - }); + expect(parsed.a.$numberLong).to.equal(serializedValue.toString()); + }); + 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('when passed bigint values that are more than 64 bits wide', function () { - let parsed; + 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); - before(function () { - const number = { a: 0x1234_5678_1234_5678_9999n }; - const serialized = EJSON.stringify(number, { relaxed: false }); - parsed = JSON.parse(serialized); - }); + const VALUE_OFFSET = 7; + const dataView = BSONDataView.fromUint8Array(serializedDoc); + const parsed = JSON.parse(stringified); - 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; - }); + expect(parsed).to.have.property('a'); + expect(parsed.a).to.equal(0x0000_1234_5678_9999); - it('not equal to results of BigInt.toString', function () { - expect(parsed.a.$numberLong).to.not.equal(0x1234_5678_1234_5678_9999n.toString()); - }); - }); + expect(parsed.a).to.equal(Number(dataView.getBigInt64(VALUE_OFFSET, true))); + }); + + 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 () { + 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 () { + let parsed; + + before(function () { + const number = { a: 12345n }; + const serialized = EJSON.stringify(number, { relaxed: false }); + parsed = JSON.parse(serialized); + }); + + 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 () { + expect(parsed.a.$numberLong).to.equal(12345n.toString()); + }); + }); + + context('when passed bigint values that are more than 64 bits wide', function () { + let parsed; + + before(function () { + const number = { a: 0x1234_5678_1234_5678_9999n }; + const serialized = EJSON.stringify(number, { relaxed: false }); + parsed = JSON.parse(serialized); + }); + + 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 () { + expect(parsed.a.$numberLong).to.not.equal(0x1234_5678_1234_5678_9999n.toString()); }); }); }); From a0b5cf00485bc2f9fbe39dde1a8a00b6e314a62d Mon Sep 17 00:00:00 2001 From: Warren James Date: Thu, 19 Jan 2023 15:24:27 -0500 Subject: [PATCH 36/36] test(NODE-4874): Remove todo --- test/node/bigint.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index fca8bf66..9c962eef 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -295,7 +295,6 @@ describe('BSON BigInt support', function () { } function generateBehaviourDescription(entry: TestTableEntry, inputString: string): string { - // TODO(NODE-4874): When NODE-4873 is merged in, replace this with EJSON.stringify return `parses field 'a' of '${inputString}' to '${entry.expectedResult.a.constructor.name}' `; }