Skip to content

Instantly share code, notes, and snippets.

@Qix-
Created September 20, 2014 00:14
Show Gist options
  • Save Qix-/693bfda36e2deb89ec24 to your computer and use it in GitHub Desktop.
Save Qix-/693bfda36e2deb89ec24 to your computer and use it in GitHub Desktop.
Pure Javascript BSON Encoding
/**
* BSON Pure Javascript Implementation
* by Qix
*
* Known shortcomings:
* - ObjectID is excluded (for now)
* - DBPointer is excluded
* - Javascript code is excluded (for now; scoped encoding
* will always be excluded)
* - Timestamp (Mongo's specific type, not UTC) is excluded (for now)
* - Binary types are excluded (for now)
* - Min/Max keys are excluded
*
* Released under the MIT License. Use at your own risk!
*/
var BSON = (function() {
'use strict';
var bson = {};
var make = {};
bson.encode = function encode(obj) {
var bytes = [];
make.document.call(bytes, obj);
return bytes;
};
make['cstring'] = function(str) {
for (var i = 0, len = str.length; i < len; i++) {
var c = str.charCodeAt(i);
if (c === 0) {
continue;
}
this.push(c);
}
this.push(0);
};
// Thanks to Haravikk at http://stackoverflow.com/questions/15935365
// Reformatted and slightly edited by Qix.
make['double'] = function(val, name, tag) {
if (name) {
tag = tag || 0x01;
this.push(tag);
make.cstring.call(this, name);
}
var hiWord = 0, loWord = 0;
switch (value) {
case Number.POSITIVE_INFINITY:
hiWord = 0x7FF00000;
break;
case Number.NEGATIVE_INFINITY:
hiWord = 0xFFF00000;
break;
case +0.0:
hiWord = 0x40000000;
break;
case -0.0:
hiWord = 0xC0000000;
break;
default:
if (Number.isNaN(value)) {
hiWord = 0x7FF80000;
break;
}
if (value <= -0.0) {
hiWord = 0x80000000;
value = -value;
}
var exponent = Math.floor(Math.log(value) / Math.log(2));
var significand = Math.floor(
(value / Math.pow(2, exponent)) * Math.pow(2, 52));
loWord = significand & 0xFFFFFFFF;
significand /= Math.pow(2, 32);
exponent += 1023;
if (exponent >= 0x7FF) {
exponent = 0x7FF;
significand = 0;
} else if (exponent < 0) {
exponent = 0;
}
hiWord = hiWord | (exponent << 20);
hiWord = hiWord | (significand & ~(-1 << 20));
break;
}
make.int32.call(this, hiWord);
make.int32.call(this, loWord);
};
// Thanks to Joni from http://stackoverflow.com/questions/18729405
// Reformatted and slightly modified by Qix
// http://jsperf.com/utf8-raw-encoding-vs-simple-encoding
make['string'] = function(str, name, tag) {
if (name) {
tag = tag || 0x02;
this.push(tag);
make.cstring.call(this, name);
}
// Reserve length
var lengthIndex = this.length;
for (var i = 0; i < 4; i++) {
this.push(0);
}
for (var i=0; i < str.length; i++) {
var charcode = str.charCodeAt(i);
if (charcode < 0x80) this.push(charcode);
else if (charcode < 0x800) {
this.push(0xc0 | (charcode >> 6),
0x80 | (charcode & 0x3f));
}
else if (charcode < 0xd800 || charcode >= 0xe000) {
this.push(0xe0 | (charcode >> 12),
0x80 | ((charcode>>6) & 0x3f),
0x80 | (charcode & 0x3f));
}
// surrogate pair
else {
i++;
// UTF-16 encodes 0x10000-0x10FFFF by
// subtracting 0x10000 and splitting the
// 20 bits of 0x0-0xFFFFF into two halves
charcode = 0x10000 + (((charcode & 0x3ff)<<10)
| (str.charCodeAt(i) & 0x3ff))
this.push(0xf0 | (charcode >>18),
0x80 | ((charcode>>12) & 0x3f),
0x80 | ((charcode>>6) & 0x3f),
0x80 | (charcode & 0x3f));
}
}
this.push(0);
// Fill reserved length
var strLen = this.length - (lengthIndex + 4);
this[lengthIndex++] = strLen & 0xFF;
this[lengthIndex++] = (strLen >> 8) & 0xFF;
this[lengthIndex++] = (strLen >> 16) & 0xFF;
this[lengthIndex] = (strLen >> 24) & 0xFF;
};
make['document'] = function(obj, name, tag) {
if (name) {
tag = tag || 0x03;
this.push(tag);
make.cstring.call(this, name);
}
// Reserve bytes
var lengthIndex = this.length;
for (var i = 0; i < 4; i++) {
this.push(0);
}
for (var k in obj) {
if (!obj.hasOwnProperty(k)) {
continue;
}
make.dispatch.call(this, obj[k], k);
}
// Fill previously reserved length
var docLen = this.length - (lengthIndex + 4);
this[lengthIndex++] = docLen & 0xFF;
this[lengthIndex++] = (docLen >> 8) & 0xFF;
this[lengthIndex++] = (docLen >> 16) & 0xFF;
this[lengthIndex] = (docLen >> 24) & 0xFF;
this.push(0);
};
make['array'] = function(arr, name, tag) {
if (name) {
tag = tag || 0x04;
}
make['document'].call(this, arr, name, tag);
};
make['undefined'] = function(val, name, tag) {
if (name) {
tag = tag || 0x06;
this.push(tag);
make.cstring.call(this, name);
}
};
make['boolean'] = function(val, name, tag) {
if (name) {
tag = tag || 0x08;
this.push(tag);
make.cstring.call(this, name);
}
// http://jsperf.com/boolean-int-conversion/3
this.push(val === true ? 1 : 0);
};
make['datetime'] = function(val, name, tag) {
if (name) {
tag = tag || 0x09;
}
make['int64'].call(this, val.getTime(), name, tag);
};
make['null'] = function(val, name, tag) {
if (name) {
tag = tag || 0x0A;
this.push(tag);
make.cstring.call(this, name);
}
};
make['regex'] = function(val, name, tag) {
if (name) {
tag = tag || 0x0B;
this.push(tag);
make.cstring.call(this, name);
}
var matches = /^\/(.+)\/(.+)$/.exec(val.toString());
make.cstring.call(this, matches[1]);
make.cstring.call(this, matches[2] || '');
};
make['int32'] = function(val, name, tag) {
if (name) {
tag = tag || 0x10;
this.push(tag);
make.cstring.call(this, name);
}
this.push(val & 0xFF);
this.push((val >> 8) & 0xFF);
this.push((val >> 16) & 0xFF);
this.push((val >> 24) & 0xFF);
};
make['int64'] = function(val, name, tag) {
if (name) {
tag = tag || 0x12;
this.push(tag);
make.cstring.call(this, name);
}
this.push(val & 0xFF);
this.push((val >> 8) & 0xFF);
this.push((val >> 16) & 0xFF);
this.push((val >> 24) & 0xFF);
var val = val % 0x100000000;
this.push(val & 0xFF);
this.push((val >> 8) & 0xFF);
this.push((val >> 16) & 0xFF);
this.push((val >> 24) & 0xFF);
};
make.dispatch = function(value, name) {
var type = typeof value;
var target = null;
switch (true) {
case type === 'number':
// Is it an integer?
if (value % 1 === 0) {
// Is it 64 bits?
if (Math.abs(value) > 0x7FFFFFFF) {
target = 'int64';
} else {
target = 'int32';
}
} else {
target = 'float';
}
break;
case type === 'string':
target = 'string';
break;
case type === 'boolean':
target = 'boolean';
break;
case type === 'undefined':
target = 'undefined';
break;
case type === 'symbol':
throw 'symbols are not yet supported by BSON';
case type === 'function':
target = 'function';
break;
case value === null:
target = 'null';
break;
case value instanceof Array:
target = 'array';
break;
case value instanceof RegExp:
target = 'regex';
break;
case value instanceof Date:
target = 'datetime';
break;
default:
target = 'document';
break;
}
make[target].call(this, value, name);
};
return bson;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment