mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
Issue #8741: Fixed the TarFile.makelink() method that is responsible
for extracting symbolic and hard link entries as regular files as a work-around on platforms that do not support filesystem links. This stopped working reliably after a change in r74571. I also added a few tests for this functionality.
This commit is contained in:
parent
2ee9c6fa50
commit
4da7d410b3
3 changed files with 90 additions and 26 deletions
|
@ -2127,8 +2127,7 @@ class TarFile(object):
|
||||||
raise StreamError("cannot extract (sym)link as file object")
|
raise StreamError("cannot extract (sym)link as file object")
|
||||||
else:
|
else:
|
||||||
# A (sym)link's file object is its target's file object.
|
# A (sym)link's file object is its target's file object.
|
||||||
return self.extractfile(self._getmember(tarinfo.linkname,
|
return self.extractfile(self._find_link_target(tarinfo))
|
||||||
tarinfo))
|
|
||||||
else:
|
else:
|
||||||
# If there's no data associated with the member (directory, chrdev,
|
# If there's no data associated with the member (directory, chrdev,
|
||||||
# blkdev, etc.), return None instead of a file object.
|
# blkdev, etc.), return None instead of a file object.
|
||||||
|
@ -2237,27 +2236,21 @@ class TarFile(object):
|
||||||
(platform limitation), we try to make a copy of the referenced file
|
(platform limitation), we try to make a copy of the referenced file
|
||||||
instead of a link.
|
instead of a link.
|
||||||
"""
|
"""
|
||||||
try:
|
if hasattr(os, "symlink") and hasattr(os, "link"):
|
||||||
|
# For systems that support symbolic and hard links.
|
||||||
if tarinfo.issym():
|
if tarinfo.issym():
|
||||||
os.symlink(tarinfo.linkname, targetpath)
|
os.symlink(tarinfo.linkname, targetpath)
|
||||||
else:
|
else:
|
||||||
# See extract().
|
# See extract().
|
||||||
os.link(tarinfo._link_target, targetpath)
|
if os.path.exists(tarinfo._link_target):
|
||||||
except AttributeError:
|
os.link(tarinfo._link_target, targetpath)
|
||||||
if tarinfo.issym():
|
else:
|
||||||
linkpath = os.path.dirname(tarinfo.name) + "/" + \
|
self._extract_member(self._find_link_target(tarinfo), targetpath)
|
||||||
tarinfo.linkname
|
else:
|
||||||
else:
|
|
||||||
linkpath = tarinfo.linkname
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._extract_member(self.getmember(linkpath), targetpath)
|
self._extract_member(self._find_link_target(tarinfo), targetpath)
|
||||||
except (EnvironmentError, KeyError), e:
|
except KeyError:
|
||||||
linkpath = linkpath.replace("/", os.sep)
|
raise ExtractError("unable to resolve link inside archive")
|
||||||
try:
|
|
||||||
shutil.copy2(linkpath, targetpath)
|
|
||||||
except EnvironmentError, e:
|
|
||||||
raise IOError("link could not be created")
|
|
||||||
|
|
||||||
def chown(self, tarinfo, targetpath):
|
def chown(self, tarinfo, targetpath):
|
||||||
"""Set owner of targetpath according to tarinfo.
|
"""Set owner of targetpath according to tarinfo.
|
||||||
|
@ -2356,21 +2349,28 @@ class TarFile(object):
|
||||||
#--------------------------------------------------------------------------
|
#--------------------------------------------------------------------------
|
||||||
# Little helper methods:
|
# Little helper methods:
|
||||||
|
|
||||||
def _getmember(self, name, tarinfo=None):
|
def _getmember(self, name, tarinfo=None, normalize=False):
|
||||||
"""Find an archive member by name from bottom to top.
|
"""Find an archive member by name from bottom to top.
|
||||||
If tarinfo is given, it is used as the starting point.
|
If tarinfo is given, it is used as the starting point.
|
||||||
"""
|
"""
|
||||||
# Ensure that all members have been loaded.
|
# Ensure that all members have been loaded.
|
||||||
members = self.getmembers()
|
members = self.getmembers()
|
||||||
|
|
||||||
if tarinfo is None:
|
# Limit the member search list up to tarinfo.
|
||||||
end = len(members)
|
if tarinfo is not None:
|
||||||
else:
|
members = members[:members.index(tarinfo)]
|
||||||
end = members.index(tarinfo)
|
|
||||||
|
|
||||||
for i in xrange(end - 1, -1, -1):
|
if normalize:
|
||||||
if name == members[i].name:
|
name = os.path.normpath(name)
|
||||||
return members[i]
|
|
||||||
|
for member in reversed(members):
|
||||||
|
if normalize:
|
||||||
|
member_name = os.path.normpath(member.name)
|
||||||
|
else:
|
||||||
|
member_name = member.name
|
||||||
|
|
||||||
|
if name == member_name:
|
||||||
|
return member
|
||||||
|
|
||||||
def _load(self):
|
def _load(self):
|
||||||
"""Read through the entire archive file and look for readable
|
"""Read through the entire archive file and look for readable
|
||||||
|
@ -2391,6 +2391,25 @@ class TarFile(object):
|
||||||
if mode is not None and self.mode not in mode:
|
if mode is not None and self.mode not in mode:
|
||||||
raise IOError("bad operation for mode %r" % self.mode)
|
raise IOError("bad operation for mode %r" % self.mode)
|
||||||
|
|
||||||
|
def _find_link_target(self, tarinfo):
|
||||||
|
"""Find the target member of a symlink or hardlink member in the
|
||||||
|
archive.
|
||||||
|
"""
|
||||||
|
if tarinfo.issym():
|
||||||
|
# Always search the entire archive.
|
||||||
|
linkname = os.path.dirname(tarinfo.name) + "/" + tarinfo.linkname
|
||||||
|
limit = None
|
||||||
|
else:
|
||||||
|
# Search the archive before the link, because a hard link is
|
||||||
|
# just a reference to an already archived file.
|
||||||
|
linkname = tarinfo.linkname
|
||||||
|
limit = tarinfo
|
||||||
|
|
||||||
|
member = self._getmember(linkname, tarinfo=limit, normalize=True)
|
||||||
|
if member is None:
|
||||||
|
raise KeyError("linkname %r not found" % linkname)
|
||||||
|
return member
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
"""Provide an iterator object.
|
"""Provide an iterator object.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -134,6 +134,26 @@ class UstarReadTest(ReadTest):
|
||||||
"read() after readline() failed")
|
"read() after readline() failed")
|
||||||
fobj.close()
|
fobj.close()
|
||||||
|
|
||||||
|
# Test if symbolic and hard links are resolved by extractfile(). The
|
||||||
|
# test link members each point to a regular member whose data is
|
||||||
|
# supposed to be exported.
|
||||||
|
def _test_fileobj_link(self, lnktype, regtype):
|
||||||
|
a = self.tar.extractfile(lnktype)
|
||||||
|
b = self.tar.extractfile(regtype)
|
||||||
|
self.assertEqual(a.name, b.name)
|
||||||
|
|
||||||
|
def test_fileobj_link1(self):
|
||||||
|
self._test_fileobj_link("ustar/lnktype", "ustar/regtype")
|
||||||
|
|
||||||
|
def test_fileobj_link2(self):
|
||||||
|
self._test_fileobj_link("./ustar/linktest2/lnktype", "ustar/linktest1/regtype")
|
||||||
|
|
||||||
|
def test_fileobj_symlink1(self):
|
||||||
|
self._test_fileobj_link("ustar/symtype", "ustar/regtype")
|
||||||
|
|
||||||
|
def test_fileobj_symlink2(self):
|
||||||
|
self._test_fileobj_link("./ustar/linktest2/symtype", "ustar/linktest1/regtype")
|
||||||
|
|
||||||
|
|
||||||
class CommonReadTest(ReadTest):
|
class CommonReadTest(ReadTest):
|
||||||
|
|
||||||
|
@ -1376,6 +1396,29 @@ class ContextManagerTest(unittest.TestCase):
|
||||||
fobj.close()
|
fobj.close()
|
||||||
|
|
||||||
|
|
||||||
|
class LinkEmulationTest(ReadTest):
|
||||||
|
|
||||||
|
# Test for issue #8741 regression. On platforms that do not support
|
||||||
|
# symbolic or hard links tarfile tries to extract these types of members as
|
||||||
|
# the regular files they point to.
|
||||||
|
def _test_link_extraction(self, name):
|
||||||
|
self.tar.extract(name, TEMPDIR)
|
||||||
|
data = open(os.path.join(TEMPDIR, name), "rb").read()
|
||||||
|
self.assertEqual(md5sum(data), md5_regtype)
|
||||||
|
|
||||||
|
def test_hardlink_extraction1(self):
|
||||||
|
self._test_link_extraction("ustar/lnktype")
|
||||||
|
|
||||||
|
def test_hardlink_extraction2(self):
|
||||||
|
self._test_link_extraction("./ustar/linktest2/lnktype")
|
||||||
|
|
||||||
|
def test_symlink_extraction1(self):
|
||||||
|
self._test_link_extraction("ustar/symtype")
|
||||||
|
|
||||||
|
def test_symlink_extraction2(self):
|
||||||
|
self._test_link_extraction("./ustar/linktest2/symtype")
|
||||||
|
|
||||||
|
|
||||||
class GzipMiscReadTest(MiscReadTest):
|
class GzipMiscReadTest(MiscReadTest):
|
||||||
tarname = gzipname
|
tarname = gzipname
|
||||||
mode = "r:gz"
|
mode = "r:gz"
|
||||||
|
@ -1460,6 +1503,8 @@ def test_main():
|
||||||
|
|
||||||
if hasattr(os, "link"):
|
if hasattr(os, "link"):
|
||||||
tests.append(HardlinkTest)
|
tests.append(HardlinkTest)
|
||||||
|
else:
|
||||||
|
tests.append(LinkEmulationTest)
|
||||||
|
|
||||||
fobj = open(tarname, "rb")
|
fobj = open(tarname, "rb")
|
||||||
data = fobj.read()
|
data = fobj.read()
|
||||||
|
|
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue