feat(support): deflate-compress support bundles (#3218)#3691
Conversation
Support bundles are log-dominated and compress ~5-10x, but #3206's in-process ZIP writer only did stored (uncompressed) entries — a user with 50 MB of logs got a 50 MB bundle, blowing past Discord/email/GitHub attachment limits. Add a raw-DEFLATE writer using the same bundled zlib that already powers the inflate-on-read path (negative window bits, no zlib header) — no new dependency, no subprocess, no new MSIX content, so all of #3206's Store-compliance wins are preserved. writeStoredZip/writeDeflatedZip now share one writeZipImpl (method + independent compressed/uncompressed sizes; CRC always over the uncompressed data). SupportBundle switches to writeDeflatedZip; ProfileTransfer stays stored. Test: zip_archive_test gains a deflated round-trip that asserts method-8 framing, smaller-than-raw output, and a clean round-trip through the existing reader (the implicit writer/reader cross-check). Closes #3218. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Nice work, @ten9876 — this is a tidy, well-scoped change and the approach is exactly right. Reusing the bundled zlib with negative window bits (-MAX_WBITS) keeps the writer's raw-DEFLATE framing in lockstep with the existing inflateInit2(-MAX_WBITS) read path, so no new dependency and no MSIX impact. The shared writeZipImpl(entries, compress) cleanly factors the stored/deflated split.
Things I checked and confirmed correct:
- CRC is over uncompressed data, while local + central headers carry compressed (
payload) and uncompressed (raw) sizes independently — matches the ZIP spec and whatreadZipEntriesexpects (it inflatescompressedSizebytes, then CRCs the inflated result). - Bounds:
raw,payload,name, andoutsizes are all checked against theirquint32/quint16limits before casting.rawDeflatechecksdeflateInit2/Z_STREAM_ENDreturn codes anddeflateEnds on every path. - Method byte = 8 in both local and central headers; version-needed stays 20 (2.0), correct for deflate.
- Empty-file edge case is covered by the test (
empty.txtround-trips), which is the one spot raw-deflate framing can be subtle — good that the test asserts it through the real reader rather than just the writer. - The writer/reader cross-check test (round-trip through
readZipEntries) is the right way to test this, and all 6 CI checks are green.
One optional thought (not blocking): writeDeflatedZip always emits method 8 per entry, so a tiny or already-incompressible file can come out slightly larger than stored (deflate overhead). For log-dominated support bundles this is irrelevant, and the output is always a valid ZIP regardless — just noting that a per-entry min(stored, deflated) choice would be the fully general form if this writer ever gets reused for less compressible payloads. Fine to leave as-is for this use case.
LGTM. Thanks for preserving all of #3206's Store-compliance properties.
🤖 aethersdr-agent · cost: $2.4515 · model: claude-opus-4-8
Summary
Closes #3218. Support bundles are log-dominated and compress ~5–10×, but the in-process ZIP writer from #3206 only emitted stored (uncompressed) entries — a user with 50 MB of logs produced a 50 MB bundle, hitting Discord/email/GitHub attachment limits much sooner.
Adds a raw-DEFLATE writer using the same bundled zlib that already powers the inflate-on-read path (negative window bits → no zlib header, which ZIP doesn't use). No new dependency, no subprocess, no new MSIX content — all of #3206's Store-compliance wins are preserved.
Changes
ZipArchive:writeStoredZipand the newwriteDeflatedZipnow share onewriteZipImpl(entries, compress)— method field + independent compressed/uncompressed sizes; CRC is always over the uncompressed data.SupportBundle::writeSupportBundleZip→writeDeflatedZip.ProfileTransferstays stored (tiny payloads).zip_archive_test: new deflated round-trip asserting method-8 framing, smaller-than-raw output, and a clean round-trip through the existing reader (writer/reader cross-check, sincereadZipEntriesalready handles method 8).Why it's not RFC-gated
Internal storage-format change with no UX/behavior change (bundles are still ZIPs, just smaller); the file is never inside the MSIX so WACK's archive rules don't apply.
Test plan
zip_archive_testpasses (stored + deflated round-trips, method byte, size check)🤖 Generated with Claude Code