UNPKG

streamstructure

Version:
512 lines (409 loc) 23.3 kB
interface stringtotype { string: string; number: number; boolean: boolean; bigint: bigint; object: object; undefined: undefined; symbol: symbol; function: Function; } function isProperty<K extends string, V extends (keyof stringtotype)[]>(key: K, inObject: object, ...valueType: V): inObject is typeof inObject & {[_ in K]: V["length"] extends 0 ? unknown : stringtotype[V[number]]} { return key in inObject && (!valueType.length || valueType.includes(typeof (inObject as {[_ in K]: unknown})[key])); }; function assertIsNotProperty<K extends string, V extends (keyof stringtotype)[]>(key: K, inObject: object, path: string, ...valueType: V): asserts inObject is typeof inObject & {[_ in K]: V["length"] extends 0 ? unknown : stringtotype[V[number]]} { const exists = key in inObject; const propertyType = typeof (inObject as {[_ in K]: unknown})[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: any, path: string): asserts data is Record<string, unknown> { if (!data || (typeof data !== "object" && typeof data !== "function")) { throw new TypeError(`Expected 'object' but got '${typeof data}': ${path}`); } } class StreamStructure { public structure: string[]; private endian: "BE" | "LE" = "BE"; private typesDefinitions: Record<string, string[]> = {} private typeConditions: Record<string, { indexType: string, data: Record<string | number | symbol, string[]> }> = {} private preProcessing: Record<string, (value: unknown) => unknown> = {} private posProcessing: Record<string, (value: unknown) => unknown> = {} public static readonly 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, } as const); /** Picks the value from `(key): (value[size])` */ private static readonly typeObjectReader = /^(\w*)\s*:\s*(\w*\s*(?:\[!?[1-6]\]\s*)*)$/i; /** Picks the value from `(value)([size])` */ private static readonly typeReader = /^(\w*)\s*((?:\[!?[1-6]\]\s*)*)$/i; /** Breaks the array size from `[(!)(size1)]([size2][size3])` */ private static readonly typeArrayBreaker = /\[(!?)([1-6])\]((?:\[!?[1-6]\])*)/; /** * 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: string[]) { 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: Record<string, unknown>): Buffer { assertNotObject(data,""); let outBuffers: Buffer[] = []; /** * 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: string, size: string, data: unknown, path: string) => { // 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 as Record<string, unknown>)[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: Buffer): Record<string, unknown> { if (!Buffer.isBuffer(buffer)) throw new TypeError(`The input must be a buffer`); let bufferIndex = 0; let result: Record<string, unknown> = {}; /** * 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: string, endian: "BE" | "LE", path: string): unknown => { type ntn = (offset?: number) => number; 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}`] as ntn)(bufferIndex - 4); case "uint": bufferIndex += 4; return (buffer[`readUInt32${endian}`] as ntn)(bufferIndex - 4); case "long": bufferIndex += 8; return (buffer[`readBigInt64${endian}` as keyof Buffer] as ntn)(bufferIndex - 8); case "ulong": bufferIndex += 8; return (buffer[`readBigUInt64${endian}` as keyof Buffer] as ntn)(bufferIndex - 8); case "float": bufferIndex += 4; return (buffer[`readFloat${endian}` as keyof Buffer] as ntn)(bufferIndex - 4); case "double": bufferIndex += 8; return (buffer[`readDouble${endian}` as keyof Buffer] as ntn)(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: string, type: string, size: string, data: Record<string, unknown>, path: string): void => { if (size) { const [, invertEndian, indexSize, rest] = StreamStructure.typeArrayBreaker.exec(size)!; const indexEndian = (this.endian === "BE") === (!invertEndian) ? "BE" : "LE"; let arrayLength: number; 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 as Record<string, unknown[]>)[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: string, ...structure: string[]): this { 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<A, B>(type: string, preProcessing: (value: A) => B, postProcessing: (value: B) => A): this { 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 as (value: unknown) => unknown; this.posProcessing[type] = postProcessing as (value: unknown) => unknown; return this; } /** * Sets the type of key from a typeConditional, normally used only with "string" or "byte". * @param type * @param indexType * @returns */ setTypeConditionalIndex(type: string, indexType: string): this { 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: string, condition: string | number | symbol, ...structure: string[]): this { 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: string, indexType: string) { 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: string, condition: string, structure: string[]) { 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: "BE" | "LE") { if (endian !== "BE" && endian !== "LE") throw new Error("The endian must be 'BE' or 'LE'"); this.endian = endian; return this; } } export = StreamStructure;