netkitty
Version:
Network tools kit
436 lines (388 loc) • 14.2 kB
text/typescript
import {ProtocolJSONSchema} from '../../schema/ProtocolJSONSchema'
import {CodecModule} from '../types/CodecModule'
import {ProtocolFieldJSONSchema} from '../../schema/ProtocolFieldJSONSchema'
import {CodecErrorInfo} from '../types/CodecErrorInfo'
import {PostHandlerItem} from '../types/PostHandlerItem'
import {SortPostHandlers} from '../lib/SortPostHandlers'
import {CodecData} from '../types/CodecData'
import {FlexibleObject} from '../lib/FlexibleObject'
import {Ajv, ErrorObject, ValidateFunction} from 'ajv'
import {HeaderTreeNode} from '../types/HeaderTreeNode'
import {CodecSchemaValidateError} from '../../../errors/CodecSchemaValidateError'
const CONSTRUCTOR_VALIDATE_KEY: string = '__validate'
export abstract class BaseHeader {
protected static get CODEC_INSTANCE(): CodecModule {
return new (this as any)()
}
protected static CREATE_CODEC_INSTANCE_WITCH_CODEC_MODULES(codecData: CodecData, codecModules: CodecModule[]): CodecModule {
return this.CREATE_INSTANCE(codecData, codecModules)
}
public static get PROTOCOL_ID(): string {
return this.CODEC_INSTANCE.id
}
public static get PROTOCOL_NAME(): string {
return this.CODEC_INSTANCE.name
}
public static get PROTOCOL_SCHEMA(): ProtocolJSONSchema {
const schema: ProtocolJSONSchema = JSON.parse(JSON.stringify(this.CODEC_INSTANCE.SCHEMA))
if (!Object.hasOwn(this, CONSTRUCTOR_VALIDATE_KEY)) {
const validate: ValidateFunction = new Ajv({
strict: false,
useDefaults: true,
coerceTypes: true
}).compile(schema)
Object.defineProperty(this, CONSTRUCTOR_VALIDATE_KEY, {
enumerable: false,
configurable: false,
value: validate
})
}
return schema
}
public static MATCH(codecData: CodecData, codecModules: CodecModule[]): boolean {
return this.CREATE_CODEC_INSTANCE_WITCH_CODEC_MODULES(codecData, codecModules ? codecModules : []).match()
}
public static CREATE_INSTANCE(codecData: CodecData, codecModules: CodecModule[]): CodecModule {
return new (this as any)(codecData, codecModules)
}
/**
* Protocol schema definition
*/
public readonly abstract SCHEMA: ProtocolJSONSchema
/**
* Program used header id
*/
public readonly abstract id: string
/**
* Human-readable header name
*/
public readonly abstract name: string
/**
* Human-readable header nickname
*/
public readonly abstract nickname: string
/**
* Current header is a protocol or not
*/
public readonly isProtocol: boolean = true
/**
* Encode/Decode error info objects
*/
public errors: CodecErrorInfo[] = []
/**
* Header schema instance
*/
public instance: FlexibleObject = new FlexibleObject()
/**
* Entire packet buffer data getter
*/
public get packet(): Buffer {
return this.codecData.packet
}
/**
* Entire packet buffer data setter
* @param packet
*/
public set packet(packet: Buffer) {
this.codecData.packet = packet
}
/**
* The start position of this header in the entire packet's buffer
*/
public startPos: number = 0
/**
* The end position of this header in the entire packet's buffer
*/
public get endPos(): number {
return this.startPos + this.headerLength
}
/**
* Readonly Header length
*/
public get length(): number {
return this.headerLength
}
/**
* Header length
* @protected
*/
protected headerLength: number = 0
/**
* Previous Codec module
* @protected
*/
protected readonly prevCodecModule: CodecModule
/**
* Codec modules
* @protected
*/
protected readonly codecModules: CodecModule[]
/**
* Previous codec modules
* @protected
*/
protected readonly prevCodecModules: CodecModule[]
/**
* Current header index in packet headers
* @protected
*/
protected readonly headerIndex: number = 0
/**
* Post packet handlers
* @protected
*/
protected get postPacketHandlers(): PostHandlerItem[] {
if (this.codecData.postHandlers[this.headerIndex] === undefined) this.codecData.postHandlers[this.headerIndex] = []
return this.codecData.postHandlers[this.headerIndex]
}
/**
* Registered post encode handlers (CodecModule)
* @protected
*/
protected readonly postSelfEncodeHandlers: PostHandlerItem[] = []
/**
* Registered post decode handlers (CodecModule)
* @protected
*/
protected readonly postSelfDecodeHandlers: PostHandlerItem[] = []
/**
* Codec data
* @protected
*/
protected readonly codecData: CodecData
constructor(codecData: CodecData, codecModules: CodecModule[]) {
this.codecData = codecData
this.startPos = codecData?.startPos ? codecData.startPos : 0
codecModules = codecModules ? codecModules : []
this.codecModules = codecModules
this.prevCodecModules = [...codecModules]
const prevCodecModuleIndex: number = this.prevCodecModules.length - 1
this.prevCodecModule = this.prevCodecModules[prevCodecModuleIndex > -1 ? prevCodecModuleIndex : 0]
this.headerIndex = this.prevCodecModules.length
}
/**
* Get packet data offset
* @param offset
* @protected
*/
protected getPacketOffset(offset: number): number {
return this.startPos + offset
}
/**
* Is data buffer fits current header codec
*/
public abstract match(): boolean
/**
* Internal read bytes from buffer
* @param offset
* @param length
* @param expandBufferLength
* @param changeHeaderLength
* @private
*/
#readBytes(offset: number, length: number, expandBufferLength: boolean, changeHeaderLength: boolean): Buffer {
const packetOffset: number = this.getPacketOffset(offset)
let readEndPos: number = packetOffset + length
if (this.packet.length < readEndPos) {
if (expandBufferLength) {
this.packet = Buffer.concat([this.packet, Buffer.alloc(readEndPos - this.packet.length, 0)])
} else {
readEndPos = this.packet.length
}
}
const headerLength: number = readEndPos - this.startPos
if (changeHeaderLength) this.headerLength = this.headerLength < headerLength ? headerLength : this.headerLength
return this.packet.subarray(packetOffset, packetOffset + length)
}
/**
* Read bytes from buffer
* @param offset
* @param length
* @param dryRun
* @protected
*/
protected readBytes(offset: number, length: number, dryRun: boolean = false): Buffer {
return this.#readBytes(offset, length, false, !dryRun)
}
/**
* Write bytes to buffer
* @param offset
* @param buffer
* @protected
*/
protected writeBytes(offset: number, buffer: Buffer): void {
const packetOffset: number = this.getPacketOffset(offset)
const writeEndPos: number = packetOffset + buffer.length
this.#readBytes(offset, buffer.length, true, true)
this.packet.fill(buffer, packetOffset, writeEndPos)
}
/**
* Read bits from buffer
* @param offset
* @param length
* @param bitOffset
* @param bitLength
* @protected
*/
protected readBits(offset: number, length: number, bitOffset: number, bitLength: number): number {
const buffer: Buffer = this.#readBytes(offset, length, false, true)
const bitString: string = parseInt(buffer.toString('hex'), 16).toString(2).padStart(length * 8, '0')
return parseInt(bitString.substring(bitOffset, bitOffset + bitLength), 2)
}
/**
* Write bits to buffer
* @param offset
* @param length
* @param bitOffset
* @param bitLength
* @param value
* @protected
*/
protected writeBits(offset: number, length: number, bitOffset: number, bitLength: number, value: number): void {
const buffer: Buffer = this.#readBytes(offset, length, true, true)
let bitArray: string[] = Array.from(parseInt(buffer.toString('hex'), 16).toString(2).padStart(buffer.length * 8, '0'))
const valueBitArray: string[] = Array.from(value.toString(2).padStart(bitLength, '0'))
bitArray = bitArray.map((bit: string, index: number): string => {
if (index < bitOffset) return bit
if (index >= (bitOffset + bitLength)) return bit
return valueBitArray[index - bitOffset]
})
this.writeBytes(offset, Buffer.from(parseInt(bitArray.join(''), 2).toString(16).padStart(buffer.length * 2, '0'), 'hex'))
}
/**
* Get field codecs from schema tree
* @param schema
* @param codecName
* @param execBeforeSubCodecs
* @param codecs
* @protected
*/
protected getFieldCodecs(schema: ProtocolFieldJSONSchema, codecName: string, execBeforeSubCodecs: boolean, codecs: (() => Promise<void>)[] = []): (() => Promise<void>)[] {
if (!schema.properties) return codecs
for (const propertyName of Object.keys(schema.properties)) {
const fieldSchema: ProtocolFieldJSONSchema = schema.properties[propertyName]
let codec: (() => void | Promise<void>) | undefined = fieldSchema[codecName]
if (!codec) codec = async (): Promise<void> => (void (0))
if (execBeforeSubCodecs) codecs.push(async (): Promise<void> => await codec())
if (fieldSchema.properties) this.getFieldCodecs(fieldSchema, codecName, execBeforeSubCodecs, codecs)
if (!execBeforeSubCodecs) codecs.push(async (): Promise<void> => await codec())
}
return codecs
}
/**
* Record encode/decode error
* @param path
* @param message
* @protected
*/
protected recordError(path: string, message: string): void {
this.errors.push({
id: this.id,
path: path,
message: message
})
}
/**
* Register post encode handler for current codec
* @param handler
* @param priority
* @protected
*/
protected addPostSelfEncodeHandler(handler: () => void | Promise<void>, priority: number = 0): void {
this.postSelfEncodeHandlers.push({
priority: priority,
handler: handler
})
}
/**
* Register post decode handler for current codec
* @param handler
* @param priority
* @protected
*/
protected addPostSelfDecodeHandler(handler: () => void | Promise<void>, priority: number = 0): void {
this.postSelfDecodeHandlers.push({
priority: priority,
handler: handler
})
}
/**
* Register post encode handler for packet
* @description Registered handler call sequence: LIFO (Last In First Out)
* @param handler
* @param priority
* @protected
*/
protected addPostPacketEncodeHandler(handler: () => void | Promise<void>, priority: number = 0): void {
this.postPacketHandlers.push({
priority: priority,
handler: handler
})
}
/**
* Register post decode handler for packet
* @description Registered handler call sequence: FIFO (First In First Out)
* @param handler
* @param priority
* @protected
*/
protected addPostPacketDecodeHandler(handler: () => void | Promise<void>, priority: number = 0): void {
this.postPacketHandlers.push({
priority: priority,
handler: handler
})
}
/**
* Validate input json node is valid
* @param headerTreeNode
*/
public validate(headerTreeNode: HeaderTreeNode): HeaderTreeNode {
let validate: ValidateFunction = this.constructor[CONSTRUCTOR_VALIDATE_KEY]
if (!validate) {
validate = new Ajv({
strict: false,
useDefaults: true,
coerceTypes: true
}).compile(this.SCHEMA)
this.constructor[CONSTRUCTOR_VALIDATE_KEY] = validate
}
const isValid: boolean = validate(headerTreeNode)
if (!isValid) {
let errorObject: ErrorObject | undefined | null
if (validate.errors) errorObject = validate.errors[0]
const errorMessage: string = errorObject?.message ? errorObject.message : 'Unknown Error'
throw new CodecSchemaValidateError(errorMessage)
}
return headerTreeNode
}
/**
* Decode packet header field by field
*/
public async decode(): Promise<void> {
const decodes: (() => Promise<void>)[] = this.getFieldCodecs(this.SCHEMA as ProtocolFieldJSONSchema, 'decode', true)
for (const decode of decodes) {
await decode()
}
const postSelfDecodeHandlers: PostHandlerItem[] = SortPostHandlers(this.postSelfDecodeHandlers)
let postDecodeHandler: PostHandlerItem | undefined = postSelfDecodeHandlers.shift()
while (postDecodeHandler) {
await postDecodeHandler.handler()
postDecodeHandler = postSelfDecodeHandlers.shift()
}
}
/**
* Encode packet header field by field
*/
public async encode(): Promise<void> {
const encodes: (() => Promise<void>)[] = this.getFieldCodecs(this.SCHEMA as ProtocolFieldJSONSchema, 'encode', false)
for (const encode of encodes) {
await encode()
}
const postSelfEncodeHandlers: PostHandlerItem[] = SortPostHandlers(this.postSelfEncodeHandlers)
let postEncodeHandler: PostHandlerItem | undefined = postSelfEncodeHandlers.shift()
while (postEncodeHandler) {
await postEncodeHandler.handler()
postEncodeHandler = postSelfEncodeHandlers.shift()
}
}
}