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-5958): add BSON iterating API #656

Merged
merged 8 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all 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 .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-enum-comparison": "off",
"@typescript-eslint/consistent-type-imports": [
"error",
{
Expand Down
2 changes: 1 addition & 1 deletion src/bson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export { BSONValue } from './bson_value';
export { BSONError, BSONVersionError, BSONRuntimeError } from './error';
export { BSONType } from './constants';
export { EJSON } from './extended_json';
export { onDemand } from './parser/on_demand/index';
export { onDemand, type OnDemand } from './parser/on_demand/index';

/** @public */
export interface Document {
Expand Down
17 changes: 17 additions & 0 deletions src/parser/on_demand/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type BSONError, BSONOffsetError } from '../../error';
import { type BSONElement, parseToElements } from './parse_to_elements';
import { type BSONReviver, type Container, parseToStructure } from './parse_to_structure';
/**
* @experimental
* @public
Expand All @@ -12,6 +13,21 @@ export type OnDemand = {
isBSONError(value: unknown): value is BSONError;
};
parseToElements: (this: void, bytes: Uint8Array, startOffset?: number) => Iterable<BSONElement>;
parseToStructure: <
TRoot extends Container = {
dest: Record<string, unknown>;
kind: 'object';
}
>(
bytes: Uint8Array,
startOffset?: number,
root?: TRoot,
reviver?: BSONReviver
) => TRoot extends undefined ? Record<string, unknown> : TRoot['dest'];
// Types
BSONElement: BSONElement;
Container: Container;
BSONReviver: BSONReviver;
};

/**
Expand All @@ -21,6 +37,7 @@ export type OnDemand = {
const onDemand: OnDemand = Object.create(null);

onDemand.parseToElements = parseToElements;
onDemand.parseToStructure = parseToStructure;
onDemand.BSONOffsetError = BSONOffsetError;

Object.freeze(onDemand);
Expand Down
61 changes: 41 additions & 20 deletions src/parser/on_demand/parse_to_elements.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
import { BSONOffsetError } from '../../error';

/**
Expand All @@ -9,7 +8,7 @@ import { BSONOffsetError } from '../../error';
* - `minKey` is set to 255 so unsigned comparisons succeed
* - Modify with caution, double check the bundle contains literals
*/
const enum t {
const enum BSONElementType {
double = 1,
string = 2,
object = 3,
Expand Down Expand Up @@ -45,8 +44,11 @@ export type BSONElement = [
length: number
];

/** Parses a int32 little-endian at offset, throws if it is negative */
function getSize(source: Uint8Array, offset: number): number {
/**
* @internal
* Parses a int32 little-endian at offset, throws if it is negative
*/
export function getSize(source: Uint8Array, offset: number): number {
if (source[offset + 3] > 127) {
throw new BSONOffsetError('BSON size cannot be negative', offset);
}
Expand Down Expand Up @@ -80,7 +82,12 @@ function findNull(bytes: Uint8Array, offset: number): number {
* @public
* @experimental
*/
export function parseToElements(bytes: Uint8Array, startOffset = 0): Iterable<BSONElement> {
export function parseToElements(
bytes: Uint8Array,
startOffset: number | null = 0
): Iterable<BSONElement> {
startOffset ??= 0;

if (bytes.length < 5) {
throw new BSONOffsetError(
`Input must be at least 5 bytes, got ${bytes.length} bytes`,
Expand Down Expand Up @@ -121,37 +128,51 @@ export function parseToElements(bytes: Uint8Array, startOffset = 0): Iterable<BS

let length: number;

if (type === t.double || type === t.long || type === t.date || type === t.timestamp) {
if (
type === BSONElementType.double ||
type === BSONElementType.long ||
type === BSONElementType.date ||
type === BSONElementType.timestamp
) {
length = 8;
} else if (type === t.int) {
} else if (type === BSONElementType.int) {
length = 4;
} else if (type === t.objectId) {
} else if (type === BSONElementType.objectId) {
length = 12;
} else if (type === t.decimal) {
} else if (type === BSONElementType.decimal) {
length = 16;
} else if (type === t.bool) {
} else if (type === BSONElementType.bool) {
length = 1;
} else if (type === t.null || type === t.undefined || type === t.maxKey || type === t.minKey) {
} else if (
type === BSONElementType.null ||
type === BSONElementType.undefined ||
type === BSONElementType.maxKey ||
type === BSONElementType.minKey
) {
length = 0;
}
// Needs a size calculation
else if (type === t.regex) {
else if (type === BSONElementType.regex) {
length = findNull(bytes, findNull(bytes, offset) + 1) + 1 - offset;
} else if (type === t.object || type === t.array || type === t.javascriptWithScope) {
} else if (
type === BSONElementType.object ||
type === BSONElementType.array ||
type === BSONElementType.javascriptWithScope
) {
length = getSize(bytes, offset);
} else if (
type === t.string ||
type === t.binData ||
type === t.dbPointer ||
type === t.javascript ||
type === t.symbol
type === BSONElementType.string ||
type === BSONElementType.binData ||
type === BSONElementType.dbPointer ||
type === BSONElementType.javascript ||
type === BSONElementType.symbol
) {
length = getSize(bytes, offset) + 4;
if (type === t.binData) {
if (type === BSONElementType.binData) {
// binary subtype
length += 1;
}
if (type === t.dbPointer) {
if (type === BSONElementType.dbPointer) {
// dbPointer's objectId
length += 12;
}
Expand Down
145 changes: 145 additions & 0 deletions src/parser/on_demand/parse_to_structure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { type Code } from '../../code';
import { type BSONElement, getSize, parseToElements } from './parse_to_elements';

/** @internal */
const DEFAULT_REVIVER: BSONReviver = (
_bytes: Uint8Array,
_container: Container,
_element: BSONElement
) => null;

/** @internal */
function parseToElementsToArray(bytes: Uint8Array, offset?: number | null): BSONElement[] {
const res = parseToElements(bytes, offset);
return Array.isArray(res) ? res : [...res];
}

/** @internal */
type ParseContext = {
elementOffset: number;
elements: BSONElement[];
container: Container;
previous: ParseContext | null;
};

/**
* @experimental
* @public
* A union of the possible containers for BSON elements.
*
* Depending on kind, a reviver can accurately assign a value to a name on the container.
*/
export type Container =
| {
dest: Record<string, unknown>;
kind: 'object';
}
| {
dest: Map<string, unknown>;
kind: 'map';
}
| {
dest: Array<unknown>;
kind: 'array';
}
| {
dest: Code;
kind: 'code';
}
| {
kind: 'custom';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
dest: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
};
baileympearson marked this conversation as resolved.
Show resolved Hide resolved

/**
* @experimental
* @public
*/
export type BSONReviver = (
bytes: Uint8Array,
container: Container,
element: BSONElement
) => Container | null;

/**
* @experimental
* @public
*/
export function parseToStructure<
TRoot extends Container = {
dest: Record<string, unknown>;
kind: 'object';
}
>(
bytes: Uint8Array,
startOffset?: number | null,
pRoot?: TRoot | null,
pReviver?: BSONReviver | null
baileympearson marked this conversation as resolved.
Show resolved Hide resolved
): TRoot extends undefined ? Record<string, unknown> : TRoot['dest'] {
const root = pRoot ?? {
kind: 'object',
dest: Object.create(null) as Record<string, unknown>
};

const reviver = pReviver ?? DEFAULT_REVIVER;

let ctx: ParseContext | null = {
elementOffset: 0,
elements: parseToElementsToArray(bytes, startOffset),
container: root,
previous: null
};

/** BSONElement offsets: type indicator and value offset */
const enum BSONElementOffset {
type = 0,
offset = 3
}

/** BSON Embedded types */
const enum BSONElementType {
object = 3,
array = 4,
javascriptWithScope = 15
}

embedded: while (ctx !== null) {
for (
let bsonElement: BSONElement | undefined = ctx.elements[ctx.elementOffset++];
bsonElement != null;
bsonElement = ctx.elements[ctx.elementOffset++]
) {
const type = bsonElement[BSONElementOffset.type];
const offset = bsonElement[BSONElementOffset.offset];

const container = reviver(bytes, ctx.container, bsonElement);
const isEmbeddedType =
type === BSONElementType.object ||
type === BSONElementType.array ||
type === BSONElementType.javascriptWithScope;

if (container != null && isEmbeddedType) {
const docOffset: number =
type !== BSONElementType.javascriptWithScope
? offset
: // value offset + codeSize + value int + code int
offset + getSize(bytes, offset + 4) + 4 + 4;

ctx = {
elementOffset: 0,
elements: parseToElementsToArray(bytes, docOffset),
container,
previous: ctx
};

continue embedded;
}
}
ctx = ctx.previous;
}

return root.dest;
}
Loading