Fix LZXPRESS Huffman decompression - #132
Conversation
The decoder previously preloaded two words and refilled inside skip(), which could consume raw length extension bytes from the bit stream. Refill now only happens at symbol boundaries, the LE32 raw length extension path is handled, an EOD symbol (256) terminates the stream, and matches are truncated at the 64KiB chunk boundary per MS-XCA. The basic/large test fixtures were regenerated with a spec-correct chunked encoder (validated byte-exact against the klauspost/compress Go decoder and the Sleuth Kit xpress decoder) and a new le32 fixture exercises the LE32 raw length path and the chunk boundary.
|
Can you detail what you used to regenerate the test fixtures? |
|
The fixtures were regenerated with a small three-stage pipeline (all scripts are scratch, not part of the PR):
Happy to commit the encoder + generator into the repo (e.g. under |
|
Does this overlap with my PR? #129 "Fixes two bugs in lzxpress_huffman decompress." |
|
Good question - let me give you a concrete answer instead of a hand-wave, since I already have both decoders locally to compare. What overlaps (both PRs touch the same function):
What does NOT overlap - and I think this is the important part. I ran my regenerated fixtures against each decoder:
The le32 fixture fails under #129's decoder (wrong output, no exception). It is the fixture that exercises a match crossing the 64 KiB chunk boundary. MS-XCA 2.2.1.3 requires truncating the copy at the chunk boundary; #129 keeps the unbounded copy loop, so the chunk overshoots and everything after it is misparsed. Adding the one-line truncation (length = min(length, 65536 - chunk_size)) to your decoder makes all three fixtures pass, which pins down the missing fix. My PR additionally refactors refilling so it only happens at symbol boundaries: raw length bytes are read before the offset bits are consumed, instead of peeking the offset first and refilling inside skip(), which can pull words from past the interleaved raw bytes. And the fixtures genuinely needed regenerating - the current main-branch decoder fails all three of mine, so the old digests were only consistent with the old decoder. To avoid duplication, I suggest merging #129 first (it is the larger feature PR), then I rebase #132 on top and keep only the delta: the truncation fix, the refill refactor, and the regenerated fixtures. Alternatively, if you would rather have a single PR, I can close #132 and port the delta onto #129 directly. Happy to go either way - let me know your preference. |
Fixes the XPRESS LZ77+Huffman (MS-XCA 2.2) decompressor and regenerates its test fixtures, which previously did not match the current decoder semantics.
Decoder fixes
skip(), which could consume raw length extension bytes that are interleaved with the bit stream._read_32_bit).[MS-XCA]2.2.1.3).Fixture regeneration
basicandlargewere re-encoded with a spec-correct chunked encoder; the old fixtures did not decode to their stated digests under the fixed decoder.le32fixture exercises the LE32 raw-length extension path and the 64KiB chunk boundary.AppendHDecompressed, per chunk)tsk/fs/xpress.cdecoder (TSK vector suite, 41/42 — the single mismatch is a Go single-chunk stream that is incompatible with chunked decoding by design)Verification
pytest: 159 passed, 58 skipped, 0 failedruff checkandruff format --check: clean