Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions Doc/library/zipfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
16 changes: 15 additions & 1 deletion Lib/test/test_zipfile/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions Lib/zipfile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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``.
Loading