1 /// reduced to ~ 410 LOCs (parser only 300 vs. 1400+) with (some, needed) BSON classes "inlined".
2 /// Compare ~ 4,300 (22KB vs. 157KB) in browser build at: https://github.com/mongodb/js-bson/blob/master/browser_build/bson.js
4 module.exports.calculateObjectSize = calculateObjectSize;
6 function calculateObjectSize(object) {
7 var totalLength = (4 + 1); /// handles the obj.length prefix + terminating '0' ?!
8 for(var key in object) { /// looks like it handles arrays under the same for...in loop!?
9 totalLength += calculateElement(key, object[key])
14 function calculateElement(name, value) {
15 var len = 1; /// always starting with 1 for the data type byte!
16 if (name) len += Buffer.byteLength(name, 'utf8') + 1; /// cstring: name + '0' termination
18 if (value === undefined || value === null) return len; /// just the type byte plus name cstring
19 switch( value.constructor ) { /// removed all checks 'isBuffer' if Node.js Buffer class is present!?
21 case ObjectID: /// we want these sorted from most common case to least common/deprecated;
24 return len + 4 + Buffer.byteLength(value, 'utf8') +1; ///
26 if (Math.floor(value) === value) { /// case: integer; pos.# more common, '&&' stops if 1st fails!
27 if ( value <= 2147483647 && value >= -2147483647 ) // 32 bit
29 else return len + 8; /// covers Long-ish JS integers as Longs!
30 } else return len + 8; /// 8+1 --- covers Double & std. float
36 return len + calculateObjectSize(value);
38 case Buffer: /// replaces the entire Binary class!
39 return len + 4 + value.length + 1;
41 case Regex: /// these are handled as strings by serializeFast() later, hence 'gim' opts = 3 + 1 chars
42 return len + Buffer.byteLength(value.source, 'utf8') + 1
43 + (value.global ? 1 : 0) + (value.ignoreCase ? 1 : 0) + (value.multiline ? 1 : 0) +1;
52 return len; /// these two return the type byte and name cstring only!
57 module.exports.serializeFast = serializeFast;
58 module.exports.serialize = function(object, checkKeys, asBuffer, serializeFunctions, index) {
59 var buffer = new Buffer(calculateObjectSize(object));
60 return serializeFast(object, checkKeys, buffer, 0);
63 function serializeFast(object, checkKeys, buffer, i) { /// set checkKeys = false in query(..., options object to save performance IFF you're certain your keys are safe/system-set!
64 var size = buffer.length;
65 buffer[i++] = size & 0xff; buffer[i++] = (size >> 8) & 0xff; /// these get overwritten later!
66 buffer[i++] = (size >> 16) & 0xff; buffer[i++] = (size >> 24) & 0xff;
68 if (object.constructor === Array) { /// any need to checkKeys here?!? since we're doing for rather than for...in, should be safe from extra (non-numeric) keys added to the array?!
69 for(var j = 0; j < object.length; j++) {
70 i = packElement(j.toString(), object[j], checkKeys, buffer, i);
72 } else { /// checkKeys is needed if any suspicion of end-user key tampering/"injection" (a la SQL)
73 for(var key in object) { /// mostly there should never be direct access to them!?
74 if (checkKeys && (key.indexOf('\x00') >= 0 || key === '$where') ) { /// = "no script"?!; could add back key.indexOf('$') or maybe check for 'eval'?!
75 /// took out: || key.indexOf('.') >= 0... Don't we allow dot notation queries?!
76 console.log('checkKeys error: ');
77 return new Error('Illegal object key!');
79 i = packElement(key, object[key], checkKeys, buffer, i); /// checkKeys pass needed for recursion!
82 buffer[i++] = 0; /// write terminating zero; !we do NOT -1 the index increase here as original does!
86 function packElement(name, value, checkKeys, buffer, i) { /// serializeFunctions removed! checkKeys needed for Array & Object cases pass through (calling serializeFast recursively!)
87 if (value === undefined || value === null){
88 buffer[i++] = 10; /// = BSON.BSON_DATA_NULL;
89 i += buffer.write(name, i, 'utf8'); buffer[i++] = 0; /// buffer.write(...) returns bytesWritten!
92 switch(value.constructor) {
95 buffer[i++] = 7; /// = BSON.BSON_DATA_OID;
96 i += buffer.write(name, i, 'utf8'); buffer[i++] = 0;
97 /// i += buffer.write(value.id, i, 'binary'); /// OLD: writes a String to a Buffer; 'binary' deprecated!!
98 value.id.copy(buffer, i); /// NEW ObjectID version has this.id = Buffer at the ready!
102 buffer[i++] = 2; /// = BSON.BSON_DATA_STRING;
103 i += buffer.write(name, i, 'utf8'); buffer[i++] = 0;
105 var size = Buffer.byteLength(value) + 1; /// includes the terminating '0'!?
106 buffer[i++] = size & 0xff; buffer[i++] = (size >> 8) & 0xff;
107 buffer[i++] = (size >> 16) & 0xff; buffer[i++] = (size >> 24) & 0xff;
109 i += buffer.write(value, i, 'utf8'); buffer[i++] = 0;
113 if ( ~~(value) === value) { /// double-Tilde is equiv. to Math.floor(value)
114 if ( value <= 2147483647 && value >= -2147483647){ /// = BSON.BSON_INT32_MAX / MIN asf.
115 buffer[i++] = 16; /// = BSON.BSON_DATA_INT;
116 i += buffer.write(name, i, 'utf8'); buffer[i++] = 0;
117 buffer[i++] = value & 0xff; buffer[i++] = (value >> 8) & 0xff;
118 buffer[i++] = (value >> 16) & 0xff; buffer[i++] = (value >> 24) & 0xff;
120 // Else large-ish JS int!? to Long!?
121 } else { /// if (value <= BSON.JS_INT_MAX && value >= BSON.JS_INT_MIN){ /// 9007199254740992 asf.
122 buffer[i++] = 18; /// = BSON.BSON_DATA_LONG;
123 i += buffer.write(name, i, 'utf8'); buffer[i++] = 0;
124 var lowBits = ( value % 4294967296 ) | 0, highBits = ( value / 4294967296 ) | 0;
126 buffer[i++] = lowBits & 0xff; buffer[i++] = (lowBits >> 8) & 0xff;
127 buffer[i++] = (lowBits >> 16) & 0xff; buffer[i++] = (lowBits >> 24) & 0xff;
128 buffer[i++] = highBits & 0xff; buffer[i++] = (highBits >> 8) & 0xff;
129 buffer[i++] = (highBits >> 16) & 0xff; buffer[i++] = (highBits >> 24) & 0xff;
131 } else { /// we have a float / Double
132 buffer[i++] = 1; /// = BSON.BSON_DATA_NUMBER;
133 i += buffer.write(name, i, 'utf8'); buffer[i++] = 0;
134 /// OLD: writeIEEE754(buffer, value, i, 'little', 52, 8);
135 buffer.writeDoubleLE(value, i); i += 8;
140 buffer[i++] = 8; /// = BSON.BSON_DATA_BOOLEAN;
141 i += buffer.write(name, i, 'utf8'); buffer[i++] = 0;
142 buffer[i++] = value ? 1 : 0;
147 buffer[i++] = value.constructor === Array ? 4 : 3; /// = BSON.BSON_DATA_ARRAY / _OBJECT;
148 i += buffer.write(name, i, 'utf8'); buffer[i++] = 0;
150 var endIndex = serializeFast(value, checkKeys, buffer, i); /// + 4); no longer needed b/c serializeFast writes a temp 4 bytes for length
151 var size = endIndex - i;
152 buffer[i++] = size & 0xff; buffer[i++] = (size >> 8) & 0xff;
153 buffer[i++] = (size >> 16) & 0xff; buffer[i++] = (size >> 24) & 0xff;
156 /// case Binary: /// is basically identical unless special/deprecated options!
157 case Buffer: /// solves ALL of our Binary needs without the BSON.Binary class!?
158 buffer[i++] = 5; /// = BSON.BSON_DATA_BINARY;
159 i += buffer.write(name, i, 'utf8'); buffer[i++] = 0;
160 var size = value.length;
161 buffer[i++] = size & 0xff; buffer[i++] = (size >> 8) & 0xff;
162 buffer[i++] = (size >> 16) & 0xff; buffer[i++] = (size >> 24) & 0xff;
164 buffer[i++] = 0; /// write BSON.BSON_BINARY_SUBTYPE_DEFAULT;
165 value.copy(buffer, i); ///, 0, size); << defaults to sourceStart=0, sourceEnd=sourceBuffer.length);
170 buffer[i++] = 11; /// = BSON.BSON_DATA_REGEXP;
171 i += buffer.write(name, i, 'utf8'); buffer[i++] = 0;
172 i += buffer.write(value.source, i, 'utf8'); buffer[i++] = 0x00;
174 if (value.global) buffer[i++] = 0x73; // s = 'g' for JS Regex!
175 if (value.ignoreCase) buffer[i++] = 0x69; // i
176 if (value.multiline) buffer[i++] = 0x6d; // m
181 buffer[i++] = 9; /// = BSON.BSON_DATA_DATE;
182 i += buffer.write(name, i, 'utf8'); buffer[i++] = 0;
183 var millis = value.getTime();
184 var lowBits = ( millis % 4294967296 ) | 0, highBits = ( millis / 4294967296 ) | 0;
186 buffer[i++] = lowBits & 0xff; buffer[i++] = (lowBits >> 8) & 0xff;
187 buffer[i++] = (lowBits >> 16) & 0xff; buffer[i++] = (lowBits >> 24) & 0xff;
188 buffer[i++] = highBits & 0xff; buffer[i++] = (highBits >> 8) & 0xff;
189 buffer[i++] = (highBits >> 16) & 0xff; buffer[i++] = (highBits >> 24) & 0xff;
194 buffer[i++] = value.constructor === Long ? 18 : 17; /// = BSON.BSON_DATA_LONG / _TIMESTAMP
195 i += buffer.write(name, i, 'utf8'); buffer[i++] = 0;
196 var lowBits = value.getLowBits(), highBits = value.getHighBits();
198 buffer[i++] = lowBits & 0xff; buffer[i++] = (lowBits >> 8) & 0xff;
199 buffer[i++] = (lowBits >> 16) & 0xff; buffer[i++] = (lowBits >> 24) & 0xff;
200 buffer[i++] = highBits & 0xff; buffer[i++] = (highBits >> 8) & 0xff;
201 buffer[i++] = (highBits >> 16) & 0xff; buffer[i++] = (highBits >> 24) & 0xff;
205 buffer[i++] = 1; /// = BSON.BSON_DATA_NUMBER;
206 i += buffer.write(name, i, 'utf8'); buffer[i++] = 0;
207 /// OLD: writeIEEE754(buffer, value, i, 'little', 52, 8); i += 8;
208 buffer.writeDoubleLE(value, i); i += 8;
211 case MinKey: /// = BSON.BSON_DATA_MINKEY;
212 buffer[i++] = 127; i += buffer.write(name, i, 'utf8'); buffer[i++] = 0;
214 case MaxKey: /// = BSON.BSON_DATA_MAXKEY;
215 buffer[i++] = 255; i += buffer.write(name, i, 'utf8'); buffer[i++] = 0;
219 return i; /// ?! If no value to serialize
223 module.exports.deserializeFast = deserializeFast;
225 function deserializeFast(buffer, i, isArray){ //// , options, isArray) { //// no more options!
226 if (buffer.length < 5) return new Error('Corrupt bson message < 5 bytes long'); /// from 'throw'
227 var elementType, tempindex = 0, name;
228 var string, low, high; /// = lowBits / highBits
229 /// using 'i' as the index to keep the lines shorter:
230 i || ( i = 0 ); /// for parseResponse it's 0; set to running index in deserialize(object/array) recursion
231 var object = isArray ? [] : {}; /// needed for type ARRAY recursion later!
232 var size = buffer[i++] | buffer[i++] << 8 | buffer[i++] << 16 | buffer[i++] << 24;
233 if(size < 5 || size > buffer.length) return new Error('Corrupt BSON message');
234 /// 'size' var was not used by anything after this, so we can reuse it
236 while(true) { // While we have more left data left keep parsing
237 elementType = buffer[i++]; // Read the type
238 if (elementType === 0) break; // If we get a zero it's the last byte, exit
240 tempindex = i; /// inlined readCStyleString & removed extra i<buffer.length check slowing EACH loop!
241 while( buffer[tempindex] !== 0x00 ) tempindex++; /// read ahead w/out changing main 'i' index
242 if (tempindex >= buffer.length) return new Error('Corrupt BSON document: illegal CString')
243 name = buffer.toString('utf8', i, tempindex);
244 i = tempindex + 1; /// Update index position to after the string + '0' termination
246 switch(elementType) {
248 case 7: /// = BSON.BSON_DATA_OID:
249 var buf = new Buffer(12);
250 buffer.copy(buf, 0, i, i += 12 ); /// copy 12 bytes from the current 'i' offset into fresh Buffer
251 object[name] = new ObjectID(buf); ///... & attach to the new ObjectID instance
254 case 2: /// = BSON.BSON_DATA_STRING:
255 size = buffer[i++] | buffer[i++] <<8 | buffer[i++] <<16 | buffer[i++] <<24;
256 object[name] = buffer.toString('utf8', i, i += size -1 );
257 i++; break; /// need to get the '0' index "tick-forward" back!
259 case 16: /// = BSON.BSON_DATA_INT: // Decode the 32bit value
260 object[name] = buffer[i++] | buffer[i++] << 8 | buffer[i++] << 16 | buffer[i++] << 24; break;
262 case 1: /// = BSON.BSON_DATA_NUMBER: // Decode the double value
263 object[name] = buffer.readDoubleLE(i); /// slightly faster depending on dec.points; a LOT cleaner
264 /// OLD: object[name] = readIEEE754(buffer, i, 'little', 52, 8);
267 case 8: /// = BSON.BSON_DATA_BOOLEAN:
268 object[name] = buffer[i++] == 1; break;
270 case 6: /// = BSON.BSON_DATA_UNDEFINED: /// deprecated
271 case 10: /// = BSON.BSON_DATA_NULL:
272 object[name] = null; break;
274 case 4: /// = BSON.BSON_DATA_ARRAY
275 size = buffer[i] | buffer[i+1] <<8 | buffer[i+2] <<16 | buffer[i+3] <<24; /// NO 'i' increment since the size bytes are reread during the recursion!
276 object[name] = deserializeFast(buffer, i, true ); /// pass current index & set isArray = true
278 case 3: /// = BSON.BSON_DATA_OBJECT:
279 size = buffer[i] | buffer[i+1] <<8 | buffer[i+2] <<16 | buffer[i+3] <<24;
280 object[name] = deserializeFast(buffer, i, false ); /// isArray = false => Object
283 case 5: /// = BSON.BSON_DATA_BINARY: // Decode the size of the binary blob
284 size = buffer[i++] | buffer[i++] << 8 | buffer[i++] << 16 | buffer[i++] << 24;
285 buffer[i++]; /// Skip, as we assume always default subtype, i.e. 0!
286 object[name] = buffer.slice(i, i += size); /// creates a new Buffer "slice" view of the same memory!
289 case 9: /// = BSON.BSON_DATA_DATE: /// SEE notes below on the Date type vs. other options...
290 low = buffer[i++] | buffer[i++] << 8 | buffer[i++] << 16 | buffer[i++] << 24;
291 high = buffer[i++] | buffer[i++] << 8 | buffer[i++] << 16 | buffer[i++] << 24;
292 object[name] = new Date( high * 4294967296 + (low < 0 ? low + 4294967296 : low) ); break;
294 case 18: /// = BSON.BSON_DATA_LONG: /// usage should be somewhat rare beyond parseResponse() -> cursorId, where it is handled inline, NOT as part of deserializeFast(returnedObjects); get lowBits, highBits:
295 low = buffer[i++] | buffer[i++] << 8 | buffer[i++] << 16 | buffer[i++] << 24;
296 high = buffer[i++] | buffer[i++] << 8 | buffer[i++] << 16 | buffer[i++] << 24;
298 size = high * 4294967296 + (low < 0 ? low + 4294967296 : low); /// from long.toNumber()
299 if (size < JS_INT_MAX && size > JS_INT_MIN) object[name] = size; /// positive # more likely!
300 else object[name] = new Long(low, high); break;
302 case 127: /// = BSON.BSON_DATA_MIN_KEY: /// do we EVER actually get these BACK from MongoDB server?!
303 object[name] = new MinKey(); break;
304 case 255: /// = BSON.BSON_DATA_MAX_KEY:
305 object[name] = new MaxKey(); break;
307 case 17: /// = BSON.BSON_DATA_TIMESTAMP: /// somewhat obscure internal BSON type; MongoDB uses it for (pseudo) high-res time timestamp (past millisecs precision is just a counter!) in the Oplog ts: field, etc.
308 low = buffer[i++] | buffer[i++] << 8 | buffer[i++] << 16 | buffer[i++] << 24;
309 high = buffer[i++] | buffer[i++] << 8 | buffer[i++] << 16 | buffer[i++] << 24;
310 object[name] = new Timestamp(low, high); break;
312 /// case 11: /// = RegExp is skipped; we should NEVER be getting any from the MongoDB server!?
313 } /// end of switch(elementType)
314 } /// end of while(1)
315 return object; // Return the finalized object
319 function MinKey() { this._bsontype = 'MinKey'; } /// these are merely placeholders/stubs to signify the type!?
321 function MaxKey() { this._bsontype = 'MaxKey'; }
323 function Long(low, high) {
324 this._bsontype = 'Long';
325 this.low_ = low | 0; this.high_ = high | 0; /// force into 32 signed bits.
327 Long.prototype.getLowBits = function(){ return this.low_; }
328 Long.prototype.getHighBits = function(){ return this.high_; }
330 Long.prototype.toNumber = function(){
331 return this.high_ * 4294967296 + (this.low_ < 0 ? this.low_ + 4294967296 : this.low_);
333 Long.fromNumber = function(num){
334 return new Long(num % 4294967296, num / 4294967296); /// |0 is forced in the constructor!
336 function Double(value) {
337 this._bsontype = 'Double';
340 function Timestamp(low, high) {
341 this._bsontype = 'Timestamp';
342 this.low_ = low | 0; this.high_ = high | 0; /// force into 32 signed bits.
344 Timestamp.prototype.getLowBits = function(){ return this.low_; }
345 Timestamp.prototype.getHighBits = function(){ return this.high_; }
347 /////////////////////////////// ObjectID /////////////////////////////////
348 /// machine & proc IDs stored as 1 string, b/c Buffer shouldn't be held for long periods (could use SlowBuffer?!)
350 var MACHINE = parseInt(Math.random() * 0xFFFFFF, 10);
351 var PROCESS = process.pid % 0xFFFF;
352 var MACHINE_AND_PROC = encodeIntBE(MACHINE, 3) + encodeIntBE(PROCESS, 2); /// keep as ONE string, ready to go.
354 function encodeIntBE(data, bytes){ /// encode the bytes to a string
356 if (bytes >= 4){ result += String.fromCharCode(Math.floor(data / 0x1000000)); data %= 0x1000000; }
357 if (bytes >= 3){ result += String.fromCharCode(Math.floor(data / 0x10000)); data %= 0x10000; }
358 if (bytes >= 2){ result += String.fromCharCode(Math.floor(data / 0x100)); data %= 0x100; }
359 result += String.fromCharCode(Math.floor(data));
362 var _counter = ~~(Math.random() * 0xFFFFFF); /// double-tilde is equivalent to Math.floor()
363 var checkForHex = new RegExp('^[0-9a-fA-F]{24}$');
365 function ObjectID(id) {
366 this._bsontype = 'ObjectID';
367 if (!id){ this.id = createFromScratch(); /// base case, DONE.
369 if (id.constructor === Buffer){
370 this.id = id; /// case of
371 } else if (id.constructor === String) {
372 if ( id.length === 24 && checkForHex.test(id) ) {
373 this.id = new Buffer(id, 'hex');
375 this.id = new Error('Illegal/faulty Hexadecimal string supplied!'); /// changed from 'throw'
377 } else if (id.constructor === Number) {
378 this.id = createFromTime(id); /// this is what should be the only interface for this!?
382 function createFromScratch() {
383 var buf = new Buffer(12), i = 0;
384 var ts = ~~(Date.now()/1000); /// 4 bytes timestamp in seconds, BigEndian notation!
385 buf[i++] = (ts >> 24) & 0xFF; buf[i++] = (ts >> 16) & 0xFF;
386 buf[i++] = (ts >> 8) & 0xFF; buf[i++] = (ts) & 0xFF;
388 buf.write(MACHINE_AND_PROC, i, 5, 'utf8'); i += 5; /// write 3 bytes + 2 bytes MACHINE_ID and PROCESS_ID
389 _counter = ++_counter % 0xFFFFFF; /// 3 bytes internal _counter for subsecond resolution; BigEndian
390 buf[i++] = (_counter >> 16) & 0xFF;
391 buf[i++] = (_counter >> 8) & 0xFF;
392 buf[i++] = (_counter) & 0xFF;
395 function createFromTime(ts) {
396 ts || ( ts = ~~(Date.now()/1000) ); /// 4 bytes timestamp in seconds only
397 var buf = new Buffer(12), i = 0;
398 buf[i++] = (ts >> 24) & 0xFF; buf[i++] = (ts >> 16) & 0xFF;
399 buf[i++] = (ts >> 8) & 0xFF; buf[i++] = (ts) & 0xFF;
401 for (;i < 12; ++i) buf[i] = 0x00; /// indeces 4 through 11 (8 bytes) get filled up with nulls
404 ObjectID.prototype.toHexString = function toHexString() {
405 return this.id.toString('hex');
407 ObjectID.prototype.getTimestamp = function getTimestamp() {
408 return this.id.readUIntBE(0, 4);
410 ObjectID.prototype.getTimestampDate = function getTimestampDate() {
412 ts.setTime(this.id.readUIntBE(0, 4) * 1000);
415 ObjectID.createPk = function createPk () { ///?override if a PrivateKey factory w/ unique factors is warranted?!
416 return new ObjectID();
418 ObjectID.prototype.toJSON = function toJSON() {
419 return "ObjectID('" +this.id.toString('hex')+ "')";
422 /// module.exports.BSON = BSON; /// not needed anymore!? exports.Binary = Binary;
423 module.exports.ObjectID = ObjectID;
424 module.exports.MinKey = MinKey;
425 module.exports.MaxKey = MaxKey;
426 module.exports.Long = Long; /// ?! we really don't want to do the complicated Long math anywhere for now!?
428 //module.exports.Double = Double;
429 //module.exports.Timestamp = Timestamp;