Skip to content

GH-78079: Fix UNC device path root normalization in pathlib #102003

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

Merged
merged 10 commits into from
Apr 14, 2023
Merged
11 changes: 8 additions & 3 deletions Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,14 @@ def _parse_parts(cls, parts):
if altsep:
path = path.replace(altsep, sep)
drv, root, rel = cls._flavour.splitroot(path)
if drv.startswith(sep):
# pathlib assumes that UNC paths always have a root.
root = sep
if drv and not root and not drv.endswith(sep):
drv_parts = drv.split(sep)
if len(drv_parts) == 4 and drv_parts[2] not in '?.':
# e.g. //server/share
root = sep
elif len(drv_parts) == 6:
# e.g. //?/unc/server/share
root = sep
unfiltered_parsed = [drv + root] + rel.split(sep)
parsed = [sys.intern(x) for x in unfiltered_parsed if x and x != '.']
return drv, root, parsed
Expand Down
8 changes: 8 additions & 0 deletions Lib/test/test_ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ def test_splitroot(self):

# gh-81790: support device namespace, including UNC drives.
tester('ntpath.splitroot("//?/c:")', ("//?/c:", "", ""))
tester('ntpath.splitroot("//./c:")', ("//./c:", "", ""))
tester('ntpath.splitroot("//?/c:/")', ("//?/c:", "/", ""))
tester('ntpath.splitroot("//?/c:/dir")', ("//?/c:", "/", "dir"))
tester('ntpath.splitroot("//?/UNC")', ("//?/UNC", "", ""))
Expand All @@ -178,8 +179,12 @@ def test_splitroot(self):
tester('ntpath.splitroot("//?/VOLUME{00000000-0000-0000-0000-000000000000}/spam")',
('//?/VOLUME{00000000-0000-0000-0000-000000000000}', '/', 'spam'))
tester('ntpath.splitroot("//?/BootPartition/")', ("//?/BootPartition", "/", ""))
tester('ntpath.splitroot("//./BootPartition/")', ("//./BootPartition", "/", ""))
tester('ntpath.splitroot("//./PhysicalDrive0")', ("//./PhysicalDrive0", "", ""))
tester('ntpath.splitroot("//./nul")', ("//./nul", "", ""))

tester('ntpath.splitroot("\\\\?\\c:")', ("\\\\?\\c:", "", ""))
tester('ntpath.splitroot("\\\\.\\c:")', ("\\\\.\\c:", "", ""))
tester('ntpath.splitroot("\\\\?\\c:\\")', ("\\\\?\\c:", "\\", ""))
tester('ntpath.splitroot("\\\\?\\c:\\dir")', ("\\\\?\\c:", "\\", "dir"))
tester('ntpath.splitroot("\\\\?\\UNC")', ("\\\\?\\UNC", "", ""))
Expand All @@ -192,6 +197,9 @@ def test_splitroot(self):
tester('ntpath.splitroot("\\\\?\\VOLUME{00000000-0000-0000-0000-000000000000}\\spam")',
('\\\\?\\VOLUME{00000000-0000-0000-0000-000000000000}', '\\', 'spam'))
tester('ntpath.splitroot("\\\\?\\BootPartition\\")', ("\\\\?\\BootPartition", "\\", ""))
tester('ntpath.splitroot("\\\\.\\BootPartition\\")', ("\\\\.\\BootPartition", "\\", ""))
tester('ntpath.splitroot("\\\\.\\PhysicalDrive0")', ("\\\\.\\PhysicalDrive0", "", ""))
tester('ntpath.splitroot("\\\\.\\nul")', ("\\\\.\\nul", "", ""))

# gh-96290: support partial/invalid UNC drives
tester('ntpath.splitroot("//")', ("//", "", "")) # empty server & missing share
Expand Down
17 changes: 17 additions & 0 deletions Lib/test/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ def test_parse_parts(self):
check(['c:/a'], ('c:', '\\', ['c:\\', 'a']))
check(['/a'], ('', '\\', ['\\', 'a']))
# UNC paths.
check(['//'], ('\\\\', '', ['\\\\']))
check(['//a'], ('\\\\a', '', ['\\\\a']))
check(['//a/'], ('\\\\a\\', '', ['\\\\a\\']))
check(['//a/b'], ('\\\\a\\b', '\\', ['\\\\a\\b\\']))
check(['//a/b/'], ('\\\\a\\b', '\\', ['\\\\a\\b\\']))
check(['//a/b/c'], ('\\\\a\\b', '\\', ['\\\\a\\b\\', 'c']))
Expand All @@ -108,12 +111,26 @@ def test_parse_parts(self):
# UNC paths.
check(['a', '//b/c//', 'd'], ('\\\\b\\c', '\\', ['\\\\b\\c\\', 'd']))
# Extended paths.
check(['//./c:'], ('\\\\.\\c:', '', ['\\\\.\\c:']))
check(['//?/c:/'], ('\\\\?\\c:', '\\', ['\\\\?\\c:\\']))
check(['//?/c:/a'], ('\\\\?\\c:', '\\', ['\\\\?\\c:\\', 'a']))
check(['//?/c:/a', '/b'], ('\\\\?\\c:', '\\', ['\\\\?\\c:\\', 'b']))
# Extended UNC paths (format is "\\?\UNC\server\share").
check(['//?'], ('\\\\?', '', ['\\\\?']))
check(['//?/'], ('\\\\?\\', '', ['\\\\?\\']))
check(['//?/UNC'], ('\\\\?\\UNC', '', ['\\\\?\\UNC']))
check(['//?/UNC/'], ('\\\\?\\UNC\\', '', ['\\\\?\\UNC\\']))
check(['//?/UNC/b'], ('\\\\?\\UNC\\b', '', ['\\\\?\\UNC\\b']))
check(['//?/UNC/b/'], ('\\\\?\\UNC\\b\\', '', ['\\\\?\\UNC\\b\\']))
check(['//?/UNC/b/c'], ('\\\\?\\UNC\\b\\c', '\\', ['\\\\?\\UNC\\b\\c\\']))
check(['//?/UNC/b/c/'], ('\\\\?\\UNC\\b\\c', '\\', ['\\\\?\\UNC\\b\\c\\']))
check(['//?/UNC/b/c/d'], ('\\\\?\\UNC\\b\\c', '\\', ['\\\\?\\UNC\\b\\c\\', 'd']))
# UNC device paths
check(['//./BootPartition/'], ('\\\\.\\BootPartition', '\\', ['\\\\.\\BootPartition\\']))
check(['//?/BootPartition/'], ('\\\\?\\BootPartition', '\\', ['\\\\?\\BootPartition\\']))
check(['//./PhysicalDrive0'], ('\\\\.\\PhysicalDrive0', '', ['\\\\.\\PhysicalDrive0']))
check(['//?/Volume{}/'], ('\\\\?\\Volume{}', '\\', ['\\\\?\\Volume{}\\']))
check(['//./nul'], ('\\\\.\\nul', '', ['\\\\.\\nul']))
# Second part has a root but not drive.
check(['a', '/b', 'c'], ('', '\\', ['\\', 'b', 'c']))
check(['Z:/a', '/b', 'c'], ('Z:', '\\', ['Z:\\', 'b', 'c']))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix incorrect normalization of UNC device path roots, and partial UNC share
path roots, in :class:`pathlib.PurePath`. Pathlib no longer appends a
trailing slash to such paths.