Skip to content

fix(xz): decode compressed LZMA2 chunks as their bytes arrive - #150

Merged
MagicalTux merged 1 commit into
masterfrom
fix/xz-partial-chunk
Oct 22, 2026
Merged

MagicalTux merged 1 commit into
masterfrom
fix/xz-partial-chunk

Conversation

@MagicalTux

@MagicalTux MagicalTux commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Rebased onto master after #149 merged; this is now the single partial-chunk commit.

Problem

A compressed LZMA2 chunk (up to 64 KiB of input, 2 MiB of output) was only decoded once all of its bytes were buffered.

  • Truncated streams lost the whole chunk the cut fell in. A 4 MB file cut 40 bytes short gave 2,096,914 bytes; xz -dc gives 3,968,329.
  • Streaming callers got no output from a chunk until its last byte arrived.

Fix

Packets are now decoded as the compressed bytes arrive, the way liblzma does it.

  • While the chunk is incomplete, a packet is decoded only when at least 21 unread bytes are buffered. That is liblzma's LZMA_IN_REQUIRED, the most input one packet can read: at most 22 probability-coded bits (each shrinks the range by at most about 6.05 bits) plus 26 direct bits, so 159 bits, read one byte per 8 bits of shrink, plus one. A packet therefore never runs out of input halfway and leaves the coder state half-updated.
  • Once the chunk is complete, it decodes to the end.
  • At finish on a stream cut inside a chunk, one last pass decodes every complete packet the bytes hold. A packet writes nothing until it has been read in full, so every byte before the cut is delivered, and then UnexpectedEnd is returned.

LzmaCore::decode_partial is new; decode_chunk now wraps it.

Verification

  • Against xz -dc: truncated output matches byte for byte at 4 cut points in each of 4 streams: 2 MiB chunks of zeros, a binary, an x86 BCJ stream, and a SHA-256 multi-block stream.
  • New tests, no xz tool needed: half of a compressed chunk already yields output when fed in 1-, 100- and whole-piece slices, and cuts inside a chunk recover more than the complete chunks hold, with any buffer size. When xz is installed, the second test also compares against xz -d. Both tests fail on master.
  • Speed: full-stream decode is unchanged against master (0.037s vs 0.037s on a 4 MB binary).
  • cargo test --features all: 1925 passed. clippy (-D warnings, all and --all-features) and fmt are clean.

Not changed

The raw lzma2 decoder (src/lzma2) still decodes whole chunks. It can use the same decode_partial, as a follow-up.

🤖 Generated with Claude Code

Trigger: an xz stream cut short inside a compressed LZMA2 chunk, or any
stream fed to `decode` in pieces smaller than a chunk.
Effect: a compressed chunk (up to 64 KiB of input, 2 MiB of output) was
only decoded once all of its bytes were buffered. A truncated stream
lost the whole chunk it was cut in: a 4 MB file cut 40 bytes short gave
2,096,914 bytes where `xz -dc` gives 3,968,329. Streaming callers also
got no output from a chunk until its last byte arrived.

Fix: decode packets as the compressed bytes arrive, like liblzma. While
the chunk is incomplete, a packet is decoded only if at least 21 unread
bytes are buffered (liblzma's LZMA_IN_REQUIRED, the most input one
packet can read), so it never runs out of input halfway and leaves the
coder state half-updated. Once the chunk is complete it decodes to the
end. At `finish` on a stream cut inside a chunk, one last pass decodes
every packet the bytes hold. A packet writes nothing until it has been
read in full, so all output before the cut is delivered, then
UnexpectedEnd.
New `LzmaCore::decode_partial`; `decode_chunk` wraps it.

Verified: truncated output equals `xz -dc`'s, byte for byte, at 4 cut
points in each of 4 streams (2 MiB chunks of zeros, a binary, x86 BCJ,
SHA-256 multi-block). Full-stream decode speed is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MagicalTux
MagicalTux force-pushed the fix/xz-partial-chunk branch from e775052 to 17b319e Compare October 22, 2026 18:38
@MagicalTux
MagicalTux merged commit cb536ec into master Oct 22, 2026
50 checks passed
@MagicalTux
MagicalTux deleted the fix/xz-partial-chunk branch October 22, 2026 18:39
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