Skip to content

Commit 8af8f52

Browse files
barneygaleeryksun
andauthored
GH-78079: Fix UNC device path root normalization in pathlib (GH-102003)
We no longer add a root to device paths such as `//./PhysicalDrive0`, `//?/BootPartition` and `//./c:` while normalizing. We also avoid adding a root to incomplete UNC share paths, like `//`, `//a` and `//a/`. Co-authored-by: Eryk Sun <[email protected]>
1 parent 7c1b0a4 commit 8af8f52

File tree

4 files changed

+43
-3
lines changed

4 files changed

+43
-3
lines changed

Lib/pathlib.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -322,9 +322,14 @@ def _parse_path(cls, path):
322322
if altsep:
323323
path = path.replace(altsep, sep)
324324
drv, root, rel = cls._flavour.splitroot(path)
325-
if drv.startswith(sep):
326-
# pathlib assumes that UNC paths always have a root.
327-
root = sep
325+
if not root and drv.startswith(sep) and not drv.endswith(sep):
326+
drv_parts = drv.split(sep)
327+
if len(drv_parts) == 4 and drv_parts[2] not in '?.':
328+
# e.g. //server/share
329+
root = sep
330+
elif len(drv_parts) == 6:
331+
# e.g. //?/unc/server/share
332+
root = sep
328333
parsed = [sys.intern(str(x)) for x in rel.split(sep) if x and x != '.']
329334
return drv, root, parsed
330335

Lib/test/test_ntpath.py

+8
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ def test_splitroot(self):
169169

170170
# gh-81790: support device namespace, including UNC drives.
171171
tester('ntpath.splitroot("//?/c:")', ("//?/c:", "", ""))
172+
tester('ntpath.splitroot("//./c:")', ("//./c:", "", ""))
172173
tester('ntpath.splitroot("//?/c:/")', ("//?/c:", "/", ""))
173174
tester('ntpath.splitroot("//?/c:/dir")', ("//?/c:", "/", "dir"))
174175
tester('ntpath.splitroot("//?/UNC")', ("//?/UNC", "", ""))
@@ -179,8 +180,12 @@ def test_splitroot(self):
179180
tester('ntpath.splitroot("//?/VOLUME{00000000-0000-0000-0000-000000000000}/spam")',
180181
('//?/VOLUME{00000000-0000-0000-0000-000000000000}', '/', 'spam'))
181182
tester('ntpath.splitroot("//?/BootPartition/")', ("//?/BootPartition", "/", ""))
183+
tester('ntpath.splitroot("//./BootPartition/")', ("//./BootPartition", "/", ""))
184+
tester('ntpath.splitroot("//./PhysicalDrive0")', ("//./PhysicalDrive0", "", ""))
185+
tester('ntpath.splitroot("//./nul")', ("//./nul", "", ""))
182186

183187
tester('ntpath.splitroot("\\\\?\\c:")', ("\\\\?\\c:", "", ""))
188+
tester('ntpath.splitroot("\\\\.\\c:")', ("\\\\.\\c:", "", ""))
184189
tester('ntpath.splitroot("\\\\?\\c:\\")', ("\\\\?\\c:", "\\", ""))
185190
tester('ntpath.splitroot("\\\\?\\c:\\dir")', ("\\\\?\\c:", "\\", "dir"))
186191
tester('ntpath.splitroot("\\\\?\\UNC")', ("\\\\?\\UNC", "", ""))
@@ -193,6 +198,9 @@ def test_splitroot(self):
193198
tester('ntpath.splitroot("\\\\?\\VOLUME{00000000-0000-0000-0000-000000000000}\\spam")',
194199
('\\\\?\\VOLUME{00000000-0000-0000-0000-000000000000}', '\\', 'spam'))
195200
tester('ntpath.splitroot("\\\\?\\BootPartition\\")', ("\\\\?\\BootPartition", "\\", ""))
201+
tester('ntpath.splitroot("\\\\.\\BootPartition\\")', ("\\\\.\\BootPartition", "\\", ""))
202+
tester('ntpath.splitroot("\\\\.\\PhysicalDrive0")', ("\\\\.\\PhysicalDrive0", "", ""))
203+
tester('ntpath.splitroot("\\\\.\\nul")', ("\\\\.\\nul", "", ""))
196204

197205
# gh-96290: support partial/invalid UNC drives
198206
tester('ntpath.splitroot("//")', ("//", "", "")) # empty server & missing share

Lib/test/test_pathlib.py

