bson 6.5.0 → 6.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "vendor"
15
15
  ],
16
16
  "types": "bson.d.ts",
17
- "version": "6.5.0",
17
+ "version": "6.7.0",
18
18
  "author": {
19
19
  "name": "The MongoDB NodeJS Team",
20
20
  "email": "dbx-node@mongodb.com"
@@ -27,40 +27,40 @@
27
27
  },
28
28
  "devDependencies": {
29
29
  "@istanbuljs/nyc-config-typescript": "^1.0.2",
30
- "@microsoft/api-extractor": "^7.40.5",
31
- "@octokit/core": "^5.1.0",
30
+ "@microsoft/api-extractor": "^7.43.1",
31
+ "@octokit/core": "^6.1.2",
32
32
  "@rollup/plugin-node-resolve": "^15.2.3",
33
33
  "@rollup/plugin-typescript": "^11.1.6",
34
- "@types/chai": "^4.3.11",
34
+ "@types/chai": "^4.3.14",
35
35
  "@types/mocha": "^10.0.6",
36
- "@types/node": "^20.11.19",
36
+ "@types/node": "^20.12.7",
37
37
  "@types/sinon": "^17.0.3",
38
38
  "@types/sinon-chai": "^3.2.12",
39
- "@typescript-eslint/eslint-plugin": "^7.0.2",
40
- "@typescript-eslint/parser": "^7.0.2",
39
+ "@typescript-eslint/eslint-plugin": "^7.7.0",
40
+ "@typescript-eslint/parser": "^7.7.0",
41
41
  "benchmark": "^2.1.4",
42
- "chai": "^4.3.10",
42
+ "chai": "^4.4.1",
43
43
  "chalk": "^5.3.0",
44
44
  "dbx-js-tools": "github:mongodb-js/dbx-js-tools",
45
- "eslint": "^8.56.0",
45
+ "eslint": "^8.57.0",
46
46
  "eslint-config-prettier": "^9.1.0",
47
47
  "eslint-plugin-no-bigint-usage": "file:etc/eslint/no-bigint-usage",
48
48
  "eslint-plugin-prettier": "^5.1.3",
49
49
  "eslint-plugin-tsdoc": "^0.2.17",
50
- "magic-string": "^0.30.7",
51
- "mocha": "10.3.0",
50
+ "magic-string": "^0.30.10",
51
+ "mocha": "^10.4.0",
52
52
  "node-fetch": "^3.3.2",
53
53
  "nyc": "^15.1.0",
54
54
  "prettier": "^3.2.5",
55
- "rollup": "^4.12.0",
55
+ "rollup": "^4.14.3",
56
56
  "sinon": "^17.0.1",
57
57
  "sinon-chai": "^3.7.0",
58
58
  "source-map-support": "^0.5.21",
59
59
  "standard-version": "^9.5.0",
60
- "tar": "^6.2.0",
60
+ "tar": "^7.0.1",
61
61
  "ts-node": "^10.9.2",
62
- "tsd": "^0.30.5",
63
- "typescript": "^5.0.4",
62
+ "tsd": "^0.31.0",
63
+ "typescript": "5.3",
64
64
  "typescript-cached-transpile": "0.0.6",
65
65
  "uuid": "^9.0.1"
66
66
  },
package/src/binary.ts CHANGED
@@ -186,15 +186,15 @@ export class Binary extends BSONValue {
186
186
  }
187
187
 
188
188
  toJSON(): string {
189
- return ByteUtils.toBase64(this.buffer);
189
+ return ByteUtils.toBase64(this.buffer.subarray(0, this.position));
190
190
  }
191
191
 
