From 998bccac244d12c98a443af56332d263a7ba90b0 Mon Sep 17 00:00:00 2001 From: lipengyu Date: Sat, 29 Aug 2026 09:33:41 +0800 Subject: [PATCH] gh-156539: Validate duplicate ZIP members individually in testzip Pass each ZipInfo directly to open() so testzip() does not resolve duplicate filenames to the last matching member. Co-authored-by: lipengyu --- Lib/test/test_zipfile/test_core.py | 24 +++++++++++++++++++ Lib/zipfile/__init__.py | 5 ++-- ...-08-29-09-28-53.gh-issue-156539.mH4AGb.rst | 3 +++ 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-29-09-28-53.gh-issue-156539.mH4AGb.rst diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index fdf2cd26f8c7c6..58cf972089f263 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -4335,6 +4335,30 @@ def test_closed_zip_raises_ValueError(self): f.write('zipfile test data') self.assertRaises(ValueError, zipf.write, TESTFN) + def test_testzip_with_duplicate_names(self): + data = io.BytesIO() + with zipfile.ZipFile(data, mode="w") as zipf: + zipf.writestr("duplicate", b"corrupt") + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + zipf.writestr("duplicate", b"valid") + + zipdata = bytearray(data.getvalue()) + with zipfile.ZipFile(io.BytesIO(zipdata)) as zipf: + zinfo = zipf.infolist()[0] + file_header = struct.unpack_from( + zipfile.structFileHeader, zipdata, zinfo.header_offset + ) + name_length, extra_length = file_header[-2:] + data_offset = (zinfo.header_offset + zipfile.sizeFileHeader + + name_length + extra_length) + zipdata[data_offset] ^= 1 + + with zipfile.ZipFile(io.BytesIO(zipdata)) as zipf: + self.assertRaises(zipfile.BadZipFile, zipf.read, + zipf.infolist()[0]) + self.assertEqual("duplicate", zipf.testzip()) + def test_bad_constructor_mode(self): """Check that bad modes passed to ZipFile constructor are caught.""" self.assertRaises(ValueError, zipfile.ZipFile, TESTFN, "q") diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 0accf324c90e3f..d0c4a92c59a0a5 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -2145,7 +2145,7 @@ def testzip(self): try: # Read by chunks, to avoid an OverflowError or a # MemoryError with very large embedded files. - with self.open(zinfo.filename, "r") as f: + with self.open(zinfo, "r") as f: while f.read(chunk_size): # Check CRC-32 pass except BadZipFile: @@ -2401,8 +2401,7 @@ def remove(self, zinfo_or_arcname): except KeyError: pass - # Avoid missing entry if there is another entry having the same name, - # to prevent an error on `testzip()`. + # Keep the last remaining entry with this name in NameToInfo. # Reverse the order as NameToInfo normally stores the last added one. for zi in reversed(self.filelist): if zi.filename == zinfo.filename: diff --git a/Misc/NEWS.d/next/Library/2026-08-29-09-28-53.gh-issue-156539.mH4AGb.rst b/Misc/NEWS.d/next/Library/2026-08-29-09-28-53.gh-issue-156539.mH4AGb.rst new file mode 100644 index 00000000000000..08963764677bf7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-29-09-28-53.gh-issue-156539.mH4AGb.rst @@ -0,0 +1,3 @@ +Fix :meth:`zipfile.ZipFile.testzip` to validate every member in a ZIP +archive with duplicate names. It could previously miss corruption in an +earlier member.