Skip to content

Commit b73c4d4

Browse files
leameowjsirois
andauthored
Extract zip files with current default fs permission. (#3119)
However add the executable bits if the zipped file had it. This match pip behavior and fix #3118. --------- Co-authored-by: John Sirois <john.sirois@gmail.com>
1 parent f8496e6 commit b73c4d4

File tree

5 files changed

+136
-25
lines changed

5 files changed

+136
-25
lines changed

CHANGES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Release Notes
22

3+
## 2.91.3
4+
5+
This release fixes extraction of wheels containing entries with bad permissions. Instead of
6+
preserving zip entry permissions, just the executable bit is preserved for file entries.
7+
8+
* Extract zip files with current default fs permission. (#3119)
9+
310
## 2.91.2
411

512
This release fixes hermeticity of the Pex boot code against the Python stdlib itself. In some corner

pex/common.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -309,9 +309,9 @@ def from_path(cls, path):
309309
class Value(_ZipFileTypeValue):
310310
pass
311311

312-
DIRECTORY = Value("directory", 0o755)
313-
EXECUTABLE = Value("executable", 0o755)
314-
FILE = Value("file", 0o644)
312+
DIRECTORY = Value("directory", stat.S_IFDIR | 0o755)
313+
EXECUTABLE = Value("executable", stat.S_IFREG | 0o755)
314+
FILE = Value("file", stat.S_IFREG | 0o644)
315315

316316

317317
ZipFileType.seal()
@@ -449,7 +449,9 @@ def _chmod(
449449
# https://www.forensicswiki.org/wiki/ZIP#External_file_attributes
450450
if info.external_attr > 0xFFFF:
451451
attr = info.external_attr >> 16
452-
os.chmod(path, attr)
452+
# If the archive file was executable, we set the extracted file as executable.
453+
if stat.S_ISREG(attr) and attr & 0o111:
454+
chmod_plus_x(path)
453455

454456
# Python 3 also takes PathLike[str] for the path arg, but we only ever pass str since we support
455457
# Python 2.7 and don't use pathlib as a result.

pex/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# Copyright 2015 Pex project contributors.
22
# Licensed under the Apache License, Version 2.0 (see LICENSE).
33

4-
__version__ = "2.91.2"
4+
__version__ = "2.91.3"
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Copyright 2026 Pex project contributors.
2+
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3+
4+
from __future__ import absolute_import
5+
6+
import subprocess
7+
import sys
8+
9+
import pytest
10+
11+
from pex.compatibility import commonpath
12+
from testing import run_pex_command
13+
from testing.pytest_utils.tmp import Tempdir
14+
15+
16+
@pytest.mark.skipif(
17+
sys.version_info < (3, 9),
18+
reason="The ag-ui-protocol 0.1.14 distribution under test requires Python >=3.9.",
19+
)
20+
def test_bad_perms_ignored(tmpdir):
21+
# type: (Tempdir) -> None
22+
23+
pex_root = tmpdir.join("pex-root")
24+
pex = tmpdir.join("pex")
25+
run_pex_command(
26+
args=[
27+
"--pex-root",
28+
pex_root,
29+
"--runtime-pex-root",
30+
pex_root,
31+
"ag-ui-protocol==0.1.14",
32+
"--intransitive",
33+
"--ignore-errors",
34+
"-o",
35+
pex,
36+
]
37+
).assert_success()
38+
39+
assert pex_root == commonpath(
40+
(
41+
pex_root,
42+
subprocess.check_output(args=[pex, "-c", "import ag_ui; print(ag_ui.__file__)"])
43+
.decode("utf-8")
44+
.strip(),
45+
)
46+
)

tests/test_common.py

Lines changed: 76 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,25 @@
44
import contextlib
55
import errno
66
import os
7+
import stat
78

89
import pytest
910

1011
from pex.common import (
1112
Chroot,
1213
ZipFileEx,
14+
ZipFileType,
1315
deterministic_walk,
1416
open_zip,
17+
safe_mkdir,
1518
safe_open,
1619
temporary_dir,
1720
touch,
1821
)
1922
from pex.executables import chmod_plus_x
2023
from pex.typing import TYPE_CHECKING
2124
from testing import NonDeterministicWalk
25+
from testing.pytest_utils.tmp import Tempdir
2226

2327
try:
2428
from unittest import mock
@@ -29,51 +33,103 @@
2933
from typing import Iterator, Tuple
3034

3135

36+
def test_zip_file_type_mode(tmpdir):
37+
# type: (Tempdir) -> None
38+
39+
directory = safe_mkdir(tmpdir.join("dir"))
40+
zip_dir = ZipFileType.from_path(directory)
41+
assert stat.S_ISDIR(zip_dir.deterministic_mode)
42+
assert 0o755 == 0o755 & zip_dir.deterministic_mode
43+
44+
regular_file = touch(tmpdir.join("file"))
45+
zip_reg_file = ZipFileType.from_path(regular_file)
46+
assert stat.S_ISREG(zip_reg_file.deterministic_mode)
47+
assert 0o644 == 0o644 & zip_reg_file.deterministic_mode
48+
49+
executable_file = touch(tmpdir.join("exe"))
50+
chmod_plus_x(executable_file)
51+
zip_exe_file = ZipFileType.from_path(executable_file)
52+
assert stat.S_ISREG(zip_exe_file.deterministic_mode)
53+
assert 0o755 == 0o755 & zip_exe_file.deterministic_mode
54+
55+
3256
def extract_perms(path):
3357
# type: (str) -> str
3458
return oct(os.stat(path).st_mode)
3559

3660

3761
@contextlib.contextmanager
3862
def zip_fixture():
39-
# type: () -> Iterator[Tuple[str, str, str, str]]
63+
# type: () -> Iterator[Tuple[str, str]]
4064
with temporary_dir() as target_dir:
41-
one = os.path.join(target_dir, "one")
42-
touch(one)
65+
no_x = os.path.join(target_dir, "no-x")
66+
touch(no_x)
4367

44-
two = os.path.join(target_dir, "two")
45-
touch(two)
46-
chmod_plus_x(two)
68+
no_w = os.path.join(target_dir, "no-w")
69+
touch(no_w)
70+
os.chmod(no_w, 0o444)
71+
72+
with_x = os.path.join(target_dir, "with-x")
73+
touch(with_x)
74+
chmod_plus_x(with_x)
4775

48-
assert extract_perms(one) != extract_perms(two)
76+
assert extract_perms(no_x) != extract_perms(no_w) != extract_perms(with_x)
4977

5078
zip_file = os.path.join(target_dir, "test.zip")
5179
with contextlib.closing(ZipFileEx(zip_file, "w")) as zf:
52-
zf.write(one, "one")
53-
zf.write(two, "two")
80+
zf.write(no_x, "no-x")
81+
zf.write(no_w, "no-w")
82+
zf.write(with_x, "with-x")
5483

55-
yield zip_file, os.path.join(target_dir, "extract"), one, two
84+
yield zip_file, os.path.join(target_dir, "extract")
85+
86+
87+
def is_writeable(path):
88+
# type: (str) -> bool
89+
return bool(os.stat(path).st_mode & 0o222)
90+
91+
92+
def is_executable(path):
93+
# type: (str) -> bool
94+
return bool(os.stat(path).st_mode & 0o111)
5695

5796

5897
def test_perm_preserving_zipfile_extractall():
5998
# type: () -> None
60-
with zip_fixture() as (zip_file, extract_dir, one, two):
99+
with zip_fixture() as (zip_file, extract_dir):
61100
with contextlib.closing(ZipFileEx(zip_file)) as zf:
62101
zf.extractall(extract_dir)
63102

64-
assert extract_perms(one) == extract_perms(os.path.join(extract_dir, "one"))
65-
assert extract_perms(two) == extract_perms(os.path.join(extract_dir, "two"))
103+
no_x = os.path.join(extract_dir, "no-x")
104+
no_w = os.path.join(extract_dir, "no-w")
105+
with_x = os.path.join(extract_dir, "with-x")
106+
107+
assert not is_executable(no_x)
108+
assert not is_executable(no_w)
109+
assert is_executable(with_x)
110+
111+
files = [no_x, no_w, with_x]
112+
assert all(is_writeable(f) for f in files)
66113

67114

68115
def test_perm_preserving_zipfile_extract():
69116
# type: () -> None
70-
with zip_fixture() as (zip_file, extract_dir, one, two):
117+
with zip_fixture() as (zip_file, extract_dir):
71118
with contextlib.closing(ZipFileEx(zip_file)) as zf:
72-
zf.extract("one", path=extract_dir)
73-
zf.extract("two", path=extract_dir)
119+
zf.extract("no-x", path=extract_dir)
120+
zf.extract("no-w", path=extract_dir)
121+
zf.extract("with-x", path=extract_dir)
122+
123+
no_x = os.path.join(extract_dir, "no-x")
124+
no_w = os.path.join(extract_dir, "no-w")
125+
with_x = os.path.join(extract_dir, "with-x")
126+
127+
assert not is_executable(no_x)
128+
assert not is_executable(no_w)
129+
assert is_executable(with_x)
74130

75-
assert extract_perms(one) == extract_perms(os.path.join(extract_dir, "one"))
76-
assert extract_perms(two) == extract_perms(os.path.join(extract_dir, "two"))
131+
files = [no_x, no_w, with_x]
132+
assert all(is_writeable(f) for f in files)
77133

78134

79135
def assert_chroot_perms(copyfn):
@@ -98,8 +154,8 @@ def assert_chroot_perms(copyfn):
98154
with contextlib.closing(ZipFileEx(zip_path)) as zf:
99155
zf.extractall(extract_dir)
100156

101-
assert extract_perms(one) == extract_perms(os.path.join(extract_dir, "one"))
102-
assert extract_perms(two) == extract_perms(os.path.join(extract_dir, "two"))
157+
assert not is_executable(os.path.join(extract_dir, "one"))
158+
assert is_executable(os.path.join(extract_dir, "two"))
103159

104160

105161
def test_chroot_perms_copy():

0 commit comments

Comments
 (0)