Conversation
decompress_alloc's streaming branch (unknown content size, or a reported size above 16MB) was the only user of the private ZstdReaderArrayList, a Box-allocated reader holding the ZSTD_DStream as a raw pointer and reusing State::End to also mean that the stream had already been freed. The branch now uses the existing StreamingDecoder, which owns the stream as a NonNull and frees it exactly once in Drop; the whole reader (six unsafe blocks) is deleted. StreamingDecoder::decompress performs the same per-iteration work as the deleted read_all: the same 4096-byte reserve, the same ZSTD_decompressStream call, the same re-init on frame completion and the same error mapping, so the only runtime difference is one Box allocation less per call.
There was a problem hiding this comment.
LGTM — deletes the crate-private ZstdReaderArrayList and rewires its sole call site in decompress_alloc to the existing StreamingDecoder.
What was reviewed:
- Line-by-line comparison of
read_allvsStreamingDecoder::decompress: same 4096-byte reserve, samemax_output_sizeclamp, samerc == 0frame-boundary re-init, sameZstdDecompressionError/ShortReadmapping — behavior-identical for the singleis_done=truecall. - Confirmed
ZstdReaderArrayListhad no other references;decompress_alloc's signature and callers (BunObject.rs,add_completions.rs,node_fallbacks.rs) are unaffected. StreamingDecoderfrees theZSTD_DStreamonce inDrop(NonNull), so the deletedend()'s early free is now handled by decoder drop at end of scope — no leak on the error path.
Extended reasoning...
Overview
This PR deletes the crate-private ZstdReaderArrayList<'a> struct (~170 lines, 6 unsafe blocks) from src/zstd/lib.rs and switches its only caller — the unknown-size / >16MB branch of decompress_alloc — to the pre-existing public StreamingDecoder. The StreamingDecoder doc comment is trimmed to no longer reference the deleted type. No public API changes: decompress_alloc, StreamingDecoder, and the State re-export keep their signatures.
Security risks
None introduced. The decompression-bomb guard (max_output_size clamping the per-call output window) is present identically in StreamingDecoder::decompress and defaults to usize::MAX in both types, so the decompress_alloc path is unchanged. The deleted type held a raw *mut ZSTD_DStream and used a State::End flag to prevent double-free; StreamingDecoder uses NonNull with a single free in Drop, which is strictly safer.
Level of scrutiny
Low-to-medium. This is a pure consolidation: deleting a private duplicate implementation in favour of an existing one already exercised by bun_http's Decompressor. The two loops are structurally identical — same empty-input handling, same reserve step, same ZSTD_decompressStream invocation with clamped output, same ZSTD_initDStream on rc == 0 for concatenated frames, same error/state transitions. The only per-call differences (total_in on self vs local; end() freeing eagerly vs Drop freeing at scope exit) don't matter for decompress_alloc's single-shot is_done=true usage where the decoder drops immediately after.
Other factors
Grep confirms zero remaining references to ZstdReaderArrayList in the tree. Callers of decompress_alloc (BunObject.rs sync/async paths, add_completions.rs, node_fallbacks.rs) are untouched. The PR reports 87 passing tests across zstd.test.ts and both 23314 large-input regression tests, which exercise exactly this streaming branch. This aligns with the repo's "one implementation, in the right place" and "delete dead code in the same PR that makes it dead" review guidance.
WalkthroughThe oversized or unknown-size zstd decompression path now uses ChangesZstd streaming decompression
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/zstd/lib.rs`:
- Around line 312-315: Call output.shrink_to_fit() after
StreamingDecoder::decompress completes and before returning output from this
decompression path, ensuring the buffer passed through output.leak() contains no
spare capacity.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 18a4b1af-4eac-48f9-ad37-1f0fb85edabc
📒 Files selected for processing (1)
src/zstd/lib.rs
|
Updated 11:15 AM PT - Aug 11th, 2026
✅ @robobun, your commit 4539471105e982f7184a65c133d3f0049bb41bea passed in 🧪 To try this PR locally: bunx bun-pr 37555That installs a local version of the PR into your bun-37555 --bun |
|
Heads-up: #39417 edits |
What
src/zstd/lib.rshad two streaming decompression loops. The privateZstdReaderArrayList<'a>borrowed the input slice and the outputVec, was handed out as aBox, stored the C handle as a bare*mut ZSTD_DStream, and reusedState::Endto also mean "the stream has already been freed" (end()freed it and setEnd;Dropcalledend()again, relying on that flag). The publicStreamingDecoder(already used bybun_http'sDecompressor) is the same loop with the stream held as aNonNulland freed once inDrop, and input and output passed per call.decompress_alloc, the only user of the reader (the unknown-content-size and above-16MB branch), now usesStreamingDecoder:ZstdReaderArrayList(struct,init,init_with_list_allocator,end,read_all,Drop) is deleted, removing 6unsafeblocks and about 170 lines; theStreamingDecoderdoc comment no longer refers to it. 1 call site changes and nothing outside the crate is affected:decompress_alloc,StreamingDecoderand theStatere-export keep their signatures.Why
The crate now has a single owner type for a
ZSTD_DStream:StreamingDecodercannot exist without a live stream and frees it exactly once inDrop, so the "freed" state thatZstdReaderArrayListencoded in itsStatefield is unrepresentable, and there is one streaming loop to read instead of two.StreamingDecoder::decompressdoes the same work per iteration as the deletedread_all(the same 4096-bytereserve, the sameZSTD_decompressStreamcall with the same clamped output size, the sameZSTD_initDStreamon frame completion for concatenated frames, the sameZstdDecompressionErrorandShortReadmapping), so the outputVec, its capacity and the error returned are identical; the only runtime difference is that the reader'sBoxallocation is gone.Part of a series of small type-system hardening changes; each PR stands alone.
Verification
cargo checkandcargo clippyare clean for the touched crates. Debug build succeeds.bun bd test test/js/bun/util/zstd.test.ts test/regression/issue/23314/zstd-large-decompression.test.ts test/regression/issue/23314/zstd-large-input.test.ts: 87 pass, 0 fail across 3 files.