node-serum2-preset-packager
Version:
Xfer Serum 2 Preset Packager for NodeJS
75 lines (71 loc) • 3.48 kB
JavaScript
;
const promises = require('node:fs/promises');
const cbor2 = require('cbor2');
const zstd = require('@mongodb-js/zstd');
/**
* @fileoverview Utilities for **packing** and **unpacking** Xfer Serum preset
* (`*.SerumPreset`) files.
*
* The binary layout, reconstructed from the reference Python implementation, is:
*
* ```
* ┌───────────────┬────────────────────────────────────────────────────────┐
* │ Offset (hex) │ Description │
* ├───────────────┼────────────────────────────────────────────────────────┤
* │ 00 │ MAGIC = "XferJson\0" (9 bytes, ASCII) │
* │ 09 │ JSON length (u32 LE) │
* │ 0D │ Reserved (u32 LE, always 0) │
* │ 11 │ Metadata (UTF-8 JSON, `JSON length` bytes) │
* │ … │ CBOR length (u32 LE) │
* │ … │ Flags (u32 LE, 2 ⇒ Zstandard compressed) │
* │ … │ Data (zstd-compressed CBOR, `CBOR length` bytes) │
* └───────────────┴────────────────────────────────────────────────────────┘
* ```
*
* All multi-byte integers are little-endian. The helpers below abstract the
* repeated 32-bit-LE conversions; the public API is exposed through the
* {@link unpack} and {@link pack} functions.
*
* @author (c) 2025 — Charles Brébant
* @license MIT
*/
const MAGIC = Buffer.from("XferJson\0", "ascii");
function u32le(buf, offset) {
return buf.readUInt32LE(offset);
}
function u32leBuf(val) {
const b = Buffer.allocUnsafe(4);
b.writeUInt32LE(val, 0);
return b;
}
async function unpack(srcPath, dstPath) {
const buf = await promises.readFile(srcPath);
if (!buf.subarray(0, MAGIC.length).equals(MAGIC)) {
throw new Error("Not a valid .SerumPreset file (magic mismatch)");
}
let off = MAGIC.length;
const jsonLen = u32le(buf, off);
off += 8;
const meta = JSON.parse(buf.subarray(off, off + jsonLen).toString("utf8"));
off += jsonLen;
const cborLen = u32le(buf, off);
off += 8;
const cborBuf = await zstd.decompress(buf.subarray(off));
if (cborBuf.length !== cborLen) {
throw new Error("Decompressed length mismatch");
}
const data = cbor2.decode(cborBuf);
await promises.writeFile(dstPath, JSON.stringify({ metadata: meta, data }, null, 2));
}
async function pack(srcPath, dstPath) {
const obj = JSON.parse(await promises.readFile(srcPath, "utf8"));
const mBuf = Buffer.from(JSON.stringify(obj.metadata), "utf8");
const cBuf = cbor2.encode(obj.data);
const zBuf = await zstd.compress(Buffer.from(cBuf), 3);
const header1 = Buffer.concat([u32leBuf(mBuf.length), u32leBuf(0)]);
const header2 = Buffer.concat([u32leBuf(cBuf.length), u32leBuf(2)]);
const out = Buffer.concat([MAGIC, header1, mBuf, header2, zBuf]);
await promises.writeFile(dstPath, out);
}
exports.pack = pack;
exports.unpack = unpack;