mirror of https://github.com/python/cpython
Issue #6975: os.path.realpath() now correctly resolves multiple nested symlinks on POSIX platforms.
This commit is contained in:
commit
f2619236eb
|
@ -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):
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue