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+ let 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 ) - parseInt ( b ) ) ;
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+ }
0 commit comments