diff -r e19d2e3a3a58 Lib/posixpath.py --- a/Lib/posixpath.py Thu Apr 28 17:00:19 2011 +0800 +++ b/Lib/posixpath.py Thu Apr 28 08:11:56 2011 -0700 @@ -354,46 +354,56 @@ def realpath(filename): """Return the canonical path of the specified filename, eliminating any -symbolic links encountered in the path.""" - if isabs(filename): - bits = ['/'] + filename.split('/')[1:] - else: - bits = [''] + filename.split('/') - - 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, unicode): + cwd = os.getcwdu() + empty = u'' + _sep = unicode(sep) + 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 ('.', u'.'): + continue + elif comp in ('..', u'..') 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 = (sys.platform == 'darwin') diff -r e19d2e3a3a58 Lib/test/test_posixpath.py --- a/Lib/test/test_posixpath.py Thu Apr 28 17:00:19 2011 +0800 +++ b/Lib/test/test_posixpath.py Thu Apr 28 08:11:56 2011 -0700 @@ -309,6 +309,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) + test_support.unlink(ABSTFN + '/zlink') + test_support.unlink(ABSTFN + '/xlink') + test_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, + u'/..' * 10, + '../' * depth, + u'../' * depth, + posixpath.join(os.getcwd(), '../' * depth), + ] + for path in paths_to_root: + root = u'/' if isinstance(path, unicode) 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 = [ + '', + u'', + '.', + u'.', + ] + for path in paths_to_current: + cwd = os.getcwdu() if isinstance(path, unicode) 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: