diff --git a/.eslintrc.json b/.eslintrc.json index 615cd963..3e482551 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -75,7 +75,8 @@ "no-bigint-usage/no-bigint-literals": "error", "no-restricted-globals": [ "error", - "BigInt" + "BigInt", + "DataView" ] }, "overrides": [ 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 69c2a24f..09ccd0ff 100644 --- a/src/objectid.ts +++ b/src/objectid.ts @@ -1,7 +1,8 @@ import { BSONValue } from './bson_value'; import { BSONError } from './error'; import { type InspectFn, defaultInspect } from './parser/utils'; -import { BSONDataView, ByteUtils } from './utils/byte_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,7 +180,7 @@ export class ObjectId extends BSONValue { const buffer = ByteUtils.allocate(12); // 4-byte timestamp - BSONDataView.fromUint8Array(buffer).setUint32(0, time, false); + NumberUtils.setInt32BE(buffer, 0, time); // set PROCESS_UNIQUE if yet not initialized if (PROCESS_UNIQUE === null) { @@ -259,7 +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 = BSONDataView.fromUint8Array(this.id).getUint32(0, false); + const time = NumberUtils.getUint32BE(this.buffer, 0); timestamp.setTime(Math.floor(time) * 1000); return timestamp; } @@ -292,9 +293,10 @@ export class ObjectId extends BSONValue { * @param time - an integer number representing a number of seconds. */ static createFromTime(time: number): ObjectId { - const buffer = ByteUtils.fromNumberArray([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + const buffer = ByteUtils.allocate(12); + for (let i = 11; i >= 4; i--) buffer[i] = 0; // Encode time into first 4 bytes - BSONDataView.fromUint8Array(buffer).setUint32(0, time, false); + 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 c23b3658..004f5bb1 100644 --- a/src/parser/deserializer.ts +++ b/src/parser/deserializer.ts @@ -14,7 +14,8 @@ import { ObjectId } from '../objectid'; import { BSONRegExp } from '../regexp'; import { BSONSymbol } from '../symbol'; import { Timestamp } from '../timestamp'; -import { BSONDataView, ByteUtils } from '../utils/byte_utils'; +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}`); @@ -204,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'); @@ -218,8 +215,6 @@ function deserializeObject( let isPossibleDBRef = isArray ? false : null; - let dataView; - // While we have more left data left keep parsing while (!done) { // Read the type @@ -257,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 || @@ -277,34 +269,19 @@ 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); - } else if (elementType === constants.BSON_DATA_NUMBER && promoteValues === false) { - dataView ??= new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); - value = new Double(dataView.getFloat64(index, true)); - index = index + 8; + value = NumberUtils.getInt32LE(buffer, index); + index += 4; } else if (elementType === constants.BSON_DATA_NUMBER) { - dataView ??= new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); - value = dataView.getFloat64(index, true); - index = index + 8; + 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); + const highBits = NumberUtils.getInt32LE(buffer, index + 4); + index += 8; value = new Date(new Long(lowBits, highBits).toNumber()); } else if (elementType === constants.BSON_DATA_BOOLEAN) { @@ -313,11 +290,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'); @@ -335,11 +309,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 @@ -363,30 +333,25 @@ function deserializeObject( } else if (elementType === constants.BSON_DATA_NULL) { 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) | - (buffer[index++] << 16) | - (buffer[index++] << 24); - const highBits = - buffer[index++] | - (buffer[index++] << 8) | - (buffer[index++] << 16) | - (buffer[index++] << 24); - const long = new Long(lowBits, highBits); 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() - : long; + value = NumberUtils.getBigInt64LE(buffer, index); + index += 8; } else { - value = long; + // Unpack the low and high bits + const lowBits = NumberUtils.getInt32LE(buffer, index); + const highBits = NumberUtils.getInt32LE(buffer, index + 4); + index += 8; + + const long = new Long(lowBits, highBits); + // Promote the long if possible + if (promoteLongs && promoteValues === true) { + value = + long.lessThanOrEqual(JS_INT_MAX_LONG) && long.greaterThanOrEqual(JS_INT_MIN_LONG) + ? long.toNumber() + : long; + } else { + value = long; + } } } else if (elementType === constants.BSON_DATA_DECIMAL128) { // Buffer to contain the decimal bytes @@ -398,11 +363,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++]; @@ -417,11 +379,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) @@ -442,11 +401,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) @@ -545,11 +501,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 || @@ -561,31 +514,18 @@ 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 = - 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); - - 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) { 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 || @@ -605,11 +545,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) { @@ -617,11 +554,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 || @@ -643,11 +577,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 @@ -666,11 +596,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 0c6ddcea..86490798 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,10 +70,6 @@ function serializeString(buffer: Uint8Array, key: string, value: string, index: return index; } -const NUMBER_SPACE = new DataView(new ArrayBuffer(8), 0, 8); -const FOUR_BYTE_VIEW_ON_NUMBER = new Uint8Array(NUMBER_SPACE.buffer, 0, 4); -const EIGHT_BYTE_VIEW_ON_NUMBER = new Uint8Array(NUMBER_SPACE.buffer, 0, 8); - function serializeNumber(buffer: Uint8Array, key: string, value: number, index: number) { const isNegativeZero = Object.is(value, -0); @@ -87,23 +81,17 @@ function serializeNumber(buffer: Uint8Array, key: string, value: number, index: ? constants.BSON_DATA_INT : constants.BSON_DATA_NUMBER; - if (type === constants.BSON_DATA_INT) { - NUMBER_SPACE.setInt32(0, value, true); - } else { - NUMBER_SPACE.setFloat64(0, value, true); - } - - const bytes = - type === constants.BSON_DATA_INT ? FOUR_BYTE_VIEW_ON_NUMBER : EIGHT_BYTE_VIEW_ON_NUMBER; - buffer[index++] = type; const numberOfWrittenBytes = ByteUtils.encodeUTF8Into(buffer, key, index); index = index + numberOfWrittenBytes; buffer[index++] = 0x00; - buffer.set(bytes, index); - index += bytes.byteLength; + if (type === constants.BSON_DATA_INT) { + index += NumberUtils.setInt32LE(buffer, index, value); + } else { + index += NumberUtils.setFloat64LE(buffer, index, value); + } return index; } @@ -115,10 +103,9 @@ function serializeBigInt(buffer: Uint8Array, key: string, value: bigint, 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; + + index += NumberUtils.setBigInt64LE(buffer, index, value); + return index; } @@ -162,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; } @@ -273,10 +254,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 @@ -355,15 +333,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; } @@ -377,10 +349,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; } @@ -396,11 +365,8 @@ function serializeDouble(buffer: Uint8Array, key: string, value: Double, index: buffer[index++] = 0; // Write float - NUMBER_SPACE.setFloat64(0, value.value, true); - buffer.set(EIGHT_BYTE_VIEW_ON_NUMBER, index); + index += NumberUtils.setFloat64LE(buffer, index, value.value); - // Adjust index - index = index + 8; return index; } @@ -417,10 +383,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 @@ -459,10 +422,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 @@ -485,10 +445,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 { @@ -503,10 +460,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 @@ -531,20 +485,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); } if (size <= 16) { @@ -568,14 +516,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; } @@ -622,10 +567,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; } @@ -797,7 +739,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 '.'"); } } @@ -905,7 +847,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 '.'"); } } @@ -995,9 +937,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/byte_utils.ts b/src/utils/byte_utils.ts index 8cc7e56d..e4c51dcf 100644 --- a/src/utils/byte_utils.ts +++ b/src/utils/byte_utils.ts @@ -51,9 +51,3 @@ const hasGlobalBuffer = typeof Buffer === 'function' && Buffer.prototype?._isBuf * @internal */ export const ByteUtils: ByteUtils = hasGlobalBuffer ? nodeJsByteUtils : webByteUtils; - -export class BSONDataView extends DataView { - static fromUint8Array(input: Uint8Array) { - return new DataView(input.buffer, input.byteOffset, input.byteLength); - } -} diff --git a/src/utils/number_utils.ts b/src/utils/number_utils.ts new file mode 100644 index 00000000..a3ccc8e2 --- /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/src/utils/web_byte_utils.ts b/src/utils/web_byte_utils.ts index 374e3835..62ae589e 100644 --- a/src/utils/web_byte_utils.ts +++ b/src/utils/web_byte_utils.ts @@ -189,9 +189,9 @@ export const webByteUtils = { return new TextEncoder().encode(input).byteLength; }, - encodeUTF8Into(buffer: Uint8Array, source: string, byteOffset: number): number { + encodeUTF8Into(uint8array: Uint8Array, source: string, byteOffset: number): number { const bytes = new TextEncoder().encode(source); - buffer.set(bytes, byteOffset); + uint8array.set(bytes, byteOffset); return bytes.byteLength; }, diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index 8daa2bb2..9532859f 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -2,7 +2,6 @@ import { BSON, BSONError, EJSON, __noBigInt__ } 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 () { beforeEach(function () { @@ -126,7 +125,11 @@ describe('BSON BigInt support', function () { const DATA_TYPE_OFFSET = 4; const KEY_OFFSET = 5; - const dataView = BSONDataView.fromUint8Array(serializedDoc); + const dataView = new DataView( + serializedDoc.buffer, + serializedDoc.byteOffset, + serializedDoc.byteLength + ); const keySlice = serializedDoc.slice(KEY_OFFSET); let keyLength = 0; @@ -407,7 +410,11 @@ describe('BSON BigInt support', function () { const serialized = BSON.serialize(number); const VALUE_OFFSET = 7; - const dataView = BSONDataView.fromUint8Array(serialized); + const dataView = new DataView( + serialized.buffer, + serialized.byteOffset, + serialized.byteLength + ); const serializedValue = dataView.getBigInt64(VALUE_OFFSET, true); const parsed = JSON.parse(stringified); @@ -431,7 +438,11 @@ describe('BSON BigInt support', function () { const serializedDoc = BSON.serialize(number); const VALUE_OFFSET = 7; - const dataView = BSONDataView.fromUint8Array(serializedDoc); + const dataView = new DataView( + serializedDoc.buffer, + serializedDoc.byteOffset, + serializedDoc.byteLength + ); const parsed = JSON.parse(stringified); expect(parsed).to.have.property('a'); 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', diff --git a/test/node/utils/number_utils.test.ts b/test/node/utils/number_utils.test.ts new file mode 100644 index 00000000..00d9c84a --- /dev/null +++ b/test/node/utils/number_utils.test.ts @@ -0,0 +1,130 @@ +import { expect } from 'chai'; +import { NumberUtils } from '../../../src/utils/number_utils'; + +describe('NumberUtils', () => { + /** Make a Uint8Array in a less verbose way */ + const b = (...values) => new Uint8Array(values); + + context('getInt32LE()', () => { + it('parses an int32 little endian', () => { + expect(NumberUtils.getInt32LE(b(0, 0, 0, 1), 0)).to.equal(1 << 24); + }); + + it('parses an signed int32 little endian', () => { + expect(NumberUtils.getInt32LE(b(255, 255, 255, 255), 0)).to.equal(-1); + }); + + it('parses an int32 little endian at offset', () => { + expect(NumberUtils.getInt32LE(b(0, 0, 0, 0, 0, 1), 2)).to.equal(1 << 24); + }); + + it('does not check bounds of offset', () => { + expect(NumberUtils.getInt32LE(b(0, 0, 0, 1), 4)).to.equal(0); + }); + }); + + context('getUint32LE()', () => { + it('parses an unsigned int32 little endian', () => { + expect(NumberUtils.getUint32LE(b(255, 255, 255, 255), 0)).to.equal(0xffff_ffff); + }); + + it('parses an int32 little endian at offset', () => { + expect(NumberUtils.getUint32LE(b(0, 0, 255, 255, 255, 255), 2)).to.equal(0xffff_ffff); + }); + + it('does not check bounds of offset', () => { + expect(NumberUtils.getUint32LE(b(0, 0, 0, 1), 4)).to.be.NaN; + }); + }); + + context('getUint32BE()', () => { + it('parses an int32 big endian', () => { + expect(NumberUtils.getUint32BE(b(0, 0, 0, 1), 0)).to.equal(1); + }); + + it('parses an unsigned int32 big endian', () => { + expect(NumberUtils.getUint32LE(b(255, 255, 255, 255), 0)).to.equal(0xffff_ffff); + }); + + it('parses an int32 big endian at offset', () => { + expect(NumberUtils.getUint32BE(b(0, 0, 0, 0, 0, 1), 2)).to.equal(1); + }); + + it('does not check bounds of offset', () => { + expect(NumberUtils.getUint32BE(b(0, 0, 0, 1), 4)).to.be.NaN; + }); + }); + + context('getBigInt64LE()', () => { + it('parses an int64 little endian', () => { + expect(NumberUtils.getBigInt64LE(b(0, 0, 0, 0, 0, 0, 0, 1), 0)).to.equal(1n << 56n); + }); + + it('parses an int64 little endian at offset', () => { + expect(NumberUtils.getBigInt64LE(b(0, 0, 0, 0, 0, 0, 0, 0, 0, 1), 2)).to.equal(1n << 56n); + }); + + it('throws if offset is out of bounds', () => { + expect(() => NumberUtils.getBigInt64LE(b(0, 0, 0, 0, 0, 0, 0, 1), 4)).to.throw(RangeError); + }); + }); + + context('getFloat64LE()', () => { + /** 2.3 in bytes */ + const num = [0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x02, 0x40]; + + it('parses an float64 little endian', () => { + expect(NumberUtils.getFloat64LE(b(...num), 0)).to.equal(2.3); + }); + + it('parses an float64 little endian at offset', () => { + expect(NumberUtils.getFloat64LE(b(0, 0, ...num), 2)).to.equal(2.3); + }); + + it('does not check bounds of offset', () => { + expect(NumberUtils.getFloat64LE(b(...num), 4)).to.not.equal(2.3); + }); + }); + + context('setInt32BE()', () => { + it('writes an int32 big endian', () => { + const space = new Uint8Array(4); + expect(NumberUtils.setInt32BE(space, 0, 1)).to.equal(4); + expect(space).to.deep.equal(b(0, 0, 0, 1)); + }); + + it('writes an int32 big endian at offset', () => { + const space = new Uint8Array(6); + expect(NumberUtils.setInt32BE(space, 2, 1)).to.equal(4); + expect(space).to.deep.equal(b(0, 0, 0, 0, 0, 1)); + }); + + it('does not bound or type check', () => { + const space = {}; + // @ts-expect-error: testing incorrect type + expect(NumberUtils.setInt32BE(space, 'a', 1)).to.equal(4); + expect(space).to.deep.equal({ a: 0, a1: 0, a2: 0, a3: 1 }); + }); + }); + + context('setInt32LE()', () => { + it('writes an int32 big endian', () => { + const space = new Uint8Array(4); + expect(NumberUtils.setInt32LE(space, 0, 1)).to.equal(4); + expect(space).to.deep.equal(b(1, 0, 0, 0)); + }); + + it('writes an int32 big endian at offset', () => { + const space = new Uint8Array(6); + expect(NumberUtils.setInt32LE(space, 2, 1)).to.equal(4); + expect(space).to.deep.equal(b(0, 0, 1, 0, 0, 0)); + }); + + it('does not bound or type check', () => { + const space = {}; + // @ts-expect-error: testing incorrect type + expect(NumberUtils.setInt32LE(space, 'a', 1)).to.equal(4); + expect(space).to.deep.equal({ a: 1, a1: 0, a2: 0, a3: 0 }); + }); + }); +});