UNPKG

@kai3341/bsonfy

Version:

Ultrafast BSON serializer/parser

523 lines (522 loc) 20.7 kB
"use strict"; /////////////////////////////////////////////////////////////////////////////// // \author (c) Marco Paland (marco@paland.com) // 2016-2018, PALANDesign Hannover, Germany // // \license The MIT License (MIT) // // This file is part of the bsonfy library. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // // \brief Extrem fast BSON implementation in typescript with NO dependencies // See http://bsonspec.org for details // Usage: // import { BSON } from './bsonfy'; // let obj = { id: 10, time: new BSON.UTC(), arr: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]) }; // let bson = BSON.serialize(obj); // let orig = BSON.deserialize(bson); // /////////////////////////////////////////////////////////////////////////////// Object.defineProperty(exports, "__esModule", { value: true }); exports.BSON = void 0; var BSON; (function (BSON) { /** * BSON module version */ var version = '1.0.2'; /** * UUID class */ var UUID = /** @class */ (function () { function UUID(id) { this._id = new Uint8Array(id); } UUID.prototype.buffer = function () { return this._id; }; return UUID; }()); BSON.UUID = UUID; /** * ObjectId class (for mongoDB usage) */ var ObjectId = /** @class */ (function () { function ObjectId(id) { this._id = new Uint8Array(id); } ObjectId.prototype.buffer = function () { return this._id; }; return ObjectId; }()); BSON.ObjectId = ObjectId; /** * The UTC class contains the milliseconds since the Unix epoch (1.1.1970 00:00:00 UTC) */ var UTC = /** @class */ (function () { function UTC(time) { this._time = (typeof time !== 'string') ? new Uint8Array(time || number2long(Date.now())) : number2long(+new Date(time)); } UTC.prototype.buffer = function () { return this._time; }; /** * Convert an (ISO) date string * @param {String} date (ISO) Date string */ UTC.prototype.fromString = function (date) { this._time = number2long(+new Date(date)); }; /** * Returns the milliseconds since the Unix epoch (UTC) */ UTC.prototype.toNumber = function () { return long2number(this._time); }; UTC.prototype.toDate = function () { return new Date(long2number(this._time)); }; return UTC; }()); BSON.UTC = UTC; /** * Private, return the size of the given object * @param {Object} obj The object to get the size from * @return {Number} The object size in bytes */ function getObjectSize(obj) { var len = 4 + 1; // handle the obj.length prefix + terminating '0' for (var key in obj) { len += getElementSize(key, obj[key]); } return len; } /** * Private, get the size of the given element * @param {String} name * @param {Object} value * @return {Number} The element size in bytes */ function getElementSize(name, value) { var len = 1; // always starting with 1 for the data type byte if (name) { len += strlen(name) + 1; // cstring: name + '0' termination } if (value === undefined || value === null) { return len; // just the type byte plus name cstring } switch (value.constructor) { case String: return len + 4 + strlen(value) + 1; case Number: if (Math.floor(value) === value) { if (value <= 2147483647 && value >= -2147483647) return len + 4; // 32 bit else return len + 8; // 64 bit } else return len + 8; // 64 bit double & float case Boolean: return len + 1; case Array: case Object: return len + getObjectSize(value); case Int8Array: case Uint8Array: return len + 5 + value.byteLength; case Date: case UTC: return len + 8; case UUID: return len + 5 + 16; case ObjectId: return len + 12; default: // unsupported type return 0; } } /** * Serialize an object to BSON format * @param {Object} object The object to serialize * @return {Uint8Array} An byte array with the BSON representation */ function serialize(object) { var buffer = new Uint8Array(getObjectSize(object)); serializeEx(object, buffer); return buffer; } BSON.serialize = serialize; /** * Private, used by serialize() and is called recursively * @param object * @param buffer * @param i */ function serializeEx(object, buffer, i) { if (i === void 0) { i = 0; } i += int32(buffer.length, buffer, i); if (object.constructor === Array) { for (var j = 0, len = object.length; j < len; j++) { i = packElement(j.toString(), object[j], buffer, i); } } else { for (var key in object) { i = packElement(key, object[key], buffer, i); } } buffer[i++] = 0; // terminating zero return i; } /** * Private, assemble BSON cstring element * @param name * @param buffer * @param offset * @return Element length in bytes */ function cstring(name, buffer, offset) { var cstring = str2bin(name); var clen = cstring.length; buffer.set(cstring, offset); buffer[offset + clen++] = 0; return clen; } /** * Private, assemble BSON int32 element * @param size * @param buffer * @param offset * @return Element length in bytes */ function int32(size, buffer, offset) { buffer[offset++] = (size) & 0xff; buffer[offset++] = (size >>> 8) & 0xff; buffer[offset++] = (size >>> 16) & 0xff; buffer[offset++] = (size >>> 24) & 0xff; return 4; } /** * Private, assemble BSON elements * @param name * @param value * @param buffer * @param i */ function packElement(name, value, buffer, i) { if (value === undefined || value === null) { buffer[i++] = 0x0A; // BSON type: Null i += cstring(name, buffer, i); return i; } switch (value.constructor) { case String: buffer[i++] = 0x02; // BSON type: String i += cstring(name, buffer, i); var size = cstring(value, buffer, i + 4); i += int32(size, buffer, i); return i + size; case Number: if (Math.floor(value) === value) { if (value <= 2147483647 && value >= -2147483647) { /// = BSON.BSON_INT32_MAX / MIN asf. buffer[i++] = 0x10; // BSON type: int32 i += cstring(name, buffer, i); i += int32(value, buffer, i); } else { buffer[i++] = 0x12; // BSON type: int64 i += cstring(name, buffer, i); buffer.set(number2long(value), i); i += 8; } } else { // it's a float / double buffer[i++] = 0x01; // BSON type: 64-bit floating point i += cstring(name, buffer, i); var f = new Float64Array([value]); var d = new Uint8Array(f.buffer); buffer.set(d, i); i += 8; } return i; case Boolean: buffer[i++] = 0x08; // BSON type: Boolean i += cstring(name, buffer, i); buffer[i++] = value ? 1 : 0; return i; case Array: case Object: buffer[i++] = value.constructor === Array ? 0x04 : 0x03; // BSON type: Array / Document i += cstring(name, buffer, i); var end = serializeEx(value, buffer, i); int32(end - i, buffer, i); // correct size return end; case Int8Array: case Uint8Array: buffer[i++] = 0x05; // BSON type: Binary data i += cstring(name, buffer, i); i += int32(value.byteLength, buffer, i); buffer[i++] = 0; // use generic binary subtype 0 buffer.set(value, i); i += value.byteLength; return i; case Date: buffer[i++] = 0x09; // BSON type: UTC datetime i += cstring(name, buffer, i); buffer.set(number2long(value.getTime()), i); i += 8; return i; case UTC: buffer[i++] = 0x09; // BSON type: UTC datetime i += cstring(name, buffer, i); buffer.set(value.buffer(), i); i += 8; return i; case UUID: buffer[i++] = 0x05; // BSON type: Binary data i += cstring(name, buffer, i); i += int32(16, buffer, i); buffer[i++] = 4; // use UUID subtype buffer.set(value.buffer(), i); i += 16; return i; case ObjectId: buffer[i++] = 0x07; // BSON type: ObjectId i += cstring(name, buffer, i); buffer.set(value.buffer(), i); i += 12; return i; case RegExp: buffer[i++] = 0x0B; // BSON type: Regular expression i += cstring(name, buffer, i); i += cstring(value.source, buffer, i); if (value.global) buffer[i++] = 0x73; // s = 'g' if (value.ignoreCase) buffer[i++] = 0x69; // i if (value.multiline) buffer[i++] = 0x6d; // m buffer[i++] = 0; return i; default: return i; // unknown type (ignore element) } } /** * Deserialize (parse) BSON data to an object * @param {Uint8Array} buffer The buffer with BSON data to convert * @param {Boolean} useUTC Optional, if set an UTC object is created for 'UTC datetime', else an Date object. Defaults to false * @return {Object} Returns an object or an array */ function deserialize(buffer, useUTC, i, returnArray) { if (useUTC === void 0) { useUTC = false; } if (i === void 0) { i = 0; } if (returnArray === void 0) { returnArray = false; } // check size if (buffer.length < 5) { // Document error: Size < 5 bytes return undefined; } var size = buffer[i++] | buffer[i++] << 8 | buffer[i++] << 16 | buffer[i++] << 24; if (size < 5 || size > buffer.length) { // Document error: Size mismatch return undefined; } if (buffer[buffer.length - 1] !== 0x00) { // Document error: Missing termination return undefined; } var object = returnArray ? [] : {}; // needed for type ARRAY recursion later for (;;) { // get element type var elementType = buffer[i++]; // read type if (elementType === 0) break; // zero means last byte, exit // get element name var end = i; for (; buffer[end] !== 0x00 && end < buffer.length; end++) ; if (end >= buffer.length - 1) { // Document error: Illegal key name return undefined; } var name_1 = bin2str(buffer.subarray(i, end)); if (returnArray) { name_1 = parseInt(name_1); // convert to number as array index } i = ++end; // skip terminating zero switch (elementType) { case 0x01: // BSON type: 64-bit floating point object[name_1] = (new Float64Array(buffer.slice(i, i += 8).buffer))[0]; // use slice() here to get a new array break; case 0x02: // BSON type: String size = buffer[i++] | buffer[i++] << 8 | buffer[i++] << 16 | buffer[i++] << 24; object[name_1] = bin2str(buffer.subarray(i, i += size - 1)); i++; break; case 0x03: // BSON type: Document (Object) size = buffer[i] | buffer[i + 1] << 8 | buffer[i + 2] << 16 | buffer[i + 3] << 24; object[name_1] = deserialize(buffer, useUTC, i, false); // isArray = false => Object i += size; break; case 0x04: // BSON type: Array 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 object[name_1] = deserialize(buffer, useUTC, i, true); // pass current index & return an array i += size; break; case 0x05: // BSON type: Binary data size = buffer[i++] | buffer[i++] << 8 | buffer[i++] << 16 | buffer[i++] << 24; if (buffer[i++] === 0x04) { // BSON subtype: UUID if (size !== 16) { // Element error: Wrong UUID length return undefined; } object[name_1] = new UUID(buffer.subarray(i, i += size)); } else { // all other subtypes object[name_1] = buffer.slice(i, i += size); // use slice() here to get a new array } break; case 0x06: // BSON type: Undefined (deprecated) object[name_1] = null; break; case 0x07: // BSON type: ObjectId object[name_1] = new ObjectId(buffer.subarray(i, i += 12)); break; case 0x08: // BSON type: Boolean object[name_1] = buffer[i++] === 1; break; case 0x09: // BSON type: UTC datetime object[name_1] = useUTC ? new UTC(buffer.subarray(i, i += 8)) : new Date(long2number(buffer.subarray(i, i += 8))); break; case 0x0A: // BSON type: Null object[name_1] = null; break; case 0x0B: // BSON type: RegExp end = i; // pattern while (end < buffer.length && buffer[end++] !== 0x00) ; if (end >= buffer.length) { // Document error: Illegal key name return undefined; } var pat = bin2str(buffer.subarray(i, end)); i = end; // flags while (end < buffer.length && buffer[end++] !== 0x00) ; if (end >= buffer.length) { // Document error: Illegal key name return undefined; } var flags = bin2str(buffer.subarray(i, end)); i = end; object[name_1] = new RegExp(pat, flags); break; case 0x10: // BSON type: 32-bit integer object[name_1] = buffer[i++] | buffer[i++] << 8 | buffer[i++] << 16 | buffer[i++] << 24; break; case 0x12: // BSON type: 64-bit integer object[name_1] = long2number(buffer.subarray(i, i += 8)); break; default: // Parsing error: Unknown element return undefined; } } return object; } BSON.deserialize = deserialize; ///////////////////////////////////////////////////////////////////////////// // H E L P E R /** * Convert a number to a 64 bit integer representation * @param {Number} value Number to convert * @return {Uint8Array} Converted number */ function number2long(value) { var buf = new Uint8Array(8); if (Math.floor(value) === value) { var TWO_PWR_32 = 4294967296; var lo = (value % TWO_PWR_32) | 0, hi = (value / TWO_PWR_32) | 0; if (value < 0) { lo = ~(-value % TWO_PWR_32) | 0, hi = ~(-value / TWO_PWR_32) | 0; lo = (lo + 1) & 0xffffffff; if (!lo) hi++; } var i = 0; buf[i++] = (lo & 0xff); buf[i++] = (lo >>> 8) & 0xff; buf[i++] = (lo >>> 16) & 0xff; buf[i++] = (lo >>> 24) & 0xff; buf[i++] = (hi & 0xff); buf[i++] = (hi >>> 8) & 0xff; buf[i++] = (hi >>> 16) & 0xff; buf[i] = (hi >>> 24) & 0xff; } else { // it's a float / double var f = new Float64Array([value]); var d = new Uint8Array(f.buffer); buf.set(d); } return buf; } /** * Convert 64 bit integer to Number * @param {Uint8Array} buffer Buffer containing a 64 bit integer as typed array at offset position. LSB is [0], MSB is [7] * @param {Number} offset Offset in buffer, where the integer starts * @return {Number} Converted number */ function long2number(buffer, offset) { if (offset === void 0) { offset = 0; } var TWO_PWR_32 = 4294967296; var lo = buffer[offset++] | buffer[offset++] << 8 | buffer[offset++] << 16 | buffer[offset++] << 24; var hi = buffer[offset++] | buffer[offset++] << 8 | buffer[offset++] << 16 | buffer[offset] << 24; return hi * TWO_PWR_32 + ((lo >= 0) ? lo : TWO_PWR_32 + lo); } var utf8Encoder = new TextEncoder(); var utf8Decoder = new TextDecoder(); /** * Convert a string (UTF-8 encoded) to a byte array * @param {String} str UTF-8 encoded string * @return {Uint8Array} Byte array */ var str2bin = utf8Encoder.encode.bind(utf8Encoder); /** * Convert a byte array to an UTF-8 string * @param {Uint8Array} bin UTF-8 text given as array of bytes * @return {String} UTF-8 Text string */ var bin2str = utf8Decoder.decode.bind(utf8Decoder); /** * Returns the UTF-8 string length in bytes * @param {String} Input string * @return {Number} Stringlength in bytes (not in chars) */ function strlen(str) { return utf8Encoder.encode(str).length; } })(BSON = exports.BSON || (exports.BSON = {})); // namespace BSON