Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 12 commits into from
Mar 10, 2023
8 changes: 5 additions & 3 deletions Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
barneygale marked this conversation as resolved.
Show resolved Hide resolved
parts = ['.'] + parts
return cls._flavour.sep.join(parts)

def __str__(self):
"""Return the string representation of the path, suitable for
Expand Down Expand Up @@ -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

Expand Down
28 changes: 27 additions & 1 deletion Lib/test/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']))


#
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'),
Expand All @@ -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')
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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().
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix handling of Windows filenames that resemble drives, such as ``./a:b``,
in :mod:`pathlib`.