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

This commit is contained in:
Serhiy Storchaka 2013-02-10 12:21:49 +02:00
parent bb80131375
commit 0dd3d309ab
3 changed files with 92 additions and 35 deletions

View file

@ -364,46 +364,51 @@ 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('/')
for i in range(2, len(bits)+1): # Join two paths, normalizing ang eliminating any symbolic links
component = join(*bits[0:i]) # encountered in the second path.
# Resolve symbolic links. def _joinrealpath(path, rest, seen):
if islink(component): if isabs(rest):
resolved = _resolve_link(component) rest = rest[1:]
if resolved is None: path = sep
# Infinite loop -- return original component + rest of the path
return abspath(join(*([component] + bits[i:]))) while rest:
name, _, rest = rest.partition(sep)
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)
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 abspath(filename) return path, True
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:
path = normpath(resolved)
return path
supports_unicode_filenames = (sys.platform == 'darwin') supports_unicode_filenames = (sys.platform == 'darwin')
def relpath(path, start=curdir): def relpath(path, start=curdir):

View file

@ -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):
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

View file

@ -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