Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ test/lambda/env.json

# files generated by tooling in drivers-evergreen-tools
secrets-export.sh
secrets-export.fish
mo-expansion.sh
mo-expansion.yml
expansions.sh
Expand Down
39 changes: 39 additions & 0 deletions etc/bash_to_fish.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { createReadStream, promises as fs } from 'node:fs';
import path from 'node:path';
import readline from 'node:readline/promises';

/**
* Takes an "exports" only bash script file
* and converts it to fish syntax.
* Will crash on any line that isn't:
* - a comment
* - an empty line
* - a bash 'set' call
* - export VAR=VAL
*/

const fileName = process.argv[2];
const outFileName = path.basename(fileName, '.sh') + '.fish';
const input = createReadStream(process.argv[2]);
const lines = readline.createInterface({ input });
const output = await fs.open(outFileName, 'w');

for await (let line of lines) {
line = line.trim();

if (!line.startsWith('export ')) {
if (line.startsWith('#')) continue;
if (line === '') continue;
if (line.startsWith('set')) continue;
throw new Error('Cannot translate: ' + line);
}

const varVal = line.slice('export '.length);
const variable = varVal.slice(0, varVal.indexOf('='));
const value = varVal.slice(varVal.indexOf('=') + 1);
await output.appendFile(`set -x ${variable} ${value}\n`);
}

output.close();
input.close();
lines.close();
2 changes: 2 additions & 0 deletions src/client-side-encryption/auto_encrypter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ export class AutoEncrypter {
this._kmsProviders = options.kmsProviders || {};

const mongoCryptOptions: MongoCryptOptions = {
//@ts-expect-error: not yet in the defs
enableMultipleCollinfo: true,
cryptoCallbacks
};
if (options.schemaMap) {
Expand Down
53 changes: 31 additions & 22 deletions src/client-side-encryption/state_machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { getSocks, type SocksLib } from '../deps';
import { MongoOperationTimeoutError } from '../error';
import { type MongoClient, type MongoClientOptions } from '../mongo_client';
import { type Abortable } from '../mongo_types';
import { type CollectionInfo } from '../operations/list_collections';
import { Timeout, type TimeoutContext, TimeoutError } from '../timeout';
import {
addAbortListener,
Expand Down Expand Up @@ -205,11 +206,19 @@ export class StateMachine {
const mongocryptdManager = executor._mongocryptdManager;
let result: Uint8Array | null = null;

while (context.state !== MONGOCRYPT_CTX_DONE && context.state !== MONGOCRYPT_CTX_ERROR) {
// Typescript treats getters just like properties: Once you've tested it for equality
// it cannot change. Which is exactly the opposite of what we use state and status for.
// Every call to at least `addMongoOperationResponse` and `finalize` can change the state.
// These wrappers let us write code more naturally and not add compiler exceptions
// to conditions checks inside the state machine.
const getStatus = () => context.status;
const getState = () => context.state;

while (getState() !== MONGOCRYPT_CTX_DONE && getState() !== MONGOCRYPT_CTX_ERROR) {
options.signal?.throwIfAborted();
debug(`[context#${context.id}] ${stateToString.get(context.state) || context.state}`);
debug(`[context#${context.id}] ${stateToString.get(getState()) || getState()}`);

switch (context.state) {
switch (getState()) {
case MONGOCRYPT_CTX_NEED_MONGO_COLLINFO: {
const filter = deserialize(context.nextMongoOperation());
if (!metaDataClient) {
Expand All @@ -218,22 +227,28 @@ export class StateMachine {
);
}

const collInfo = await this.fetchCollectionInfo(
const collInfoCursor = this.fetchCollectionInfo(
metaDataClient,
context.ns,
filter,
options
);
if (collInfo) {
context.addMongoOperationResponse(collInfo);

for await (const collInfo of collInfoCursor) {
context.addMongoOperationResponse(serialize(collInfo));
if (getState() === MONGOCRYPT_CTX_ERROR) break;
}

if (getState() === MONGOCRYPT_CTX_ERROR) break;

context.finishMongoOperation();
break;
}

case MONGOCRYPT_CTX_NEED_MONGO_MARKINGS: {
const command = context.nextMongoOperation();
if (getState() === MONGOCRYPT_CTX_ERROR) break;

if (!mongocryptdClient) {
throw new MongoCryptError(
'unreachable state machine state: entered MONGOCRYPT_CTX_NEED_MONGO_MARKINGS but mongocryptdClient is undefined'
Expand Down Expand Up @@ -283,22 +298,21 @@ export class StateMachine {

case MONGOCRYPT_CTX_READY: {
const finalizedContext = context.finalize();
// @ts-expect-error finalize can change the state, check for error
if (context.state === MONGOCRYPT_CTX_ERROR) {
const message = context.status.message || 'Finalization error';
if (getState() === MONGOCRYPT_CTX_ERROR) {
const message = getStatus().message || 'Finalization error';
throw new MongoCryptError(message);
}
result = finalizedContext;
break;
}

default:
throw new MongoCryptError(`Unknown state: ${context.state}`);
throw new MongoCryptError(`Unknown state: ${getState()}`);
}
}

if (context.state === MONGOCRYPT_CTX_ERROR || result == null) {
const message = context.status.message;
if (getState() === MONGOCRYPT_CTX_ERROR || result == null) {
const message = getStatus().message;
if (!message) {
debug(
`unidentifiable error in MongoCrypt - received an error status from \`libmongocrypt\` but received no error message.`
Expand Down Expand Up @@ -527,29 +541,24 @@ export class StateMachine {
* @param filter - A filter for the listCollections command
* @param callback - Invoked with the info of the requested collection, or with an error
*/
async fetchCollectionInfo(
fetchCollectionInfo(
client: MongoClient,
ns: string,
filter: Document,
options?: { timeoutContext?: TimeoutContext } & Abortable
): Promise<Uint8Array | null> {
): AsyncIterable<CollectionInfo> {
const { db } = MongoDBCollectionNamespace.fromString(ns);

const cursor = client.db(db).listCollections(filter, {
promoteLongs: false,
promoteValues: false,
timeoutContext:
options?.timeoutContext && new CursorTimeoutContext(options?.timeoutContext, Symbol()),
signal: options?.signal
signal: options?.signal,
nameOnly: false
});

// There is always exactly zero or one matching documents, so this should always exhaust the cursor
// in a single batch. We call `toArray()` just to be safe and ensure that the cursor is always
// exhausted and closed.
const collections = await cursor.toArray();

const info = collections.length > 0 ? serialize(collections[0]) : null;
return info;
return cursor;
}

/**
Expand Down
Loading