Skip to content

Commit 90f1d77

Browse files
barneygalekmaork
andauthored
GH-80486: Fix handling of NTFS alternate data streams in pathlib (GH-102454)
Co-authored-by: Maor Kleinberger <kmaork@gmail.com>
1 parent 12226be commit 90f1d77

File tree

3 files changed

+34
-4
lines changed

3 files changed

+34
-4
lines changed

Lib/pathlib.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,9 @@ def _from_parsed_parts(cls, drv, root, parts):
320320
def _format_parsed_parts(cls, drv, root, parts):
321321
if drv or root:
322322
return drv + root + cls._flavour.sep.join(parts[1:])
323-
else:
324-
return cls._flavour.sep.join(parts)
323+
elif parts and cls._flavour.splitdrive(parts[0])[0]:
324+
parts = ['.'] + parts
325+
return cls._flavour.sep.join(parts)
325326

326327
def __str__(self):
327328
"""Return the string representation of the path, suitable for
@@ -1188,7 +1189,8 @@ def expanduser(self):
11881189
homedir = self._flavour.expanduser(self._parts[0])
11891190
if homedir[:1] == "~":
11901191
raise RuntimeError("Could not determine home directory.")
1191-
return self._from_parts([homedir] + self._parts[1:])
1192+
drv, root, parts = self._parse_parts((homedir,))
1193+
return self._from_parsed_parts(drv, root, parts + self._parts[1:])
11921194

11931195
return self
11941196

Lib/test/test_pathlib.py

+27-1
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,13 @@ def test_parse_parts(self):
122122
# the second path is relative.
123123
check(['c:/a/b', 'c:x/y'], ('c:', '\\', ['c:\\', 'a', 'b', 'x', 'y']))
124124
check(['c:/a/b', 'c:/x/y'], ('c:', '\\', ['c:\\', 'x', 'y']))
125+
# Paths to files with NTFS alternate data streams
126+
check(['./c:s'], ('', '', ['c:s']))
127+
check(['cc:s'], ('', '', ['cc:s']))
128+
check(['C:c:s'], ('C:', '', ['C:', 'c:s']))
129+
check(['C:/c:s'], ('C:', '\\', ['C:\\', 'c:s']))
130+
check(['D:a', './c:b'], ('D:', '', ['D:', 'a', 'c:b']))
131+
check(['D:/a', './c:b'], ('D:', '\\', ['D:\\', 'a', 'c:b']))
125132

126133

127134
#
@@ -165,6 +172,7 @@ def test_constructor_common(self):
165172
self.assertEqual(P(P('a'), 'b'), P('a/b'))
166173
self.assertEqual(P(P('a'), P('b')), P('a/b'))
167174
self.assertEqual(P(P('a'), P('b'), P('c')), P(FakePath("a/b/c")))
175+
self.assertEqual(P(P('./a:b')), P('./a:b'))
168176

169177
def test_bytes(self):
170178
P = self.cls
@@ -814,7 +822,8 @@ class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase):
814822

815823
equivalences = _BasePurePathTest.equivalences.copy()
816824
equivalences.update({
817-
'c:a': [ ('c:', 'a'), ('c:', 'a/'), ('/', 'c:', 'a') ],
825+
'./a:b': [ ('./a:b',) ],
826+
'c:a': [ ('c:', 'a'), ('c:', 'a/'), ('.', 'c:', 'a') ],
818827
'c:/a': [
819828
('c:/', 'a'), ('c:', '/', 'a'), ('c:', '/a'),
820829
('/z', 'c:/', 'a'), ('//x/y', 'c:/', 'a'),
@@ -838,6 +847,7 @@ def test_str(self):
838847
self.assertEqual(str(p), '\\\\a\\b\\c\\d')
839848

840849
def test_str_subclass(self):
850+
self._check_str_subclass('.\\a:b')
841851
self._check_str_subclass('c:')
842852
self._check_str_subclass('c:a')
843853
self._check_str_subclass('c:a\\b.txt')
@@ -1005,6 +1015,7 @@ def test_drive(self):
10051015
self.assertEqual(P('//a/b').drive, '\\\\a\\b')
10061016
self.assertEqual(P('//a/b/').drive, '\\\\a\\b')
10071017
self.assertEqual(P('//a/b/c/d').drive, '\\\\a\\b')
1018+
self.assertEqual(P('./c:a').drive, '')
10081019

10091020
def test_root(self):
10101021
P = self.cls
@@ -1341,6 +1352,14 @@ def test_join(self):
13411352
self.assertEqual(pp, P('C:/a/b/x/y'))
13421353
pp = p.joinpath('c:/x/y')
13431354
self.assertEqual(pp, P('C:/x/y'))
1355+
# Joining with files with NTFS data streams => the filename should
1356+
# not be parsed as a drive letter
1357+
pp = p.joinpath(P('./d:s'))
1358+
self.assertEqual(pp, P('C:/a/b/d:s'))
1359+
pp = p.joinpath(P('./dd:s'))
1360+
self.assertEqual(pp, P('C:/a/b/dd:s'))
1361+
pp = p.joinpath(P('E:d:s'))
1362+
self.assertEqual(pp, P('E:d:s'))
13441363

13451364
def test_div(self):
13461365
# Basically the same as joinpath().
@@ -1361,6 +1380,11 @@ def test_div(self):
13611380
# the second path is relative.
13621381
self.assertEqual(p / 'c:x/y', P('C:/a/b/x/y'))
13631382
self.assertEqual(p / 'c:/x/y', P('C:/x/y'))
1383+
# Joining with files with NTFS data streams => the filename should
1384+
# not be parsed as a drive letter
1385+
self.assertEqual(p / P('./d:s'), P('C:/a/b/d:s'))
1386+
self.assertEqual(p / P('./dd:s'), P('C:/a/b/dd:s'))
1387+
self.assertEqual(p / P('E:d:s'), P('E:d:s'))
13641388

13651389
def test_is_reserved(self):
13661390
P = self.cls
@@ -1626,6 +1650,8 @@ def test_expanduser_common(self):
16261650
self.assertEqual(p.expanduser(), p)
16271651
p = P(P('').absolute().anchor) / '~'
16281652
self.assertEqual(p.expanduser(), p)
1653+
p = P('~/a:b')
1654+
self.assertEqual(p.expanduser(), P(os.path.expanduser('~'), './a:b'))
16291655

16301656
def test_exists(self):
16311657
P = self.cls
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix handling of Windows filenames that resemble drives, such as ``./a:b``,
2+
in :mod:`pathlib`.

0 commit comments

Comments
 (0)