Skip to content
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
248 changes: 248 additions & 0 deletions src/bson/BsonDecoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import {Reader} from '@jsonjoy.com/util/lib/buffers/Reader';
import {
BsonBinary,
BsonDbPointer,
BsonDecimal128,
BsonFloat,
BsonInt32,
BsonInt64,
BsonJavascriptCode,
BsonJavascriptCodeWithScope,
BsonMaxKey,
BsonMinKey,
BsonObjectId,
BsonTimestamp,
} from './values';
import type {IReader, IReaderResettable} from '@jsonjoy.com/util/lib/buffers';
import type {BinaryJsonDecoder} from '../types';

export class BsonDecoder implements BinaryJsonDecoder {
public constructor(public reader: IReader & IReaderResettable = new Reader()) {}

public read(uint8: Uint8Array): unknown {
this.reader.reset(uint8);
return this.readDocument();
}

public decode(uint8: Uint8Array): unknown {
this.reader.reset(uint8);
return this.readDocument();
}

public readDocument(): Record<string, unknown> {
const reader = this.reader;
const documentSize = reader.view.getInt32(reader.x, true); // true = little-endian
reader.x += 4;
const startPos = reader.x; // Position after reading the size
const endPos = startPos + documentSize - 4 - 1; // End position before the terminating null
const obj: Record<string, unknown> = {};

while (reader.x < endPos) {
const elementType = reader.u8();
if (elementType === 0) break; // End of document

const key = this.readCString();
const value = this.readElementValue(elementType);
obj[key] = value;
}

// Skip to the end of document (including the terminating null if we haven't read it)
if (reader.x <= endPos) {
reader.x = startPos + documentSize - 4; // Move to just after the terminating null
}

return obj;
}

public readCString(): string {
const reader = this.reader;
const uint8 = reader.uint8;
const x = reader.x;
let length = 0;

// Find the null terminator
while (uint8[x + length] !== 0) {
length++;
}

if (length === 0) {
reader.x++; // Skip the null byte
return '';
}

const str = reader.utf8(length);
reader.x++; // Skip the null terminator
return str;
}

public readString(): string {
const reader = this.reader;
const length = reader.view.getInt32(reader.x, true); // true = little-endian
reader.x += 4;
if (length <= 0) {
throw new Error('Invalid string length');
}
const str = reader.utf8(length - 1); // Length includes null terminator
reader.x++; // Skip null terminator
return str;
}

public readElementValue(type: number): unknown {
const reader = this.reader;

switch (type) {
case 0x01: // double - 64-bit binary floating point
const doubleVal = reader.view.getFloat64(reader.x, true);
reader.x += 8;
return doubleVal;

case 0x02: // string - UTF-8 string
return this.readString();

case 0x03: // document - Embedded document
return this.readDocument();

case 0x04: // array - Array
return this.readArray();

case 0x05: // binary - Binary data
return this.readBinary();

case 0x06: // undefined (deprecated)
return undefined;

case 0x07: // ObjectId
return this.readObjectId();

case 0x08: // boolean
return reader.u8() === 1;

case 0x09: // UTC datetime
const dateVal = reader.view.getBigInt64(reader.x, true);
reader.x += 8;
return new Date(Number(dateVal));

case 0x0a: // null
return null;

case 0x0b: // regex
return this.readRegex();

case 0x0c: // DBPointer (deprecated)
return this.readDbPointer();

case 0x0d: // JavaScript code
return new BsonJavascriptCode(this.readString());

case 0x0e: // Symbol (deprecated)
return Symbol(this.readString());

case 0x0f: // JavaScript code with scope (deprecated)
return this.readCodeWithScope();

case 0x10: // 32-bit integer
const int32Val = reader.view.getInt32(reader.x, true);
reader.x += 4;
return int32Val;

case 0x11: // Timestamp
return this.readTimestamp();

case 0x12: // 64-bit integer
const int64Val = reader.view.getBigInt64(reader.x, true);
reader.x += 8;
return Number(int64Val);

case 0x13: // 128-bit decimal floating point
return this.readDecimal128();

case 0xff: // Min key
return new BsonMinKey();

case 0x7f: // Max key
return new BsonMaxKey();

default:
throw new Error(`Unsupported BSON type: 0x${type.toString(16)}`);
}
}

public readArray(): unknown[] {
const doc = this.readDocument() as Record<string, unknown>;
const keys = Object.keys(doc).sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
return keys.map((key) => doc[key]);
}

public readBinary(): BsonBinary | Uint8Array {
const reader = this.reader;
const length = reader.view.getInt32(reader.x, true);
reader.x += 4;
const subtype = reader.u8();
const data = reader.buf(length);

// For generic binary subtype, return Uint8Array for compatibility
if (subtype === 0) {
return data;
}

return new BsonBinary(subtype, data);
}

public readObjectId(): BsonObjectId {
const reader = this.reader;
const uint8 = reader.uint8;
const x = reader.x;

// Timestamp (4 bytes, big-endian)
const timestamp = (uint8[x] << 24) | (uint8[x + 1] << 16) | (uint8[x + 2] << 8) | uint8[x + 3];

// Process ID (5 bytes) - first 4 bytes are little-endian, then 1 high byte
const processLo = uint8[x + 4] | (uint8[x + 5] << 8) | (uint8[x + 6] << 16) | (uint8[x + 7] << 24);
const processHi = uint8[x + 8];
// Convert to unsigned 32-bit first, then combine with high byte
const processLoUnsigned = processLo >>> 0; // Convert to unsigned
const process = processLoUnsigned + processHi * 0x100000000;

// Counter (3 bytes, big-endian)
const counter = (uint8[x + 9] << 16) | (uint8[x + 10] << 8) | uint8[x + 11];

reader.x += 12;
return new BsonObjectId(timestamp, process, counter);
}

public readRegex(): RegExp {
const pattern = this.readCString();
const flags = this.readCString();
return new RegExp(pattern, flags);
}

public readDbPointer(): BsonDbPointer {
const name = this.readString();
const id = this.readObjectId();
return new BsonDbPointer(name, id);
}

public readCodeWithScope(): BsonJavascriptCodeWithScope {
const reader = this.reader;
const totalLength = reader.view.getInt32(reader.x, true);
reader.x += 4;
const code = this.readString();
const scope = this.readDocument() as Record<string, unknown>;
return new BsonJavascriptCodeWithScope(code, scope);
}

public readTimestamp(): BsonTimestamp {
const reader = this.reader;
const increment = reader.view.getInt32(reader.x, true);
reader.x += 4;
const timestamp = reader.view.getInt32(reader.x, true);
reader.x += 4;
return new BsonTimestamp(increment, timestamp);
}

public readDecimal128(): BsonDecimal128 {
const reader = this.reader;
const data = reader.buf(16);
return new BsonDecimal128(data);
}
}
43 changes: 32 additions & 11 deletions src/bson/BsonEncoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
BsonInt32,
BsonInt64,
BsonJavascriptCode,
BsonJavascriptCodeWithScope,
BsonMaxKey,
BsonMinKey,
BsonObjectId,
Expand Down Expand Up @@ -35,27 +36,33 @@ export class BsonEncoder implements BinaryJsonEncoder {
}

