diff --git a/src/bson.ts b/src/bson.ts index 7bd83a98..0dc8346e 100644 --- a/src/bson.ts +++ b/src/bson.ts @@ -51,7 +51,7 @@ export { Decimal128 }; export { BSONValue } from './bson_value'; -export { BSONError, BSONVersionError, BSONRuntimeError } from './error'; +export { BSONError, BSONVersionError, BSONRuntimeError, BSONOffsetError } from './error'; export { BSONType } from './constants'; export { EJSON } from './extended_json'; export { onDemand, type OnDemand } from './parser/on_demand/index'; diff --git a/src/error.ts b/src/error.ts index 7203f46b..ef5184a4 100644 --- a/src/error.ts +++ b/src/error.ts @@ -98,8 +98,8 @@ export class BSONOffsetError extends BSONError { public offset: number; - constructor(message: string, offset: number) { - super(`${message}. offset: ${offset}`); + constructor(message: string, offset: number, options?: { cause?: unknown }) { + super(`${message}. offset: ${offset}`, options); this.offset = offset; } } diff --git a/src/parser/on_demand/index.ts b/src/parser/on_demand/index.ts index 1b86d728..f099c115 100644 --- a/src/parser/on_demand/index.ts +++ b/src/parser/on_demand/index.ts @@ -1,8 +1,6 @@ -import { type BSONError, BSONOffsetError } from '../../error'; import { ByteUtils } from '../../utils/byte_utils'; import { NumberUtils } from '../../utils/number_utils'; -import { type BSONElement, parseToElements, getSize } from './parse_to_elements'; -import { type BSONReviver, type Container, parseToStructure } from './parse_to_structure'; +import { type BSONElement, parseToElements } from './parse_to_elements'; /** * @experimental * @public @@ -10,31 +8,13 @@ import { type BSONReviver, type Container, parseToStructure } from './parse_to_s * A new set of BSON APIs that are currently experimental and not intended for production use. */ export type OnDemand = { - BSONOffsetError: { - new (message: string, offset: number): BSONOffsetError; - isBSONError(value: unknown): value is BSONError; - }; parseToElements: (this: void, bytes: Uint8Array, startOffset?: number) => Iterable; - parseToStructure: < - TRoot extends Container = { - dest: Record; - kind: 'object'; - } - >( - bytes: Uint8Array, - startOffset?: number, - root?: TRoot, - reviver?: BSONReviver - ) => TRoot extends undefined ? Record : TRoot['dest']; // Types BSONElement: BSONElement; - Container: Container; - BSONReviver: BSONReviver; // Utils ByteUtils: ByteUtils; NumberUtils: NumberUtils; - getSize: (source: Uint8Array, offset: number) => number; }; /** @@ -44,11 +24,8 @@ export type OnDemand = { const onDemand: OnDemand = Object.create(null); onDemand.parseToElements = parseToElements; -onDemand.parseToStructure = parseToStructure; -onDemand.BSONOffsetError = BSONOffsetError; onDemand.ByteUtils = ByteUtils; onDemand.NumberUtils = NumberUtils; -onDemand.getSize = getSize; Object.freeze(onDemand); diff --git a/src/parser/on_demand/parse_to_elements.ts b/src/parser/on_demand/parse_to_elements.ts index 3672b6f5..f2c8e697 100644 --- a/src/parser/on_demand/parse_to_elements.ts +++ b/src/parser/on_demand/parse_to_elements.ts @@ -1,4 +1,5 @@ import { BSONOffsetError } from '../../error'; +import { NumberUtils } from '../../utils/number_utils'; /** * @internal @@ -44,22 +45,12 @@ export type BSONElement = [ length: number ]; -/** - * @experimental - * @public - * - * Parses a int32 little-endian at offset, throws if it is negative - */ -export function getSize(source: Uint8Array, offset: number): number { - if (source[offset + 3] > 127) { - throw new BSONOffsetError('BSON size cannot be negative', offset); +function getSize(source: Uint8Array, offset: number) { + try { + return NumberUtils.getNonnegativeInt32LE(source, offset); + } catch (cause) { + throw new BSONOffsetError('BSON size cannot be negative', offset, { cause }); } - return ( - source[offset] | - (source[offset + 1] << 8) | - (source[offset + 2] << 16) | - (source[offset + 3] << 24) - ); } /** diff --git a/src/parser/on_demand/parse_to_structure.ts b/src/parser/on_demand/parse_to_structure.ts deleted file mode 100644 index 2924c725..00000000 --- a/src/parser/on_demand/parse_to_structure.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { type Code } from '../../code'; -import { type BSONElement, getSize, parseToElements } from './parse_to_elements'; - -/** @internal */ -const DEFAULT_REVIVER: BSONReviver = ( - _bytes: Uint8Array, - _container: Container, - _element: BSONElement -) => null; - -/** @internal */ -function parseToElementsToArray(bytes: Uint8Array, offset?: number | null): BSONElement[] { - const res = parseToElements(bytes, offset); - return Array.isArray(res) ? res : [...res]; -} - -/** @internal */ -type ParseContext = { - elementOffset: number; - elements: BSONElement[]; - container: Container; - previous: ParseContext | null; -}; - -/** - * @experimental - * @public - * A union of the possible containers for BSON elements. - * - * Depending on kind, a reviver can accurately assign a value to a name on the container. - */ -export type Container = - | { - dest: Record; - kind: 'object'; - } - | { - dest: Map; - kind: 'map'; - } - | { - dest: Array; - kind: 'array'; - } - | { - dest: Code; - kind: 'code'; - } - | { - kind: 'custom'; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - dest: any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; - }; - -/** - * @experimental - * @public - */ -export type BSONReviver = ( - bytes: Uint8Array, - container: Container, - element: BSONElement -) => Container | null; - -/** - * @experimental - * @public - */ -export function parseToStructure< - TRoot extends Container = { - dest: Record; - kind: 'object'; - } ->( - bytes: Uint8Array, - startOffset?: number | null, - pRoot?: TRoot | null, - pReviver?: BSONReviver | null -): TRoot extends undefined ? Record : TRoot['dest'] { - const root = pRoot ?? { - kind: 'object', - dest: Object.create(null) as Record - }; - - const reviver = pReviver ?? DEFAULT_REVIVER; - - let ctx: ParseContext | null = { - elementOffset: 0, - elements: parseToElementsToArray(bytes, startOffset), - container: root, - previous: null - }; - - /** BSONElement offsets: type indicator and value offset */ - const enum BSONElementOffset { - type = 0, - offset = 3 - } - - /** BSON Embedded types */ - const enum BSONElementType { - object = 3, - array = 4, - javascriptWithScope = 15 - } - - embedded: while (ctx !== null) { - for ( - let bsonElement: BSONElement | undefined = ctx.elements[ctx.elementOffset++]; - bsonElement != null; - bsonElement = ctx.elements[ctx.elementOffset++] - ) { - const type = bsonElement[BSONElementOffset.type]; - const offset = bsonElement[BSONElementOffset.offset]; - - const container = reviver(bytes, ctx.container, bsonElement); - const isEmbeddedType = - type === BSONElementType.object || - type === BSONElementType.array || - type === BSONElementType.javascriptWithScope; - - if (container != null && isEmbeddedType) { - const docOffset: number = - type !== BSONElementType.javascriptWithScope - ? offset - : // value offset + codeSize + value int + code int - offset + getSize(bytes, offset + 4) + 4 + 4; - - ctx = { - elementOffset: 0, - elements: parseToElementsToArray(bytes, docOffset), - container, - previous: ctx - }; - - continue embedded; - } - } - ctx = ctx.previous; - } - - return root.dest; -} diff --git a/src/utils/byte_utils.ts b/src/utils/byte_utils.ts index 63ad742e..f3da53fd 100644 --- a/src/utils/byte_utils.ts +++ b/src/utils/byte_utils.ts @@ -10,7 +10,7 @@ import { webByteUtils } from './web_byte_utils'; */ export type ByteUtils = { /** Transforms the input to an instance of Buffer if running on node, otherwise Uint8Array */ - toLocalBufferType(buffer: Uint8Array | ArrayBufferView | ArrayBuffer): Uint8Array; + toLocalBufferType: (buffer: Uint8Array | ArrayBufferView | ArrayBuffer) => Uint8Array; /** Create empty space of size */ allocate: (size: number) => Uint8Array; /** Create empty space of size, use pooled memory when available */ @@ -36,9 +36,9 @@ export type ByteUtils = { /** Get the utf8 code unit count from a string if it were to be transformed to utf8 */ utf8ByteLength: (input: string) => number; /** Encode UTF8 bytes generated from `source` string into `destination` at byteOffset. Returns the number of bytes encoded. */ - encodeUTF8Into(destination: Uint8Array, source: string, byteOffset: number): number; + encodeUTF8Into: (destination: Uint8Array, source: string, byteOffset: number) => number; /** Generate a Uint8Array filled with random bytes with byteLength */ - randomBytes(byteLength: number): Uint8Array; + randomBytes: (byteLength: number) => Uint8Array; }; declare const Buffer: { new (): unknown; prototype?: { _isBuffer?: boolean } } | undefined; diff --git a/src/utils/number_utils.ts b/src/utils/number_utils.ts index 162897e9..32f6f5cc 100644 --- a/src/utils/number_utils.ts +++ b/src/utils/number_utils.ts @@ -13,15 +13,19 @@ const isBigEndian = FLOAT_BYTES[7] === 0; * A collection of functions that get or set various numeric types and bit widths from a Uint8Array. */ export type NumberUtils = { - getInt32LE(source: Uint8Array, offset: number): number; - getUint32LE(source: Uint8Array, offset: number): number; - getUint32BE(source: Uint8Array, offset: number): number; - getBigInt64LE(source: Uint8Array, offset: number): bigint; - getFloat64LE(source: Uint8Array, offset: number): number; - setInt32BE(destination: Uint8Array, offset: number, value: number): 4; - setInt32LE(destination: Uint8Array, offset: number, value: number): 4; - setBigInt64LE(destination: Uint8Array, offset: number, value: bigint): 8; - setFloat64LE(destination: Uint8Array, offset: number, value: number): 8; + /** + * Parses a signed int32 at offset. Throws a `RangeError` if value is negative. + */ + getNonnegativeInt32LE: (source: Uint8Array, offset: number) => number; + getInt32LE: (source: Uint8Array, offset: number) => number; + getUint32LE: (source: Uint8Array, offset: number) => number; + getUint32BE: (source: Uint8Array, offset: number) => number; + getBigInt64LE: (source: Uint8Array, offset: number) => bigint; + getFloat64LE: (source: Uint8Array, offset: number) => number; + setInt32BE: (destination: Uint8Array, offset: number, value: number) => 4; + setInt32LE: (destination: Uint8Array, offset: number, value: number) => 4; + setBigInt64LE: (destination: Uint8Array, offset: number, value: bigint) => 8; + setFloat64LE: (destination: Uint8Array, offset: number, value: number) => 8; }; /** @@ -31,6 +35,18 @@ export type NumberUtils = { * @public */ export const NumberUtils: NumberUtils = { + getNonnegativeInt32LE(source: Uint8Array, offset: number): number { + if (source[offset + 3] > 127) { + throw new RangeError(`Size cannot be negative at offset: ${offset}`); + } + return ( + source[offset] | + (source[offset + 1] << 8) | + (source[offset + 2] << 16) | + (source[offset + 3] << 24) + ); + }, + /** Reads a little-endian 32-bit integer from source */ getInt32LE(source: Uint8Array, offset: number): number { return ( diff --git a/test/node/error.test.ts b/test/node/error.test.ts index 5a126d07..9cb91dfd 100644 --- a/test/node/error.test.ts +++ b/test/node/error.test.ts @@ -6,7 +6,7 @@ import { BSONError, BSONVersionError, BSONRuntimeError, - onDemand + BSONOffsetError } from '../register-bson'; const instanceOfChecksWork = !__isWeb__; @@ -111,19 +111,19 @@ describe('BSONError', function () { describe('class BSONOffsetError', () => { it('is a BSONError instance', function () { - expect(BSONError.isBSONError(new onDemand.BSONOffsetError('Oopsie', 3))).to.be.true; + expect(BSONError.isBSONError(new BSONOffsetError('Oopsie', 3))).to.be.true; }); it('has a name property equal to "BSONOffsetError"', function () { - expect(new onDemand.BSONOffsetError('Woops!', 3)).to.have.property('name', 'BSONOffsetError'); + expect(new BSONOffsetError('Woops!', 3)).to.have.property('name', 'BSONOffsetError'); }); it('sets the offset property', function () { - expect(new onDemand.BSONOffsetError('Woops!', 3)).to.have.property('offset', 3); + expect(new BSONOffsetError('Woops!', 3)).to.have.property('offset', 3); }); it('includes the offset in the message', function () { - expect(new onDemand.BSONOffsetError('Woops!', 3)) + expect(new BSONOffsetError('Woops!', 3)) .to.have.property('message') .that.matches(/offset: 3/i); }); diff --git a/test/node/exports.test.ts b/test/node/exports.test.ts index 953e8872..f3f08a54 100644 --- a/test/node/exports.test.ts +++ b/test/node/exports.test.ts @@ -30,6 +30,7 @@ const EXPECTED_EXPORTS = [ 'Decimal128', 'BSONError', 'BSONRuntimeError', + 'BSONOffsetError', 'setInternalBufferSize', 'serialize', 'serializeWithBufferAndIndex', diff --git a/test/node/parser/on_demand/parse_to_elements.test.ts b/test/node/parser/on_demand/parse_to_elements.test.ts index 068641fc..62c617f3 100644 --- a/test/node/parser/on_demand/parse_to_elements.test.ts +++ b/test/node/parser/on_demand/parse_to_elements.test.ts @@ -5,7 +5,7 @@ import * as BSON from '../../../register-bson'; import { bufferFromHexArray, stringToUTF8HexBytes, int32LEToHex } from '../../tools/utils'; const parseToElements = BSON.onDemand.parseToElements; -const BSONOffsetError = BSON.onDemand.BSONOffsetError; +const BSONOffsetError = BSON.BSONOffsetError; describe('parseToElements()', () => { context('when given less than 5 bytes', () => { diff --git a/test/node/parser/on_demand/parse_to_structure.test.ts b/test/node/parser/on_demand/parse_to_structure.test.ts deleted file mode 100644 index 86b6c4e8..00000000 --- a/test/node/parser/on_demand/parse_to_structure.test.ts +++ /dev/null @@ -1,421 +0,0 @@ -import * as sinon from 'sinon'; -import { expect } from 'chai'; -import { Code, onDemand } from '../../../register-bson'; -import { ByteUtils } from '../../../../src/utils/byte_utils'; - -import { bufferFromHexArray, stringToUTF8HexBytes, int32LEToHex } from '../../tools/utils'; - -const parseToStructure = onDemand.parseToStructure; - -const enum e { - type = 0, - nameOffset = 1, - nameLength = 2, - offset = 3, - length = 4 -} - -describe('parseToStructure()', () => { - context('when called with an empty document sequence', () => { - it('returns an object with no properties', () => { - const res = parseToStructure(new Uint8Array([5, 0, 0, 0, 0])); - expect(res).to.deep.equal(Object.create(null)); - }); - - it('returns an object with a null prototype', () => { - const res = parseToStructure(new Uint8Array([5, 0, 0, 0, 0])); - expect(Object.getPrototypeOf(res)).to.be.null; - }); - - it('never calls reviver', () => { - const spy = sinon.spy(); - parseToStructure(new Uint8Array([5, 0, 0, 0, 0]), undefined, undefined, spy); - expect(spy).to.not.have.been.called; - }); - - it('returns given root container', () => { - const dest = new Map(); - const res = parseToStructure(new Uint8Array([5, 0, 0, 0, 0]), undefined, { - kind: 'map', - dest - }); - // instance eq check - expect(res).to.equal(dest); - }); - }); - - context('when called with a single element sequence', () => { - const bsonBytes = bufferFromHexArray([ - '10', // int32 type - '6100', // 'a' key with key null terminator - '01000000' // little endian int32 - ]); - - it('calls the reviver with the same instance of the input bytes', () => { - const spy = sinon.spy(); - parseToStructure(bsonBytes, undefined, undefined, spy); - expect(spy).to.have.been.calledWith(sinon.match.same(bsonBytes)); - }); - - it('calls the reviver with default object container', () => { - const spy = sinon.spy(); - parseToStructure(bsonBytes, undefined, undefined, spy); - expect(spy).to.have.been.calledWith( - sinon.match.any, - sinon.match({ kind: 'object', dest: {} }) - ); - }); - - it('calls the reviver with the int element', () => { - const spy = sinon.spy(); - parseToStructure(bsonBytes, undefined, undefined, spy); - expect(spy).to.have.been.calledWith( - sinon.match.any, - sinon.match.any, - sinon.match( - Object.values({ - type: 0x10, // int - nameOffset: 5, - nameLength: 1, - offset: 7, - length: 4 - }) - ) - ); - }); - }); - - context(`when given a bson document`, () => { - const common = { nameOffset: 5, nameLength: 1, offset: 7 }; - const regexp = [ - Buffer.from('abc').toString('hex'), - '00', - Buffer.from('imx').toString('hex'), - '00' - ].join(''); - const code_w_scope = [ - int32LEToHex(13 + 5 + 4), // code is 13, document is 5, 4 for leading int - stringToUTF8HexBytes('() => {}'), - int32LEToHex(5), - '00' - ].join(''); - const tableTest = [ - { - name: 'double', - input: ['01', '6100', '0100000000000000'], - output: { type: 1, length: 8 } - }, - { - name: 'string', - input: ['02', '6100', stringToUTF8HexBytes('hello')], - output: { type: 2, length: 'hello'.length + 4 + 1 } // 4 for the size, 1 for the null - }, - { - name: 'empty object', - input: ['03', '6100', int32LEToHex(5), '00'], - output: { type: 3, length: 5 } - }, - { - name: 'empty array', - input: ['04', '6100', int32LEToHex(5), '00'], - output: { type: 4, length: 5 } - }, - { - name: 'binary', - input: ['05', '6100', int32LEToHex(5), '23', '00'], - output: { type: 5, length: 10 } - }, - { - name: 'undefined', - input: ['06', '6100'], - output: { type: 6, length: 0 } - }, - { - name: 'objectId', - input: ['07', '6100', '00'.repeat(12)], - output: { type: 7, length: 12 } - }, - { - name: 'boolean', - input: ['08', '6100', '45'], - output: { type: 8, length: 1 } - }, - { - name: 'date', - input: ['09', '6100', '00'.repeat(8)], - output: { type: 9, length: 8 } - }, - { - name: 'null', - input: ['0A', '6100'], - output: { type: 10, length: 0 } - }, - { - name: 'regexp', - input: ['0B', '6100', regexp], - output: { type: 11, length: 8 } - }, - { - name: 'dbpointer', - input: ['0C', '6100', stringToUTF8HexBytes('db.coll'), '00'.repeat(12)], - output: { type: 12, length: 'db.coll'.length + 4 + 1 + 12 } - }, - { - name: 'code', - input: ['0D', '6100', stringToUTF8HexBytes('() => {}')], - output: { type: 13, length: '() => {}'.length + 4 + 1 } - }, - { - name: 'symbol', - input: ['0E', '6100', stringToUTF8HexBytes('symbol')], - output: { type: 14, length: 'symbol'.length + 4 + 1 } - }, - { - name: 'empty code_w_scope', - input: ['0F', '6100', code_w_scope], - output: { type: 15, length: '() => {}'.length + 4 + 1 + 5 + 4 } - }, - { - name: 'int', - input: ['10', '6100', int32LEToHex(320)], - output: { type: 16, length: 4 } - }, - { - name: 'timestamp', - input: ['11', '6100', '00'.repeat(8)], - output: { type: 17, length: 8 } - }, - { - name: 'long', - input: ['12', '6100', '00'.repeat(8)], - output: { type: 18, length: 8 } - }, - { - name: 'decimal128', - input: ['13', '6100', '00'.repeat(16)], - output: { type: 19, length: 16 } - }, - { - name: 'minkey', - input: ['FF', '6100'], - output: { type: 255, length: 0 } - }, - { - name: 'maxkey', - input: ['7F', '6100'], - output: { type: 127, length: 0 } - } - ]; - - context('when reviver returns null', () => { - it('does not iterate the embedded documents', () => { - const embedded = bufferFromHexArray([ - '03', // object - '6200', // 'b' - bufferFromHexArray(['01', '6100', '0100000000000000']).toString('hex') - ]); - - const spy = sinon.stub().returns(null); - const res = parseToStructure(embedded, undefined, { kind: 'custom' }, spy); - expect(spy).to.have.been.calledOnceWith( - sinon.match.same(embedded), - sinon.match({ kind: 'custom' }), - sinon.match( - Object.values({ type: 3, nameOffset: 5, nameLength: 1, offset: 7, length: 16 }) - ) - ); - expect(res).to.be.undefined; - }); - }); - - for (const test of tableTest) { - context(`with one ${test.name} element`, () => { - it(`calls reviver with bytes, container, and element with type=${test.output.type} and length=${test.output.length}`, () => { - const bsonBytes = bufferFromHexArray(test.input); - const output = { ...common, ...test.output }; - const spy = sinon.spy(); - parseToStructure(bsonBytes, undefined, undefined, spy); - expect(spy).to.have.been.calledWith( - sinon.match.same(bsonBytes), - sinon.match({ kind: 'object', dest: {} }), - sinon.match([ - output.type, - output.nameOffset, - output.nameLength, - output.offset, - output.length - ]) - ); - }); - }); - } - - for (const test of tableTest) { - context(`with embedded document that contains ${test.name}`, () => { - const embedded = bufferFromHexArray([ - '03', // object - '6200', // 'b' - bufferFromHexArray(test.input).toString('hex') - ]); - - const makeReviverSpy = () => - sinon.stub().callsFake(function myReviver(bytes, container, element) { - const key = ByteUtils.toUTF8( - bytes, - element[e.nameOffset], - element[e.nameOffset] + element[e.nameLength], - true - ); - if (element[0] === 3 && key === 'b') { - // key is 'b' and element is object (top-level) - - container.dest[key] = Object.create(null); - return { - kind: 'object', - dest: container.dest[key] - }; - } - - container.dest[key] = element; - }); - - it(`calls reviver with embedded element`, () => { - const output = Object.values({ - type: test.output.type, - // 4 size bytes + doc type byte + 2 'b\x00' + 4 size bytes + value type byte == 12 - nameOffset: 12, - nameLength: 1, - offset: 14, // 12 + 'a\x00' - length: test.output.length - }); - const spy = makeReviverSpy(); - const res = parseToStructure(embedded, undefined, undefined, spy); - expect(res).to.deep.equal({ b: { a: output } }); - }); - }); - - context(`with embedded array that contains ${test.name}`, () => { - const embedded = bufferFromHexArray([ - '04', // array - '6200', // 'b' - bufferFromHexArray(test.input).toString('hex') - ]); - - const makeReviverSpy = () => - sinon.stub().callsFake(function myReviver(bytes, container, element) { - if (element[0] === 4) { - const key = ByteUtils.toUTF8( - bytes, - element[e.nameOffset], - element[e.nameOffset] + element[e.nameLength], - true - ); - if (key === 'b') { - // key is 'b' and element is array (top-level) - - container.dest[key] = []; - return { - kind: 'array', - dest: container.dest[key] - }; - } - } - - // wow! no key parsing necessary! - container.dest.push(element); - }); - - it(`calls reviver with embedded element`, () => { - const output = Object.values({ - type: test.output.type, - // 4 size bytes + doc type byte + 2 'b\x00' + 4 size bytes + value type byte == 12 - nameOffset: 12, - nameLength: 1, - offset: 14, // 12 + 'a\x00' - length: test.output.length - }); - const spy = makeReviverSpy(); - const res = parseToStructure(embedded, undefined, undefined, spy); - expect(res).to.deep.equal({ b: [output] }); - }); - }); - - context(`with embedded code_w_scope that contains ${test.name}`, () => { - const scope = bufferFromHexArray(test.input); - const embedded = bufferFromHexArray([ - '0F', // code_w_scope - '6200', // 'b' - int32LEToHex(13 + scope.length + 4), // code is 13, document is scope.length, 4 for leading int - stringToUTF8HexBytes('() => {}'), - scope.toString('hex') - ]); - - const makeReviverSpy = () => - sinon.stub().callsFake(function myReviver(bytes, container, element) { - const key = ByteUtils.toUTF8( - bytes, - element[e.nameOffset], - element[e.nameOffset] + element[e.nameLength], - true - ); - if (element[0] === 15 && key === 'b') { - // key is 'b' and element is code_w_scope (top-level) - const offset = element[e.offset]; - const functionStringLength = - bytes[offset + 4] | - (bytes[offset + 5] << 8) | - (bytes[offset + 6] << 16) | - (bytes[offset + 7] << 24); - const start = offset + 4 + 4; - const end = start + functionStringLength - 1; - const codeString = ByteUtils.toUTF8(bytes, start, end, true); - const code = new Code(codeString, Object.create(null)); - container.dest[key] = code; - return { - kind: 'code', - dest: container.dest[key] - }; - } - - // wow! no key parsing necessary! - container.dest.scope[key] = element; - }); - - it(`calls reviver with embedded element`, () => { - const output = Object.values({ - type: test.output.type, - /** - * 29 comes from: - * - 4 bytes for the embedded document - * - 1 type byte - * - etc... todo math - */ - nameOffset: 29, - nameLength: 1, - offset: 31, // 12 + 'a\x00' - length: test.output.length - }); - const spy = makeReviverSpy(); - const res = parseToStructure(embedded, undefined, undefined, spy); - expect(res).to.deep.equal({ b: new Code('() => {}', { a: output }) }); - }); - }); - } - }); - - context('when given a bson document with an array that has 100 items', () => { - it('calls the reviver 101 times, 1 for the document, 100 for the array items', () => { - // Cheating by making an array of 0 length keys - const intValue = ['10', '00', int32LEToHex(1)].join(''); - const bsonBytes = bufferFromHexArray([ - '04', // array - '6100', // 'a' key with key null terminator - bufferFromHexArray([intValue.repeat(100)]).toString('hex') - ]); - - const spy = sinon.stub().returnsArg(1); - parseToStructure(bsonBytes, undefined, undefined, spy); - expect(spy).to.have.callCount(101); - }); - }); -}); diff --git a/test/node/release.test.ts b/test/node/release.test.ts index bfcae1a0..da69230d 100644 --- a/test/node/release.test.ts +++ b/test/node/release.test.ts @@ -42,7 +42,6 @@ const REQUIRED_FILES = [ 'src/parser/utils.ts', 'src/parser/on_demand/index.ts', 'src/parser/on_demand/parse_to_elements.ts', - 'src/parser/on_demand/parse_to_structure.ts', 'src/regexp.ts', 'src/symbol.ts', 'src/timestamp.ts', diff --git a/test/node/utils/number_utils.test.ts b/test/node/utils/number_utils.test.ts index 86d86f88..c826829a 100644 --- a/test/node/utils/number_utils.test.ts +++ b/test/node/utils/number_utils.test.ts @@ -30,6 +30,24 @@ describe('NumberUtils', () => { /** Make a Uint8Array in a less verbose way */ const b = (...values) => new Uint8Array(values); + context('getNonnegativeInt32LE()', () => { + it('parses an int32 little endian', () => { + expect(NumberUtils.getNonnegativeInt32LE(b(0, 0, 0, 1), 0)).to.equal(1 << 24); + }); + + it('throws if int32 is negative', () => { + expect(() => NumberUtils.getNonnegativeInt32LE(b(0, 0, 0, 128), 0)).to.throw(RangeError); + }); + + it('parses an int32 little endian at offset', () => { + expect(NumberUtils.getNonnegativeInt32LE(b(0, 0, 0, 0, 0, 1), 2)).to.equal(1 << 24); + }); + + it('does not check bounds of offset', () => { + expect(NumberUtils.getNonnegativeInt32LE(b(0, 0, 0, 1), 4)).to.equal(0); + }); + }); + context('getInt32LE()', () => { it('parses an int32 little endian', () => { expect(NumberUtils.getInt32LE(b(0, 0, 0, 1), 0)).to.equal(1 << 24);