From f4de693b7bb9e1603a0bb779b4f003ec1b1c49ca Mon Sep 17 00:00:00 2001 From: lipengyu Date: Thu, 23 Jul 2026 20:02:22 +0800 Subject: [PATCH] gh-154481: Fix reading multi-frame Zstandard members in zipfile --- Lib/test/test_zipfile/test_core.py | 30 +++++++++++++++++++ Lib/zipfile/__init__.py | 11 +++++++ ...-07-23-19-51-05.gh-issue-154481.6cFDFx.rst | 2 ++ 3 files changed, 43 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-07-23-19-51-05.gh-issue-154481.6cFDFx.rst diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index cd498ba13e6f46..ed2204010a9423 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -715,6 +715,36 @@ class ZstdTestsWithSourceFile(AbstractTestsWithSourceFile, unittest.TestCase): compression = zipfile.ZIP_ZSTANDARD + def test_read_multiframe(self): + first = b'first frame' * 20 + second = b'second frame' * 20 + data = first + second + + class MultiframeCompressor: + def compress(self, value): + value = bytes(value) + return (zipfile.zstd.compress(value[:len(first)]) + + zipfile.zstd.compress(value[len(first):])) + + def flush(self): + return b'' + + archive = io.BytesIO() + with mock.patch.object(zipfile, '_get_compressor', + return_value=MultiframeCompressor()): + with zipfile.ZipFile(archive, 'w', self.compression) as zipfp: + zipfp.writestr('multiframe', data) + + with zipfile.ZipFile(archive) as zipfp: + self.assertEqual(zipfp.read('multiframe'), data) + + # Exercise a frame boundary between two reads from the archive. + with zipfile.ZipFile(archive) as zipfp: + with zipfp.open('multiframe') as fp: + fp.MIN_READ_SIZE = 1 + self.assertEqual(b''.join(iter(lambda: fp.read(1), b'')), + data) + class AbstractTestZip64InSmallFiles: # These tests test the ZIP64 functionality without using large files, # see test_zipfile64 for proper tests. diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 418933a2e8d9e8..3bbdc0afdf00d0 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -1186,6 +1186,13 @@ def _read1(self, n): data = self._decompressor.unconsumed_tail if n > len(data): data += self._read2(n - len(data)) + elif (self._compress_type == ZIP_ZSTANDARD and + self._decompressor.eof): + # Continue with the next Zstandard frame. + data = self._decompressor.unused_data + self._decompressor = _get_decompressor(self._compress_type) + if not data: + data = self._read2(n) else: data = self._read2(n) @@ -1199,6 +1206,10 @@ def _read1(self, n): not self._decompressor.unconsumed_tail) if self._eof: data += self._decompressor.flush() + elif self._compress_type == ZIP_ZSTANDARD: + data = self._decompressor.decompress(data) + self._eof = (self._compress_left <= 0 and + not self._decompressor.unused_data) else: data = self._decompressor.decompress(data) self._eof = self._decompressor.eof or self._compress_left <= 0 diff --git a/Misc/NEWS.d/next/Library/2026-07-23-19-51-05.gh-issue-154481.6cFDFx.rst b/Misc/NEWS.d/next/Library/2026-07-23-19-51-05.gh-issue-154481.6cFDFx.rst new file mode 100644 index 00000000000000..1dff74bc2597fa --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-23-19-51-05.gh-issue-154481.6cFDFx.rst @@ -0,0 +1,2 @@ +Fix :mod:`zipfile` to correctly read Zstandard-compressed archive members +that could previously be truncated or incorrectly rejected.