Skip to content

Commit 8a758e2

Browse files
committed
Merge branch 'main' into NODE-1921-serializer-input-validation
2 parents 06f6bca + 0427eb5 commit 8a758e2

File tree

10 files changed

+182
-125
lines changed

10 files changed

+182
-125
lines changed

.github/pull_request_template.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ You can do that here: https://jira.mongodb.org/projects/NODE
2020

2121
- [ ] Ran `npm run lint` script
2222
- [ ] Self-review completed using the [steps outlined here](https://github.com/mongodb/node-mongodb-native/blob/HEAD/CONTRIBUTING.md#reviewer-guidelines)
23-
- [ ] PR title follows the correct format: `<type>(NODE-xxxx)<!>: <description>`
23+
- [ ] PR title follows the [correct format](https://www.conventionalcommits.org/en/v1.0.0/): `type(NODE-xxxx)[!]: description`
24+
- Example: `feat(NODE-1234)!: rewriting everything in coffeescript`
2425
- [ ] Changes are covered by tests
2526
- [ ] New TODOs have a related JIRA ticket

docs/upgrade-to-v5.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,37 @@ EJSON.stringify({}, { strict: false }); /* migrate to */ EJSON.stringify({}, { r
187187
* If you import BSON esmodule style `import BSON from 'bson'` then this code will crash upon loading. **TODO: This is not the case right now but it will be after NODE-4713.**
188188
* This error will throw: `SyntaxError: The requested module 'bson' does not provide an export named 'default'`.
189189

190+
### `class Code` always converts `.code` to string
191+
192+
The `Code` class still supports the same constructor arguments as before.
193+
It will now convert the first argument to a string before saving it to the code property, see the following:
194+
195+
```typescript
196+
const myCode = new Code(function iLoveJavascript() { console.log('I love javascript') });
197+
// myCode.code === "function iLoveJavascript() { console.log('I love javascript') }"
198+
// typeof myCode.code === 'string'
199+
```
200+
201+
### `BSON.deserialize()` only returns `Code` instances
202+
203+
The deserialize options: `evalFunctions`, `cacheFunctions`, and `cacheFunctionsCrc32` have been removed.
204+
The `evalFunctions` option, when enabled, would return BSON Code typed values as eval-ed javascript functions, now it will always return Code instances.
205+
206+
See the following snippet for how to migrate:
207+
```typescript
208+
const bsonBytes = BSON.serialize(
209+
{ iLoveJavascript: function () { console.log('I love javascript') } },
210+
{ serializeFunctions: true } // serializeFunctions still works!
211+
);
212+
const result = BSON.deserialize(bsonBytes)
213+
// result.iLoveJavascript instanceof Code
214+
// result.iLoveJavascript.code === "function () { console.log('I love javascript') }"
215+
const iLoveJavascript = new Function(`return ${result.iLoveJavascript.code}`)();
216+
iLoveJavascript();
217+
// prints "I love javascript"
218+
// iLoveJavascript.name === "iLoveJavascript"
219+
```
220+
190221
### `BSON.serialize()` validation
191222

192223
The BSON format does not support encoding arrays as the **root** object.

src/code.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Document } from './bson';
22

33
/** @public */
44
export interface CodeExtended {
5-
$code: string | Function;
5+
$code: string;
66
$scope?: Document;
77
}
88

@@ -16,19 +16,27 @@ export class Code {
1616
return 'Code';
1717
}
1818

19-
code!: string | Function;
20-
scope?: Document;
19+
code: string;
20+
21+
// a code instance having a null scope is what determines whether
22+
// it is BSONType 0x0D (just code) / 0x0F (code with scope)
23+
scope: Document | null;
24+
2125
/**
2226
* @param code - a string or function.
2327
* @param scope - an optional scope for the function.
2428
*/
25-
constructor(code: string | Function, scope?: Document) {
26-
this.code = code;
27-
this.scope = scope;
29+
constructor(code: string | Function, scope?: Document | null) {
30+
this.code = code.toString();
31+
this.scope = scope ?? null;
2832
}
2933

30-
toJSON(): { code: string | Function; scope?: Document } {
31-
return { code: this.code, scope: this.scope };
34+
toJSON(): { code: string; scope?: Document } {
35+
if (this.scope != null) {
36+
return { code: this.code, scope: this.scope };
37+
}
38+
39+
return { code: this.code };
3240
}
3341

3442
/** @internal */
@@ -53,7 +61,7 @@ export class Code {
5361
inspect(): string {
5462
const codeJson = this.toJSON();
5563
return `new Code("${String(codeJson.code)}"${
56-
codeJson.scope ? `, ${JSON.stringify(codeJson.scope)}` : ''
64+
codeJson.scope != null ? `, ${JSON.stringify(codeJson.scope)}` : ''
5765
})`;
5866
}
5967
}

src/parser/calculate_size.ts

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Binary } from '../binary';
22
import type { Document } from '../bson';
33
import * as constants from '../constants';
44
import { ByteUtils } from '../utils/byte_utils';
5-
import { isAnyArrayBuffer, isDate, isRegExp, normalizedFunctionString } from './utils';
5+
import { isAnyArrayBuffer, isDate, isRegExp } from './utils';
66

77
export function calculateObjectSize(
88
object: Document,
@@ -189,38 +189,14 @@ function calculateElement(
189189
);
190190
}
191191
case 'function':
192-
// WTF for 0.4.X where typeof /someregexp/ === 'function'
193-
if (value instanceof RegExp || isRegExp(value) || String.call(value) === '[object RegExp]') {
192+
if (serializeFunctions) {
194193
return (
195194
(name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) +
196195
1 +
197-
ByteUtils.utf8ByteLength(value.source) +
198-
1 +
199-
(value.global ? 1 : 0) +
200-
(value.ignoreCase ? 1 : 0) +
201-
(value.multiline ? 1 : 0) +
196+
4 +
197+
ByteUtils.utf8ByteLength(value.toString()) +
202198
1
203199
);
204-
} else {
205-
if (serializeFunctions && value.scope != null && Object.keys(value.scope).length > 0) {
206-
return (
207-
(name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) +
208-
1 +
209-
4 +
210-
4 +
211-
ByteUtils.utf8ByteLength(normalizedFunctionString(value)) +
212-
1 +
213-
calculateObjectSize(value.scope, serializeFunctions, ignoreUndefined)
214-
);
215-
} else if (serializeFunctions) {
216-
return (
217-
(name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) +
218-
1 +
219-
4 +
220-
ByteUtils.utf8ByteLength(normalizedFunctionString(value)) +
221-
1
222-
);
223-
}
224200
}
225201
}
226202

src/parser/deserializer.ts

Lines changed: 2 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,6 @@ import { validateUtf8 } from '../validate_utf8';
1919

2020
/** @public */
2121
export interface DeserializeOptions {
22-
/** evaluate functions in the BSON document scoped to the object deserialized. */
23-
evalFunctions?: boolean;
24-
/** cache evaluated functions for reuse. */
25-
cacheFunctions?: boolean;
26-
/**
27-
* use a crc32 code for caching, otherwise use the string of the function.
28-
* @deprecated this option to use the crc32 function never worked as intended
29-
* due to the fact that the crc32 function itself was never implemented.
30-
* */
31-
cacheFunctionsCrc32?: boolean;
3222
/** when deserializing a Long will fit it into a Number if it's smaller than 53 bits */
3323
promoteLongs?: boolean;
3424
/** when deserializing a Binary will return it as a node.js Buffer instance. */
@@ -67,8 +57,6 @@ export interface DeserializeOptions {
6757
const JS_INT_MAX_LONG = Long.fromNumber(constants.JS_INT_MAX);
6858
const JS_INT_MIN_LONG = Long.fromNumber(constants.JS_INT_MIN);
6959

70-
const functionCache: { [hash: string]: Function } = {};
71-
7260
export function deserialize(
7361
buffer: Uint8Array,
7462
options: DeserializeOptions,
@@ -120,9 +108,6 @@ function deserializeObject(
120108
options: DeserializeOptions,
121109
isArray = false
122110
) {
123-
const evalFunctions = options['evalFunctions'] == null ? false : options['evalFunctions'];
124-
const cacheFunctions = options['cacheFunctions'] == null ? false : options['cacheFunctions'];
125-
126111
const fieldsAsRaw = options['fieldsAsRaw'] == null ? null : options['fieldsAsRaw'];
127112

128113
// Return raw bson buffer instead of parsing it
@@ -569,18 +554,7 @@ function deserializeObject(
569554
shouldValidateKey
570555
);
571556

572-
// If we are evaluating the functions
573-
if (evalFunctions) {
574-
// If we have cache enabled let's look for the md5 of the function in the cache
575-
if (cacheFunctions) {
576-
// Got to do this to avoid V8 deoptimizing the call due to finding eval
577-
value = isolateEval(functionString, functionCache, object);
578-
} else {
579-
value = isolateEval(functionString);
580-
}
581-
} else {
582-
value = new Code(functionString);
583-
}
557+
value = new Code(functionString);
584558

585559
// Update parse index position
586560
index = index + stringSize;
@@ -643,20 +617,7 @@ function deserializeObject(
643617
throw new BSONError('code_w_scope total size is too long, clips outer document');
644618
}
645619

646-
// If we are evaluating the functions
647-
if (evalFunctions) {
648-
// If we have cache enabled let's look for the md5 of the function in the cache
649-
if (cacheFunctions) {
650-
// Got to do this to avoid V8 deoptimizing the call due to finding eval
651-
value = isolateEval(functionString, functionCache, object);
652-
} else {
653-
value = isolateEval(functionString);
654-
}
655-
656-
value.scope = scopeObject;
657-
} else {
658-
value = new Code(functionString, scopeObject);
659-
}
620+
value = new Code(functionString, scopeObject);
660621
} else if (elementType === constants.BSON_DATA_DBPOINTER) {
661622
// Get the code string size
662623
const stringSize =
@@ -728,28 +689,6 @@ function deserializeObject(
728689
return object;
729690
}
730691

731-
/**
732-
* Ensure eval is isolated, store the result in functionCache.
733-
*
734-
* @internal
735-
*/
736-
function isolateEval(
737-
functionString: string,
738-
functionCache?: { [hash: string]: Function },
739-
object?: Document
740-
) {
741-
// eslint-disable-next-line @typescript-eslint/no-implied-eval
742-
if (!functionCache) return new Function(functionString);
743-
// Check for cache hit, eval if missing and return cached function
744-
if (functionCache[functionString] == null) {
745-
// eslint-disable-next-line @typescript-eslint/no-implied-eval
746-
functionCache[functionString] = new Function(functionString);
747-
}
748-
749-
// Set the object
750-
return functionCache[functionString].bind(object);
751-
}
752-
753692
function getValidatedString(
754693
buffer: Uint8Array,
755694
start: number,

src/parser/serializer.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ import {
2020
isDate,
2121
isMap,
2222
isRegExp,
23-
isUint8Array,
24-
normalizedFunctionString
23+
isUint8Array
2524
} from './utils';
2625

2726
/** @public */
@@ -388,22 +387,15 @@ function serializeDouble(buffer: Uint8Array, key: string, value: Double, index:
388387
return index;
389388
}
390389

391-
function serializeFunction(
392-
buffer: Uint8Array,
393-
key: string,
394-
value: Function,
395-
index: number,
396-
_checkKeys = false,
397-
_depth = 0
398-
) {
390+
function serializeFunction(buffer: Uint8Array, key: string, value: Function, index: number) {
399391
buffer[index++] = constants.BSON_DATA_CODE;
400392
// Number of written bytes
401393
const numberOfWrittenBytes = ByteUtils.encodeUTF8Into(buffer, key, index);
402394
// Encode the name
403395
index = index + numberOfWrittenBytes;
404396
buffer[index++] = 0;
405397
// Function string
406-
const functionString = normalizedFunctionString(value);
398+
const functionString = value.toString();
407399

408400
// Write the string
409401
const size = ByteUtils.encodeUTF8Into(buffer, functionString, index + 4) + 1;
@@ -444,7 +436,7 @@ function serializeCode(
444436

445437
// Serialize the function
446438
// Get the function string
447-
const functionString = typeof value.code === 'string' ? value.code : value.code.toString();
439+
const functionString = value.code;
448440
// Index adjustment
449441
index = index + 4;
450442
// Write string into buffer
@@ -722,7 +714,7 @@ export function serializeInto(
722714
} else if (value['_bsontype'] === 'Double') {
723715
index = serializeDouble(buffer, key, value, index);
724716
} else if (typeof value === 'function' && serializeFunctions) {
725-
index = serializeFunction(buffer, key, value, index, checkKeys, depth);
717+
index = serializeFunction(buffer, key, value, index);
726718
} else if (value['_bsontype'] === 'Code') {
727719
index = serializeCode(
728720
buffer,
@@ -835,7 +827,7 @@ export function serializeInto(
835827
path
836828
);
837829
} else if (typeof value === 'function' && serializeFunctions) {
838-
index = serializeFunction(buffer, key, value, index, checkKeys, depth);
830+
index = serializeFunction(buffer, key, value, index);
839831
} else if (value['_bsontype'] === 'Binary') {
840832
index = serializeBinary(buffer, key, value, index);
841833
} else if (value['_bsontype'] === 'Symbol') {
@@ -940,7 +932,7 @@ export function serializeInto(
940932
path
941933
);
942934
} else if (typeof value === 'function' && serializeFunctions) {
943-
index = serializeFunction(buffer, key, value, index, checkKeys, depth);
935+
index = serializeFunction(buffer, key, value, index);
944936
} else if (value['_bsontype'] === 'Binary') {
945937
index = serializeBinary(buffer, key, value, index);
946938
} else if (value['_bsontype'] === 'Symbol') {

src/parser/utils.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
/**
2-
* Normalizes our expected stringified form of a function across versions of node
3-
* @param fn - The function to stringify
4-
*/
5-
export function normalizedFunctionString(fn: Function): string {
6-
return fn.toString().replace('function(', 'function (');
7-
}
8-
91
export function isAnyArrayBuffer(value: unknown): value is ArrayBuffer {
102
return ['[object ArrayBuffer]', '[object SharedArrayBuffer]'].includes(
113
Object.prototype.toString.call(value)

0 commit comments

Comments
 (0)