Skip to content

Commit e5ab7c5

Browse files
committed
fix(shell-api,shell-bson): increase runtime independence
Make the `shell-bson` and `shell-api` packages more runtime-independent (as they are supposed to be) by using `util` and `crypto` as progressive enhancement enablers rather than required packages. This specifically makes `shell-bson` fully usable in a bare-bones JS environment.
1 parent e403868 commit e5ab7c5

File tree

7 files changed

+91
-40
lines changed

7 files changed

+91
-40
lines changed

packages/service-provider-node-driver/src/node-driver-service-provider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ const bsonlib = () => {
109109
Decimal128,
110110
BSONSymbol,
111111
BSONRegExp,
112+
UUID,
112113
BSON,
113114
} = driver;
114115
return {
@@ -126,6 +127,7 @@ const bsonlib = () => {
126127
BSONSymbol,
127128
calculateObjectSize: BSON.calculateObjectSize,
128129
EJSON: BSON.EJSON,
130+
UUID,
129131
BSONRegExp,
130132
};
131133
};

packages/shell-api/src/helpers.ts

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
MongoshInvalidInputError,
1616
MongoshUnimplementedError,
1717
} from '@mongosh/errors';
18-
import crypto from 'crypto';
1918
import type { Database } from './database';
2019
import type { Collection } from './collection';
2120
import type { CursorIterationResult } from './result';
@@ -27,8 +26,12 @@ import { shellApiType } from './enums';
2726
import type { AbstractFiniteCursor } from './abstract-cursor';
2827
import type ChangeStreamCursor from './change-stream-cursor';
2928
import type { BSON, ShellBson } from '@mongosh/shell-bson';
30-
import { inspect } from 'util';
3129
import type { MQLPipeline, MQLQuery } from './mql-types';
30+
import type { InspectOptions } from 'util';
31+
32+
let coreUtilInspect: ((obj: any, options: InspectOptions) => string) & {
33+
defaultOptions: InspectOptions;
34+
};
3235

