diff --git a/README.md b/README.md
index 273fa356..0e7a675e 100644
--- a/README.md
+++ b/README.md
@@ -238,12 +238,13 @@ Serialize a Javascript object using a predefined Buffer and index into the buffe
| buffer | Buffer
| | the buffer containing the serialized set of BSON documents. |
| [options.evalFunctions] | Object
| false
| evaluate functions in the BSON document scoped to the object deserialized. |
| [options.cacheFunctions] | Object
| false
| cache evaluated functions for reuse. |
+| [options.useBigInt64] | Object
| false
| when deserializing a Long will return a BigInt. |
| [options.promoteLongs] | Object
| true
| when deserializing a Long will fit it into a Number if it's smaller than 53 bits |
| [options.promoteBuffers] | Object
| false
| when deserializing a Binary will return it as a node.js Buffer instance. |
| [options.promoteValues] | Object
| false
| when deserializing will promote BSON values to their Node.js closest equivalent types. |
| [options.fieldsAsRaw] | Object
|
| allow to specify if there what fields we wish to return as unserialized raw buffer. |
| [options.bsonRegExp] | Object
| false
| return BSON regular expressions as BSONRegExp instances. |
-| [options.allowObjectSmallerThanBufferSize] | boolean
| false
| allows the buffer to be larger than the parsed BSON object |
+| [options.allowObjectSmallerThanBufferSize] | boolean
| false
| allows the buffer to be larger than the parsed BSON object. |
Deserialize data as BSON.
diff --git a/src/double.ts b/src/double.ts
index 379aebd7..6477f381 100644
--- a/src/double.ts
+++ b/src/double.ts
@@ -57,23 +57,17 @@ export class Double {
return this.value;
}
- // NOTE: JavaScript has +0 and -0, apparently to model limit calculations. If a user
- // explicitly provided `-0` then we need to ensure the sign makes it into the output
if (Object.is(Math.sign(this.value), -0)) {
- return { $numberDouble: `-${this.value.toFixed(1)}` };
+ // NOTE: JavaScript has +0 and -0, apparently to model limit calculations. If a user
+ // explicitly provided `-0` then we need to ensure the sign makes it into the output
+ return { $numberDouble: '-0.0' };
}
- let $numberDouble: string;
if (Number.isInteger(this.value)) {
- $numberDouble = this.value.toFixed(1);
- if ($numberDouble.length >= 13) {
- $numberDouble = this.value.toExponential(13).toUpperCase();
- }
+ return { $numberDouble: `${this.value}.0` };
} else {
- $numberDouble = this.value.toString();
+ return { $numberDouble: `${this.value}` };
}
-
- return { $numberDouble };
}
/** @internal */
diff --git a/src/parser/deserializer.ts b/src/parser/deserializer.ts
index 1a7d7690..50d7a8d8 100644
--- a/src/parser/deserializer.ts
+++ b/src/parser/deserializer.ts
@@ -14,12 +14,14 @@ import { ObjectId } from '../objectid';
import { BSONRegExp } from '../regexp';
import { BSONSymbol } from '../symbol';
import { Timestamp } from '../timestamp';
-import { ByteUtils } from '../utils/byte_utils';
+import { BSONDataView, ByteUtils } from '../utils/byte_utils';
import { validateUtf8 } from '../validate_utf8';
/** @public */
export interface DeserializeOptions {
- /** when deserializing a Long will fit it into a Number if it's smaller than 53 bits */
+ /** when deserializing a Long will return as a BigInt. */
+ useBigInt64?: boolean;
+ /** when deserializing a Long will fit it into a Number if it's smaller than 53 bits. */
promoteLongs?: boolean;
/** when deserializing a Binary will return it as a node.js Buffer instance. */
promoteBuffers?: boolean;
@@ -29,7 +31,7 @@ export interface DeserializeOptions {
fieldsAsRaw?: Document;
/** return BSON regular expressions as BSONRegExp instances. */
bsonRegExp?: boolean;
- /** allows the buffer to be larger than the parsed BSON object */
+ /** allows the buffer to be larger than the parsed BSON object. */
allowObjectSmallerThanBufferSize?: boolean;
/** Offset into buffer to begin reading document from */
index?: number;
@@ -96,7 +98,7 @@ export function internalDeserialize(
);
}
- // Start deserializtion
+ // Start deserialization
return deserializeObject(buffer, index, options, isArray);
}
@@ -117,9 +119,18 @@ function deserializeObject(
const bsonRegExp = typeof options['bsonRegExp'] === 'boolean' ? options['bsonRegExp'] : false;
// Controls the promotion of values vs wrapper classes
- const promoteBuffers = options['promoteBuffers'] == null ? false : options['promoteBuffers'];
- const promoteLongs = options['promoteLongs'] == null ? true : options['promoteLongs'];
- const promoteValues = options['promoteValues'] == null ? true : options['promoteValues'];
+ const promoteBuffers = options.promoteBuffers ?? false;
+ const promoteLongs = options.promoteLongs ?? true;
+ const promoteValues = options.promoteValues ?? true;
+ const useBigInt64 = options.useBigInt64 ?? false;
+
+ if (useBigInt64 && !promoteValues) {
+ throw new BSONError('Must either request bigint or Long for int64 deserialization');
+ }
+
+ if (useBigInt64 && !promoteLongs) {
+ throw new BSONError('Must either request bigint or Long for int64 deserialization');
+ }
// Ensures default validation option if none given
const validation = options.validation == null ? { utf8: true } : options.validation;
@@ -323,6 +334,8 @@ function deserializeObject(
value = null;
} else if (elementType === constants.BSON_DATA_LONG) {
// Unpack the low and high bits
+ const dataview = BSONDataView.fromUint8Array(buffer.subarray(index, index + 8));
+
const lowBits =
buffer[index++] |
(buffer[index++] << 8) |
@@ -334,8 +347,10 @@ function deserializeObject(
(buffer[index++] << 16) |
(buffer[index++] << 24);
const long = new Long(lowBits, highBits);
- // Promote the long if possible
- if (promoteLongs && promoteValues === true) {
+ if (useBigInt64) {
+ value = dataview.getBigInt64(0, true);
+ } else if (promoteLongs && promoteValues === true) {
+ // Promote the long if possible
value =
long.lessThanOrEqual(JS_INT_MAX_LONG) && long.greaterThanOrEqual(JS_INT_MIN_LONG)
? long.toNumber()
diff --git a/src/parser/serializer.ts b/src/parser/serializer.ts
index 1ee93a23..ba694df1 100644
--- a/src/parser/serializer.ts
+++ b/src/parser/serializer.ts
@@ -12,15 +12,7 @@ import type { MinKey } from '../min_key';
import type { ObjectId } from '../objectid';
import type { BSONRegExp } from '../regexp';
import { ByteUtils } from '../utils/byte_utils';
-import {
- isAnyArrayBuffer,
- isBigInt64Array,
- isBigUInt64Array,
- isDate,
- isMap,
- isRegExp,
- isUint8Array
-} from './utils';
+import { isAnyArrayBuffer, isDate, isMap, isRegExp, isUint8Array } from './utils';
/** @public */
export interface SerializeOptions {
@@ -103,6 +95,20 @@ function serializeNumber(buffer: Uint8Array, key: string, value: number, index:
return index;
}
+function serializeBigInt(buffer: Uint8Array, key: string, value: bigint, index: number) {
+ buffer[index++] = constants.BSON_DATA_LONG;
+ // Number of written bytes
+ const numberOfWrittenBytes = ByteUtils.encodeUTF8Into(buffer, key, index);
+ // Encode the name
+ index += numberOfWrittenBytes;
+ buffer[index++] = 0;
+ NUMBER_SPACE.setBigInt64(0, value, true);
+ // Write BigInt value
+ buffer.set(EIGHT_BYTE_VIEW_ON_NUMBER, index);
+ index += EIGHT_BYTE_VIEW_ON_NUMBER.byteLength;
+ return index;
+}
+
function serializeNull(buffer: Uint8Array, key: string, _: unknown, index: number) {
// Set long type
buffer[index++] = constants.BSON_DATA_NULL;
@@ -675,7 +681,7 @@ export function serializeInto(
} else if (typeof value === 'number') {
index = serializeNumber(buffer, key, value, index);
} else if (typeof value === 'bigint') {
- throw new BSONError('Unsupported type BigInt, please use Decimal128');
+ index = serializeBigInt(buffer, key, value, index);
} else if (typeof value === 'boolean') {
index = serializeBoolean(buffer, key, value, index);
} else if (value instanceof Date || isDate(value)) {
@@ -779,8 +785,8 @@ export function serializeInto(
index = serializeString(buffer, key, value, index);
} else if (type === 'number') {
index = serializeNumber(buffer, key, value, index);
- } else if (type === 'bigint' || isBigInt64Array(value) || isBigUInt64Array(value)) {
- throw new BSONError('Unsupported type BigInt, please use Decimal128');
+ } else if (type === 'bigint') {
+ index = serializeBigInt(buffer, key, value, index);
} else if (type === 'boolean') {
index = serializeBoolean(buffer, key, value, index);
} else if (value instanceof Date || isDate(value)) {
@@ -885,7 +891,7 @@ export function serializeInto(
} else if (type === 'number') {
index = serializeNumber(buffer, key, value, index);
} else if (type === 'bigint') {
- throw new BSONError('Unsupported type BigInt, please use Decimal128');
+ index = serializeBigInt(buffer, key, value, index);
} else if (type === 'boolean') {
index = serializeBoolean(buffer, key, value, index);
} else if (value instanceof Date || isDate(value)) {
diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts
new file mode 100644
index 00000000..fcc95790
--- /dev/null
+++ b/test/node/bigint.test.ts
@@ -0,0 +1,266 @@
+import { BSON, BSONError } from '../register-bson';
+import { bufferFromHexArray } from './tools/utils';
+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 () {
+ type DeserialzationOptions = {
+ useBigInt64: boolean | undefined;
+ promoteValues: boolean | undefined;
+ promoteLongs: boolean | undefined;
+ };
+ type TestTableEntry = {
+ options: DeserialzationOptions;
+ shouldThrow: boolean;
+ expectedResult: BSON.Document;
+ expectedErrorMessage: string;
+ };
+ const testSerializedDoc = bufferFromHexArray(['12', '6100', '2300000000000000']); // key 'a', value 0x23 as int64
+ const useBigInt64Values = [true, false, undefined];
+ const promoteLongsValues = [true, false, undefined];
+ const promoteValuesValues = [true, false, undefined];
+
+ const testTable = useBigInt64Values.flatMap(useBigInt64 => {
+ return promoteLongsValues.flatMap(promoteLongs => {
+ return promoteValuesValues.flatMap(promoteValues => {
+ const useBigInt64Set = useBigInt64 ?? false;
+ const promoteLongsSet = promoteLongs ?? true;
+ const promoteValuesSet = promoteValues ?? true;
+ const shouldThrow = useBigInt64Set && (!promoteValuesSet || !promoteLongsSet);
+ let expectedResult: BSON.Document;
+ if (useBigInt64Set) {
+ expectedResult = { a: 0x23n };
+ } else if (promoteLongsSet && promoteValuesSet) {
+ expectedResult = { a: 0x23 };
+ } else {
+ expectedResult = { a: new BSON.Long(0x23) };
+ }
+ const expectedErrorMessage = shouldThrow
+ ? 'Must either request bigint or Long for int64 deserialization'
+ : '';
+ return [
+ {
+ options: { useBigInt64, promoteValues, promoteLongs },
+ shouldThrow,
+ expectedResult,
+ expectedErrorMessage
+ }
+ ];
+ });
+ });
+ });
+
+ it('meta test: generates 27 tests with exactly 5 error cases and 22 success cases', () => {
+ expect(testTable).to.have.lengthOf(27);
+ expect(testTable.filter(t => t.shouldThrow)).to.have.lengthOf(5);
+ expect(testTable.filter(t => !t.shouldThrow)).to.have.lengthOf(22);
+ });
+
+ 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 flagString = `${useBigInt64}, ${promoteValues}, and ${promoteLongs}`;
+ if (entry.shouldThrow) {
+ return `throws when ${flagString}`;
+ } else {
+ return `deserializes int64 to ${entry.expectedResult.a.constructor.name} when ${flagString}`;
+ }
+ }
+
+ function generateTest(test: TestTableEntry) {
+ const options = test.options;
+ const deserialize = () => {
+ return BSON.deserialize(testSerializedDoc, options);
+ };
+ if (test.shouldThrow) {
+ return () => {
+ expect(deserialize).to.throw(BSONError, test.expectedErrorMessage);
+ };
+ } else {
+ return () => {
+ const deserializedDoc = deserialize();
+ expect(deserializedDoc).to.deep.equal(test.expectedResult);
+ };
+ }
+ }
+
+ for (const tableEntry of testTable) {
+ const test = generateTest(tableEntry);
+ const description = generateTestDescription(tableEntry);
+
+ it(description, test);
+ }
+ });
+
+ 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
+ type SerializedDocParts = {
+ dataType: number;
+ key: string;
+ value: bigint;
+ };
+ /**
+ * NOTE: this function operates on serialized BSON documents with the shape { : }
+ * where n is some int64. This function assumes that keys are properly encoded
+ * with the necessary null byte at the end and only at the end of the key string
+ */
+ function getSerializedDocParts(serializedDoc: Uint8Array): SerializedDocParts {
+ const DATA_TYPE_OFFSET = 4;
+ const KEY_OFFSET = 5;
+
+ const dataView = BSONDataView.fromUint8Array(serializedDoc);
+ const keySlice = serializedDoc.slice(KEY_OFFSET);
+
+ let keyLength = 0;
+ while (keySlice[keyLength++] !== 0);
+
+ const valueOffset = KEY_OFFSET + keyLength;
+ const key = Buffer.from(serializedDoc.slice(KEY_OFFSET, KEY_OFFSET + keyLength)).toString(
+ 'utf8'
+ );
+
+ return {
+ dataType: dataView.getInt8(DATA_TYPE_OFFSET),
+ key: key.slice(0, keyLength - 1),
+ value: dataView.getBigInt64(valueOffset, true)
+ };
+ }
+
+ 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 () {
+ const testDoc = { a: 0x1234567812345678n };
+ const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc));
+ const expectedResult = getSerializedDocParts(
+ bufferFromHexArray([
+ '12', // int64 type
+ '6100', // 'a' key with null terminator
+ '7856341278563412'
+ ])
+ );
+
+ expect(expectedResult.value).to.equal(serializedDoc.value);
+ });
+
+ 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(
+ bufferFromHexArray([
+ '12', // int64 type
+ '6100', // 'a' key with null terminator
+ '2300000000000000' // little endian int64
+ ])
+ );
+ expect(serializedDoc).to.deep.equal(expectedResult);
+ });
+
+ 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(
+ bufferFromHexArray([
+ '12', // int64
+ '6100', // 'a' key with null terminator
+ 'f1ffffffffffffff'
+ ])
+ );
+ expect(serializedDoc).to.deep.equal(expectedResult);
+ });
+
+ 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(
+ bufferFromHexArray([
+ '12', // int64
+ '6100', // 'a' key with null terminator
+ '0000000000000080'
+ ])
+ );
+ expect(serializedMaxIntPlusOne).to.deep.equal(expectedResultForMaxIntPlusOne);
+ });
+
+ 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(
+ bufferFromHexArray([
+ '12', // int64
+ '6100', // 'a' key with null terminator
+ 'ffffffffffffff7f'
+ ])
+ );
+ expect(serializedMaxPositiveInt64).to.deep.equal(expectedSerializationForMaxPositiveInt64);
+
+ const minPositiveInt64 = { a: -(2n ** 63n) };
+ const serializedMinPositiveInt64 = getSerializedDocParts(BSON.serialize(minPositiveInt64));
+ const expectedSerializationForMinPositiveInt64 = getSerializedDocParts(
+ bufferFromHexArray([
+ '12', // int64
+ '6100', // 'a' key with null terminator
+ '0000000000000080'
+ ])
+ );
+ expect(serializedMinPositiveInt64).to.deep.equal(expectedSerializationForMinPositiveInt64);
+ });
+
+ 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(
+ bufferFromHexArray([
+ '12', //int64
+ '6100', // 'a' key with null terminator
+ '0100000000000000'
+ ])
+ );
+ expect(serializedDoc).to.deep.equal(expectedSerialization);
+ });
+
+ it('serializes array of BigInts', function () {
+ const testArr = { a: [1n] };
+ const serializedArr = BSON.serialize(testArr);
+ const expectedSerialization = bufferFromHexArray([
+ '04', // array
+ '6100', // 'a' key with null terminator
+ bufferFromHexArray([
+ '12', // int64
+ '3000', // '0' key with null terminator
+ '0100000000000000' // 1n (little-endian)
+ ]).toString('hex')
+ ]);
+ expect(serializedArr).to.deep.equal(expectedSerialization);
+ });
+
+ it('serializes Map with BigInt values', function () {
+ const testMap = new Map();
+ testMap.set('a', 1n);
+ const serializedMap = getSerializedDocParts(BSON.serialize(testMap));
+ const expectedSerialization = getSerializedDocParts(
+ bufferFromHexArray([
+ '12', // int64
+ '6100', // 'a' key with null terminator
+ '0100000000000000'
+ ])
+ );
+ expect(serializedMap).to.deep.equal(expectedSerialization);
+ });
+ });
+});
diff --git a/test/node/bigint_tests.js b/test/node/bigint_tests.js
deleted file mode 100644
index d20f8def..00000000
--- a/test/node/bigint_tests.js
+++ /dev/null
@@ -1,72 +0,0 @@
-'use strict';
-
-const BSON = require('../register-bson');
-const BSONTypeError = BSON.BSONTypeError;
-
-describe('BSON BigInt Support', function () {
- before(function () {
- try {
- BigInt(0);
- } catch (_) {
- this.skip('JS VM does not support BigInt');
- }
- });
- it('Should serialize an int that fits in int32', function () {
- const testDoc = { b: BigInt(32) };
- expect(() => BSON.serialize(testDoc)).to.throw(BSONTypeError);
-
- // const serializedDoc = BSON.serialize(testDoc);
- // // prettier-ignore
- // const resultBuffer = Buffer.from([0x0C, 0x00, 0x00, 0x00, 0x10, 0x62, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00]);
- // const resultDoc = BSON.deserialize(serializedDoc);
- // expect(Array.from(serializedDoc)).to.have.members(Array.from(resultBuffer));
- // expect(BigInt(resultDoc.b)).to.equal(testDoc.b);
- });
-
- it('Should serialize an int that fits in int64', function () {
- const testDoc = { b: BigInt(0x1ffffffff) };
- expect(() => BSON.serialize(testDoc)).to.throw(BSONTypeError);
-
- // const serializedDoc = BSON.serialize(testDoc);
- // // prettier-ignore
- // const resultBuffer = Buffer.from([0x10, 0x00, 0x00, 0x00, 0x12, 0x62, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00]);
- // const resultDoc = BSON.deserialize(serializedDoc);
- // expect(Array.from(serializedDoc)).to.have.members(Array.from(resultBuffer));
- // expect(BigInt(resultDoc.b)).to.equal(testDoc.b);
- });
-
- it('Should serialize an int that fits in decimal128', function () {
- const testDoc = { b: BigInt('9223372036854776001') }; // int64 max + 1
- expect(() => BSON.serialize(testDoc)).to.throw(BSONTypeError);
-
- // const serializedDoc = BSON.serialize(testDoc);
- // // prettier-ignore
- // const resultBuffer = Buffer.from([0x18, 0x00, 0x00, 0x00, 0x13, 0x62, 0x00, 0xC1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x30, 0x00]);
- // const resultDoc = BSON.deserialize(serializedDoc);
- // expect(Array.from(serializedDoc)).to.have.members(Array.from(resultBuffer));
- // expect(resultDoc.b._bsontype).to.equal('Decimal128');
- // expect(BigInt(resultDoc.b.toString())).to.equal(testDoc.b);
- });
-
- it('Should throw if BigInt is too large to serialize', function () {
- const testDoc = {
- b: BigInt('9'.repeat(35))
- }; // decimal 128 can only encode 34 digits of precision
- expect(() => BSON.serialize(testDoc)).to.throw(BSONTypeError);
- // expect(() => BSON.serialize(testDoc)).to.throw();
- });
-
- it('Should accept BigInts in Long constructor', function (done) {
- const Long = BSON.Long;
- expect(new Long(BigInt('0')).toString()).to.equal('0');
- expect(new Long(BigInt('-1')).toString()).to.equal('-1');
- expect(new Long(BigInt('-1'), true).toString()).to.equal('18446744073709551615');
- expect(new Long(BigInt('123456789123456789')).toString()).to.equal('123456789123456789');
- expect(new Long(BigInt('123456789123456789'), true).toString()).to.equal('123456789123456789');
- expect(new Long(BigInt('13835058055282163712')).toString()).to.equal('-4611686018427387904');
- expect(new Long(BigInt('13835058055282163712'), true).toString()).to.equal(
- '13835058055282163712'
- );
- done();
- });
-});
diff --git a/test/node/bson_corpus.spec.test.js b/test/node/bson_corpus.spec.test.js
index 7b98bb50..36828bef 100644
--- a/test/node/bson_corpus.spec.test.js
+++ b/test/node/bson_corpus.spec.test.js
@@ -184,8 +184,26 @@ describe('BSON Corpus', function () {
// convert inputs to native Javascript objects
const nativeFromCB = bsonToNative(cB);
- // round tripped EJSON should match the original
- expect(nativeToCEJSON(jsonToNative(cEJ))).to.equal(cEJ);
+ if (cEJ.includes('1.2345678921232E+18')) {
+ // The following is special test logic for a "Double type" bson corpus test that uses a different
+ // string format for the resulting double value
+ // The test does not have a loss in precision, just different exponential output
+ // We want to ensure that the stringified value when interpreted as a double is equal
+ // as opposed to the string being precisely the same
+ if (description !== 'Double type') {
+ throw new Error('Unexpected test using 1.2345678921232E+18');
+ }
+ const eJSONParsedAsJSON = JSON.parse(cEJ);
+ const eJSONParsed = EJSON.parse(cEJ, { relaxed: false });
+ expect(eJSONParsedAsJSON).to.have.nested.property('d.$numberDouble');
+ expect(eJSONParsed).to.have.nested.property('d._bsontype', 'Double');
+ const testInputAsFloat = Number.parseFloat(eJSONParsedAsJSON.d.$numberDouble);
+ const ejsonOutputAsFloat = eJSONParsed.d.valueOf();
+ expect(ejsonOutputAsFloat).to.equal(testInputAsFloat);
+ } else {
+ // round tripped EJSON should match the original
+ expect(nativeToCEJSON(jsonToNative(cEJ))).to.equal(cEJ);
+ }
// invalid, but still parseable, EJSON. if provided, make sure that we
// properly convert it to canonical EJSON and BSON.
@@ -205,8 +223,22 @@ describe('BSON Corpus', function () {
expect(nativeToBson(jsonToNative(cEJ))).to.deep.equal(cB);
}
- // the reverse direction, BSON -> native -> EJSON, should match canonical EJSON.
- expect(nativeToCEJSON(nativeFromCB)).to.equal(cEJ);
+ if (cEJ.includes('1.2345678921232E+18')) {
+ // The round tripped value should be equal in interpreted value, not in exact character match
+ const eJSONFromBSONAsJSON = JSON.parse(
+ EJSON.stringify(BSON.deserialize(cB), { relaxed: false })
+ );
+ const eJSONParsed = EJSON.parse(cEJ, { relaxed: false });
+ // TODO(NODE-4377): EJSON transforms large doubles into longs
+ expect(eJSONFromBSONAsJSON).to.have.nested.property('d.$numberLong');
+ expect(eJSONParsed).to.have.nested.property('d._bsontype', 'Double');
+ const testInputAsFloat = Number.parseFloat(eJSONFromBSONAsJSON.d.$numberLong);
+ const ejsonOutputAsFloat = eJSONParsed.d.valueOf();
+ expect(ejsonOutputAsFloat).to.equal(testInputAsFloat);
+ } else {
+ // the reverse direction, BSON -> native -> EJSON, should match canonical EJSON.
+ expect(nativeToCEJSON(nativeFromCB)).to.equal(cEJ);
+ }
if (v.relaxed_extjson) {
let rEJ = normalize(v.relaxed_extjson);
diff --git a/test/node/bson_test.js b/test/node/bson_test.js
index c3423045..8365d6f4 100644
--- a/test/node/bson_test.js
+++ b/test/node/bson_test.js
@@ -1410,14 +1410,14 @@ describe('BSON', function () {
expect(() => {
parser.deserialize(data);
- }).to.throw();
+ }).to.throw(BSONError);
data = Buffer.alloc(5);
data[0] = 0xff;
data[1] = 0xff;
expect(() => {
parser.deserialize(data);
- }).to.throw();
+ }).to.throw(BSONError);
// Finish up
done();
@@ -1817,9 +1817,9 @@ describe('BSON', function () {
['c', badArray]
]);
- expect(() => BSON.serialize(badDoc)).to.throw();
- expect(() => BSON.serialize(badArray)).to.throw();
- expect(() => BSON.serialize(badMap)).to.throw();
+ expect(() => BSON.serialize(badDoc)).to.throw(BSONError);
+ expect(() => BSON.serialize(badArray)).to.throw(BSONError);
+ expect(() => BSON.serialize(badMap)).to.throw(BSONError);
});
describe('Should support util.inspect for', function () {
diff --git a/test/node/double.test.ts b/test/node/double.test.ts
index 00ed30f8..450017a4 100644
--- a/test/node/double.test.ts
+++ b/test/node/double.test.ts
@@ -2,6 +2,7 @@ import { expect } from 'chai';
import { BSON, Double } from '../register-bson';
import { BSON_DATA_NUMBER, BSON_DATA_INT } from '../../src/constants';
+import { inspect } from 'node:util';
describe('BSON Double Precision', function () {
context('class Double', function () {
@@ -36,6 +37,30 @@ describe('BSON Double Precision', function () {
});
}
});
+
+ describe('.toExtendedJSON()', () => {
+ const tests = [
+ { input: new Double(0), output: { $numberDouble: '0.0' } },
+ { input: new Double(-0), output: { $numberDouble: '-0.0' } },
+ { input: new Double(3), output: { $numberDouble: '3.0' } },
+ { input: new Double(-3), output: { $numberDouble: '-3.0' } },
+ { input: new Double(3.4), output: { $numberDouble: '3.4' } },
+ { input: new Double(Number.EPSILON), output: { $numberDouble: '2.220446049250313e-16' } },
+ { input: new Double(12345e7), output: { $numberDouble: '123450000000.0' } },
+ { input: new Double(12345e-1), output: { $numberDouble: '1234.5' } },
+ { input: new Double(-12345e-1), output: { $numberDouble: '-1234.5' } },
+ { input: new Double(Infinity), output: { $numberDouble: 'Infinity' } },
+ { input: new Double(-Infinity), output: { $numberDouble: '-Infinity' } },
+ { input: new Double(NaN), output: { $numberDouble: 'NaN' } }
+ ];
+
+ for (const { input, output } of tests) {
+ const title = `returns ${inspect(output)} when Double is ${input}`;
+ it(title, () => {
+ expect(output).to.deep.equal(input.toExtendedJSON({ relaxed: false }));
+ });
+ }
+ });
});
function serializeThenDeserialize(value) {
diff --git a/test/node/extended_json.test.ts b/test/node/extended_json.test.ts
index 89cc8881..f0840074 100644
--- a/test/node/extended_json.test.ts
+++ b/test/node/extended_json.test.ts
@@ -2,6 +2,7 @@ import * as BSON from '../register-bson';
const EJSON = BSON.EJSON;
import * as vm from 'node:vm';
import { expect } from 'chai';
+import { BSONError } from '../../src';
// BSON types
const Binary = BSON.Binary;
@@ -305,8 +306,8 @@ describe('Extended JSON', function () {
const badDoc = { bad: badBsonType };
const badArray = [oid, badDoc];
// const badMap = new Map([['a', badBsonType], ['b', badDoc], ['c', badArray]]);
- expect(() => EJSON.serialize(badDoc)).to.throw();
- expect(() => EJSON.serialize(badArray)).to.throw();
+ expect(() => EJSON.serialize(badDoc)).to.throw(BSONError);
+ expect(() => EJSON.serialize(badArray)).to.throw(BSONError);
// expect(() => EJSON.serialize(badMap)).to.throw(); // uncomment when EJSON supports ES6 Map
});
diff --git a/test/node/long.test.ts b/test/node/long.test.ts
new file mode 100644
index 00000000..9a73aedc
--- /dev/null
+++ b/test/node/long.test.ts
@@ -0,0 +1,24 @@
+import { Long } from '../register-bson';
+
+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');
+ expect(new Long('-1', true).toString()).to.equal('18446744073709551615');
+ expect(new Long('123456789123456789').toString()).to.equal('123456789123456789');
+ expect(new Long('123456789123456789', true).toString()).to.equal('123456789123456789');
+ expect(new Long('13835058055282163712').toString()).to.equal('-4611686018427387904');
+ expect(new Long('13835058055282163712', true).toString()).to.equal('13835058055282163712');
+ });
+
+ 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');
+ expect(new Long(123456789123456789n).toString()).to.equal('123456789123456789');
+ expect(new Long(123456789123456789n, true).toString()).to.equal('123456789123456789');
+ expect(new Long(13835058055282163712n).toString()).to.equal('-4611686018427387904');
+ expect(new Long(13835058055282163712n, true).toString()).to.equal('13835058055282163712');
+ });
+});
diff --git a/test/node/long_tests.js b/test/node/long_tests.js
deleted file mode 100644
index a1ea2e4c..00000000
--- a/test/node/long_tests.js
+++ /dev/null
@@ -1,18 +0,0 @@
-'use strict';
-
-const BSON = require('../register-bson');
-const Long = BSON.Long;
-
-describe('Long', function () {
- it('accepts strings in the constructor', function (done) {
- expect(new Long('0').toString()).to.equal('0');
- expect(new Long('00').toString()).to.equal('0');
- expect(new Long('-1').toString()).to.equal('-1');
- expect(new Long('-1', true).toString()).to.equal('18446744073709551615');
- expect(new Long('123456789123456789').toString()).to.equal('123456789123456789');
- expect(new Long('123456789123456789', true).toString()).to.equal('123456789123456789');
- expect(new Long('13835058055282163712').toString()).to.equal('-4611686018427387904');
- expect(new Long('13835058055282163712', true).toString()).to.equal('13835058055282163712');
- done();
- });
-});
diff --git a/test/node/object_id_tests.js b/test/node/object_id_tests.js
index 0d3e37b3..8bbf4420 100644
--- a/test/node/object_id_tests.js
+++ b/test/node/object_id_tests.js
@@ -1,10 +1,7 @@
'use strict';
const Buffer = require('buffer').Buffer;
-const BSON = require('../register-bson');
-const EJSON = BSON.EJSON;
-const BSONTypeError = BSON.BSONTypeError;
-const ObjectId = BSON.ObjectId;
+const { BSON, BSONError, EJSON, ObjectId } = require('../register-bson');
const util = require('util');
const { expect } = require('chai');
const { bufferFromHexArray } = require('./tools/utils');
@@ -103,7 +100,7 @@ describe('ObjectId', function () {
for (const { input, description } of invalidInputs) {
it(`should throw error if ${description} is passed in`, function () {
- expect(() => new ObjectId(input)).to.throw(BSONTypeError);
+ expect(() => new ObjectId(input)).to.throw(BSONError);
});
}
@@ -114,7 +111,7 @@ describe('ObjectId', function () {
return noArgObjID.toHexString();
}
};
- expect(() => new ObjectId(objectIdLike)).to.throw(BSONTypeError);
+ expect(() => new ObjectId(objectIdLike)).to.throw(BSONError);
});
it('should correctly create ObjectId from object with valid string id', function () {
@@ -186,15 +183,15 @@ describe('ObjectId', function () {
const objectNullId = {
id: null
};
- expect(() => new ObjectId(objectNumId)).to.throw(BSONTypeError);
- expect(() => new ObjectId(objectNullId)).to.throw(BSONTypeError);
+ expect(() => new ObjectId(objectNumId)).to.throw(BSONError);
+ expect(() => new ObjectId(objectNullId)).to.throw(BSONError);
});
it('should throw an error if object with invalid string id is passed in', function () {
const objectInvalid24HexStr = {
id: 'FFFFFFFFFFFFFFFFFFFFFFFG'
};
- expect(() => new ObjectId(objectInvalid24HexStr)).to.throw(BSONTypeError);
+ expect(() => new ObjectId(objectInvalid24HexStr)).to.throw(BSONError);
});
it('should correctly create ObjectId from object with invalid string id and toHexString method', function () {
@@ -213,7 +210,7 @@ describe('ObjectId', function () {
const objectInvalidBuffer = {
id: Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13])
};
- expect(() => new ObjectId(objectInvalidBuffer)).to.throw(BSONTypeError);
+ expect(() => new ObjectId(objectInvalidBuffer)).to.throw(BSONError);
});
it('should correctly create ObjectId from object with invalid Buffer id and toHexString method', function () {
@@ -270,11 +267,11 @@ describe('ObjectId', function () {
});
it('should throw error if non-12 byte non-24 hex string passed in', function () {
- expect(() => new ObjectId('FFFFFFFFFFFFFFFFFFFFFFFG')).to.throw(BSONTypeError);
- expect(() => new ObjectId('thisstringisdefinitelytoolong')).to.throw(BSONTypeError);
- expect(() => new ObjectId('tooshort')).to.throw(BSONTypeError);
- expect(() => new ObjectId('101010')).to.throw(BSONTypeError);
- expect(() => new ObjectId('')).to.throw(BSONTypeError);
+ expect(() => new ObjectId('FFFFFFFFFFFFFFFFFFFFFFFG')).to.throw(BSONError);
+ expect(() => new ObjectId('thisstringisdefinitelytoolong')).to.throw(BSONError);
+ expect(() => new ObjectId('tooshort')).to.throw(BSONError);
+ expect(() => new ObjectId('101010')).to.throw(BSONError);
+ expect(() => new ObjectId('')).to.throw(BSONError);
});
it('should correctly create ObjectId from 24 hex string', function () {
@@ -319,7 +316,7 @@ describe('ObjectId', function () {
it('should throw an error if invalid Buffer passed in', function () {
const a = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]);
- expect(() => new ObjectId(a)).to.throw(BSONTypeError);
+ expect(() => new ObjectId(a)).to.throw(BSONError);
});
it('should correctly allow for node.js inspect to work with ObjectId', function (done) {
@@ -345,11 +342,11 @@ describe('ObjectId', function () {
const characterCodesLargerThan256 = 'abcdefŽhijkl';
const length12Not12Bytes = '🐶🐶🐶🐶🐶🐶';
expect(() => new ObjectId(characterCodesLargerThan256).toHexString()).to.throw(
- BSONTypeError,
+ BSONError,
'Argument passed in must be a string of 12 bytes'
);
expect(() => new ObjectId(length12Not12Bytes).id).to.throw(
- BSONTypeError,
+ BSONError,
'Argument passed in must be a string of 12 bytes'
);
});
diff --git a/test/node/uuid_tests.js b/test/node/uuid_tests.js
index 9224f7b6..f5088c4a 100644
--- a/test/node/uuid_tests.js
+++ b/test/node/uuid_tests.js
@@ -4,8 +4,7 @@ const { Buffer } = require('buffer');
const { Binary, UUID } = require('../register-bson');
const { inspect } = require('util');
const { validate: uuidStringValidate, version: uuidStringVersion } = require('uuid');
-const BSON = require('../register-bson');
-const BSONTypeError = BSON.BSONTypeError;
+const { BSON, BSONError } = require('../register-bson');
const BSON_DATA_BINARY = BSON.BSONType.binData;
const { BSON_BINARY_SUBTYPE_UUID_NEW } = require('../../src/constants');
@@ -80,7 +79,7 @@ describe('UUID', () => {
*/
it('should throw if passed invalid 36-char uuid hex string', () => {
expect(() => new UUID(LOWERCASE_DASH_SEPARATED_UUID_STRING)).to.not.throw();
- expect(() => new UUID('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')).to.throw(BSONTypeError);
+ expect(() => new UUID('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')).to.throw(BSONError);
// Note: The version is missing here ^
});
@@ -89,7 +88,7 @@ describe('UUID', () => {
*/
it('should throw if passed unsupported argument', () => {
expect(() => new UUID(LOWERCASE_DASH_SEPARATED_UUID_STRING)).to.not.throw();
- expect(() => new UUID({})).to.throw(BSONTypeError);
+ expect(() => new UUID({})).to.throw(BSONError);
});
/**
@@ -142,13 +141,13 @@ describe('UUID', () => {
const validRandomBuffer = Buffer.from('Hello World!');
const binRand = new Binary(validRandomBuffer);
- expect(() => binRand.toUUID()).to.throw();
+ expect(() => binRand.toUUID()).to.throw(BSONError);
const validUuidV3String = '25f0d698-15b9-3a7a-96b1-a573061e29c9';
const validUuidV3Buffer = Buffer.from(validUuidV3String.replace(/-/g, ''), 'hex');
const binV3 = new Binary(validUuidV3Buffer, Binary.SUBTYPE_UUID_OLD);
- expect(() => binV3.toUUID()).to.throw();
+ expect(() => binV3.toUUID()).to.throw(BSONError);
const validUuidV4String = 'bd2d74fe-bad8-430c-aeac-b01d073a1eb6';
const validUuidV4Buffer = Buffer.from(validUuidV4String.replace(/-/g, ''), 'hex');
diff --git a/test/register-bson.js b/test/register-bson.js
index 3dd57b58..37fda7e0 100644
--- a/test/register-bson.js
+++ b/test/register-bson.js
@@ -19,6 +19,15 @@ const { loadCJSModuleBSON } = require('./load_bson');
chai.use(function (chai) {
const throwsAssertion = chai.Assertion.prototype.throw;
chai.Assertion.addMethod('throw', function (...args) {
+ const isNegated = chai.util.flag(this, 'negate');
+ if (!isNegated) {
+ // to.not.throw() is acceptable to not have an argument
+ // to.throw() should always provide an Error class
+ expect(args).to.have.length.of.at.least(1);
+ // prevent to.throw(undefined) nor to.throw(null)
+ expect(args[0]).to.exist;
+ }
+
try {
throwsAssertion.call(this, ...args);
} catch (assertionError) {