-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
gh-59999: Add option to preserve permissions in ZipFile.extract #32289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4fea41a
f2f44ea
a09e5d3
e2fa678
8a056b4
a56d934
06aff01
9a4b4f6
5776f4c
8a3a778
8a6a2c4
6470201
5c8d82b
89da6f2
30e5528
5a1edac
8a82294
41a350d
1b09aec
c456bb3
5616e0a
0065d2e
4064c32
f2c816a
7d72b3b
cb2478b
a082204
fbba438
0d6b864
b466493
9e94581
500ae2e
a81a857
2cad611
5ef404a
c74c006
865771c
a2d7e77
44f5c89
9f5ebc3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,9 +4,12 @@ | |
XXX references to utf-8 need further investigation. | ||
""" | ||
import binascii | ||
import contextlib | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unused? |
||
import enum | ||
import importlib.util | ||
import io | ||
import os | ||
import pathlib | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is used? |
||
import shutil | ||
import stat | ||
import struct | ||
|
@@ -33,6 +36,7 @@ | |
|
||
__all__ = ["BadZipFile", "BadZipfile", "error", | ||
"ZIP_STORED", "ZIP_DEFLATED", "ZIP_BZIP2", "ZIP_LZMA", | ||
"PreserveMode", | ||
"is_zipfile", "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile", | ||
"Path"] | ||
|
||
|
@@ -373,6 +377,12 @@ def _sanitize_filename(filename): | |
return filename | ||
|
||
|
||
class PreserveMode(enum.Enum): | ||
"""Options for preserving file permissions upon extraction.""" | ||
NONE = enum.auto() | ||
SAFE = enum.auto() | ||
ALL = enum.auto() | ||
Comment on lines
+380
to
+384
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There needs to be a mode for It is not very natural to use an enum here - much more general is to define The constants here could directly be PRESERVE_NONE = 0
PRESERVE_SAFE = 0o0777
PRESERVE_ALL = 0o7777 |
||
|
||
class ZipInfo: | ||
"""Class with attributes describing each file in the ZIP archive.""" | ||
|
||
|
@@ -1792,26 +1802,30 @@ def _open_to_write(self, zinfo, force_zip64=False): | |
self._writing = True | ||
return _ZipWriteFile(self, zinfo, zip64) | ||
|
||
def extract(self, member, path=None, pwd=None): | ||
def extract(self, member, path=None, pwd=None, | ||
preserve_permissions=PreserveMode.NONE): | ||
"""Extract a member from the archive to the current working directory, | ||
using its full name. Its file information is extracted as accurately | ||
as possible. 'member' may be a filename or a ZipInfo object. You can | ||
specify a different directory using 'path'. You can specify the | ||
password to decrypt the file using 'pwd'. | ||
password to decrypt the file using 'pwd'. `preserve_permissions' | ||
controls whether permissions of zipped files are preserved. | ||
""" | ||
if path is None: | ||
path = os.getcwd() | ||
else: | ||
path = os.fspath(path) | ||
|
||
return self._extract_member(member, path, pwd) | ||
return self._extract_member(member, path, pwd, preserve_permissions) | ||
|
||
def extractall(self, path=None, members=None, pwd=None): | ||
def extractall(self, path=None, members=None, pwd=None, | ||
preserve_permissions=PreserveMode.NONE): | ||
"""Extract all members from the archive to the current working | ||
directory. 'path' specifies a different directory to extract to. | ||
'members' is optional and must be a subset of the list returned | ||
by namelist(). You can specify the password to decrypt all files | ||
using 'pwd'. | ||
using 'pwd'. `preserve_permissions' controls whether permissions | ||
of zipped files are preserved. | ||
""" | ||
if members is None: | ||
members = self.namelist() | ||
|
@@ -1822,7 +1836,7 @@ def extractall(self, path=None, members=None, pwd=None): | |
path = os.fspath(path) | ||
|
||
for zipinfo in members: | ||
self._extract_member(zipinfo, path, pwd) | ||
self._extract_member(zipinfo, path, pwd, preserve_permissions) | ||
|
||
@classmethod | ||
def _sanitize_windows_name(cls, arcname, pathsep): | ||
|
@@ -1839,7 +1853,27 @@ def _sanitize_windows_name(cls, arcname, pathsep): | |
arcname = pathsep.join(x for x in arcname if x) | ||
return arcname | ||
|
||
def _extract_member(self, member, targetpath, pwd): | ||
def _apply_permissions(self, member, path, mode): | ||
""" | ||
Apply ZipFile permissions to a file on the filesystem with | ||
specified PreserveMode | ||
""" | ||
if mode == PreserveMode.NONE: | ||
return | ||
|
||
# Ignore permissions if the archive was created on Windows | ||
if member.create_system == 0: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the check should probably be more conservative than this, there is also 10 which is Windows NTFS, and 20-255 are unused and could be invalid for this use case. An allow-list would be safer. |
||
return | ||
|
||
mask = { | ||
PreserveMode.SAFE: 0o777, | ||
PreserveMode.ALL: 0xFFFF, | ||
} | ||
Comment on lines
+1868
to
+1871
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could define class PreserveMode(enum.IntEnum):
NONE = 0
SAFE = 0o777
ALL = 0o7777 then here it's just (member.external_attr >> 16) & mode (However, see my other comment about not restricting this to specific values - users may have arbitrary requirements that an int can capture better.) |
||
new_mode = (member.external_attr >> 16) & mask[mode] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should honour the current umask. It's clumsy to get the current umask so the easiest way to do this is not to chmod the file after creation but to pass the requested mode bits into |
||
os.chmod(path, new_mode) | ||
|
||
def _extract_member(self, member, targetpath, pwd, | ||
preserve_permissions=PreserveMode.NONE): | ||
"""Extract the ZipInfo object 'member' to a physical | ||
file on the path targetpath. | ||
""" | ||
|
@@ -1886,6 +1920,7 @@ def _extract_member(self, member, targetpath, pwd): | |
open(targetpath, "wb") as target: | ||
shutil.copyfileobj(source, target) | ||
|
||
self._apply_permissions(member, targetpath, preserve_permissions) | ||
return targetpath | ||
|
||
def _writecheck(self, zinfo): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Add options to preserve file permissions in :meth:`zipfile.ZipFile.extract` and :meth:`zipfile.ZipFile.extractall` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a mis-copy perhaps?