Skip to content

Commit 5e0ef3f

Browse files
miss-islingtonencukousethmlarson
authored
[3.14] gh-151987: Pass filter_function to TarFile._extract_one() during .extract() (GH-151988) (#152609)
(cherry picked from commit 7ccdbab) Co-authored-by: Petr Viktorin <encukou@gmail.com> Co-authored-by: Seth Michael Larson <seth@python.org>
1 parent f5969e7 commit 5e0ef3f

3 files changed

Lines changed: 96 additions & 1 deletion

File tree

Lib/tarfile.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2538,7 +2538,8 @@ def extract(self, member, path="", set_attrs=True, *, numeric_owner=False,
25382538
tarinfo, unfiltered = self._get_extract_tarinfo(
25392539
member, filter_function, path)
25402540
if tarinfo is not None:
2541-
self._extract_one(tarinfo, path, set_attrs, numeric_owner)
2541+
self._extract_one(tarinfo, path, set_attrs, numeric_owner,
2542+
filter_function=filter_function)
25422543

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

Lib/test/test_tarfile.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4470,6 +4470,98 @@ def test_chmod_outside_dir(self):
44704470
st_mode = cc.outerdir.stat().st_mode
44714471
self.assertNotEqual(st_mode & 0o777, 0o777)
44724472

4473+
@symlink_test
4474+
@unittest.skipUnless(hasattr(os, 'chown'), "missing os.chown")
4475+
@unittest.skipUnless(hasattr(os, 'lchown'), "missing os.lchown")
4476+
@unittest.skipUnless(hasattr(os, 'geteuid'), "missing os.geteuid")
4477+
@support.subTests('link_type', (tarfile.SYMTYPE, tarfile.LNKTYPE))
4478+
def test_chown_links_on_extract(self, link_type):
4479+
with ArchiveMaker() as arc:
4480+
arc.add("test.txt",
4481+
uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
4482+
arc.add("link",
4483+
type=link_type,
4484+
linkname='test.txt',
4485+
uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
4486+
4487+
with (
4488+
os_helper.temp_dir() as tmpdir,
4489+
arc.open() as tar,
4490+
unittest.mock.patch("os.chown") as mock_chown,
4491+
unittest.mock.patch("os.lchown") as mock_lchown,
4492+
unittest.mock.patch("os.geteuid") as mock_geteuid,
4493+
):
4494+
# Set UID to 0 so chown() is attempted.
4495+
mock_geteuid.return_value = 0
4496+
tar.extract("link", path=tmpdir, filter='data')
4497+
extract_path = os.path.join(tmpdir, "link")
4498+
4499+
if link_type == tarfile.SYMTYPE:
4500+
mock_chown.assert_not_called()
4501+
mock_lchown.assert_called_once_with(extract_path, -1, -1)
4502+
else:
4503+
mock_chown.assert_has_calls([
4504+
unittest.mock.call(extract_path, -1, -1),
4505+
unittest.mock.call(extract_path, -1, -1)
4506+
])
4507+
mock_lchown.assert_not_called()
4508+
4509+
@symlink_test
4510+
@unittest.skipUnless(hasattr(os, 'chown'), "missing os.chown")
4511+
@unittest.skipUnless(hasattr(os, 'lchown'), "missing os.lchown")
4512+
@unittest.skipUnless(hasattr(os, 'geteuid'), "missing os.geteuid")
4513+
@support.subTests('link_type', (tarfile.SYMTYPE, tarfile.LNKTYPE))
4514+
def test_chown_links_on_extractall(self, link_type):
4515+
with ArchiveMaker() as arc:
4516+
arc.add("test.txt",
4517+
uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
4518+
arc.add("link",
4519+
type=link_type,
4520+
linkname='test.txt',
4521+
uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
4522+
4523+
with (
4524+
os_helper.temp_dir() as tmpdir,
4525+
arc.open() as tar,
4526+
unittest.mock.patch("os.chown") as mock_chown,
4527+
unittest.mock.patch("os.lchown") as mock_lchown,
4528+
unittest.mock.patch("os.geteuid") as mock_geteuid,
4529+
):
4530+
# Set UID to 0 so chown() is attempted.
4531+
mock_geteuid.return_value = 0
4532+
tar.extractall(path=tmpdir, filter='data')
4533+
extract_link_path = os.path.join(tmpdir, "link")
4534+
extract_file_path = os.path.join(tmpdir, "test.txt")
4535+
4536+
if link_type == tarfile.SYMTYPE:
4537+
mock_chown.assert_called_once_with(extract_file_path, -1, -1)
4538+
mock_lchown.assert_called_once_with(extract_link_path, -1, -1)
4539+
else:
4540+
mock_chown.assert_has_calls([
4541+
unittest.mock.call(extract_file_path, -1, -1),
4542+
unittest.mock.call(extract_link_path, -1, -1)
4543+
])
4544+
mock_lchown.assert_not_called()
4545+
4546+
def test_extract_filters_target(self):
4547+
# Test that when extract() falls back to extracting (rather than
4548+
# linking) a hardlink target, it filters the target.
4549+
with ArchiveMaker() as arc:
4550+
arc.add("target")
4551+
arc.add("link", hardlink_to="target")
4552+
def testing_filter(member, path):
4553+
if member.name == 'target':
4554+
# target: set read-only
4555+
return member.replace(mode=stat.S_IRUSR)
4556+
# link: don't overwrite the mode
4557+
return member.replace(mode=None)
4558+
tempdir = pathlib.Path(TEMPDIR) / 'extract'
4559+
with os_helper.temp_dir(tempdir), arc.open() as tar:
4560+
tar.extract("link", path=tempdir, filter=testing_filter)
4561+
path = tempdir / 'link'
4562+
if os_helper.can_chmod():
4563+
self.assertFalse(path.stat().st_mode & stat.S_IWUSR)
4564+
44734565
def test_link_fallback_normalizes(self):
44744566
# Make sure hardlink fallbacks work for non-normalized paths for all
44754567
# 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)