Commits

Antoine Pitrou committed 5a9b487

Backport 12a52186b4fd

  • Participants
  • Parent commits e39c980
  • Branches pep428

Comments (0)

Files changed (3)

 History
 -------
 
-In development (unreleased)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Version 1.0
+^^^^^^^^^^^
 
+- Python issue #19887: Improve the Path.resolve() algorithm to support
+  certain symlink chains.
 - Make pathlib usable under Python 2.7 with unicode pathnames (only pure
   ASCII, though).
 - Issue #21: fix TypeError under Python 2.7 when using new division.
 
     def resolve(self, path):
         sep = self.sep
-        def split(p):
-            return [x for x in p.split(sep) if x]
-        def absparts(p):
-            # Our own abspath(), since the posixpath one makes
-            # the mistake of "normalizing" the path without resolving the
-            # symlinks first.
-            if not p.startswith(sep):
-                return split(os.getcwd()) + split(p)
-            else:
-                return split(p)
-        parts = absparts(str(path))[::-1]
         accessor = path._accessor
-        resolved = cur = ""
-        symlinks = {}
-        while parts:
-            part = parts.pop()
-            cur = resolved + sep + part
-            if cur in symlinks and symlinks[cur] <= len(parts):
-                # We've already seen the symlink and there's not less
-                # work to do than the last time.
-                raise RuntimeError("Symlink loop from %r" % cur)
-            try:
-                target = accessor.readlink(cur)
-            except OSError as e:
-                if e.errno != EINVAL:
-                    raise
-                # Not a symlink
-                resolved = cur
-            else:
-                # Take note of remaining work from this symlink
-                symlinks[cur] = len(parts)
-                if target.startswith(sep):
-                    # Symlink points to absolute path
-                    resolved = ""
-                parts.extend(split(target)[::-1])
-        return resolved or sep
+        seen = {}
+        def _resolve(path, rest):
+            if rest.startswith(sep):
+                path = ''
+
+            for name in rest.split(sep):
+                if not name or name == '.':
+                    # current dir
+                    continue
+                if name == '..':
+                    # parent dir
+                    path, _, _ = path.rpartition(sep)
+                    continue
+                newpath = path + sep + name
+                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.
+                    raise RuntimeError("Symlink loop from %r" % newpath)
+                # Resolve the symbolic link
+                try:
+                    target = accessor.readlink(newpath)
+                except OSError as e:
+                    if e.errno != EINVAL:
+                        raise
+                    # Not a symlink
+                    path = newpath
+                else:
+                    seen[newpath] = None # not resolved symlink
+                    path = _resolve(path, target)
+                    seen[newpath] = path # resolved symlink
+
+            return path
+        # NOTE: according to POSIX, getcwd() cannot contain path components
+        # which are symlinks.
+        base = '' if path.is_absolute() else os.getcwd()
+        return _resolve(base, str(path)) or sep
 
     def is_reserved(self, parts):
         return False
         # 'bin'
         self.assertIs(p.parts[2], q.parts[3])
 
+    def _check_complex_symlinks(self, link0_target):
+        # Test solving a non-looping chain of symlinks (issue #19887)
+        P = self.cls(BASE)
+        self.dirlink(os.path.join('link0', 'link0'), join('link1'))
+        self.dirlink(os.path.join('link1', 'link1'), join('link2'))
+        self.dirlink(os.path.join('link2', 'link2'), join('link3'))
+        self.dirlink(link0_target, join('link0'))
+
+        # Resolve absolute paths
+        p = (P / 'link0').resolve()
+        self.assertEqual(p, P)
+        self.assertEqual(str(p), BASE)
+        p = (P / 'link1').resolve()
+        self.assertEqual(p, P)
+        self.assertEqual(str(p), BASE)
+        p = (P / 'link2').resolve()
+        self.assertEqual(p, P)
+        self.assertEqual(str(p), BASE)
+        p = (P / 'link3').resolve()
+        self.assertEqual(p, P)
+        self.assertEqual(str(p), BASE)
+
+        # Resolve relative paths
+        old_path = os.getcwd()
+        os.chdir(BASE)
+        try:
+            p = self.cls('link0').resolve()
+            self.assertEqual(p, P)
+            self.assertEqual(str(p), BASE)
+            p = self.cls('link1').resolve()
+            self.assertEqual(p, P)
+            self.assertEqual(str(p), BASE)
+            p = self.cls('link2').resolve()
+            self.assertEqual(p, P)
+            self.assertEqual(str(p), BASE)
+            p = self.cls('link3').resolve()
+            self.assertEqual(p, P)
+            self.assertEqual(str(p), BASE)
+        finally:
+            os.chdir(old_path)
+
+    @with_symlinks
+    def test_complex_symlinks_absolute(self):
+        self._check_complex_symlinks(BASE)
+
+    @with_symlinks
+    def test_complex_symlinks_relative(self):
+        self._check_complex_symlinks('.')
+
+    @with_symlinks
+    def test_complex_symlinks_relative_dot_dot(self):
+        self._check_complex_symlinks(os.path.join('dirA', '..'))
+
 
 class PathTest(_BasePathTest, unittest.TestCase):
     cls = pathlib.Path