diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index 98d2a5e5cdf00e2..9a592d1694524e5 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -538,17 +538,23 @@ ZipFile objects If set, it uses this value as the modification timestamp for the file written into the ZIP archive, instead of using the current time. -.. method:: ZipFile.mkdir(zinfo_or_directory, mode=511) +.. method:: ZipFile.mkdir(zinfo_or_directory, mode=0o777) Create a directory inside the archive. If *zinfo_or_directory* is a string, a directory is created inside the archive with the mode that is specified in - the *mode* argument. If, however, *zinfo_or_directory* is - a :class:`ZipInfo` instance then the *mode* argument is ignored. + the *mode* argument. Only the permission bits of *mode* are used; other + bits are ignored. If, however, *zinfo_or_directory* is a :class:`ZipInfo` + instance then the *mode* argument is ignored. The archive must be opened with mode ``'w'``, ``'x'`` or ``'a'``. .. versionadded:: 3.11 + .. versionchanged:: next + Bits of *mode* other than the permission bits are now ignored. + Previously, file type bits in *mode* could mark the created entry as + something other than a directory. + .. method:: ZipFile.remove(zinfo_or_arcname) diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index cd498ba13e6f461..1749c08b8d39016 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -5478,10 +5478,24 @@ def test_mkdir(self): self.assertEqual(old_zinfo.filename, "directory4/") self.assertEqual(old_zinfo.external_attr, new_zinfo.external_attr) + # gh-154490: file type bits in *mode* must not leak into the + # entry's external attributes; only the permission bits are kept. + zf.mkdir("directory5", mode=0o107777) # S_IFREG | 0o7777 + zinfo = zf.filelist[4] + self.assertEqual(zinfo.filename, "directory5/") + self.assertEqual(zinfo.external_attr, (0o47777 << 16) | 0x10) + + zf.mkdir("directory6", mode=0o120555) # S_IFLNK | 0o555 + zinfo = zf.filelist[5] + self.assertEqual(zinfo.filename, "directory6/") + self.assertEqual(zinfo.external_attr, (0o40555 << 16) | 0x10) + target = os.path.join(TESTFN2, "target") os.mkdir(target) zf.extractall(target) - self.assertEqual(set(os.listdir(target)), {"directory", "directory2", "directory3", "directory4"}) + self.assertEqual(set(os.listdir(target)), + {"directory", "directory2", "directory3", + "directory4", "directory5", "directory6"}) def test_create_directory_with_write(self): with zipfile.ZipFile(TESTFN, "w") as zf: diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 418933a2e8d9e87..ee7e01d40d76505 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -2579,7 +2579,7 @@ def writestr(self, zinfo_or_arcname, data, with self.open(zinfo, mode='w') as dest: dest.write(data) - def mkdir(self, zinfo_or_directory_name, mode=511): + def mkdir(self, zinfo_or_directory_name, mode=0o777): """Creates a directory inside the zip archive.""" if isinstance(zinfo_or_directory_name, ZipInfo): zinfo = zinfo_or_directory_name @@ -2592,7 +2592,7 @@ def mkdir(self, zinfo_or_directory_name, mode=511): zinfo = ZipInfo(directory_name) zinfo.compress_size = 0 zinfo.CRC = 0 - zinfo.external_attr = ((0o40000 | mode) & 0xFFFF) << 16 + zinfo.external_attr = (0o40000 | (mode & 0o7777)) << 16 zinfo.file_size = 0 zinfo.external_attr |= 0x10 else: diff --git a/Misc/NEWS.d/next/Library/2026-07-22-17-18-57.gh-issue-154490.tLFMqx.rst b/Misc/NEWS.d/next/Library/2026-07-22-17-18-57.gh-issue-154490.tLFMqx.rst new file mode 100644 index 000000000000000..68d762b2d277146 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-22-17-18-57.gh-issue-154490.tLFMqx.rst @@ -0,0 +1,5 @@ +Fix :meth:`zipfile.ZipFile.mkdir` so that only the permission bits of the +*mode* argument are stored. Previously, file type bits in *mode* could mark +the created entry as something other than a directory. Also change the +default value of *mode* in the signature and documentation from ``511`` to +the equivalent but clearer ``0o777``.