st_decompress() decodes only the first frame of a concatenated lz5/lizard stream
st_decompress() — the path taken for a raw (non-container) .lz5 / .liz stream — drives its outer loop from the frame's own "bytes still wanted" value:
// C/zstdmt/lz5-mt_decompress.c:432
for (; nextToLoad; pos = 0) {so it exits as soon as one frame ends, even when the reader still has bytes to give. The function then falls through to return 0: 7z t says the archive is fine and 7z x writes a silently truncated file.
lz4-mt_decompress.c had the identical shape until 71a0a67a ("Fix extracting multiple LZ4 frames", 2020-12-13), which rewrote the loop to run until the reader reports EOF. lz5 and lizard were never updated — not here, and not in mcmilk/zstdmt either, where the same fix landed the same day as 939dcd0c, also lz4-only. The three st_decompress() bodies were identical apart from type names when they were introduced (58069903 for lz4/lz5, 294d7a00 for lizard), so this looks like the port simply never happened.
Measured against a test that decodes three differently-sized frames and their concatenations:
| lz4 (master) | lz5 (master) | lizard (master) | |
|---|---|---|---|
| frame A alone | ok | ok | ok |
| A+B | ok | 3145728 of 8388608 | 3145728 of 8388608 |
| A+B+C, mixed blockSizeID | ok | 3145728 of 9437184 | 3145728 of 9437184 |
| B+A (reversed) | ok | 5242880 of 8388608 | 5242880 of 8388608 |
| A+empty+C | ok | 3145728 of 4194304 | 3145728 of 4194304 |
Every one of those returns 0 — no error is reported.
How reachable is it
This project's own encoder always wraps its output in the MAGIC_SKIPPABLE container (lz5-mt_compress.c has no threads != 1 branch, unlike lz4), so a .lz5 / .liz produced here never takes this path. Reaching it needs a raw stream from somewhere else — a foreign LZ5F/LizardF encoder, or a payload lifted out of a container. LZ5MT_decompressDCtx() dispatches on the file's magic (lz5-mt_decompress.c:515-522), so such a file is accepted and decoded; it just stops early.
PR follows.
Source: mcmilk/7-Zip-zstd