diff -r d54f047312a8 -r b17f9a10235f Doc/library/zipfile.rst --- a/Doc/library/zipfile.rst Fri Aug 31 11:31:20 2012 -0400 +++ b/Doc/library/zipfile.rst Sat Sep 01 16:38:00 2012 +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.3 + +.. 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.3 + +.. 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.3 + .. data:: ZIP_LZMA The numeric constant for the LZMA compression method. This requires the @@ -234,21 +258,27 @@ ZIP file that contains members with duplicate names. -.. 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. + *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`). -.. method:: ZipFile.extractall(path=None, members=None, pwd=None) +.. 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. + 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:: diff -r d54f047312a8 -r b17f9a10235f Lib/test/test_zipfile.py --- a/Lib/test/test_zipfile.py Fri Aug 31 11:31:20 2012 -0400 +++ b/Lib/test/test_zipfile.py Sat Sep 01 16:38:00 2012 +0400 @@ -4,6 +4,7 @@ import imp import time import shutil +import stat import struct import zipfile import unittest @@ -636,6 +637,102 @@ 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 = [] + os.mkdir(TESTFNDIR) + for permgroup in ('user', 'group', 'other'): + for index in range(8): + for specialindex in range(3): + filename = '{dir:s}/{permgroup:s}_{octalcode:03b}_{specialcode:03b}'.\ + 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) + os.rmdir(TESTFNDIR) + + 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) + shutil.rmtree(TESTFNDIR) + + 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) + shutil.rmtree(TESTFNDIR) + + 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) + shutil.rmtree(TESTFNDIR) + + 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) + shutil.rmtree(TESTFNDIR) + + 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) + shutil.rmtree(TESTFNDIR) + + 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) + shutil.rmtree(TESTFNDIR) + + def tearDown(self): + if os.path.exists(TESTFNDIR): + shutil.rmtree(TESTFNDIR) + if os.path.exists(TESTFN2): + os.remove(TESTFN2) + + class TestZip64InSmallFiles(unittest.TestCase): # These tests test the ZIP64 functionality without using large files, # see test_zipfile64 for proper tests. @@ -1780,7 +1877,7 @@ run_unittest(TestsWithSourceFile, TestZip64InSmallFiles, OtherTests, PyZipFileTests, DecryptionTests, TestsWithMultipleOpens, TestWithDirectory, UniversalNewlineTests, - TestsWithRandomBinaryFiles) + TestsWithRandomBinaryFiles, TestsPermissionExtraction) if __name__ == "__main__": test_main() diff -r d54f047312a8 -r b17f9a10235f Lib/zipfile.py --- a/Lib/zipfile.py Fri Aug 31 11:31:20 2012 -0400 +++ b/Lib/zipfile.py Sat Sep 01 16:38:00 2012 +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): @@ -1192,7 +1197,7 @@ return ZipExtFile(zef_file, mode, zinfo, zd, close_fileobj=not self._filePassed) - 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 @@ -1204,21 +1209,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(). + 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) - 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. """ @@ -1253,6 +1261,16 @@ source.close() target.close() + 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 d54f047312a8 -r b17f9a10235f Misc/ACKS --- a/Misc/ACKS Fri Aug 31 11:31:20 2012 -0400 +++ b/Misc/ACKS Sat Sep 01 16:38:00 2012 +0400 @@ -114,6 +114,7 @@ Forest Bond Gregory Bond Matias Bordese +Alexey Boriskin Jurjen Bos Peter Bosch Dan Boswell