Commit df32691e authored by Serhiy Storchaka's avatar Serhiy Storchaka

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

parent 4de74570
...@@ -390,51 +390,59 @@ def abspath(path): ...@@ -390,51 +390,59 @@ 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: if isabs(rest):
bits = [empty] + filename.split(sep) rest = rest[1:]
path = sep
for i in range(2, len(bits)+1):
component = join(*bits[0:i]) while rest:
# Resolve symbolic links. name, _, rest = rest.partition(sep)
if islink(component): if not name or name == curdir:
resolved = _resolve_link(component) # current dir
if resolved is None: continue
# Infinite loop -- return original component + rest of the path if name == pardir:
return abspath(join(*([component] + bits[i:]))) # 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')
......
...@@ -375,6 +375,22 @@ class PosixPathTest(unittest.TestCase): ...@@ -375,6 +375,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)
...@@ -383,6 +399,45 @@ class PosixPathTest(unittest.TestCase): ...@@ -383,6 +399,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")
......
...@@ -218,6 +218,9 @@ Core and Builtins ...@@ -218,6 +218,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 uses an encoding of source file and correctly - Issue #17156: pygettext.py now uses an encoding of source file and correctly
writes and escapes non-ascii characters. writes and escapes non-ascii characters.
......
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