Skip to content

Commit 1595212

Browse files
authored
Merge pull request #18 from jsonjoy-com/copilot/fix-17
Complete BSON codec implementation with decoder and encoder improvements
2 parents 2982051 + 5c3ba1c commit 1595212

File tree

6 files changed

+658
-11
lines changed

6 files changed

+658
-11
lines changed

src/bson/BsonDecoder.ts

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import {Reader} from '@jsonjoy.com/util/lib/buffers/Reader';
2+
import {
3+
BsonBinary,
4+
BsonDbPointer,
5+
BsonDecimal128,
6+
BsonFloat,
7+
BsonInt32,
8+
BsonInt64,
9+
BsonJavascriptCode,
10+
BsonJavascriptCodeWithScope,
11+
BsonMaxKey,
12+
BsonMinKey,
13+
BsonObjectId,
14+
BsonTimestamp,
15+
} from './values';
16+
import type {IReader, IReaderResettable} from '@jsonjoy.com/util/lib/buffers';
17+
import type {BinaryJsonDecoder} from '../types';
18+
19+
export class BsonDecoder implements BinaryJsonDecoder {
20+
public constructor(public reader: IReader & IReaderResettable = new Reader()) {}
21+
22+
public read(uint8: Uint8Array): unknown {
23+
this.reader.reset(uint8);
24+
return this.readDocument();
25+
}
26+
27+
public decode(uint8: Uint8Array): unknown {
28+
this.reader.reset(uint8);
29+
return this.readDocument();
30+
}
31+
32+
public readDocument(): Record<string, unknown> {
33+
const reader = this.reader;
34+
const documentSize = reader.view.getInt32(reader.x, true); // true = little-endian
35+
reader.x += 4;
36+
const startPos = reader.x; // Position after reading the size
37+
const endPos = startPos + documentSize - 4 - 1; // End position before the terminating null
38+
const obj: Record<string, unknown> = {};
39+
40+
while (reader.x < endPos) {
41+
const elementType = reader.u8();
42+
if (elementType === 0) break; // End of document
43+
44+
const key = this.readCString();
45+
const value = this.readElementValue(elementType);
46+
obj[key] = value;
47+
}
48+
49+
// Skip to the end of document (including the terminating null if we haven't read it)
50+
if (reader.x <= endPos) {
51+
reader.x = startPos + documentSize - 4; // Move to just after the terminating null
52+
}
53+
54+
return obj;
55+
}
56+
57+
public readCString(): string {
58+
const reader = this.reader;
59+
const uint8 = reader.uint8;
60+
const x = reader.x;
61+
let length = 0;
62+
63+
// Find the null terminator
64+
while (uint8[x + length] !== 0) {
65+
length++;
66+
}
67+
68+
if (length === 0) {
69+
reader.x++; // Skip the null byte
70+
return '';
71+
}
72+
73+
const str = reader.utf8(length);
74+
reader.x++; // Skip the null terminator
75+
return str;
76+
}
77+
78+
public readString(): string {
79+
const reader = this.reader;
80+
const length = reader.view.getInt32(reader.x, true); // true = little-endian
81+
reader.x += 4;
82+
if (length <= 0) {
83+
throw new Error('Invalid string length');
84+
}
85+
const str = reader.utf8(length - 1); // Length includes null terminator
86+
reader.x++; // Skip null terminator
87+
return str;
88+
}
89+
90+
public readElementValue(type: number): unknown {
91+
const reader = this.reader;
92+
93+
switch (type) {
94+
case 0x01: // double - 64-bit binary floating point
95+
const doubleVal = reader.view.getFloat64(reader.x, true);
96+
reader.x += 8;
97+
return doubleVal;
98+
99+
case 0x02: // string - UTF-8 string
100+
return this.readString();
101+
102+
case 0x03: // document - Embedded document
103+
return this.readDocument();
104+
105+
case 0x04: // array - Array
106+
return this.readArray();
107+
108+
case 0x05: // binary - Binary data
109+
return this.readBinary();
110+
111+
case 0x06: // undefined (deprecated)
112+
return undefined;
113+
114+
case 0x07: // ObjectId
115+
return this.readObjectId();
116+
117+
case 0x08: // boolean
118+
return reader.u8() === 1;
119+
120+
case 0x09: // UTC datetime
121+
const dateVal = reader.view.getBigInt64(reader.x, true);
122+
reader.x += 8;
123+
return new Date(Number(dateVal));
124+
125+
case 0x0a: // null
126+
return null;
127+
128+
case 0x0b: // regex
129+
return this.readRegex();
130+
131+
case 0x0c: // DBPointer (deprecated)
132+
return this.readDbPointer();
133+
134+
case 0x0d: // JavaScript code
135+
return new BsonJavascriptCode(this.readString());
136+
137+
case 0x0e: // Symbol (deprecated)
138+
return Symbol(this.readString());
139+
140+
case 0x0f: // JavaScript code with scope (deprecated)
141+
return this.readCodeWithScope();
142+
143+
case 0x10: // 32-bit integer
144+
const int32Val = reader.view.getInt32(reader.x, true);
145+
reader.x += 4;
146+
return int32Val;
147+
148+
case 0x11: // Timestamp
149+
return this.readTimestamp();
150+
151+
case 0x12: // 64-bit integer
152+
const int64Val = reader.view.getBigInt64(reader.x, true);
153+
reader.x += 8;
154+
return Number(int64Val);
155+
156+
case 0x13: // 128-bit decimal floating point
157+
return this.readDecimal128();
158+
159+
case 0xff: // Min key
160+
return new BsonMinKey();
161+
162+
case 0x7f: // Max key
163+
return new BsonMaxKey();
164+
165+
default:
166+
throw new Error(`Unsupported BSON type: 0x${type.toString(16)}`);
167+
}
168+
}
169+
170+
public readArray(): unknown[] {
171+
const doc = this.readDocument() as Record<string, unknown>;
172+
const keys = Object.keys(doc).sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
173+
return keys.map((key) => doc[key]);
174+
}
175+
176+
public readBinary(): BsonBinary | Uint8Array {
177+
const reader = this.reader;
178+
const length = reader.view.getInt32(reader.x, true);
179+
reader.x += 4;
180+
const subtype = reader.u8();
181+
const data = reader.buf(length);
182+
183+
// For generic binary subtype, return Uint8Array for compatibility
184+
if (subtype === 0) {
185+
return data;
186+
}
187+
188+
return new BsonBinary(subtype, data);
189+
}
190+
191+
public readObjectId(): BsonObjectId {
192+
const reader = this.reader;
193+
const uint8 = reader.uint8;
194+
const x = reader.x;
195+
196+
// Timestamp (4 bytes, big-endian)
197+
const timestamp = (uint8[x] << 24) | (uint8[x + 1] << 16) | (uint8[x + 2] << 8) | uint8[x + 3];
198+
199+
// Process ID (5 bytes) - first 4 bytes are little-endian, then 1 high byte
200+
const processLo = uint8[x + 4] | (uint8[x + 5] << 8) | (uint8[x + 6] << 16) | (uint8[x + 7] << 24);
201+
const processHi = uint8[x + 8];
202+
// Convert to unsigned 32-bit first, then combine with high byte
203+
const processLoUnsigned = processLo >>> 0; // Convert to unsigned
204+
const process = processLoUnsigned + processHi * 0x100000000;
205+
206+
// Counter (3 bytes, big-endian)
207+
const counter = (uint8[x + 9] << 16) | (uint8[x + 10] << 8) | uint8[x + 11];
208+
209+
reader.x += 12;
210+
return new BsonObjectId(timestamp, process, counter);
211+
}
212+
213+
public readRegex(): RegExp {
214+
const pattern = this.readCString();
215+
const flags = this.readCString();
216+
return new RegExp(pattern, flags);
217+
}
218+
219+
public readDbPointer(): BsonDbPointer {
220+
const name = this.readString();
221+
const id = this.readObjectId();
222+
return new BsonDbPointer(name, id);
223+
}
224+
225+
public readCodeWithScope(): BsonJavascriptCodeWithScope {
226+
const reader = this.reader;
227+
const totalLength = reader.view.getInt32(reader.x, true);
228+
reader.x += 4;
229+
const code = this.readString();
230+
const scope = this.readDocument() as Record<string, unknown>;
231+
return new BsonJavascriptCodeWithScope(code, scope);
232+
}
233+
234+
public readTimestamp(): BsonTimestamp {
235+
const reader = this.reader;
236+
const increment = reader.view.getInt32(reader.x, true);
237+
reader.x += 4;
238+
const timestamp = reader.view.getInt32(reader.x, true);
239+
reader.x += 4;
240+
return new BsonTimestamp(increment, timestamp);
241+
}
242+
243+
public readDecimal128(): BsonDecimal128 {
244+
const reader = this.reader;
245+
const data = reader.buf(16);
246+
return new BsonDecimal128(data);
247+
}
248+
}

