191 lines
6.4 KiB
TypeScript
191 lines
6.4 KiB
TypeScript
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<string, unknown>;
|
|
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;
|
|
}
|
|
}
|