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:24:06 +02:00
commit f2619236eb
3 changed files with 104 additions and 38 deletions

View File

@ -363,52 +363,60 @@ 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 isinstance(filename, bytes): path, ok = _joinrealpath(filename[:0], filename, {})
return abspath(path)
# Join two paths, normalizing ang eliminating any symbolic links
# encountered in the second path.
def _joinrealpath(path, rest, seen):
if isinstance(path, bytes):
sep = b'/' sep = b'/'
empty = b'' curdir = b'.'
pardir = b'..'
else: else:
sep = '/' sep = '/'
empty = '' curdir = '.'
if isabs(filename): pardir = '..'
bits = [sep] + filename.split(sep)[1:]
else:
bits = [empty] + filename.split(sep)
for i in range(2, len(bits)+1): if isabs(rest):
component = join(*bits[0:i]) rest = rest[1:]
# Resolve symbolic links. path = sep
if islink(component):
resolved = _resolve_link(component) while rest:
if resolved is None: name, _, rest = rest.partition(sep)
# Infinite loop -- return original component + rest of the path if not name or name == curdir:
return abspath(join(*([component] + bits[i:]))) # 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=None): def relpath(path, start=None):

View File

@ -320,6 +320,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)
@ -328,6 +344,45 @@ class PosixPathTest(unittest.TestCase):
support.unlink(ABSTFN) support.unlink(ABSTFN)
support.unlink(ABSTFN+"1") support.unlink(ABSTFN+"1")
support.unlink(ABSTFN+"2") support.unlink(ABSTFN+"2")
support.unlink(ABSTFN+"y")
support.unlink(ABSTFN+"c")
@unittest.skipUnless(hasattr(os, "symlink"),
"Missing symlink implementation")
@skip_if_ABSTFN_contains_backslash
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:
support.unlink(ABSTFN + '/self')
support.unlink(ABSTFN + '/link')
safe_rmdir(ABSTFN)
@unittest.skipUnless(hasattr(os, "symlink"),
"Missing symlink implementation")
@skip_if_ABSTFN_contains_backslash
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):
support.unlink(ABSTFN + '/%d' % i)
safe_rmdir(ABSTFN)
@unittest.skipUnless(hasattr(os, "symlink"), @unittest.skipUnless(hasattr(os, "symlink"),
"Missing symlink implementation") "Missing symlink implementation")

View File

@ -244,6 +244,9 @@ Core and Builtins
Library Library
------- -------
- Issue #6975: os.path.realpath() now correctly resolves multiple nested
symlinks on POSIX platforms.
- Issue #13773: sqlite3.connect() gets a new `uri` parameter to pass the - Issue #13773: sqlite3.connect() gets a new `uri` parameter to pass the
filename as a URI, allowing to pass custom options. filename as a URI, allowing to pass custom options.