192
192
  toString(encoding?: 'hex' | 'base64' | 'utf8' | 'utf-8'): string {
193
- if (encoding === 'hex') return ByteUtils.toHex(this.buffer);
194
- if (encoding === 'base64') return ByteUtils.toBase64(this.buffer);
193
+ if (encoding === 'hex') return ByteUtils.toHex(this.buffer.subarray(0, this.position));
194
+ if (encoding === 'base64') return ByteUtils.toBase64(this.buffer.subarray(0, this.position));
195
195
  if (encoding === 'utf8' || encoding === 'utf-8')
196
- return ByteUtils.toUTF8(this.buffer, 0, this.buffer.byteLength, false);
197
- return ByteUtils.toUTF8(this.buffer, 0, this.buffer.byteLength, false);
196
+ return ByteUtils.toUTF8(this.buffer, 0, this.position, false);
197
+ return ByteUtils.toUTF8(this.buffer, 0, this.position, false);
198
198
  }
199
199
 
200
200
  /** @internal */
package/src/bson.ts CHANGED
@@ -51,10 +51,10 @@ export {
51
51
  Decimal128
52
52
  };
53
53
  export { BSONValue } from './bson_value';
54
- export { BSONError, BSONVersionError, BSONRuntimeError } from './error';
54
+ export { BSONError, BSONVersionError, BSONRuntimeError, BSONOffsetError } from './error';
55
55
  export { BSONType } from './constants';
56
56
  export { EJSON } from './extended_json';
57
- export { onDemand } from './parser/on_demand/index';
57
+ export { onDemand, type OnDemand } from './parser/on_demand/index';
58
58
 
59
59
  /** @public */
