diff -r 7c307fd57021 Lib/posixpath.py --- a/Lib/posixpath.py Thu Apr 28 17:05:55 2011 +0800 +++ b/Lib/posixpath.py Thu Apr 28 08:03:22 2011 -0700 @@ -377,52 +377,57 @@ def realpath(filename): """Return the canonical path of the specified filename, eliminating any -symbolic links encountered in the path.""" - if isinstance(filename, bytes): - sep = b'/' - empty = b'' - else: - sep = '/' - empty = '' - if isabs(filename): - bits = [sep] + filename.split(sep)[1:] - else: - bits = [empty] + filename.split(sep) - - for i in range(2, len(bits)+1): - component = join(*bits[0:i]) - # Resolve symbolic links. - if islink(component): - resolved = _resolve_link(component) - if resolved is None: - # Infinite loop -- return original component + rest of the path - return abspath(join(*([component] + bits[i:]))) - else: - newpath = join(*([resolved] + bits[i:])) - return realpath(newpath) - - return abspath(filename) - - -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). + symbolic links encountered in the path. """ 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 + current = [] # the working stack + # get abspath of filename without normalization + if isinstance(filename, bytes): + cwd = os.getcwdb() + empty = b'' + _sep = sep.encode(sys.getfilesystemencoding()) + else: + cwd = os.getcwd() + empty = '' + _sep = sep + components = (join(cwd, filename).split(_sep) if filename else + cwd.split(_sep)) # corner case: '' or b'' + while len(components): + comp = components.pop(0) + if comp in ('.', b'.'): + continue + elif comp in ('..', b'..') and current: + if not (len(current) == 1 and current[0] == empty): + # guard against popping [''] + current.pop() + elif not (current and comp == empty): + # guard so that 'a//b/c' -> '/path/to/a/b/c' + current.append(comp) + + # join what we have so far + cwd = _sep.join(current) + if islink(cwd): + if cwd in paths_seen: + # already seen this path, so we must have a symlink loop + return abspath(filename) + paths_seen.add(cwd) + # resolve the symlink + tmp = os.readlink(cwd) + tmplist = tmp.split(_sep) + # insert the symlink expansion into the components stack + components = tmplist + components + if isabs(tmp): + # symlink to an abs path; start over because there might be + # another symlink inside the target abs path + current = [] + else: + # pop the previously pushed symlink; continue popping + # components, starting with the recently pushed expansion + current.pop() + + result = _sep.join(current) + return result or _sep + supports_unicode_filenames = False diff -r 7c307fd57021 Lib/test/test_posixpath.py --- a/Lib/test/test_posixpath.py Thu Apr 28 17:05:55 2011 +0800 +++ b/Lib/test/test_posixpath.py Thu Apr 28 08:03:22 2011 -0700 @@ -588,6 +588,62 @@ safe_rmdir(ABSTFN + "/k") safe_rmdir(ABSTFN) + def test_realpath_issue11397(self): + # Issue #11397: realpath() fails to return the correct real path of + # some tree of symlinks. + try: + old_path = abspath('.') + os.mkdir(ABSTFN) + os.chdir(ABSTFN) + os.mkdir(ABSTFN + '/a') + os.symlink('../a', 'a/blink') + os.makedirs(ABSTFN + '/a/c/d') + os.symlink('a/c/d/..', 'xlink') # xlink -> /a/c + os.symlink('xlink/../blink', 'zlink') # zlink -> /a/blink -> /a + + self.assertEqual(realpath('zlink'), ABSTFN + '/a') + finally: + os.chdir(old_path) + support.unlink(ABSTFN + '/zlink') + support.unlink(ABSTFN + '/xlink') + support.unlink(ABSTFN + '/a/blink') + safe_rmdir(ABSTFN + '/a/c/d') + safe_rmdir(ABSTFN + '/a/c') + safe_rmdir(ABSTFN + '/a') + safe_rmdir(ABSTFN) + + def test_realpath_root(self): + # make sure multiple arbitrary pops still work + depth = len(os.getcwd().split('/')) + paths_to_root = [ + '/..' * 10, + b'/..' * 10, + '../' * depth, + b'../' * depth, + posixpath.join(os.getcwd(), '../' * depth), + ] + for path in paths_to_root: + root = b'/' if isinstance(path, bytes) else '/' + rpath = realpath(path) + self.assertEqual(rpath, root, + "realpath(%s) => %s != %s" % + (repr(path), repr(rpath), repr(root))) + + def test_realpath_current(self): + # make sure current directory realpath still works + paths_to_current = [ + '', + b'', + '.', + b'.', + ] + for path in paths_to_current: + cwd = os.getcwdb() if isinstance(path, bytes) else os.getcwd() + rpath = realpath(path) + self.assertEqual(rpath, cwd, + 'realpath(%s) => %s != %s' % + (path, repr(rpath), repr(cwd))) + def test_relpath(self): (real_getcwd, os.getcwd) = (os.getcwd, lambda: r"/home/user/bar") try: