From df57e42457f1cbda6896e7b2c57e5e22480be142 Mon Sep 17 00:00:00 2001 From: HipHoff Date: Wed, 28 Oct 2026 12:15:32 -0700 Subject: [PATCH] gh-160842: Read every frame of a multi-frame Zstandard zipfile member ZipExtFile._read1() treated the end of the first zstd frame as the end of the member. When the decompressor reaches the end of a frame and compressed input remains, continue with a new ZstdDecompressor. Bytes that are not a valid frame raise ZstdError, and a truncated last frame still fails the CRC check. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016uX6RoHqnajMDyVi3LrJjs --- Lib/test/test_zipfile/test_core.py | 64 +++++++++++++++++++ Lib/zipfile/__init__.py | 22 ++++++- ...-10-28-17-05-12.gh-issue-160842.Zs7mFr.rst | 3 + 3 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-28-17-05-12.gh-issue-160842.Zs7mFr.rst diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index ab1995e91286e68..5c9a6fc980d3c95 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -4935,6 +4935,70 @@ class ZstdBoundedDecompressTests(AbstractBoundedDecompressTests, compression = zipfile.ZIP_ZSTANDARD +@requires_zstd() +class ZstdMultipleFramesTests(unittest.TestCase): + # A zstd stream may consist of several concatenated frames (RFC 8878, + # section 3.1), as written by multi-threaded or streaming compressors. + part1 = b'first frame ' * 1000 + part2 = b'second frame ' * 1000 + + def make_zip(self, member_data, content): + # Store member_data, then relabel the member as Zstandard-compressed + # with the CRC and size of the decompressed content. + buf = io.BytesIO() + with zipfile.ZipFile(buf, 'w') as zf: + zf.writestr(zipfile.ZipInfo('a'), member_data) + data = bytearray(buf.getvalue()) + crc = zipfile.crc32(content) + cd = data.rindex(zipfile.stringCentralDir) + # Offsets of the method, CRC-32 and uncompressed size fields in the + # local file header and in the central directory record. + for method, crc_pos, size_pos in ((8, 14, 22), + (cd + 10, cd + 16, cd + 24)): + struct.pack_into('