streamstructure
Version:
Parses objects into very compact buffer
465 lines • 22.7 kB
JavaScript
;
function isProperty(key, inObject, ...valueType) {
return key in inObject && (!valueType.length || valueType.includes(typeof inObject[key]));
}
;
function assertIsNotProperty(key, inObject, path, ...valueType) {
const exists = key in inObject;
const propertyType = typeof inObject[key];
const testTypes = !!valueType.length;
if (!exists || testTypes && !valueType.includes(propertyType)) {
if (!testTypes) {
throw new TypeError(`Expected some value but got ${propertyType}: ${path}.${key}`);
}
const expectedTypes = valueType.map(v => `'${v}'`).reduce((c, t, i) => i === 0 ? t : i === 1 ? t + " or " : t + ", ", "");
throw new TypeError(`Expected ${expectedTypes} but got ${propertyType}: ${path}.${key}`);
}
}
;
function assertNotObject(data, path) {
if (!data || (typeof data !== "object" && typeof data !== "function")) {
throw new TypeError(`Expected 'object' but got '${typeof data}': ${path}`);
}
}
class StreamStructure {
/**
* Create a StreamStructure, must be created using the sequence `key: type`
*
* @example //Creating a structure for a simple object `{name: string,age: number}`
* cosnt SS = new StreamStructure("name: string", "age: byte");
*/
constructor(...types) {
this.endian = "BE";
this.typesDefinitions = {};
this.typeConditions = {};
this.preProcessing = {};
this.posProcessing = {};
this.structure = types;
const err = types.find(str => !StreamStructure.typeObjectReader.test(str));
if (err)
throw new Error(`The string "${err}" don't match with pattern "key: type"`);
}
toBuffer(data) {
assertNotObject(data, "");
let outBuffers = [];
/**
* Pega uma valor com um tipo e envia para o buffer
* @param type o tipo da data que foi encaminhada
* @param size a lista em formato de string ex: '[2][1]' | ''
* @param data o valor para ser passado adiante
* @param path caminho a ser utilizado caso ocorra algum erro
*/
const transformVal = (type, size, data, path) => {
// If the type is a array
if (size.length) {
if (!Array.isArray(data)) {
throw new TypeError(`Expected array (${type}${size}) but got "${typeof data}": ${path}`);
}
const [, invertEndian, indexSize, rest] = StreamStructure.typeArrayBreaker.exec(size);
const buff = Buffer.allocUnsafe(+indexSize);
if ((this.endian === "BE") === (!invertEndian)) {
buff.writeUIntBE(data.length, 0, +indexSize);
outBuffers.push(buff);
}
else {
buff.writeUIntLE(data.length, 0, +indexSize);
outBuffers.push(buff);
}
for (let i = 0; i < data.length; i++) {
transformVal(type, rest, data[i], `${path}[${i}]`);
}
return;
}
//make pre-process if can
if (type in this.preProcessing)
data = this.preProcessing[type](data);
//if is another structure
if (type in this.typesDefinitions) {
assertNotObject(data, path);
for (const ObjType of this.typesDefinitions[type]) {
const [, key, ArrType] = StreamStructure.typeObjectReader.exec(ObjType);
const [, type, size] = StreamStructure.typeReader.exec(ArrType);
transformVal(type, size, data[key], `${path}.${key}`);
}
return;
}
//If is a condition
if (type in this.typeConditions) {
// Detect wrong inputs
assertNotObject(data, path);
assertIsNotProperty("type", data, path, "string", "number");
assertIsNotProperty("data", data, path, "object", "function");
// detect if input exist
if (!(data.type in this.typeConditions[type].data))
throw new TypeError(`Don't have any condition in '${type}' when value is '${data.type}'`);
(() => {
const [, ntype, size] = StreamStructure.typeReader.exec(this.typeConditions[type].indexType);
transformVal(ntype, size, data.type, `${path}.key(${type})`);
})();
for (const ObjType of this.typeConditions[type].data[data.type]) {
const [, key, ArrType] = StreamStructure.typeObjectReader.exec(ObjType);
const [, ntype, size] = StreamStructure.typeReader.exec(ArrType);
transformVal(ntype, size, data.data[key], `${path}.${key}`);
}
return;
}
// Detect the types and send to the buffers
switch (type) {
case "boolean": {
if (typeof data !== "boolean")
throw new TypeError(`Expected 'boolean', got '${typeof data}': ${path}`);
let buff = Buffer.allocUnsafe(1);
buff.writeInt8(+data);
return outBuffers.push(buff);
}
case "char": {
if (typeof data !== "string")
throw new TypeError(`Expected 'string', got '${typeof data}': ${path}`);
let buff = Buffer.allocUnsafe(1);
buff.write(data);
return outBuffers.push(buff);
}
case "string": {
if (typeof data !== "string")
throw new TypeError(`Expected 'string', got '${typeof data}': ${path}`);
let buff = Buffer.allocUnsafe(data.length + 2);
buff[`writeInt16${this.endian}`](data.length);
buff.write(data, 2);
return outBuffers.push(buff);
}
case "byte":
case "ubyte": {
const usig = type.startsWith("u");
const sigChar = usig ? "U" : "";
const min = (+usig - 1) * 128;
const max = (+usig + 1) * 128;
if (typeof data !== "number" && typeof data !== "bigint")
throw new TypeError(`Expected 'number' or 'bigint', got '${typeof data}': ${path}`);
if (data < min || data >= max)
throw new RangeError(`The number '${data}' must be in range ${min} ~ ${max - 1}: ${path}`);
let buff = Buffer.allocUnsafe(1);
buff[`write${sigChar}Int8`](Number(data));
return outBuffers.push(buff);
}
case "short":
case "ushort": {
const usig = type.startsWith("u");
const sigChar = usig ? "U" : "";
const min = (+usig - 1) * 32768;
const max = (+usig + 1) * 32768;
if (typeof data !== "number" && typeof data !== "bigint")
throw new TypeError(`Expected 'number' or 'bigint', got '${typeof data}': ${path}`);
if (data < min || data >= max)
throw new RangeError(`The number '${data}' must be in range ${min} ~ ${max - 1}: ${path}`);
let buff = Buffer.allocUnsafe(2);
buff[`write${sigChar}Int16${this.endian}`](Number(data));
return outBuffers.push(buff);
}
case "int":
case "uint": {
const usig = type.startsWith("u");
const sigChar = usig ? "U" : "";
const min = (+usig - 1) * 2147483648;
const max = (+usig + 1) * 2147483648;
if (typeof data !== "number" && typeof data !== "bigint")
throw new TypeError(`Expected 'number' or 'bigint', got '${typeof data}': ${path}`);
if (data < min || data >= max)
throw new RangeError(`The number '${data}' must be in range ${min} ~ ${max - 1}: ${path}`);
let buff = Buffer.allocUnsafe(4);
buff[`write${sigChar}Int32${this.endian}`](Number(data));
return outBuffers.push(buff);
}
case "long":
case "ulong": {
const usig = type.startsWith("u");
const sigChar = usig ? "U" : "";
const min = BigInt(+usig - 1) * 9223372036854775808n;
const max = BigInt(+usig + 1) * 9223372036854775808n;
if (typeof data !== "number" && typeof data !== "bigint")
throw new TypeError(`Expected 'number' or 'bigint', got '${typeof data}': ${path}`);
if (data < min || data >= max)
throw new RangeError(`The number '${data}' must be in range ${min} ~ ${max - 1n}: ${path}`);
let buff = Buffer.allocUnsafe(8);
buff[`writeBig${sigChar}Int64${this.endian}`](BigInt(data));
return outBuffers.push(buff);
}
case "float": {
if (typeof data !== "number")
throw new TypeError(`Expected 'number', got '${typeof data}': ${path}`);
let buff = Buffer.allocUnsafe(4);
buff[`writeFloat${this.endian}`](data);
return outBuffers.push(buff);
}
case "double": {
if (typeof data !== "number")
throw new TypeError(`Expected 'number', got '${typeof data}': ${path}`);
let buff = Buffer.allocUnsafe(8);
buff[`writeDouble${this.endian}`](data);
return outBuffers.push(buff);
}
}
//If don't have registred
throw new TypeError(`Unknown type "${type}"`);
};
for (const ObjType of this.structure) {
const [, key, ArrType] = StreamStructure.typeObjectReader.exec(ObjType);
const [, type, size] = StreamStructure.typeReader.exec(ArrType);
transformVal(type, size, data[key], `.${key}`);
}
return Buffer.concat(outBuffers);
}
fromBuffer(buffer) {
if (!Buffer.isBuffer(buffer))
throw new TypeError(`The input must be a buffer`);
let bufferIndex = 0;
let result = {};
/**
* Picks the buffer value at actual index
* @param type Type of property to be extracted
* @param endian in case of been BigEndian or LowEndian
* @param path path to actual variable (debug)
* @returns value
*/
const getValue = (type, endian, path) => {
let data = {};
if (type in this.typesDefinitions) { //
for (const ObjType of this.typesDefinitions[type]) {
const [, key, ArrType] = StreamStructure.typeObjectReader.exec(ObjType);
const [, type, size] = StreamStructure.typeReader.exec(ArrType);
transformVal(key, type, size, data, `${path}.${key}`);
}
}
else if (type in this.typeConditions) {
const index = getValue(this.typeConditions[type].indexType, this.endian, `${path}.key(${type})`);
if (typeof index !== "string" && typeof index !== "number")
throw new TypeError(`Expected a 'string' or 'number' but got '${typeof index}'`);
if (!(index in this.typeConditions[type].data)) {
console.log(type, path, index);
throw new TypeError(`Don't exist any index '${index}' at type '${type}': ${path}`);
}
for (const ObjType of this.typeConditions[type].data[index]) {
const [, key, ArrType] = StreamStructure.typeObjectReader.exec(ObjType);
const [, type, size] = StreamStructure.typeReader.exec(ArrType);
transformVal(key, type, size, data, `${path}.${key}`);
}
data = { type: index, data: data };
}
else {
if (bufferIndex > buffer.length)
throw new RangeError(`The Buffer suddenly end when reading the type '${type}': ${path}`);
try {
switch (type) {
case "boolean":
bufferIndex += 1;
return !!buffer.readInt8(bufferIndex - 1);
case "char":
bufferIndex += 1;
return buffer.toString("ascii", bufferIndex - 1, bufferIndex);
case "string": {
bufferIndex += 2;
const size = buffer[`readInt16${endian}`](bufferIndex - 2);
bufferIndex += size;
return buffer.toString("ascii", bufferIndex - size, bufferIndex);
}
case "byte":
bufferIndex += 1;
return buffer.readInt8(bufferIndex - 1);
case "ubyte":
bufferIndex += 1;
return buffer.readUInt8(bufferIndex - 1);
case "short":
bufferIndex += 2;
return buffer[`readInt16${endian}`](bufferIndex - 2);
case "ushort":
bufferIndex += 2;
return buffer[`readUInt16${endian}`](bufferIndex - 2);
case "int":
bufferIndex += 4;
return buffer[`readInt32${endian}`](bufferIndex - 4);
case "uint":
bufferIndex += 4;
return buffer[`readUInt32${endian}`](bufferIndex - 4);
case "long":
bufferIndex += 8;
return buffer[`readBigInt64${endian}`](bufferIndex - 8);
case "ulong":
bufferIndex += 8;
return buffer[`readBigUInt64${endian}`](bufferIndex - 8);
case "float":
bufferIndex += 4;
return buffer[`readFloat${endian}`](bufferIndex - 4);
case "double":
bufferIndex += 8;
return buffer[`readDouble${endian}`](bufferIndex - 8);
}
}
catch (err) {
throw new RangeError(`The Buffer suddenly end when reading the type ${type}: ${path}`);
}
}
if (type in this.posProcessing) {
return this.posProcessing[type](data);
}
return data;
};
/**
* Picks 'type' from the buffer and add to the 'data' in property 'key'
* @param key field to be inserted in 'data'
* @param type type to be extracted from buffer
* @param size if is a array, and tell your size. ex: '[2][6]'
* @param data object to be added
* @param path path to actual variable (debug)
*/
const transformVal = (key, type, size, data, path) => {
if (size) {
const [, invertEndian, indexSize, rest] = StreamStructure.typeArrayBreaker.exec(size);
const indexEndian = (this.endian === "BE") === (!invertEndian) ? "BE" : "LE";
let arrayLength;
try {
arrayLength = buffer[`readInt${indexEndian}`](bufferIndex, +indexSize);
}
catch (err) {
throw new RangeError(`The Buffer suddenly end while iterating: ${path}`);
}
bufferIndex += +indexSize;
data[key] = [];
for (let i = 0; i < arrayLength; i++) {
data[key][i] = getValue(type, this.endian, `${path}[${i}]`);
;
}
return;
}
data[key] = getValue(type, this.endian, path);
};
for (const ObjType of this.structure) {
const [, key, ArrType] = StreamStructure.typeObjectReader.exec(ObjType);
const [, type, size] = StreamStructure.typeReader.exec(ArrType);
transformVal(key, type, size, result, `.${key}`);
}
return result;
}
/**
* Creates a Complex type, maded of anothers types.
*
* @param type the type that will be created
* @param structure a sequence of `key: type`
*/
setType(type, ...structure) {
if (typeof type !== "string")
throw new TypeError(`The type must be string`);
const err = structure.find(str => !StreamStructure.typeObjectReader.test(str));
if (err)
throw new Error(`The string '${err}' don't match with pattern 'key: type'`);
this.typesDefinitions[type] = structure;
return this;
}
/**
* Creates a pre-process and post-process for any type, userful for get a better reading out or input.
* @param type the type that will be pre-processed and post-processed
* @param preProcessing the pre-processor used to change this type when is going to transform into buffer
* @param postProcessing the post-processor used to change this type when is going to transform from buffer
*/
setTypeProcess(type, preProcessing, postProcessing) {
if (typeof type !== "string")
throw new TypeError(`The type must be string`);
if (typeof preProcessing !== "function")
throw new TypeError(`The preProcessing must be function`);
if (typeof postProcessing !== "function")
throw new TypeError(`The postProcessing must be function`);
this.preProcessing[type] = preProcessing;
this.posProcessing[type] = postProcessing;
return this;
}
/**
* Sets the type of key from a typeConditional, normally used only with "string" or "byte".
* @param type
* @param indexType
* @returns
*/
setTypeConditionalIndex(type, indexType) {
if (typeof type !== "string")
throw new TypeError(`The type must be string`);
if (typeof indexType !== "string")
throw new TypeError(`The indexType must be string`);
if (type in this.typeConditions)
this.typeConditions[type].indexType = indexType;
else
this.typeConditions[type] = { indexType: indexType, data: {} };
return this;
}
/**
* Creates a type that changes the structure based on the key setted before, usefull for recursive objects
* @param type the type to be created
* @param condition if the key is equal to this argument, will use this structure
* @param structure structure to be used
*/
setTypeConditional(type, condition, ...structure) {
if (typeof type !== "string")
throw new TypeError(`The type must be string`);
if (!["string", "number", "symbol"].includes(typeof condition))
throw new TypeError(`The type must be string or number`);
const err = structure.find(str => !StreamStructure.typeObjectReader.test(str));
if (err)
throw new Error(`The string '${err}' don't match with pattern 'key: type'`);
if (!(type in this.typeConditions))
this.setTypeConditionalIndex(type, "string");
this.typeConditions[type].data[condition] = structure;
return this;
}
/**
* @deprecated mismatch :P, instead use the `SS.setTypeConditionalIndex()`
*/
setTypeCondicionalIndex(type, indexType) {
if (type in this.typeConditions)
this.typeConditions[type].indexType = indexType;
else
this.typeConditions[type] = { indexType: indexType, data: {} };
return this;
}
/**
* @deprecated mismatch :P, instead use the `SS.setTypeConditional()`
*/
setTypeCondicional(type, condition, structure) {
const err = structure.find(str => !StreamStructure.typeObjectReader.test(str));
if (err)
throw new Error(`The structure's string "${err}" don't match with pattern "key: type"`);
if (!(type in this.typeConditions))
this.setTypeConditionalIndex(type, "string");
this.typeConditions[type].data[condition] = structure;
return this;
}
/**
* Set the default endian for the numbers, arrays, etc.
* @param endian the default endian
* @returns
*/
setDefaultEndian(endian) {
if (endian !== "BE" && endian !== "LE")
throw new Error("The endian must be 'BE' or 'LE'");
this.endian = endian;
return this;
}
}
StreamStructure.primitivesLength = Object.freeze({
"boolean": 1,
"char": 1,
"string": 2,
"byte": 1,
"ubyte": 1,
"short": 2,
"ushort": 2,
"int": 4,
"uint": 4,
"long": 8,
"ulong": 8,
"float": 4,
"double": 8,
});
/** Picks the value from `(key): (value[size])` */
StreamStructure.typeObjectReader = /^(\w*)\s*:\s*(\w*\s*(?:\[!?[1-6]\]\s*)*)$/i;
/** Picks the value from `(value)([size])` */
StreamStructure.typeReader = /^(\w*)\s*((?:\[!?[1-6]\]\s*)*)$/i;
/** Breaks the array size from `[(!)(size1)]([size2][size3])` */
StreamStructure.typeArrayBreaker = /\[(!?)([1-6])\]((?:\[!?[1-6]\])*)/;
module.exports = StreamStructure;
//# sourceMappingURL=index.js.map