diff -r 2d33cbf02522 Doc/library/zipfile.rst --- a/Doc/library/zipfile.rst Tue Apr 15 14:24:53 2014 +0100 +++ b/Doc/library/zipfile.rst Tue Apr 15 16:07:45 2014 -0400 @@ -97,6 +97,30 @@ .. versionadded:: 3.3 +.. data:: PERMS_PRESERVE_NONE + + Constant for use in :meth:`extractall` and :meth:`extract` methods. Do not + preserve permissions of zipped files. + + .. versionadded:: 3.5 + +.. data:: PERMS_PRESERVE_SAFE + + Constant for use in :meth:`extractall` and :meth:`extract` methods. + Preserve safe subset of permissions of the zipped files only: permissions + for reading, writing, execution for user, group and others. + + .. versionadded:: 3.5 + +.. data:: PERMS_PRESERVE_ALL + + Constant for use in :meth:`extractall` and :meth:`extract` methods. + Preserve all the permissions of the zipped files, including unsafe ones: + UID bit (:data:`stat.S_ISUID`), group UID bit (:data:`stat.S_ISGID`), + sticky bit (:data:`stat.S_ISVTX`). + + .. versionadded:: 3.5 + .. data:: ZIP_LZMA The numeric constant for the LZMA compression method. This requires the @@ -238,13 +262,19 @@ The ``'U'`` or ``'rU'`` mode. Use :class:`io.TextIOWrapper` for reading compressed text files in :term:`universal newlines` mode. -.. method:: ZipFile.extract(member, path=None, pwd=None) +.. method:: ZipFile.extract(member, path=None, pwd=None, \ + preserve_permissions=zipfile.PERMS_PRESERVE_NONE) Extract a member from the archive to the current working directory; *member* - must be its full name or a :class:`ZipInfo` object). Its file information is - extracted as accurately as possible. *path* specifies a different directory - to extract to. *member* can be a filename or a :class:`ZipInfo` object. - *pwd* is the password used for encrypted files. + must be its full name or a :class:`ZipInfo` object). Its file information + is extracted as accurately as possible. *path* specifies a different + directory to extract to. *member* can be a filename or a :class:`ZipInfo` + object. *pwd* is the password used for encrypted files. + *preserve_permissions* controls whether permissions of zipped files are + preserved or not. Default is :data:`PERMS_PRESERVE_NONE` --- do not preserve + any permissions. Other options are to preserve safe subset of permissions + (:data:`PERMS_PRESERVE_SAFE`) or all permissions + (:data:`PERMS_PRESERVE_ALL`). .. note:: @@ -256,21 +286,33 @@ characters (``:``, ``<``, ``>``, ``|``, ``"``, ``?``, and ``*``) replaced by underscore (``_``). + .. versionadded:: 3.5 + The *preserve_permissions* argument. -.. method:: ZipFile.extractall(path=None, members=None, pwd=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 :meth:`namelist`. *pwd* is the password - used for encrypted files. +.. method:: ZipFile.extractall(path=None, members=None, pwd=None, \ + preserve_permissions=zipfile.PERMS_PRESERVE_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 :meth:`namelist`. *pwd* is the + password used for encrypted files. *preserve_permissions* controls whether + permissions of zipped files are preserved or not. Default is + :data:`PERMS_PRESERVE_NONE` --- do not preserve any permissions. Other + options are to preserve safe subset of permissions + (:data:`PERMS_PRESERVE_SAFE`) or all permissions + (:data:`PERMS_PRESERVE_ALL`). .. warning:: Never extract archives from untrusted sources without prior inspection. It is possible that files are created outside of *path*, e.g. members that have absolute filenames starting with ``"/"`` or filenames with two - dots ``".."``. This module attempts to prevent that. - See :meth:`extract` note. + dots ``".."``. This module attempts to prevent that. See :meth:`extract` + note. + + .. versionadded:: 3.5 + The *preserve_permissions* argument. .. method:: ZipFile.printdir() diff -r 2d33cbf02522 Doc/whatsnew/3.5.rst --- a/Doc/whatsnew/3.5.rst Tue Apr 15 14:24:53 2014 +0100 +++ b/Doc/whatsnew/3.5.rst Tue Apr 15 16:07:45 2014 -0400 @@ -158,6 +158,10 @@ *module* contains no docstrings instead of raising :exc:`ValueError` (contributed by Glenn Jones in :issue:`15916`). +* :meth:`ZipFile.extract` and :meth:`ZipFile.extractall` take an extra argument + that specifies which file permissions to preserve when the files are + extracted. (contributed by Alexey Boriskin in :issue:`17621`). + Optimizations ============= diff -r 2d33cbf02522 Lib/test/test_zipfile.py --- a/Lib/test/test_zipfile.py Tue Apr 15 14:24:53 2014 +0100 +++ b/Lib/test/test_zipfile.py Tue Apr 15 16:07:45 2014 -0400 @@ -4,6 +4,7 @@ import importlib.util import time import shutil +import stat import struct import zipfile import unittest @@ -14,7 +15,7 @@ from test.support import (TESTFN, findfile, unlink, requires_zlib, requires_bz2, requires_lzma, - captured_stdout, check_warnings) + rmtree, captured_stdout, check_warnings) TESTFN2 = TESTFN + "2" TESTFNDIR = TESTFN + "d" @@ -1746,6 +1747,108 @@ unlink(TESTFN2) +class TestsPermissionExtraction(unittest.TestCase): + def setUp(self): + permissions = { + 'user': (stat.S_IRUSR, stat.S_IWUSR, stat.S_IXUSR), + 'group': (stat.S_IRGRP, stat.S_IWGRP, stat.S_IXGRP), + 'other': (stat.S_IROTH, stat.S_IWOTH, stat.S_IXOTH), + 'special': (stat.S_ISUID, stat.S_ISGID, stat.S_ISVTX) + } + self.files = [] + name_pattern = '{dir:s}/{permgroup:s}_{octalcode:03b}_{specialcode:03b}' + os.mkdir(TESTFNDIR) + self.addCleanup(rmtree, TESTFNDIR) + for permgroup in ('user', 'group', 'other'): + for index in range(8): + for specialindex in range(3): + filename = name_pattern.\ + format(dir=TESTFNDIR, permgroup=permgroup, + octalcode=index, specialcode=specialindex) + with open(filename, 'wt') as file_: + file_.write(filename) + mode = stat.S_IRUSR + for order in range(3): + if index & 1 << order: + mode |= permissions[permgroup][order] + for order in range(3): + if specialindex & 1 << order: + mode |= permissions['special'][order] + os.chmod(filename, mode) + real_permission = os.stat(filename).st_mode & 0xFFF + self.files.append((filename, real_permission)) + with zipfile.ZipFile(TESTFN2, 'w', zipfile.ZIP_STORED) as zipfp: + for filename, mode in self.files: + zipfp.write(filename) + os.remove(filename) + + + def test_extractall_preserve_none(self): + umask = os.umask(0) + os.umask(umask) + with zipfile.ZipFile(TESTFN2, 'r') as zipfp: + zipfp.extractall() + for filename, mode in self.files: + expected_mode = 0o666 & ~umask + self.assertTrue(os.path.exists(filename)) + self.assertEqual(os.stat(filename).st_mode & 0xFFF, + expected_mode) + + + def test_extractall_preserve_safe(self): + with zipfile.ZipFile(TESTFN2, 'r') as zipfp: + zipfp.extractall(preserve_permissions=zipfile.PERMS_PRESERVE_SAFE) + for filename, mode in self.files: + expected_mode = mode & 0x1FF + self.assertTrue(os.path.exists(filename)) + self.assertEqual(os.stat(filename).st_mode & 0xFFF, + expected_mode) + + + def test_extractall_preserve_all(self): + with zipfile.ZipFile(TESTFN2, 'r') as zipfp: + zipfp.extractall(preserve_permissions=zipfile.PERMS_PRESERVE_ALL) + for filename, mode in self.files: + self.assertTrue(os.path.exists(filename)) + self.assertEqual(os.stat(filename).st_mode & 0xFFF, mode) + + + def test_extract_preserve_none(self): + umask = os.umask(0) + os.umask(umask) + with zipfile.ZipFile(TESTFN2, 'r') as zipfp: + for filename, mode in self.files: + zipfp.extract(filename) + expected_mode = 0o666 & ~umask + self.assertTrue(os.path.exists(filename)) + self.assertEqual(os.stat(filename).st_mode & 0xFFF, + expected_mode) + + + def test_extract_preserve_safe(self): + with zipfile.ZipFile(TESTFN2, 'r') as zipfp: + for filename, mode in self.files: + zipfp.extract(filename, + preserve_permissions=zipfile.PERMS_PRESERVE_SAFE) + expected_mode = mode & 0x1FF + self.assertTrue(os.path.exists(filename)) + self.assertEqual(os.stat(filename).st_mode & 0xFFF, + expected_mode) + + + def test_extract_preserve_all(self): + with zipfile.ZipFile(TESTFN2, 'r') as zipfp: + for filename, mode in self.files: + zipfp.extract(filename, + preserve_permissions=zipfile.PERMS_PRESERVE_ALL) + self.assertTrue(os.path.exists(filename)) + self.assertEqual(os.stat(filename).st_mode & 0xFFF, mode) + + + def tearDown(self): + unlink(TESTFN2) + + class StoredUniversalNewlineTests(AbstractUniversalNewlineTests, unittest.TestCase): compression = zipfile.ZIP_STORED diff -r 2d33cbf02522 Lib/zipfile.py --- a/Lib/zipfile.py Tue Apr 15 14:24:53 2014 +0100 +++ b/Lib/zipfile.py Tue Apr 15 16:07:45 2014 -0400 @@ -34,6 +34,7 @@ __all__ = ["BadZipFile", "BadZipfile", "error", "ZIP_STORED", "ZIP_DEFLATED", "ZIP_BZIP2", "ZIP_LZMA", + "PERMS_PRESERVE_NONE", 'PERMS_PRESERVE_SAFE', "PERMS_PRESERVE_ALL", "is_zipfile", "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile"] class BadZipFile(Exception): @@ -60,6 +61,9 @@ ZIP_LZMA = 14 # Other ZIP compression methods not supported +#Enum choices for Zipfile.extractall preserve_permissions argument +PERMS_PRESERVE_NONE, PERMS_PRESERVE_SAFE, PERMS_PRESERVE_ALL = range(3) + DEFAULT_VERSION = 20 ZIP64_VERSION = 45 BZIP2_VERSION = 46 @@ -160,6 +164,7 @@ _CD64_DIRECTORY_SIZE = 8 _CD64_OFFSET_START_CENTDIR = 9 + def _check_zipfile(fp): try: if _EndRecData(fp): @@ -1213,7 +1218,8 @@ zef_file.close() raise - def extract(self, member, path=None, pwd=None): + def extract(self, member, path=None, pwd=None, + preserve_permissions=PERMS_PRESERVE_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 @@ -1225,19 +1231,24 @@ if path is None: path = os.getcwd() - 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=PERMS_PRESERVE_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(). + `members' is optional and must be a subset of the list returned by + namelist(). `preserve_permissions` controls whether permissions of + zipped files are preserved or not. Default is PERMS_PRESERVE_NONE - + do not preserve any permissions. Other options are to preserve safe + subset of permissions PERMS_PRESERVE_SAFE or all permissions + PERMS_PRESERVE_ALL. """ if members is None: members = self.namelist() for zipinfo in members: - self.extract(zipinfo, path, pwd) + self.extract(zipinfo, path, pwd, preserve_permissions) @classmethod def _sanitize_windows_name(cls, arcname, pathsep): @@ -1254,7 +1265,7 @@ arcname = pathsep.join(x for x in arcname if x) return arcname - def _extract_member(self, member, targetpath, pwd): + def _extract_member(self, member, targetpath, pwd, preserve_permissions): """Extract the ZipInfo object 'member' to a physical file on the path targetpath. """ @@ -1291,6 +1302,16 @@ open(targetpath, "wb") as target: shutil.copyfileobj(source, target) + if preserve_permissions in (PERMS_PRESERVE_SAFE, PERMS_PRESERVE_ALL): + if preserve_permissions == PERMS_PRESERVE_ALL: + #preserve bits 0-11: sugrwxrwxrwx, this include + #sticky bit, uid bit, gid bit + mode = member.external_attr >> 16 & 0xFFF + elif PERMS_PRESERVE_SAFE: + #preserve bits 0-8 only: rwxrwxrwx + mode = member.external_attr >> 16 & 0x1FF + os.chmod(targetpath, mode) + return targetpath def _writecheck(self, zinfo): diff -r 2d33cbf02522 Misc/ACKS --- a/Misc/ACKS Tue Apr 15 14:24:53 2014 +0100 +++ b/Misc/ACKS Tue Apr 15 16:07:45 2014 -0400 @@ -143,6 +143,7 @@ Forest Bond Gregory Bond Matias Bordese +Alexey Boriskin Jonas Borgström Jurjen Bos Peter Bosch