Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,224 @@
import type {
BitPackerSchema,
PackingContext,
PackingStrategy,
} from "@suaveplan/bitpacker";
import {
BITPACKER_CODES,
BitpackerError,
isFixedFloatSchema,
isFloatSchema,
isUuidSchema,
} from "@suaveplan/bitpacker";
function bitsRequired(maxValue: number): number {
if (maxValue <= 0) return 0;
return Math.floor(Math.log2(maxValue)) + 1;
}
/**
* Strategy for string schemas.
*
* Handles:
* - Fixed strings: `"20"` → exactly 20 bytes
* - Variable strings:`"-20"` → up to 20 bytes with length prefix
* - UUID: `"uuid"` → 16 bytes
* - Float: `"f32"`, `"f64"` → IEEE-754 floats
* - Fixed float: `"4.2"`, `"-4.2"` → fixed-point numbers encoded as strings
*/
export class StringPackingStrategy implements PackingStrategy {
canHandle(schema: BitPackerSchema): boolean {
return typeof schema === "string";
}
pack(
data: unknown,
schema: BitPackerSchema,
context: PackingContext
): void {
const s = schema as string;
if (isUuidSchema(s)) {
this.#packUuid(data, context);
} else if (isFloatSchema(s)) {
this.#packFloat(data as number, s, context);
} else if (isFixedFloatSchema(s)) {
this.#packFixedFloat(data as number | string, s, context);
} else {
this.#packString(data as string, s, context);
}
}
unpack(schema: BitPackerSchema, context: PackingContext): unknown {
const s = schema as string;
if (isUuidSchema(s)) {
return this.#unpackUuid(context);
}
if (isFloatSchema(s)) {
return this.#unpackFloat(s, context);
}
if (isFixedFloatSchema(s)) {
return this.#unpackFixedFloat(s, context);
}
return this.#unpackString(s, context);
}
#packString(value: string, schema: string, context: PackingContext): void {
const isFixed = !schema.startsWith("-");
const maxLength = Number(isFixed ? schema : schema.slice(1));
if (typeof value !== "string") {
throw new BitpackerError(
BITPACKER_CODES.INVALID_SCHEMA,
`Expected string for schema "${schema}", got ${typeof value}`,
{ context: { schema, value } }
);
}
const bytes = new TextEncoder().encode(value);
const length = Math.min(bytes.length, maxLength);
if (isFixed && bytes.length > maxLength) {
throw new BitpackerError(
BITPACKER_CODES.VALUE_OUT_OF_RANGE,
`String length ${bytes.length} exceeds fixed schema length ${maxLength}`,
{ context: { schema, length: bytes.length, maxLength } }
);
}
if (!isFixed) {
context.writer.writeUint(length, bitsRequired(maxLength));
}
const writeCount = isFixed ? maxLength : length;
for (let i = 0; i < writeCount; i++) {
context.writer.writeUint(i < bytes.length ? bytes[i]! : 0, 8);
}
}
#unpackString(schema: string, context: PackingContext): string {
const isFixed = !schema.startsWith("-");
const maxLength = Number(isFixed ? schema : schema.slice(1));
let length = maxLength;
if (!isFixed) {
length = context.reader.readUint(bitsRequired(maxLength));
}
const bytes = new Uint8Array(length);
for (let i = 0; i < length; i++) {
bytes[i] = context.reader.readUint(8);
}
let actualLength = length;
while (actualLength > 0 && bytes[actualLength - 1] === 0) {
actualLength--;
}
return new TextDecoder().decode(bytes.subarray(0, actualLength));
}
#packUuid(value: unknown, context: PackingContext): void {
let bytes: Uint8Array;
if (typeof value === "string" && value.includes("-")) {
const hex = value.replace(/-/g, "");
if (hex.length !== 32) {
throw new BitpackerError(
BITPACKER_CODES.VALUE_OUT_OF_RANGE,
`Invalid UUID string: ${value}`,
{ context: { value } }
);
}
bytes = new Uint8Array(16);
for (let i = 0; i < 16; i++) {
bytes[i] = Number.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
}
} else if (value instanceof Uint8Array && value.length === 16) {
bytes = value;
} else {
throw new BitpackerError(
BITPACKER_CODES.INVALID_SCHEMA,
"UUID must be a 36-char string or 16-byte Uint8Array",
{ context: { value } }
);
}
context.writer.writeBytes(bytes);
}
#unpackUuid(context: PackingContext): string {
const bytes = context.reader.readBytes(16);
const hex = Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
}
#packFloat(value: number, schema: string, context: PackingContext): void {
if (typeof value !== "number") {
throw new BitpackerError(
BITPACKER_CODES.INVALID_SCHEMA,
`Expected number for float schema "${schema}", got ${typeof value}`,
{ context: { schema, value } }
);
}
if (schema === "f32") {
const buf = new ArrayBuffer(4);
new DataView(buf).setFloat32(0, value, false);
context.writer.writeBytes(new Uint8Array(buf));
} else {
const buf = new ArrayBuffer(8);
new DataView(buf).setFloat64(0, value, false);
context.writer.writeBytes(new Uint8Array(buf));
}
}
#unpackFloat(schema: string, context: PackingContext): number {
const bytes = context.reader.readBytes(schema === "f32" ? 4 : 8);
const view = new DataView(
bytes.buffer,
bytes.byteOffset,
bytes.byteLength
);
if (schema === "f32") {
return view.getFloat32(0, false);
}
return view.getFloat64(0, false);
}
#packFixedFloat(
value: number | string,
schema: string,
context: PackingContext
): void {
const [wholeStr, fracStr] = schema.split(".");
const wholeBits = Math.abs(Number(wholeStr));
const fracBits = Number(fracStr);
const totalBits = wholeBits + fracBits;
const isSigned = Number(wholeStr) < 0;
const numValue = typeof value === "string" ? Number(value) : value;
const factor = 1 << fracBits;
const scaled = Math.round(numValue * factor);
if (isSigned) {
context.writer.writeInt(scaled, totalBits);
} else {
context.writer.writeUint(scaled, totalBits);
}
}
#unpackFixedFloat(schema: string, context: PackingContext): number {
const [wholeStr, fracStr] = schema.split(".");
const wholeBits = Math.abs(Number(wholeStr));
const fracBits = Number(fracStr);
const totalBits = wholeBits + fracBits;
const isSigned = Number(wholeStr) < 0;
const scaled = isSigned
? context.reader.readInt(totalBits)
: context.reader.readUint(totalBits);
return scaled / (1 << fracBits);
}
}