+24
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,9 @@ def test_drive_root_parts(self):
810810
check(('c:/a',), 'c:', '\\', ('c:\\', 'a'))
811811
check(('/a',), '', '\\', ('\\', 'a'))
812812
# UNC paths.
813+
check(('//',), '\\\\', '', ('\\\\',))
814+
check(('//a',), '\\\\a', '', ('\\\\a',))
815+
check(('//a/',), '\\\\a\\', '', ('\\\\a\\',))
813816
check(('//a/b',), '\\\\a\\b', '\\', ('\\\\a\\b\\',))
814817
check(('//a/b/',), '\\\\a\\b', '\\', ('\\\\a\\b\\',))
815818
check(('//a/b/c',), '\\\\a\\b', '\\', ('\\\\a\\b\\', 'c'))
@@ -823,12 +826,26 @@ def test_drive_root_parts(self):
823826
# UNC paths.
824827
check(('a', '//b/c//', 'd'), '\\\\b\\c', '\\', ('\\\\b\\c\\', 'd'))
825828
# Extended paths.
829+
check(('//./c:',), '\\\\.\\c:', '', ('\\\\.\\c:',))
826830
check(('//?/c:/',), '\\\\?\\c:', '\\', ('\\\\?\\c:\\',))
827831
check(('//?/c:/a',), '\\\\?\\c:', '\\', ('\\\\?\\c:\\', 'a'))
828832
check(('//?/c:/a', '/b'), '\\\\?\\c:', '\\', ('\\\\?\\c:\\', 'b'))
829833
# Extended UNC paths (format is "\\?\UNC\server\share").
834+
check(('//?',), '\\\\?', '', ('\\\\?',))
835+
check(('//?/',), '\\\\?\\', '', ('\\\\?\\',))
836+
check(('//?/UNC',), '\\\\?\\UNC', '', ('\\\\?\\UNC',))
837+
check(('//?/UNC/',), '\\\\?\\UNC\\', '', ('\\\\?\\UNC\\',))
838+
check(('//?/UNC/b',), '\\\\?\\UNC\\b', '', ('\\\\?\\UNC\\b',))
839+
check(('//?/UNC/b/',), '\\\\?\\UNC\\b\\', '', ('\\\\?\\UNC\\b\\',))
830840
check(('//?/UNC/b/c',), '\\\\?\\UNC\\b\\c', '\\', ('\\\\?\\UNC\\b\\c\\',))
841+
check(('//?/UNC/b/c/',), '\\\\?\\UNC\\b\\c', '\\', ('\\\\?\\UNC\\b\\c\\',))
831842
check(('//?/UNC/b/c/d',), '\\\\?\\UNC\\b\\c', '\\', ('\\\\?\\UNC\\b\\c\\', 'd'))
843+
# UNC device paths
844+
check(('//./BootPartition/',), '\\\\.\\BootPartition', '\\', ('\\\\.\\BootPartition\\',))
845+
check(('//?/BootPartition/',), '\\\\?\\BootPartition', '\\', ('\\\\?\\BootPartition\\',))
846+
check(('//./PhysicalDrive0',), '\\\\.\\PhysicalDrive0', '', ('\\\\.\\PhysicalDrive0',))
847+
check(('//?/Volume{}/',), '\\\\?\\Volume{}', '\\', ('\\\\?\\Volume{}\\',))
848+
check(('//./nul',), '\\\\.\\nul', '', ('\\\\.\\nul',))
832849
# Second part has a root but not drive.
833850
check(('a', '/b', 'c'), '', '\\', ('\\', 'b', 'c'))
834851
check(('Z:/a', '/b', 'c'), 'Z:', '\\', ('Z:\\', 'b', 'c'))
@@ -1371,6 +1388,13 @@ def test_join(self):
13711388
self.assertEqual(pp, P('C:/a/b/dd:s'))
13721389
pp = p.joinpath(P('E:d:s'))
13731390
self.assertEqual(pp, P('E:d:s'))
1391+
# Joining onto a UNC path with no root
1392+
pp = P('//').joinpath('server')
1393+
self.assertEqual(pp, P('//server'))
1394+
pp = P('//server').joinpath('share')
1395+
self.assertEqual(pp, P('//server/share'))
1396+
pp = P('//./BootPartition').joinpath('Windows')
1397+
self.assertEqual(pp, P('//./BootPartition/Windows'))
13741398

13751399
def test_div(self):
13761400
# Basically the same as joinpath().
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix incorrect normalization of UNC device path roots, and partial UNC share
2+
path roots, in :class:`pathlib.PurePath`. Pathlib no longer appends a
3+
trailing slash to such paths.

0 commit comments

Comments
 (0)