Initial Commit
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user