Skip to content

Commit 69438a6

Browse files
committed
gh-75989: tarfile.extractall fails to overwrite symlinks
1 parent fe0e921 commit 69438a6

File tree

3 files changed

+56
-0
lines changed

3 files changed

+56
-0
lines changed

Lib/tarfile.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2723,6 +2723,9 @@ def makelink_with_filter(self, tarinfo, targetpath,
27232723
return
27242724
else:
27252725
if os.path.exists(tarinfo._link_target):
2726+
if os.path.lexists(targetpath):
2727+
# Avoid FileExistsError on following os.link.
2728+
os.unlink(targetpath)
27262729
os.link(tarinfo._link_target, targetpath)
27272730
return
27282731
except symlink_exception:

Lib/test/test_tarfile.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,57 @@ def test_next_on_empty_tarfile(self):
841841
with tarfile.open(fileobj=fd, mode="r") as tf:
842842
self.assertEqual(tf.next(), None)
843843

844+
def _setup_symlink_to_target(self, temp_dirpath):
845+
target_filepath = os.path.join(temp_dirpath, "target")
846+
ustar_dirpath = os.path.join(temp_dirpath, "ustar")
847+
hardlink_filepath = os.path.join(ustar_dirpath, "lnktype")
848+
with open(target_filepath, "wb") as f:
849+
f.write(b"target")
850+
os.makedirs(ustar_dirpath)
851+
os.symlink(target_filepath, hardlink_filepath)
852+
return target_filepath, hardlink_filepath
853+
854+
def _assert_on_file_content(self, filepath, digest):
855+
with open(filepath, "rb") as f:
856+
data = f.read()
857+
self.assertEqual(sha256sum(data), digest)
858+
859+
@unittest.skipUnless(
860+
hasattr(os, "link"), "Missing hardlink implementation"
861+
)
862+
@os_helper.skip_unless_symlink
863+
def test_extract_hardlink_on_symlink(self):
864+
"""
865+
This test verifies that extracting a hardlink will not follow an
866+
existing symlink after a FileExistsError on os.link.
867+
"""
868+
with os_helper.temp_dir() as DIR:
869+
target_filepath, hardlink_filepath = self._setup_symlink_to_target(DIR)
870+
with tarfile.open(tarname, encoding="iso8859-1") as tar:
871+
tar.extract("ustar/regtype", DIR, filter="data")
872+
tar.extract("ustar/lnktype", DIR, filter="data")
873+
self._assert_on_file_content(target_filepath, sha256sum(b"target"))
874+
self._assert_on_file_content(hardlink_filepath, sha256_regtype)
875+
876+
@unittest.skipUnless(
877+
hasattr(os, "link"), "Missing hardlink implementation"
878+
)
879+
@os_helper.skip_unless_symlink
880+
def test_extractall_hardlink_on_symlink(self):
881+
"""
882+
This test verifies that extracting a hardlink will not follow an
883+
existing symlink after a FileExistsError on os.link.
884+
"""
885+
with os_helper.temp_dir() as DIR:
886+
target_filepath, hardlink_filepath = self._setup_symlink_to_target(DIR)
887+
with tarfile.open(tarname, encoding="iso8859-1") as tar:
888+
tar.extractall(
889+
DIR, members=["ustar/regtype", "ustar/lnktype"], filter="data",
890+
)
891+
self._assert_on_file_content(target_filepath, sha256sum(b"target"))
892+
self._assert_on_file_content(hardlink_filepath, sha256_regtype)
893+
894+
844895
class MiscReadTest(MiscReadTestBase, unittest.TestCase):
845896
test_fail_comp = None
846897

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:func:`tarfile.TarFile.extractall` and :func:`tarfile.TarFile.extract` now
2+
overwrite symlinks when extracting hardlinks.

0 commit comments

Comments
 (0)