@chestnutlabs/gcode-bgcode 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @chestnutlabs/gcode-bgcode
2
+
3
+ Binary G-code (`.bgcode`) **decode** for the Chestnut Labs G-code toolpath stack (DD-011, epic #188).
4
+
5
+ Prusa's `.bgcode` is a *container* of ordinary G-code, not a new toolpath language. This package walks
6
+ the binary block structure, verifies per-block CRC32, decompresses, decodes, and concatenates the GCode
7
+ blocks into **plain G-code** — which the existing parser / dialect / IR / renderer / progress pipeline
8
+ consumes unchanged. **Decode-only** (it never writes `.bgcode`), **in-memory only** (no filesystem, no
9
+ network), zero runtime dependencies beyond `@chestnutlabs/toolpath-core` and
10
+ `@chestnutlabs/gcode-containers`.
11
+
12
+ ## Licensing (why this is a clean-room decoder)
13
+
14
+ The reference `libbgcode` and OctoPrint-MeatPack are **AGPL-3.0**; none of their code is used here — the
15
+ block walker is written clean-room from the published format spec ([RR-003](https://github.com/ChestnutLabs/gcode-preview/blob/dev/docs/research/RR-003-bgcode-licensing-and-format-audit.md)).
16
+ The **MeatPack** decoder is a faithful port of the **MIT** [`jamesgopsill/meatpack`](https://github.com/jamesgopsill/meatpack)
17
+ (© 2025 James Gopsill), and the **heatshrink** decoder is a port of the **ISC**
18
+ [`atomicobject/heatshrink`](https://github.com/atomicobject/heatshrink) (© 2013–2015 Scott Vokes) —
19
+ both attributions preserved in their source files. This package is MIT.
20
+
21
+ ## Status — phased (DD-011 §14)
22
+
23
+ | Capability | Status |
24
+ |---|---|
25
+ | Block walker + CRC32 + `sniff`/`open` | ✅ phase 1 |
26
+ | Compression: None, DEFLATE | ✅ phase 1 |
27
+ | Encoding: None | ✅ phase 1 |
28
+ | Encoding: MeatPack (both variants) | ✅ phase 2 |
29
+ | Compression: heatshrink 11/12 | ✅ phase 3 |
30
+ | DEFLATE zlib-flavor fix + golden-equivalence killer test (real Prusa files) | ✅ phase 4 |
31
+ | Container adapter + batteries-worker registration + metadata/thumbnail surfacing | ✅ phase 4c |
32
+ | Adversarial fuzz corpus + §7 security review + decode benchmark | ✅ phase 5 |
33
+
34
+ ## Usage
35
+
36
+ ```ts
37
+ import { openBgcode, sniffBgcode } from '@chestnutlabs/gcode-bgcode';
38
+
39
+ if (sniffBgcode(firstBytes, name)) {
40
+ const { gcode, blocks, checksum } = await openBgcode(bytes, { metadata: true });
41
+ // `gcode` is plain G-code (Uint8Array) → feed the existing parser.
42
+ }
43
+ ```
44
+
45
+ It also registers as a **container adapter** in the batteries parser worker, so a `.bgcode` file "just
46
+ works" through `GcodeParseSession` (`containers: 'auto'`) — decoded to G-code, with the machine geometry
47
+ (`bed_shape`) and whitelisted slicer settings surfaced as container metadata. Thumbnails are decoded and
48
+ available on `openBgcodeContainer(bytes).thumbnails`.
49
+
50
+ Every failure — bad magic/version, CRC mismatch, truncation, unknown/unsupported codec, or a
51
+ decompression bomb — is a structured, bounded `ContainerError` (never a crash or an unbounded read).
@@ -0,0 +1,47 @@
1
+ /**
2
+ * `.bgcode` container adapter (DD-011 phase 4c, #188) — plugs `.bgcode` into the existing parser
3
+ * pipeline via the DD-005 §4.4 container-adapter contract (`{ id, sniff, open }`), beside `.gcode.3mf`.
4
+ * The worker sniffs the input, calls `open`, and parses the single decoded plate; the metadata
5
+ * (machine geometry + whitelisted settings) rides beside the IR.
6
+ *
7
+ * `.bgcode` is single-payload — one plate, no `openPlate` index beyond 0.
8
+ */
9
+ import type { MachineGeometry } from '@chestnutlabs/toolpath-core';
10
+ import { sniffBgcode, type BgcodeThumbnail } from './bgcode.js';
11
+ /** Minimal readable-stream mirror the parser worker consumes (same shape as gcode-containers). */
12
+ export interface BgcodeStreamLike {
13
+ getReader(): {
14
+ read(): Promise<{
15
+ done: boolean;
16
+ value?: Uint8Array;
17
+ }>;
18
+ releaseLock?(): void;
19
+ };
20
+ }
21
+ export interface OpenedBgcode {
22
+ id: 'bgcode';
23
+ plates: {
24
+ index: number;
25
+ name: string;
26
+ estimatedBytes: number;
27
+ }[];
28
+ metadata: {
29
+ machine?: MachineGeometry;
30
+ raw: Record<string, string>;
31
+ warnings: {
32
+ code: string;
33
+ message: string;
34
+ }[];
35
+ };
36
+ /** Thumbnails from the container (bgcode carries them as binary blocks, not G-code comments). */
37
+ thumbnails: BgcodeThumbnail[];
38
+ openPlate(index: number): BgcodeStreamLike;
39
+ }
40
+ /**
41
+ * Open a `.bgcode` buffer as a container: decode it to plain G-code (the single plate) and surface its
42
+ * metadata + thumbnails. Errors are the structured `ContainerError`s from {@link openBgcode}.
43
+ */
44
+ export declare function openBgcodeContainer(bytes: Uint8Array): Promise<OpenedBgcode>;
45
+ /** Re-export the sniff so a worker entry can register `{ id, sniff, open }` in one import. */
46
+ export { sniffBgcode };
47
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,eAAe,EAAU,MAAM,6BAA6B,CAAC;AAC3E,OAAO,EAAc,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAE5E,kGAAkG;AAClG,MAAM,WAAW,gBAAgB;IAC/B,SAAS,IAAI;QAAE,IAAI,IAAI,OAAO,CAAC;YAAE,IAAI,EAAE,OAAO,CAAC;YAAC,KAAK,CAAC,EAAE,UAAU,CAAA;SAAE,CAAC,CAAC;QAAC,WAAW,CAAC,IAAI,IAAI,CAAA;KAAE,CAAC;CAC/F;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,QAAQ,CAAC;IACb,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAClE,QAAQ,EAAE;QACR,OAAO,CAAC,EAAE,eAAe,CAAC;QAC1B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC5B,QAAQ,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;KAC/C,CAAC;IACF,iGAAiG;IACjG,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAAC;CAC5C;AA+DD;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAclF;AAED,8FAA8F;AAC9F,OAAO,EAAE,WAAW,EAAE,CAAC"}
@@ -0,0 +1,84 @@
1
+ import { openBgcode, sniffBgcode } from './bgcode.js';
2
+ /** Settings keys copied into `metadata.raw` — a whitelist (feeds dialect detection + provenance), never the whole 390-key config. */
3
+ const RAW_WHITELIST = [
4
+ 'Producer',
5
+ 'printer_model',
6
+ 'printer_settings_id',
7
+ 'print_settings_id',
8
+ 'bed_shape',
9
+ 'max_print_height',
10
+ 'printable_height',
11
+ 'nozzle_diameter',
12
+ 'filament_type',
13
+ 'filament_colour'
14
+ ];
15
+ /** Parse a PrusaSlicer `bed_shape` ("0x0,360x0,360x360,0x360") into points. */
16
+ function parseBedShape(value) {
17
+ const parts = value.split(',');
18
+ if (parts.length < 3 || parts.length > 64)
19
+ return null;
20
+ const points = [];
21
+ for (const p of parts) {
22
+ const m = /^\s*(-?\d+(?:\.\d+)?)x(-?\d+(?:\.\d+)?)\s*$/.exec(p);
23
+ if (m === null)
24
+ return null;
25
+ points.push({ x: parseFloat(m[1]), y: parseFloat(m[2]) });
26
+ }
27
+ return points;
28
+ }
29
+ /** Build a MachineGeometry from the decoded `.bgcode` settings (bed_shape + max_print_height + model). */
30
+ function machineFrom(settings) {
31
+ const points = settings.bed_shape ? parseBedShape(settings.bed_shape) : null;
32
+ if (points === null)
33
+ return undefined;
34
+ const xs = points.map((p) => p.x);
35
+ const ys = points.map((p) => p.y);
36
+ const min = { x: Math.min(...xs), y: Math.min(...ys) };
37
+ const max = { x: Math.max(...xs), y: Math.max(...ys) };
38
+ const rect = points.length === 4 && points.every((p) => (p.x === min.x || p.x === max.x) && (p.y === min.y || p.y === max.y));
39
+ const h = parseFloat(settings.max_print_height ?? settings.printable_height ?? '');
40
+ return {
41
+ bed: rect ? { kind: 'rect', min, max } : { kind: 'polygon', points },
42
+ origin: { x: 0, y: 0 },
43
+ heightMm: Number.isFinite(h) ? h : undefined,
44
+ printerName: settings.printer_model,
45
+ confidence: 'inferred', // read from the slicer's embedded settings, not machine config
46
+ source: { adapterId: 'bgcode', evidence: 'bgcode metadata (bed_shape)' }
47
+ };
48
+ }
49
+ /** A one-shot stream over an in-memory buffer. */
50
+ function streamOf(bytes) {
51
+ return {
52
+ getReader() {
53
+ let done = false;
54
+ return {
55
+ read: () => Promise.resolve(done ? { done: true } : ((done = true), { done: false, value: bytes })),
56
+ releaseLock() { }
57
+ };
58
+ }
59
+ };
60
+ }
61
+ /**
62
+ * Open a `.bgcode` buffer as a container: decode it to plain G-code (the single plate) and surface its
63
+ * metadata + thumbnails. Errors are the structured `ContainerError`s from {@link openBgcode}.
64
+ */
65
+ export async function openBgcodeContainer(bytes) {
66
+ const { gcode, settings, thumbnails } = await openBgcode(bytes, { metadata: true });
67
+ const raw = {};
68
+ for (const key of RAW_WHITELIST)
69
+ if (settings[key] !== undefined)
70
+ raw[key] = settings[key];
71
+ return {
72
+ id: 'bgcode',
73
+ plates: [{ index: 0, name: 'bgcode', estimatedBytes: gcode.length }],
74
+ metadata: { machine: machineFrom(settings), raw, warnings: [] },
75
+ thumbnails,
76
+ openPlate(index) {
77
+ if (index !== 0)
78
+ throw new Error(`.bgcode has a single plate; requested ${index}`);
79
+ return streamOf(gcode);
80
+ }
81
+ };
82
+ }
83
+ /** Re-export the sniff so a worker entry can register `{ id, sniff, open }` in one import. */
84
+ export { sniffBgcode };
@@ -0,0 +1,74 @@
1
+ /** ASCII magic at the start of every `.bgcode` file: "GCDE". */
2
+ export declare const BGCODE_MAGIC = "GCDE";
3
+ /** The only file format version this decoder accepts (DD-011 pins v1). */
4
+ export declare const BGCODE_VERSION = 1;
5
+ /** Block type IDs (spec §5.2). */
6
+ export declare const BgcodeBlockType: {
7
+ readonly FileMetadata: 0;
8
+ readonly GCode: 1;
9
+ readonly SlicerMetadata: 2;
10
+ readonly PrinterMetadata: 3;
11
+ readonly PrintMetadata: 4;
12
+ readonly Thumbnail: 5;
13
+ };
14
+ /** Compression algorithm IDs (spec §5.2). */
15
+ export declare const BgcodeCompression: {
16
+ readonly None: 0;
17
+ readonly Deflate: 1;
18
+ readonly Heatshrink11: 2;
19
+ readonly Heatshrink12: 3;
20
+ };
21
+ /** GCode-block encoding IDs (spec §5.2). */
22
+ export declare const BgcodeEncoding: {
23
+ readonly None: 0;
24
+ readonly MeatPack: 1;
25
+ readonly MeatPackComments: 2;
26
+ };
27
+ /** Metadata read off a walked block header (no payload for non-GCode blocks in phase 1). */
28
+ export interface BgcodeBlockInfo {
29
+ type: number;
30
+ compression: number;
31
+ /** GCode/metadata encoding param; 0 for thumbnails. */
32
+ encoding: number;
33
+ uncompressedSize: number;
34
+ compressedSize: number;
35
+ }
36
+ /** A thumbnail decoded from a `.bgcode` Thumbnail block (#188 phase 4c). */
37
+ export interface BgcodeThumbnail {
38
+ format: 'png' | 'jpg' | 'qoi' | 'unknown';
39
+ width: number;
40
+ height: number;
41
+ bytes: Uint8Array;
42
+ }
43
+ export interface BgcodeDecodeResult {
44
+ /** The decoded plain G-code (all GCode blocks concatenated in file order). */
45
+ gcode: Uint8Array;
46
+ /** Every block walked, in file order (for diagnostics / phase-4 metadata). */
47
+ blocks: BgcodeBlockInfo[];
48
+ /** Whether the file declared per-block CRC32. */
49
+ checksum: 'none' | 'crc32';
50
+ /**
51
+ * Merged INI key/values from the File/Printer/Print/Slicer metadata blocks (#188 phase 4c) — only
52
+ * populated when `metadata: true`. Later blocks win on key conflicts. Empty otherwise.
53
+ */
54
+ settings: Record<string, string>;
55
+ /** Thumbnails from Thumbnail blocks (#188 phase 4c) — only when `metadata: true`. */
56
+ thumbnails: BgcodeThumbnail[];
57
+ }
58
+ export interface BgcodeDecodeOptions {
59
+ /** Hard cap on total decoded G-code bytes — decompression-bomb defense (default 512 MiB). */
60
+ maxOutputBytes?: number;
61
+ /** Also decode the metadata (INI) + thumbnail blocks into `settings`/`thumbnails` (default false). */
62
+ metadata?: boolean;
63
+ }
64
+ /** True if `prefix`/`name` look like a `.bgcode` file (magic "GCDE", or a `.bgcode` name). */
65
+ export declare function sniffBgcode(prefix: Uint8Array, name?: string): boolean;
66
+ /**
67
+ * Decode a `.bgcode` v1 buffer to plain G-code. Walks every block, verifies per-block CRC32 (when the
68
+ * file declares it), decompresses + decodes the GCode blocks, and concatenates them in file order.
69
+ * Non-GCode blocks are walked past (their metadata/thumbnails are surfaced in phase 4). Every failure
70
+ * is a structured, bounded `ContainerError` (from `@chestnutlabs/gcode-containers`) — never a crash or
71
+ * an unbounded read/allocation.
72
+ */
73
+ export declare function openBgcode(bytes: Uint8Array, opts?: BgcodeDecodeOptions): Promise<BgcodeDecodeResult>;
74
+ //# sourceMappingURL=bgcode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bgcode.d.ts","sourceRoot":"","sources":["../src/bgcode.ts"],"names":[],"mappings":"AAkBA,gEAAgE;AAChE,eAAO,MAAM,YAAY,SAAS,CAAC;AACnC,0EAA0E;AAC1E,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,kCAAkC;AAClC,eAAO,MAAM,eAAe;;;;;;;CAOlB,CAAC;AAEX,6CAA6C;AAC7C,eAAO,MAAM,iBAAiB;;;;;CAKpB,CAAC;AAEX,4CAA4C;AAC5C,eAAO,MAAM,cAAc;;;;CAIjB,CAAC;AAEX,4FAA4F;AAC5F,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,SAAS,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,8EAA8E;IAC9E,KAAK,EAAE,UAAU,CAAC;IAClB,8EAA8E;IAC9E,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,iDAAiD;IACjD,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,qFAAqF;IACrF,UAAU,EAAE,eAAe,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,6FAA6F;IAC7F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sGAAsG;IACtG,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA0BD,8FAA8F;AAC9F,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAStE;AA0FD;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA4F/G"}
package/dist/bgcode.js ADDED
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Binary G-code (`.bgcode`) decoder — spec **version 1** (DD-011; format audited in RR-003).
3
+ *
4
+ * Clean-room from the published Prusa block-format spec — NO code is derived from the AGPL
5
+ * `libbgcode` reference or the AGPL OctoPrint-MeatPack (RR-003 §6). In-memory only, no fs/net.
6
+ *
7
+ * `.bgcode` is a *container* of ordinary G-code: this walks the block structure, verifies per-block
8
+ * CRC32, decompresses, decodes, and concatenates the GCode blocks into a plain-G-code buffer the
9
+ * existing parser consumes unchanged. **Decode-only** — never writes `.bgcode`.
10
+ *
11
+ * Phase 1 (this file) implements the walker + CRC + **None/DEFLATE** compression and **None**
12
+ * encoding. MeatPack (encoding) and heatshrink (compression) are honest, structured "not yet
13
+ * supported" errors until phases 2 and 3.
14
+ */
15
+ import { ContainerError, crc32, crc32Final } from '@chestnutlabs/gcode-containers';
16
+ import { meatpackDecode } from './meatpack.js';
17
+ import { heatshrinkDecode } from './heatshrink.js';
18
+ /** ASCII magic at the start of every `.bgcode` file: "GCDE". */
19
+ export const BGCODE_MAGIC = 'GCDE';
20
+ /** The only file format version this decoder accepts (DD-011 pins v1). */
21
+ export const BGCODE_VERSION = 1;
22
+ /** Block type IDs (spec §5.2). */
23
+ export const BgcodeBlockType = {
24
+ FileMetadata: 0,
25
+ GCode: 1,
26
+ SlicerMetadata: 2,
27
+ PrinterMetadata: 3,
28
+ PrintMetadata: 4,
29
+ Thumbnail: 5
30
+ };
31
+ /** Compression algorithm IDs (spec §5.2). */
32
+ export const BgcodeCompression = {
33
+ None: 0,
34
+ Deflate: 1,
35
+ Heatshrink11: 2, // window 11, lookahead 4
36
+ Heatshrink12: 3 // window 12, lookahead 4
37
+ };
38
+ /** GCode-block encoding IDs (spec §5.2). */
39
+ export const BgcodeEncoding = {
40
+ None: 0,
41
+ MeatPack: 1,
42
+ MeatPackComments: 2
43
+ };
44
+ const THUMB_FORMAT = { 0: 'png', 1: 'jpg', 2: 'qoi' };
45
+ /** Parse a PrusaSlicer-style INI block (`key = value` lines) into a record. */
46
+ function parseIni(text) {
47
+ const out = {};
48
+ for (const line of text.split('\n')) {
49
+ const eq = line.indexOf('=');
50
+ if (eq <= 0)
51
+ continue;
52
+ const key = line.slice(0, eq).trim();
53
+ if (key.length === 0 || key.startsWith(';') || key.startsWith('#') || key.startsWith('['))
54
+ continue;
55
+ out[key] = line.slice(eq + 1).trim();
56
+ }
57
+ return out;
58
+ }
59
+ const DEFAULT_MAX_OUTPUT = 512 * 1024 * 1024;
60
+ function readU16(b, o) {
61
+ return b[o] | (b[o + 1] << 8);
62
+ }
63
+ function readU32(b, o) {
64
+ return (b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24)) >>> 0;
65
+ }
66
+ /** True if `prefix`/`name` look like a `.bgcode` file (magic "GCDE", or a `.bgcode` name). */
67
+ export function sniffBgcode(prefix, name) {
68
+ if (name !== undefined && /\.bgcode$/i.test(name))
69
+ return true;
70
+ return (prefix.length >= 4 &&
71
+ prefix[0] === 0x47 /* G */ &&
72
+ prefix[1] === 0x43 /* C */ &&
73
+ prefix[2] === 0x44 /* D */ &&
74
+ prefix[3] === 0x45 /* E */);
75
+ }
76
+ /** DEFLATE-decode `data` to at most `limit` bytes via the platform stream (bounded — bomb-safe). */
77
+ async function inflate(data, expected, limit) {
78
+ // Flavor: zlib-wrapped DEFLATE. Confirmed against a real Prusa XL `.bgcode` (phase 4) — its
79
+ // DEFLATE metadata blocks decode with the zlib header, and fail as raw. (bgcode GCode blocks use
80
+ // heatshrink, not DEFLATE; only Slicer/Print metadata blocks are DEFLATE.)
81
+ const ds = new DecompressionStream('deflate');
82
+ const writer = ds.writable.getWriter();
83
+ const reader = ds.readable.getReader();
84
+ // The writer promise must never float unobserved: a corrupt stream errors BOTH ends, and an
85
+ // unawaited writer rejection crashes the process (the #131 fuzzer lesson from gcode-containers).
86
+ let writeError;
87
+ const writeAll = (async () => {
88
+ await writer.write(data);
89
+ await writer.close();
90
+ })().catch((e) => {
91
+ writeError = e;
92
+ });
93
+ const chunks = [];
94
+ let total = 0;
95
+ try {
96
+ for (;;) {
97
+ const { done, value } = await reader.read();
98
+ if (done)
99
+ break;
100
+ if (value !== undefined && value.byteLength > 0) {
101
+ total += value.byteLength;
102
+ if (total > expected || total > limit) {
103
+ throw new ContainerError('E_BGCODE_BOMB', `deflate output exceeds the declared/limit size (${total})`);
104
+ }
105
+ chunks.push(value);
106
+ }
107
+ }
108
+ await writeAll;
109
+ if (writeError !== undefined)
110
+ throw writeError;
111
+ }
112
+ catch (err) {
113
+ await writeAll; // settled by construction; never rejects
114
+ if (err instanceof ContainerError)
115
+ throw err;
116
+ throw new ContainerError('E_BGCODE_INFLATE', `deflate failed: ${err instanceof Error ? err.message : err}`);
117
+ }
118
+ const out = new Uint8Array(total);
119
+ let o = 0;
120
+ for (const c of chunks) {
121
+ out.set(c, o);
122
+ o += c.byteLength;
123
+ }
124
+ return out;
125
+ }
126
+ /** Decompress one block's data by its compression ID, clamped to the declared/limit size. */
127
+ async function decompress(compression, data, uncompressed, limit) {
128
+ if (compression === BgcodeCompression.None) {
129
+ if (data.length !== uncompressed) {
130
+ throw new ContainerError('E_BGCODE_SIZE', `stored block size disagreement (${data.length} vs ${uncompressed})`);
131
+ }
132
+ return data;
133
+ }
134
+ if (compression === BgcodeCompression.Deflate) {
135
+ const out = await inflate(data, uncompressed, limit);
136
+ if (out.length !== uncompressed) {
137
+ throw new ContainerError('E_BGCODE_SIZE', `deflate size disagreement (${out.length} vs ${uncompressed})`);
138
+ }
139
+ return out;
140
+ }
141
+ if (compression === BgcodeCompression.Heatshrink11 || compression === BgcodeCompression.Heatshrink12) {
142
+ const windowBits = compression === BgcodeCompression.Heatshrink11 ? 11 : 12;
143
+ const out = heatshrinkDecode(data, windowBits, 4, limit);
144
+ if (out.length !== uncompressed) {
145
+ throw new ContainerError('E_BGCODE_SIZE', `heatshrink size disagreement (${out.length} vs ${uncompressed})`);
146
+ }
147
+ return out;
148
+ }
149
+ throw new ContainerError('E_BGCODE_UNSUPPORTED_COMPRESSION', `unknown compression id ${compression}`);
150
+ }
151
+ /** Decode a GCode block's post-decompression bytes by its encoding ID, bounded to `limit` output bytes. */
152
+ function decode(encoding, bytes, limit) {
153
+ if (encoding === BgcodeEncoding.None)
154
+ return bytes;
155
+ // Both MeatPack variants decode identically — comment stripping is an encoder choice (#188 phase 2).
156
+ if (encoding === BgcodeEncoding.MeatPack || encoding === BgcodeEncoding.MeatPackComments) {
157
+ return meatpackDecode(bytes, limit);
158
+ }
159
+ throw new ContainerError('E_BGCODE_UNSUPPORTED_ENCODING', `unknown gcode encoding id ${encoding}`);
160
+ }
161
+ /**
162
+ * Decode a `.bgcode` v1 buffer to plain G-code. Walks every block, verifies per-block CRC32 (when the
163
+ * file declares it), decompresses + decodes the GCode blocks, and concatenates them in file order.
164
+ * Non-GCode blocks are walked past (their metadata/thumbnails are surfaced in phase 4). Every failure
165
+ * is a structured, bounded `ContainerError` (from `@chestnutlabs/gcode-containers`) — never a crash or
166
+ * an unbounded read/allocation.
167
+ */
168
+ export async function openBgcode(bytes, opts = {}) {
169
+ const limit = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT;
170
+ const len = bytes.length;
171
+ if (len < 10)
172
+ throw new ContainerError('E_BGCODE_TRUNCATED', 'file shorter than the 10-byte header');
173
+ if (!sniffBgcode(bytes))
174
+ throw new ContainerError('E_BGCODE_MAGIC', 'missing "GCDE" magic');
175
+ const version = readU32(bytes, 4);
176
+ if (version !== BGCODE_VERSION)
177
+ throw new ContainerError('E_BGCODE_VERSION', `unsupported file version ${version} (expected 1)`);
178
+ const checksumType = readU16(bytes, 8);
179
+ if (checksumType > 1)
180
+ throw new ContainerError('E_BGCODE_CHECKSUM', `unknown checksum type ${checksumType}`);
181
+ const hasCrc = checksumType === 1;
182
+ const wantMeta = opts.metadata === true;
183
+ const blocks = [];
184
+ const gcodeParts = [];
185
+ const settings = {};
186
+ const thumbnails = [];
187
+ let totalOut = 0;
188
+ let off = 10;
189
+ while (off < len) {
190
+ const blockStart = off;
191
+ if (off + 8 > len)
192
+ throw new ContainerError('E_BGCODE_TRUNCATED', 'truncated block header');
193
+ const type = readU16(bytes, off);
194
+ const compression = readU16(bytes, off + 2);
195
+ const uncompressedSize = readU32(bytes, off + 4);
196
+ off += 8;
197
+ let compressedSize = uncompressedSize;
198
+ if (compression !== BgcodeCompression.None) {
199
+ if (off + 4 > len)
200
+ throw new ContainerError('E_BGCODE_TRUNCATED', 'truncated block header (compressed size)');
201
+ compressedSize = readU32(bytes, off);
202
+ off += 4;
203
+ }
204
+ // Block parameters: 2 bytes (encoding u16) for metadata/GCode; 6 bytes (format+w+h) for thumbnails.
205
+ const isThumb = type === BgcodeBlockType.Thumbnail;
206
+ const paramLen = isThumb ? 6 : 2;
207
+ if (off + paramLen > len)
208
+ throw new ContainerError('E_BGCODE_TRUNCATED', 'truncated block parameters');
209
+ const encoding = isThumb ? 0 : readU16(bytes, off);
210
+ const thumbFormat = isThumb ? readU16(bytes, off) : 0;
211
+ const thumbWidth = isThumb ? readU16(bytes, off + 2) : 0;
212
+ const thumbHeight = isThumb ? readU16(bytes, off + 4) : 0;
213
+ off += paramLen;
214
+ const dataLen = compression === BgcodeCompression.None ? uncompressedSize : compressedSize;
215
+ if (off + dataLen > len)
216
+ throw new ContainerError('E_BGCODE_TRUNCATED', 'truncated block data');
217
+ const data = bytes.subarray(off, off + dataLen);
218
+ off += dataLen;
219
+ if (hasCrc) {
220
+ if (off + 4 > len)
221
+ throw new ContainerError('E_BGCODE_TRUNCATED', 'truncated block checksum');
222
+ const stored = readU32(bytes, off);
223
+ const calc = crc32Final(crc32(bytes.subarray(blockStart, off))); // over header + params + data
224
+ if (stored !== calc) {
225
+ throw new ContainerError('E_BGCODE_CRC', `block CRC32 mismatch at offset ${blockStart}`);
226
+ }
227
+ off += 4;
228
+ }
229
+ blocks.push({ type, compression, encoding, uncompressedSize, compressedSize });
230
+ if (type === BgcodeBlockType.GCode) {
231
+ const decompressed = await decompress(compression, data, uncompressedSize, limit - totalOut);
232
+ const decoded = decode(encoding, decompressed, limit - totalOut);
233
+ totalOut += decoded.length;
234
+ if (totalOut > limit)
235
+ throw new ContainerError('E_BGCODE_BOMB', `decoded G-code exceeds the output limit (${totalOut})`);
236
+ gcodeParts.push(decoded);
237
+ }
238
+ else if (wantMeta && isThumb) {
239
+ // Thumbnails are stored as image bytes (PrusaSlicer uses None compression); decompress if flagged.
240
+ const raw = await decompress(compression, data, uncompressedSize, limit);
241
+ thumbnails.push({
242
+ format: THUMB_FORMAT[thumbFormat] ?? 'unknown',
243
+ width: thumbWidth,
244
+ height: thumbHeight,
245
+ bytes: raw.slice()
246
+ });
247
+ }
248
+ else if (wantMeta && (type === 0 || type === 2 || type === 3 || type === 4)) {
249
+ // File/Printer/Print/Slicer metadata: an INI block (encoding 0), maybe DEFLATE-compressed.
250
+ const ini = await decompress(compression, data, uncompressedSize, limit);
251
+ Object.assign(settings, parseIni(new TextDecoder().decode(ini)));
252
+ }
253
+ }
254
+ const gcode = new Uint8Array(totalOut);
255
+ let o = 0;
256
+ for (const part of gcodeParts) {
257
+ gcode.set(part, o);
258
+ o += part.length;
259
+ }
260
+ return { gcode, blocks, checksum: hasCrc ? 'crc32' : 'none', settings, thumbnails };
261
+ }
@@ -0,0 +1,3 @@
1
+ /** Decode a heatshrink stream. `windowBits`/`lookaheadBits` come from the bgcode compression id. */
2
+ export declare function heatshrinkDecode(data: Uint8Array, windowBits: number, lookaheadBits: number, limit: number): Uint8Array;
3
+ //# sourceMappingURL=heatshrink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heatshrink.d.ts","sourceRoot":"","sources":["../src/heatshrink.ts"],"names":[],"mappings":"AAeA,oGAAoG;AACpG,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,MAAM,GACZ,UAAU,CAyDZ"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * heatshrink LZSS decoder (DD-011 phase 3, #188).
3
+ *
4
+ * A TypeScript port of the decoder from **atomicobject/heatshrink** (ISC, © 2013–2015 Scott Vokes —
5
+ * https://github.com/atomicobject/heatshrink). ISC is permissive and MIT-compatible; porting with the
6
+ * attribution preserved is the sanctioned path (RR-003 §8) — the AGPL `libbgcode` is never copied.
7
+ *
8
+ * Wire format (whole-buffer form of the reference's streaming state machine): a big-endian (MSB-first)
9
+ * bit stream of tokens. Each token starts with a tag bit — `1` = an 8-bit literal byte; `0` = a
10
+ * back-reference: `windowBits` of index then `lookaheadBits` of count (both stored as value−1). A
11
+ * back-reference copies `count+1` bytes from `index+1` bytes back in a circular window, re-emitting
12
+ * each into the window (so runs can reference themselves). bgcode uses window 11 or 12, lookahead 4.
13
+ */
14
+ import { ContainerError } from '@chestnutlabs/gcode-containers';
15
+ /** Decode a heatshrink stream. `windowBits`/`lookaheadBits` come from the bgcode compression id. */
16
+ export function heatshrinkDecode(data, windowBits, lookaheadBits, limit) {
17
+ const windowSize = 1 << windowBits;
18
+ const mask = windowSize - 1;
19
+ const window = new Uint8Array(windowSize);
20
+ let head = 0; // write cursor into the circular window (monotonic; masked on access)
21
+ let out = new Uint8Array(Math.max(64, Math.min(limit, data.length * 4)));
22
+ let pos = 0;
23
+ const emit = (c) => {
24
+ if (pos >= limit)
25
+ throw new ContainerError('E_BGCODE_BOMB', `heatshrink output exceeds the limit (${pos})`);
26
+ if (pos >= out.length) {
27
+ const bigger = new Uint8Array(Math.min(limit, Math.max(out.length * 2, pos + 1)));
28
+ bigger.set(out.subarray(0, pos));
29
+ out = bigger;
30
+ }
31
+ out[pos++] = c;
32
+ window[head++ & mask] = c;
33
+ };
34
+ // MSB-first bit reader over `data`. Returns null when fewer than `count` bits remain (end of input).
35
+ let bytePos = 0;
36
+ let bitIndex = 0; // 0 means "load next byte"; otherwise a single-bit mask (0x80..0x01)
37
+ let currentByte = 0;
38
+ const getBits = (count) => {
39
+ let acc = 0;
40
+ for (let i = 0; i < count; i++) {
41
+ if (bitIndex === 0) {
42
+ if (bytePos >= data.length)
43
+ return null;
44
+ currentByte = data[bytePos++];
45
+ bitIndex = 0x80;
46
+ }
47
+ acc = (acc << 1) | (currentByte & bitIndex ? 1 : 0);
48
+ bitIndex >>= 1;
49
+ }
50
+ return acc;
51
+ };
52
+ for (;;) {
53
+ const tag = getBits(1);
54
+ if (tag === null)
55
+ break; // end of input (trailing byte-padding can never form a full token)
56
+ if (tag === 1) {
57
+ const byte = getBits(8);
58
+ if (byte === null)
59
+ break;
60
+ emit(byte & 0xff);
61
+ }
62
+ else {
63
+ const index = getBits(windowBits);
64
+ if (index === null)
65
+ break;
66
+ const count = getBits(lookaheadBits);
67
+ if (count === null)
68
+ break;
69
+ const negOffset = index + 1;
70
+ const copyCount = count + 1;
71
+ for (let i = 0; i < copyCount; i++) {
72
+ emit(window[(head - negOffset) & mask]);
73
+ }
74
+ }
75
+ }
76
+ return out.subarray(0, pos);
77
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @chestnutlabs/gcode-bgcode — Binary G-code (`.bgcode`) decode adapter (DD-011, epic #188).
3
+ *
4
+ * A license-clean, in-memory decoder for Prusa's binary G-code container. `.bgcode` is a container
5
+ * of ordinary G-code, so decoding it (block walk → CRC verify → decompress → decode → concatenate)
6
+ * yields plain G-code the existing parser/dialect/IR/renderer stack consumes unchanged. **Decode-only.**
7
+ *
8
+ * Clean-room from the published spec (RR-003) — no AGPL `libbgcode`/MeatPack code. Depends only on
9
+ * `@chestnutlabs/toolpath-core` and `@chestnutlabs/gcode-containers` (for `crc32`/`ContainerError`);
10
+ * no `three`, framework, filesystem, or network.
11
+ *
12
+ * All spec codecs are decoded (None/DEFLATE/heatshrink × None/MeatPack), validated against real Prusa
13
+ * files (golden-equivalence). Phase 4c registers a **container adapter** so `.bgcode` flows through the
14
+ * parser pipeline via `containers: 'auto'`, surfacing machine geometry + settings + thumbnails.
15
+ */
16
+ export { BGCODE_MAGIC, BGCODE_VERSION, BgcodeBlockType, BgcodeCompression, BgcodeEncoding, sniffBgcode, openBgcode } from './bgcode.js';
17
+ export type { BgcodeBlockInfo, BgcodeDecodeResult, BgcodeDecodeOptions, BgcodeThumbnail } from './bgcode.js';
18
+ export { meatpackDecode } from './meatpack.js';
19
+ export { heatshrinkDecode } from './heatshrink.js';
20
+ export { openBgcodeContainer } from './adapter.js';
21
+ export type { OpenedBgcode, BgcodeStreamLike } from './adapter.js';
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EACL,YAAY,EACZ,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,WAAW,EACX,UAAU,EACX,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,eAAe,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC7G,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @chestnutlabs/gcode-bgcode — Binary G-code (`.bgcode`) decode adapter (DD-011, epic #188).
3
+ *
4
+ * A license-clean, in-memory decoder for Prusa's binary G-code container. `.bgcode` is a container
5
+ * of ordinary G-code, so decoding it (block walk → CRC verify → decompress → decode → concatenate)
6
+ * yields plain G-code the existing parser/dialect/IR/renderer stack consumes unchanged. **Decode-only.**
7
+ *
8
+ * Clean-room from the published spec (RR-003) — no AGPL `libbgcode`/MeatPack code. Depends only on
9
+ * `@chestnutlabs/toolpath-core` and `@chestnutlabs/gcode-containers` (for `crc32`/`ContainerError`);
10
+ * no `three`, framework, filesystem, or network.
11
+ *
12
+ * All spec codecs are decoded (None/DEFLATE/heatshrink × None/MeatPack), validated against real Prusa
13
+ * files (golden-equivalence). Phase 4c registers a **container adapter** so `.bgcode` flows through the
14
+ * parser pipeline via `containers: 'auto'`, surfacing machine geometry + settings + thumbnails.
15
+ */
16
+ export { BGCODE_MAGIC, BGCODE_VERSION, BgcodeBlockType, BgcodeCompression, BgcodeEncoding, sniffBgcode, openBgcode } from './bgcode.js';
17
+ export { meatpackDecode } from './meatpack.js';
18
+ export { heatshrinkDecode } from './heatshrink.js';
19
+ // Container-adapter integration (DD-005 §4.4, #188 phase 4c).
20
+ export { openBgcodeContainer } from './adapter.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Decode a MeatPack byte stream to plain G-code, bounded to `limit` output bytes (decompression-bomb
3
+ * defense). Both bgcode GCode encodings (MeatPack, MeatPack-comments) decode through this identically —
4
+ * comment preservation is an *encoder* choice; the decoder simply reproduces the packed bytes.
5
+ */
6
+ export declare function meatpackDecode(data: Uint8Array, limit: number): Uint8Array;
7
+ //# sourceMappingURL=meatpack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meatpack.d.ts","sourceRoot":"","sources":["../src/meatpack.ts"],"names":[],"mappings":"AAkDA;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,UAAU,CAgG1E"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * MeatPack G-code decoder (DD-011 phase 2, #188).
3
+ *
4
+ * A faithful TypeScript reimplementation of the MeatPack unpacker state machine from
5
+ * **jamesgopsill/meatpack** (MIT, © 2025 James Gopsill — https://github.com/jamesgopsill/meatpack),
6
+ * which is itself derived from the published Prusa spec (`libbgcode/binarize/meatpack.cpp`). Porting
7
+ * from an MIT source with attribution is the sanctioned path (RR-003 §8) — the AGPL `libbgcode` and
8
+ * OctoPrint-MeatPack are never copied.
9
+ *
10
+ * MeatPack packs the 15 most common G-code characters into 4-bit nibbles (two per byte); a nibble of
11
+ * `0b1111` escapes a full literal byte, and a `0xFF 0xFF <cmd>` sequence toggles packing / no-spaces
12
+ * state. The stream begins in the *disabled* state; the encoder emits `FF FF FB` (enable packing) up
13
+ * front (the `MEATPACK_HEADER`). The first character of a packed byte is the LOW nibble.
14
+ */
15
+ import { ContainerError } from '@chestnutlabs/gcode-containers';
16
+ const SIGNAL = 0xff;
17
+ // Command bytes (spec §meatpack; the two 0xFF signal bytes precede one of these).
18
+ const CMD_NO_SPACES_DISABLED = 246;
19
+ const CMD_NO_SPACES_ENABLED = 247;
20
+ const CMD_QUERY_CONFIG = 248;
21
+ const CMD_RESET_ALL = 249;
22
+ const CMD_PACKING_DISABLED = 250;
23
+ const CMD_PACKING_ENABLED = 251;
24
+ /**
25
+ * 4-bit code → byte. Codes 0..14 are the packable characters; `0b1011` is space (or `'E'` when
26
+ * no-spaces is active). Code `0b1111` (15) is the full-width escape marker — returned here as `0`,
27
+ * which callers treat as "a full literal byte follows".
28
+ */
29
+ function reverseLookup(nibble, noSpaces) {
30
+ if (nibble <= 9)
31
+ return 0x30 + nibble; // '0'..'9'
32
+ switch (nibble) {
33
+ case 10:
34
+ return 0x2e; // '.'
35
+ case 11:
36
+ return noSpaces ? 0x45 : 0x20; // 'E' or ' '
37
+ case 12:
38
+ return 0x0a; // '\n'
39
+ case 13:
40
+ return 0x47; // 'G'
41
+ case 14:
42
+ return 0x58; // 'X'
43
+ default:
44
+ return 0; // 0b1111 → NUL: full-width escape marker
45
+ }
46
+ }
47
+ /**
48
+ * Decode a MeatPack byte stream to plain G-code, bounded to `limit` output bytes (decompression-bomb
49
+ * defense). Both bgcode GCode encodings (MeatPack, MeatPack-comments) decode through this identically —
50
+ * comment preservation is an *encoder* choice; the decoder simply reproduces the packed bytes.
51
+ */
52
+ export function meatpackDecode(data, limit) {
53
+ let out = new Uint8Array(Math.max(64, Math.min(limit, data.length * 2)));
54
+ let pos = 0;
55
+ const push = (b) => {
56
+ if (pos >= limit)
57
+ throw new ContainerError('E_BGCODE_BOMB', `MeatPack output exceeds the limit (${pos})`);
58
+ if (pos >= out.length) {
59
+ const bigger = new Uint8Array(Math.min(limit, Math.max(out.length * 2, pos + 1)));
60
+ bigger.set(out.subarray(0, pos));
61
+ out = bigger;
62
+ }
63
+ out[pos++] = b;
64
+ };
65
+ let state = 'disabled';
66
+ let noSpaces = false;
67
+ for (let i = 0; i < data.length; i++) {
68
+ const byte = data[i];
69
+ if (byte === SIGNAL) {
70
+ if (state === 'first')
71
+ state = 'second';
72
+ else if (state === 'disabled' || state === 'enabled')
73
+ state = 'first';
74
+ else
75
+ throw new ContainerError('E_BGCODE_MEATPACK', `invalid MeatPack state at byte ${i}`);
76
+ continue;
77
+ }
78
+ switch (state) {
79
+ case 'disabled':
80
+ push(byte);
81
+ break;
82
+ case 'enabled': {
83
+ const most = reverseLookup(byte >> 4, noSpaces); // high nibble → 2nd char
84
+ const least = reverseLookup(byte & 0x0f, noSpaces); // low nibble → 1st char
85
+ if (most === 10 && least === 10) {
86
+ push(10); // a packed (\n, \n) collapses to a single newline
87
+ }
88
+ else if (most === 0 && least !== 0) {
89
+ // 2nd char is full-width (follows in the stream); 1st char is real.
90
+ push(least);
91
+ state = 'rightFull';
92
+ }
93
+ else if (most !== 0 && least === 0) {
94
+ // 1st char is full-width (follows); place a placeholder, then the real 2nd char.
95
+ push(least);
96
+ push(most);
97
+ state = 'leftFull';
98
+ }
99
+ else if (most === 0 && least === 0) {
100
+ // byte 0xFF — a signal, intercepted above; never reachable here.
101
+ throw new ContainerError('E_BGCODE_MEATPACK', `unexpected 0xFF in packed data at byte ${i}`);
102
+ }
103
+ else {
104
+ push(least); // 1st char (low nibble)
105
+ push(most); // 2nd char (high nibble)
106
+ }
107
+ break;
108
+ }
109
+ case 'second': {
110
+ switch (byte) {
111
+ case CMD_PACKING_ENABLED:
112
+ state = 'enabled';
113
+ break;
114
+ case CMD_PACKING_DISABLED:
115
+ state = 'disabled';
116
+ break;
117
+ case CMD_RESET_ALL:
118
+ state = 'disabled';
119
+ noSpaces = false;
120
+ break;
121
+ case CMD_NO_SPACES_ENABLED:
122
+ noSpaces = true;
123
+ state = 'enabled';
124
+ break;
125
+ case CMD_NO_SPACES_DISABLED:
126
+ noSpaces = false;
127
+ state = 'enabled';
128
+ break;
129
+ case CMD_QUERY_CONFIG:
130
+ // A serial-link query, not expected inside a file — no-op, resume packing.
131
+ state = 'enabled';
132
+ break;
133
+ default:
134
+ throw new ContainerError('E_BGCODE_MEATPACK', `invalid MeatPack command ${byte} at byte ${i}`);
135
+ }
136
+ break;
137
+ }
138
+ case 'first':
139
+ // A lone 0xFF (not a command) → this byte and the next are two full-width literals.
140
+ state = 'rightFull';
141
+ push(byte);
142
+ break;
143
+ case 'rightFull':
144
+ state = 'enabled';
145
+ push(byte);
146
+ break;
147
+ case 'leftFull':
148
+ state = 'enabled';
149
+ out[pos - 2] = byte; // overwrite the placeholder with the full-width byte
150
+ break;
151
+ }
152
+ }
153
+ return out.subarray(0, pos);
154
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@chestnutlabs/gcode-bgcode",
3
+ "version": "0.3.0",
4
+ "description": "Binary G-code (.bgcode) decode adapter for the Chestnut Labs G-code toolpath stack (DD-011): a license-clean, in-memory block walker that decodes Prusa .bgcode to plain G-code for the existing parser/dialect/renderer pipeline. Decode-only.",
5
+ "keywords": [
6
+ "gcode",
7
+ "bgcode",
8
+ "binary-gcode",
9
+ "3d-printing",
10
+ "toolpath",
11
+ "prusa",
12
+ "chestnutlabs",
13
+ "container"
14
+ ],
15
+ "author": "Chestnut Labs",
16
+ "homepage": "https://github.com/ChestnutLabs/gcode-preview#readme",
17
+ "bugs": "https://github.com/ChestnutLabs/gcode-preview/issues",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/ChestnutLabs/gcode-preview.git",
21
+ "directory": "packages/gcode-bgcode"
22
+ },
23
+ "license": "MIT",
24
+ "engines": {
25
+ "node": ">=22"
26
+ },
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/index.js",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "import": "./dist/index.js"
36
+ }
37
+ },
38
+ "files": [
39
+ "dist"
40
+ ],
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc -p tsconfig.json",
46
+ "typecheck": "tsc -p tsconfig.json --noEmit",
47
+ "test": "vitest run"
48
+ },
49
+ "dependencies": {
50
+ "@chestnutlabs/toolpath-core": "0.3.0",
51
+ "@chestnutlabs/gcode-containers": "0.3.0"
52
+ }
53
+ }