Skip to content

Commit cf23b91

Browse files
miss-islingtonencukousethmlarson
authored
[3.10] gh-151987: Pass filter_function to TarFile._extract_one() during .extract() (GH-151988) (#152613)
* gh-151987: Pass filter_function to TarFile._extract_one() during .extract() (GH-151988) (cherry picked from commit 7ccdbab) Co-authored-by: Petr Viktorin <encukou@gmail.com> Co-authored-by: Seth Michael Larson <seth@python.org> * Remove newer test helpers 3.11+ has more granular platform support, mostly for WASI. Use the 3.10 idioms instead: - @symlink_test is a no-op on non-WASI platforms - can_chmod is approximated by "not win32" elsewhere in test_tarfile --------- Co-authored-by: Petr Viktorin <encukou@gmail.com> Co-authored-by: Seth Michael Larson <seth@python.org>
1 parent c063191 commit cf23b91

3 files changed

Lines changed: 94 additions & 1 deletion

File tree

Lib/tarfile.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2401,7 +2401,8 @@ def extract(self, member, path="", set_attrs=True, *, numeric_owner=False,
24012401
tarinfo, unfiltered = self._get_extract_tarinfo(
24022402
member, filter_function, path)
24032403
if tarinfo is not None:
2404-
self._extract_one(tarinfo, path, set_attrs, numeric_owner)
2404+
self._extract_one(tarinfo, path, set_attrs, numeric_owner,
2405+
filter_function=filter_function)
24052406

24062407
def _get_extract_tarinfo(self, member, filter_function, path):
24072408
"""Get (filtered, unfiltered) TarInfos from *member*

Lib/test/test_tarfile.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4040,6 +4040,96 @@ def test_chmod_outside_dir(self):
40404040
st_mode = cc.outerdir.stat().st_mode
40414041
self.assertNotEqual(st_mode & 0o777, 0o777)
40424042

4043+
@unittest.skipUnless(hasattr(os, 'chown'), "missing os.chown")
4044+
@unittest.skipUnless(hasattr(os, 'lchown'), "missing os.lchown")
4045+
@unittest.skipUnless(hasattr(os, 'geteuid'), "missing os.geteuid")
4046+
@support.subTests('link_type', (tarfile.SYMTYPE, tarfile.LNKTYPE))
4047+
def test_chown_links_on_extract(self, link_type):
4048+
with ArchiveMaker() as arc:
4049+
arc.add("test.txt",
4050+
uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
4051+
arc.add("link",
4052+
type=link_type,
4053+
linkname='test.txt',
4054+
uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
4055+
4056+
with (
4057+
os_helper.temp_dir() as tmpdir,
4058+
arc.open() as tar,
4059+
unittest.mock.patch("os.chown") as mock_chown,
4060+
unittest.mock.patch("os.lchown") as mock_lchown,
4061+
unittest.mock.patch("os.geteuid") as mock_geteuid,
4062+
):
4063+
# Set UID to 0 so chown() is attempted.
4064+
mock_geteuid.return_value = 0
4065+
tar.extract("link", path=tmpdir, filter='data')
4066+
extract_path = os.path.join(tmpdir, "link")
4067+
4068+
if link_type == tarfile.SYMTYPE:
4069+
mock_chown.assert_not_called()
4070+
mock_lchown.assert_called_once_with(extract_path, -1, -1)
4071+
else:
4072+
mock_chown.assert_has_calls([
4073+
unittest.mock.call(extract_path, -1, -1),
4074+
unittest.mock.call(extract_path, -1, -1)
4075+
])
4076+
mock_lchown.assert_not_called()
4077+
4078+
@unittest.skipUnless(hasattr(os, 'chown'), "missing os.chown")
4079+
@unittest.skipUnless(hasattr(os, 'lchown'), "missing os.lchown")
4080+
@unittest.skipUnless(hasattr(os, 'geteuid'), "missing os.geteuid")
4081+
@support.subTests('link_type', (tarfile.SYMTYPE, tarfile.LNKTYPE))
4082+
def test_chown_links_on_extractall(self, link_type):
4083+
with ArchiveMaker() as arc:
4084+
arc.add("test.txt",
4085+
uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
4086+
arc.add("link",
4087+
type=link_type,
4088+
linkname='test.txt',
4089+
uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
4090+
4091+
with (
4092+
os_helper.temp_dir() as tmpdir,
4093+
arc.open() as tar,
4094+
unittest.mock.patch("os.chown") as mock_chown,
4095+
unittest.mock.patch("os.lchown") as mock_lchown,
4096+
unittest.mock.patch("os.geteuid") as mock_geteuid,
4097+
):
4098+
# Set UID to 0 so chown() is attempted.
4099+
mock_geteuid.return_value = 0
4100+
tar.extractall(path=tmpdir, filter='data')
4101+
extract_link_path = os.path.join(tmpdir, "link")
4102+
extract_file_path = os.path.join(tmpdir, "test.txt")
4103+
4104+
if link_type == tarfile.SYMTYPE:
4105+
mock_chown.assert_called_once_with(extract_file_path, -1, -1)
4106+
mock_lchown.assert_called_once_with(extract_link_path, -1, -1)
4107+
else:
4108+
mock_chown.assert_has_calls([
4109+
unittest.mock.call(extract_file_path, -1, -1),
4110+
unittest.mock.call(extract_link_path, -1, -1)
4111+
])
4112+
mock_lchown.assert_not_called()
4113+
4114+
def test_extract_filters_target(self):
4115+
# Test that when extract() falls back to extracting (rather than
4116+
# linking) a hardlink target, it filters the target.
4117+
with ArchiveMaker() as arc:
4118+
arc.add("target")
4119+
arc.add("link", hardlink_to="target")
4120+
def testing_filter(member, path):
4121+
if member.name == 'target':
4122+
# target: set read-only
4123+
return member.replace(mode=stat.S_IRUSR)
4124+
# link: don't overwrite the mode
4125+
return member.replace(mode=None)
4126+
tempdir = pathlib.Path(TEMPDIR) / 'extract'
4127+
with os_helper.temp_dir(tempdir), arc.open() as tar:
4128+
tar.extract("link", path=tempdir, filter=testing_filter)
4129+
path = tempdir / 'link'
4130+
if sys.platform != 'win32':
4131+
self.assertFalse(path.stat().st_mode & stat.S_IWUSR)
4132+
40434133
def test_link_fallback_normalizes(self):
40444134
# Make sure hardlink fallbacks work for non-normalized paths for all
40454135
# filters
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The :meth:`tarfile.TarFile.extract` method now applies the given filter when
2+
it extracts a link target from the archive as a fallback.

0 commit comments

Comments
 (0)