60
60
  export interface Document {
package/src/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** @internal */
2
- export const BSON_MAJOR_VERSION = 6 as const;
2
+ export const BSON_MAJOR_VERSION = 6;
3
3
 
4
4
  /** @internal */
5
5
  export const BSON_INT32_MAX = 0x7fffffff;
package/src/double.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { BSONValue } from './bson_value';
2
+ import { BSONError } from './error';
2
3
  import type { EJSONOptions } from './extended_json';
3
4
  import { type InspectFn, defaultInspect } from './parser/utils';
4
5
 
@@ -32,6 +33,41 @@ export class Double extends BSONValue {
32
33
  this.value = +value;
33
34
  }
34
35
 
36
+ /**
37
+ * Attempt to create an double type from string.
38
+ *
39
+ * This method will throw a BSONError on any string input that is not representable as a IEEE-754 64-bit double.
40
+ * Notably, this method will also throw on the following string formats:
41
+ * - Strings in non-decimal and non-exponential formats (binary, hex, or octal digits)
42
+ * - Strings with characters other than numeric, floating point, or leading sign characters (Note: 'Infinity', '-Infinity', and 'NaN' input strings are still allowed)
43
+ * - Strings with leading and/or trailing whitespace
44
+ *
45
+ * Strings with leading zeros, however, are also allowed
46
+ *
47
+ * @param value - the string we want to represent as a double.
48
+ */
49
+ static fromString(value: string): Double {
50
+ const coercedValue = Number(value);
51
+
52
+ if (value === 'NaN') return new Double(NaN);
53
+ if (value === 'Infinity') return new Double(Infinity);
54
+ if (value === '-Infinity') return new Double(-Infinity);
55
+
56
+ if (!Number.isFinite(coercedValue)) {
57
+ throw new BSONError(`Input: ${value} is not representable as a Double`);
58
+ }
59
+ if (value.trim() !== value) {
60
+ throw new BSONError(`Input: '${value}' contains whitespace`);
61
+ }
62
+ if (value === '') {
63
+ throw new BSONError(`Input is an empty string`);
64
+ }
65
+ if (/[^-0-9.+eE]/.test(value)) {
66
+ throw new BSONError(`Input: '${value}' is not in decimal or exponential notation`);
67
+ }
68
+ return new Double(coercedValue);
69
+ }
70
+
35
71
  /**
36
72
  * Access the number value.
37
73
  *
package/src/error.ts CHANGED
@@ -98,8 +98,8 @@ export class BSONOffsetError extends BSONError {
98
98
 
99
99
  public offset: number;
100
100
 
101
- constructor(message: string, offset: number) {
102
- super(`${message}. offset: ${offset}`);
101
+ constructor(message: string, offset: number, options?: { cause?: unknown }) {
102
+ super(`${message}. offset: ${offset}`, options);
103
103
  this.offset = offset;
104
104
  }
105
105
  }
package/src/int_32.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { BSONValue } from './bson_value';
2
+ import { BSON_INT32_MAX, BSON_INT32_MIN } from './constants';
3
+ import { BSONError } from './error';
2
4
  import type { EJSONOptions } from './extended_json';
3
5
  import { type InspectFn, defaultInspect } from './parser/utils';
6
+ import { removeLeadingZerosAndExplicitPlus } from './utils/string_utils';
4
7
 
5
8
  /** @public */
6
9
  export interface Int32Extended {
@@ -32,6 +35,37 @@ export class Int32 extends BSONValue {
32
35
  this.value = +value | 0;
33
36
  }
34
37
 
38
+ /**
39
+ * Attempt to create an Int32 type from string.
40
+ *
41
+ * This method will throw a BSONError on any string input that is not representable as an Int32.
42
+ * Notably, this method will also throw on the following string formats:
43
+ * - Strings in non-decimal formats (exponent notation, binary, hex, or octal digits)
44
+ * - Strings non-numeric and non-leading sign characters (ex: '2.0', '24,000')
45
+ * - Strings with leading and/or trailing whitespace
46
+ *
47
+ * Strings with leading zeros, however, are allowed.
48
+ *
49
+ * @param value - the string we want to represent as an int32.
50
+ */
51
+ static fromString(value: string): Int32 {
52
+ const cleanedValue = removeLeadingZerosAndExplicitPlus(value);
53
+
54
+ const coercedValue = Number(value);
55
+
56
+ if (BSON_INT32_MAX < coercedValue) {
57
+ throw new BSONError(`Input: '${value}' is larger than the maximum value for Int32`);
58
+ } else if (BSON_INT32_MIN > coercedValue) {
59
+ throw new BSONError(`Input: '${value}' is smaller than the minimum value for Int32`);
60
+ } else if (!Number.isSafeInteger(coercedValue)) {
61
+ throw new BSONError(`Input: '${value}' is not a safe integer`);
62
+ } else if (coercedValue.toString() !== cleanedValue) {
63
+ // catch all case
64
+ throw new BSONError(`Input: '${value}' is not a valid Int32 string`);
65
+ }
66
+ return new Int32(coercedValue);
67
+ }
68
+
35
69
  /**
36
70
  * Access the number value.
37
71
  *
package/src/long.ts CHANGED
@@ -3,6 +3,7 @@ import { BSONError } from './error';
3
3
  import type { EJSONOptions } from './extended_json';
4
4
  import { type InspectFn, defaultInspect } from './parser/utils';
5
5
  import type { Timestamp } from './timestamp';
6
+ import * as StringUtils from './utils/string_utils';
6
7
 
7
8
  interface LongWASMHelpers {
8
9
  /** Gets the high bits of the last operation performed */
@@ -246,29 +247,24 @@ export class Long extends BSONValue {
246
247
  }
247
248
 
248
249
  /**
250
+ * @internal
249
251
  * Returns a Long representation of the given string, written using the specified radix.
252
+ * Throws an error if `throwsError` is set to true and any of the following conditions are true:
253
+ * - the string contains invalid characters for the given radix
254
+ * - the string contains whitespace
250
255
  * @param str - The textual representation of the Long
251
256
  * @param unsigned - Whether unsigned or not, defaults to signed
252
257
  * @param radix - The radix in which the text is written (2-36), defaults to 10
253
258
  * @returns The corresponding Long value
254
259
  */
255
- static fromString(str: string, unsigned?: boolean, radix?: number): Long {
260
+ private static _fromString(str: string, unsigned: boolean, radix: number): Long {
256
261
  if (str.length === 0) throw new BSONError('empty string');
257
- if (str === 'NaN' || str === 'Infinity' || str === '+Infinity' || str === '-Infinity')
258
- return Long.ZERO;
259
- if (typeof unsigned === 'number') {
260
- // For goog.math.long compatibility
261
- (radix = unsigned), (unsigned = false);
262
- } else {
263
- unsigned = !!unsigned;
264
- }
265
- radix = radix || 10;
266
262
  if (radix < 2 || 36 < radix) throw new BSONError('radix');
267
263
 
268
264
  let p;
269
265
  if ((p = str.indexOf('-')) > 0) throw new BSONError('interior hyphen');
270
266
  else if (p === 0) {
271
- return Long.fromString(str.substring(1), unsigned, radix).neg();
267
+ return Long._fromString(str.substring(1), unsigned, radix).neg();
272
268
  }
273
269
 
274
270
  // Do several (8) digits each time through the loop, so as to
@@ -291,6 +287,167 @@ export class Long extends BSONValue {
291
287
  return result;
292
288
  }
293
289
 
290
+ /**
291
+ * Returns a signed Long representation of the given string, written using radix 10.
292
+ * Will throw an error if the given text is not exactly representable as a Long.
293
+ * Throws an error if any of the following conditions are true:
294
+ * - the string contains invalid characters for the radix 10
295
+ * - the string contains whitespace
296
+ * - the value the string represents is too large or too small to be a Long
297
+ * Unlike Long.fromString, this method does not coerce '+/-Infinity' and 'NaN' to Long.Zero
298
+ * @param str - The textual representation of the Long
299
+ * @returns The corresponding Long value
300
+ */
301
+ static fromStringStrict(str: string): Long;
302
+ /**
303
+ * Returns a Long representation of the given string, written using the radix 10.
304
+ * Will throw an error if the given parameters are not exactly representable as a Long.
305
+ * Throws an error if any of the following conditions are true:
306
+ * - the string contains invalid characters for the given radix
307
+ * - the string contains whitespace
308
+ * - the value the string represents is too large or too small to be a Long
309
+ * Unlike Long.fromString, this method does not coerce '+/-Infinity' and 'NaN' to Long.Zero
310
+ * @param str - The textual representation of the Long
311
+ * @param unsigned - Whether unsigned or not, defaults to signed
312
+ * @returns The corresponding Long value
313
+ */
314
+ static fromStringStrict(str: string, unsigned?: boolean): Long;
315
+ /**
316
+ * Returns a signed Long representation of the given string, written using the specified radix.
317
+ * Will throw an error if the given parameters are not exactly representable as a Long.
318
+ * Throws an error if any of the following conditions are true:
319
+ * - the string contains invalid characters for the given radix
320
+ * - the string contains whitespace
321
+ * - the value the string represents is too large or too small to be a Long
322
+ * Unlike Long.fromString, this method does not coerce '+/-Infinity' and 'NaN' to Long.Zero
323
+ * @param str - The textual representation of the Long
324
+ * @param radix - The radix in which the text is written (2-36), defaults to 10
325
+ * @returns The corresponding Long value
326
+ */
327
+ static fromStringStrict(str: string, radix?: boolean): Long;
328
+ /**
329
+ * Returns a Long representation of the given string, written using the specified radix.
330
+ * Will throw an error if the given parameters are not exactly representable as a Long.
331
+ * Throws an error if any of the following conditions are true:
332
+ * - the string contains invalid characters for the given radix
333
+ * - the string contains whitespace
334
+ * - the value the string represents is too large or too small to be a Long
335
+ * Unlike Long.fromString, this method does not coerce '+/-Infinity' and 'NaN' to Long.Zero
336
+ * @param str - The textual representation of the Long
337
+ * @param unsigned - Whether unsigned or not, defaults to signed
338
+ * @param radix - The radix in which the text is written (2-36), defaults to 10
339
+ * @returns The corresponding Long value
340
+ */
341
+ static fromStringStrict(str: string, unsigned?: boolean, radix?: number): Long;
342
+ static fromStringStrict(str: string, unsignedOrRadix?: boolean | number, radix?: number): Long {
343
+ let unsigned = false;
344
+ if (typeof unsignedOrRadix === 'number') {
345
+ // For goog.math.long compatibility
346
+ (radix = unsignedOrRadix), (unsignedOrRadix = false);
347
+ } else {
348
+ unsigned = !!unsignedOrRadix;
349
+ }
350
+ radix ??= 10;
351
+
352
+ if (str.trim() !== str) {
353
+ throw new BSONError(`Input: '${str}' contains leading and/or trailing whitespace`);
354
+ }
355
+ if (!StringUtils.validateStringCharacters(str, radix)) {
356
+ throw new BSONError(`Input: '${str}' contains invalid characters for radix: ${radix}`);
357
+ }
358
+
359
+ // remove leading zeros (for later string comparison and to make math faster)
360
+ const cleanedStr = StringUtils.removeLeadingZerosAndExplicitPlus(str);
361
+
362
+ // check roundtrip result
363
+ const result = Long._fromString(cleanedStr, unsigned, radix);
364
+ if (result.toString(radix).toLowerCase() !== cleanedStr.toLowerCase()) {
365
+ throw new BSONError(
366
+ `Input: ${str} is not representable as ${result.unsigned ? 'an unsigned' : 'a signed'} 64-bit Long ${radix != null ? `with radix: ${radix}` : ''}`
367
+ );
368
+ }
369
+ return result;
370
+ }
371
+
372
+ /**
373
+ * Returns a signed Long representation of the given string, written using radix 10.
374
+ *
375
+ * If the input string is empty, this function will throw a BSONError.
376
+ *
377
+ * If input string does not have valid signed 64-bit Long representation, this method will return a coerced value:
378
+ * - inputs that overflow 64-bit signed long will be coerced to Long.MAX_VALUE and Long.MIN_VALUE respectively
379
+ * - 'NaN' or '+/-Infinity' are coerced to Long.ZERO
380
+ * - other invalid characters sequences have variable behavior
381
+ *
382
+ * @param str - The textual representation of the Long
383
+ * @returns The corresponding Long value
384
+ */
385
+ static fromString(str: string): Long;
386
+ /**
387
+ * Returns a signed Long representation of the given string, written using the provided radix.
388
+ *
389
+ * If the input string is empty or a provided radix is not within (2-36), this function will throw a BSONError.
390
+ *
391
+ * If input parameters do not have valid signed 64-bit Long representation, this method will return a coerced value:
392
+ * - inputs that overflow 64-bit signed long will be coerced to Long.MAX_VALUE and Long.MIN_VALUE respectively
393
+ * - if the radix is less than 24, 'NaN' is coerced to Long.ZERO
394
+ * - if the radix is less than 35, '+/-Infinity' inputs are coerced to Long.ZERO
395
+ * - other invalid characters sequences have variable behavior
396
+ * @param str - The textual representation of the Long
397
+ * @param radix - The radix in which the text is written (2-36), defaults to 10
398
+ * @returns The corresponding Long value
399
+ */
400
+ static fromString(str: string, radix?: number): Long;
401
+ /**
402
+ * Returns a Long representation of the given string, written using radix 10.
403
+ *
404
+ * If the input string is empty, this function will throw a BSONError.
405
+ *
406
+ * If input parameters do not have a valid 64-bit Long representation, this method will return a coerced value:
407
+ * - inputs that overflow 64-bit long will be coerced to max or min (if signed) values
408
+ * - if the radix is less than 24, 'NaN' is coerced to Long.ZERO
409
+ * - if the radix is less than 35, '+/-Infinity' inputs are coerced to Long.ZERO
410
+ * - other invalid characters sequences have variable behavior
411
+ * @param str - The textual representation of the Long
412
+ * @param unsigned - Whether unsigned or not, defaults to signed
413
+ * @returns The corresponding Long value
414
+ */
415
+ static fromString(str: string, unsigned?: boolean): Long;
416
+ /**
417
+ * Returns a Long representation of the given string, written using the specified radix.
418
+ *
419
+ * If the input string is empty or a provided radix is not within (2-36), this function will throw a BSONError.
420
+ *
421
+ * If input parameters do not have a valid 64-bit Long representation, this method will return a coerced value:
422
+ * - inputs that overflow 64-bit long will be coerced to max or min (if signed) values
423
+ * - if the radix is less than 24, 'NaN' is coerced to Long.ZERO
424
+ * - if the radix is less than 35, '+/-Infinity' inputs are coerced to Long.ZERO
425
+ * - other invalid characters sequences have variable behavior
426
+ * @param str - The textual representation of the Long
427
+ * @param unsigned - Whether unsigned or not, defaults to signed
428
+ * @param radix - The radix in which the text is written (2-36), defaults to 10
429
+ * @returns The corresponding Long value
430
+ */
431
+ static fromString(str: string, unsigned?: boolean, radix?: number): Long;
432
+ static fromString(str: string, unsignedOrRadix?: boolean | number, radix?: number): Long {
433
+ let unsigned = false;
434
+ if (typeof unsignedOrRadix === 'number') {
435
+ // For goog.math.long compatibility
436
+ (radix = unsignedOrRadix), (unsignedOrRadix = false);
437
+ } else {
438
+ unsigned = !!unsignedOrRadix;
439
+ }
440
+ radix ??= 10;
441
+ if (str === 'NaN' && radix < 24) {
442
+ // radix does not support n, so coerce to zero
443
+ return Long.ZERO;
444
+ } else if ((str === 'Infinity' || str === '+Infinity' || str === '-Infinity') && radix < 35) {
445
+ // radix does not support y, so coerce to zero
446
+ return Long.ZERO;
447
+ }
448
+ return Long._fromString(str, unsigned, radix);
449
+ }
450
+
294
451
  /**
295
452
  * Creates a Long from its byte representation.
296
453
  * @param bytes - Byte representation
@@ -0,0 +1,35 @@
1
+ import { BSONError } from './error';
2
+
3
+ type TextDecoder = {
4
+ readonly encoding: string;
5
+ readonly fatal: boolean;
6
+ readonly ignoreBOM: boolean;
7
+ decode(input?: Uint8Array): string;
8
+ };
9
+ type TextDecoderConstructor = {
10
+ new (label: 'utf8', options: { fatal: boolean; ignoreBOM?: boolean }): TextDecoder;
11
+ };
12
+
13
+ // parse utf8 globals
14
+ declare const TextDecoder: TextDecoderConstructor;
15
+ let TextDecoderFatal: TextDecoder;
16
+ let TextDecoderNonFatal: TextDecoder;
17
+
18
+ /**
19
+ * Determines if the passed in bytes are valid utf8
20
+ * @param bytes - An array of 8-bit bytes. Must be indexable and have length property
21
+ * @param start - The index to start validating
22
+ * @param end - The index to end validating
23
+ */
24
+ export function parseUtf8(buffer: Uint8Array, start: number, end: number, fatal: boolean): string {
25
+ if (fatal) {
26
+ TextDecoderFatal ??= new TextDecoder('utf8', { fatal: true });
27
+ try {
28
+ return TextDecoderFatal.decode(buffer.subarray(start, end));
29
+ } catch (cause) {
30
+ throw new BSONError('Invalid UTF-8 string in BSON document', { cause });
31
+ }
32
+ }
33
+ TextDecoderNonFatal ??= new TextDecoder('utf8', { fatal: false });
34
+ return TextDecoderNonFatal.decode(buffer.subarray(start, end));
35
+ }
@@ -16,7 +16,6 @@ import { BSONSymbol } from '../symbol';
16
16
  import { Timestamp } from '../timestamp';
17
17
  import { ByteUtils } from '../utils/byte_utils';
18
18
  import { NumberUtils } from '../utils/number_utils';
19
- import { validateUtf8 } from '../validate_utf8';
20
19
 
21
20
  /** @public */
22
21
  export interface DeserializeOptions {
@@ -604,12 +603,7 @@ function deserializeObject(
604
603
  )
605
604
  throw new BSONError('bad string length in bson');
606
605
  // Namespace
607
- if (validation != null && validation.utf8) {
608
- if (!validateUtf8(buffer, index, index + stringSize - 1)) {
609
- throw new BSONError('Invalid UTF-8 string in BSON document');
610
- }
611
- }
612
- const namespace = ByteUtils.toUTF8(buffer, index, index + stringSize - 1, false);
606
+ const namespace = ByteUtils.toUTF8(buffer, index, index + stringSize - 1, shouldValidateKey);
613
607
  // Update parse index position
614
608
  index = index + stringSize;
615
609
 
@@ -1,4 +1,5 @@
1
- import { type BSONError, BSONOffsetError } from '../../error';
1
+ import { ByteUtils } from '../../utils/byte_utils';
2
+ import { NumberUtils } from '../../utils/number_utils';
2
3
  import { type BSONElement, parseToElements } from './parse_to_elements';
3
4
  /**
4
5
  * @experimental
@@ -7,11 +8,13 @@ import { type BSONElement, parseToElements } from './parse_to_elements';
7
8
  * A new set of BSON APIs that are currently experimental and not intended for production use.
8
9
  */
9
10
  export type OnDemand = {
10
- BSONOffsetError: {
11
- new (message: string, offset: number): BSONOffsetError;
12
- isBSONError(value: unknown): value is BSONError;
13
- };
14
11
  parseToElements: (this: void, bytes: Uint8Array, startOffset?: number) => Iterable<BSONElement>;
12
+ // Types
13
+ BSONElement: BSONElement;
14
+
15
+ // Utils
16
+ ByteUtils: ByteUtils;
17
+ NumberUtils: NumberUtils;
15
18
  };
16
19
 
17
20
  /**
@@ -21,7 +24,8 @@ export type OnDemand = {
21
24
  const onDemand: OnDemand = Object.create(null);
22
25
 
23
26
  onDemand.parseToElements = parseToElements;
24
- onDemand.BSONOffsetError = BSONOffsetError;
27
+ onDemand.ByteUtils = ByteUtils;
28
+ onDemand.NumberUtils = NumberUtils;
25
29
 
26
30
  Object.freeze(onDemand);
27
31
 
@@ -1,5 +1,5 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
2
1
  import { BSONOffsetError } from '../../error';
2
+ import { NumberUtils } from '../../utils/number_utils';
3
3
 
4
4
  /**
5
5
  * @internal
@@ -9,7 +9,7 @@ import { BSONOffsetError } from '../../error';
9
9
  * - `minKey` is set to 255 so unsigned comparisons succeed
10
10
  * - Modify with caution, double check the bundle contains literals
11
11
  */
12
- const enum t {
12
+ const enum BSONElementType {
13
13
  double = 1,
14
14
  string = 2,
15
15
  object = 3,
@@ -45,17 +45,12 @@ export type BSONElement = [
45
45
  length: number
46
46
  ];
47
47
 
48
- /** Parses a int32 little-endian at offset, throws if it is negative */
49
- function getSize(source: Uint8Array, offset: number): number {
50
- if (source[offset + 3] > 127) {
51
- throw new BSONOffsetError('BSON size cannot be negative', offset);
48
+ function getSize(source: Uint8Array, offset: number) {
49
+ try {
50
+ return NumberUtils.getNonnegativeInt32LE(source, offset);
51
+ } catch (cause) {
52
+ throw new BSONOffsetError('BSON size cannot be negative', offset, { cause });
52
53
  }
53
- return (
54
- source[offset] |
55
- (source[offset + 1] << 8) |
56
- (source[offset + 2] << 16) |
57
- (source[offset + 3] << 24)
58
- );
59
54
  }
60
55
 
61
56
  /**
@@ -80,7 +75,12 @@ function findNull(bytes: Uint8Array, offset: number): number {
80
75
  * @public
81
76
  * @experimental
82
77
  */
83
- export function parseToElements(bytes: Uint8Array, startOffset = 0): Iterable<BSONElement> {
78
+ export function parseToElements(
79
+ bytes: Uint8Array,
80
+ startOffset: number | null = 0
81
+ ): Iterable<BSONElement> {
82
+ startOffset ??= 0;
83
+
84
84
  if (bytes.length < 5) {
85
85
  throw new BSONOffsetError(
86
86
  `Input must be at least 5 bytes, got ${bytes.length} bytes`,
@@ -121,37 +121,51 @@ export function parseToElements(bytes: Uint8Array, startOffset = 0): Iterable<BS
121
121
 
122
122
  let length: number;
123
123
 
124
- if (type === t.double || type === t.long || type === t.date || type === t.timestamp) {
124
+ if (
125
+ type === BSONElementType.double ||
126
+ type === BSONElementType.long ||
127
+ type === BSONElementType.date ||
128
+ type === BSONElementType.timestamp
129
+ ) {
125
130
  length = 8;
126
- } else if (type === t.int) {
131
+ } else if (type === BSONElementType.int) {
127
132
  length = 4;
128
- } else if (type === t.objectId) {
133
+ } else if (type === BSONElementType.objectId) {
129
134
  length = 12;
130
- } else if (type === t.decimal) {
135
+ } else if (type === BSONElementType.decimal) {
131
136
  length = 16;
132
- } else if (type === t.bool) {
137
+ } else if (type === BSONElementType.bool) {
133
138
  length = 1;
134
- } else if (type === t.null || type === t.undefined || type === t.maxKey || type === t.minKey) {
139
+ } else if (
140
+ type === BSONElementType.null ||
141
+ type === BSONElementType.undefined ||
142
+ type === BSONElementType.maxKey ||
143
+ type === BSONElementType.minKey
144
+ ) {
135
145
  length = 0;
136
146
  }
137
147
  // Needs a size calculation
138
- else if (type === t.regex) {
148
+ else if (type === BSONElementType.regex) {
139
149
  length = findNull(bytes, findNull(bytes, offset) + 1) + 1 - offset;
140
- } else if (type === t.object || type === t.array || type === t.javascriptWithScope) {
150
+ } else if (
151
+ type === BSONElementType.object ||
152
+ type === BSONElementType.array ||
153
+ type === BSONElementType.javascriptWithScope
154
+ ) {
141
155
  length = getSize(bytes, offset);
142
156
  } else if (
143
- type === t.string ||
144
- type === t.binData ||
145
- type === t.dbPointer ||
146
- type === t.javascript ||
147
- type === t.symbol
157
+ type === BSONElementType.string ||
158
+ type === BSONElementType.binData ||
159
+ type === BSONElementType.dbPointer ||
160
+ type === BSONElementType.javascript ||
161
+ type === BSONElementType.symbol
148
162
  ) {
149
163
  length = getSize(bytes, offset) + 4;
150
- if (type === t.binData) {
164
+ if (type === BSONElementType.binData) {
151
165
  // binary subtype
152
166
  length += 1;
153
167
  }
154
- if (type === t.dbPointer) {
168
+ if (type === BSONElementType.dbPointer) {
155
169
  // dbPointer's objectId
156
170
  length += 12;
157
171
  }