UNPKG

node-serum2-preset-packager

Version:
72 lines (69 loc) 3.42 kB
import { readFile, writeFile } from 'node:fs/promises'; import { decode, encode } from 'cbor2'; import { decompress, compress } from '@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 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 decompress(buf.subarray(off)); if (cborBuf.length !== cborLen) { throw new Error("Decompressed length mismatch"); } const data = decode(cborBuf); await writeFile(dstPath, JSON.stringify({ metadata: meta, data }, null, 2)); } async function pack(srcPath, dstPath) { const obj = JSON.parse(await readFile(srcPath, "utf8")); const mBuf = Buffer.from(JSON.stringify(obj.metadata), "utf8"); const cBuf = encode(obj.data); const zBuf = await 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 writeFile(dstPath, out); } export { pack, unpack };