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