diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index 4f20209927e7b3..aa558337156bdd 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -4827,6 +4827,29 @@ def test_overlap_with_archive_comment(self): with self.assertRaisesRegex(zipfile.BadZipFile, 'Overlapped entries'): zipf.read('a') + def test_append_keep_filename(self): + """Files loaded from an archive should keep original filename when + rewritten to central directory in append mode.""" + with mock.patch('os.sep', '/'), mock.patch('os.altsep', None): + with zipfile.ZipFile(TESTFN, mode="w") as zipfp: + zinfo = zipfile.ZipInfo('MyFolder/My\\File.txt') + zipfp.writestr(zinfo, 'foo') + + with mock.patch('os.sep', '\\'), mock.patch('os.altsep', '/'): + with zipfile.ZipFile(TESTFN, "a") as zipfp: + zi = zipfp.infolist()[0] + self.assertEqual(zi.orig_filename, 'MyFolder/My\\File.txt') + self.assertEqual(zi.filename, 'MyFolder/My/File.txt') + self.assertIsNone(zipfp.testzip()) + # trigger archive rewriting + zipfp.comment = b'' + + with zipfile.ZipFile(TESTFN, "r") as zipfp: + zi = zipfp.infolist()[0] + self.assertEqual(zi.orig_filename, 'MyFolder/My\\File.txt') + self.assertEqual(zi.filename, 'MyFolder/My/File.txt') + self.assertIsNone(zipfp.testzip()) + def tearDown(self): unlink(TESTFN) unlink(TESTFN2) diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 418933a2e8d9e8..c6cee20ba9952b 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -432,7 +432,7 @@ class ZipInfo: """Class with attributes describing each file in the ZIP archive.""" __slots__ = ( - 'orig_filename', + '_orig_filename', 'filename', 'date_time', 'compress_type', @@ -456,8 +456,6 @@ class ZipInfo: ) def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)): - self.orig_filename = filename # Original file name in archive - # Terminate the file name at the first null byte and # ensure paths always use forward slashes as the directory separator. filename = _sanitize_filename(filename) @@ -487,11 +485,25 @@ def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)): self.external_attr = 0 # External file attributes self.compress_size = 0 # Size of the compressed file self.file_size = 0 # Size of the uncompressed file + + # Special internal attributes set by class ZipFile when read from an archive: + self._orig_filename = None # Original file name in archive self._end_offset = None # Start of the next local header or central directory + # Other attributes are set by class ZipFile: # header_offset Byte offset to the file header # CRC CRC-32 of the uncompressed file + @property + def orig_filename(self): + return (self._orig_filename if self._orig_filename is not None + else self.filename) + + # Maintain backward compatibility in case someone sets it. + @orig_filename.setter + def orig_filename(self, value): + self._orig_filename = value + # Maintain backward compatibility with the old protected attribute name. @property def _compresslevel(self): @@ -579,9 +591,9 @@ def _encodeFilenameFlags(self): else: encoding = 'cp437' try: - return self.filename.encode(encoding), self.flag_bits & ~_MASK_UTF_FILENAME + return self.orig_filename.encode(encoding), self.flag_bits & ~_MASK_UTF_FILENAME except UnicodeEncodeError: - return self.filename.encode('utf-8'), self.flag_bits | _MASK_UTF_FILENAME + return self.orig_filename.encode('utf-8'), self.flag_bits | _MASK_UTF_FILENAME def _decodeExtra(self, filename_crc): # Try to decode the extra field. @@ -2056,6 +2068,7 @@ def _RealGetContents(self): filename = filename.decode(self.metadata_encoding or 'cp437') # Create ZipInfo instance to store file information x = ZipInfo(filename) + x._orig_filename = filename x.extra = fp.read(centdir[_CD_EXTRA_FIELD_LENGTH]) x.comment = fp.read(centdir[_CD_COMMENT_LENGTH]) x.header_offset = centdir[_CD_LOCAL_HEADER_OFFSET] diff --git a/Misc/NEWS.d/next/Library/2026-07-12-16-11-18.gh-issue-153625.-wpBTM.rst b/Misc/NEWS.d/next/Library/2026-07-12-16-11-18.gh-issue-153625.-wpBTM.rst new file mode 100644 index 00000000000000..e0d65783291216 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-12-16-11-18.gh-issue-153625.-wpBTM.rst @@ -0,0 +1 @@ +Fix an issue that an auto-sanitized filename corrupts a ZIP archive when rewritten to the central directory in append mode.