Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NODE-4711)!: remove evalFunctions option #539

Merged
merged 10 commits into from
Dec 9, 2022
32 changes: 32 additions & 0 deletions docs/upgrade-to-v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,41 @@ EJSON.parse("...", { strict: false }); /* migrate to */ EJSON.parse("...", { r
// stringify
EJSON.stringify({}, { strict: true }); /* migrate to */ EJSON.stringify({}, { relaxed: false });
EJSON.stringify({}, { strict: false }); /* migrate to */ EJSON.stringify({}, { relaxed: true });
```

### The BSON default export has been removed.

* If you import BSON commonjs style `const BSON = require('bson')` then the `BSON.default` property is no longer present.
* 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.**
* This error will throw: `SyntaxError: The requested module 'bson' does not provide an export named 'default'`.

### `class Code` always converts `.code` to string

The `Code` class still supports the same constructor arguments as before.
It will now convert the first argument to a string before saving it to the code property, see the following:

```typescript
const myCode = new Code(function iLoveJavascript() { console.log('I love javascript') });
// myCode.code === "function iLoveJavascript() { console.log('I love javascript') }"
// typeof myCode.code === 'string'
```

### `BSON.deserialize()` only returns `Code` instances

The deserialize options: `evalFunctions`, `cacheFunctions`, and `cacheFunctionsCrc32` have been removed.
The `evalFunctions` option, when enabled, would return BSON Code typed values as eval-ed javascript functions, now it will always return Code instances.

See the following snippet for how to migrate:
```typescript
const bsonBytes = BSON.serialize(
{ iLoveJavascript: function () { console.log('I love javascript') } },
{ serializeFunctions: true } // serializeFunctions still works!
);
const result = BSON.deserialize(bsonBytes)
// result.iLoveJavascript instanceof Code
// result.iLoveJavascript.code === "function () { console.log('I love javascript') }"
const iLoveJavascript = new Function(`return ${result.iLoveJavascript.code}`)();
iLoveJavascript();
// prints "I love javascript"
// iLoveJavascript.name === "iLoveJavascript"
```
26 changes: 17 additions & 9 deletions src/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Document } from './bson';

/** @public */
export interface CodeExtended {
$code: string | Function;
$code: string;
$scope?: Document;
}

Expand All @@ -16,19 +16,27 @@ export class Code {
return 'Code';
}

code!: string | Function;
scope?: Document;
code: string;

// a code instance having a null scope is what determines whether
// it is BSONType 0x0D (just code) / 0x0F (code with scope)
scope: Document | null;

/**
* @param code - a string or function.
* @param scope - an optional scope for the function.
*/
constructor(code: string | Function, scope?: Document) {
this.code = code;
this.scope = scope;
constructor(code: string | Function, scope?: Document | null) {
this.code = code.toString();
this.scope = scope ?? null;
}

toJSON(): { code: string | Function; scope?: Document } {
return { code: this.code, scope: this.scope };
toJSON(): { code: string; scope?: Document } {
durran marked this conversation as resolved.
Show resolved Hide resolved
if (this.scope != null) {
return { code: this.code, scope: this.scope };
}

return { code: this.code };
}

/** @internal */
Expand All @@ -53,7 +61,7 @@ export class Code {
inspect(): string {
const codeJson = this.toJSON();
return `new Code("${String(codeJson.code)}"${
codeJson.scope ? `, ${JSON.stringify(codeJson.scope)}` : ''
codeJson.scope != null ? `, ${JSON.stringify(codeJson.scope)}` : ''
})`;
}
}
32 changes: 4 additions & 28 deletions src/parser/calculate_size.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Binary } from '../binary';
import type { Document } from '../bson';
import * as constants from '../constants';
import { ByteUtils } from '../utils/byte_utils';
import { isAnyArrayBuffer, isDate, isRegExp, normalizedFunctionString } from './utils';
import { isAnyArrayBuffer, isDate, isRegExp } from './utils';

export function calculateObjectSize(
object: Document,
Expand Down Expand Up @@ -189,38 +189,14 @@ function calculateElement(
);
}
case 'function':
// WTF for 0.4.X where typeof /someregexp/ === 'function'
if (value instanceof RegExp || isRegExp(value) || String.call(value) === '[object RegExp]') {
if (serializeFunctions) {
return (
(name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) +
1 +
ByteUtils.utf8ByteLength(value.source) +
1 +
(value.global ? 1 : 0) +
(value.ignoreCase ? 1 : 0) +
(value.multiline ? 1 : 0) +
4 +
ByteUtils.utf8ByteLength(value.toString()) +
1
);
} else {
if (serializeFunctions && value.scope != null && Object.keys(value.scope).length > 0) {
return (
(name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) +
1 +
4 +
4 +
ByteUtils.utf8ByteLength(normalizedFunctionString(value)) +
1 +
calculateObjectSize(value.scope, serializeFunctions, ignoreUndefined)
);
} else if (serializeFunctions) {
return (
(name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) +
1 +
4 +
ByteUtils.utf8ByteLength(normalizedFunctionString(value)) +
1
);
}
}
}

Expand Down
65 changes: 2 additions & 63 deletions src/parser/deserializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,6 @@ import { validateUtf8 } from '../validate_utf8';

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

const functionCache: { [hash: string]: Function } = {};

export function deserialize(
buffer: Uint8Array,
options: DeserializeOptions,
Expand Down Expand Up @@ -120,9 +108,6 @@ function deserializeObject(
options: DeserializeOptions,
isArray = false
) {
const evalFunctions = options['evalFunctions'] == null ? false : options['evalFunctions'];
const cacheFunctions = options['cacheFunctions'] == null ? false : options['cacheFunctions'];

const fieldsAsRaw = options['fieldsAsRaw'] == null ? null : options['fieldsAsRaw'];

// Return raw bson buffer instead of parsing it
Expand Down Expand Up @@ -569,18 +554,7 @@ function deserializeObject(
shouldValidateKey
);

// If we are evaluating the functions
if (evalFunctions) {
// If we have cache enabled let's look for the md5 of the function in the cache
if (cacheFunctions) {
// Got to do this to avoid V8 deoptimizing the call due to finding eval
value = isolateEval(functionString, functionCache, object);
} else {
value = isolateEval(functionString);
}
} else {
value = new Code(functionString);
}
value = new Code(functionString);

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

// If we are evaluating the functions
if (evalFunctions) {
// If we have cache enabled let's look for the md5 of the function in the cache
if (cacheFunctions) {
// Got to do this to avoid V8 deoptimizing the call due to finding eval
value = isolateEval(functionString, functionCache, object);
} else {
value = isolateEval(functionString);
}

value.scope = scopeObject;
} else {
value = new Code(functionString, scopeObject);
}
value = new Code(functionString, scopeObject);
} else if (elementType === constants.BSON_DATA_DBPOINTER) {
// Get the code string size
const stringSize =
Expand Down Expand Up @@ -728,28 +689,6 @@ function deserializeObject(
return object;
}

/**
* Ensure eval is isolated, store the result in functionCache.
*
* @internal
*/
function isolateEval(
functionString: string,
functionCache?: { [hash: string]: Function },
object?: Document
) {
// eslint-disable-next-line @typescript-eslint/no-implied-eval
if (!functionCache) return new Function(functionString);
// Check for cache hit, eval if missing and return cached function
if (functionCache[functionString] == null) {
// eslint-disable-next-line @typescript-eslint/no-implied-eval
functionCache[functionString] = new Function(functionString);
}

// Set the object
return functionCache[functionString].bind(object);
}

function getValidatedString(
buffer: Uint8Array,
start: number,
Expand Down
29 changes: 7 additions & 22 deletions src/parser/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,7 @@ import type { MinKey } from '../min_key';
import type { ObjectId } from '../objectid';
import type { BSONRegExp } from '../regexp';
import { ByteUtils } from '../utils/byte_utils';
import {
isBigInt64Array,
isBigUInt64Array,
isDate,
isMap,
isRegExp,
isUint8Array,
normalizedFunctionString
} from './utils';
import { isBigInt64Array, isBigUInt64Array, isDate, isMap, isRegExp, isUint8Array } from './utils';

/** @public */
export interface SerializeOptions {
Expand Down Expand Up @@ -386,22 +378,15 @@ function serializeDouble(buffer: Uint8Array, key: string, value: Double, index:
return index;
}

function serializeFunction(
buffer: Uint8Array,
key: string,
value: Function,
index: number,
_checkKeys = false,
_depth = 0
) {
function serializeFunction(buffer: Uint8Array, key: string, value: Function, index: number) {
buffer[index++] = constants.BSON_DATA_CODE;
// Number of written bytes
const numberOfWrittenBytes = ByteUtils.encodeUTF8Into(buffer, key, index);
// Encode the name
index = index + numberOfWrittenBytes;
buffer[index++] = 0;
// Function string
const functionString = normalizedFunctionString(value);
const functionString = value.toString();

// Write the string
const size = ByteUtils.encodeUTF8Into(buffer, functionString, index + 4) + 1;
Expand Down Expand Up @@ -441,7 +426,7 @@ function serializeCode(

// Serialize the function
// Get the function string
const functionString = typeof value.code === 'string' ? value.code : value.code.toString();
const functionString = value.code;
// Index adjustment
index = index + 4;
// Write string into buffer
Expand Down Expand Up @@ -679,7 +664,7 @@ export function serializeInto(
} else if (value['_bsontype'] === 'Double') {
index = serializeDouble(buffer, key, value, index);
} else if (typeof value === 'function' && serializeFunctions) {
index = serializeFunction(buffer, key, value, index, checkKeys, depth);
index = serializeFunction(buffer, key, value, index);
} else if (value['_bsontype'] === 'Code') {
index = serializeCode(
buffer,
Expand Down Expand Up @@ -790,7 +775,7 @@ export function serializeInto(
ignoreUndefined
);
} else if (typeof value === 'function' && serializeFunctions) {
index = serializeFunction(buffer, key, value, index, checkKeys, depth);
index = serializeFunction(buffer, key, value, index);
} else if (value['_bsontype'] === 'Binary') {
index = serializeBinary(buffer, key, value, index);
} else if (value['_bsontype'] === 'Symbol') {
Expand Down Expand Up @@ -894,7 +879,7 @@ export function serializeInto(
ignoreUndefined
);
} else if (typeof value === 'function' && serializeFunctions) {
index = serializeFunction(buffer, key, value, index, checkKeys, depth);
index = serializeFunction(buffer, key, value, index);
} else if (value['_bsontype'] === 'Binary') {
index = serializeBinary(buffer, key, value, index);
} else if (value['_bsontype'] === 'Symbol') {
Expand Down
8 changes: 0 additions & 8 deletions src/parser/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
/**
* Normalizes our expected stringified form of a function across versions of node
* @param fn - The function to stringify
*/
export function normalizedFunctionString(fn: Function): string {
return fn.toString().replace('function(', 'function (');
}

export function isAnyArrayBuffer(value: unknown): value is ArrayBuffer {
return ['[object ArrayBuffer]', '[object SharedArrayBuffer]'].includes(
Object.prototype.toString.call(value)
Expand Down
Loading