Skip to content

Commit 7ccdbab

Browse files
encukousethmlarson
andauthored
gh-151987: Pass filter_function to TarFile._extract_one() during .extract() (GH-151988)
Co-authored-by: Seth Michael Larson <seth@python.org>
1 parent be4eebb commit 7ccdbab

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
@@ -2534,7 +2534,8 @@ def extract(self, member, path="", set_attrs=True, *, numeric_owner=False,
25342534
tarinfo, unfiltered = self._get_extract_tarinfo(
25352535
member, filter_function, path)
25362536
if tarinfo is not None:
2537-
self._extract_one(tarinfo, path, set_attrs, numeric_owner)
2537+
self._extract_one(tarinfo, path, set_attrs, numeric_owner,
2538+
filter_function=filter_function)
25382539

25392540
def _get_extract_tarinfo(self, member, filter_function, path):
25402541
"""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
@@ -4539,6 +4539,98 @@ def test_chmod_outside_dir(self):
45394539
st_mode = cc.outerdir.stat().st_mode
45404540
self.assertNotEqual(st_mode & 0o777, 0o777)
45414541

4542+
@symlink_test
4543+
@unittest.skipUnless(hasattr(os, 'chown'), "missing os.chown")
4544+
@unittest.skipUnless(hasattr(os, 'lchown'), "missing os.lchown")
4545+
@unittest.skipUnless(hasattr(os, 'geteuid'), "missing os.geteuid")
4546+
@support.subTests('link_type', (tarfile.SYMTYPE, tarfile.LNKTYPE))
4547+
def test_chown_links_on_extract(self, link_type):
4548+
with ArchiveMaker() as arc:
4549+
arc.add("test.txt",
4550+
uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
4551+
arc.add("link",
4552+
type=link_type,
4553+
linkname='test.txt',
4554+
uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
4555+
4556+
with (
4557+
os_helper.temp_dir() as tmpdir,
4558+
arc.open() as tar,
4559+
unittest.mock.patch("os.chown") as mock_chown,
4560+
unittest.mock.patch("os.lchown") as mock_lchown,
4561+
unittest.mock.patch("os.geteuid") as mock_geteuid,
4562+
):
4563+
# Set UID to 0 so chown() is attempted.
4564+
mock_geteuid.return_value = 0
4565+
tar.extract("link", path=tmpdir, filter='data')
4566+
extract_path = os.path.join(tmpdir, "link")
4567+
4568+
if link_type == tarfile.SYMTYPE:
4569+
mock_chown.assert_not_called()
4570+
mock_lchown.assert_called_once_with(extract_path, -1, -1)
4571+
else:
4572+
mock_chown.assert_has_calls([
4573+
unittest.mock.call(extract_path, -1, -1),
4574+
unittest.mock.call(extract_path, -1, -1)
4575+
])
4576+
mock_lchown.assert_not_called()
4577+
4578+
@symlink_test
4579+
@unittest.skipUnless(hasattr(os, 'chown'), "missing os.chown")
4580+
@unittest.skipUnless(hasattr(os, 'lchown'), "missing os.lchown")
4581+
@unittest.skipUnless(hasattr(os, 'geteuid'), "missing os.geteuid")
4582+
@support.subTests('link_type', (tarfile.SYMTYPE, tarfile.LNKTYPE))
4583+
def test_chown_links_on_extractall(self, link_type):
4584+
with ArchiveMaker() as arc:
4585+
arc.add("test.txt",
4586+
uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
4587+
arc.add("link",
4588+
type=link_type,
4589+
linkname='test.txt',
4590+
uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
4591+
4592+
with (
4593+
os_helper.temp_dir() as tmpdir,
4594+
arc.open() as tar,
4595+
unittest.mock.patch("os.chown") as mock_chown,
4596+
unittest.mock.patch("os.lchown") as mock_lchown,
4597+
unittest.mock.patch("os.geteuid") as mock_geteuid,
4598+
):
4599+
# Set UID to 0 so chown() is attempted.
4600+
mock_geteuid.return_value = 0
4601+
tar.extractall(path=tmpdir, filter='data')
4602+
extract_link_path = os.path.join(tmpdir, "link")
4603+
extract_file_path = os.path.join(tmpdir, "test.txt")
4604+
4605+
if link_type == tarfile.SYMTYPE:
4606+
mock_chown.assert_called_once_with(extract_file_path, -1, -1)
4607+
mock_lchown.assert_called_once_with(extract_link_path, -1, -1)
4608+
else:
4609+
mock_chown.assert_has_calls([
4610+
unittest.mock.call(extract_file_path, -1, -1),
4611+
unittest.mock.call(extract_link_path, -1, -1)
4612+
])
4613+
mock_lchown.assert_not_called()
4614+
4615+
def test_extract_filters_target(self):
4616+
# Test that when extract() falls back to extracting (rather than
4617+
# linking) a hardlink target, it filters the target.
4618+
with ArchiveMaker() as arc:
4619+
arc.add("target")
4620+
arc.add("link", hardlink_to="target")
4621+
def testing_filter(member, path):
4622+
if member.name == 'target':
4623+
# target: set read-only
4624+
return member.replace(mode=stat.S_IRUSR)
4625+
# link: don't overwrite the mode
4626+
return member.replace(mode=None)
4627+
tempdir = pathlib.Path(TEMPDIR) / 'extract'
4628+
with os_helper.temp_dir(tempdir), arc.open() as tar:
4629+
tar.extract("link", path=tempdir, filter=testing_filter)
4630+
path = tempdir / 'link'
4631+
if os_helper.can_chmod():
4632+
self.assertFalse(path.stat().st_mode & stat.S_IWUSR)
4633+
45424634
def test_link_fallback_normalizes(self):
45434635
# Make sure hardlink fallbacks work for non-normalized paths for all
45444636
# 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)