From 5e03a7cacab14405601e822e556efc5739635eb3 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 26 Feb 2024 18:36:03 -0500 Subject: [PATCH 1/2] refactor: try consolidating bit work --- src/bson.ts | 7 +- src/objectid.ts | 23 +---- src/parser/deserializer.ts | 176 ++++++++++--------------------------- src/parser/serializer.ts | 141 +++++------------------------ src/utils/number_utils.ts | 135 ++++++++++++++++++++++++++++ test/node/release.test.ts | 1 + 6 files changed, 212 insertions(+), 271 deletions(-) create mode 100644 src/utils/number_utils.ts diff --git a/src/bson.ts b/src/bson.ts index d86bf1c4..64b433ca 100644 --- a/src/bson.ts +++ b/src/bson.ts @@ -16,6 +16,7 @@ import { BSONRegExp } from './regexp'; import { BSONSymbol } from './symbol'; import { Timestamp } from './timestamp'; import { ByteUtils } from './utils/byte_utils'; +import { NumberUtils } from './utils/number_utils'; export type { UUIDExtended, BinaryExtended, BinaryExtendedLegacy, BinarySequence } from './binary'; export type { CodeExtended } from './code'; export type { DBRefLike } from './db_ref'; @@ -232,11 +233,7 @@ export function deserializeStream( // Loop over all documents for (let i = 0; i < numberOfDocuments; i++) { // Find size of the document - const size = - bufferData[index] | - (bufferData[index + 1] << 8) | - (bufferData[index + 2] << 16) | - (bufferData[index + 3] << 24); + const size = NumberUtils.getInt32LE(bufferData, index); // Update options with index internalOptions.index = index; // Parse the document at this point diff --git a/src/objectid.ts b/src/objectid.ts index 4bf83627..90dfa495 100644 --- a/src/objectid.ts +++ b/src/objectid.ts @@ -2,6 +2,7 @@ import { BSONValue } from './bson_value'; import { BSONError } from './error'; import { type InspectFn, defaultInspect } from './parser/utils'; import { ByteUtils } from './utils/byte_utils'; +import { NumberUtils } from './utils/number_utils'; // Regular expression that checks for hex value const checkForHexRegExp = new RegExp('^[0-9a-fA-F]{24}$'); @@ -179,13 +180,7 @@ export class ObjectId extends BSONValue { const buffer = ByteUtils.allocate(12); // 4-byte timestamp - buffer[3] = time; - time >>>= 8; - buffer[2] = time; - time >>>= 8; - buffer[1] = time; - time >>>= 8; - buffer[0] = time; + NumberUtils.setInt32BE(buffer, 0, time); // set PROCESS_UNIQUE if yet not initialized if (PROCESS_UNIQUE === null) { @@ -265,11 +260,7 @@ export class ObjectId extends BSONValue { /** Returns the generation date (accurate up to the second) that this ID was generated. */ getTimestamp(): Date { const timestamp = new Date(); - const time = - this.buffer[3] + - this.buffer[2] * (1 << 8) + - this.buffer[1] * (1 << 16) + - this.buffer[0] * (1 << 24); + const time = NumberUtils.getUint32BE(this.buffer, 0); timestamp.setTime(Math.floor(time) * 1000); return timestamp; } @@ -288,13 +279,7 @@ export class ObjectId extends BSONValue { const buffer = ByteUtils.allocate(12); for (let i = 11; i >= 4; i--) buffer[i] = 0; // Encode time into first 4 bytes - buffer[3] = time; - time >>>= 8; - buffer[2] = time; - time >>>= 8; - buffer[1] = time; - time >>>= 8; - buffer[0] = time; + NumberUtils.setInt32BE(buffer, 0, time); // Return the new objectId return new ObjectId(buffer); } diff --git a/src/parser/deserializer.ts b/src/parser/deserializer.ts index 8f9e4667..c43eb3cc 100644 --- a/src/parser/deserializer.ts +++ b/src/parser/deserializer.ts @@ -15,6 +15,7 @@ import { BSONRegExp } from '../regexp'; import { BSONSymbol } from '../symbol'; import { Timestamp } from '../timestamp'; import { ByteUtils } from '../utils/byte_utils'; +import { NumberUtils } from '../utils/number_utils'; import { validateUtf8 } from '../validate_utf8'; /** @public */ @@ -91,11 +92,7 @@ export function internalDeserialize( options = options == null ? {} : options; const index = options && options.index ? options.index : 0; // Read the document size - const size = - buffer[index] | - (buffer[index + 1] << 8) | - (buffer[index + 2] << 16) | - (buffer[index + 3] << 24); + const size = NumberUtils.getInt32LE(buffer, index); if (size < 5) { throw new BSONError(`bson size must be >= 5, is ${size}`); @@ -128,9 +125,6 @@ export function internalDeserialize( const allowedDBRefKeys = /^\$ref$|^\$id$|^\$db$/; -const FLOAT_READ = new Float64Array(1); -const FLOAT_WRITE_BYTES = new Uint8Array(FLOAT_READ.buffer, 0, 8); - function deserializeObject( buffer: Uint8Array, index: number, @@ -207,8 +201,8 @@ function deserializeObject( if (buffer.length < 5) throw new BSONError('corrupt bson message < 5 bytes long'); // Read the document size - const size = - buffer[index++] | (buffer[index++] << 8) | (buffer[index++] << 16) | (buffer[index++] << 24); + const size = NumberUtils.getInt32LE(buffer, index); + index += 4; // Ensure buffer is valid size if (size < 5 || size > buffer.length) throw new BSONError('corrupt bson message'); @@ -258,11 +252,8 @@ function deserializeObject( index = i + 1; if (elementType === constants.BSON_DATA_STRING) { - const stringSize = - buffer[index++] | - (buffer[index++] << 8) | - (buffer[index++] << 16) | - (buffer[index++] << 24); + const stringSize = NumberUtils.getInt32LE(buffer, index); + index += 4; if ( stringSize <= 0 || stringSize > buffer.length - index || @@ -278,37 +269,20 @@ function deserializeObject( value = new ObjectId(oid); index = index + 12; } else if (elementType === constants.BSON_DATA_INT && promoteValues === false) { - value = new Int32( - buffer[index++] | (buffer[index++] << 8) | (buffer[index++] << 16) | (buffer[index++] << 24) - ); + value = new Int32(NumberUtils.getInt32LE(buffer, index)); + index += 4; } else if (elementType === constants.BSON_DATA_INT) { - value = - buffer[index++] | - (buffer[index++] << 8) | - (buffer[index++] << 16) | - (buffer[index++] << 24); + value = NumberUtils.getInt32LE(buffer, index); + index += 4; } else if (elementType === constants.BSON_DATA_NUMBER) { - FLOAT_WRITE_BYTES[0] = buffer[index++]; - FLOAT_WRITE_BYTES[1] = buffer[index++]; - FLOAT_WRITE_BYTES[2] = buffer[index++]; - FLOAT_WRITE_BYTES[3] = buffer[index++]; - FLOAT_WRITE_BYTES[4] = buffer[index++]; - FLOAT_WRITE_BYTES[5] = buffer[index++]; - FLOAT_WRITE_BYTES[6] = buffer[index++]; - FLOAT_WRITE_BYTES[7] = buffer[index++]; - value = FLOAT_READ[0]; + value = NumberUtils.getFloat64LE(buffer, index); + index += 8; if (promoteValues === false) value = new Double(value); } else if (elementType === constants.BSON_DATA_DATE) { - const lowBits = - buffer[index++] | - (buffer[index++] << 8) | - (buffer[index++] << 16) | - (buffer[index++] << 24); - const highBits = - buffer[index++] | - (buffer[index++] << 8) | - (buffer[index++] << 16) | - (buffer[index++] << 24); + const lowBits = NumberUtils.getInt32LE(buffer, index); + index += 4; + const highBits = NumberUtils.getInt32LE(buffer, index); + index += 4; value = new Date(new Long(lowBits, highBits).toNumber()); } else if (elementType === constants.BSON_DATA_BOOLEAN) { @@ -317,11 +291,8 @@ function deserializeObject( value = buffer[index++] === 1; } else if (elementType === constants.BSON_DATA_OBJECT) { const _index = index; - const objectSize = - buffer[index] | - (buffer[index + 1] << 8) | - (buffer[index + 2] << 16) | - (buffer[index + 3] << 24); + const objectSize = NumberUtils.getInt32LE(buffer, index); + if (objectSize <= 0 || objectSize > buffer.length - index) throw new BSONError('bad embedded document length in bson'); @@ -339,11 +310,7 @@ function deserializeObject( index = index + objectSize; } else if (elementType === constants.BSON_DATA_ARRAY) { const _index = index; - const objectSize = - buffer[index] | - (buffer[index + 1] << 8) | - (buffer[index + 2] << 16) | - (buffer[index + 3] << 24); + const objectSize = NumberUtils.getInt32LE(buffer, index); let arrayOptions: DeserializeOptions = options; // Stop index @@ -368,32 +335,15 @@ function deserializeObject( value = null; } else if (elementType === constants.BSON_DATA_LONG) { if (useBigInt64) { - const lo = - buffer[index] + - buffer[index + 1] * 2 ** 8 + - buffer[index + 2] * 2 ** 16 + - buffer[index + 3] * 2 ** 24; - const hi = - buffer[index + 4] + - buffer[index + 5] * 2 ** 8 + - buffer[index + 6] * 2 ** 16 + - (buffer[index + 7] << 24); // Overflow - - /* eslint-disable-next-line no-restricted-globals -- This is allowed here as useBigInt64=true */ - value = (BigInt(hi) << BigInt(32)) + BigInt(lo); + value = NumberUtils.getBigInt64LE(buffer, index); index += 8; } else { // Unpack the low and high bits - const lowBits = - buffer[index++] | - (buffer[index++] << 8) | - (buffer[index++] << 16) | - (buffer[index++] << 24); - const highBits = - buffer[index++] | - (buffer[index++] << 8) | - (buffer[index++] << 16) | - (buffer[index++] << 24); + const lowBits = NumberUtils.getInt32LE(buffer, index); + index += 4; + const highBits = NumberUtils.getInt32LE(buffer, index); + index += 4; + const long = new Long(lowBits, highBits); // Promote the long if possible if (promoteLongs && promoteValues === true) { @@ -415,11 +365,8 @@ function deserializeObject( // Assign the new Decimal128 value value = new Decimal128(bytes); } else if (elementType === constants.BSON_DATA_BINARY) { - let binarySize = - buffer[index++] | - (buffer[index++] << 8) | - (buffer[index++] << 16) | - (buffer[index++] << 24); + let binarySize = NumberUtils.getInt32LE(buffer, index); + index += 4; const totalBinarySize = binarySize; const subType = buffer[index++]; @@ -434,11 +381,8 @@ function deserializeObject( if (buffer['slice'] != null) { // If we have subtype 2 skip the 4 bytes for the size if (subType === Binary.SUBTYPE_BYTE_ARRAY) { - binarySize = - buffer[index++] | - (buffer[index++] << 8) | - (buffer[index++] << 16) | - (buffer[index++] << 24); + binarySize = NumberUtils.getInt32LE(buffer, index); + index += 4; if (binarySize < 0) throw new BSONError('Negative binary type element size found for subtype 0x02'); if (binarySize > totalBinarySize - 4) @@ -459,11 +403,8 @@ function deserializeObject( const _buffer = ByteUtils.allocate(binarySize); // If we have subtype 2 skip the 4 bytes for the size if (subType === Binary.SUBTYPE_BYTE_ARRAY) { - binarySize = - buffer[index++] | - (buffer[index++] << 8) | - (buffer[index++] << 16) | - (buffer[index++] << 24); + binarySize = NumberUtils.getInt32LE(buffer, index); + index += 4; if (binarySize < 0) throw new BSONError('Negative binary type element size found for subtype 0x02'); if (binarySize > totalBinarySize - 4) @@ -562,11 +503,8 @@ function deserializeObject( // Set the object value = new BSONRegExp(source, regExpOptions); } else if (elementType === constants.BSON_DATA_SYMBOL) { - const stringSize = - buffer[index++] | - (buffer[index++] << 8) | - (buffer[index++] << 16) | - (buffer[index++] << 24); + const stringSize = NumberUtils.getInt32LE(buffer, index); + index += 4; if ( stringSize <= 0 || stringSize > buffer.length - index || @@ -581,16 +519,10 @@ function deserializeObject( // We intentionally **do not** use bit shifting here // Bit shifting in javascript coerces numbers to **signed** int32s // We need to keep i, and t unsigned - const i = - buffer[index++] + - buffer[index++] * (1 << 8) + - buffer[index++] * (1 << 16) + - buffer[index++] * (1 << 24); - const t = - buffer[index++] + - buffer[index++] * (1 << 8) + - buffer[index++] * (1 << 16) + - buffer[index++] * (1 << 24); + const i = NumberUtils.getUInt32LE(buffer, index); + index += 4; + const t = NumberUtils.getUInt32LE(buffer, index); + index += 4; value = new Timestamp({ i, t }); } else if (elementType === constants.BSON_DATA_MIN_KEY) { @@ -598,11 +530,8 @@ function deserializeObject( } else if (elementType === constants.BSON_DATA_MAX_KEY) { value = new MaxKey(); } else if (elementType === constants.BSON_DATA_CODE) { - const stringSize = - buffer[index++] | - (buffer[index++] << 8) | - (buffer[index++] << 16) | - (buffer[index++] << 24); + const stringSize = NumberUtils.getInt32LE(buffer, index); + index += 4; if ( stringSize <= 0 || stringSize > buffer.length - index || @@ -622,11 +551,8 @@ function deserializeObject( // Update parse index position index = index + stringSize; } else if (elementType === constants.BSON_DATA_CODE_W_SCOPE) { - const totalSize = - buffer[index++] | - (buffer[index++] << 8) | - (buffer[index++] << 16) | - (buffer[index++] << 24); + const totalSize = NumberUtils.getInt32LE(buffer, index); + index += 4; // Element cannot be shorter than totalSize + stringSize + documentSize + terminator if (totalSize < 4 + 4 + 4 + 1) { @@ -634,11 +560,8 @@ function deserializeObject( } // Get the code string size - const stringSize = - buffer[index++] | - (buffer[index++] << 8) | - (buffer[index++] << 16) | - (buffer[index++] << 24); + const stringSize = NumberUtils.getInt32LE(buffer, index); + index += 4; // Check if we have a valid string if ( stringSize <= 0 || @@ -660,11 +583,7 @@ function deserializeObject( // Parse the element const _index = index; // Decode the size of the object document - const objectSize = - buffer[index] | - (buffer[index + 1] << 8) | - (buffer[index + 2] << 16) | - (buffer[index + 3] << 24); + const objectSize = NumberUtils.getInt32LE(buffer, index); // Decode the scope object const scopeObject = deserializeObject(buffer, _index, options, false); // Adjust the index @@ -683,11 +602,8 @@ function deserializeObject( value = new Code(functionString, scopeObject); } else if (elementType === constants.BSON_DATA_DBPOINTER) { // Get the code string size - const stringSize = - buffer[index++] | - (buffer[index++] << 8) | - (buffer[index++] << 16) | - (buffer[index++] << 24); + const stringSize = NumberUtils.getInt32LE(buffer, index); + index += 4; // Check if we have a valid string if ( stringSize <= 0 || diff --git a/src/parser/serializer.ts b/src/parser/serializer.ts index fb97ad42..25a5fd65 100644 --- a/src/parser/serializer.ts +++ b/src/parser/serializer.ts @@ -12,6 +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 { NumberUtils } from '../utils/number_utils'; import { isAnyArrayBuffer, isDate, isMap, isRegExp, isUint8Array } from './utils'; /** @public */ @@ -61,10 +62,7 @@ function serializeString(buffer: Uint8Array, key: string, value: string, index: // Write the string const size = ByteUtils.encodeUTF8Into(buffer, value, index + 4); // Write the size of the string to buffer - buffer[index + 3] = ((size + 1) >> 24) & 0xff; - buffer[index + 2] = ((size + 1) >> 16) & 0xff; - buffer[index + 1] = ((size + 1) >> 8) & 0xff; - buffer[index] = (size + 1) & 0xff; + NumberUtils.setInt32LE(buffer, index, size + 1); // Update index index = index + 4 + size; // Write zero @@ -72,9 +70,6 @@ function serializeString(buffer: Uint8Array, key: string, value: string, index: return index; } -const FLOAT_WRITE = new Float64Array(1); -const FLOAT_READ_BYTES = new Uint8Array(FLOAT_WRITE.buffer, 0, 8); - function serializeNumber(buffer: Uint8Array, key: string, value: number, index: number) { const isNegativeZero = Object.is(value, -0); @@ -93,24 +88,9 @@ function serializeNumber(buffer: Uint8Array, key: string, value: number, index: buffer[index++] = 0x00; if (type === constants.BSON_DATA_INT) { - let int32 = value; - buffer[index++] = int32; - int32 >>>= 8; - buffer[index++] = int32; - int32 >>>= 8; - buffer[index++] = int32; - int32 >>>= 8; - buffer[index++] = int32; + index += NumberUtils.setInt32LE(buffer, index, value); } else { - FLOAT_WRITE[0] = value; - buffer[index++] = FLOAT_READ_BYTES[0]; - buffer[index++] = FLOAT_READ_BYTES[1]; - buffer[index++] = FLOAT_READ_BYTES[2]; - buffer[index++] = FLOAT_READ_BYTES[3]; - buffer[index++] = FLOAT_READ_BYTES[4]; - buffer[index++] = FLOAT_READ_BYTES[5]; - buffer[index++] = FLOAT_READ_BYTES[6]; - buffer[index++] = FLOAT_READ_BYTES[7]; + index += NumberUtils.setFloat64LE(buffer, index, value); } return index; @@ -124,27 +104,7 @@ function serializeBigInt(buffer: Uint8Array, key: string, value: bigint, index: index += numberOfWrittenBytes; buffer[index++] = 0; - /* eslint-disable-next-line no-restricted-globals -- This is allowed here as useBigInt64=true */ - const mask32bits = BigInt(0xffff_ffff); - - let lo = Number(value & mask32bits); - buffer[index++] = lo; - lo >>= 8; - buffer[index++] = lo; - lo >>= 8; - buffer[index++] = lo; - lo >>= 8; - buffer[index++] = lo; - - /* eslint-disable-next-line no-restricted-globals -- This is allowed here as useBigInt64=true */ - let hi = Number((value >> BigInt(32)) & mask32bits); - buffer[index++] = hi; - hi >>= 8; - buffer[index++] = hi; - hi >>= 8; - buffer[index++] = hi; - hi >>= 8; - buffer[index++] = hi; + index += NumberUtils.setBigInt64LE(buffer, index, value); return index; } @@ -189,15 +149,9 @@ function serializeDate(buffer: Uint8Array, key: string, value: Date, index: numb const lowBits = dateInMilis.getLowBits(); const highBits = dateInMilis.getHighBits(); // Encode low bits - buffer[index++] = lowBits & 0xff; - buffer[index++] = (lowBits >> 8) & 0xff; - buffer[index++] = (lowBits >> 16) & 0xff; - buffer[index++] = (lowBits >> 24) & 0xff; + index += NumberUtils.setInt32LE(buffer, index, lowBits); // Encode high bits - buffer[index++] = highBits & 0xff; - buffer[index++] = (highBits >> 8) & 0xff; - buffer[index++] = (highBits >> 16) & 0xff; - buffer[index++] = (highBits >> 24) & 0xff; + index += NumberUtils.setInt32LE(buffer, index, highBits); return index; } @@ -309,10 +263,7 @@ function serializeBuffer(buffer: Uint8Array, key: string, value: Uint8Array, ind // Get size of the buffer (current write point) const size = value.length; // Write the size of the string to buffer - buffer[index++] = size & 0xff; - buffer[index++] = (size >> 8) & 0xff; - buffer[index++] = (size >> 16) & 0xff; - buffer[index++] = (size >> 24) & 0xff; + index += NumberUtils.setInt32LE(buffer, index, size); // Write the default subtype buffer[index++] = constants.BSON_BINARY_SUBTYPE_DEFAULT; // Copy the content form the binary field to the buffer @@ -387,15 +338,9 @@ function serializeLong(buffer: Uint8Array, key: string, value: Long, index: numb const lowBits = value.getLowBits(); const highBits = value.getHighBits(); // Encode low bits - buffer[index++] = lowBits & 0xff; - buffer[index++] = (lowBits >> 8) & 0xff; - buffer[index++] = (lowBits >> 16) & 0xff; - buffer[index++] = (lowBits >> 24) & 0xff; + index += NumberUtils.setInt32LE(buffer, index, lowBits); // Encode high bits - buffer[index++] = highBits & 0xff; - buffer[index++] = (highBits >> 8) & 0xff; - buffer[index++] = (highBits >> 16) & 0xff; - buffer[index++] = (highBits >> 24) & 0xff; + index += NumberUtils.setInt32LE(buffer, index, highBits); return index; } @@ -409,10 +354,7 @@ function serializeInt32(buffer: Uint8Array, key: string, value: Int32 | number, index = index + numberOfWrittenBytes; buffer[index++] = 0; // Write the int value - buffer[index++] = value & 0xff; - buffer[index++] = (value >> 8) & 0xff; - buffer[index++] = (value >> 16) & 0xff; - buffer[index++] = (value >> 24) & 0xff; + index += NumberUtils.setInt32LE(buffer, index, value); return index; } @@ -428,15 +370,7 @@ function serializeDouble(buffer: Uint8Array, key: string, value: Double, index: buffer[index++] = 0; // Write float - FLOAT_WRITE[0] = value.value; - buffer[index++] = FLOAT_READ_BYTES[0]; - buffer[index++] = FLOAT_READ_BYTES[1]; - buffer[index++] = FLOAT_READ_BYTES[2]; - buffer[index++] = FLOAT_READ_BYTES[3]; - buffer[index++] = FLOAT_READ_BYTES[4]; - buffer[index++] = FLOAT_READ_BYTES[5]; - buffer[index++] = FLOAT_READ_BYTES[6]; - buffer[index++] = FLOAT_READ_BYTES[7]; + index += NumberUtils.setFloat64LE(buffer, index, value.value); return index; } @@ -454,10 +388,7 @@ function serializeFunction(buffer: Uint8Array, key: string, value: Function, ind // Write the string const size = ByteUtils.encodeUTF8Into(buffer, functionString, index + 4) + 1; // Write the size of the string to buffer - buffer[index] = size & 0xff; - buffer[index + 1] = (size >> 8) & 0xff; - buffer[index + 2] = (size >> 16) & 0xff; - buffer[index + 3] = (size >> 24) & 0xff; + NumberUtils.setInt32LE(buffer, index, size); // Update index index = index + 4 + size - 1; // Write zero @@ -496,10 +427,7 @@ function serializeCode( // Write string into buffer const codeSize = ByteUtils.encodeUTF8Into(buffer, functionString, index + 4) + 1; // Write the size of the string to buffer - buffer[index] = codeSize & 0xff; - buffer[index + 1] = (codeSize >> 8) & 0xff; - buffer[index + 2] = (codeSize >> 16) & 0xff; - buffer[index + 3] = (codeSize >> 24) & 0xff; + NumberUtils.setInt32LE(buffer, index, codeSize); // Write end 0 buffer[index + 4 + codeSize - 1] = 0; // Write the @@ -522,10 +450,7 @@ function serializeCode( const totalSize = endIndex - startIndex; // Write the total size of the object - buffer[startIndex++] = totalSize & 0xff; - buffer[startIndex++] = (totalSize >> 8) & 0xff; - buffer[startIndex++] = (totalSize >> 16) & 0xff; - buffer[startIndex++] = (totalSize >> 24) & 0xff; + startIndex += NumberUtils.setInt32LE(buffer, startIndex, totalSize); // Write trailing zero buffer[index++] = 0; } else { @@ -540,10 +465,7 @@ function serializeCode( // Write the string const size = ByteUtils.encodeUTF8Into(buffer, functionString, index + 4) + 1; // Write the size of the string to buffer - buffer[index] = size & 0xff; - buffer[index + 1] = (size >> 8) & 0xff; - buffer[index + 2] = (size >> 16) & 0xff; - buffer[index + 3] = (size >> 24) & 0xff; + NumberUtils.setInt32LE(buffer, index, size); // Update index index = index + 4 + size - 1; // Write zero @@ -568,20 +490,14 @@ function serializeBinary(buffer: Uint8Array, key: string, value: Binary, index: // Add the deprecated 02 type 4 bytes of size to total if (value.sub_type === Binary.SUBTYPE_BYTE_ARRAY) size = size + 4; // Write the size of the string to buffer - buffer[index++] = size & 0xff; - buffer[index++] = (size >> 8) & 0xff; - buffer[index++] = (size >> 16) & 0xff; - buffer[index++] = (size >> 24) & 0xff; + index += NumberUtils.setInt32LE(buffer, index, size); // Write the subtype to the buffer buffer[index++] = value.sub_type; // If we have binary type 2 the 4 first bytes are the size if (value.sub_type === Binary.SUBTYPE_BYTE_ARRAY) { size = size - 4; - buffer[index++] = size & 0xff; - buffer[index++] = (size >> 8) & 0xff; - buffer[index++] = (size >> 16) & 0xff; - buffer[index++] = (size >> 24) & 0xff; + index += NumberUtils.setInt32LE(buffer, index, size); } // Write the data to the object @@ -602,14 +518,11 @@ function serializeSymbol(buffer: Uint8Array, key: string, value: BSONSymbol, ind // Write the string const size = ByteUtils.encodeUTF8Into(buffer, value.value, index + 4) + 1; // Write the size of the string to buffer - buffer[index] = size & 0xff; - buffer[index + 1] = (size >> 8) & 0xff; - buffer[index + 2] = (size >> 16) & 0xff; - buffer[index + 3] = (size >> 24) & 0xff; + NumberUtils.setInt32LE(buffer, index, size); // Update index index = index + 4 + size - 1; // Write zero - buffer[index++] = 0x00; + buffer[index++] = 0; return index; } @@ -656,10 +569,7 @@ function serializeDBRef( // Calculate object size const size = endIndex - startIndex; // Write the size - buffer[startIndex++] = size & 0xff; - buffer[startIndex++] = (size >> 8) & 0xff; - buffer[startIndex++] = (size >> 16) & 0xff; - buffer[startIndex++] = (size >> 24) & 0xff; + startIndex += NumberUtils.setInt32LE(buffer, index, size); // Set index return endIndex; } @@ -831,7 +741,7 @@ export function serializeInto( if (checkKeys) { if ('$' === key[0]) { throw new BSONError('key ' + key + " must not start with '$'"); - } else if (~key.indexOf('.')) { + } else if (key.includes('.')) { throw new BSONError('key ' + key + " must not contain '.'"); } } @@ -939,7 +849,7 @@ export function serializeInto( if (checkKeys) { if ('$' === key[0]) { throw new BSONError('key ' + key + " must not start with '$'"); - } else if (~key.indexOf('.')) { + } else if (key.includes('.')) { throw new BSONError('key ' + key + " must not contain '.'"); } } @@ -1029,9 +939,6 @@ export function serializeInto( // Final size const size = index - startingIndex; // Write the size of the object - buffer[startingIndex++] = size & 0xff; - buffer[startingIndex++] = (size >> 8) & 0xff; - buffer[startingIndex++] = (size >> 16) & 0xff; - buffer[startingIndex++] = (size >> 24) & 0xff; + startingIndex += NumberUtils.setInt32LE(buffer, startingIndex, size); return index; } diff --git a/src/utils/number_utils.ts b/src/utils/number_utils.ts new file mode 100644 index 00000000..8c866a1a --- /dev/null +++ b/src/utils/number_utils.ts @@ -0,0 +1,135 @@ +const FLOAT = new Float64Array(1); +const FLOAT_BYTES = new Uint8Array(FLOAT.buffer, 0, 8); + +/** + * Number parsing and serializing utilities. + * + * @internal + */ +export const NumberUtils = { + /** Reads a little-endian 32-bit integer from source */ + getInt32LE(source: Uint8Array, offset: number): number { + return ( + source[offset] | + (source[offset + 1] << 8) | + (source[offset + 2] << 16) | + (source[offset + 3] << 24) + ); + }, + + /** Reads a little-endian 32-bit unsigned integer from source */ + getUInt32LE(source: Uint8Array, offset: number): number { + return ( + source[offset] + + source[offset + 1] * 256 + + source[offset + 2] * 65536 + + source[offset + 3] * 16777216 + ); + }, + + /** Reads a big-endian 32-bit integer from source */ + getUint32BE(source: Uint8Array, offset: number): number { + return ( + source[offset + 3] + + source[offset + 2] * 256 + + source[offset + 1] * 65536 + + source[offset] * 16777216 + ); + }, + + /** Reads a little-endian 64-bit integer from source */ + getBigInt64LE(source: Uint8Array, offset: number): bigint { + const lo = NumberUtils.getUInt32LE(source, offset); + const hi = NumberUtils.getUInt32LE(source, offset + 4); + + /* + eslint-disable-next-line no-restricted-globals + -- This is allowed since this helper should not be called unless bigint features are enabled + */ + return (BigInt(hi) << BigInt(32)) + BigInt(lo); + }, + + /** Reads a little-endian 64-bit float from source */ + getFloat64LE(source: Uint8Array, offset: number): number { + FLOAT_BYTES[0] = source[offset]; + FLOAT_BYTES[1] = source[offset + 1]; + FLOAT_BYTES[2] = source[offset + 2]; + FLOAT_BYTES[3] = source[offset + 3]; + FLOAT_BYTES[4] = source[offset + 4]; + FLOAT_BYTES[5] = source[offset + 5]; + FLOAT_BYTES[6] = source[offset + 6]; + FLOAT_BYTES[7] = source[offset + 7]; + return FLOAT[0]; + }, + + /** Writes a big-endian 32-bit integer to destination, can be signed or unsigned */ + setInt32BE(destination: Uint8Array, offset: number, value: number): 4 { + destination[offset + 3] = value; + value >>>= 8; + destination[offset + 2] = value; + value >>>= 8; + destination[offset + 1] = value; + value >>>= 8; + destination[offset] = value; + return 4; + }, + + /** Writes a little-endian 32-bit integer to destination, can be signed or unsigned */ + setInt32LE(destination: Uint8Array, offset: number, value: number): 4 { + destination[offset] = value; + value >>>= 8; + destination[offset + 1] = value; + value >>>= 8; + destination[offset + 2] = value; + value >>>= 8; + destination[offset + 3] = value; + return 4; + }, + + /** Write a little-endian 64-bit integer to source */ + setBigInt64LE(destination: Uint8Array, offset: number, value: bigint): 8 { + /* eslint-disable-next-line no-restricted-globals -- This is allowed here as useBigInt64=true */ + const mask32bits = BigInt(0xffff_ffff); + + /** lower 32 bits */ + let lo = Number(value & mask32bits); + destination[offset] = lo; + lo >>= 8; + destination[offset + 1] = lo; + lo >>= 8; + destination[offset + 2] = lo; + lo >>= 8; + destination[offset + 3] = lo; + + /* + eslint-disable-next-line no-restricted-globals + -- This is allowed here as useBigInt64=true + + upper 32 bits + */ + let hi = Number((value >> BigInt(32)) & mask32bits); + destination[offset + 4] = hi; + hi >>= 8; + destination[offset + 5] = hi; + hi >>= 8; + destination[offset + 6] = hi; + hi >>= 8; + destination[offset + 7] = hi; + + return 8; + }, + + /** Writes a little-endian 64-bit float to destination */ + setFloat64LE(destination: Uint8Array, offset: number, value: number): 8 { + FLOAT[0] = value; + destination[offset] = FLOAT_BYTES[0]; + destination[offset + 1] = FLOAT_BYTES[1]; + destination[offset + 2] = FLOAT_BYTES[2]; + destination[offset + 3] = FLOAT_BYTES[3]; + destination[offset + 4] = FLOAT_BYTES[4]; + destination[offset + 5] = FLOAT_BYTES[5]; + destination[offset + 6] = FLOAT_BYTES[6]; + destination[offset + 7] = FLOAT_BYTES[7]; + return 8; + } +}; diff --git a/test/node/release.test.ts b/test/node/release.test.ts index c2a20cf4..c04b5124 100644 --- a/test/node/release.test.ts +++ b/test/node/release.test.ts @@ -45,6 +45,7 @@ const REQUIRED_FILES = [ 'src/timestamp.ts', 'src/utils/byte_utils.ts', 'src/utils/node_byte_utils.ts', + 'src/utils/number_utils.ts', 'src/utils/web_byte_utils.ts', 'src/utils/latin.ts', 'src/validate_utf8.ts', From 10045be5a1ff16fccaf07cf6187d5d38aa4b9617 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 27 Feb 2024 13:13:44 -0500 Subject: [PATCH 2/2] cleanups --- src/parser/deserializer.ts | 24 +++++++++--------------- src/utils/number_utils.ts | 6 +++--- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/parser/deserializer.ts b/src/parser/deserializer.ts index c43eb3cc..004f5bb1 100644 --- a/src/parser/deserializer.ts +++ b/src/parser/deserializer.ts @@ -280,9 +280,8 @@ function deserializeObject( if (promoteValues === false) value = new Double(value); } else if (elementType === constants.BSON_DATA_DATE) { const lowBits = NumberUtils.getInt32LE(buffer, index); - index += 4; - const highBits = NumberUtils.getInt32LE(buffer, index); - index += 4; + const highBits = NumberUtils.getInt32LE(buffer, index + 4); + index += 8; value = new Date(new Long(lowBits, highBits).toNumber()); } else if (elementType === constants.BSON_DATA_BOOLEAN) { @@ -340,9 +339,8 @@ function deserializeObject( } else { // Unpack the low and high bits const lowBits = NumberUtils.getInt32LE(buffer, index); - index += 4; - const highBits = NumberUtils.getInt32LE(buffer, index); - index += 4; + const highBits = NumberUtils.getInt32LE(buffer, index + 4); + index += 8; const long = new Long(lowBits, highBits); // Promote the long if possible @@ -516,15 +514,11 @@ function deserializeObject( value = promoteValues ? symbol : new BSONSymbol(symbol); index = index + stringSize; } else if (elementType === constants.BSON_DATA_TIMESTAMP) { - // We intentionally **do not** use bit shifting here - // Bit shifting in javascript coerces numbers to **signed** int32s - // We need to keep i, and t unsigned - const i = NumberUtils.getUInt32LE(buffer, index); - index += 4; - const t = NumberUtils.getUInt32LE(buffer, index); - index += 4; - - value = new Timestamp({ i, t }); + value = new Timestamp({ + i: NumberUtils.getUint32LE(buffer, index), + t: NumberUtils.getUint32LE(buffer, index + 4) + }); + index += 8; } else if (elementType === constants.BSON_DATA_MIN_KEY) { value = new MinKey(); } else if (elementType === constants.BSON_DATA_MAX_KEY) { diff --git a/src/utils/number_utils.ts b/src/utils/number_utils.ts index 8c866a1a..a3ccc8e2 100644 --- a/src/utils/number_utils.ts +++ b/src/utils/number_utils.ts @@ -18,7 +18,7 @@ export const NumberUtils = { }, /** Reads a little-endian 32-bit unsigned integer from source */ - getUInt32LE(source: Uint8Array, offset: number): number { + getUint32LE(source: Uint8Array, offset: number): number { return ( source[offset] + source[offset + 1] * 256 + @@ -39,8 +39,8 @@ export const NumberUtils = { /** Reads a little-endian 64-bit integer from source */ getBigInt64LE(source: Uint8Array, offset: number): bigint { - const lo = NumberUtils.getUInt32LE(source, offset); - const hi = NumberUtils.getUInt32LE(source, offset + 4); + const lo = NumberUtils.getUint32LE(source, offset); + const hi = NumberUtils.getUint32LE(source, offset + 4); /* eslint-disable-next-line no-restricted-globals