Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit df32691

Browse files
Issue #6975: os.path.realpath() now correctly resolves multiple nested symlinks on POSIX platforms.
1 parent 4de7457 commit df32691

3 files changed

Lines changed: 107 additions & 41 deletions

File tree

Lib/posixpath.py

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -390,51 +390,59 @@ def abspath(path):
390390
def realpath(filename):
391391
"""Return the canonical path of the specified filename, eliminating any
392392
symbolic links encountered in the path."""
393-
if isinstance(filename, bytes):
393+
path, ok = _joinrealpath(filename[:0], filename, {})
394+
return abspath(path)
395+
396+
# Join two paths, normalizing ang eliminating any symbolic links
397+
# encountered in the second path.
398+
def _joinrealpath(path, rest, seen):
399+
if isinstance(path, bytes):
394400
sep = b'/'
395-
empty = b''
401+
curdir = b'.'
402+
pardir = b'..'
396403
else:
397404
sep = '/'
398-
empty = ''
399-
if isabs(filename):
400-
bits = [sep] + filename.split(sep)[1:]
401-
else:
402-
bits = [empty] + filename.split(sep)
403-
404-
for i in range(2, len(bits)+1):
405-
component = join(*bits[0:i])
406-
# Resolve symbolic links.
407-
if islink(component):
408-
resolved = _resolve_link(component)
409-
if resolved is None:
410-
# Infinite loop -- return original component + rest of the path
411-
return abspath(join(*([component] + bits[i:])))
405+
curdir = '.'
406+
pardir = '..'
407+
408+
if isabs(rest):
409+
rest = rest[1:]
410+
path = sep
411+
412+
while rest:
413+
name, _, rest = rest.partition(sep)
414+
if not name or name == curdir:
415+
# current dir
416+
continue
417+
if name == pardir:
418+
# parent dir
419+
if path:
420+
path = dirname(path)
412421
else:
413-
newpath = join(*([resolved] + bits[i:]))
414-
return realpath(newpath)
415-
416-
return abspath(filename)
417-
418-
419-
def _resolve_link(path):
420-
"""Internal helper function. Takes a path and follows symlinks
421-
until we either arrive at something that isn't a symlink, or
422-
encounter a path we've seen before (meaning that there's a loop).
423-
"""
424-
paths_seen = set()
425-
while islink(path):
426-
if path in paths_seen:
427-
# Already seen this path, so we must have a symlink loop
428-
return None
429-
paths_seen.add(path)
430-
# Resolve where the link points to
431-
resolved = os.readlink(path)
432-
if not isabs(resolved):
433-
dir = dirname(path)
434-
path = normpath(join(dir, resolved))
435-
else:
436-
path = normpath(resolved)
437-
return path
422+
path = name
423+
continue
424+
newpath = join(path, name)
425+
if not islink(newpath):
426+
path = newpath
427+
continue
428+
# Resolve the symbolic link
429+
if newpath in seen:
430+
# Already seen this path
431+
path = seen[newpath]
432+
if path is not None:
433+
# use cached value
434+
continue
435+
# The symlink is not resolved, so we must have a symlink loop.
436+
# Return already resolved part + rest of the path unchanged.
437+
return join(newpath, rest), False
438+
seen[newpath] = None # not resolved symlink
439+
path, ok = _joinrealpath(path, os.readlink(newpath), seen)
440+
if not ok:
441+
return join(path, rest), False
442+
seen[newpath] = path # resolved symlink
443+
444+
return path, True
445+
438446

439447
supports_unicode_filenames = (sys.platform == 'darwin')
440448

Lib/test/test_posixpath.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,22 @@ def test_realpath_symlink_loops(self):
375375
self.assertEqual(realpath(ABSTFN+"1"), ABSTFN+"1")
376376
self.assertEqual(realpath(ABSTFN+"2"), ABSTFN+"2")
377377

378+
self.assertEqual(realpath(ABSTFN+"1/x"), ABSTFN+"1/x")
379+
self.assertEqual(realpath(ABSTFN+"1/.."), dirname(ABSTFN))
380+
self.assertEqual(realpath(ABSTFN+"1/../x"), dirname(ABSTFN) + "/x")
381+
os.symlink(ABSTFN+"x", ABSTFN+"y")
382+
self.assertEqual(realpath(ABSTFN+"1/../" + basename(ABSTFN) + "y"),
383+
ABSTFN + "y")
384+
self.assertEqual(realpath(ABSTFN+"1/../" + basename(ABSTFN) + "1"),
385+
ABSTFN + "1")
386+
387+
os.symlink(basename(ABSTFN) + "a/b", ABSTFN+"a")
388+
self.assertEqual(realpath(ABSTFN+"a"), ABSTFN+"a/b")
389+
390+
os.symlink("../" + basename(dirname(ABSTFN)) + "/" +
391+
basename(ABSTFN) + "c", ABSTFN+"c")
392+
self.assertEqual(realpath(ABSTFN+"c"), ABSTFN+"c")
393+
378394
# Test using relative path as well.
379395
os.chdir(dirname(ABSTFN))
380396
self.assertEqual(realpath(basename(ABSTFN)), ABSTFN)
@@ -383,6 +399,45 @@ def test_realpath_symlink_loops(self):
383399
support.unlink(ABSTFN)
384400
support.unlink(ABSTFN+"1")
385401
support.unlink(ABSTFN+"2")
402+
support.unlink(ABSTFN+"y")
403+
support.unlink(ABSTFN+"c")
404+
405+
@unittest.skipUnless(hasattr(os, "symlink"),
406+
"Missing symlink implementation")
407+
@skip_if_ABSTFN_contains_backslash
408+
def test_realpath_repeated_indirect_symlinks(self):
409+
# Issue #6975.
410+
try:
411+
os.mkdir(ABSTFN)
412+
os.symlink('../' + basename(ABSTFN), ABSTFN + '/self')
413+
os.symlink('self/self/self', ABSTFN + '/link')
414+
self.assertEqual(realpath(ABSTFN + '/link'), ABSTFN)
415+
finally:
416+
support.unlink(ABSTFN + '/self')
417+
support.unlink(ABSTFN + '/link')
418+
safe_rmdir(ABSTFN)
419+
420+
@unittest.skipUnless(hasattr(os, "symlink"),
421+
"Missing symlink implementation")
422+
@skip_if_ABSTFN_contains_backslash
423+
def test_realpath_deep_recursion(self):
424+
depth = 10
425+
old_path = abspath('.')
426+
try:
427+
os.mkdir(ABSTFN)
428+
for i in range(depth):
429+
os.symlink('/'.join(['%d' % i] * 10), ABSTFN + '/%d' % (i + 1))
430+
os.symlink('.', ABSTFN + '/0')
431+
self.assertEqual(realpath(ABSTFN + '/%d' % depth), ABSTFN)
432+
433+
# Test using relative path as well.
434+
os.chdir(ABSTFN)
435+
self.assertEqual(realpath('%d' % depth), ABSTFN)
436+
finally:
437+
os.chdir(old_path)
438+
for i in range(depth + 1):
439+
support.unlink(ABSTFN + '/%d' % i)
440+
safe_rmdir(ABSTFN)
386441

387442
@unittest.skipUnless(hasattr(os, "symlink"),
388443
"Missing symlink implementation")

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ Core and Builtins
218218
Library
219219
-------
220220

221+
- Issue #6975: os.path.realpath() now correctly resolves multiple nested
222+
symlinks on POSIX platforms.
223+
221224
- Issue #17156: pygettext.py now uses an encoding of source file and correctly
222225
writes and escapes non-ascii characters.
223226

0 commit comments

Comments
 (0)