mirror of
https://github.com/python/cpython.git
synced 2025-08-25 11:15:02 +00:00
Issue #19887: Improve the Path.resolve() algorithm to support certain symlink chains.
Original patch by Serhiy.
This commit is contained in:
parent
d2e48ca813
commit
c274fd22ed
3 changed files with 96 additions and 35 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue