mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 08:17:09 +00:00
It encodes / decodes sequence / preplot data using an efficient binary format for sending large amounts of data across the wire and for (relatively) memory efficient client-side use.
328 lines
11 KiB
JavaScript
328 lines
11 KiB
JavaScript
const codeToType = {
|
|
0: Int8Array,
|
|
1: Uint8Array,
|
|
2: Int16Array,
|
|
3: Uint16Array,
|
|
4: Int32Array,
|
|
5: Uint32Array,
|
|
7: Float32Array,
|
|
8: Float64Array,
|
|
9: BigInt64Array,
|
|
10: BigUint64Array
|
|
};
|
|
|
|
const typeToBytes = {
|
|
Int8Array: 1,
|
|
Uint8Array: 1,
|
|
Int16Array: 2,
|
|
Uint16Array: 2,
|
|
Int32Array: 4,
|
|
Uint32Array: 4,
|
|
Float32Array: 4,
|
|
Float64Array: 8,
|
|
BigInt64Array: 8,
|
|
BigUint64Array: 8
|
|
};
|
|
|
|
function sequential(binary) {
|
|
if (!(binary instanceof Uint8Array) || binary.length < 4) {
|
|
throw new Error('Invalid binary input');
|
|
}
|
|
|
|
const view = new DataView(binary.buffer, binary.byteOffset, binary.byteLength);
|
|
let offset = 0;
|
|
|
|
// Initialize result (assuming single i value for simplicity; extend for multiple i values if needed)
|
|
const result = { i: null, j: [], Δelems: [], elems: [] };
|
|
|
|
// Process bundles
|
|
while (offset < binary.length) {
|
|
// Read bundle header
|
|
if (offset + 4 > binary.length) throw new Error('Incomplete bundle header');
|
|
|
|
const bundleHeader = view.getUint32(offset, true);
|
|
if ((bundleHeader & 0xFF) !== 0x1C) throw new Error('Invalid bundle marker');
|
|
const bundleLength = bundleHeader >> 8;
|
|
offset += 4;
|
|
const bundleEnd = offset + bundleLength;
|
|
|
|
if (bundleEnd > binary.length) throw new Error('Bundle length exceeds input size');
|
|
|
|
// Process chunks in bundle
|
|
while (offset < bundleEnd) {
|
|
// Read chunk header
|
|
if (offset + 12 > bundleEnd) throw new Error('Incomplete chunk header');
|
|
const chunkType = view.getUint8(offset);
|
|
if (chunkType !== 0x11) throw new Error(`Unsupported chunk type: ${chunkType}`);
|
|
offset += 1; // Skip udv
|
|
offset += 1;
|
|
const count = view.getUint16(offset, true); offset += 2;
|
|
if (count > 65535) throw new Error('Chunk count exceeds 65535');
|
|
const iValue = view.getUint16(offset, true); offset += 2;
|
|
const j0 = view.getUint16(offset, true); offset += 2;
|
|
const Δj = view.getInt16(offset, true); offset += 2;
|
|
const ΔelemCount = view.getUint8(offset++); // Δelem_count
|
|
const elemCount = view.getUint8(offset++); // elem_count
|
|
|
|
// Set i value (assuming all chunks share the same i)
|
|
if (result.i === null) result.i = iValue;
|
|
else if (result.i !== iValue) throw new Error('Multiple i values not supported');
|
|
|
|
// Read preface (element types)
|
|
const ΔelemTypes = [];
|
|
for (let i = 0; i < ΔelemCount; i++) {
|
|
if (offset >= bundleEnd) throw new Error('Incomplete Δelem types');
|
|
const typeByte = view.getUint8(offset++);
|
|
const baseCode = typeByte & 0x0F;
|
|
const incrCode = typeByte >> 4;
|
|
if (!codeToType[baseCode] || !codeToType[incrCode]) {
|
|
throw new Error(`Invalid type code in Δelem: ${typeByte}`);
|
|
}
|
|
ΔelemTypes.push({ baseType: codeToType[baseCode], incrType: codeToType[incrCode] });
|
|
}
|
|
const elemTypes = [];
|
|
for (let i = 0; i < elemCount; i++) {
|
|
if (offset >= bundleEnd) throw new Error('Incomplete elem types');
|
|
const typeCode = view.getUint8(offset++);
|
|
if (!codeToType[typeCode]) throw new Error(`Invalid type code in elem: ${typeCode}`);
|
|
elemTypes.push(codeToType[typeCode]);
|
|
}
|
|
|
|
// Initialize Δelems and elems arrays if first chunk
|
|
if (!result.Δelems.length && ΔelemCount > 0) {
|
|
result.Δelems = Array(ΔelemCount).fill().map(() => []);
|
|
}
|
|
if (!result.elems.length && elemCount > 0) {
|
|
result.elems = Array(elemCount).fill().map(() => []);
|
|
}
|
|
|
|
// Read initial values for Δelems
|
|
const initialValues = [];
|
|
for (const { baseType } of ΔelemTypes) {
|
|
if (offset + typeToBytes[baseType.name] > bundleEnd) {
|
|
throw new Error('Incomplete initial values');
|
|
}
|
|
initialValues.push(readTypedValue(view, offset, baseType));
|
|
offset += typeToBytes[baseType.name];
|
|
}
|
|
// Skip padding
|
|
while (offset % 4 !== 0) {
|
|
if (offset >= bundleEnd) throw new Error('Incomplete padding after initial values');
|
|
offset++;
|
|
}
|
|
|
|
// Reconstruct j values
|
|
for (let idx = 0; idx < count; idx++) {
|
|
result.j.push(j0 + idx * Δj);
|
|
}
|
|
|
|
// Read record data (non-interleaved)
|
|
for (let i = 0; i < ΔelemCount; i++) {
|
|
let current = initialValues[i];
|
|
const values = result.Δelems[i];
|
|
const incrType = ΔelemTypes[i].incrType;
|
|
const isBigInt = typeof current === 'bigint';
|
|
for (let idx = 0; idx < count; idx++) {
|
|
if (offset + typeToBytes[incrType.name] > bundleEnd) {
|
|
throw new Error('Incomplete Δelem data');
|
|
}
|
|
let delta = readTypedValue(view, offset, incrType);
|
|
if (idx === 0) {
|
|
values.push(isBigInt ? Number(current) : current);
|
|
} else {
|
|
if (isBigInt) {
|
|
delta = BigInt(delta);
|
|
current += delta;
|
|
values.push(Number(current));
|
|
} else {
|
|
current += delta;
|
|
values.push(current);
|
|
}
|
|
}
|
|
offset += typeToBytes[incrType.name];
|
|
}
|
|
}
|
|
for (let i = 0; i < elemCount; i++) {
|
|
const values = result.elems[i];
|
|
const type = elemTypes[i];
|
|
const isBigInt = type === BigInt64Array || type === BigUint64Array;
|
|
for (let idx = 0; idx < count; idx++) {
|
|
if (offset + typeToBytes[type.name] > bundleEnd) {
|
|
throw new Error('Incomplete elem data');
|
|
}
|
|
let value = readTypedValue(view, offset, type);
|
|
values.push(isBigInt ? Number(value) : value);
|
|
offset += typeToBytes[type.name];
|
|
}
|
|
}
|
|
// Skip padding
|
|
while (offset % 4 !== 0) {
|
|
if (offset >= bundleEnd) throw new Error('Incomplete padding after record data');
|
|
offset++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
function interleaved(binary) {
|
|
if (!(binary instanceof Uint8Array) || binary.length < 4) {
|
|
throw new Error('Invalid binary input');
|
|
}
|
|
|
|
const view = new DataView(binary.buffer, binary.byteOffset, binary.byteLength);
|
|
let offset = 0;
|
|
|
|
// Initialize result (assuming single i value for simplicity; extend for multiple i values if needed)
|
|
const result = { i: null, j: [], Δelems: [], elems: [] };
|
|
|
|
// Process bundles
|
|
while (offset < binary.length) {
|
|
// Read bundle header
|
|
if (offset + 4 > binary.length) throw new Error('Incomplete bundle header');
|
|
|
|
const bundleHeader = view.getUint32(offset, true);
|
|
if ((bundleHeader & 0xFF) !== 0x1C) throw new Error('Invalid bundle marker');
|
|
const bundleLength = bundleHeader >> 8;
|
|
offset += 4;
|
|
const bundleEnd = offset + bundleLength;
|
|
|
|
if (bundleEnd > binary.length) throw new Error('Bundle length exceeds input size');
|
|
|
|
// Process chunks in bundle
|
|
while (offset < bundleEnd) {
|
|
// Read chunk header
|
|
if (offset + 12 > bundleEnd) throw new Error('Incomplete chunk header');
|
|
const chunkType = view.getUint8(offset);
|
|
if (chunkType !== 0x12) throw new Error(`Unsupported chunk type: ${chunkType}`);
|
|
offset += 1; // Skip udv
|
|
offset += 1;
|
|
const count = view.getUint16(offset, true); offset += 2;
|
|
if (count > 65535) throw new Error('Chunk count exceeds 65535');
|
|
const iValue = view.getUint16(offset, true); offset += 2;
|
|
const j0 = view.getUint16(offset, true); offset += 2;
|
|
const Δj = view.getInt16(offset, true); offset += 2;
|
|
const ΔelemCount = view.getUint8(offset++); // Δelem_count
|
|
const elemCount = view.getUint8(offset++); // elem_count
|
|
|
|
// Set i value (assuming all chunks share the same i)
|
|
if (result.i === null) result.i = iValue;
|
|
else if (result.i !== iValue) throw new Error('Multiple i values not supported');
|
|
|
|
// Read preface (element types)
|
|
const ΔelemTypes = [];
|
|
for (let i = 0; i < ΔelemCount; i++) {
|
|
if (offset >= bundleEnd) throw new Error('Incomplete Δelem types');
|
|
const typeByte = view.getUint8(offset++);
|
|
const baseCode = typeByte & 0x0F;
|
|
const incrCode = typeByte >> 4;
|
|
if (!codeToType[baseCode] || !codeToType[incrCode]) {
|
|
throw new Error(`Invalid type code in Δelem: ${typeByte}`);
|
|
}
|
|
ΔelemTypes.push({ baseType: codeToType[baseCode], incrType: codeToType[incrCode] });
|
|
}
|
|
const elemTypes = [];
|
|
for (let i = 0; i < elemCount; i++) {
|
|
if (offset >= bundleEnd) throw new Error('Incomplete elem types');
|
|
const typeCode = view.getUint8(offset++);
|
|
if (!codeToType[typeCode]) throw new Error(`Invalid type code in elem: ${typeCode}`);
|
|
elemTypes.push(codeToType[typeCode]);
|
|
}
|
|
|
|
// Initialize Δelems and elems arrays if first chunk
|
|
if (!result.Δelems.length && ΔelemCount > 0) {
|
|
result.Δelems = Array(ΔelemCount).fill().map(() => []);
|
|
}
|
|
if (!result.elems.length && elemCount > 0) {
|
|
result.elems = Array(elemCount).fill().map(() => []);
|
|
}
|
|
|
|
// Read initial values for Δelems
|
|
const initialValues = [];
|
|
for (const { baseType } of ΔelemTypes) {
|
|
if (offset + typeToBytes[baseType.name] > bundleEnd) {
|
|
throw new Error('Incomplete initial values');
|
|
}
|
|
initialValues.push(readTypedValue(view, offset, baseType));
|
|
offset += typeToBytes[baseType.name];
|
|
}
|
|
// Skip padding
|
|
while (offset % 4 !== 0) {
|
|
if (offset >= bundleEnd) throw new Error('Incomplete padding after initial values');
|
|
offset++;
|
|
}
|
|
|
|
// Reconstruct j values
|
|
for (let idx = 0; idx < count; idx++) {
|
|
result.j.push(j0 + idx * Δj);
|
|
}
|
|
|
|
// Read interleaved record data
|
|
for (let idx = 0; idx < count; idx++) {
|
|
// Read Δelems
|
|
for (let i = 0; i < ΔelemCount; i++) {
|
|
const values = result.Δelems[i];
|
|
const incrType = ΔelemTypes[i].incrType;
|
|
const isBigInt = typeof initialValues[i] === 'bigint';
|
|
if (offset + typeToBytes[incrType.name] > bundleEnd) {
|
|
throw new Error('Incomplete Δelem data');
|
|
}
|
|
let delta = readTypedValue(view, offset, incrType);
|
|
offset += typeToBytes[incrType.name];
|
|
if (idx === 0) {
|
|
values.push(isBigInt ? Number(initialValues[i]) : initialValues[i]);
|
|
} else {
|
|
if (isBigInt) {
|
|
delta = BigInt(delta);
|
|
initialValues[i] += delta;
|
|
values.push(Number(initialValues[i]));
|
|
} else {
|
|
initialValues[i] += delta;
|
|
values.push(initialValues[i]);
|
|
}
|
|
}
|
|
}
|
|
// Read elems
|
|
for (let i = 0; i < elemCount; i++) {
|
|
const values = result.elems[i];
|
|
const type = elemTypes[i];
|
|
const isBigInt = type === BigInt64Array || type === BigUint64Array;
|
|
if (offset + typeToBytes[type.name] > bundleEnd) {
|
|
throw new Error('Incomplete elem data');
|
|
}
|
|
let value = readTypedValue(view, offset, type);
|
|
values.push(isBigInt ? Number(value) : value);
|
|
offset += typeToBytes[type.name];
|
|
}
|
|
}
|
|
// Skip padding
|
|
while (offset % 4 !== 0) {
|
|
if (offset >= bundleEnd) throw new Error('Incomplete padding after record data');
|
|
offset++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function readTypedValue(view, offset, type) {
|
|
switch (type) {
|
|
case Int8Array: return view.getInt8(offset);
|
|
case Uint8Array: return view.getUint8(offset);
|
|
case Int16Array: return view.getInt16(offset, true);
|
|
case Uint16Array: return view.getUint16(offset, true);
|
|
case Int32Array: return view.getInt32(offset, true);
|
|
case Uint32Array: return view.getUint32(offset, true);
|
|
case Float32Array: return view.getFloat32(offset, true);
|
|
case Float64Array: return view.getFloat64(offset, true);
|
|
case BigInt64Array: return view.getBigInt64(offset, true);
|
|
case BigUint64Array: return view.getBigUint64(offset, true);
|
|
default: throw new Error(`Unsupported type: ${type.name}`);
|
|
}
|
|
}
|
|
|
|
module.exports = { sequential, interleaved };
|