Created
September 20, 2014 00:14
-
-
Save Qix-/693bfda36e2deb89ec24 to your computer and use it in GitHub Desktop.
Pure Javascript BSON Encoding
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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