public writeNull(): void {
throw new Error('Method not implemented.');
// Not used directly in BSON - handled in writeKey
throw new Error('Use writeKey for BSON encoding');
}

public writeUndef(): void {
throw new Error('Method not implemented.');
// Not used directly in BSON - handled in writeKey
throw new Error('Use writeKey for BSON encoding');
}

public writeBoolean(bool: boolean): void {
throw new Error('Method not implemented.');
// Not used directly in BSON - handled in writeKey
throw new Error('Use writeKey for BSON encoding');
}

public writeNumber(num: number): void {
throw new Error('Method not implemented.');
// Not used directly in BSON - handled in writeKey
throw new Error('Use writeKey for BSON encoding');
}

public writeInteger(int: number): void {
throw new Error('Method not implemented.');
// Not used directly in BSON - handled in writeKey
throw new Error('Use writeKey for BSON encoding');
}

public writeUInteger(uint: number): void {
throw new Error('Method not implemented.');
// Not used directly in BSON - handled in writeKey
throw new Error('Use writeKey for BSON encoding');
}

public writeInt32(int: number): void {
Expand All @@ -74,13 +81,14 @@ export class BsonEncoder implements BinaryJsonEncoder {

public writeFloat(float: number): void {
const writer = this.writer;
writer.ensureCapacity(4);
writer.ensureCapacity(8);
writer.view.setFloat64(writer.x, float, true);
writer.x += 8;
}

public writeBigInt(int: bigint): void {
throw new Error('Method not implemented.');
// Not used directly in BSON - handled in writeKey
throw new Error('Use writeKey for BSON encoding');
}

public writeBin(buf: Uint8Array): void {
Expand All @@ -106,7 +114,8 @@ export class BsonEncoder implements BinaryJsonEncoder {
}

public writeAsciiStr(str: string): void {
throw new Error('Method not implemented.');
// Use writeStr for BSON - it handles UTF-8 properly
this.writeStr(str);
}

public writeArr(arr: unknown[]): void {
Expand Down Expand Up @@ -296,6 +305,18 @@ export class BsonEncoder implements BinaryJsonEncoder {
this.writeStr((value as BsonJavascriptCode).code);
break;
}
case BsonJavascriptCodeWithScope: {
writer.u8(0x0f);
this.writeCString(key);
const codeWithScope = value as BsonJavascriptCodeWithScope;
const x0 = writer.x;
writer.x += 4; // Reserve space for total length
this.writeStr(codeWithScope.code);
this.writeObj(codeWithScope.scope);
const totalLength = writer.x - x0;
writer.view.setInt32(x0, totalLength, true);
break;
}
case BsonInt32: {
writer.u8(0x10);
this.writeCString(key);
Expand All @@ -305,13 +326,13 @@ export class BsonEncoder implements BinaryJsonEncoder {
case BsonInt64: {
writer.u8(0x12);
this.writeCString(key);
this.writeInt64((value as BsonInt32).value);
this.writeInt64((value as BsonInt64).value);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch.

break;
}
case BsonFloat: {
writer.u8(0x01);
this.writeCString(key);
this.writeFloat((value as BsonInt32).value);
this.writeFloat((value as BsonFloat).value);
break;
}
case BsonTimestamp: {
Expand Down
Loading