UNPKG

@ferrisk/bsonfy

Version:

Ultrafast BSON serializer/parser

621 lines (534 loc) 19.4 kB
/////////////////////////////////////////////////////////////////////////////// // \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); // /////////////////////////////////////////////////////////////////////////////// export namespace BSON { /** * BSON module version */ const version: string = '1.0.2'; /** * UUID class */ export class UUID { private _id: Uint8Array; constructor(id: Uint8Array | Array<number>) { this._id = new Uint8Array(id); } buffer(): Uint8Array { return this._id; } } /** * ObjectId class (for mongoDB usage) */ export class ObjectId { private _id: Uint8Array; constructor(id: Uint8Array | Array<number>) { this._id = new Uint8Array(id); } buffer(): Uint8Array { return this._id; } } /** * The UTC class contains the milliseconds since the Unix epoch (1.1.1970 00:00:00 UTC) */ export class UTC { private _time: Uint8Array; constructor(time?: Uint8Array | Array<number> | string) { this._time = (typeof time !== 'string') ? new Uint8Array(time || number2long(Date.now())) : number2long(+new Date(time)); } buffer(): Uint8Array { return this._time; } /** * Convert an (ISO) date string * @param {String} date (ISO) Date string */ fromString(date: string): void { this._time = number2long(+new Date(date)); } /** * Returns the milliseconds since the Unix epoch (UTC) */ toNumber(): number { return long2number(this._time); } toDate(): Date { return new Date(long2number(this._time)); } } /** * 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: Object): number { let len = 4 + 1; // handle the obj.length prefix + terminating '0' for (let 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: string, value: any): number { let 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 */ export function serialize(object: any): Uint8Array { if (!((object.constructor as any) in [Object, Array])) object = {'__v': object} let buffer = new Uint8Array(getObjectSize(object)); serializeEx(object, buffer); return buffer; } /** * Private, used by serialize() and is called recursively * @param object * @param buffer * @param i */ function serializeEx(object: any, buffer: Uint8Array, i: number = 0): number { i += int32(buffer.length, buffer, i); if (object.constructor === Array) { for (let j = 0, len = object.length; j < len; j++) { i = packElement(j.toString(), object[j], buffer, i); } } else { for (let 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: string, buffer: Uint8Array, offset: number): number { let cstring = str2bin(name); let 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: number, buffer: Uint8Array, offset: number): number { 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: string, value: any, buffer: Uint8Array, i: number): number { 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); let 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); let f = new Float64Array([value]); let 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); let 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 */ export function deserialize(buffer: Uint8Array, useUTC: boolean = false, i: number = 0, returnArray: boolean = false): Array<any> | Object { // check size if (buffer.length < 5) { // Document error: Size < 5 bytes return undefined; } let 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; } let object = returnArray ? [] : {}; // needed for type ARRAY recursion later for (;;) { // get element type let elementType = buffer[i++]; // read type if (elementType === 0) break; // zero means last byte, exit // get element name let end = i; for (; buffer[end] !== 0x00 && end < buffer.length; end++); if (end >= buffer.length - 1) { // Document error: Illegal key name return undefined; } let name: any = bin2str(buffer.subarray(i, end)); if (returnArray) { name = parseInt(name); // convert to number as array index } i = ++end; // skip terminating zero switch (elementType) { case 0x01: // BSON type: 64-bit floating point object[name] = (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] = 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] = 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] = 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] = new UUID(buffer.subarray(i, i += size)); } else { // all other subtypes object[name] = buffer.slice(i, i += size); // use slice() here to get a new array } break; case 0x06: // BSON type: Undefined (deprecated) object[name] = null; break; case 0x07: // BSON type: ObjectId object[name] = new ObjectId(buffer.subarray(i, i += 12)); break; case 0x08: // BSON type: Boolean object[name] = buffer[i++] === 1; break; case 0x09: // BSON type: UTC datetime object[name] = useUTC ? new UTC(buffer.subarray(i, i += 8)) : new Date(long2number(buffer.subarray(i, i += 8))); break; case 0x0A: // BSON type: Null object[name] = 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; } let 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; } let flags = bin2str(buffer.subarray(i, end)); i = end; object[name] = new RegExp(pat, flags); break; case 0x10: // BSON type: 32-bit integer object[name] = buffer[i++] | buffer[i++] << 8 | buffer[i++] << 16 | buffer[i++] << 24; break; case 0x12: // BSON type: 64-bit integer object[name] = long2number(buffer.subarray(i, i += 8)); break; default: // Parsing error: Unknown element return undefined; } if (name === '__v') return object[name]; } return object; } ///////////////////////////////////////////////////////////////////////////// // 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: number): Uint8Array { let buf = new Uint8Array(8); if (Math.floor(value) === value) { const TWO_PWR_32 = 4294967296; let 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++; } let 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 let f = new Float64Array([value]); let 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: Uint8Array, offset: number = 0): number { const TWO_PWR_32 = 4294967296; let lo = buffer[offset++] | buffer[offset++] << 8 | buffer[offset++] << 16 | buffer[offset++] << 24; let hi = buffer[offset++] | buffer[offset++] << 8 | buffer[offset++] << 16 | buffer[offset] << 24; return hi * TWO_PWR_32 + ((lo >= 0) ? lo : TWO_PWR_32 + lo); } /** * Convert a string (UTF-8 encoded) to a byte array * @param {String} str UTF-8 encoded string * @return {Uint8Array} Byte array */ function str2bin(str: string): Uint8Array { str = str.replace(/\r\n/g, '\n'); let bin = [], p = 0; for (let i = 0, len = str.length; i < len; i++) { let c = str.charCodeAt(i); if (c < 128) { bin[p++] = c; } else if (c < 2048) { bin[p++] = (c >>> 6) | 192; bin[p++] = (c & 63) | 128; } else { bin[p++] = (c >>> 12) | 224; bin[p++] = ((c >>> 6) & 63) | 128; bin[p++] = (c & 63) | 128; } } return new Uint8Array(bin); } /** * 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 */ function bin2str(bin: Uint8Array): string { let str = '', len = bin.length, i = 0, c, c2, c3; while (i < len) { c = bin[i]; if (c < 128) { str += String.fromCharCode(c); i++; } else if ((c > 191) && (c < 224)) { c2 = bin[i + 1]; str += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); i += 2; } else { c2 = bin[i + 1]; c3 = bin[i + 2]; str += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); i += 3; } } return str; } /** * Returns the UTF-8 string length in bytes * @param {String} Input string * @return {Number} Stringlength in bytes (not in chars) */ function strlen(str: string): number { return encodeURI(str).split(/%..|./).length - 1; } } // namespace BSON