diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index 0d407371f40a0f..9e8c45900c7b81 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -1715,6 +1715,25 @@ def test_extract_all_with_target_pathlike(self): with temp_dir() as extdir: self._test_extract_all_with_target(FakePath(extdir)) + @unittest.skipUnless(hasattr(os, 'chmod'), 'requires os.chmod') + @unittest.skipIf(sys.platform == 'win32', 'Unix permissions only') + def test_extract_preserves_unix_permissions(self): + """_extract_member must apply Unix mode stored in external_attr.""" + info = zipfile.ZipInfo('secret.txt') + info.external_attr = (stat.S_IFREG | 0o600) << 16 + with zipfile.ZipFile(TESTFN2, 'w') as zf: + zf.writestr(info, b'data') + + with temp_dir() as extdir: + with zipfile.ZipFile(TESTFN2, 'r') as zf: + zf.extract('secret.txt', path=extdir) + + extracted = os.path.join(extdir, 'secret.txt') + actual = stat.S_IMODE(os.stat(extracted).st_mode) + self.assertEqual(actual, 0o600, f'expected 0o600, got 0o{actual:03o}') + + unlink(TESTFN2) + def check_file(self, filename, content): self.assertTrue(os.path.isfile(filename)) with open(filename, 'rb') as f: diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 86c3bc36b695c7..05dab0dfad6b5b 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -1933,12 +1933,20 @@ def _extract_member(self, member, targetpath, pwd): except FileExistsError: if not os.path.isdir(targetpath): raise + unix_mode = member.external_attr >> 16 + if unix_mode: + os.chmod(targetpath, unix_mode) return targetpath with self.open(member, pwd=pwd) as source, \ open(targetpath, "wb") as target: shutil.copyfileobj(source, target) + # Restore Unix permissions stored in the upper 16 bits of external_attr. + unix_mode = member.external_attr >> 16 + if unix_mode: + os.chmod(targetpath, unix_mode) + return targetpath def _writecheck(self, zinfo):