Issue #19887: Improve the Path.resolve() algorithm to support certain symlink chains.

Original patch by Serhiy.
This commit is contained in:
Antoine Pitrou 2013-12-16 19:57:41 +01:00
parent d2e48ca813
commit c274fd22ed
3 changed files with 96 additions and 35 deletions

View file

@ -254,42 +254,47 @@ class _PosixFlavour(_Flavour):
def resolve(self, path): def resolve(self, path):
sep = self.sep sep = self.sep
def split(p):
return [x for x in p.split(sep) if x]
def absparts(p):
# Our own abspath(), since the posixpath one makes
# the mistake of "normalizing" the path without resolving the
# symlinks first.
if not p.startswith(sep):
return split(os.getcwd()) + split(p)
else:
return split(p)
parts = absparts(str(path))[::-1]
accessor = path._accessor accessor = path._accessor
resolved = cur = "" seen = {}
symlinks = {} def _resolve(path, rest):
while parts: if rest.startswith(sep):
part = parts.pop() path = ''
cur = resolved + sep + part
if cur in symlinks and symlinks[cur] <= len(parts): for name in rest.split(sep):
# We've already seen the symlink and there's not less if not name or name == '.':
# work to do than the last time. # current dir
raise RuntimeError("Symlink loop from %r" % cur) continue
if name == '..':
# parent dir
path, _, _ = path.rpartition(sep)
continue
newpath = path + sep + name
if newpath in seen:
# Already seen this path
path = seen[newpath]
if path is not None:
# use cached value
continue
# The symlink is not resolved, so we must have a symlink loop.
raise RuntimeError("Symlink loop from %r" % newpath)
# Resolve the symbolic link
try: try:
target = accessor.readlink(cur) target = accessor.readlink(newpath)
except OSError as e: except OSError as e:
if e.errno != EINVAL: if e.errno != EINVAL:
raise raise
# Not a symlink # Not a symlink
resolved = cur path = newpath
else: else:
# Take note of remaining work from this symlink seen[newpath] = None # not resolved symlink
symlinks[cur] = len(parts) path = _resolve(path, target)
if target.startswith(sep): seen[newpath] = path # resolved symlink
# Symlink points to absolute path
resolved = "" return path
parts.extend(split(target)[::-1]) # NOTE: according to POSIX, getcwd() cannot contain path components
return resolved or sep # which are symlinks.
base = '' if path.is_absolute() else os.getcwd()
return _resolve(base, str(path)) or sep
def is_reserved(self, parts): def is_reserved(self, parts):
return False return False

View file

@ -1620,6 +1620,59 @@ class _BasePathTest(object):
# 'bin' # 'bin'
self.assertIs(p.parts[2], q.parts[3]) self.assertIs(p.parts[2], q.parts[3])
def _check_complex_symlinks(self, link0_target):
# Test solving a non-looping chain of symlinks (issue #19887)
P = self.cls(BASE)
self.dirlink(os.path.join('link0', 'link0'), join('link1'))
self.dirlink(os.path.join('link1', 'link1'), join('link2'))
self.dirlink(os.path.join('link2', 'link2'), join('link3'))
self.dirlink(link0_target, join('link0'))
# Resolve absolute paths
p = (P / 'link0').resolve()
self.assertEqual(p, P)
self.assertEqual(str(p), BASE)
p = (P / 'link1').resolve()
self.assertEqual(p, P)
self.assertEqual(str(p), BASE)
p = (P / 'link2').resolve()
self.assertEqual(p, P)
self.assertEqual(str(p), BASE)
p = (P / 'link3').resolve()
self.assertEqual(p, P)
self.assertEqual(str(p), BASE)
# Resolve relative paths
old_path = os.getcwd()
os.chdir(BASE)
try:
p = self.cls('link0').resolve()
self.assertEqual(p, P)
self.assertEqual(str(p), BASE)
p = self.cls('link1').resolve()
self.assertEqual(p, P)
self.assertEqual(str(p), BASE)
p = self.cls('link2').resolve()
self.assertEqual(p, P)
self.assertEqual(str(p), BASE)
p = self.cls('link3').resolve()
self.assertEqual(p, P)
self.assertEqual(str(p), BASE)
finally:
os.chdir(old_path)
@with_symlinks
def test_complex_symlinks_absolute(self):
self._check_complex_symlinks(BASE)
@with_symlinks
def test_complex_symlinks_relative(self):
self._check_complex_symlinks('.')
@with_symlinks
def test_complex_symlinks_relative_dot_dot(self):
self._check_complex_symlinks(os.path.join('dirA', '..'))
class PathTest(_BasePathTest, unittest.TestCase): class PathTest(_BasePathTest, unittest.TestCase):
cls = pathlib.Path cls = pathlib.Path

View file

@ -44,6 +44,9 @@ Core and Builtins
Library Library
------- -------
- Issue #19887: Improve the Path.resolve() algorithm to support certain
symlink chains.
- Issue #19912: Fixed numerous bugs in ntpath.splitunc(). - Issue #19912: Fixed numerous bugs in ntpath.splitunc().
- Issue #19911: ntpath.splitdrive() now correctly processes the 'İ' character - Issue #19911: ntpath.splitdrive() now correctly processes the 'İ' character