bson 6.6.0 → 6.8.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.6.0",
17
+ "version": "6.8.0",
18
18
  "author": {
19
19
  "name": "The MongoDB NodeJS Team",
20
20
  "email": "dbx-node@mongodb.com"
@@ -27,40 +27,39 @@
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",
32
31
  "@rollup/plugin-node-resolve": "^15.2.3",
33
32
  "@rollup/plugin-typescript": "^11.1.6",
34
- "@types/chai": "^4.3.11",
33
+ "@types/chai": "^4.3.14",
35
34
  "@types/mocha": "^10.0.6",
36
- "@types/node": "^20.11.19",
35
+ "@types/node": "^20.12.7",
37
36
  "@types/sinon": "^17.0.3",
38
37
  "@types/sinon-chai": "^3.2.12",
39
- "@typescript-eslint/eslint-plugin": "^7.0.2",
40
- "@typescript-eslint/parser": "^7.0.2",
38
+ "@typescript-eslint/eslint-plugin": "^7.7.0",
39
+ "@typescript-eslint/parser": "^7.7.0",
41
40
  "benchmark": "^2.1.4",
42
- "chai": "^4.3.10",
41
+ "chai": "^4.4.1",
43
42
  "chalk": "^5.3.0",
44
43
  "dbx-js-tools": "github:mongodb-js/dbx-js-tools",
45
- "eslint": "^8.56.0",
44
+ "eslint": "^8.57.0",
46
45
  "eslint-config-prettier": "^9.1.0",
47
46
  "eslint-plugin-no-bigint-usage": "file:etc/eslint/no-bigint-usage",
48
47
  "eslint-plugin-prettier": "^5.1.3",
49
48
  "eslint-plugin-tsdoc": "^0.2.17",
50
- "magic-string": "^0.30.7",
51
- "mocha": "10.3.0",
49
+ "magic-string": "^0.30.10",
50
+ "mocha": "^10.4.0",
52
51
  "node-fetch": "^3.3.2",
53
52
  "nyc": "^15.1.0",
54
53
  "prettier": "^3.2.5",
55
- "rollup": "^4.12.0",
54
+ "rollup": "^4.14.3",
56
55
  "sinon": "^17.0.1",
57
56
  "sinon-chai": "^3.7.0",
58
57
  "source-map-support": "^0.5.21",
59
58
  "standard-version": "^9.5.0",
60
- "tar": "^6.2.0",
59
+ "tar": "^7.0.1",
61
60
  "ts-node": "^10.9.2",
62
- "tsd": "^0.30.5",
63
- "typescript": "^5.0.4",
61
+ "tsd": "^0.31.0",
62
+ "typescript": "5.3",
64
63
  "typescript-cached-transpile": "0.0.6",
65
64
  "uuid": "^9.0.1"
66
65
  },
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/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 */
@@ -118,42 +119,57 @@ export class Long extends BSONValue {
118
119
  /**
119
120
  * The high 32 bits as a signed value.
120
121
  */
121
- high!: number;
122
+ high: number;
122
123
 
123
124
  /**
124
125
  * The low 32 bits as a signed value.
125
126
  */
126
- low!: number;
127
+ low: number;
127
128
 
128
129
  /**
129
130
  * Whether unsigned or not.
130
131
  */
131
- unsigned!: boolean;
132
+ unsigned: boolean;
132
133
 
133
134
  /**
134
135
  * Constructs a 64 bit two's-complement integer, given its low and high 32 bit values as *signed* integers.
135
- * See the from* functions below for more convenient ways of constructing Longs.
136
- *
137
- * Acceptable signatures are:
138
- * - Long(low, high, unsigned?)
139
- * - Long(bigint, unsigned?)
140
- * - Long(string, unsigned?)
141
136
  *
142
137
  * @param low - The low (signed) 32 bits of the long
143
138
  * @param high - The high (signed) 32 bits of the long
144
139
  * @param unsigned - Whether unsigned or not, defaults to signed
145
140
  */
146
- constructor(low: number | bigint | string = 0, high?: number | boolean, unsigned?: boolean) {
141
+ constructor(low: number, high?: number, unsigned?: boolean);
142
+ /**
143
+ * Constructs a 64 bit two's-complement integer, given a bigint representation.
144
+ *
145
+ * @param value - BigInt representation of the long value
146
+ * @param unsigned - Whether unsigned or not, defaults to signed
147
+ */
148
+ constructor(value: bigint, unsigned?: boolean);
149
+ /**
150
+ * Constructs a 64 bit two's-complement integer, given a string representation.
151
+ *
152
+ * @param value - String representation of the long value
153
+ * @param unsigned - Whether unsigned or not, defaults to signed
154
+ */
155
+ constructor(value: string, unsigned?: boolean);
156
+ constructor(
157
+ lowOrValue: number | bigint | string = 0,
158
+ highOrUnsigned?: number | boolean,
159
+ unsigned?: boolean
160
+ ) {
147
161
  super();
148
- if (typeof low === 'bigint') {
149
- Object.assign(this, Long.fromBigInt(low, !!high));
150
- } else if (typeof low === 'string') {
151
- Object.assign(this, Long.fromString(low, !!high));
152
- } else {
153
- this.low = low | 0;
154
- this.high = (high as number) | 0;
155
- this.unsigned = !!unsigned;
156
- }
162
+ const unsignedBool = typeof highOrUnsigned === 'boolean' ? highOrUnsigned : Boolean(unsigned);
163
+ const high = typeof highOrUnsigned === 'number' ? highOrUnsigned : 0;
164
+ const res =
165
+ typeof lowOrValue === 'string'
166
+ ? Long.fromString(lowOrValue, unsignedBool)
167
+ : typeof lowOrValue === 'bigint'
168
+ ? Long.fromBigInt(lowOrValue, unsignedBool)
169
+ : { low: lowOrValue | 0, high: high | 0, unsigned: unsignedBool };
170
+ this.low = res.low;
171
+ this.high = res.high;
172
+ this.unsigned = res.unsigned;
157
173
  }
158
174
 
159
175
  static TWO_PWR_24 = Long.fromInt(TWO_PWR_24_DBL);
@@ -242,33 +258,36 @@ export class Long extends BSONValue {
242
258
  * @returns The corresponding Long value
243
259
  */
244
260
  static fromBigInt(value: bigint, unsigned?: boolean): Long {
245
- return Long.fromString(value.toString(), unsigned);
261
+ // eslint-disable-next-line no-restricted-globals
262
+ const FROM_BIGINT_BIT_MASK = BigInt(0xffffffff);
263
+ // eslint-disable-next-line no-restricted-globals
264
+ const FROM_BIGINT_BIT_SHIFT = BigInt(32);
265
+ return new Long(
266
+ Number(value & FROM_BIGINT_BIT_MASK),
267
+ Number((value >> FROM_BIGINT_BIT_SHIFT) & FROM_BIGINT_BIT_MASK),
268
+ unsigned
269
+ );
246
270
  }
247
271
 
248
272
  /**
273
+ * @internal
249
274
  * Returns a Long representation of the given string, written using the specified radix.
275
+ * Throws an error if `throwsError` is set to true and any of the following conditions are true:
276
+ * - the string contains invalid characters for the given radix
277
+ * - the string contains whitespace
250
278
  * @param str - The textual representation of the Long
251
279
  * @param unsigned - Whether unsigned or not, defaults to signed
252
280
  * @param radix - The radix in which the text is written (2-36), defaults to 10
253
281
  * @returns The corresponding Long value
254
282
  */
255
- static fromString(str: string, unsigned?: boolean, radix?: number): Long {
283
+ private static _fromString(str: string, unsigned: boolean, radix: number): Long {
256
284
  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
285
  if (radix < 2 || 36 < radix) throw new BSONError('radix');
267
286
 
268
287
  let p;
269
288
  if ((p = str.indexOf('-')) > 0) throw new BSONError('interior hyphen');
270
289
  else if (p === 0) {
271
- return Long.fromString(str.substring(1), unsigned, radix).neg();
290
+ return Long._fromString(str.substring(1), unsigned, radix).neg();
272
291
  }
273
292
 
274
293
  // Do several (8) digits each time through the loop, so as to
@@ -291,6 +310,167 @@ export class Long extends BSONValue {
291
310
  return result;
292
311
  }
293
312
 
313
+ /**
314
+ * Returns a signed Long representation of the given string, written using radix 10.
315
+ * Will throw an error if the given text is not exactly representable as a Long.
316
+ * Throws an error if any of the following conditions are true:
317
+ * - the string contains invalid characters for the radix 10
318
+ * - the string contains whitespace
319
+ * - the value the string represents is too large or too small to be a Long
320
+ * Unlike Long.fromString, this method does not coerce '+/-Infinity' and 'NaN' to Long.Zero
321
+ * @param str - The textual representation of the Long
322
+ * @returns The corresponding Long value
323
+ */
324
+ static fromStringStrict(str: string): Long;
325
+ /**
326
+ * Returns a Long representation of the given string, written using the radix 10.
327
+ * Will throw an error if the given parameters are not exactly representable as a Long.
328
+ * Throws an error if any of the following conditions are true:
329
+ * - the string contains invalid characters for the given radix
330
+ * - the string contains whitespace
331
+ * - the value the string represents is too large or too small to be a Long
332
+ * Unlike Long.fromString, this method does not coerce '+/-Infinity' and 'NaN' to Long.Zero
333
+ * @param str - The textual representation of the Long
334
+ * @param unsigned - Whether unsigned or not, defaults to signed
335
+ * @returns The corresponding Long value
336
+ */
337
+ static fromStringStrict(str: string, unsigned?: boolean): Long;
338
+ /**
339
+ * Returns a signed Long representation of the given string, written using the specified radix.
340
+ * Will throw an error if the given parameters are not exactly representable as a Long.
341
+ * Throws an error if any of the following conditions are true:
342
+ * - the string contains invalid characters for the given radix
343
+ * - the string contains whitespace
344
+ * - the value the string represents is too large or too small to be a Long
345
+ * Unlike Long.fromString, this method does not coerce '+/-Infinity' and 'NaN' to Long.Zero
346
+ * @param str - The textual representation of the Long
347
+ * @param radix - The radix in which the text is written (2-36), defaults to 10
348
+ * @returns The corresponding Long value
349
+ */
350
+ static fromStringStrict(str: string, radix?: boolean): Long;
351
+ /**
352
+ * Returns a Long representation of the given string, written using the specified radix.
353
+ * Will throw an error if the given parameters are not exactly representable as a Long.
354
+ * Throws an error if any of the following conditions are true:
355
+ * - the string contains invalid characters for the given radix
356
+ * - the string contains whitespace
357
+ * - the value the string represents is too large or too small to be a Long
358
+ * Unlike Long.fromString, this method does not coerce '+/-Infinity' and 'NaN' to Long.Zero
359
+ * @param str - The textual representation of the Long
360
+ * @param unsigned - Whether unsigned or not, defaults to signed
361
+ * @param radix - The radix in which the text is written (2-36), defaults to 10
362
+ * @returns The corresponding Long value
363
+ */
364
+ static fromStringStrict(str: string, unsigned?: boolean, radix?: number): Long;
365
+ static fromStringStrict(str: string, unsignedOrRadix?: boolean | number, radix?: number): Long {
366
+ let unsigned = false;
367
+ if (typeof unsignedOrRadix === 'number') {
368
+ // For goog.math.long compatibility
369
+ (radix = unsignedOrRadix), (unsignedOrRadix = false);
370
+ } else {
371
+ unsigned = !!unsignedOrRadix;
372
+ }
373
+ radix ??= 10;
374
+
375
+ if (str.trim() !== str) {
376
+ throw new BSONError(`Input: '${str}' contains leading and/or trailing whitespace`);
377
+ }
378
+ if (!StringUtils.validateStringCharacters(str, radix)) {
379
+ throw new BSONError(`Input: '${str}' contains invalid characters for radix: ${radix}`);
380
+ }
381
+
382
+ // remove leading zeros (for later string comparison and to make math faster)
383
+ const cleanedStr = StringUtils.removeLeadingZerosAndExplicitPlus(str);
384
+
385
+ // check roundtrip result
386
+ const result = Long._fromString(cleanedStr, unsigned, radix);
387
+ if (result.toString(radix).toLowerCase() !== cleanedStr.toLowerCase()) {
388
+ throw new BSONError(
389
+ `Input: ${str} is not representable as ${result.unsigned ? 'an unsigned' : 'a signed'} 64-bit Long ${radix != null ? `with radix: ${radix}` : ''}`
390
+ );
391
+ }
392
+ return result;
393
+ }
394
+
395
+ /**
396
+ * Returns a signed Long representation of the given string, written using radix 10.
397
+ *
398
+ * If the input string is empty, this function will throw a BSONError.
399
+ *
400
+ * If input string does not have valid signed 64-bit Long representation, this method will return a coerced value:
401
+ * - inputs that overflow 64-bit signed long will be coerced to Long.MAX_VALUE and Long.MIN_VALUE respectively
402
+ * - 'NaN' or '+/-Infinity' are coerced to Long.ZERO
403
+ * - other invalid characters sequences have variable behavior
404
+ *
405
+ * @param str - The textual representation of the Long
406
+ * @returns The corresponding Long value
407
+ */
408
+ static fromString(str: string): Long;
409
+ /**
410
+ * Returns a signed Long representation of the given string, written using the provided radix.
411
+ *
412
+ * If the input string is empty or a provided radix is not within (2-36), this function will throw a BSONError.
413
+ *
414
+ * If input parameters do not have valid signed 64-bit Long representation, this method will return a coerced value:
415
+ * - inputs that overflow 64-bit signed long will be coerced to Long.MAX_VALUE and Long.MIN_VALUE respectively
416
+ * - if the radix is less than 24, 'NaN' is coerced to Long.ZERO
417
+ * - if the radix is less than 35, '+/-Infinity' inputs are coerced to Long.ZERO
418
+ * - other invalid characters sequences have variable behavior
419
+ * @param str - The textual representation of the Long
420
+ * @param radix - The radix in which the text is written (2-36), defaults to 10
421
+ * @returns The corresponding Long value
422
+ */
423
+ static fromString(str: string, radix?: number): Long;
424
+ /**
425
+ * Returns a Long representation of the given string, written using radix 10.
426
+ *
427
+ * If the input string is empty, this function will throw a BSONError.
428
+ *
429
+ * If input parameters do not have a valid 64-bit Long representation, this method will return a coerced value:
430
+ * - inputs that overflow 64-bit long will be coerced to max or min (if signed) values
431
+ * - if the radix is less than 24, 'NaN' is coerced to Long.ZERO
432
+ * - if the radix is less than 35, '+/-Infinity' inputs are coerced to Long.ZERO
433
+ * - other invalid characters sequences have variable behavior
434
+ * @param str - The textual representation of the Long
435
+ * @param unsigned - Whether unsigned or not, defaults to signed
436
+ * @returns The corresponding Long value
437
+ */
438
+ static fromString(str: string, unsigned?: boolean): Long;
439
+ /**
440
+ * Returns a Long representation of the given string, written using the specified radix.
441
+ *
442
+ * If the input string is empty or a provided radix is not within (2-36), this function will throw a BSONError.
443
+ *
444
+ * If input parameters do not have a valid 64-bit Long representation, this method will return a coerced value:
445
+ * - inputs that overflow 64-bit long will be coerced to max or min (if signed) values
446
+ * - if the radix is less than 24, 'NaN' is coerced to Long.ZERO
447
+ * - if the radix is less than 35, '+/-Infinity' inputs are coerced to Long.ZERO
448
+ * - other invalid characters sequences have variable behavior
449
+ * @param str - The textual representation of the Long
450
+ * @param unsigned - Whether unsigned or not, defaults to signed
451
+ * @param radix - The radix in which the text is written (2-36), defaults to 10
452
+ * @returns The corresponding Long value
453
+ */
454
+ static fromString(str: string, unsigned?: boolean, radix?: number): Long;
455
+ static fromString(str: string, unsignedOrRadix?: boolean | number, radix?: number): Long {
456
+ let unsigned = false;
457
+ if (typeof unsignedOrRadix === 'number') {
458
+ // For goog.math.long compatibility
459
+ (radix = unsignedOrRadix), (unsignedOrRadix = false);
460
+ } else {
461
+ unsigned = !!unsignedOrRadix;
462
+ }
463
+ radix ??= 10;
464
+ if (str === 'NaN' && radix < 24) {
465
+ // radix does not support n, so coerce to zero
466
+ return Long.ZERO;
467
+ } else if ((str === 'Infinity' || str === '+Infinity' || str === '-Infinity') && radix < 35) {
468
+ // radix does not support y, so coerce to zero
469
+ return Long.ZERO;
470
+ }
471
+ return Long._fromString(str, unsigned, radix);
472
+ }
473
+
294
474
  /**
295
475
  * Creates a Long from its byte representation.
296
476
  * @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,5 +1,5 @@
1
1
  import { BSONError } from '../error';
2
- import { validateUtf8 } from '../validate_utf8';
2
+ import { parseUtf8 } from '../parse_utf8';
3
3
  import { tryReadBasicLatin, tryWriteBasicLatin } from './latin';
4
4
 
5
5
  type NodeJsEncoding = 'base64' | 'hex' | 'utf8' | 'binary';
@@ -136,12 +136,9 @@ export const nodeJsByteUtils = {
136
136
 
137
137
  const string = nodeJsByteUtils.toLocalBufferType(buffer).toString('utf8', start, end);
138
138
  if (fatal) {
139
- // TODO(NODE-4930): Insufficiently strict BSON UTF8 validation
140
139
  for (let i = 0; i < string.length; i++) {
141
140
  if (string.charCodeAt(i) === 0xfffd) {
142
- if (!validateUtf8(buffer, start, end)) {
143
- throw new BSONError('Invalid UTF-8 string in BSON document');
144
- }
141
+ parseUtf8(buffer, start, end, true);
145
142
  break;
146
143
  }
147
144
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @internal
3
+ * Removes leading zeros and explicit plus from textual representation of a number.
4
+ */
5
+ export function removeLeadingZerosAndExplicitPlus(str: string): string {
6
+ if (str === '') {
7
+ return str;
8
+ }
9
+
10
+ let startIndex = 0;
11
+
12
+ const isNegative = str[startIndex] === '-';
13
+ const isExplicitlyPositive = str[startIndex] === '+';
14
+
15
+ if (isExplicitlyPositive || isNegative) {
16
+ startIndex += 1;
17
+ }
18
+
19
+ let foundInsignificantZero = false;
20
+
21
+ for (; startIndex < str.length && str[startIndex] === '0'; ++startIndex) {
22
+ foundInsignificantZero = true;
23
+ }
24
+
25
+ if (!foundInsignificantZero) {
26
+ return isExplicitlyPositive ? str.slice(1) : str;
27
+ }
28
+
29
+ return `${isNegative ? '-' : ''}${str.length === startIndex ? '0' : str.slice(startIndex)}`;
30
+ }
31
+
32
+ /**
33
+ * @internal
34
+ * Returns false for an string that contains invalid characters for its radix, else returns the original string.
35
+ * @param str - The textual representation of the Long
36
+ * @param radix - The radix in which the text is written (2-36), defaults to 10
37
+ */
38
+ export function validateStringCharacters(str: string, radix?: number): false | string {
39
+ radix = radix ?? 10;
40
+ const validCharacters = '0123456789abcdefghijklmnopqrstuvwxyz'.slice(0, radix);
41
+ // regex is case insensitive and checks that each character within the string is one of the validCharacters
42
+ const regex = new RegExp(`[^-+${validCharacters}]`, 'i');
43
+ return regex.test(str) ? false : str;
44
+ }
@@ -1,5 +1,6 @@
1
1
  import { BSONError } from '../error';
2
2
  import { tryReadBasicLatin } from './latin';
3
+ import { parseUtf8 } from '../parse_utf8';
3
4
 
4
5
  type TextDecoder = {
5
6
  readonly encoding: string;
@@ -179,14 +180,7 @@ export const webByteUtils = {
179
180
  return basicLatin;
180
181
  }
181
182
 
182
- if (fatal) {
183
- try {
184
- return new TextDecoder('utf8', { fatal }).decode(uint8array.slice(start, end));
185
- } catch (cause) {
186
- throw new BSONError('Invalid UTF-8 string in BSON document', { cause });
187
- }
188
- }
189
- return new TextDecoder('utf8', { fatal }).decode(uint8array.slice(start, end));
183
+ return parseUtf8(uint8array, start, end, fatal);
190
184
  },
191
185
 
192
186
  utf8ByteLength(input: string): number {
@@ -1,47 +0,0 @@
1
- const FIRST_BIT = 0x80;
2
- const FIRST_TWO_BITS = 0xc0;
3
- const FIRST_THREE_BITS = 0xe0;
4
- const FIRST_FOUR_BITS = 0xf0;
5
- const FIRST_FIVE_BITS = 0xf8;
6
-
7
- const TWO_BIT_CHAR = 0xc0;
8
- const THREE_BIT_CHAR = 0xe0;
9
- const FOUR_BIT_CHAR = 0xf0;
10
- const CONTINUING_CHAR = 0x80;
11
-
12
- /**
13
- * Determines if the passed in bytes are valid utf8
14
- * @param bytes - An array of 8-bit bytes. Must be indexable and have length property
15
- * @param start - The index to start validating
16
- * @param end - The index to end validating
17
- */
18
- export function validateUtf8(
19
- bytes: { [index: number]: number },
20
- start: number,
21
- end: number
22
- ): boolean {
23
- let continuation = 0;
24
-
25
- for (let i = start; i < end; i += 1) {
26
- const byte = bytes[i];
27
-
28
- if (continuation) {
29
- if ((byte & FIRST_TWO_BITS) !== CONTINUING_CHAR) {
30
- return false;
31
- }
32
- continuation -= 1;
33
- } else if (byte & FIRST_BIT) {
34
- if ((byte & FIRST_THREE_BITS) === TWO_BIT_CHAR) {
35
- continuation = 1;
36
- } else if ((byte & FIRST_FOUR_BITS) === THREE_BIT_CHAR) {
37
- continuation = 2;
38
- } else if ((byte & FIRST_FIVE_BITS) === FOUR_BIT_CHAR) {
39
- continuation = 3;
40
- } else {
41
- return false;
42
- }
43
- }
44
- }
45
-
46
- return !continuation;
47
- }