https://github.com/python/cpython/commit/be7dbf98e7d5227f14bc73d8b3f422f60c431d60
commit: be7dbf98e7d5227f14bc73d8b3f422f60c431d60
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: gpshead <[email protected]>
date: 2026-11-09T23:52:17-08:00
summary:

[3.15] gh-159643: Fix EOFError in tarfile stream mode with bz2, xz and zstd 
(GH-159647) (#161733)

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.

---------
(cherry picked from commit 1ce279ed32c8a6b7850111a21efc39448926ab91)

Co-authored-by: digit50 <[email protected]>
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 66a918ca47e099..3ad82113a06418 100644
--- a/Lib/tarfile.py
+++ b/Lib/tarfile.py
@@ -593,6 +593,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 e16da56af42f12..d892a424d555c4 100644
--- a/Lib/test/test_tarfile.py
+++ b/Lib/test/test_tarfile.py
@@ -1060,6 +1060,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]

Reply via email to