Commit 1b51272b authored by Lars Gustäbel's avatar Lars Gustäbel

Merged revisions 81667 via svnmerge from

svn+ssh://pythondev@svn.python.org/python/trunk

........
  r81667 | lars.gustaebel | 2010-06-03 14:34:14 +0200 (Thu, 03 Jun 2010) | 8 lines

  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.
........
parent 2470ff19
...@@ -2163,8 +2163,7 @@ class TarFile(object): ...@@ -2163,8 +2163,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.
...@@ -2273,27 +2272,21 @@ class TarFile(object): ...@@ -2273,27 +2272,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) as e: except KeyError:
linkpath = linkpath.replace("/", os.sep) raise ExtractError("unable to resolve link inside archive")
try:
shutil.copy2(linkpath, targetpath)
except EnvironmentError as 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.
...@@ -2392,21 +2385,28 @@ class TarFile(object): ...@@ -2392,21 +2385,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)
if normalize:
name = os.path.normpath(name)
for member in reversed(members):
if normalize:
member_name = os.path.normpath(member.name)
else:
member_name = member.name
for i in range(end - 1, -1, -1): if name == member_name:
if name == members[i].name: return member
return members[i]
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
...@@ -2427,6 +2427,25 @@ class TarFile(object): ...@@ -2427,6 +2427,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.
""" """
......
...@@ -133,6 +133,26 @@ class UstarReadTest(ReadTest): ...@@ -133,6 +133,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):
...@@ -1378,6 +1398,29 @@ class ContextManagerTest(unittest.TestCase): ...@@ -1378,6 +1398,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"
...@@ -1463,6 +1506,8 @@ def test_main(): ...@@ -1463,6 +1506,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()
......
This diff was suppressed by a .gitattributes entry.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment