bson-rpc
Version:
the nodejs version of bson-rpc
654 lines (589 loc) • 23 kB
JavaScript
"use strict"
var readIEEE754 = require('../float_parser').readIEEE754,
f = require('util').format,
Long = require('../long').Long,
Double = require('../double').Double,
Timestamp = require('../timestamp').Timestamp,
ObjectID = require('../objectid').ObjectID,
Symbol = require('../symbol').Symbol,
Code = require('../code').Code,
MinKey = require('../min_key').MinKey,
MaxKey = require('../max_key').MaxKey,
Decimal128 = require('../decimal128'),
Int32 = require('../int_32'),
DBRef = require('../db_ref').DBRef,
BSONRegExp = require('../regexp').BSONRegExp,
Binary = require('../binary').Binary;
var deserialize = function(buffer, options, isArray) {
options = options == null ? {} : options;
var index = options && options.index ? options.index : 0;
// Read the document size
var size = buffer[index] | buffer[index+1] << 8 | buffer[index+2] << 16 | buffer[index+3] << 24;
// Ensure buffer is valid size
if(size < 5 || buffer.length < size || (size + index) > buffer.length) {
throw new Error("corrupt bson message");
}
// Illegal end value
if(buffer[index + size - 1] != 0) {
throw new Error("One object, sized correctly, with a spot for an EOO, but the EOO isn't 0x00");
}
// Start deserializtion
return deserializeObject(buffer, index, options, isArray);
}
var deserializeObject = function(buffer, index, options, isArray) {
var evalFunctions = options['evalFunctions'] == null ? false : options['evalFunctions'];
var cacheFunctions = options['cacheFunctions'] == null ? false : options['cacheFunctions'];
var cacheFunctionsCrc32 = options['cacheFunctionsCrc32'] == null ? false : options['cacheFunctionsCrc32'];
var fieldsAsRaw = options['fieldsAsRaw'] == null ? null : options['fieldsAsRaw'];
// Return raw bson buffer instead of parsing it
var raw = options['raw'] == null ? false : options['raw'];
// Return BSONRegExp objects instead of native regular expressions
var bsonRegExp = typeof options['bsonRegExp'] == 'boolean' ? options['bsonRegExp'] : false;
// Controls the promotion of values vs wrapper classes
var promoteBuffers = options['promoteBuffers'] == null ? false : options['promoteBuffers'];
var promoteLongs = options['promoteLongs'] == null ? true : options['promoteLongs'];
var promoteValues = options['promoteValues'] == null ? true : options['promoteValues'];
// Set the start index
var startIndex = index;
// Validate that we have at least 4 bytes of buffer
if(buffer.length < 5) throw new Error("corrupt bson message < 5 bytes long");
// Read the document size
var size = buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24;
// Ensure buffer is valid size
if(size < 5 || size > buffer.length) throw new Error("corrupt bson message");
// Create holding object
var object = isArray ? [] : {};
// Used for arrays to skip having to perform utf8 decoding
var arrayIndex = 0;
// While we have more left data left keep parsing
while(true) {
// Read the type
var elementType = buffer[index++];
// If we get a zero it's the last byte, exit
if(elementType == 0) {
break;
}
// Get the start search index
var i = index;
// Locate the end of the c string
while(buffer[i] !== 0x00 && i < buffer.length) {
i++
}
// If are at the end of the buffer there is a problem with the document
if(i >= buffer.length) throw new Error("Bad BSON Document: illegal CString")
var name = isArray ? arrayIndex++ : buffer.toString('utf8', index, i);
index = i + 1;
if(elementType == BSON.BSON_DATA_STRING) {
var stringSize = buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24;
if(stringSize <= 0 || stringSize > (buffer.length - index) || buffer[index + stringSize - 1] != 0) throw new Error("bad string length in bson");
object[name] = buffer.toString('utf8', index, index + stringSize - 1);
index = index + stringSize;
} else if(elementType == BSON.BSON_DATA_OID) {
var oid = new Buffer(12);
buffer.copy(oid, 0, index, index + 12);
object[name] = new ObjectID(oid);
index = index + 12;
} else if(elementType == BSON.BSON_DATA_INT && promoteValues == false) {
object[name] = new Int32(buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24);
} else if(elementType == BSON.BSON_DATA_INT) {
object[name] = buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24;
} else if(elementType == BSON.BSON_DATA_NUMBER && promoteValues == false) {
object[name] = new Double(buffer.readDoubleLE(index));
index = index + 8;
} else if(elementType == BSON.BSON_DATA_NUMBER) {
object[name] = buffer.readDoubleLE(index);
index = index + 8;
} else if(elementType == BSON.BSON_DATA_DATE) {
var lowBits = buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24;
var highBits = buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24;
object[name] = new Date(new Long(lowBits, highBits).toNumber());
} else if(elementType == BSON.BSON_DATA_BOOLEAN) {
if(buffer[index] != 0 && buffer[index] != 1) throw new Error('illegal boolean type value');
object[name] = buffer[index++] == 1;
} else if(elementType == BSON.BSON_DATA_OBJECT) {
var _index = index;
var objectSize = buffer[index] | buffer[index + 1] << 8 | buffer[index + 2] << 16 | buffer[index + 3] << 24;
if(objectSize <= 0 || objectSize > (buffer.length - index)) throw new Error("bad embedded document length in bson");
// We have a raw value
if(raw) {
object[name] = buffer.slice(index, index + objectSize);
} else {
object[name] = deserializeObject(buffer, _index, options, false);
}
index = index + objectSize;
} else if(elementType == BSON.BSON_DATA_ARRAY) {
var _index = index;
var objectSize = buffer[index] | buffer[index + 1] << 8 | buffer[index + 2] << 16 | buffer[index + 3] << 24;
var arrayOptions = options;
// Stop index
var stopIndex = index + objectSize;
// All elements of array to be returned as raw bson
if(fieldsAsRaw && fieldsAsRaw[name]) {
arrayOptions = {};
for(var n in options) arrayOptions[n] = options[n];
arrayOptions['raw'] = true;
}
object[name] = deserializeObject(buffer, _index, arrayOptions, true);
index = index + objectSize;
if(buffer[index - 1] != 0) throw new Error('invalid array terminator byte');
if(index != stopIndex) throw new Error('corrupted array bson');
} else if(elementType == BSON.BSON_DATA_UNDEFINED) {
object[name] = undefined;
} else if(elementType == BSON.BSON_DATA_NULL) {
object[name] = null;
} else if(elementType == BSON.BSON_DATA_LONG) {
// Unpack the low and high bits
var lowBits = buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24;
var highBits = buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24;
var long = new Long(lowBits, highBits);
// Promote the long if possible
if(promoteLongs && promoteValues == true) {
object[name] = long.lessThanOrEqual(JS_INT_MAX_LONG) && long.greaterThanOrEqual(JS_INT_MIN_LONG) ? long.toNumber() : long;
} else {
object[name] = long;
}
} else if(elementType == BSON.BSON_DATA_DECIMAL128) {
// Buffer to contain the decimal bytes
var bytes = new Buffer(16);
// Copy the next 16 bytes into the bytes buffer
buffer.copy(bytes, 0, index, index + 16);
// Update index
index = index + 16;
// Assign the new Decimal128 value
var decimal128 = new Decimal128(bytes);
// If we have an alternative mapper use that
object[name] = decimal128.toObject ? decimal128.toObject() : decimal128;
} else if(elementType == BSON.BSON_DATA_BINARY) {
var binarySize = buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24;
var totalBinarySize = binarySize;
var subType = buffer[index++];
// Did we have a negative binary size, throw
if(binarySize < 0) throw new Error('Negative binary type element size found');
// Is the length longer than the document
if(binarySize > buffer.length) throw new Error('Binary type size larger than document size');
// Decode as raw Buffer object if options specifies it
if(buffer['slice'] != null) {
// If we have subtype 2 skip the 4 bytes for the size
if(subType == Binary.SUBTYPE_BYTE_ARRAY) {
binarySize = buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24;
if(binarySize < 0) throw new Error('Negative binary type element size found for subtype 0x02');
if(binarySize > (totalBinarySize - 4)) throw new Error('Binary type with subtype 0x02 contains to long binary size');
if(binarySize < (totalBinarySize - 4)) throw new Error('Binary type with subtype 0x02 contains to short binary size');
}
if(promoteBuffers && promoteValues) {
object[name] = buffer.slice(index, index + binarySize);
} else {
object[name] = new Binary(buffer.slice(index, index + binarySize), subType);
}
} else {
var _buffer = typeof Uint8Array != 'undefined' ? new Uint8Array(new ArrayBuffer(binarySize)) : new Array(binarySize);
// If we have subtype 2 skip the 4 bytes for the size
if(subType == Binary.SUBTYPE_BYTE_ARRAY) {
binarySize = buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24;
if(binarySize < 0) throw new Error('Negative binary type element size found for subtype 0x02');
if(binarySize > (totalBinarySize - 4)) throw new Error('Binary type with subtype 0x02 contains to long binary size');
if(binarySize < (totalBinarySize - 4)) throw new Error('Binary type with subtype 0x02 contains to short binary size');
}
// Copy the data
for(var i = 0; i < binarySize; i++) {
_buffer[i] = buffer[index + i];
}
if(promoteBuffers && promoteValues) {
object[name] = _buffer;
} else {
object[name] = new Binary(_buffer, subType);
}
}
// Update the index
index = index + binarySize;
} else if(elementType == BSON.BSON_DATA_REGEXP && bsonRegExp == false) {
// Get the start search index
var i = index;
// Locate the end of the c string
while(buffer[i] !== 0x00 && i < buffer.length) {
i++
}
// If are at the end of the buffer there is a problem with the document
if(i >= buffer.length) throw new Error("Bad BSON Document: illegal CString")
// Return the C string
var source = buffer.toString('utf8', index, i);
// Create the regexp
index = i + 1;
// Get the start search index
var i = index;
// Locate the end of the c string
while(buffer[i] !== 0x00 && i < buffer.length) {
i++
}
// If are at the end of the buffer there is a problem with the document
if(i >= buffer.length) throw new Error("Bad BSON Document: illegal CString")
// Return the C string
var regExpOptions = buffer.toString('utf8', index, i);
index = i + 1;
// For each option add the corresponding one for javascript
var optionsArray = new Array(regExpOptions.length);
// Parse options
for(var i = 0; i < regExpOptions.length; i++) {
switch(regExpOptions[i]) {
case 'm':
optionsArray[i] = 'm';
break;
case 's':
optionsArray[i] = 'g';
break;
case 'i':
optionsArray[i] = 'i';
break;
}
}
object[name] = new RegExp(source, optionsArray.join(''));
} else if(elementType == BSON.BSON_DATA_REGEXP && bsonRegExp == true) {
// Get the start search index
var i = index;
// Locate the end of the c string
while(buffer[i] !== 0x00 && i < buffer.length) {
i++
}
// If are at the end of the buffer there is a problem with the document
if(i >= buffer.length) throw new Error("Bad BSON Document: illegal CString")
// Return the C string
var source = buffer.toString('utf8', index, i);
index = i + 1;
// Get the start search index
var i = index;
// Locate the end of the c string
while(buffer[i] !== 0x00 && i < buffer.length) {
i++
}
// If are at the end of the buffer there is a problem with the document
if(i >= buffer.length) throw new Error("Bad BSON Document: illegal CString")
// Return the C string
var regExpOptions = buffer.toString('utf8', index, i);
index = i + 1;
// Set the object
object[name] = new BSONRegExp(source, regExpOptions);
} else if(elementType == BSON.BSON_DATA_SYMBOL) {
var stringSize = buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24;
if(stringSize <= 0 || stringSize > (buffer.length - index) || buffer[index + stringSize - 1] != 0) throw new Error("bad string length in bson");
object[name] = new Symbol(buffer.toString('utf8', index, index + stringSize - 1));
index = index + stringSize;
} else if(elementType == BSON.BSON_DATA_TIMESTAMP) {
var lowBits = buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24;
var highBits = buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24;
object[name] = new Timestamp(lowBits, highBits);
} else if(elementType == BSON.BSON_DATA_MIN_KEY) {
object[name] = new MinKey();
} else if(elementType == BSON.BSON_DATA_MAX_KEY) {
object[name] = new MaxKey();
} else if(elementType == BSON.BSON_DATA_CODE) {
var stringSize = buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24;
if(stringSize <= 0 || stringSize > (buffer.length - index) || buffer[index + stringSize - 1] != 0) throw new Error("bad string length in bson");
var functionString = buffer.toString('utf8', index, index + stringSize - 1);
// If we are evaluating the functions
if(evalFunctions) {
var value = null;
// If we have cache enabled let's look for the md5 of the function in the cache
if(cacheFunctions) {
var hash = cacheFunctionsCrc32 ? crc32(functionString) : functionString;
// Got to do this to avoid V8 deoptimizing the call due to finding eval
object[name] = isolateEvalWithHash(functionCache, hash, functionString, object);
} else {
object[name] = isolateEval(functionString);
}
} else {
object[name] = new Code(functionString);
}
// Update parse index position
index = index + stringSize;
} else if(elementType == BSON.BSON_DATA_CODE_W_SCOPE) {
var totalSize = buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24;
// Element cannot be shorter than totalSize + stringSize + documentSize + terminator
if(totalSize < (4 + 4 + 4 + 1)) {
throw new Error("code_w_scope total size shorter minimum expected length");
}
// Get the code string size
var stringSize = buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24;
// Check if we have a valid string
if(stringSize <= 0 || stringSize > (buffer.length - index) || buffer[index + stringSize - 1] != 0) throw new Error("bad string length in bson");
// Javascript function
var functionString = buffer.toString('utf8', index, index + stringSize - 1);
// Update parse index position
index = index + stringSize;
// Parse the element
var _index = index;
// Decode the size of the object document
var objectSize = buffer[index] | buffer[index + 1] << 8 | buffer[index + 2] << 16 | buffer[index + 3] << 24;
// Decode the scope object
var scopeObject = deserializeObject(buffer, _index, options, false);
// Adjust the index
index = index + objectSize;
// Check if field length is to short
if(totalSize < (4 + 4 + objectSize + stringSize)) {
throw new Error('code_w_scope total size is to short, truncating scope');
}
// Check if totalSize field is to long
if(totalSize > (4 + 4 + objectSize + stringSize)) {
throw new Error('code_w_scope total size is to long, clips outer document');
}
// If we are evaluating the functions
if(evalFunctions) {
// Contains the value we are going to set
var value = null;
// If we have cache enabled let's look for the md5 of the function in the cache
if(cacheFunctions) {
var hash = cacheFunctionsCrc32 ? crc32(functionString) : functionString;
// Got to do this to avoid V8 deoptimizing the call due to finding eval
object[name] = isolateEvalWithHash(functionCache, hash, functionString, object);
} else {
object[name] = isolateEval(functionString);
}
object[name].scope = scopeObject;
} else {
object[name] = new Code(functionString, scopeObject);
}
} else if(elementType == BSON.BSON_DATA_DBPOINTER) {
// Get the code string size
var stringSize = buffer[index++] | buffer[index++] << 8 | buffer[index++] << 16 | buffer[index++] << 24;
// Check if we have a valid string
if(stringSize <= 0 || stringSize > (buffer.length - index) || buffer[index + stringSize - 1] != 0) throw new Error("bad string length in bson");
// Namespace
var namespace = buffer.toString('utf8', index, index + stringSize - 1);
// Update parse index position
index = index + stringSize;
// Read the oid
var oidBuffer = new Buffer(12);
buffer.copy(oidBuffer, 0, index, index + 12);
var oid = new ObjectID(oidBuffer);
// Update the index
index = index + 12;
// Split the namespace
var parts = namespace.split('.');
var db = parts.shift();
var collection = parts.join('.');
// Upgrade to DBRef type
object[name] = new DBRef(collection, oid, db);
} else {
throw new Error("Detected unknown BSON type " + elementType.toString(16) + " for fieldname \"" + name + "\", are you using the latest BSON parser");
}
}
// Check if the deserialization was against a valid array/object
if(size != (index - startIndex)) {
if(isArray) throw new Error('corrupt array bson');
throw new Error('corrupt object bson');
}
// Check if we have a db ref object
if(object['$id'] != null) object = new DBRef(object['$ref'], object['$id'], object['$db']);
return object;
}
/**
* Ensure eval is isolated.
*
* @ignore
* @api private
*/
var isolateEvalWithHash = function(functionCache, hash, functionString, object) {
// Contains the value we are going to set
var value = null;
// Check for cache hit, eval if missing and return cached function
if(functionCache[hash] == null) {
eval("value = " + functionString);
functionCache[hash] = value;
}
// Set the object
return functionCache[hash].bind(object);
}
/**
* Ensure eval is isolated.
*
* @ignore
* @api private
*/
var isolateEval = function(functionString) {
// Contains the value we are going to set
var value = null;
// Eval the function
eval("value = " + functionString);
return value;
}
var BSON = {};
/**
* Contains the function cache if we have that enable to allow for avoiding the eval step on each deserialization, comparison is by md5
*
* @ignore
* @api private
*/
var functionCache = BSON.functionCache = {};
/**
* Number BSON Type
*
* @classconstant BSON_DATA_NUMBER
**/
BSON.BSON_DATA_NUMBER = 1;
/**
* String BSON Type
*
* @classconstant BSON_DATA_STRING
**/
BSON.BSON_DATA_STRING = 2;
/**
* Object BSON Type
*
* @classconstant BSON_DATA_OBJECT
**/
BSON.BSON_DATA_OBJECT = 3;
/**
* Array BSON Type
*
* @classconstant BSON_DATA_ARRAY
**/
BSON.BSON_DATA_ARRAY = 4;
/**
* Binary BSON Type
*
* @classconstant BSON_DATA_BINARY
**/
BSON.BSON_DATA_BINARY = 5;
/**
* Binary BSON Type
*
* @classconstant BSON_DATA_UNDEFINED
**/
BSON.BSON_DATA_UNDEFINED = 6;
/**
* ObjectID BSON Type
*
* @classconstant BSON_DATA_OID
**/
BSON.BSON_DATA_OID = 7;
/**
* Boolean BSON Type
*
* @classconstant BSON_DATA_BOOLEAN
**/
BSON.BSON_DATA_BOOLEAN = 8;
/**
* Date BSON Type
*
* @classconstant BSON_DATA_DATE
**/
BSON.BSON_DATA_DATE = 9;
/**
* null BSON Type
*
* @classconstant BSON_DATA_NULL
**/
BSON.BSON_DATA_NULL = 10;
/**
* RegExp BSON Type
*
* @classconstant BSON_DATA_REGEXP
**/
BSON.BSON_DATA_REGEXP = 11;
/**
* Code BSON Type
*
* @classconstant BSON_DATA_DBPOINTER
**/
BSON.BSON_DATA_DBPOINTER = 12;
/**
* Code BSON Type
*
* @classconstant BSON_DATA_CODE
**/
BSON.BSON_DATA_CODE = 13;
/**
* Symbol BSON Type
*
* @classconstant BSON_DATA_SYMBOL
**/
BSON.BSON_DATA_SYMBOL = 14;
/**
* Code with Scope BSON Type
*
* @classconstant BSON_DATA_CODE_W_SCOPE
**/
BSON.BSON_DATA_CODE_W_SCOPE = 15;
/**
* 32 bit Integer BSON Type
*
* @classconstant BSON_DATA_INT
**/
BSON.BSON_DATA_INT = 16;
/**
* Timestamp BSON Type
*
* @classconstant BSON_DATA_TIMESTAMP
**/
BSON.BSON_DATA_TIMESTAMP = 17;
/**
* Long BSON Type
*
* @classconstant BSON_DATA_LONG
**/
BSON.BSON_DATA_LONG = 18;
/**
* Long BSON Type
*
* @classconstant BSON_DATA_DECIMAL128
**/
BSON.BSON_DATA_DECIMAL128 = 19;
/**
* MinKey BSON Type
*
* @classconstant BSON_DATA_MIN_KEY
**/
BSON.BSON_DATA_MIN_KEY = 0xff;
/**
* MaxKey BSON Type
*
* @classconstant BSON_DATA_MAX_KEY
**/
BSON.BSON_DATA_MAX_KEY = 0x7f;
/**
* Binary Default Type
*
* @classconstant BSON_BINARY_SUBTYPE_DEFAULT
**/
BSON.BSON_BINARY_SUBTYPE_DEFAULT = 0;
/**
* Binary Function Type
*
* @classconstant BSON_BINARY_SUBTYPE_FUNCTION
**/
BSON.BSON_BINARY_SUBTYPE_FUNCTION = 1;
/**
* Binary Byte Array Type
*
* @classconstant BSON_BINARY_SUBTYPE_BYTE_ARRAY
**/
BSON.BSON_BINARY_SUBTYPE_BYTE_ARRAY = 2;
/**
* Binary UUID Type
*
* @classconstant BSON_BINARY_SUBTYPE_UUID
**/
BSON.BSON_BINARY_SUBTYPE_UUID = 3;
/**
* Binary MD5 Type
*
* @classconstant BSON_BINARY_SUBTYPE_MD5
**/
BSON.BSON_BINARY_SUBTYPE_MD5 = 4;
/**
* Binary User Defined Type
*
* @classconstant BSON_BINARY_SUBTYPE_USER_DEFINED
**/
BSON.BSON_BINARY_SUBTYPE_USER_DEFINED = 128;
// BSON MAX VALUES
BSON.BSON_INT32_MAX = 0x7FFFFFFF;
BSON.BSON_INT32_MIN = -0x80000000;
BSON.BSON_INT64_MAX = Math.pow(2, 63) - 1;
BSON.BSON_INT64_MIN = -Math.pow(2, 63);
// JS MAX PRECISE VALUES
BSON.JS_INT_MAX = 0x20000000000000; // Any integer up to 2^53 can be precisely represented by a double.
BSON.JS_INT_MIN = -0x20000000000000; // Any integer down to -2^53 can be precisely represented by a double.
// Internal long versions
var JS_INT_MAX_LONG = Long.fromNumber(0x20000000000000); // Any integer up to 2^53 can be precisely represented by a double.
var JS_INT_MIN_LONG = Long.fromNumber(-0x20000000000000); // Any integer down to -2^53 can be precisely represented by a double.
module.exports = deserialize