This commit is contained in:
seb
2026-07-09 10:57:37 +02:00
commit 9466d295a4
7 changed files with 855 additions and 0 deletions

126
parse-log.mjs Normal file
View File

@@ -0,0 +1,126 @@
// CLI: parse ESC/POS .bin print jobs logged by the proxy.
//
// This is a *parser*: it decomposes the raw byte stream into the ESC/POS
// command grammar — each control sequence recognized by its opcode with its
// parsed parameters, and text as raw byte spans (decoded via the selected code
// page). Nothing is interpreted into higher-level meaning (no "this is bold",
// no line/style inference). Every byte is preserved and the stream can be
// rebuilt via `serialize()` (verified by default).
//
// Usage:
// node parse-log.mjs <file-or-dir> [options]
//
// Options:
// --all treat each argument as a directory and parse all *.bin in it
// --hex also print the raw bytes of every token
// --text print only the decoded text spans (convenience view)
// --image render raster bit images as ASCII art (downsampled)
// --max-width n ASCII image width (default 80)
// --no-verify skip the losslessness round-trip check
import fs from "node:fs";
import path from "node:path";
import { parse, serialize, renderText, renderImage, renderTokens, summarize } from "./parser.js";
function parseArgs(argv) {
const opts = { files: [], mode: "tokens", hex: false, image: false, maxWidth: 80, all: false, verify: true };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === "--text") opts.mode = "text";
else if (a === "--hex") opts.hex = true;
else if (a === "--image") opts.image = true;
else if (a === "--all") opts.all = true;
else if (a === "--no-verify") opts.verify = false;
else if (a === "--max-width") opts.maxWidth = Number(argv[++i]) || 80;
else if (a.startsWith("--")) console.error(`[warn] unknown option ${a}`);
else opts.files.push(a);
}
return opts;
}
function expandTargets(targets, all) {
const out = [];
for (const t of targets) {
const stat = fs.statSync(t);
if (stat.isDirectory()) {
out.push(
...fs
.readdirSync(t)
.filter((f) => f.endsWith(".bin"))
.map((f) => path.join(t, f))
.sort()
);
} else out.push(t);
}
return out;
}
function main() {
const opts = parseArgs(process.argv.slice(2));
if (opts.files.length === 0) {
console.error(
"usage: node parse-log.mjs <file-or-dir> [--hex|--text] [--image] [--all] [--no-verify]"
);
process.exit(1);
}
const files = expandTargets(opts.files, opts.all);
let hadError = false;
let allOk = true;
for (const f of files) {
let buf;
try {
buf = fs.readFileSync(f);
} catch (err) {
console.error(`[error] cannot read ${f}: ${err.message}`);
hadError = true;
continue;
}
const events = parse(buf);
const s = summarize(events);
const name = path.basename(f);
let ok = true;
if (opts.verify) {
const out = serialize(events);
ok = out.length === buf.length && out.equals(buf);
if (!ok) {
allOk = false;
console.error(`[FAIL] ${name}: round-trip mismatch (${out.length} vs ${buf.length} bytes)`);
}
}
const status = opts.verify ? (ok ? "OK" : "LOSSY") : "unverified";
if (opts.mode === "text") {
console.log(`\n===== ${name} (${buf.length} bytes) =====\n`);
console.log(renderText(events));
continue;
}
console.log(`\n===== ${name} (${buf.length} bytes) — round-trip: ${status} =====\n`);
console.log(renderTokens(events, { hex: opts.hex }));
if (opts.image) {
for (const e of events) {
if (e.type === "image") {
console.log(`\n-- raster image ${e.width}x${e.height} --`);
console.log(renderImage(e, opts.maxWidth));
}
}
}
console.log(
`\n[summary] ${s.commands} commands, ${s.images} image(s), ${s.cuts} cut(s), ` +
`${s.text} text runs (${s.textBytes} bytes)`
);
}
if (opts.verify && !allOk) {
console.error("\n[error] at least one file was not losslessly reconstructed");
process.exit(1);
}
process.exit(hadError ? 1 : 0);
}
main();