Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/bgcode-deflate-zlib-flavor.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion packages/gcode-bgcode/src/__tests__/assemble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export interface AssembleOptions {
}

async function deflateRaw(data: Uint8Array): Promise<Uint8Array> {
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 () => {
Expand Down
42 changes: 42 additions & 0 deletions packages/gcode-bgcode/src/__tests__/bgcode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uint8Array> {
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);
Expand Down Expand Up @@ -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) }]);
Expand Down
7 changes: 4 additions & 3 deletions packages/gcode-bgcode/src/bgcode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uint8Array> {
// 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
Expand Down
Loading