Skip to content

Add libdeflate_deflate_decompress_stream() for streaming decompression - #2

Merged
alexey-milovidov merged 1 commit into
v1.25-basefrom
add-deflate-decompress-stream
Jun 21, 2026
Merged

Add libdeflate_deflate_decompress_stream() for streaming decompression#2
alexey-milovidov merged 1 commit into
v1.25-basefrom
add-deflate-decompress-stream

Conversation

@alexey-milovidov

Copy link
Copy Markdown
Member

What

Adds libdeflate_deflate_decompress_stream() (+ _reset()): a resumable raw-DEFLATE decoder that suspends at block boundaries, so arbitrarily large zlib/gzip/deflate streams can be decompressed with bounded memory using libdeflate's fast table-driven decoder.

Why

libdeflate's decompressor decodes until BFINAL and cannot resume on a partial stream, so it cannot back a streaming decompressor (e.g. reading large .gz files or HTTP bodies) without buffering the entire input. This lets ClickHouse route streaming gzip/zlib decompression through libdeflate (~1.4–1.5× faster than zlib-ng in our benchmarks) instead of keeping a separate streaming codec.

How

  • The only cross-block state is {bitbuf, bitsleft} + the 32 KiB window. At each block boundary we checkpoint the byte-aligned bit state. If the input runs out mid-block (a REFILL_BITS overread while end_of_input is false) or the output fills, we roll back to the checkpoint and return LIBDEFLATE_STREAM_NEED_INPUT / LIBDEFLATE_STREAM_NEED_OUTPUT.
  • Window/back-references: the caller places up to 32 KiB of previously produced output immediately before out and passes window_nbytes; the match-offset validation base is shifted by window_nbytes. (DEFLATE offsets are ≤ 32 KiB, so this resolves every back-reference with no per-reference special-casing.)
  • Safety: the streaming code is a second instantiation of decompress_template.h guarded by DEFLATE_STREAMING. The existing one-shot decoder is byte-for-byte unchanged; the only shared-macro change is a no-op OVERREAD_HANDLER() hook in REFILL_BITS.

Limitation

Suspension is at block boundaries only. A single block larger than the output buffer is handled by the caller growing the buffer and retrying (the decoder re-decodes from the block start). Mid-block suspension is intentionally not implemented (it would roughly reimplement zlib's inflate state machine in libdeflate's hot loop).

Testing

50,000 randomized round-trip cases against zlib: random data styles (incompressible, low-entropy, text, RLE-runs), sizes up to 3 MB, all compression levels, and random input-chunk and output-region sizes that exercise both suspension paths, the 32 KiB window carry-over, and oversized-block grow-and-retry. All passed.

Based on v1.25-base (= v1.25 + the merged streaming-compress PR), so the diff is exactly this addition.

🤖 Generated with Claude Code

libdeflate's decompressor is one-shot: it decodes until BFINAL and cannot
resume on a partial stream, so it can't back a streaming decompressor for
arbitrarily large zlib/gzip/deflate input without buffering the whole thing.

Add a resumable variant that suspends at DEFLATE block boundaries. The only
cross-block state is {bitbuf, bitsleft} plus the 32 KiB history window, so
suspension is cheap: at each block boundary we checkpoint the byte-aligned
bit state, and if the input runs out (REFILL would overread and
end_of_input is false) or the output fills mid-block, we roll back to that
checkpoint and return STREAM_NEED_INPUT / STREAM_NEED_OUTPUT. The caller
supplies more input or drains output and calls again. Back-references are
resolved by having the caller place up to 32 KiB of previously produced
output immediately before 'out' and pass 'window_nbytes'; the match-offset
validation base is shifted accordingly.

The streaming code is a second instantiation of decompress_template.h guarded
by DEFLATE_STREAMING, so the existing one-shot decoder is byte-for-byte
unchanged (the only shared-macro change is a no-op OVERREAD_HANDLER() hook in
REFILL_BITS). A single block larger than the output buffer is handled by the
caller growing the buffer and retrying; mid-block suspension is intentionally
not supported.

Verified with 50000 randomized round-trip cases against zlib (random data
styles, sizes up to 3 MB, all levels, random input-chunk and output-region
sizes exercising both suspension paths and the window carry-over).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant