[Python-checkins] bpo-46208: Fix normalization of relative paths in _Py_normpath()/os.path.normpath (GH-30362)

zooba webhook-mailer at python.org
Thu Jan 6 14:13:29 EST 2022


https://github.com/python/cpython/commit/9c5fa9c97c5c5336e60e4ae7a2e6e3f67acedfc7
commit: 9c5fa9c97c5c5336e60e4ae7a2e6e3f67acedfc7
branch: main
author: neonene <53406459+neonene at users.noreply.github.com>
committer: zooba <steve.dower at microsoft.com>
date: 2022-01-06T19:13:10Z
summary:

bpo-46208: Fix normalization of relative paths in _Py_normpath()/os.path.normpath (GH-30362)

files:
A Misc/NEWS.d/next/Core and Builtins/2022-01-04-01-53-35.bpo-46208.i00Vz5.rst
M Lib/test/test_ntpath.py
M Lib/test/test_posixpath.py
M Python/fileutils.c

diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py
index a8d87e53db9e9..cc29881049224 100644
--- a/Lib/test/test_ntpath.py
+++ b/Lib/test/test_ntpath.py
@@ -235,6 +235,15 @@ def test_normpath(self):
 
         tester("ntpath.normpath('\\\\.\\NUL')", r'\\.\NUL')
         tester("ntpath.normpath('\\\\?\\D:/XY\\Z')", r'\\?\D:/XY\Z')
+        tester("ntpath.normpath('handbook/../../Tests/image.png')", r'..\Tests\image.png')
+        tester("ntpath.normpath('handbook/../../../Tests/image.png')", r'..\..\Tests\image.png')
+        tester("ntpath.normpath('handbook///../a/.././../b/c')", r'..\b\c')
+        tester("ntpath.normpath('handbook/a/../..///../../b/c')", r'..\..\b\c')
+
+        tester("ntpath.normpath('//server/share/..')" ,    '\\\\server\\share\\')
+        tester("ntpath.normpath('//server/share/../')" ,   '\\\\server\\share\\')
+        tester("ntpath.normpath('//server/share/../..')",  '\\\\server\\share\\')
+        tester("ntpath.normpath('//server/share/../../')", '\\\\server\\share\\')
 
     def test_realpath_curdir(self):
         expected = ntpath.normpath(os.getcwd())
diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py
index e4d8384ef0b4b..5fc4205beb125 100644
--- a/Lib/test/test_posixpath.py
+++ b/Lib/test/test_posixpath.py
@@ -329,13 +329,30 @@ def test_expanduser_pwd(self):
         ("/..", "/"),
         ("/../", "/"),
         ("/..//", "/"),
+        ("//.", "//"),
         ("//..", "//"),
+        ("//...", "//..."),
+        ("//../foo", "//foo"),
+        ("//../../foo", "//foo"),
         ("/../foo", "/foo"),
         ("/../../foo", "/foo"),
         ("/../foo/../", "/"),
         ("/../foo/../bar", "/bar"),
         ("/../../foo/../bar/./baz/boom/..", "/bar/baz"),
         ("/../../foo/../bar/./baz/boom/.", "/bar/baz/boom"),
+        ("foo/../bar/baz", "bar/baz"),
+        ("foo/../../bar/baz", "../bar/baz"),
+        ("foo/../../../bar/baz", "../../bar/baz"),
+        ("foo///../bar/.././../baz/boom", "../baz/boom"),
+        ("foo/bar/../..///../../baz/boom", "../../baz/boom"),
+        ("/foo/..", "/"),
+        ("/foo/../..", "/"),
+        ("//foo/..", "//"),
+        ("//foo/../..", "//"),
+        ("///foo/..", "/"),
+        ("///foo/../..", "/"),
+        ("////foo/..", "/"),
+        ("/////foo/..", "/"),
     ]
 
     def test_normpath(self):
diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-01-04-01-53-35.bpo-46208.i00Vz5.rst b/Misc/NEWS.d/next/Core and Builtins/2022-01-04-01-53-35.bpo-46208.i00Vz5.rst
new file mode 100644
index 0000000000000..92025a02d5b25
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2022-01-04-01-53-35.bpo-46208.i00Vz5.rst	
@@ -0,0 +1 @@
+Fix the regression of os.path.normpath("A/../../B") not returning expected "../B" but "B".
\ No newline at end of file
diff --git a/Python/fileutils.c b/Python/fileutils.c
index cae6b75b6ae9b..d570be5577ae9 100644
--- a/Python/fileutils.c
+++ b/Python/fileutils.c
@@ -2218,11 +2218,11 @@ _Py_normpath(wchar_t *path, Py_ssize_t size)
     if (!path[0] || size == 0) {
         return path;
     }
-    wchar_t lastC = L'\0';
-    wchar_t *p1 = path;
     wchar_t *pEnd = size >= 0 ? &path[size] : NULL;
-    wchar_t *p2 = path;
-    wchar_t *minP2 = path;
+    wchar_t *p1 = path;     // sequentially scanned address in the path
+    wchar_t *p2 = path;     // destination of a scanned character to be ljusted
+    wchar_t *minP2 = path;  // the beginning of the destination range
+    wchar_t lastC = L'\0';  // the last ljusted character, p2[-1] in most cases
 
 #define IS_END(x) (pEnd ? (x) == pEnd : !*(x))
 #ifdef ALTSEP
@@ -2264,14 +2264,18 @@ _Py_normpath(wchar_t *path, Py_ssize_t size)
                 *p2++ = lastC = *p1;
             }
         }
-        minP2 = p2;
+        if (sepCount) {
+            minP2 = p2;      // Invalid path
+        } else {
+            minP2 = p2 - 1;  // Absolute path has SEP at minP2
+        }
     }
 #else
     // Skip past two leading SEPs
     else if (IS_SEP(&p1[0]) && IS_SEP(&p1[1]) && !IS_SEP(&p1[2])) {
         *p2++ = *p1++;
         *p2++ = *p1++;
-        minP2 = p2;
+        minP2 = p2 - 1;  // Absolute path has SEP at minP2
         lastC = SEP;
     }
 #endif /* MS_WINDOWS */
@@ -2292,8 +2296,11 @@ _Py_normpath(wchar_t *path, Py_ssize_t size)
                     wchar_t *p3 = p2;
                     while (p3 != minP2 && *--p3 == SEP) { }
                     while (p3 != minP2 && *(p3 - 1) != SEP) { --p3; }
-                    if (p3[0] == L'.' && p3[1] == L'.' && IS_SEP(&p3[2])) {
-                        // Previous segment is also ../, so append instead
+                    if (p2 == minP2
+                        || (p3[0] == L'.' && p3[1] == L'.' && IS_SEP(&p3[2])))
+                    {
+                        // Previous segment is also ../, so append instead.
+                        // Relative path does not absorb ../ at minP2 as well.
                         *p2++ = L'.';
                         *p2++ = L'.';
                         lastC = L'.';
@@ -2314,7 +2321,7 @@ _Py_normpath(wchar_t *path, Py_ssize_t size)
             }
         } else {
             *p2++ = lastC = c;
-        } 
+        }
     }
     *p2 = L'\0';
     if (p2 != minP2) {



More information about the Python-checkins mailing list