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

Skip to content

Commit d83c824

Browse files
Issue #6975: os.path.realpath() now correctly resolves multiple nested symlinks on POSIX platforms.
2 parents 986bbfc + df32691 commit d83c824

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

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

Lib/test/test_posixpath.py

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

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

389444
@unittest.skipUnless(hasattr(os, "symlink"),
390445
"Missing symlink implementation")

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ Core and Builtins
172172
Library
173173
-------
174174

175+
- Issue #6975: os.path.realpath() now correctly resolves multiple nested
176+
symlinks on POSIX platforms.
177+
175178
- Issue #17156: pygettext.py now uses an encoding of source file and correctly
176179
writes and escapes non-ascii characters.
177180

0 commit comments

Comments
 (0)