https://github.com/python/cpython/commit/1ce279ed32c8a6b7850111a21efc39448926ab91 commit: 1ce279ed32c8a6b7850111a21efc39448926ab91 branch: main author: digit50 <[email protected]> committer: gpshead <[email protected]> date: 2026-11-10T06:45:40Z summary:
gh-159643: Fix EOFError in tarfile stream mode with bz2, xz and zstd (GH-159647) In stream mode (`r|bz2`, `r|xz`, `r|zst`, and `r|*` when it detects one of them), `_Stream._read` called `decompress(b"")` on a decompressor that had already reached the end of its data, because an exhausted `BZ2Decompressor`, `LZMADecompressor` or `ZstdDecompressor` reports `needs_input` as false. That raised a bare `EOFError` whenever tarfile read past the end of the compressed stream, for example with `ignore_zeros=True` or on an archive without end-of-archive blocks. Stop the read loop once the decompressor reports `eof`. This restores the pre-gh-121109 results for the cases above. Data after the end of the compressed stream is now always ignored; before gh-121109 it was ignored only if it arrived in the same read as the end of the stream, and raised `EOFError` otherwise. The gzip branch is unchanged. --------- Co-authored-by: Gregory P. Smith <[email protected]> files: A Misc/NEWS.d/next/Library/2026-10-17-19-33-13.gh-issue-159643.EJfe0IXetV.rst M Lib/tarfile.py M Lib/test/test_tarfile.py diff --git a/Lib/tarfile.py b/Lib/tarfile.py index abf7786812a063..b052ca0dd40729 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -594,6 +594,8 @@ def _read(self, size): else: # Other decompressors have needs_input. # decompress() can buffer data internally. + if self.cmp.eof: + break if self.cmp.needs_input: cbuf = self.fileobj.read(self.bufsize) if not cbuf: diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index 2ad6d0452ce29a..1927ff227d5356 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -1061,6 +1061,44 @@ def test_compare_members(self): finally: tar1.close() + def test_ignore_zeros_concatenated(self): + # gh-159643: reading past the end of the compressed data + # must not raise EOFError. + with self.open(tmpname, "wb") as fobj: + for name in ("a", "b"): + with tarfile.open(fileobj=fobj, mode="w") as tar: + tar.addfile(tarfile.TarInfo(name)) + with tarfile.open(tmpname, self.mode, ignore_zeros=True) as tar: + self.assertEqual(tar.getnames(), ["a", "b"]) + + def test_no_end_of_archive_blocks(self): + # gh-159643 + tarinfo = tarfile.TarInfo("a") + tarinfo.size = 5 + with self.open(tmpname, "wb") as fobj: + fobj.write(tarinfo.tobuf()) + fobj.write(b"hello".ljust(tarfile.BLOCKSIZE, b"\0")) + with tarfile.open(tmpname, self.mode) as tar: + self.assertEqual(tar.getnames(), ["a"]) + + def test_data_after_end_of_stream(self): + # gh-159643: with a small bufsize, the compressed data spans + # several reads of the file and the padding that follows it + # is not read together with the end of the compressed data. + bufsize = tarfile.BLOCKSIZE + data = Random(0).randbytes(8 * bufsize) + tarinfo = tarfile.TarInfo("a") + tarinfo.size = len(data) + with self.open(tmpname, "wb") as fobj: + with tarfile.open(fileobj=fobj, mode="w") as tar: + tar.addfile(tarinfo, io.BytesIO(data)) + with open(tmpname, "ab") as fobj: + fobj.write(bytes(4 * bufsize)) + with tarfile.open(tmpname, self.mode, bufsize=bufsize, + ignore_zeros=True) as tar: + members = [(m.name, tar.extractfile(m).read()) for m in tar] + self.assertEqual(members, [("a", data)]) + class GzipStreamReadTest(GzipTest, GzipReadTestBase, StreamReadTest): pass diff --git a/Misc/NEWS.d/next/Library/2026-10-17-19-33-13.gh-issue-159643.EJfe0IXetV.rst b/Misc/NEWS.d/next/Library/2026-10-17-19-33-13.gh-issue-159643.EJfe0IXetV.rst new file mode 100644 index 00000000000000..a76e54de38cc4e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-17-19-33-13.gh-issue-159643.EJfe0IXetV.rst @@ -0,0 +1,3 @@ +Fix :exc:`EOFError` raised by :mod:`tarfile` in stream mode with bz2, xz or +zstd compression when reading past the end of the compressed data, for +example with *ignore_zeros* or an archive without end-of-archive blocks. _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]
