]> granicus.if.org Git - python/commitdiff
Issue #19887: Improve the Path.resolve() algorithm to support certain symlink chains.
authorAntoine Pitrou <solipsis@pitrou.net>
Mon, 16 Dec 2013 18:57:41 +0000 (19:57 +0100)
committerAntoine Pitrou <solipsis@pitrou.net>
Mon, 16 Dec 2013 18:57:41 +0000 (19:57 +0100)
Original patch by Serhiy.

Lib/pathlib.py
Lib/test/test_pathlib.py
Misc/NEWS

index b404c1f023f470bc00861bc5dba19d0b9858ea7d..9b4fde1d156b82ea81d30f5dab04dadc66fcdb2d 100644 (file)
@@ -254,42 +254,47 @@ class _PosixFlavour(_Flavour):
 
     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
index a8d740f3ae2a9b3c8a9e1a6e4800dc224031fb0c..0ad77e052967ecdf4bcde8e1016480aa7a85bb5e 100755 (executable)
@@ -1620,6 +1620,59 @@ class _BasePathTest(object):
         # '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
index 628c2fe3e525baa074b644abfc69a06dbfcd5dd9..52b0a3d5c1c989dd4631a45a2eea74d1d497ea1e 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -44,6 +44,9 @@ Core and Builtins
 Library
 -------
 
+- Issue #19887: Improve the Path.resolve() algorithm to support certain
+  symlink chains.
+
 - Issue #19912: Fixed numerous bugs in ntpath.splitunc().
 
 - Issue #19911: ntpath.splitdrive() now correctly processes the 'İ' character