UNPKG

zigbee-herdsman

Version:

An open source ZigBee gateway solution with node.js.

368 lines (349 loc) 15.5 kB
import assert from "node:assert"; import type {SerializableMemoryObject} from "./serializable-memory-object"; /* Helper Types */ type StructMemberType = "uint8" | "uint16" | "uint32" | "uint8array" | "uint8array-reversed" | "struct"; type StructBuildOmitKeys = "member" | "method" | "padding" | "build" | "default"; type StructChild = {offset: number; struct: Struct}; export type BuiltStruct<T = Struct> = Omit<T, StructBuildOmitKeys>; export type StructFactorySignature<T = Struct> = (data?: Buffer) => T; export type StructMemoryAlignment = "unaligned" | "aligned"; /** * Struct provides a builder-like interface to create Buffer-based memory * structures for read/write interfacing with data structures from adapters. */ export class Struct implements SerializableMemoryObject { /** * Creates an empty struct. Further calls to `member()` and `method()` functions will form the structure. * Finally call to `build()` will type the resulting structure appropriately without internal functions. */ public static new(): Struct { return new Struct(); } // @ts-expect-error initialized in `build()` private buffer: Buffer; private defaultData: Buffer | undefined; private members: {key: string; offset: number; type: StructMemberType; length?: number}[] = []; private childStructs: {[key: string]: StructChild} = {}; private length = 0; private paddingByte = 0x00; private constructor() {} /** * Returns raw contents of the structure as a sliced Buffer. * Mutations to the returned buffer will not be reflected within struct. */ public serialize(alignment: StructMemoryAlignment = "unaligned", padLength = true, parentOffset = 0): Buffer { switch (alignment) { case "unaligned": { /* update child struct values and return as-is (unaligned) */ for (const key of Object.keys(this.childStructs)) { const child = this.childStructs[key]; this.buffer.set(child.struct.serialize(alignment), child.offset); } return Buffer.from(this.buffer); } case "aligned": { /* create 16-bit aligned buffer */ const aligned = Buffer.alloc(this.getLength(alignment, padLength, parentOffset), this.paddingByte); let offset = 0; for (const member of this.members) { switch (member.type) { case "uint8": aligned.set(this.buffer.slice(member.offset, member.offset + 1), offset); offset += 1; break; case "uint16": offset += offset % 2; aligned.set(this.buffer.slice(member.offset, member.offset + 2), offset); offset += 2; break; case "uint32": offset += offset % 2; aligned.set(this.buffer.slice(member.offset, member.offset + 4), offset); offset += 4; break; case "uint8array": case "uint8array-reversed": assert(member.length !== undefined); aligned.set(this.buffer.slice(member.offset, member.offset + member.length), offset); offset += member.length; break; case "struct": { const structData = this.childStructs[member.key].struct.serialize(alignment, false, offset); aligned.set(structData, offset); offset += structData.length; break; } } } return aligned; } } } /** * Returns total length of the struct. Struct length is always fixed and configured * by calls to `member()` methods. */ public getLength(alignment: StructMemoryAlignment = "unaligned", padLength = true, parentOffset = 0): number { switch (alignment) { case "unaligned": { /* return actual length */ return this.length; } case "aligned": { /* compute aligned length and return */ let length = this.members.reduce((offset, member) => { switch (member.type) { case "uint8": offset += 1; break; case "uint16": offset += ((parentOffset + offset) % 2) + 2; break; case "uint32": offset += ((parentOffset + offset) % 2) + 4; break; case "uint8array": case "uint8array-reversed": assert(member.length !== undefined); offset += member.length; break; case "struct": offset += this.childStructs[member.key].struct.getLength(alignment, false); break; } return offset; }, 0); if (padLength) { length += length % 2; } return length; } } } /** * Returns structure contents in JS object format. */ public toJSON() { return this.members.reduce((a, c) => { // biome-ignore lint/suspicious/noExplicitAny: API a[c.key] = (this as any)[c.key]; return a; // biome-ignore lint/suspicious/noExplicitAny: API }, {} as any); } /** * Adds a numeric member of `uint8`, `uint16` or `uint32` type. * Internal representation is always little endian. * * *This method is stripped from type on struct `build()`.* * * @param type Underlying data type (uint8, uint16 or uint32). * @param name Name of the struct member. */ public member<T extends number, N extends string, R extends this & Record<N, T>>(type: "uint8" | "uint16" | "uint32", name: N): R; /** * Adds an uint8 array (byte array) as a struct member. * * *This method is stripped from type on struct `build()`.* * * @param type Underlying data type. Must be `uint8array`. * @param name Name of the struct member. * @param length Length of the byte array. */ public member<T extends Buffer, N extends string, R extends this & Record<N, T>>( type: "uint8array" | "uint8array-reversed", name: N, length: number, ): R; /** * Adds another struct type as a struct member. Struct factory is provided * as a child struct definition source. * * *This method is stripped from type on struct `build()`.* * * @param type Underlying data type. Must be `struct`. * @param name Name of the struct member. * @param structFactory Factory providing the wanted child struct. */ public member<T extends BuiltStruct, N extends string, R extends this & Record<N, T>>( type: "struct", name: N, structFactory: StructFactorySignature<T>, ): R; public member<T extends number | BuiltStruct, N extends string, R extends this & Record<N, T>>( type: StructMemberType, name: N, lengthOrStructFactory?: number | StructFactorySignature<T>, ): R { const offset = this.length; const structFactory = type === "struct" ? (lengthOrStructFactory as StructFactorySignature<T>) : undefined; const length = structFactory ? (structFactory() as unknown as Struct).length : (lengthOrStructFactory as number); switch (type) { case "uint8": { Object.defineProperty(this, name, { enumerable: true, get: () => this.buffer.readUInt8(offset), set: (value: number) => this.buffer.writeUInt8(value, offset), }); this.length += 1; break; } case "uint16": { Object.defineProperty(this, name, { enumerable: true, get: () => this.buffer.readUInt16LE(offset), set: (value: number) => this.buffer.writeUInt16LE(value, offset), }); this.length += 2; break; } case "uint32": { Object.defineProperty(this, name, { enumerable: true, get: () => this.buffer.readUInt32LE(offset), set: (value: number) => this.buffer.writeUInt32LE(value, offset), }); this.length += 4; break; } case "uint8array": case "uint8array-reversed": { /* v8 ignore start */ if (!length) { throw new Error("Struct builder requires length for `uint8array` and `uint8array-reversed` type"); } /* v8 ignore stop */ Object.defineProperty(this, name, { enumerable: true, get: () => type === "uint8array-reversed" ? Buffer.from(this.buffer.slice(offset, offset + length)).reverse() : Buffer.from(this.buffer.slice(offset, offset + length)), set: (value: Buffer) => { if (value.length !== length) { throw new Error(`Invalid length for member ${name} (expected=${length}, got=${value.length})`); } if (type === "uint8array-reversed") { value = Buffer.from(value).reverse(); } for (let i = 0; i < length; i++) { this.buffer[offset + i] = value[i]; } }, }); this.length += length; break; } case "struct": { assert(structFactory); this.childStructs[name] = {offset, struct: structFactory() as unknown as Struct}; Object.defineProperty(this, name, { enumerable: true, get: () => this.childStructs[name].struct, }); this.length += length; } } this.members.push({key: name, offset, type, length}); return this as unknown as R; } /** * Adds a custom method to the struct. * * *This method is stripped from type on struct `build()`.* * * @param name Name of the method to be appended. * @param _returnType Return type (eg. `Buffer.prototype`). * @param body Function implementation. Takes struct as a first and single input parameter. */ public method<T, N extends string, R extends this & Record<N, () => T>>(name: N, _returnType: T, body: (struct: R) => T): R { Object.defineProperty(this, name, { enumerable: true, configurable: false, writable: false, // @ts-expect-error ignore because we are using `this` value: () => body.bind(this)(this), }); return this as unknown as R; } /** * Sets default data to initialize empty struct with. * * @param data Data to initialize empty struct with. */ public default(data: Buffer): this { /* v8 ignore start */ if (data.length !== this.length) { throw new Error("Default value needs to have the length of unaligned structure."); } /* v8 ignore stop */ this.defaultData = Buffer.from(data); return this; } /** * Sets byte to use for padding. * * @param padding Byte to use for padding */ public padding(padding = 0x00): this { this.paddingByte = padding; return this; } /** * Creates the struct and optionally fills it with data. If data is provided, the length * of the provided buffer needs to match the structure length. * * *This method is stripped from type on struct `build()`.* */ public build(data?: Buffer): BuiltStruct<this> { if (data) { if (data.length === this.getLength("unaligned")) { this.buffer = Buffer.from(data); for (const key of Object.keys(this.childStructs)) { const child = this.childStructs[key]; child.struct.build(this.buffer.slice(child.offset, child.offset + child.struct.length)); } } else if (data.length === this.getLength("aligned")) { this.buffer = Buffer.alloc(this.length, this.paddingByte); let offset = 0; for (const member of this.members) { switch (member.type) { case "uint8": this.buffer.set(data.slice(offset, offset + 1), member.offset); offset += 1; break; case "uint16": offset += offset % 2; this.buffer.set(data.slice(offset, offset + 2), member.offset); offset += 2; break; case "uint32": offset += offset % 2; this.buffer.set(data.slice(offset, offset + 4), member.offset); offset += 4; break; case "uint8array": case "uint8array-reversed": assert(member.length !== undefined); this.buffer.set(data.slice(offset, offset + member.length), member.offset); offset += member.length; break; case "struct": { const child = this.childStructs[member.key]; child.struct.build(data.slice(offset, offset + child.struct.length)); this.buffer.set(child.struct.serialize(), member.offset); offset += child.struct.length; break; } } } } else { const expectedLengths = `${this.getLength("unaligned")}/${this.getLength("aligned")}`; throw new Error(`Struct length mismatch (expected=${expectedLengths}, got=${data.length})`); } } else { this.buffer = this.defaultData ? Buffer.from(this.defaultData) : Buffer.alloc(this.length); } return this; } }