From 4fea41a68182cb863472550e4d86c1fb426a9234 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sun, 3 Apr 2022 16:46:53 +0100 Subject: [PATCH 01/32] Add option to preserve permissions in ZipFile.extract --- Lib/test/test_zipfile.py | 74 +++++++++++++++++++++++++++++++++++++++- Lib/zipfile.py | 36 +++++++++++++++---- 2 files changed, 102 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py index 26c40457e62a05..2b619e11015dcb 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -15,6 +15,7 @@ import unittest.mock as mock import zipfile import functools +import stat from tempfile import TemporaryFile @@ -1342,7 +1343,6 @@ def test_write_pathlike(self): class ExtractTests(unittest.TestCase): - def make_test_file(self): with zipfile.ZipFile(TESTFN2, "w", zipfile.ZIP_STORED) as zipfp: for fpath, fdata in SMALL_TEST_DATA: @@ -2055,6 +2055,78 @@ def tearDown(self): unlink(TESTFN) unlink(TESTFN2) +class TestsPermissionExtraction(unittest.TestCase): + def setUp(self): + self.files = [] + + for mode in range(0o1000): + filename = str(mode) + pathlib.Path(filename).touch(mode=mode) + real_mode = os.stat(filename).st_mode & 0xFFFF + self.files.append((filename, real_mode)) + + with zipfile.ZipFile(TESTFN2, 'w', zipfile.ZIP_STORED) as zf: + for filename, mode in self.files: + zf.write(filename) + os.remove(filename) + + def tearDown(self): + os.unlink(TESTFN2) + + def test_extractall_preserve_none(self): + with zipfile.ZipFile(TESTFN2, 'r') as zf: + zf.extractall() + for filename, mode in self.files: + expected_mode = 0o644 # TODO: Why are the files extracted with mode 777? + self.assertTrue(os.path.exists(filename)) + self.assertEqual(os.stat(filename).st_mode, + stat.S_IFREG | expected_mode) + + def test_extractall_preserve_safe(self): + with zipfile.ZipFile(TESTFN2, 'r') as zf: + zf.extractall(preserve_permissions=zipfile.PERMS_PRESERVE_SAFE) + for filename, mode in self.files: + expected_mode = mode + self.assertTrue(os.path.exists(filename)) + self.assertEqual(os.stat(filename).st_mode, + stat.S_IFREG | (expected_mode & 0o777)) + + + @unittest.skipUnless(os.getuid() == 0, "requires root") + def test_extractall_preserve_all(self): + with zipfile.ZipFile(TESTFN2, 'r') as zf: + zf.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, mode) + + def test_extract_preserve_none(self): + with zipfile.ZipFile(TESTFN2, 'r') as zf: + for filename, mode in self.files: + zf.extract(filename) + expected_mode = 0o644 + self.assertTrue(os.path.exists(filename)) + self.assertEqual(os.stat(filename).st_mode, + stat.S_IFREG | expected_mode) + + def test_extract_preserve_safe(self): + with zipfile.ZipFile(TESTFN2, 'r') as zf: + for filename, mode in self.files: + zf.extract(filename, + preserve_permissions=zipfile.PERMS_PRESERVE_SAFE) + expected_mode = mode + self.assertTrue(os.path.exists(filename)) + self.assertEqual(os.stat(filename).st_mode, + stat.S_IFREG | (expected_mode & 0o777)) + + @unittest.skipUnless(os.getuid() == 0, "requires root") + def test_extract_preserve_all(self): + with zipfile.ZipFile(TESTFN2, 'r') as zf: + for filename, mode in self.files: + zf.extract(filename, + preserve_permissions=zipfile.PERMS_PRESERVE_ALL) + self.assertTrue(os.path.exists(filename)) + self.assertEqual(os.stat(filename).st_mode, mode) class AbstractBadCrcTests: def test_testzip_with_bad_crc(self): diff --git a/Lib/zipfile.py b/Lib/zipfile.py index 721834aff13a74..434390ba6f5a73 100644 --- a/Lib/zipfile.py +++ b/Lib/zipfile.py @@ -37,6 +37,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", "Path"] @@ -64,6 +65,11 @@ class LargeZipFile(Exception): ZIP_LZMA = 14 # Other ZIP compression methods not supported +# Options for preserving file permissions upon extraction +PERMS_PRESERVE_NONE = 0 +PERMS_PRESERVE_SAFE = 1 +PERMS_PRESERVE_ALL = 2 + DEFAULT_VERSION = 20 ZIP64_VERSION = 45 BZIP2_VERSION = 46 @@ -1644,24 +1650,28 @@ 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=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 - specify a different directory using `path'. + specify a different directory using `path'. `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=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. """ if members is None: members = self.namelist() @@ -1672,7 +1682,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): @@ -1689,7 +1699,8 @@ 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 _extract_member(self, member, targetpath, pwd, + preserve_permissions=PERMS_PRESERVE_NONE): """Extract the ZipInfo object 'member' to a physical file on the path targetpath. """ @@ -1729,6 +1740,17 @@ def _extract_member(self, member, targetpath, pwd): open(targetpath, "wb") as target: shutil.copyfileobj(source, target) + if preserve_permissions == PERMS_PRESERVE_NONE: + return targetpath + + if preserve_permissions == PERMS_PRESERVE_SAFE: + mode = (member.external_attr >> 16) & 0o777 + + if preserve_permissions == PERMS_PRESERVE_ALL: + mode = (member.external_attr >> 16) & 0xFFFF + + os.chmod(targetpath, mode) + return targetpath def _writecheck(self, zinfo): From f2f44ea5c972fc861492089b0d00608a375c5bbf Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sun, 3 Apr 2022 16:50:52 +0100 Subject: [PATCH 02/32] Update ACKS --- Misc/ACKS | 1 + 1 file changed, 1 insertion(+) diff --git a/Misc/ACKS b/Misc/ACKS index 72e2cfe13775af..ee210edadad485 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -201,6 +201,7 @@ Gregory Bond Angelin Booz Médéric Boquien Matias Bordese +Alexey Boriskin Jonas Borgström Jurjen Bos Peter Bosch From a09e5d391a0ac320a78bdf1947626166d07443ff Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sun, 3 Apr 2022 17:03:33 +0100 Subject: [PATCH 03/32] Document option contents --- Doc/library/zipfile.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index bfcc883de69271..b35621a7279602 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -120,6 +120,29 @@ The module defines the following items: methods, and may either refuse to process the ZIP file altogether, or fail to extract individual files. +.. data:: PERMS_PRESERVE_NONE + + Constant for use in :meth:`extractall` and :meth:`extract` methods. Do not + preserve permissions of zipped files. + + .. versionadded:: 3.11 + +.. 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.11 + +.. 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.11 .. seealso:: From e2fa678f2036fc97f1f0ab1fa2f4acb13a7d8732 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Mon, 4 Apr 2022 09:59:16 +0100 Subject: [PATCH 04/32] Document preserve_permissions parameter --- Doc/library/zipfile.rst | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index b35621a7279602..ef7b4f6bcb652c 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -342,13 +342,19 @@ ZipFile Objects Previously, a :exc:`RuntimeError` was raised. -.. 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 how the permissions of zipped files are preserved. The default is + :data:`PERMS_PRESERVE_NONE` --- do not preserve any permissions. Other + options are to preserve a safe subset of permissions + (:data:`PERMS_PRESERVE_SAFE`) or all permissions + (:data:`PERMS_PRESERVE_ALL`). Returns the normalized path created (a directory or new file). @@ -370,12 +376,18 @@ ZipFile Objects The *path* parameter accepts a :term:`path-like object`. -.. 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 how the permissions + of zipped files are preserved. The default is :data:`PERMS_PRESERVE_NONE` + --- do not preserve any permissions. Other options are to preserve a safe + subset of permissions (:data:`PERMS_PRESERVE_SAFE`) or all permissions + (:data:`PERMS_PRESERVE_ALL`). + .. warning:: From 8a056b4fc2b9ecd004205aa14f686a637a389940 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Tue, 5 Apr 2022 19:33:19 +0100 Subject: [PATCH 05/32] Continue work on tests --- Lib/test/test_zipfile.py | 55 ++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py index 2b619e11015dcb..8fb39208ceb58d 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -2061,53 +2061,49 @@ def setUp(self): for mode in range(0o1000): filename = str(mode) - pathlib.Path(filename).touch(mode=mode) - real_mode = os.stat(filename).st_mode & 0xFFFF - self.files.append((filename, real_mode)) + # pathlib.Path(filename).touch(mode=mode) + # real_mode = os.stat(filename).st_mode & 0xFFFF + # self.files.append((filename, real_mode)) with zipfile.ZipFile(TESTFN2, 'w', zipfile.ZIP_STORED) as zf: - for filename, mode in self.files: - zf.write(filename) - os.remove(filename) + for mode in range(0o1000): + filename = str(mode) + zinfo = zipfile.ZipInfo(filename) + zinfo.external_attr = mode << 16 + zf.writestr(zinfo, filename) + self.files.append((filename, mode)) def tearDown(self): os.unlink(TESTFN2) + for filename, _mode in self.files: + os.unlink(filename) def test_extractall_preserve_none(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: zf.extractall() for filename, mode in self.files: - expected_mode = 0o644 # TODO: Why are the files extracted with mode 777? + expected_mode = 0o644 self.assertTrue(os.path.exists(filename)) self.assertEqual(os.stat(filename).st_mode, stat.S_IFREG | expected_mode) - def test_extractall_preserve_safe(self): + def test_extract_preserve_none(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: - zf.extractall(preserve_permissions=zipfile.PERMS_PRESERVE_SAFE) for filename, mode in self.files: - expected_mode = mode + zf.extract(filename) + expected_mode = 0o644 self.assertTrue(os.path.exists(filename)) self.assertEqual(os.stat(filename).st_mode, - stat.S_IFREG | (expected_mode & 0o777)) - - - @unittest.skipUnless(os.getuid() == 0, "requires root") - def test_extractall_preserve_all(self): - with zipfile.ZipFile(TESTFN2, 'r') as zf: - zf.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, mode) + stat.S_IFREG | expected_mode) - def test_extract_preserve_none(self): + def test_extractall_preserve_safe(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: + zf.extractall(preserve_permissions=zipfile.PERMS_PRESERVE_SAFE) for filename, mode in self.files: - zf.extract(filename) - expected_mode = 0o644 + expected_mode = mode self.assertTrue(os.path.exists(filename)) self.assertEqual(os.stat(filename).st_mode, - stat.S_IFREG | expected_mode) + stat.S_IFREG | (expected_mode & 0o777)) def test_extract_preserve_safe(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: @@ -2119,7 +2115,16 @@ def test_extract_preserve_safe(self): self.assertEqual(os.stat(filename).st_mode, stat.S_IFREG | (expected_mode & 0o777)) - @unittest.skipUnless(os.getuid() == 0, "requires root") + @unittest.skip("") + def test_extractall_preserve_all(self): + with zipfile.ZipFile(TESTFN2, 'r') as zf: + zf.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, mode) + + # @unittest.skipUnless(os.getuid() == 0, "requires root") + @unittest.skip("") def test_extract_preserve_all(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: for filename, mode in self.files: From a56d934ab31ae16b19b282b641b885deb9877713 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Wed, 13 Apr 2022 00:54:02 +0100 Subject: [PATCH 06/32] Fix tests --- Lib/test/test_zipfile.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py index 8fb39208ceb58d..47d855f37ec8e7 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -2059,14 +2059,8 @@ class TestsPermissionExtraction(unittest.TestCase): def setUp(self): self.files = [] - for mode in range(0o1000): - filename = str(mode) - # pathlib.Path(filename).touch(mode=mode) - # real_mode = os.stat(filename).st_mode & 0xFFFF - # self.files.append((filename, real_mode)) - with zipfile.ZipFile(TESTFN2, 'w', zipfile.ZIP_STORED) as zf: - for mode in range(0o1000): + for mode in range(1, 0o1000): filename = str(mode) zinfo = zipfile.ZipInfo(filename) zinfo.external_attr = mode << 16 @@ -2099,23 +2093,21 @@ def test_extract_preserve_none(self): def test_extractall_preserve_safe(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: zf.extractall(preserve_permissions=zipfile.PERMS_PRESERVE_SAFE) - for filename, mode in self.files: - expected_mode = mode + for filename, expected_mode in self.files: self.assertTrue(os.path.exists(filename)) self.assertEqual(os.stat(filename).st_mode, stat.S_IFREG | (expected_mode & 0o777)) def test_extract_preserve_safe(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: - for filename, mode in self.files: + for filename, expected_mode in self.files: zf.extract(filename, preserve_permissions=zipfile.PERMS_PRESERVE_SAFE) - expected_mode = mode self.assertTrue(os.path.exists(filename)) self.assertEqual(os.stat(filename).st_mode, stat.S_IFREG | (expected_mode & 0o777)) - @unittest.skip("") + @unittest.skipUnless(os.getuid() == 0, "requires root") def test_extractall_preserve_all(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: zf.extractall(preserve_permissions=zipfile.PERMS_PRESERVE_ALL) @@ -2123,8 +2115,7 @@ def test_extractall_preserve_all(self): self.assertTrue(os.path.exists(filename)) self.assertEqual(os.stat(filename).st_mode, mode) - # @unittest.skipUnless(os.getuid() == 0, "requires root") - @unittest.skip("") + @unittest.skipUnless(os.getuid() == 0, "requires root") def test_extract_preserve_all(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: for filename, mode in self.files: From 06aff018c1b6b6ae59f1145c2912a8bdeca6294e Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Wed, 13 Apr 2022 01:18:36 +0100 Subject: [PATCH 07/32] Skip tests on windows --- Lib/test/test_zipfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py index 47d855f37ec8e7..51d39bd84cbebe 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -2055,6 +2055,7 @@ def tearDown(self): unlink(TESTFN) unlink(TESTFN2) +@unittest.skipIf(sys.platform == "win32", "Requires file permissions") class TestsPermissionExtraction(unittest.TestCase): def setUp(self): self.files = [] From 9a4b4f620908b8c27ddff74b7018e0373a5224b2 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Wed, 13 Apr 2022 01:20:28 +0100 Subject: [PATCH 08/32] Add news entry --- .../next/Library/2022-04-13-01-20-21.gh-issue-59999.lfhqku.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2022-04-13-01-20-21.gh-issue-59999.lfhqku.rst diff --git a/Misc/NEWS.d/next/Library/2022-04-13-01-20-21.gh-issue-59999.lfhqku.rst b/Misc/NEWS.d/next/Library/2022-04-13-01-20-21.gh-issue-59999.lfhqku.rst new file mode 100644 index 00000000000000..9cfb833a81d2a1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-04-13-01-20-21.gh-issue-59999.lfhqku.rst @@ -0,0 +1 @@ +Add options to preserve file permissions in :meth:`ZipFile.extract` From 5776f4cd54d9768f28da8ebb1871f30e2b4b2a39 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Wed, 13 Apr 2022 10:56:13 +0100 Subject: [PATCH 09/32] Add tests for special file permissions --- Lib/test/test_zipfile.py | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py index 51d39bd84cbebe..a604fa441bbddc 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -50,6 +50,12 @@ def get_files(test): yield f test.assertFalse(f.closed) +def isroot(): + if sys.platform == "win32": + return False + + return os.getuid() == 0 + class AbstractTestsWithSourceFile: @classmethod def setUpClass(cls): @@ -2058,20 +2064,22 @@ def tearDown(self): @unittest.skipIf(sys.platform == "win32", "Requires file permissions") class TestsPermissionExtraction(unittest.TestCase): def setUp(self): + os.mkdir(TESTFNDIR) self.files = [] with zipfile.ZipFile(TESTFN2, 'w', zipfile.ZIP_STORED) as zf: - for mode in range(1, 0o1000): - filename = str(mode) - zinfo = zipfile.ZipInfo(filename) - zinfo.external_attr = mode << 16 - zf.writestr(zinfo, filename) - self.files.append((filename, mode)) + for base_mode in range(1, 0o1000): + for special_mask in range(0o10): + mode = base_mode | special_mask << 9 + filename = os.path.join(TESTFNDIR, str(mode)) + zinfo = zipfile.ZipInfo(filename) + zinfo.external_attr = mode << 16 + zf.writestr(zinfo, filename) + self.files.append((filename, mode)) def tearDown(self): os.unlink(TESTFN2) - for filename, _mode in self.files: - os.unlink(filename) + rmtree(TESTFNDIR) def test_extractall_preserve_none(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: @@ -2108,22 +2116,24 @@ def test_extract_preserve_safe(self): self.assertEqual(os.stat(filename).st_mode, stat.S_IFREG | (expected_mode & 0o777)) - @unittest.skipUnless(os.getuid() == 0, "requires root") + @unittest.skipUnless(isroot(), "requires root") def test_extractall_preserve_all(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: zf.extractall(preserve_permissions=zipfile.PERMS_PRESERVE_ALL) - for filename, mode in self.files: + for filename, expected_mode in self.files: self.assertTrue(os.path.exists(filename)) - self.assertEqual(os.stat(filename).st_mode, mode) + self.assertEqual(os.stat(filename).st_mode, + stat.S_IFREG | expected_mode) - @unittest.skipUnless(os.getuid() == 0, "requires root") + @unittest.skipUnless(isroot(), "requires root") def test_extract_preserve_all(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: - for filename, mode in self.files: + for filename, expected_mode in self.files: zf.extract(filename, preserve_permissions=zipfile.PERMS_PRESERVE_ALL) self.assertTrue(os.path.exists(filename)) - self.assertEqual(os.stat(filename).st_mode, mode) + self.assertEqual(os.stat(filename).st_mode, + stat.S_IFREG | expected_mode) class AbstractBadCrcTests: def test_testzip_with_bad_crc(self): From 8a3a778b3d1e30054cf8b4f218ee071bb081fb5b Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Wed, 13 Apr 2022 11:08:58 +0100 Subject: [PATCH 10/32] Re-introduce tests for mode 0 --- Lib/test/test_zipfile.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py index a604fa441bbddc..aaa02004ac7d75 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -2068,12 +2068,12 @@ def setUp(self): self.files = [] with zipfile.ZipFile(TESTFN2, 'w', zipfile.ZIP_STORED) as zf: - for base_mode in range(1, 0o1000): + for base_mode in range(0o1000): for special_mask in range(0o10): - mode = base_mode | special_mask << 9 + mode = stat.S_IFREG | base_mode | special_mask << 9 filename = os.path.join(TESTFNDIR, str(mode)) zinfo = zipfile.ZipInfo(filename) - zinfo.external_attr = mode << 16 + zinfo.external_attr = (mode << 16) zf.writestr(zinfo, filename) self.files.append((filename, mode)) @@ -2102,38 +2102,38 @@ def test_extract_preserve_none(self): def test_extractall_preserve_safe(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: zf.extractall(preserve_permissions=zipfile.PERMS_PRESERVE_SAFE) - for filename, expected_mode in self.files: + for filename, mode in self.files: self.assertTrue(os.path.exists(filename)) self.assertEqual(os.stat(filename).st_mode, - stat.S_IFREG | (expected_mode & 0o777)) + stat.S_IFREG | (mode & 0o777)) def test_extract_preserve_safe(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: - for filename, expected_mode in self.files: + for filename, mode in self.files: zf.extract(filename, preserve_permissions=zipfile.PERMS_PRESERVE_SAFE) self.assertTrue(os.path.exists(filename)) self.assertEqual(os.stat(filename).st_mode, - stat.S_IFREG | (expected_mode & 0o777)) + stat.S_IFREG | (mode & 0o777)) @unittest.skipUnless(isroot(), "requires root") def test_extractall_preserve_all(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: zf.extractall(preserve_permissions=zipfile.PERMS_PRESERVE_ALL) - for filename, expected_mode in self.files: + for filename, mode in self.files: self.assertTrue(os.path.exists(filename)) self.assertEqual(os.stat(filename).st_mode, - stat.S_IFREG | expected_mode) + stat.S_IFREG | mode) @unittest.skipUnless(isroot(), "requires root") def test_extract_preserve_all(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: - for filename, expected_mode in self.files: + for filename, mode in self.files: zf.extract(filename, preserve_permissions=zipfile.PERMS_PRESERVE_ALL) self.assertTrue(os.path.exists(filename)) self.assertEqual(os.stat(filename).st_mode, - stat.S_IFREG | expected_mode) + stat.S_IFREG | mode) class AbstractBadCrcTests: def test_testzip_with_bad_crc(self): From 8a6a2c44187d581050b99c4327bb15d49ed991f2 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Wed, 13 Apr 2022 11:17:02 +0100 Subject: [PATCH 11/32] Use enumeration type in place of constants --- Lib/test/test_zipfile.py | 8 ++++---- Lib/zipfile.py | 28 ++++++++++++++-------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py index aaa02004ac7d75..834840c0042a1e 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -2101,7 +2101,7 @@ def test_extract_preserve_none(self): def test_extractall_preserve_safe(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: - zf.extractall(preserve_permissions=zipfile.PERMS_PRESERVE_SAFE) + zf.extractall(preserve_permissions=zipfile.PreserveMode.SAFE) for filename, mode in self.files: self.assertTrue(os.path.exists(filename)) self.assertEqual(os.stat(filename).st_mode, @@ -2111,7 +2111,7 @@ def test_extract_preserve_safe(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: for filename, mode in self.files: zf.extract(filename, - preserve_permissions=zipfile.PERMS_PRESERVE_SAFE) + preserve_permissions=zipfile.PreserveMode.SAFE) self.assertTrue(os.path.exists(filename)) self.assertEqual(os.stat(filename).st_mode, stat.S_IFREG | (mode & 0o777)) @@ -2119,7 +2119,7 @@ def test_extract_preserve_safe(self): @unittest.skipUnless(isroot(), "requires root") def test_extractall_preserve_all(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: - zf.extractall(preserve_permissions=zipfile.PERMS_PRESERVE_ALL) + zf.extractall(preserve_permissions=zipfile.PreserveMode.ALL) for filename, mode in self.files: self.assertTrue(os.path.exists(filename)) self.assertEqual(os.stat(filename).st_mode, @@ -2130,7 +2130,7 @@ def test_extract_preserve_all(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: for filename, mode in self.files: zf.extract(filename, - preserve_permissions=zipfile.PERMS_PRESERVE_ALL) + preserve_permissions=zipfile.PreserveMode.ALL) self.assertTrue(os.path.exists(filename)) self.assertEqual(os.stat(filename).st_mode, stat.S_IFREG | mode) diff --git a/Lib/zipfile.py b/Lib/zipfile.py index 434390ba6f5a73..6b955dbdfe63fe 100644 --- a/Lib/zipfile.py +++ b/Lib/zipfile.py @@ -17,6 +17,7 @@ import time import contextlib import pathlib +import enum try: import zlib # We may need its compression method @@ -37,9 +38,8 @@ __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", - "Path"] + "PreserveMode", "is_zipfile", "ZipInfo", "ZipFile", + "PyZipFile", "LargeZipFile", "Path"] class BadZipFile(Exception): pass @@ -65,11 +65,6 @@ class LargeZipFile(Exception): ZIP_LZMA = 14 # Other ZIP compression methods not supported -# Options for preserving file permissions upon extraction -PERMS_PRESERVE_NONE = 0 -PERMS_PRESERVE_SAFE = 1 -PERMS_PRESERVE_ALL = 2 - DEFAULT_VERSION = 20 ZIP64_VERSION = 45 BZIP2_VERSION = 46 @@ -346,6 +341,11 @@ def _EndRecData(fpin): # Unable to find a valid end of central directory structure return None +# Options for preserving file permissions upon extraction +class PreserveMode(enum.Enum): + NONE = enum.auto() + SAFE = enum.auto() + ALL = enum.auto() class ZipInfo (object): """Class with attributes describing each file in the ZIP archive.""" @@ -1651,7 +1651,7 @@ def _open_to_write(self, zinfo, force_zip64=False): return _ZipWriteFile(self, zinfo, zip64) def extract(self, member, path=None, pwd=None, - preserve_permissions=PERMS_PRESERVE_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 @@ -1666,7 +1666,7 @@ def extract(self, member, path=None, pwd=None, return self._extract_member(member, path, pwd, preserve_permissions) def extractall(self, path=None, members=None, pwd=None, - preserve_permissions=PERMS_PRESERVE_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 @@ -1700,7 +1700,7 @@ def _sanitize_windows_name(cls, arcname, pathsep): return arcname def _extract_member(self, member, targetpath, pwd, - preserve_permissions=PERMS_PRESERVE_NONE): + preserve_permissions=PreserveMode.NONE): """Extract the ZipInfo object 'member' to a physical file on the path targetpath. """ @@ -1740,13 +1740,13 @@ def _extract_member(self, member, targetpath, pwd, open(targetpath, "wb") as target: shutil.copyfileobj(source, target) - if preserve_permissions == PERMS_PRESERVE_NONE: + if preserve_permissions == PreserveMode.NONE: return targetpath - if preserve_permissions == PERMS_PRESERVE_SAFE: + if preserve_permissions == PreserveMode.SAFE: mode = (member.external_attr >> 16) & 0o777 - if preserve_permissions == PERMS_PRESERVE_ALL: + if preserve_permissions == PreserveMode.ALL: mode = (member.external_attr >> 16) & 0xFFFF os.chmod(targetpath, mode) From 6470201ce606af36ab2f9266bbb95791ea6a735b Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Wed, 13 Apr 2022 11:19:24 +0100 Subject: [PATCH 12/32] Document enumeration type --- Doc/library/zipfile.rst | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index ef7b4f6bcb652c..79bb7f4fa218be 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -120,14 +120,14 @@ The module defines the following items: methods, and may either refuse to process the ZIP file altogether, or fail to extract individual files. -.. data:: PERMS_PRESERVE_NONE +.. data:: PreserveMode.NONE Constant for use in :meth:`extractall` and :meth:`extract` methods. Do not preserve permissions of zipped files. .. versionadded:: 3.11 -.. data:: PERMS_PRESERVE_SAFE +.. data:: PreserveMode.SAFE Constant for use in :meth:`extractall` and :meth:`extract` methods. Preserve safe subset of permissions of the zipped files only: permissions @@ -135,7 +135,7 @@ The module defines the following items: .. versionadded:: 3.11 -.. data:: PERMS_PRESERVE_ALL +.. data:: PreserveMode.ALL Constant for use in :meth:`extractall` and :meth:`extract` methods. Preserve all the permissions of the zipped files, including unsafe ones: @@ -343,7 +343,7 @@ ZipFile Objects .. method:: ZipFile.extract(member, path=None, pwd=None, \ - preserve_permissions=zipfile.PERMS_PRESERVE_NONE) + preserve_permissions=zipfile.PreserveMode.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 @@ -351,10 +351,10 @@ ZipFile Objects to extract to. *member* can be a filename or a :class:`ZipInfo` object. *pwd* is the password used for encrypted files. *preserve_permissions* controls how the permissions of zipped files are preserved. The default is - :data:`PERMS_PRESERVE_NONE` --- do not preserve any permissions. Other + :data:`PreserveMode.NONE` --- do not preserve any permissions. Other options are to preserve a safe subset of permissions - (:data:`PERMS_PRESERVE_SAFE`) or all permissions - (:data:`PERMS_PRESERVE_ALL`). + (:data:`PreserveMode.SAFE`) or all permissions + (:data:`PreserveMode.ALL`). Returns the normalized path created (a directory or new file). @@ -377,16 +377,16 @@ ZipFile Objects .. method:: ZipFile.extractall(path=None, members=None, pwd=None, \ - preserve_permissions=zipfile.PERMS_PRESERVE_NONE) + preserve_permissions=zipfile.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 :meth:`namelist`. *pwd* is the password used for encrypted files. *preserve_permissions* controls how the permissions - of zipped files are preserved. The default is :data:`PERMS_PRESERVE_NONE` + of zipped files are preserved. The default is :data:`PreserveMode.NONE` --- do not preserve any permissions. Other options are to preserve a safe - subset of permissions (:data:`PERMS_PRESERVE_SAFE`) or all permissions - (:data:`PERMS_PRESERVE_ALL`). + subset of permissions (:data:`PreserveMode.SAFE`) or all permissions + (:data:`PreserveMode.ALL`). .. warning:: From 5c8d82b71f4bd0477009265dbc1ee507fe9bc732 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Fri, 22 Apr 2022 19:50:22 +0100 Subject: [PATCH 13/32] Check whether files were created on unix systems before extrating permissions --- Lib/zipfile.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/zipfile.py b/Lib/zipfile.py index 6b955dbdfe63fe..62b52d5441febf 100644 --- a/Lib/zipfile.py +++ b/Lib/zipfile.py @@ -1739,8 +1739,9 @@ def _extract_member(self, member, targetpath, pwd, with self.open(member, pwd=pwd) as source, \ open(targetpath, "wb") as target: shutil.copyfileobj(source, target) - - if preserve_permissions == PreserveMode.NONE: + + # Ignore permissions if the archive was created on Windows + if member.create_system == 0 or preserve_permissions == PreserveMode.NONE: return targetpath if preserve_permissions == PreserveMode.SAFE: From 89da6f2be1ca0ed39164d00d268fbf91e29a8821 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sat, 23 Apr 2022 00:47:26 +0100 Subject: [PATCH 14/32] Remove trailing whitespace --- Lib/zipfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/zipfile.py b/Lib/zipfile.py index 62b52d5441febf..ff6707b82612b1 100644 --- a/Lib/zipfile.py +++ b/Lib/zipfile.py @@ -1739,7 +1739,7 @@ def _extract_member(self, member, targetpath, pwd, with self.open(member, pwd=pwd) as source, \ open(targetpath, "wb") as target: shutil.copyfileobj(source, target) - + # Ignore permissions if the archive was created on Windows if member.create_system == 0 or preserve_permissions == PreserveMode.NONE: return targetpath From 30e552886e3391aa3f54b968cc29cfc11db864fc Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sun, 24 Apr 2022 18:46:14 +0100 Subject: [PATCH 15/32] Check file permissions against the umask --- Lib/test/test_zipfile.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py index 834840c0042a1e..dc27234e62ee44 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -2067,6 +2067,11 @@ def setUp(self): os.mkdir(TESTFNDIR) self.files = [] + # Arbitrarily set the umask to 0 in order to retrieve the current umask + # Then restore the original value for the umask + self.umask = os.umask(0) + os.umask(self.umask) + with zipfile.ZipFile(TESTFN2, 'w', zipfile.ZIP_STORED) as zf: for base_mode in range(0o1000): for special_mask in range(0o10): @@ -2085,7 +2090,7 @@ def test_extractall_preserve_none(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: zf.extractall() for filename, mode in self.files: - expected_mode = 0o644 + expected_mode = 0o666 & ~self.umask self.assertTrue(os.path.exists(filename)) self.assertEqual(os.stat(filename).st_mode, stat.S_IFREG | expected_mode) @@ -2094,7 +2099,7 @@ def test_extract_preserve_none(self): with zipfile.ZipFile(TESTFN2, 'r') as zf: for filename, mode in self.files: zf.extract(filename) - expected_mode = 0o644 + expected_mode = 0o666 & ~self.umask self.assertTrue(os.path.exists(filename)) self.assertEqual(os.stat(filename).st_mode, stat.S_IFREG | expected_mode) From 5a1edac64960b3d6ad126c54ad1d2dfef5c9ea20 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Wed, 27 Apr 2022 19:02:03 +0100 Subject: [PATCH 16/32] Document that permissions are not preserved with archives created on Windows --- Doc/library/zipfile.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index 79bb7f4fa218be..ebcd512d0550d0 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -354,7 +354,9 @@ ZipFile Objects :data:`PreserveMode.NONE` --- do not preserve any permissions. Other options are to preserve a safe subset of permissions (:data:`PreserveMode.SAFE`) or all permissions - (:data:`PreserveMode.ALL`). + (:data:`PreserveMode.ALL`). If the archive was created on Windows, + the *preserve_permissions* argument is ignored and permissions are not + preserved. Returns the normalized path created (a directory or new file). @@ -386,7 +388,8 @@ ZipFile Objects of zipped files are preserved. The default is :data:`PreserveMode.NONE` --- do not preserve any permissions. Other options are to preserve a safe subset of permissions (:data:`PreserveMode.SAFE`) or all permissions - (:data:`PreserveMode.ALL`). + (:data:`PreserveMode.ALL`). If the archive was created on Windows, + the *preserve_pemissions* argument is ignored and permissions are not preserved. .. warning:: From 41a350dbe26b3edbce610e327baa6fb3cd0e4f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric?= Date: Fri, 7 Feb 2025 10:31:48 -0500 Subject: [PATCH 17/32] minor changes --- Doc/library/zipfile.rst | 8 ++++---- Lib/zipfile.py | 7 +++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index 2c0b898a69fa5a..26aa35a7a20b8b 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -125,7 +125,7 @@ The module defines the following items: Constant for use in :meth:`extractall` and :meth:`extract` methods. Do not preserve permissions of zipped files. - .. versionadded:: 3.11 + .. versionadded:: next .. data:: PreserveMode.SAFE @@ -133,7 +133,7 @@ The module defines the following items: Preserve safe subset of permissions of the zipped files only: permissions for reading, writing, execution for user, group and others. - .. versionadded:: 3.11 + .. versionadded:: next .. data:: PreserveMode.ALL @@ -142,7 +142,7 @@ The module defines the following items: UID bit (:data:`stat.S_ISUID`), group UID bit (:data:`stat.S_ISGID`), sticky bit (:data:`stat.S_ISVTX`). - .. versionadded:: 3.11 + .. versionadded:: next .. seealso:: @@ -343,7 +343,7 @@ ZipFile Objects .. method:: ZipFile.extract(member, path=None, pwd=None, \ - preserve_permissions=zipfile.PreserveMode.NONE) + preserve_permissions=PreserveMode.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 diff --git a/Lib/zipfile.py b/Lib/zipfile.py index 4e9016c19157ba..53ab25df3cd6ee 100644 --- a/Lib/zipfile.py +++ b/Lib/zipfile.py @@ -38,8 +38,9 @@ __all__ = ["BadZipFile", "BadZipfile", "error", "ZIP_STORED", "ZIP_DEFLATED", "ZIP_BZIP2", "ZIP_LZMA", - "PreserveMode", "is_zipfile", "ZipInfo", "ZipFile", - "PyZipFile", "LargeZipFile", "Path"] + "PreserveMode", + "is_zipfile", "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile", + "Path"] class BadZipFile(Exception): pass @@ -341,12 +342,14 @@ def _EndRecData(fpin): # Unable to find a valid end of central directory structure return None + # Options for preserving file permissions upon extraction class PreserveMode(enum.Enum): NONE = enum.auto() SAFE = enum.auto() ALL = enum.auto() + class ZipInfo (object): """Class with attributes describing each file in the ZIP archive.""" From c456bb3f11b184da248464a0e67425a78ed28521 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sat, 15 Feb 2025 00:39:35 +0000 Subject: [PATCH 18/32] Qualify PreserveMode as zipfile.PreserveMode in documentation --- Doc/library/zipfile.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index 11dff10a107b93..69fca6da980682 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -371,7 +371,7 @@ ZipFile Objects .. method:: ZipFile.extract(member, path=None, pwd=None, \ - preserve_permissions=PreserveMode.NONE) + preserve_permissions=zipfile.PreserveMode.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 From 5616e0ab5572390506532c8ebd137ba201c92b68 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sat, 15 Feb 2025 00:42:52 +0000 Subject: [PATCH 19/32] Revert PreserveMode -> zipfile.PreserveMode in documentation --- Doc/library/zipfile.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index 69fca6da980682..cc70246b37fdcb 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -371,7 +371,7 @@ ZipFile Objects .. method:: ZipFile.extract(member, path=None, pwd=None, \ - preserve_permissions=zipfile.PreserveMode.NONE) + preserve_permissions=PreserveMode.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 @@ -407,7 +407,7 @@ ZipFile Objects .. method:: ZipFile.extractall(path=None, members=None, pwd=None, \ - preserve_permissions=zipfile.PreserveMode.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 From 0065d2e0294289aa9c961bd06033f422660cb4f8 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sat, 15 Feb 2025 00:44:50 +0000 Subject: [PATCH 20/32] Replace dash with verb phrase in documentation for zipfile --- Doc/library/zipfile.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index cc70246b37fdcb..bab3487c38169e 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -379,7 +379,7 @@ ZipFile Objects to extract to. *member* can be a filename or a :class:`ZipInfo` object. *pwd* is the password used for encrypted files as a :class:`bytes` object. *preserve_permissions* controls how the permissions of zipped files are preserved. - The default is :data:`PreserveMode.NONE` --- do not preserve any permissions. Other + The default is :data:`PreserveMode.NONE` which does not preserve any permissions. Other options are to preserve a safe subset of permissions (:data:`PreserveMode.SAFE`) or all permissions (:data:`PreserveMode.ALL`). If the archive was created on Windows, @@ -414,7 +414,7 @@ ZipFile Objects be a subset of the list returned by :meth:`namelist`. *pwd* is the password used for encrypted files as a :class:`bytes` object. *preserve_permissions* controls how the permissions of zipped files are preserved. The default is - :data:`PreserveMode.NONE` --- do not preserve any permissions. Other options are + :data:`PreserveMode.NONE` which does not preserve any permissions. Other options are to preserve a safe subset of permissions (:data:`PreserveMode.SAFE`) or all permissions (:data:`PreserveMode.ALL`). If the archive was created on Windows, the *preserve_pemissions* argument is ignored and permissions are not preserved. From 4064c327d344fe101c80b33b8a38204057ac6237 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sat, 15 Feb 2025 00:52:17 +0000 Subject: [PATCH 21/32] Add versionchanged note in documentation for preserve_permissions in ZipFile.extract and ZipFile.extractall --- Doc/library/zipfile.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index bab3487c38169e..d73a235d8c8682 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -405,6 +405,9 @@ ZipFile Objects .. versionchanged:: 3.6.2 The *path* parameter accepts a :term:`path-like object`. + .. versionchanged:: next + The *preserve_permissions* parameter was added. + .. method:: ZipFile.extractall(path=None, members=None, pwd=None, \ preserve_permissions=PreserveMode.NONE) @@ -435,6 +438,9 @@ ZipFile Objects .. versionchanged:: 3.6.2 The *path* parameter accepts a :term:`path-like object`. + .. versionchanged:: next + The *preserve_permissions* parameter was added. + .. method:: ZipFile.printdir() From f2c816ad90456f96f9a034cdc001e566373bb547 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sat, 15 Feb 2025 00:54:41 +0000 Subject: [PATCH 22/32] Qualify ZipFile method references in documentation --- Doc/library/zipfile.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index d73a235d8c8682..693302cbdf7ade 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -139,14 +139,14 @@ The module defines the following items: .. data:: PreserveMode.NONE - Constant for use in :meth:`extractall` and :meth:`extract` methods. Do not + Constant for use in :meth:`ZipFile.extractall` and :meth:`ZipFile.extract` methods. Do not preserve permissions of zipped files. .. versionadded:: next .. data:: PreserveMode.SAFE - Constant for use in :meth:`extractall` and :meth:`extract` methods. + Constant for use in :meth:`ZipFile.extractall` and :meth:`ZipFile.extract` methods. Preserve safe subset of permissions of the zipped files only: permissions for reading, writing, execution for user, group and others. @@ -154,7 +154,7 @@ The module defines the following items: .. data:: PreserveMode.ALL - Constant for use in :meth:`extractall` and :meth:`extract` methods. + Constant for use in :meth:`ZipFile.extractall` and :meth:`ZipFile.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`). From 7d72b3b737ac7525cf892b88c962aa9b6eee2094 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sat, 15 Feb 2025 01:02:42 +0000 Subject: [PATCH 23/32] Qualify zipfile.ZipFile.extract in news entry --- .../next/Library/2022-04-13-01-20-21.gh-issue-59999.lfhqku.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2022-04-13-01-20-21.gh-issue-59999.lfhqku.rst b/Misc/NEWS.d/next/Library/2022-04-13-01-20-21.gh-issue-59999.lfhqku.rst index 9cfb833a81d2a1..3b1c24a567d6cc 100644 --- a/Misc/NEWS.d/next/Library/2022-04-13-01-20-21.gh-issue-59999.lfhqku.rst +++ b/Misc/NEWS.d/next/Library/2022-04-13-01-20-21.gh-issue-59999.lfhqku.rst @@ -1 +1 @@ -Add options to preserve file permissions in :meth:`ZipFile.extract` +Add options to preserve file permissions in :meth:`zipfile.ZipFile.extract` and :meth:`zipfile.ZipFile.extractall` From cb2478b80b0abd8621005dab5f2e45d49bd5a0c8 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sat, 15 Feb 2025 01:14:31 +0000 Subject: [PATCH 24/32] Disable tests that require root on wasi --- Lib/test/test_zipfile/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index fb302b41fdc4d9..b7e29c04c80cd0 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -52,7 +52,7 @@ def get_files(test): test.assertFalse(f.closed) def isroot(): - if sys.platform == "win32": + if sys.platform == "win32" or sys.platform == "wasi": return False return os.getuid() == 0 From a08220460cc6a9adc5908b7468359187e89d6352 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sat, 15 Feb 2025 01:24:24 +0000 Subject: [PATCH 25/32] Disable tests that require file permissions on wasi --- Lib/test/test_zipfile/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index b7e29c04c80cd0..30bf0544066e2c 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -2485,7 +2485,7 @@ def tearDown(self): unlink(TESTFN) unlink(TESTFN2) -@unittest.skipIf(sys.platform == "win32", "Requires file permissions") +@unittest.skipIf(sys.platform == "win32" or sys.platform == "wasi", "Requires file permissions") class TestsPermissionExtraction(unittest.TestCase): def setUp(self): os.mkdir(TESTFNDIR) From fbba438cc0815149f155570a225e3be0b1940b9b Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sat, 15 Feb 2025 03:25:39 +0000 Subject: [PATCH 26/32] Move zipfile.PreserveMode description into docstring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Éric --- Lib/zipfile/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index de57c028e3a536..149ea2437aa4e0 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -377,8 +377,8 @@ def _sanitize_filename(filename): return filename -# Options for preserving file permissions upon extraction class PreserveMode(enum.Enum): + """Options for preserving file permissions upon extraction.""" NONE = enum.auto() SAFE = enum.auto() ALL = enum.auto() @@ -1887,7 +1887,7 @@ def _extract_member(self, member, targetpath, pwd, if preserve_permissions == PreserveMode.SAFE: mode = (member.external_attr >> 16) & 0o777 - if preserve_permissions == PreserveMode.ALL: + elif preserve_permissions == PreserveMode.ALL: mode = (member.external_attr >> 16) & 0xFFFF os.chmod(targetpath, mode) From 0d6b86455cbb4610f7251c33e9b0d70cf9aa40af Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sat, 15 Feb 2025 03:26:49 +0000 Subject: [PATCH 27/32] Remove superfluous newline --- Lib/zipfile/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 149ea2437aa4e0..2635a6c2f4f5b6 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -1886,7 +1886,6 @@ def _extract_member(self, member, targetpath, pwd, if preserve_permissions == PreserveMode.SAFE: mode = (member.external_attr >> 16) & 0o777 - elif preserve_permissions == PreserveMode.ALL: mode = (member.external_attr >> 16) & 0xFFFF From b466493e309ebab74292e29fe91267749aaf7971 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sat, 15 Feb 2025 03:27:55 +0000 Subject: [PATCH 28/32] Align method arguments in zipfile documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Éric --- Doc/library/zipfile.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index 693302cbdf7ade..9fdad0305b8b5f 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -410,7 +410,7 @@ ZipFile Objects .. method:: ZipFile.extractall(path=None, members=None, pwd=None, \ - preserve_permissions=PreserveMode.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 From 9e945818627261c3c7bc22d7730df202b70b04a6 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sun, 23 Feb 2025 21:04:37 +0000 Subject: [PATCH 29/32] Separate clause with comma Co-authored-by: Jason R. Coombs --- Doc/library/zipfile.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index 9fdad0305b8b5f..67df25be1d4547 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -379,7 +379,7 @@ ZipFile Objects to extract to. *member* can be a filename or a :class:`ZipInfo` object. *pwd* is the password used for encrypted files as a :class:`bytes` object. *preserve_permissions* controls how the permissions of zipped files are preserved. - The default is :data:`PreserveMode.NONE` which does not preserve any permissions. Other + The default is :data:`PreserveMode.NONE`, which does not preserve any permissions. Other options are to preserve a safe subset of permissions (:data:`PreserveMode.SAFE`) or all permissions (:data:`PreserveMode.ALL`). If the archive was created on Windows, From 500ae2e41357770e14b75a8c2e79ab2cf1606864 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Sun, 23 Feb 2025 21:05:09 +0000 Subject: [PATCH 30/32] Documentation brevity Co-authored-by: Jason R. Coombs --- Doc/library/zipfile.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index 67df25be1d4547..82cea902325d2f 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -382,7 +382,7 @@ ZipFile Objects The default is :data:`PreserveMode.NONE`, which does not preserve any permissions. Other options are to preserve a safe subset of permissions (:data:`PreserveMode.SAFE`) or all permissions - (:data:`PreserveMode.ALL`). If the archive was created on Windows, + (:data:`PreserveMode.ALL`). On Windows, the *preserve_permissions* argument is ignored and permissions are not preserved. From 5ef404a85366ecf2a41bc2a2a263d2f13d177aaf Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Tue, 4 Mar 2025 17:00:28 +0000 Subject: [PATCH 31/32] Deduplicate Zipile path and pwd parameters --- Doc/library/zipfile.rst | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index 82cea902325d2f..d09099539cb495 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -375,16 +375,10 @@ ZipFile Objects 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 as a :class:`bytes` object. - *preserve_permissions* controls how the permissions of zipped files are preserved. - The default is :data:`PreserveMode.NONE`, which does not preserve any permissions. Other - options are to preserve a safe subset of permissions - (:data:`PreserveMode.SAFE`) or all permissions - (:data:`PreserveMode.ALL`). On Windows, - the *preserve_permissions* argument is ignored and permissions are not - preserved. + extracted as accurately as possible. *member* can be a filename or a + :class:`ZipInfo` object. + + *path*, *pwd*, and *preserve_permissions* have the same meaning as for :meth:`extract`. Returns the normalized path created (a directory or new file). @@ -412,16 +406,10 @@ ZipFile Objects .. method:: ZipFile.extractall(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 :meth:`namelist`. *pwd* is the password - used for encrypted files as a :class:`bytes` object. *preserve_permissions* - controls how the permissions of zipped files are preserved. The default is - :data:`PreserveMode.NONE` which does not preserve any permissions. Other options are - to preserve a safe subset of permissions (:data:`PreserveMode.SAFE`) or all - permissions (:data:`PreserveMode.ALL`). If the archive was created on Windows, - the *preserve_pemissions* argument is ignored and permissions are not preserved. + Extract all members from the archive to the current working directory. + *members* is optional and must be a subset of the list returned by :meth:`namelist`. + *path*, *pwd*, and *preserve_permissions* have the same meaning as for :meth:`extract`. .. warning:: From c74c0063d459007eb2a08a753d4b70e8e74cd0c4 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Tue, 4 Mar 2025 17:32:05 +0000 Subject: [PATCH 32/32] Separate permission-setting functionality from zipfile extraction code --- Lib/zipfile/__init__.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 2635a6c2f4f5b6..adffdfa66cc8ce 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -1832,6 +1832,24 @@ def _sanitize_windows_name(cls, arcname, pathsep): arcname = pathsep.join(x for x in arcname if x) return arcname + def _apply_permissions(self, member, path, mode): + """ + Apply ZipFile permissions to a file using + """ + if mode == PreserveMode.NONE: + return + + # Ignore permissions if the archive was created on Windows + if member.create_system == 0: + return + + mask = { + PreserveMode.SAFE: 0o777, + PreserveMode.ALL: 0xFFFF, + } + new_mode = (member.external_attr >> 16) & mask[mode] + os.chmod(path, new_mode) + def _extract_member(self, member, targetpath, pwd, preserve_permissions=PreserveMode.NONE): """Extract the ZipInfo object 'member' to a physical @@ -1880,17 +1898,7 @@ def _extract_member(self, member, targetpath, pwd, open(targetpath, "wb") as target: shutil.copyfileobj(source, target) - # Ignore permissions if the archive was created on Windows - if member.create_system == 0 or preserve_permissions == PreserveMode.NONE: - return targetpath - - if preserve_permissions == PreserveMode.SAFE: - mode = (member.external_attr >> 16) & 0o777 - elif preserve_permissions == PreserveMode.ALL: - mode = (member.external_attr >> 16) & 0xFFFF - - os.chmod(targetpath, mode) - + self._apply_permissions(member, targetpath, preserve_permissions) return targetpath def _writecheck(self, zinfo):