UNPKG

typed-binary-json

Version:

A TBJSON parser and serializer.

1,691 lines (1,524 loc) 69 kB
'use strict'; var fs = require('fs'); function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } // magic number for file type const MAGIC_NUMBER = '.tbj'; const SIZE_MAGIC_NUMBER = 4; const VERSION = 1; // error const ERROR = -1; // primitive types const NULL = 0; const BOOL = 1; const UINT8 = 2; const INT8 = 3; const UINT16 = 4; const INT16 = 5; const UINT32 = 6; const INT32 = 7; const FLOAT32 = 8; const FLOAT64 = 9; // higher-order types const STRING = 10; const ARRAY = 11; const OBJECT = 12; const NULLABLE = 13; const TYPED_ARRAY = 14; const UNKNOWN = 15; // extras const VARIABLE_DEF = 16; const INSTANCE = 17; const SIZE_INT8 = 1; const SIZE_UINT8 = 1; const SIZE_INT16 = 2; const SIZE_UINT16 = 2; const SIZE_INT32 = 4; const SIZE_UINT32 = 4; const SIZE_FLOAT32 = 4; const SIZE_FLOAT64 = 8; // offsets const NULLABLE_OFFSET = 16; // support 15 primitives (NULL is reserved and INSTANCE is only for casting) const TYPED_ARRAY_OFFSET = 32; const TYPE_OFFSET = 48; const PROTOTYPE_OFFSET = 64; // support up to 16 types const NULLABLE_PROTOTYPE_OFFSET = 256; // support up to 192 prototypes const ARRAY_OFFSET = 512; const OBJECT_OFFSET = 4096; // support 4x nested array // legacy offsets const L_NULLABLE_PROTOTYPE_OFFSET = 160; const L_ARRAY_OFFSET = 256; const L_OBJECT_OFFSET = 1024; // defaults const DEFAULT_BUFFER_SIZE = 1048576; const DEFAULT_NUM_ENCODING = FLOAT64; const DEFAULT_STR_ENCODING = 'utf8'; const DEFAULT_X_FACTOR = 2; class Prototype { constructor(definition, prototype = null, parentCode = null, noInherit = false) { this.definition = definition; this.prototype = prototype; this.parentCode = parentCode; this.noInherit = noInherit; } } class BufferReader { constructor(buffer, strEncoding = DEFAULT_STR_ENCODING) { _defineProperty(this, "offset", 0); this.buffer = buffer; this.strEncoding = strEncoding; } read(type) { let data; switch (type) { case BOOL: data = !!this.buffer.readUInt8(this.offset); this.offset += SIZE_UINT8; break; case UINT8: data = this.buffer.readUInt8(this.offset); this.offset += SIZE_UINT8; break; case INT8: data = this.buffer.readInt8(this.offset); this.offset += SIZE_INT8; break; case UINT16: data = this.buffer.readUInt16BE(this.offset); this.offset += SIZE_UINT16; break; case INT16: data = this.buffer.readInt16BE(this.offset); this.offset += SIZE_INT16; break; case UINT32: data = this.buffer.readUInt32BE(this.offset); this.offset += SIZE_UINT32; break; case INT32: data = this.buffer.readInt32BE(this.offset); this.offset += SIZE_INT32; break; case FLOAT32: data = this.buffer.readFloatBE(this.offset); this.offset += SIZE_FLOAT32; break; case FLOAT64: data = this.buffer.readDoubleBE(this.offset); this.offset += SIZE_FLOAT64; break; case STRING: let length; if (this.buffer[this.offset] < 128) { length = this.read(UINT8); } else if (this.buffer[this.offset] < 192) { length = this.read(UINT16) - 32768; } else { length = this.read(UINT32) - 3221225472; } data = this.buffer.toString(this.strEncoding, this.offset, this.offset + length); this.offset += length; break; case UNKNOWN: data = this.read(this.read(UINT8)); } return data; } readBuffer(length) { let buffer = this.buffer.slice(this.offset, this.offset + length); this.offset += length; return buffer; } readFixedLengthString(length) { let data = this.buffer.toString(this.strEncoding, this.offset, this.offset + length); this.offset += length; return data; } readTypedArray(type, length) { let byteOffset = this.buffer.byteOffset + this.offset; let buffer = this.buffer.buffer.slice(byteOffset, byteOffset + length); this.offset += length; switch (type) { case UINT8: return new Uint8Array(buffer); case INT8: return new Int8Array(buffer); case UINT16: return new Uint16Array(buffer); case INT16: return new Int16Array(buffer); case UINT32: return new Uint32Array(buffer); case INT32: return new Int32Array(buffer); case FLOAT32: return new Float32Array(buffer); case FLOAT64: return new Float64Array(buffer); } } /* internal */ nextNullAt() { for (let i = this.offset; i < this.buffer.length; ++i) { if (!this.buffer[i]) { return i; } } throw new Error('BufferReader could not find a null value'); } } const MAX_BYTES_PER_CHAR_UNICODE = 4; const MAX_BYTES_PER_CHAR_ASCII = 1; class BufferWriter { get size() { return this.buffer.length; } constructor(size = DEFAULT_BUFFER_SIZE, xFactor = DEFAULT_X_FACTOR, strEncoding = DEFAULT_STR_ENCODING) { _defineProperty(this, "offset", 0); this.buffer = Buffer.allocUnsafe(size); this.xFactor = xFactor; this.strEncoding = strEncoding; if (strEncoding == 'asci') { this.maxBytesPerChar = MAX_BYTES_PER_CHAR_ASCII; } else { this.maxBytesPerChar = MAX_BYTES_PER_CHAR_UNICODE; } } getBuffer() { return this.buffer.slice(0, this.offset); } grow(size) { do { this.buffer = Buffer.concat([this.buffer, Buffer.allocUnsafe(this.size * Math.floor(this.xFactor / 2))]); } while (size && this.offset + size > this.size); } write(type, val) { switch (type) { case NULL: val = 0; case BOOL: case UINT8: this.checkSize(SIZE_UINT8); this.buffer.writeUInt8(val, this.offset); this.offset += SIZE_UINT8; break; case INT8: this.checkSize(SIZE_INT8); this.buffer.writeInt8(val, this.offset); this.offset += SIZE_INT8; break; case UINT16: this.checkSize(SIZE_UINT16); this.buffer.writeUInt16BE(val, this.offset); this.offset += SIZE_UINT16; break; case INT16: this.checkSize(SIZE_INT16); this.buffer.writeInt16BE(val, this.offset); this.offset += SIZE_INT16; break; case UINT32: this.checkSize(SIZE_UINT32); this.buffer.writeUInt32BE(val, this.offset); this.offset += SIZE_UINT32; break; case INT32: this.checkSize(SIZE_INT32); this.buffer.writeInt32BE(val, this.offset); this.offset += SIZE_INT32; break; case FLOAT32: this.checkSize(SIZE_FLOAT32); this.buffer.writeFloatBE(val, this.offset); this.offset += SIZE_FLOAT32; break; case FLOAT64: this.checkSize(SIZE_FLOAT64); this.buffer.writeDoubleBE(val, this.offset); this.offset += SIZE_FLOAT64; break; case STRING: // empty string if (typeof val != 'string' || !val.length) { this.write(UINT8, 0); // variable length / encoded string } else { // max possible size let size = val.length * this.maxBytesPerChar; let lengthSize = size < 128 ? 1 : size < 16384 ? 2 : 4; this.checkSize(size + lengthSize); // write out string and pre-allocate bytes to hold string length size = this.buffer.write(val, this.offset + lengthSize, size, this.strEncoding); // R00000000 first bit reserved for signaling to use 2 bytes, max string length is: 2^7 = 128 if (lengthSize == 1) { this.buffer.writeUInt8(size, this.offset); // 1R000000 first bit must be 1, second bit reserved for signaling to use 4 bytes, max string length is: 2^16 - (2^15 + 2^14) = 16384 } else if (lengthSize == 2) { this.buffer.writeUInt16BE(size + 32768, this.offset); // 11000000 first two bits must be 1, max string length is: 2^32 - (2^31 + 2^30) = 1073741824 } else { this.buffer.writeUInt32BE(size + 3221225472, this.offset); } this.offset += size + lengthSize; } break; case UNKNOWN: switch (typeof val) { case 'boolean': this.write(UINT8, BOOL); this.write(BOOL, val); break; case 'number': this.write(UINT8, FLOAT64); this.write(FLOAT64, val); break; case 'string': this.write(UINT8, STRING); this.write(STRING, val); break; default: this.write(NULL); } } } writeBuffer(buffer) { this.checkSize(buffer.length); buffer.copy(this.buffer, this.offset); this.offset += buffer.length; } writeFixedLengthString(val) { this.checkSize(val.length * this.maxBytesPerChar); this.offset += this.buffer.write(val, this.offset, val.length * this.maxBytesPerChar, this.strEncoding); } /* internal */ checkSize(size) { if (this.offset + size > this.buffer.length) { this.grow(size); } } } class StreamBufferReader { constructor(stream, size = DEFAULT_BUFFER_SIZE, strEncoding = DEFAULT_STR_ENCODING) { this.stream = stream; this.size = size; this.strEncoding = strEncoding; this.tempSize = size; this.buffer = Buffer.allocUnsafe(size); this.writeOffset = 0; this.readOffset = 0; this.stream.on('data', chunk => { if (this.writeOffset + chunk.length > this.tempSize) { this.stream.pause(); } this.buffer.fill(chunk, this.writeOffset, this.writeOffset + chunk.length); this.writeOffset += chunk.length; if (this.waitingRead) { this.waitingRead(); } }); } readUntilNull(fn) { for (let i = this.readOffset; i < this.buffer.length; ++i) { if (this.buffer[i] == null) { fn(this.buffer.slice(this.offset, i)); this.incReadOffset(i - this.readOffset); } } } read(type, length = 0) { switch (type) { case UINT32: this.readBytes(SIZE_UINT32, readOffset => fn(this.buffer.readUInt32(readOffset))); break; case FLOAT32: this.readBytes(SIZE_FLOAT32, readOffset => fn(this.buffer.readFloat32(readOffset))); break; case STRING: if (length) { this.readBytes(length, readOffset => fn(this.buffer.toString(this.strEncoding, readOffset, length))); } else { this.readUntilNull(); } } } /* internal */ incReadOffset(length) { this.readOffset += length; if (this.readOffset > this.size) { this.writeOffset = this.buffer.length - this.writeOffset; this.readOffset = 0; this.newBuffer = Buffer.allocUnsafe(this.size); this.newBuffer.fill(this.offset, this.buffer.length); this.buffer = this.newBuffer; if (this.stream.isPaused()) { this.stream.resume(); } } } readBytes(length) { if (this.readOffset + length > this.writeOffset) { return new Promise((res, rej) => { if (this.size < this.readOffset + length) { this.tmpSize = this.readOffset + length; } this.waitingRead = () => { this.tempSize = this.size; this.readBytes(length, fn); }; }); } else { let readOffset = this.readOffset; this.incReadOffset(length); return readOffset; } } } class StreamBufferWriter extends BufferWriter { constructor(stream, size, xFactor, strEncoding) { super(size, xFactor, strEncoding); _defineProperty(this, "streamIndex", 0); _defineProperty(this, "streamReady", true); this.stream = stream; } /* internal */ flush() { this.streamReady = this.stream.write(this.buffer.slice(this.streamIndex, this.offset), () => { this.streamReady = true; }); if (this.streamReady) { this.offset = 0; this.streamIndex = 0; } else { this.streamIndex = this.offset; } return this.streamReady; } checkSize(size) { if (this.offset + size > this.size) { if (this.streamReady && !this.flush()) { this.grow(); } else { this.grow(); } } } } /** * Return the parent of a prototype. * * @param { function } prototype - prototype to check for parent of */ function getParent(prototype) { let parent = prototype ? Object.getPrototypeOf(prototype) : null; return parent && parent.name ? parent : null; } class Type { constructor(ref, size) { // a type reference (name) always starts with @ if (typeof ref == 'string') { ref = ref[0] == '@' ? ref : '@' + ref; } else { ref = null; } this.ref = ref; this.size = size || 0; } serialize() {} deserialize() {} } class BigIntType extends Type { constructor(ref) { super(ref || BigIntType.ref, BigIntType.size); } serialize(bigint) { let buffer = Buffer.allocUnsafe(BigIntType.size); buffer.writeBigInt64BE(bigint); return buffer; } deserialize(buffer) { return buffer.readBigInt64BE(); } } BigIntType.ref = '@BigInt'; BigIntType.size = 8; class DateType extends Type { constructor(ref) { super(ref || DateType.ref, DateType.size); } serialize(date) { let buffer = Buffer.allocUnsafe(DateType.size); buffer.writeBigInt64BE(BigInt(date.getTime())); return buffer; } deserialize(buffer) { return new Date(Number(buffer.readBigInt64BE())); } } DateType.ref = '@Date'; DateType.size = 8; class RegexType extends Type { constructor(ref) { super(ref || RegexType.ref); } serialize(regex) { let string = regex.toString(); let buffer = Buffer.allocUnsafe(string.length); buffer.write(string, 'utf8'); return buffer; } deserialize(buffer) { let string = buffer.toString('utf8'); let flagIndex = string.lastIndexOf('/'); return new RegExp(string.slice(1, flagIndex), string.slice(flagIndex + 1)); } } RegexType.ref = '@Regex'; /** * Cast a plain object into the typed object it represents. Only supports prototype definitions, not strings. * * @param { object } obj - object to parse * @param { function } prototype - prototype to cast into * @param { array } types - array of types with deserializers * @param { bool } castPrimitives - also cast primitives (bool, number, string) * @param { bool } freeMemory - set obj properties to undefined as the obj is cast (slower, but frees up memory) */ function cast(obj, prototype, types, castPrimitives = false, freeMemory = false, definitions = {}) { // plain object or array with a definition (ignore prototyped) if (prototype && (typeof prototype == 'function' || typeof prototype == 'object')) { let isNonNullObject = typeof obj == 'object' && obj; let isArray = Array.isArray(prototype); let isArrayTypeDef = Array.isArray(prototype) && prototype.length == 2; // array if (Array.isArray(obj) && isArray) { let typedObj; // typed array if (isArrayTypeDef && prototype[0] == ARRAY) { typedObj = new Array(obj.length); for (let i = 0; i < obj.length; ++i) { typedObj[i] = cast(obj[i], prototype[1], types, castPrimitives, freeMemory, definitions); if (freeMemory) { obj[i] = undefined; } } // unknown array } else { typedObj = new Array(prototype.length); for (let i = 0; i < prototype.length; ++i) { typedObj[i] = cast(obj[i], prototype[i], types, castPrimitives, freeMemory, definitions); if (freeMemory) { obj[i] = undefined; } } } return typedObj; // qualified type } else if (isArrayTypeDef) { switch (prototype[0]) { // uniform value object case OBJECT: let typedObj = {}; if (isNonNullObject) { for (let key in obj) { typedObj[key] = cast(obj[key], prototype[1], types, castPrimitives, freeMemory, definitions); if (freeMemory) { obj[key] = undefined; } } } return typedObj; // nullable object case NULLABLE: return obj == null ? null : cast(obj, prototype[1], types, castPrimitives, freeMemory, definitions); // variable def, won't know this when casting case VARIABLE_DEF: return obj; // instance object case INSTANCE: return cast(obj, prototype[1], types, castPrimitives, freeMemory, definitions); } // non-prototyped object } else if (!obj || !obj.constructor || obj.constructor.prototype == Object.prototype) { let tbjson = prototype.tbjson; // prototype is tbjson with a definition if (tbjson && tbjson.definition) { let typedObj; let definition; // call the cast function to instantiate the correct prototype if (tbjson.cast) { return cast(obj, tbjson.cast(obj), types, castPrimitives, freeMemory, definitions); // use the passed prototype } else { typedObj = new prototype(); } if (isNonNullObject) { // use map if (definitions[prototype.name]) { definition = definitions[prototype.name]; // check for parent } else { definition = tbjson.definition; // only check for a parent if the definition is an object if (typeof definition == 'object') { for (let parent = prototype; parent = getParent(parent);) { if (!parent.tbjson || !parent.tbjson.definition) { break; } definition = Object.assign({}, parent.tbjson.definition, definition); } definitions[prototype.name] = definition; } } // fallback to the prototype if definition is an object if (definition == OBJECT) { for (let key in typedObj) { if (key in obj) { typedObj[key] = obj[key]; if (freeMemory) { obj[key] = undefined; } } } // continue deeper } else { for (let key in definition) { if (key in obj) { typedObj[key] = cast(obj[key], definition[key], types, castPrimitives, freeMemory, definitions); if (freeMemory) { obj[key] = undefined; } } } } } // call the build function for post construction if (tbjson.build) { tbjson.build(typedObj); } return typedObj; // prototype is a raw definition } else { let typedObj = {}; if (isNonNullObject) { for (let key in prototype) { if (key in obj) { typedObj[key] = cast(obj[key], prototype[key], types, castPrimitives, freeMemory, definitions); if (freeMemory) { obj[key] = undefined; } } } } return typedObj; } } } // cast type from its base64 data if (types && typeof prototype == 'string' && typeof obj == 'string' && prototype[0] == '@' && types[prototype]) { obj = types[prototype].deserialize(Buffer.from(obj, 'base64')); // cast primitive (allow for null) } else if (castPrimitives && typeof prototype == 'number' && prototype < ARRAY) { if (typeof obj != 'boolean' && typeof obj != 'number' && typeof obj != 'string') { obj = null; // bool } else if (prototype == BOOL) { obj = !!obj; // string } else if (prototype == STRING) { obj = '' + obj; // number } else { obj = +obj; } } // primitive, untyped, or prototyped return obj; } /** * Clone the typed object into a prototyped object ignoring typing rules, but obeying which properties should be ignored. * * @param { string } obj - object to serialize */ function clone(obj, definitions = {}) { // object or array if (obj && typeof obj == 'object') { // array if (Array.isArray(obj)) { let retObj = new Array(obj.length); for (let i = 0; i < obj.length; ++i) { retObj[i] = clone(obj[i], definitions); } return retObj; // typed array } else if (ArrayBuffer.isView(obj)) { return obj.slice(); // object } else { let retObj = {}; // typed if (typeof obj.constructor == 'function' && obj.constructor.tbjson && obj.constructor.tbjson.definition) { let definition = definitions[obj.constructor.name]; // do a lookup for the parent definitions and flatten into one if (!definition) { definition = obj.constructor.tbjson.definition; for (let parent = obj.constructor; parent = getParent(parent);) { if (!parent.tbjson || !parent.tbjson.definition) { break; } definition = Object.assign({}, parent.tbjson.definition, definition); } definitions[obj.constructor.name] = definition; } let constructor = obj.constructor; // unbuild if (constructor.tbjson.unbuild) { obj = constructor.tbjson.unbuild(obj); } // custom clone function if (constructor.tbjson.clone) { retObj = constructor.tbjson.clone(obj); // generic clone function } else { for (let key in definition) { retObj[key] = clone(obj[key], definitions); } // cast retObj = cast(retObj, constructor); } // date object } else if (obj instanceof Date) { retObj = new Date(obj.getTime()); // plain } else { for (let key in obj) { retObj[key] = clone(obj[key], definitions); } } return retObj; } } // primitive return obj; } /** * Return the flattened TBJSON definition. For prototypes that have parents. * * @param { obj } obj - object to compute definition of */ function definition(obj) { if (obj && typeof obj == 'object' && obj.constructor.tbjson && obj.constructor.tbjson.definition) { let definition = obj.constructor.tbjson.definition; for (let parent = obj.constructor; parent = getParent(parent);) { if (!parent.tbjson || !parent.tbjson.definition) { break; } definition = Object.assign({}, parent.tbjson.definition, definition); } return definition; } } /** * Convert a validation result into an array with path selectors and errors. * * @param { array } validationResult - validation result to flatten */ function flattenValidation(validationResult) { return validationResult[1] > 0 ? flattenObj(validationResult[0]) : []; } /* internal */ function flattenObj(obj, errors = [], path = []) { // recurse if (typeof obj == 'object' && obj != null) { for (let key in obj) { flattenObj(obj[key], errors, path.concat(key)); } // add to array } else { errors.push([path, obj]); } return errors; } /** * Serialize the typed object into a plain object ignoring typing rules, but obeying which properties should be ignored. * * @param { string } obj - object to serialize */ function serialize(obj, definitions = {}) { // object or array if (obj && typeof obj == 'object') { // array if (Array.isArray(obj)) { let retObj = new Array(obj.length); for (let i = 0; i < obj.length; ++i) { retObj[i] = serialize(obj[i], definitions); } return retObj; // typed array (no need to check elements as they must all be primitives) } else if (ArrayBuffer.isView(obj)) { let retObj = new Array(obj.length); for (let i = 0; i < obj.length; ++i) { retObj[i] = obj[i]; } return retObj; // object } else { let retObj = {}; // typed if (typeof obj.constructor == 'function' && obj.constructor.tbjson && obj.constructor.tbjson.definition) { let definition = definitions[obj.constructor.name]; // do a lookup for the parent definitions and flatten into one if (!definition) { definition = obj.constructor.tbjson.definition; for (let parent = obj.constructor; parent = getParent(parent);) { if (!parent.tbjson || !parent.tbjson.definition) { break; } definition = Object.assign({}, parent.tbjson.definition, definition); } definitions[obj.constructor.name] = definition; } let constructor = obj.constructor; // unbuild if (constructor.tbjson.unbuild) { obj = constructor.tbjson.unbuild(obj); } for (let key in definition) { retObj[key] = serialize(obj[key], definitions); } // plain } else { for (let key in obj) { retObj[key] = serialize(obj[key], definitions); } } return retObj; } } // primitive return obj; } /** * Check a prototype instance (or plain object with specified proto) for invalid fields using the TBJSON definition when available. * Return a nested object mirroring the source object with errors. * * @param { object } obj - object to check * @param { function } prototype - prototype to to treat object as * @param { object } options - options */ function validate(obj, prototype = null, options = {}, definitions = {}, errorCount = 0) { let walkObj; let walkProto; // validate type if (typeof prototype == 'number') { if (!validTypedValue(obj, prototype, options)) { return [prototype, errorCount + 1]; } // compound or recursive check } else { let isArray = Array.isArray(prototype); let isArrayTypeDef = Array.isArray(prototype) && prototype.length == 2; // array if (isArray && (Array.isArray(obj) || isTypedArray(obj))) { // typed array if (isArrayTypeDef && (prototype[0] == ARRAY || prototype[1] == TYPED_ARRAY)) { walkObj = obj; walkProto = prototype[1]; // unknown array } else { walkObj = prototype; } // qualified type } else if (isArrayTypeDef) { switch (prototype[0]) { // uniform value object case OBJECT: // cannot be null if (typeof obj == 'object' && obj) { walkObj = obj; walkProto = prototype[1]; // a null object must be marked nullable } else { errors = OBJECT; errorCount++; } break; // nullable object case NULLABLE: // ignore if null if (obj != null) { // ignore nullable nan if (!(options.allowNullableNaN && typeof prototype[1] == 'number' && prototype[1] >= UINT8 && prototype[1] <= FLOAT64 && Number.isNaN(obj))) { return validate(obj, prototype[1], options, definitions, errorCount); } } break; // instance object case INSTANCE: return validate(obj, prototype[1], options, definitions, errorCount); } // object } else if (typeof obj == 'object' && obj) { let definition; if (!prototype) { prototype = obj.constructor; } // prototype is tbjson with a definition if (typeof prototype == 'function' && prototype.tbjson) { // call the validate function for custom validation if (prototype.tbjson.validate) { return tbjson.validate(obj, options, errorCount); // use the passed prototype } else { // use map if (definitions[prototype.name]) { definition = definitions[prototype.name]; // check for parent } else { definition = prototype.tbjson.definition; // only check for a parent if the definition is an object if (typeof definition == 'object') { for (let parent = prototype; parent = getParent(parent);) { if (!parent.tbjson || !parent.tbjson.definition) { break; } definition = Object.assign({}, parent.tbjson.definition, definition); } definitions[prototype.name] = definition; } } } // definition object } else if (typeof prototype == 'object' && prototype) { definition = prototype; // pseudo prototype } else if (typeof prototype == 'string') { definition = definitions[prototype]; } if (definition) { walkObj = definition; prototype = definition; } } } // recurse into the object if (walkObj) { let errors = {}; let inputErrorCount = errorCount; for (let key in walkObj) { let [subErrors, subErrorCount] = validate(obj[key], walkProto || prototype[key], options, definitions, errorCount); if (subErrorCount > errorCount) { errors[key] = subErrors; errorCount = subErrorCount; } if (options.returnOnNthError && errorCount >= options.returnOnNthError) { break; } } return [errorCount > inputErrorCount ? errors : null, errorCount]; // no errors } else { return [null, 0]; } } /* internal */ /** * Return true if the value passed is a typed array. * * @param { * } val - val to check */ function isTypedArray(val) { if (typeof val == 'object' && val) { return val instanceof Uint8Array || val instanceof Int8Array || val instanceof Uint16Array || val instanceof Int16Array || val instanceof Uint32Array || val instanceof Int32Array || val instanceof Float32Array || val instanceof Float64Array; } else { return false; } } /** * Check if a value conforms to a primitive type. * * @param { * } val - value to check * @param { number } type - the tbjson primitive type * @param { object } options - options */ function validTypedValue(val, type, options = {}) { switch (type) { case NULL: return val == null; case BOOL: return typeof val == 'boolean' || val === 0 || val === 1; case INT8: return typeof val == 'number' && (Number.isNaN(val) ? !!options.allowNaN : val >= -128 && val <= 127); case UINT8: return typeof val == 'number' && (Number.isNaN(val) ? !!options.allowNaN : val >= 0 && val <= 255); case INT16: return typeof val == 'number' && (Number.isNaN(val) ? !!options.allowNaN : val >= -32768 && val <= 32767); case UINT16: return typeof val == 'number' && (Number.isNaN(val) ? !!options.allowNaN : val >= 0 && val <= 65535); case INT32: return typeof val == 'number' && (Number.isNaN(val) ? !!options.allowNaN : val >= -2147483648 && val < 2147483647); case UINT32: return typeof val == 'number' && (Number.isNaN(val) ? !!options.allowNaN : val >= 0 && val <= 4294967295); case FLOAT32: case FLOAT64: return typeof val == 'number' && (!!options.allowNaN || !Number.isNaN(val)); case STRING: return typeof val == 'string' || !!options.allowNullString && val == null; case ARRAY: return Array.isArray(val); case OBJECT: return typeof val == 'object' && val != null; } return true; } /** * Tbjson * * A JS TBJSON serializer and parser. */ class Tbjson { constructor(types = [], prototypes = [], offsets = {}, options = {}) { _defineProperty(this, "version", VERSION); // for registered types (primitives) _defineProperty(this, "typeRefs", {}); _defineProperty(this, "types", {}); // for registered prototypes (classes) _defineProperty(this, "protoRefs", {}); _defineProperty(this, "protos", {}); // for plain objects that are inside of known prototypers _defineProperty(this, "objs", {}); // for variable definitions _defineProperty(this, "variableDefs", {}); // binary definition tree _defineProperty(this, "root", null); // counters for converting types and prototypes to an incrementing numeric value _defineProperty(this, "nextObjCode", 0); _defineProperty(this, "nextTypeCode", TYPE_OFFSET); _defineProperty(this, "nextProtoCode", void 0); _defineProperty(this, "finalized", false); // default offsets _defineProperty(this, "offsets", { prototype: PROTOTYPE_OFFSET, nullablePrototype: NULLABLE_PROTOTYPE_OFFSET, array: ARRAY_OFFSET, object: OBJECT_OFFSET }); // default options _defineProperty(this, "options", { bufferSize: DEFAULT_BUFFER_SIZE, numEncoding: DEFAULT_NUM_ENCODING, strEncoding: DEFAULT_STR_ENCODING, xFactor: DEFAULT_X_FACTOR }); this.offsets = { ...this.offsets, ...offsets }; this.options = { ...this.options, ...options }; this.nextProtoCode = this.offsets.prototype; this.registerTypes(types); this.registerPrototypes(prototypes); } /*-----------------------------------------------------------------------*/ /* registers */ /** * Register a variable definition so that any prototypes with the same variable definition id are replaced before serializing. * * @param { number | string } id - the identifier of this variable definition * @param { obj } def - the definition to set to */ registerVariableDef(id, def) { // format the definition def = def ? this.fmtDef(def) : null; if (def == ERROR) { throw new Error(`Invalid definition for variable: ${id}`); } this.variableDefs[id] = def; } /** * Register a pseudo prototype, rather a variable definition that should be treated as if it were a prototype (to support nullable, object, and array). * * @param { number | string } id - the identifier of this pseduo prototype * @param { obj } def - the definition to set to */ registerPseudoPrototype(id, def) { let code = this.protoRefs[id]; // already registered if (code && this.protos[this.protoRefs[id]]) { return; } // format the definition def = def ? this.fmtDef(def) : null; if (def == ERROR) { throw new Error(`Invalid definition for variable: ${id}`); } // set the code if not already set if (!code) { code = this.nextProtoCode++; this.protoRefs[id] = code; } this.protos[code] = new Prototype(def); } /** * Register a prototype / class or plain objecct for serilization and deserialization. * If using Class.tbjson = { ... } you must call this for each class, and then call finalizePrototypes for inheritance to work. * * Example: * * Tbjson.registerPrototype(Point); // point must have tbjson set on it: Point.tbjson = { definition: ... } * * Tbjson.registerPrototype({ * prototype: Point1, * definition: { * x: Tbjson.TYPES.FLOAT32, * y: Tbjson.TYPES.FLOAT32 * }, * reference: 'Point', * parentReference: Point0 * }); * * Tbjson.registerPrototype({ * reference: Point, * definition: { * x: Tbjson.TYPES.FLOAT32, * y: Tbjson.TYPES.FLOAT32 * }); * * @param { function | object } prototype - class / prototype constructor or a plain object that represents one */ registerPrototype(prototype) { // check if finalized if (this.finalized) { if (typeof prototype == 'function' && prototype.tbjson) { return this.protoRefs[prototype.name]; } return; } // a prototype if (typeof prototype == 'function') { // check if it's a known tbjson prototype if (prototype.tbjson) { // TODO: REMOVE THIS if (!prototype.tbjson.definition) { throw new Error(`Missing definition for "${prototype.name}"`); } prototype = { prototype: prototype, ...prototype.tbjson }; } else { prototype = { prototype }; } } // if the ref is not set, use the name if (!prototype.reference) { prototype.reference = prototype.prototype.name; } let code = this.protoRefs[prototype.reference]; // assign a new reference and definition if (!code) { code = this.nextProtoCode++; this.protoRefs[prototype.reference] = code; } // this code has not been defined if (!this.protos[code] || !this.protos[code].definition) { let parentCode; // get the parent code if (prototype.definition) { let parent = !prototype.noInherit && prototype.parentReference ? prototype.parentReference : getParent(prototype.prototype); parentCode = parent ? this.registerPrototype(parent) : null; } // format the definition let definition = prototype.definition ? this.fmtDef(prototype.definition) : null; if (definition == ERROR) { throw new Error(`Invalid definition for: ${prototype.prototype.name}`); } // set the prototype this.protos[code] = new Prototype(definition, prototype.prototype, parentCode, prototype.noInherit); } return code; } /** * Register an array of prototypes. * * Example: * * [{ * constructor: Point, * definition: { * x: Tbjson.TYPES.FLOAT32, * y: Tbjson.TYPES.FLOAT32, * z: Tbjson.TYPES.FLOAT32 * } * }, { * constructor: Line, * reference: 'Line2', * parentReference: 'Line1', * noInherit: true, * definition: { * point1: 'Point', * point2: 'Point' * } * }] * * @param { []object } prototypes - array of prototypes */ registerPrototypes(prototypes = []) { for (let prototype of prototypes) { this.registerPrototype(prototype); } } /** * Register a type. * * Example: * * { * ref: 'Float48', * serialize: (buffer, data) => { ... }, * deserialize: (buffer) => { ... } * } * * @param { object } type - type to add */ registerType(type) { let code = this.typeRefs[type.ref]; if (!code) { code = this.nextTypeCode++; this.typeRefs[type.ref] = code; this.types[code] = type; } return code; } /** * Register types. * * Example: * * [{ * ref: 'Float48', * serializer: function(buffer, data) { ... }, * deserializer: function(buffer) { ... } * }] * * @param { []object } types - array of types to register */ registerTypes(types = []) { for (let type of types) { this.registerType(type); } } /** * If using inheritance, this must be called before serialization to update definitions. */ finalizePrototypes() { let finalizedProtos = {}; while (Object.keys(finalizedProtos).length < Object.keys(this.protos).length) { for (let code in this.protos) { // don't run on finalized prototypes if (finalizedProtos[code]) { continue; } let prototype = this.protos[code]; // finalize if there is no parent code or if the prototype is set to not inherit if (!prototype.parentCode || prototype.noInherit) { finalizedProtos[code] = true; continue; } // throw an error if a parent code is missing if (!this.protos[prototype.parentCode]) { throw new Error('Missing a parent prototype or definition'); } // parent is finalized, so this can be to if (finalizedProtos[prototype.parentCode]) { // if the definition isn't an object, just use it and ignore any parent definitions if (typeof prototype.definition == 'object') { prototype.definition = Object.assign({}, this.protos[prototype.parentCode].definition, prototype.definition); } finalizedProtos[code] = true; } } } this.finalized = true; } /*-----------------------------------------------------------------------*/ /* serializers */ /** * Serialize the obj to a buffer. Fastest, but uses the most memory. * * @param { object } obj - object to serialize */ serializeToBuffer(obj) { try { this.processVariableDefs(); // make a writer this.writer = new BufferWriter(this.options.bufferSize, this.options.xFactor, this.options.strEncoding); // process the obj this.root = this.serialize(obj); // add the header to the front return Buffer.concat([this.getHeaderAsBuffer(), this.writer.getBuffer()]); } catch (e) { e.message = 'Tbjson failed to serialize to the buffer: ' + e.message; throw e; } } /** * Serialize the object to the stream. Slower, but uses the least memory. * * @param { stream } stream - stream to serialize to * @param { object } obj - object to serialize */ serializeToStream(stream, obj) { try { this.processVariableDefs(); // make a writer this.writer = new StreamBufferWriter(stream, this.options.bufferSize, this.options.xFactor, this.options.strEncoding); // process the obj this.root = this.serialize(obj); // flush and cleanup this.writer.flush(); this.writer = null; } catch (e) { e.message = 'Tbjson failed to serialize to the stream: ' + e.message; throw e; } } /** * Serialize the object to a file. Opens as a write stream, so it's slower and uses less memory. * * @param { string } filename - filename / path to write to * @param { object } obj - object to serialize */ serializeToFile(filename, obj) { return new Promise((res, rej) => { try { this.processVariableDefs(); let tempFilename = `${filename}.tmp`; // write the data to a tmp file let writeStream = fs.createWriteStream(tempFilename, 'binary'); this.serializeToStream(writeStream, obj); writeStream.end(); // write the final file writeStream = fs.createWriteStream(filename, 'binary'); // write the header writeStream.write(this.getHeaderAsBuffer()); // pipe the tmp file to the final file let readStream = fs.createReadStream(tempFilename, 'binary'); readStream.pipe(writeStream); readStream.on('end', () => { // cleanup fs.unlinkSync(tempFilename); res(); }); } catch (e) { e.message = `Tbjson Failed to serialize object to "${filename}": ` + e.message; rej(e); } }); } /*-----------------------------------------------------------------------*/ /* parsers */ /** * Parse a TBJSON containing buffer into ab object. Fastest, but uses the most memory. * * @param { buffer } buffer - buffer to read from * @param { array } selector - anarray that indicates the selected object path */ parseBuffer(buffer, selector = null) { try { if (!buffer) { throw new Error('Null buffer passed in'); } this.reader = new BufferReader(buffer, this.options.strEncoding); // validate the buffer type if (this.reader.readFixedLengthString(SIZE_MAGIC_NUMBER) != MAGIC_NUMBER) { throw new Error('Buffer is not a Typed Binary JSON format'); } // get the header length let headerLength = this.reader.read(UINT32); // read and parse the header this.parseHeader(this.reader.readFixedLengthString(headerLength)); // construct the object if (selector) { return this.parseAtSelection(this.root, selector); } else { return this.parse(this.root); } } catch (e) { e.message = 'Tbjson failed to parse the buffer: ' + e.message; throw e; } } /** * TODO * Parse a TBJSON containing stream into an object. Slower, but uses the least memory. * * @param { stream } stream - stream to read from * @param { array } selector - anarray that indicates the selected object path */ parseStream(stream, selector = null) { return new Promise(async (res, rej) => { this.reader = new StreamBufferReader(stream); // validate the stream type if ((await this.reader.readFixedLengthString(SIZE_MAGIC_NUMBER)) != MAGIC_NUMBER) { rej(new Error('Stream is not a Typed Binary JSON format')); } // get the header length let headerLength = await this.reader.read(UINT32); // read and parse the header this.parseHeader(await this.reader.readFixedLengthString(headerLength)); // construct the object if (selector) { res(await this.parseAtSelection(this.root, selector)); } else { res(await this.parse(this.root)); } }); } /** * Parse a TBJSON file into the object it represents. Faster, but uses more memory. * * @param { string } filename - filename / path to read from * @param { array } selector - anarray that indicates the selected object path */ parseFileAsBuffer(filename, selector = null) { try { return this.parseBuffer(fs.readFileSync(filename), selector); } catch (e) { e.message = `Tbjson failed to parse "${filename}": ` + e.message; throw e; } } /** * Parse a TBJSON file into the object it represents. Slower, but uses less memory. * * @param { string } filename - filename / path to read from * @param { array } selector - anarray that indicates the selected object path */ async parseFileAsStream(filename, selector = null) { try { return await this.parseStream(fs.createReadStream(filename), selector); } catch (e) { e.message = `Tbjson failed to parse "${filename}": ` + e.message; throw e; } } /*-----------------------------------------------------------------------*/ /* helpers */ /** * Get the header object after serialization. * Useful if you are writing your custom own stream. */ getHeader() { // get the type serializers / deserializers let typeSizes = {}; for (let code in this.types) { typeSizes[code] = this.types[code].size; } // get the prototype definitions let protoDefs = {}; for (let code in this.protos) { protoDefs[code] = this.protos[code].definition ? this.protos[code].definition : null; } return { version: VERSION, offsets: this.offsets, typeRefs: this.typeRefs, typeSizes: typeSizes, protoRefs: this.protoRefs, protoDefs: protoDefs, objs: this.objs, root: this.root }; } /** * Get the header object as a buffer. * Useful if you are writing your custom format. */ getHeaderAsBuffer() { try { // header string let headerStr = JSON.stringify(this.getHeader()); // make a new buffer, add the header, append the binary let buffer = new BufferWriter(SIZE_MAGIC_NUMBER + SIZE_UINT32 + headerStr.length); // str - magic number buffer.writeFixedLengthString(MAGIC_NUMBER); // uint32 - header length buffer.write(UINT32, Buffer.byteLength(headerStr, this.strEncoding)); // str - header buffer.writeFixedLengthString(headerStr); return buffer.getBuffer(); } catch (e) { e.message = 'Tbjson failed to create a buffer for the header: ' + e.message; throw e; } } /** * Parse a TBJSON header from a string. * Useful if you are writing your own deserializer. * * @param { string } headerStr - string containing the encoded JSON header */ parseHeader(headerStr) { try { let header = JSON.parse(headerStr); this.version = header.version || 0; // types this.typeRefs = header.typeRefs; for (let code in header.typeSizes) { if (this.types[code]) { this.types[code].size = header.typeSizes[code]; } else { this.types[code] = new Type(undefined, header.typeSizes[code]); } } // prototypes (preserve proto constructors for typed parsing) this.protoRefs = header.protoRefs; for (let code in header.protoDefs) { if (this.protos[code]) { this.protos[code].definition = header.protoDefs[code]; } else { this.protos[code] = new Prototype(header.protoDefs[code]); } } // unknown objects this.objs = header.objs; // set the root this.root = header.root; // offsets if (header.offsets) { this.offsets = header.offsets; // legacy file, use old offsets } else { this.offsets = { prototype: PROTOTYPE_OFFSET, nullablePrototype: L_NULLABLE_PROTOTYPE_OFFSET, array: L_ARRAY_OFFSET, object: L_OBJECT_OFFSET }; } } catch (e) { e.messa