3336
/**
3437
* Helper method to adapt aggregation pipeline options.
@@ -202,6 +205,19 @@ export function processDigestPassword(
202205
CommonErrors.InvalidArgument
203206
);
204207
}
208+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
209+
let crypto: typeof import('crypto');
210+
try {
211+
// Try to dynamically import crypto so that we don't break plain-JS-runtime builds.
212+
// The Web Crypto API does not provide MD5, which is reasonable for a modern API
213+
// but means that we cannot use it as a fallback.
214+
crypto = require('crypto');
215+
} catch {
216+
throw new MongoshUnimplementedError(
217+
'Legacy password digest algorithms like SCRAM-SHA-1 are not supported by this instance of mongosh',
218+
CommonErrors.Deprecated
219+
);
220+
}
205221
// NOTE: this code has raised a code scanning alert about the "use of a broken or weak cryptographic algorithm":
206222
// we inherited this code from `mongo`, and we cannot replace MD5 with a different algorithm, since MD5 is part of the SCRAM-SHA-1 protocol,
207223
// and the purpose of `passwordDigestor=client` is to improve the security of SCRAM-SHA-1, allowing the creation of new users
@@ -630,24 +646,35 @@ export async function getPrintableShardStatus(
630646
'on shard': chunk.shard,
631647
'last modified': chunk.lastmod,
632648
} as any;
633-
// Displaying a full, multi-line output for each chunk is a bit verbose,
634-
// even if there are only a few chunks. Where supported, we use a custom
635-
// inspection function to inspect a copy of this object with an unlimited
636-
// line break length (i.e. all objects on a single line).
637-
Object.defineProperty(
638-
c,
639-
Symbol.for('nodejs.util.inspect.custom'),
640-
{
641-
value: function (depth: number, options: any): string {
642-
return inspect(
643-
{ ...this },
644-
{ ...options, breakLength: Infinity }
645-
);
646-
},
647-
writable: true,
648-
configurable: true,
649-
}
650-
);
649+
try {
650+
// eslint-disable-next-line @typescript-eslint/no-var-requires
651+
coreUtilInspect ??= require('util').inspect;
652+
} catch {
653+
// No util.inspect available, e.g. in browser builds.
654+
}
655+
if (coreUtilInspect) {
656+
// Displaying a full, multi-line output for each chunk is a bit verbose,
657+
// even if there are only a few chunks. Where supported, we use a custom
658+
// inspection function to inspect a copy of this object with an unlimited
659+
// line break length (i.e. all objects on a single line).
660+
Object.defineProperty(
661+
c,
662+
Symbol.for('nodejs.util.inspect.custom'),
663+
{
664+
value: function (
665+
depth: number,
666+
options: InspectOptions
667+
): string {
668+
return coreUtilInspect(
669+
{ ...this },
670+
{ ...options, breakLength: Infinity }
671+
);
672+
},
673+
writable: true,
674+
configurable: true,
675+
}
676+
);
677+
}
651678
if (chunk.jumbo) c.jumbo = 'yes';
652679
chunksRes.push(c);
653680
} else if (chunksRes.length === 20 && !verbose) {

packages/shell-api/src/runtime-independence.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('Runtime independence', function () {
2020
// for other environments, but which we should still ideally remove in the
2121
// long run (and definitely not add anything here).
2222
// Guaranteed bonusly for anyone who removes a package from this list!
23-
const allowedNodeBuiltins = ['crypto', 'util', 'events', 'path'];
23+
const allowedNodeBuiltins = ['events', 'path'];
2424
// Our TextDecoder/TextEncoder polyfills require this, unfortunately.
2525
context.Buffer = Buffer;
2626

packages/shell-api/src/shell-api.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import {
3131
MongoshInternalError,
3232
} from '@mongosh/errors';
3333
import { DBQuery } from './dbquery';
34-
import { promisify } from 'util';
3534
import type { ClientSideFieldLevelEncryptionOptions } from './field-level-encryption';
3635
import { dirname } from 'path';
3736
import { ShellUserConfig } from '@mongosh/types';
@@ -422,7 +421,7 @@ export default class ShellApi extends ShellApiClass {
422421

423422
@returnsPromise
424423
async sleep(ms: number): Promise<void> {
425-
return await promisify(setTimeout)(ms);
424+
return await new Promise<void>((resolve) => setTimeout(resolve, ms));
426425
}
427426

428427
private async _print(

packages/shell-bson/src/bson-export.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
calculateObjectSize,
1414
Double,
1515
EJSON,
16+
UUID,
1617
BSONRegExp,
1718
} from 'bson';
1819
export type {
@@ -29,6 +30,7 @@ export type {
2930
Binary,
3031
Double,
3132
EJSON,
33+
UUID,
3234
BSONRegExp,
3335
calculateObjectSize,
3436
};
@@ -45,6 +47,7 @@ export type BSON = {
4547
Binary: typeof Binary;
4648
Double: typeof Double;
4749
EJSON: typeof EJSON;
50+
UUID: typeof UUID;
4851
BSONRegExp: typeof BSONRegExp;
4952
BSONSymbol: typeof BSONSymbol;
5053
calculateObjectSize: typeof calculateObjectSize;

packages/shell-bson/src/printable-bson.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,40 @@
11
import type { BSON } from './';
2-
import type { InspectOptionsStylized, CustomInspectFunction } from 'util';
3-
import { inspect as utilInspect } from 'util';
2+
import type {
3+
InspectOptionsStylized,
4+
CustomInspectFunction,
5+
InspectOptions,
6+
} from 'util';
47
const inspectCustom = Symbol.for('nodejs.util.inspect.custom');
58
type BSONClassKey = BSON[Exclude<
69
keyof BSON,
710
'EJSON' | 'calculateObjectSize'
811
>]['prototype']['_bsontype'];
912

13+
let coreUtilInspect: ((obj: any, options: InspectOptions) => string) & {
14+
defaultOptions: InspectOptions;
15+
};
16+
function inspectTypedArray(
17+
obj: Iterable<number>,
18+
options: InspectOptions
19+
): string {
20+
try {
21+
coreUtilInspect ??= require('util').inspect;
22+
return coreUtilInspect(obj, {
23+
...options,
24+
// These arrays can be very large, so would prefer to use the default options instead.
25+
maxArrayLength: coreUtilInspect.defaultOptions.maxArrayLength,
26+
});
27+
} catch {
28+
const arr = Array.from(obj);
29+
if (arr.length > 100) {
30+
return `[${arr.slice(0, 100).join(', ')}, ... ${
31+
arr.length - 100
32+
} more items]`;
33+
}
34+
return `[${arr.join(', ')}]`;
35+
}
36+
}
37+
1038
// Turn e.g. 'new Double(...)' into 'Double(...)' but preserve possible leading whitespace
1139
function removeNewFromInspectResult(str: string): string {
1240
return str.replace(/^(\s*)(new )/, '$1');
@@ -43,30 +71,24 @@ const makeBinaryVectorInspect = (bsonLibrary: BSON): CustomInspectFunction => {
4371
switch (this.buffer[0]) {
4472
case bsonLibrary.Binary.VECTOR_TYPE.Int8:
4573
return `Binary.fromInt8Array(new Int8Array(${removeTypedArrayPrefixFromInspectResult(
46-
utilInspect(this.toInt8Array(), {
74+
inspectTypedArray(this.toInt8Array(), {
4775
depth,
4876
...options,
49-
// These arrays can be very large, so would prefer to use the default options instead.
50-
maxArrayLength: utilInspect.defaultOptions.maxArrayLength,
5177
})
5278
)}))`;
5379
case bsonLibrary.Binary.VECTOR_TYPE.Float32:
5480
return `Binary.fromFloat32Array(new Float32Array(${removeTypedArrayPrefixFromInspectResult(
55-
utilInspect(this.toFloat32Array(), {
81+
inspectTypedArray(this.toFloat32Array(), {
5682
depth,
5783
...options,
58-
// These arrays can be very large, so would prefer to use the default options instead.
59-
maxArrayLength: utilInspect.defaultOptions.maxArrayLength,
6084
})
6185
)}))`;
6286
case bsonLibrary.Binary.VECTOR_TYPE.PackedBit: {
6387
const paddingInfo = this.buffer[1] === 0 ? '' : `, ${this.buffer[1]}`;
6488
return `Binary.fromPackedBits(new Uint8Array(${removeTypedArrayPrefixFromInspectResult(
65-
utilInspect(this.toPackedBits(), {
89+
inspectTypedArray(this.toPackedBits(), {
6690
depth,
6791
...options,
68-
// These arrays can be very large, so would prefer to use the default options instead.
69-
maxArrayLength: utilInspect.defaultOptions.maxArrayLength,
7092
})
7193
)})${paddingInfo})`;
7294
}

packages/shell-bson/src/shell-bson.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
assignAll,
1111
pickWithExactKeyMatch,
1212
} from './helpers';
13-
import { randomBytes } from 'crypto';
1413

1514
type LongWithoutAccidentallyExposedMethods = Omit<
1615
typeof Long,
@@ -327,11 +326,10 @@ export function constructShellBson<
327326
UUID: assignAll(
328327
function UUID(hexstr?: string): BinaryType {
329328
if (hexstr === undefined) {
330-
// Generate a version 4, variant 1 UUID, like the old shell did.
331-
const uuid = randomBytes(16);
332-
uuid[6] = (uuid[6] & 0x0f) | 0x40;
333-
uuid[8] = (uuid[8] & 0x3f) | 0x80;
334-
hexstr = uuid.toString('hex');
329+
// TODO(MONGOSH-2710): Actually use UUID instances from `bson`
330+
// (but then also be consistent about that when we e.g. receive
331+
// them from the server).
332+
hexstr = new bson.UUID().toString();
335333
}
336334
assertArgsDefinedType([hexstr], ['string'], 'UUID');
337335
// Strip any dashes, as they occur in the standard UUID formatting

0 commit comments

Comments
 (0)