Commit 0dd3d309 authored by Serhiy Storchaka's avatar Serhiy Storchaka

Issue #6975: os.path.realpath() now correctly resolves multiple nested symlinks on POSIX platforms.

parent bb801313
...@@ -364,45 +364,50 @@ def abspath(path): ...@@ -364,45 +364,50 @@ def abspath(path):
def realpath(filename): def realpath(filename):
"""Return the canonical path of the specified filename, eliminating any """Return the canonical path of the specified filename, eliminating any
symbolic links encountered in the path.""" symbolic links encountered in the path."""
if isabs(filename): path, ok = _joinrealpath('', filename, {})
bits = ['/'] + filename.split('/')[1:] return abspath(path)
else:
bits = [''] + filename.split('/') # Join two paths, normalizing ang eliminating any symbolic links
# encountered in the second path.
for i in range(2, len(bits)+1): def _joinrealpath(path, rest, seen):
component = join(*bits[0:i]) if isabs(rest):
# Resolve symbolic links. rest = rest[1:]
if islink(component): path = sep
resolved = _resolve_link(component)
if resolved is None: while rest:
# Infinite loop -- return original component + rest of the path name, _, rest = rest.partition(sep)
return abspath(join(*([component] + bits[i:]))) if not name or name == curdir:
else: # current dir
newpath = join(*([resolved] + bits[i:])) continue
return realpath(newpath) if name == pardir:
# parent dir
return abspath(filename) if path:
path = dirname(path)
def _resolve_link(path):
"""Internal helper function. Takes a path and follows symlinks
until we either arrive at something that isn't a symlink, or
encounter a path we've seen before (meaning that there's a loop).
"""
paths_seen = set()
while islink(path):
if path in paths_seen:
# Already seen this path, so we must have a symlink loop
return None
paths_seen.add(path)
# Resolve where the link points to
resolved = os.readlink(path)
if not isabs(resolved):
dir = dirname(path)
path = normpath(join(dir, resolved))
else: else:
path = normpath(resolved) path = name
return path continue
newpath = join(path, name)
if not islink(newpath):
path = newpath
continue
# Resolve the symbolic link
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.
# Return already resolved part + rest of the path unchanged.
return join(newpath, rest), False
seen[newpath] = None # not resolved symlink
path, ok = _joinrealpath(path, os.readlink(newpath), seen)
if not ok:
return join(path, rest), False
seen[newpath] = path # resolved symlink
return path, True
supports_unicode_filenames = (sys.platform == 'darwin') supports_unicode_filenames = (sys.platform == 'darwin')
......
...@@ -236,6 +236,22 @@ class PosixPathTest(unittest.TestCase): ...@@ -236,6 +236,22 @@ class PosixPathTest(unittest.TestCase):
self.assertEqual(realpath(ABSTFN+"1"), ABSTFN+"1") self.assertEqual(realpath(ABSTFN+"1"), ABSTFN+"1")
self.assertEqual(realpath(ABSTFN+"2"), ABSTFN+"2") self.assertEqual(realpath(ABSTFN+"2"), ABSTFN+"2")
self.assertEqual(realpath(ABSTFN+"1/x"), ABSTFN+"1/x")
self.assertEqual(realpath(ABSTFN+"1/.."), dirname(ABSTFN))
self.assertEqual(realpath(ABSTFN+"1/../x"), dirname(ABSTFN) + "/x")
os.symlink(ABSTFN+"x", ABSTFN+"y")
self.assertEqual(realpath(ABSTFN+"1/../" + basename(ABSTFN) + "y"),
ABSTFN + "y")
self.assertEqual(realpath(ABSTFN+"1/../" + basename(ABSTFN) + "1"),
ABSTFN + "1")
os.symlink(basename(ABSTFN) + "a/b", ABSTFN+"a")
self.assertEqual(realpath(ABSTFN+"a"), ABSTFN+"a/b")
os.symlink("../" + basename(dirname(ABSTFN)) + "/" +
basename(ABSTFN) + "c", ABSTFN+"c")
self.assertEqual(realpath(ABSTFN+"c"), ABSTFN+"c")
# Test using relative path as well. # Test using relative path as well.
os.chdir(dirname(ABSTFN)) os.chdir(dirname(ABSTFN))
self.assertEqual(realpath(basename(ABSTFN)), ABSTFN) self.assertEqual(realpath(basename(ABSTFN)), ABSTFN)
...@@ -244,6 +260,39 @@ class PosixPathTest(unittest.TestCase): ...@@ -244,6 +260,39 @@ class PosixPathTest(unittest.TestCase):
test_support.unlink(ABSTFN) test_support.unlink(ABSTFN)
test_support.unlink(ABSTFN+"1") test_support.unlink(ABSTFN+"1")
test_support.unlink(ABSTFN+"2") test_support.unlink(ABSTFN+"2")
test_support.unlink(ABSTFN+"y")
test_support.unlink(ABSTFN+"c")
def test_realpath_repeated_indirect_symlinks(self):
# Issue #6975.
try:
os.mkdir(ABSTFN)
os.symlink('../' + basename(ABSTFN), ABSTFN + '/self')
os.symlink('self/self/self', ABSTFN + '/link')
self.assertEqual(realpath(ABSTFN + '/link'), ABSTFN)
finally:
test_support.unlink(ABSTFN + '/self')
test_support.unlink(ABSTFN + '/link')
safe_rmdir(ABSTFN)
def test_realpath_deep_recursion(self):
depth = 10
old_path = abspath('.')
try:
os.mkdir(ABSTFN)
for i in range(depth):
os.symlink('/'.join(['%d' % i] * 10), ABSTFN + '/%d' % (i + 1))
os.symlink('.', ABSTFN + '/0')
self.assertEqual(realpath(ABSTFN + '/%d' % depth), ABSTFN)
# Test using relative path as well.
os.chdir(ABSTFN)
self.assertEqual(realpath('%d' % depth), ABSTFN)
finally:
os.chdir(old_path)
for i in range(depth + 1):
test_support.unlink(ABSTFN + '/%d' % i)
safe_rmdir(ABSTFN)
def test_realpath_resolve_parents(self): def test_realpath_resolve_parents(self):
# We also need to resolve any symlinks in the parents of a relative # We also need to resolve any symlinks in the parents of a relative
......
...@@ -202,6 +202,9 @@ Core and Builtins ...@@ -202,6 +202,9 @@ Core and Builtins
Library Library
------- -------
- Issue #6975: os.path.realpath() now correctly resolves multiple nested
symlinks on POSIX platforms.
- Issue #17156: pygettext.py now correctly escapes non-ascii characters. - Issue #17156: pygettext.py now correctly escapes non-ascii characters.
- Issue #7358: cStringIO.StringIO now supports writing to and reading from - Issue #7358: cStringIO.StringIO now supports writing to and reading from
......
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