import type { BitPackerSchema, PackingContext, PackingStrategy, } from "@suaveplan/bitpacker"; import { BITPACKER_CODES, BitpackerError } from "@suaveplan/bitpacker"; function bitsRequired(maxValue: number): number { if (maxValue <= 0) return 0; return Math.floor(Math.log2(maxValue)) + 1; } export type BitPackerArrayConfig = { type: "array"; maxLength: number; } & ( | { elementType: "integer"; elementMax?: number } | { elementType: "float"; elementMax?: number } | { elementType: "string"; elementMax?: number } | { elementType: "boolean" } | { elementType: "schema"; elementSchema: BitPackerSchema } ); export function isArrayConfig(schema: unknown): schema is BitPackerArrayConfig { if ( typeof schema !== "object" || schema === null || Array.isArray(schema) ) { return false; } const obj = schema as Record; if (obj["type"] !== "array") return false; if (typeof obj["maxLength"] !== "number") return false; if ( !["integer", "float", "string", "boolean", "schema"].includes( obj["elementType"] as string ) ) return false; if (obj["elementType"] === "schema" && !("elementSchema" in obj)) return false; return true; } export function validateArrayData( data: unknown[], config: BitPackerArrayConfig ): boolean { if (!Array.isArray(data)) return false; if (data.length > config.maxLength) return false; return data.every((item) => { switch (config.elementType) { case "integer": return ( typeof item === "number" && Number.isInteger(item) && item >= 0 && item <= (config.elementMax || 255) ); case "float": return ( typeof item === "number" && item >= 0 && item <= (config.elementMax || 100) ); case "string": return ( typeof item === "string" && item.length <= (config.elementMax || 100) ); case "boolean": return typeof item === "boolean"; case "schema": return true; default: return false; } }); } /** * Strategy for packing array configurations. * * Schema shape: `{ type: "array", maxLength: number, elementType: "integer"|"float"|"string"|"boolean"|"schema", ... }` */ export class ArrayConfigPackingStrategy implements PackingStrategy { canHandle(schema: BitPackerSchema): boolean { return isArrayConfig(schema); } pack( data: unknown, schema: BitPackerSchema, context: PackingContext ): void { const arrayConfig = schema as BitPackerArrayConfig; if (!Array.isArray(data)) { throw new BitpackerError( BITPACKER_CODES.INVALID_SCHEMA, `Expected array data for array configuration but received ${typeof data}`, { context: { data } } ); } if (!validateArrayData(data, arrayConfig)) { throw new BitpackerError( BITPACKER_CODES.INVALID_SCHEMA, `Invalid array data for array configuration with elementType "${arrayConfig.elementType}"`, { context: { data, elementType: arrayConfig.elementType } } ); } // Pack array length const lengthBits = bitsRequired(arrayConfig.maxLength); context.writer.writeUint(data.length, lengthBits); // Pack elements for (const item of data) { switch (arrayConfig.elementType) { case "integer": { const max = arrayConfig.elementMax || 255; context.writer.writeUint(item as number, bitsRequired(max)); break; } case "float": { const max = arrayConfig.elementMax || 100; context.writer.writeUint(item as number, bitsRequired(max)); break; } case "string": { const maxLen = arrayConfig.elementMax || 100; const bytes = new TextEncoder().encode(item as string); const len = Math.min(bytes.length, maxLen); for (let i = 0; i < maxLen; i++) { context.writer.writeUint(i < len ? bytes[i]! : 0, 8); } break; } case "boolean": context.writer.writeBool(item as boolean); break; case "schema": context.packValue(item, arrayConfig.elementSchema); break; } } } unpack(schema: BitPackerSchema, context: PackingContext): unknown { const arrayConfig = schema as BitPackerArrayConfig; const lengthBits = bitsRequired(arrayConfig.maxLength); const length = context.reader.readUint(lengthBits); const result: unknown[] = []; for (let i = 0; i < length; i++) { switch (arrayConfig.elementType) { case "integer": { const max = arrayConfig.elementMax || 255; result.push(context.reader.readUint(bitsRequired(max))); break; } case "float": { const max = arrayConfig.elementMax || 100; result.push(context.reader.readUint(bitsRequired(max))); break; } case "string": { const maxLen = arrayConfig.elementMax || 100; let str = ""; for (let j = 0; j < maxLen; j++) { const byte = context.reader.readUint(8); if (byte !== 0) str += String.fromCharCode(byte); } result.push(str); break; } case "boolean": result.push(context.reader.readBool()); break; case "schema": result.push(context.unpackValue(arrayConfig.elementSchema)); break; } } return result; } }