[Python-checkins] GH-80486: Fix handling of NTFS alternate data streams in pathlib (GH-102454)

zooba webhook-mailer at python.org
Fri Mar 10 12:29:12 EST 2023


https://github.com/python/cpython/commit/90f1d777177e28b6c7b8d9ba751550e373d61b0a
commit: 90f1d777177e28b6c7b8d9ba751550e373d61b0a
branch: main
author: Barney Gale <barney.gale at gmail.com>
committer: zooba <steve.dower at microsoft.com>
date: 2023-03-10T17:29:04Z
summary:

GH-80486: Fix handling of NTFS alternate data streams in pathlib (GH-102454)

Co-authored-by: Maor Kleinberger <kmaork at gmail.com>

files:
A Misc/NEWS.d/next/Library/2019-03-15-22-50-27.bpo-36305.Pbkv6u.rst
M Lib/pathlib.py
M Lib/test/test_pathlib.py

diff --git a/Lib/pathlib.py b/Lib/pathlib.py
index d375529ff5f7..55c44f12e5a2 100644
--- a/Lib/pathlib.py
+++ b/Lib/pathlib.py
@@ -320,8 +320,9 @@ def _from_parsed_parts(cls, drv, root, parts):
     def _format_parsed_parts(cls, drv, root, parts):
         if drv or root:
             return drv + root + cls._flavour.sep.join(parts[1:])
-        else:
-            return cls._flavour.sep.join(parts)
+        elif parts and cls._flavour.splitdrive(parts[0])[0]:
+            parts = ['.'] + parts
+        return cls._flavour.sep.join(parts)
 
     def __str__(self):
         """Return the string representation of the path, suitable for
@@ -1188,7 +1189,8 @@ def expanduser(self):
             homedir = self._flavour.expanduser(self._parts[0])
             if homedir[:1] == "~":
                 raise RuntimeError("Could not determine home directory.")
-            return self._from_parts([homedir] + self._parts[1:])
+            drv, root, parts = self._parse_parts((homedir,))
+            return self._from_parsed_parts(drv, root, parts + self._parts[1:])
 
         return self
 
diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py
index f8e2f44d27fc..f05dead58867 100644
--- a/Lib/test/test_pathlib.py
+++ b/Lib/test/test_pathlib.py
@@ -122,6 +122,13 @@ def test_parse_parts(self):
         # the second path is relative.
         check(['c:/a/b', 'c:x/y'], ('c:', '\\', ['c:\\', 'a', 'b', 'x', 'y']))
         check(['c:/a/b', 'c:/x/y'], ('c:', '\\', ['c:\\', 'x', 'y']))
+        # Paths to files with NTFS alternate data streams
+        check(['./c:s'],                ('', '', ['c:s']))
+        check(['cc:s'],                 ('', '', ['cc:s']))
+        check(['C:c:s'],                ('C:', '', ['C:', 'c:s']))
+        check(['C:/c:s'],               ('C:', '\\', ['C:\\', 'c:s']))
+        check(['D:a', './c:b'],         ('D:', '', ['D:', 'a', 'c:b']))
+        check(['D:/a', './c:b'],        ('D:', '\\', ['D:\\', 'a', 'c:b']))
 
 
 #
@@ -165,6 +172,7 @@ def test_constructor_common(self):
         self.assertEqual(P(P('a'), 'b'), P('a/b'))
         self.assertEqual(P(P('a'), P('b')), P('a/b'))
         self.assertEqual(P(P('a'), P('b'), P('c')), P(FakePath("a/b/c")))
+        self.assertEqual(P(P('./a:b')), P('./a:b'))
 
     def test_bytes(self):
         P = self.cls
@@ -814,7 +822,8 @@ class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase):
 
     equivalences = _BasePurePathTest.equivalences.copy()
     equivalences.update({
-        'c:a': [ ('c:', 'a'), ('c:', 'a/'), ('/', 'c:', 'a') ],
+        './a:b': [ ('./a:b',) ],
+        'c:a': [ ('c:', 'a'), ('c:', 'a/'), ('.', 'c:', 'a') ],
         'c:/a': [
             ('c:/', 'a'), ('c:', '/', 'a'), ('c:', '/a'),
             ('/z', 'c:/', 'a'), ('//x/y', 'c:/', 'a'),
@@ -838,6 +847,7 @@ def test_str(self):
         self.assertEqual(str(p), '\\\\a\\b\\c\\d')
 
     def test_str_subclass(self):
+        self._check_str_subclass('.\\a:b')
         self._check_str_subclass('c:')
         self._check_str_subclass('c:a')
         self._check_str_subclass('c:a\\b.txt')
@@ -1005,6 +1015,7 @@ def test_drive(self):
         self.assertEqual(P('//a/b').drive, '\\\\a\\b')
         self.assertEqual(P('//a/b/').drive, '\\\\a\\b')
         self.assertEqual(P('//a/b/c/d').drive, '\\\\a\\b')
+        self.assertEqual(P('./c:a').drive, '')
 
     def test_root(self):
         P = self.cls
@@ -1341,6 +1352,14 @@ def test_join(self):
         self.assertEqual(pp, P('C:/a/b/x/y'))
         pp = p.joinpath('c:/x/y')
         self.assertEqual(pp, P('C:/x/y'))
+        # Joining with files with NTFS data streams => the filename should
+        # not be parsed as a drive letter
+        pp = p.joinpath(P('./d:s'))
+        self.assertEqual(pp, P('C:/a/b/d:s'))
+        pp = p.joinpath(P('./dd:s'))
+        self.assertEqual(pp, P('C:/a/b/dd:s'))
+        pp = p.joinpath(P('E:d:s'))
+        self.assertEqual(pp, P('E:d:s'))
 
     def test_div(self):
         # Basically the same as joinpath().
@@ -1361,6 +1380,11 @@ def test_div(self):
         # the second path is relative.
         self.assertEqual(p / 'c:x/y', P('C:/a/b/x/y'))
         self.assertEqual(p / 'c:/x/y', P('C:/x/y'))
+        # Joining with files with NTFS data streams => the filename should
+        # not be parsed as a drive letter
+        self.assertEqual(p / P('./d:s'), P('C:/a/b/d:s'))
+        self.assertEqual(p / P('./dd:s'), P('C:/a/b/dd:s'))
+        self.assertEqual(p / P('E:d:s'), P('E:d:s'))
 
     def test_is_reserved(self):
         P = self.cls
@@ -1626,6 +1650,8 @@ def test_expanduser_common(self):
         self.assertEqual(p.expanduser(), p)
         p = P(P('').absolute().anchor) / '~'
         self.assertEqual(p.expanduser(), p)
+        p = P('~/a:b')
+        self.assertEqual(p.expanduser(), P(os.path.expanduser('~'), './a:b'))
 
     def test_exists(self):
         P = self.cls
diff --git a/Misc/NEWS.d/next/Library/2019-03-15-22-50-27.bpo-36305.Pbkv6u.rst b/Misc/NEWS.d/next/Library/2019-03-15-22-50-27.bpo-36305.Pbkv6u.rst
new file mode 100644
index 000000000000..d9360496ac24
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2019-03-15-22-50-27.bpo-36305.Pbkv6u.rst
@@ -0,0 +1,2 @@
+Fix handling of Windows filenames that resemble drives, such as ``./a:b``,
+in :mod:`pathlib`.



More information about the Python-checkins mailing list