diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index 708db4d4387df5e..1cf589d4c99a53d 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -719,6 +719,94 @@ class ZstdTestsWithSourceFile(AbstractTestsWithSourceFile, unittest.TestCase): compression = zipfile.ZIP_ZSTANDARD + def _make_archive(self, data, compressed): + class Compressor: + def compress(self, value): + return compressed + + def flush(self): + return b'' + + archive = io.BytesIO() + with mock.patch.object(zipfile, '_get_compressor', + return_value=Compressor()): + with zipfile.ZipFile(archive, 'w', self.compression) as zipfp: + zipfp.writestr('multiframe', data) + return archive + + def test_read_multiframe(self): + first = b'first frame' * 20 + second = b'second frame' * 20 + data = first + second + compressed = (zipfile.zstd.compress(first) + + zipfile.zstd.compress(second)) + archive = self._make_archive(data, compressed) + + with zipfile.ZipFile(archive) as zipfp: + self.assertEqual(zipfp.read('multiframe'), data) + + with zipfile.ZipFile(archive) as zipfp: + with zipfp.open('multiframe') as fp: + self.assertEqual(fp.seek(len(first) + 2), len(first) + 2) + self.assertEqual(fp.read(), second[2:]) + self.assertEqual(fp.seek(len(first) - 2), len(first) - 2) + self.assertEqual(fp.read(4), + data[len(first) - 2:len(first) + 2]) + + # Read one compressed byte at a time, splitting frames across reads. + 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) + + def test_read_multiframe_with_skippable_frame(self): + first = b'first frame' + second = b'second frame' + skippable = struct.pack(' len(data): data += self._read2(n - len(data)) + elif (self._compress_type == ZIP_ZSTANDARD and + getattr(self._decompressor, "eof", False)): + # Continue with the next Zstandard frame. + data = getattr(self._decompressor, "unused_data", b'') + self._decompressor = _get_decompressor(self._compress_type) + if not data: + data = self._read2(n) elif self._compress_type == ZIP_STORED: data = self._read2(n) else: @@ -1220,22 +1227,37 @@ def _read1(self, n): # DEFLATE path above) so that a small compressed member cannot # expand into one unbounded read. try: - data = self._decompressor.decompress(data, max(n, self.MIN_READ_SIZE)) - except TypeError: - # See MonkeypatchedDecompressorTests in test_core.py - warnings._deprecated( - 'one-argument decompress()', - 'The decompress() method of ' - + type(self._decompressor).__name__ - + ' should take two arguments, data and max_length.' - + ' One-argument calls will stop working before' - + ' Python 3.21.', - remove=(3, 21), - stacklevel=4) - data = self._decompressor.decompress(data) - self._eof = (self._decompressor.eof or - self._compress_left <= 0 and - getattr(self._decompressor, "needs_input", True)) + try: + data = self._decompressor.decompress(data, max(n, self.MIN_READ_SIZE)) + except TypeError: + # See MonkeypatchedDecompressorTests in test_core.py + warnings._deprecated( + 'one-argument decompress()', + 'The decompress() method of ' + + type(self._decompressor).__name__ + + ' should take two arguments, data and max_length.' + + ' One-argument calls will stop working before' + + ' Python 3.21.', + remove=(3, 21), + stacklevel=4) + data = self._decompressor.decompress(data) + except Exception as exc: + if (self._compress_type == ZIP_ZSTANDARD and + zstd is not None and isinstance(exc, zstd.ZstdError)): + raise BadZipFile( + f"Bad Zstandard data for file {self.name!r}") from exc + raise + if self._compress_type == ZIP_ZSTANDARD: + if getattr(self._decompressor, "eof", False): + self._eof = (self._compress_left <= 0 and + not getattr(self._decompressor, "unused_data", b'')) + else: + self._eof = (self._compress_left <= 0 and + getattr(self._decompressor, "needs_input", True)) + else: + self._eof = (self._decompressor.eof or + self._compress_left <= 0 and + getattr(self._decompressor, "needs_input", True)) data = data[:self._left] self._left -= len(data) 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 000000000000000..ae821409a55d349 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-23-19-51-05.gh-issue-154481.6cFDFx.rst @@ -0,0 +1,3 @@ +Fix :mod:`zipfile` reading of Zstandard-compressed archive members containing +multiple frames, which could previously return truncated data or raise a +spurious CRC error.