Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NODE-4871): Add support for int64 deserialization to BigInt #542

Merged
merged 20 commits into from
Jan 4, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,12 +304,13 @@ Serialize a Javascript object using a predefined Buffer and index into the buffe
| buffer | <code>Buffer</code> | | the buffer containing the serialized set of BSON documents. |
| [options.evalFunctions] | <code>Object</code> | <code>false</code> | evaluate functions in the BSON document scoped to the object deserialized. |
| [options.cacheFunctions] | <code>Object</code> | <code>false</code> | cache evaluated functions for reuse. |
| [options.useBigInt64] | <code>Object</code> | <code>false</code> | when deserializing a Long will return a BigInt. |
| [options.promoteLongs] | <code>Object</code> | <code>true</code> | when deserializing a Long will fit it into a Number if it's smaller than 53 bits |
| [options.promoteBuffers] | <code>Object</code> | <code>false</code> | when deserializing a Binary will return it as a node.js Buffer instance. |
| [options.promoteValues] | <code>Object</code> | <code>false</code> | when deserializing will promote BSON values to their Node.js closest equivalent types. |
| [options.fieldsAsRaw] | <code>Object</code> | <code></code> | allow to specify if there what fields we wish to return as unserialized raw buffer. |
| [options.bsonRegExp] | <code>Object</code> | <code>false</code> | return BSON regular expressions as BSONRegExp instances. |
| [options.allowObjectSmallerThanBufferSize] | <code>boolean</code> | <code>false</code> | allows the buffer to be larger than the parsed BSON object |
| [options.allowObjectSmallerThanBufferSize] | <code>boolean</code> | <code>false</code> | allows the buffer to be larger than the parsed BSON object. |

Deserialize data as BSON.

Expand Down
33 changes: 24 additions & 9 deletions src/parser/deserializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -96,7 +98,7 @@ export function deserialize(
);
}

// Start deserializtion
// Start deserialization
return deserializeObject(buffer, index, options, isArray);
}

Expand All @@ -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;
Expand Down Expand Up @@ -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) |
Expand All @@ -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()
Expand Down
102 changes: 102 additions & 0 deletions test/node/bigint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { BSON, BSONError } from '../register-bson';
import { bufferFromHexArray } from './tools/utils';
import { expect } from 'chai';

describe('BSON BigInt deserialization 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;
nbbeeken marked this conversation as resolved.
Show resolved Hide resolved
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) {
nbbeeken marked this conversation as resolved.
Show resolved Hide resolved
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);
}
});
});