src/bson/BsonEncoder.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
BsonInt32,
77
BsonInt64,
88
BsonJavascriptCode,
9+
BsonJavascriptCodeWithScope,
910
BsonMaxKey,
1011
BsonMinKey,
1112
BsonObjectId,
@@ -35,27 +36,33 @@ export class BsonEncoder implements BinaryJsonEncoder {
3536
}
3637

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

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

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

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

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

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

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

7582
public writeFloat(float: number): void {
7683
const writer = this.writer;
77-
writer.ensureCapacity(4);
84+
writer.ensureCapacity(8);
7885
writer.view.setFloat64(writer.x, float, true);
7986
writer.x += 8;
8087
}
8188

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

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

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

112121
public writeArr(arr: unknown[]): void {
@@ -296,6 +305,18 @@ export class BsonEncoder implements BinaryJsonEncoder {
296305
this.writeStr((value as BsonJavascriptCode).code);
297306
break;
298307
}
308+
case BsonJavascriptCodeWithScope: {
309+
writer.u8(0x0f);
310+
this.writeCString(key);
311+
const codeWithScope = value as BsonJavascriptCodeWithScope;
312+
const x0 = writer.x;
313+
writer.x += 4; // Reserve space for total length
314+
this.writeStr(codeWithScope.code);
315+
this.writeObj(codeWithScope.scope);
316+
const totalLength = writer.x - x0;
317+
writer.view.setInt32(x0, totalLength, true);
318+
break;
319+
}
299320
case BsonInt32: {
300321
writer.u8(0x10);
301322
this.writeCString(key);
@@ -305,13 +326,13 @@ export class BsonEncoder implements BinaryJsonEncoder {
305326
case BsonInt64: {
306327
writer.u8(0x12);
307328
this.writeCString(key);
308-
this.writeInt64((value as BsonInt32).value);
329+
this.writeInt64((value as BsonInt64).value);
309330
break;
310331
}
311332
case BsonFloat: {
312333
writer.u8(0x01);
313334
this.writeCString(key);
314-
this.writeFloat((value as BsonInt32).value);
335+
this.writeFloat((value as BsonFloat).value);
315336
break;
316337
}
317338
case BsonTimestamp: {

0 commit comments

Comments
 (0)