From c8109139fe8e41183cd159a5d823b8555cd99d68 Mon Sep 17 00:00:00 2001 From: Nathaniel Chestnut Date: Mon, 27 Jul 2026 14:16:36 -0700 Subject: [PATCH] =?UTF-8?q?fix(bgcode):=20DEFLATE=20is=20zlib-wrapped,=20n?= =?UTF-8?q?ot=20raw=20=E2=80=94=20real-Prusa-file=20confirmed=20(#188)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase-1 assumed raw DEFLATE ('deflate-raw'). Validating the decoder against a real Prusa XL ColorMix .bgcode (21 MB, 856 blocks) showed its DEFLATE metadata blocks (Slicer/Print) decode ONLY as zlib-wrapped — raw fails ("invalid stored block lengths"). It went unnoticed because GCode blocks use heatshrink, not DEFLATE, so the DEFLATE path was never exercised until real metadata. Switch the decoder + the test assembler to DecompressionStream('deflate') (zlib), and add a flavor-lock test asserting a raw-DEFLATE block is rejected (E_BGCODE_INFLATE) so the flavor can't silently regress. Real-file validation (local): all 856 CRC32 verified; 848 heatshrink-12 + MeatPack-comments GCode blocks → 51.4 MB of valid Prusa G-code (1,892,006 G1 moves, comments + ;WIPE_START preserved, MeatPack no-spaces mode exercised) in ~3 s — so the phase-2/3 decoders are correct on real data. Co-Authored-By: Claude Opus 4.8 --- .changeset/bgcode-deflate-zlib-flavor.md | 9 ++++ .../gcode-bgcode/src/__tests__/assemble.ts | 3 +- .../gcode-bgcode/src/__tests__/bgcode.test.ts | 42 +++++++++++++++++++ packages/gcode-bgcode/src/bgcode.ts | 7 ++-- 4 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 .changeset/bgcode-deflate-zlib-flavor.md diff --git a/.changeset/bgcode-deflate-zlib-flavor.md b/.changeset/bgcode-deflate-zlib-flavor.md new file mode 100644 index 00000000..d3ff6331 --- /dev/null +++ b/.changeset/bgcode-deflate-zlib-flavor.md @@ -0,0 +1,9 @@ +--- +'@chestnutlabs/gcode-bgcode': patch +--- + +Fix the `.bgcode` DEFLATE flavor: it is **zlib-wrapped**, not raw (DD-011 phase 4 confirmation, #188). +Verified against a real Prusa XL `.bgcode` file — its DEFLATE-compressed Slicer/Print **metadata** +blocks decode only with the zlib header and fail as raw. (GCode blocks use heatshrink, so this was +invisible until a real file's metadata was exercised.) The decoder now uses `DecompressionStream('deflate')`, +and a flavor-lock test asserts a raw-DEFLATE block is rejected so this can't regress. diff --git a/packages/gcode-bgcode/src/__tests__/assemble.ts b/packages/gcode-bgcode/src/__tests__/assemble.ts index 6207045d..436344f1 100644 --- a/packages/gcode-bgcode/src/__tests__/assemble.ts +++ b/packages/gcode-bgcode/src/__tests__/assemble.ts @@ -31,7 +31,8 @@ export interface AssembleOptions { } async function deflateRaw(data: Uint8Array): Promise { - const cs = new CompressionStream('deflate-raw'); + // zlib-wrapped DEFLATE, matching real Prusa `.bgcode` metadata blocks (confirmed phase 4). + const cs = new CompressionStream('deflate'); const writer = cs.writable.getWriter(); const reader = cs.readable.getReader(); const done = (async () => { diff --git a/packages/gcode-bgcode/src/__tests__/bgcode.test.ts b/packages/gcode-bgcode/src/__tests__/bgcode.test.ts index 675306ff..c4f8b82c 100644 --- a/packages/gcode-bgcode/src/__tests__/bgcode.test.ts +++ b/packages/gcode-bgcode/src/__tests__/bgcode.test.ts @@ -12,6 +12,32 @@ const enc = (s: string): Uint8Array => new TextEncoder().encode(s); const dec = (b: Uint8Array): string => new TextDecoder().decode(b); const GCODE = 'M83\nG1 X0 Y0 Z0.2 F1200\nG1 X10 Y0 E1\nG1 X10 Y10 E1\n'; +/** Compress via RAW DEFLATE (no zlib header) — the flavor Prusa does NOT use, for the flavor-lock test. */ +async function rawDeflate(data: Uint8Array): Promise { + const cs = new CompressionStream('deflate-raw'); + const w = cs.writable.getWriter(); + const r = cs.readable.getReader(); + const done = (async () => { + await w.write(data); + await w.close(); + })(); + const chunks: Uint8Array[] = []; + for (;;) { + const { done: d, value } = await r.read(); + if (d) break; + if (value) chunks.push(value); + } + await done; + const total = chunks.reduce((n, c) => n + c.byteLength, 0); + const out = new Uint8Array(total); + let o = 0; + for (const c of chunks) { + out.set(c, o); + o += c.byteLength; + } + return out; +} + describe('sniffBgcode', () => { it('matches the "GCDE" magic and the .bgcode extension', () => { expect(sniffBgcode(enc('GCDE....'))).toBe(true); @@ -114,6 +140,22 @@ describe('openBgcode — honest rejection & bounds', () => { await expect(openBgcode(buf.subarray(0, 14))).rejects.toMatchObject({ code: 'E_BGCODE_TRUNCATED' }); }); + it('DEFLATE is zlib-wrapped, not raw (the flavor real Prusa .bgcode uses)', async () => { + // A raw-DEFLATE stream (no zlib header) must FAIL to decode — proving the decoder expects the + // zlib-wrapped flavor confirmed against a real Prusa XL file (phase 4), not raw deflate. + const rawStored = await rawDeflate(enc(GCODE)); + const buf = await assembleBgcode([ + { + type: 1, + compression: BgcodeCompression.Deflate, + encoding: 0, + data: new Uint8Array(), + preCompressed: { stored: rawStored, uncompressedSize: GCODE.length } + } + ]); + await expect(openBgcode(buf)).rejects.toMatchObject({ code: expect.stringMatching(/E_BGCODE_(INFLATE|SIZE)/) }); + }); + it('honestly rejects an unknown compression id', async () => { // compression id 7 is outside the spec enum (0..3) — bounded structured error, not a guess. const bad = await assembleBgcode([{ type: 1, compression: 7, encoding: 0, data: enc(GCODE) }]); diff --git a/packages/gcode-bgcode/src/bgcode.ts b/packages/gcode-bgcode/src/bgcode.ts index 2c2fa757..33efbf68 100644 --- a/packages/gcode-bgcode/src/bgcode.ts +++ b/packages/gcode-bgcode/src/bgcode.ts @@ -93,9 +93,10 @@ export function sniffBgcode(prefix: Uint8Array, name?: string): boolean { /** DEFLATE-decode `data` to at most `limit` bytes via the platform stream (bounded — bomb-safe). */ async function inflate(data: Uint8Array, expected: number, limit: number): Promise { - // Flavor: raw DEFLATE (matches our ZIP reader / miniz-style raw streams). The definitive Prusa - // flavor is confirmed against real files in phase 4; phase-1 fixtures are self-consistent. - const ds = new DecompressionStream('deflate-raw'); + // Flavor: zlib-wrapped DEFLATE. Confirmed against a real Prusa XL `.bgcode` (phase 4) — its + // DEFLATE metadata blocks decode with the zlib header, and fail as raw. (bgcode GCode blocks use + // heatshrink, not DEFLATE; only Slicer/Print metadata blocks are DEFLATE.) + const ds = new DecompressionStream('deflate'); const writer = ds.writable.getWriter(); const reader = ds.readable.getReader(); // The writer promise must never float unobserved: a corrupt stream errors BOTH ends, and an