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:
# current dir
continue
if name == pardir:
# parent dir
if path:
path = dirname(path)
else: else:
newpath = join(*([resolved] + bits[i:])) path = name
return realpath(newpath) continue
newpath = join(path, name)
return abspath(filename) if not islink(newpath):
path = newpath
continue
def _resolve_link(path): # Resolve the symbolic link
"""Internal helper function. Takes a path and follows symlinks if newpath in seen:
until we either arrive at something that isn't a symlink, or # Already seen this path
encounter a path we've seen before (meaning that there's a loop). path = seen[newpath]
""" if path is not None:
paths_seen = set() # use cached value
while islink(path): continue
if path in paths_seen: # The symlink is not resolved, so we must have a symlink loop.
# Already seen this path, so we must have a symlink loop # Return already resolved part + rest of the path unchanged.
return None return join(newpath, rest), False
paths_seen.add(path) seen[newpath] = None # not resolved symlink
# Resolve where the link points to path, ok = _joinrealpath(path, os.readlink(newpath), seen)
resolved = os.readlink(path) if not ok:
if not isabs(resolved): return join(path, rest), False
dir = dirname(path) seen[newpath] = path # resolved symlink
path = normpath(join(dir, resolved))
else: return path, True
path = normpath(resolved)
return path
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