Skip to content

Commit 1fb70c7

Browse files
barneygaleeryksun
andcommitted
gh-96290: support partial/invalid UNC drives in normpath() and splitdrive()
This brings the Python implementation of `ntpath.normpath()` in line with the C implementation added in 99fcf15 Co-authored-by: Eryk Sun <eryksun@gmail.com>
1 parent 797edb2 commit 1fb70c7

File tree

5 files changed

+72
-70
lines changed

5 files changed

+72
-70
lines changed

Lib/ntpath.py

+37-51
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,20 @@ def normcase(s):
8787
def isabs(s):
8888
"""Test whether a path is absolute"""
8989
s = os.fspath(s)
90-
# Paths beginning with \\?\ are always absolute, but do not
91-
# necessarily contain a drive.
9290
if isinstance(s, bytes):
93-
if s.replace(b'/', b'\\').startswith(b'\\\\?\\'):
94-
return True
91+
sep = b'\\'
92+
altsep = b'/'
93+
colon_sep = b':\\'
9594
else:
96-
if s.replace('/', '\\').startswith('\\\\?\\'):
97-
return True
98-
s = splitdrive(s)[1]
99-
return len(s) > 0 and s[0] and s[0] in _get_bothseps(s)
95+
sep = '\\'
96+
altsep = '/'
97+
colon_sep = ':\\'
98+
s = s[:3].replace(altsep, sep)
99+
# Absolute: UNC, device, and paths with a drive and root.
100+
# LEGACY BUG: isabs("/x") should be false since the path has no drive.
101+
if s.startswith(sep) or s.startswith(colon_sep, 1):
102+
return True
103+
return False
100104

101105

102106
# Join two (or more) paths.
@@ -167,40 +171,31 @@ def splitdrive(p):
167171
168172
"""
169173
p = os.fspath(p)
170-
if len(p) >= 2:
171-
if isinstance(p, bytes):
172-
sep = b'\\'
173-
altsep = b'/'
174-
colon = b':'
175-
unc_prefix = b'\\\\?\\UNC'
176-
else:
177-
sep = '\\'
178-
altsep = '/'
179-
colon = ':'
180-
unc_prefix = '\\\\?\\UNC'
181-
normp = p.replace(altsep, sep)
182-
if (normp[0:2] == sep*2) and (normp[2:3] != sep):
183-
# is a UNC path:
184-
# vvvvvvvvvvvvvvvvvvvv drive letter or UNC path
185-
# \\machine\mountpoint\directory\etc\...
186-
# directory ^^^^^^^^^^^^^^^
187-
if normp[:8].upper().rstrip(sep) == unc_prefix:
188-
start = 8
189-
else:
190-
start = 2
191-
index = normp.find(sep, start)
192-
if index == -1:
193-
return p[:0], p
194-
index2 = normp.find(sep, index + 1)
195-
# a UNC path can't have two slashes in a row
196-
# (after the initial two)
197-
if index2 == index + 1:
198-
return p[:0], p
199-
if index2 == -1:
200-
index2 = len(p)
201-
return p[:index2], p[index2:]
202-
if normp[1:2] == colon:
203-
return p[:2], p[2:]
174+
if isinstance(p, bytes):
175+
sep = b'\\'
176+
altsep = b'/'
177+
colon = b':'
178+
unc_prefix = b'\\\\?\\UNC\\'
179+
else:
180+
sep = '\\'
181+
altsep = '/'
182+
colon = ':'
183+
unc_prefix = '\\\\?\\UNC\\'
184+
normp = p.replace(altsep, sep)
185+
if normp[1:2] == colon and normp[:1] != sep:
186+
# Drive-letter logical drives, e.g. X:
187+
return p[:2], p[2:]
188+
if normp[:2] == sep * 2:
189+
# UNC drives, e.g. \\server\share or \\?\UNC\server\share
190+
# Device drives, e.g. \\.\device or \\?\device
191+
start = 8 if normp[:8].upper() == unc_prefix else 2
192+
index = normp.find(sep, start)
193+
if index == -1:
194+
return p, p[:0]
195+
index = normp.find(sep, index + 1)
196+
if index == -1:
197+
return p, p[:0]
198+
return p[:index], p[index:]
204199
return p[:0], p
205200

206201

@@ -523,20 +518,11 @@ def normpath(path):
523518
altsep = b'/'
524519
curdir = b'.'
525520
pardir = b'..'
526-
special_prefixes = (b'\\\\.\\', b'\\\\?\\')
527521
else:
528522
sep = '\\'
529523
altsep = '/'
530524
curdir = '.'
531525
pardir = '..'
532-
special_prefixes = ('\\\\.\\', '\\\\?\\')
533-
if path.startswith(special_prefixes):
534-
# in the case of paths with these prefixes:
535-
# \\.\ -> device names
536-
# \\?\ -> literal paths
537-
# do not do any normalization, but return the path
538-
# unchanged apart from the call to os.fspath()
539-
return path
540526
path = path.replace(altsep, sep)
541527
prefix, path = splitdrive(path)
542528

Lib/test/test_ntpath.py

+22-8
Original file line numberDiff line numberDiff line change
@@ -107,22 +107,22 @@ def test_splitdrive(self):
107107
tester('ntpath.splitdrive("//conky/mountpoint/foo/bar")',
108108
('//conky/mountpoint', '/foo/bar'))
109109
tester('ntpath.splitdrive("\\\\\\conky\\mountpoint\\foo\\bar")',
110-
('', '\\\\\\conky\\mountpoint\\foo\\bar'))
110+
('\\\\\\conky', '\\mountpoint\\foo\\bar'))
111111
tester('ntpath.splitdrive("///conky/mountpoint/foo/bar")',
112-
('', '///conky/mountpoint/foo/bar'))
112+
('///conky', '/mountpoint/foo/bar'))
113113
tester('ntpath.splitdrive("\\\\conky\\\\mountpoint\\foo\\bar")',
114-
('', '\\\\conky\\\\mountpoint\\foo\\bar'))
114+
('\\\\conky\\', '\\mountpoint\\foo\\bar'))
115115
tester('ntpath.splitdrive("//conky//mountpoint/foo/bar")',
116-
('', '//conky//mountpoint/foo/bar'))
116+
('//conky/', '/mountpoint/foo/bar'))
117117
# Issue #19911: UNC part containing U+0130
118118
self.assertEqual(ntpath.splitdrive('//conky/MOUNTPOİNT/foo/bar'),
119119
('//conky/MOUNTPOİNT', '/foo/bar'))
120120
# gh-81790: support device namespace, including UNC drives.
121121
tester('ntpath.splitdrive("//?/c:")', ("//?/c:", ""))
122122
tester('ntpath.splitdrive("//?/c:/")', ("//?/c:", "/"))
123123
tester('ntpath.splitdrive("//?/c:/dir")', ("//?/c:", "/dir"))
124-
tester('ntpath.splitdrive("//?/UNC")', ("", "//?/UNC"))
125-
tester('ntpath.splitdrive("//?/UNC/")', ("", "//?/UNC/"))
124+
tester('ntpath.splitdrive("//?/UNC")', ("//?/UNC", ""))
125+
tester('ntpath.splitdrive("//?/UNC/")', ("//?/UNC/", ""))
126126
tester('ntpath.splitdrive("//?/UNC/server/")', ("//?/UNC/server/", ""))
127127
tester('ntpath.splitdrive("//?/UNC/server/share")', ("//?/UNC/server/share", ""))
128128
tester('ntpath.splitdrive("//?/UNC/server/share/dir")', ("//?/UNC/server/share", "/dir"))
@@ -133,8 +133,8 @@ def test_splitdrive(self):
133133
tester('ntpath.splitdrive("\\\\?\\c:")', ("\\\\?\\c:", ""))
134134
tester('ntpath.splitdrive("\\\\?\\c:\\")', ("\\\\?\\c:", "\\"))
135135
tester('ntpath.splitdrive("\\\\?\\c:\\dir")', ("\\\\?\\c:", "\\dir"))
136-
tester('ntpath.splitdrive("\\\\?\\UNC")', ("", "\\\\?\\UNC"))
137-
tester('ntpath.splitdrive("\\\\?\\UNC\\")', ("", "\\\\?\\UNC\\"))
136+
tester('ntpath.splitdrive("\\\\?\\UNC")', ("\\\\?\\UNC", ""))
137+
tester('ntpath.splitdrive("\\\\?\\UNC\\")', ("\\\\?\\UNC\\", ""))
138138
tester('ntpath.splitdrive("\\\\?\\UNC\\server\\")', ("\\\\?\\UNC\\server\\", ""))
139139
tester('ntpath.splitdrive("\\\\?\\UNC\\server\\share")', ("\\\\?\\UNC\\server\\share", ""))
140140
tester('ntpath.splitdrive("\\\\?\\UNC\\server\\share\\dir")',
@@ -143,6 +143,13 @@ def test_splitdrive(self):
143143
('\\\\?\\VOLUME{00000000-0000-0000-0000-000000000000}', '\\spam'))
144144
tester('ntpath.splitdrive("\\\\?\\BootPartition\\")', ("\\\\?\\BootPartition", "\\"))
145145

146+
# gh-96290: support partial/invalid UNC drives
147+
tester('ntpath.splitdrive("//")', ("//", "")) # empty server & missing share
148+
tester('ntpath.splitdrive("///")', ("///", "")) # empty server & empty share
149+
tester('ntpath.splitdrive("///y")', ("///y", "")) # empty server & valid share
150+
tester('ntpath.splitdrive("//x")', ("//x", "")) # valid server & missing share
151+
tester('ntpath.splitdrive("//x/")', ("//x/", "")) # valid server & empty share
152+
146153
def test_split(self):
147154
tester('ntpath.split("c:\\foo\\bar")', ('c:\\foo', 'bar'))
148155
tester('ntpath.split("\\\\conky\\mountpoint\\foo\\bar")',
@@ -270,6 +277,13 @@ def test_normpath(self):
270277
tester("ntpath.normpath('//server/share/../..')", '\\\\server\\share\\')
271278
tester("ntpath.normpath('//server/share/../../')", '\\\\server\\share\\')
272279

280+
# gh-96290: don't normalize partial/invalid UNC drives
281+
tester("ntpath.normpath('\\\\foo\\bar')", '\\\\foo\\bar')
282+
tester("ntpath.normpath('\\\\foo\\\\')", '\\\\foo\\\\')
283+
tester("ntpath.normpath('\\\\foo\\')", '\\\\foo\\')
284+
tester("ntpath.normpath('\\\\foo')", '\\\\foo')
285+
tester("ntpath.normpath('\\\\')", '\\\\')
286+
273287
def test_realpath_curdir(self):
274288
expected = ntpath.normpath(os.getcwd())
275289
tester("ntpath.realpath('.')", expected)

Lib/test/test_pathlib.py

+4-7
Original file line numberDiff line numberDiff line change
@@ -151,19 +151,16 @@ def test_splitroot(self):
151151
self.assertEqual(f('c:a\\b'), ('c:', '', 'a\\b'))
152152
self.assertEqual(f('c:\\a\\b'), ('c:', '\\', 'a\\b'))
153153
# Redundant slashes in the root are collapsed.
154-
self.assertEqual(f('\\\\a'), ('', '\\', 'a'))
155-
self.assertEqual(f('\\\\\\a/b'), ('', '\\', 'a/b'))
156154
self.assertEqual(f('c:\\\\a'), ('c:', '\\', 'a'))
157155
self.assertEqual(f('c:\\\\\\a/b'), ('c:', '\\', 'a/b'))
158156
# Valid UNC paths.
159157
self.assertEqual(f('\\\\a\\b'), ('\\\\a\\b', '\\', ''))
160158
self.assertEqual(f('\\\\a\\b\\'), ('\\\\a\\b', '\\', ''))
161159
self.assertEqual(f('\\\\a\\b\\c\\d'), ('\\\\a\\b', '\\', 'c\\d'))
162-
# These are non-UNC paths (according to ntpath.py and test_ntpath).
163-
# However, command.com says such paths are invalid, so it's
160+
# Invalid or partial UNC paths. Per gh-96290, it's
164161
# difficult to know what the right semantics are.
165-
self.assertEqual(f('\\\\\\a\\b'), ('', '\\', 'a\\b'))
166-
self.assertEqual(f('\\\\a'), ('', '\\', 'a'))
162+
self.assertEqual(f('\\\\\\a\\b'), ('\\\\\\a', '\\', 'b'))
163+
self.assertEqual(f('\\\\a'), ('\\\\a', '\\', ''))
167164

168165

169166
#
@@ -182,7 +179,7 @@ class _BasePurePathTest(object):
182179
('', 'a', 'b'), ('a', '', 'b'), ('a', 'b', ''),
183180
],
184181
'/b/c/d': [
185-
('a', '/b/c', 'd'), ('a', '///b//c', 'd/'),
182+
('a', '/b/c', 'd'),
186183
('/a', '/b/c', 'd'),
187184
# Empty components get removed.
188185
('/', 'b', '', 'c/d'), ('/', '', 'b/c/d'), ('', '/b/c/d'),

Lib/test/test_zipfile/test_core.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1469,10 +1469,10 @@ def test_extract_hackers_arcnames_windows_only(self):
14691469
(r'C:\foo\bar', 'foo/bar'),
14701470
(r'//conky/mountpoint/foo/bar', 'foo/bar'),
14711471
(r'\\conky\mountpoint\foo\bar', 'foo/bar'),
1472-
(r'///conky/mountpoint/foo/bar', 'conky/mountpoint/foo/bar'),
1473-
(r'\\\conky\mountpoint\foo\bar', 'conky/mountpoint/foo/bar'),
1474-
(r'//conky//mountpoint/foo/bar', 'conky/mountpoint/foo/bar'),
1475-
(r'\\conky\\mountpoint\foo\bar', 'conky/mountpoint/foo/bar'),
1472+
(r'///conky/mountpoint/foo/bar', 'mountpoint/foo/bar'),
1473+
(r'\\\conky\mountpoint\foo\bar', 'mountpoint/foo/bar'),
1474+
(r'//conky//mountpoint/foo/bar', 'mountpoint/foo/bar'),
1475+
(r'\\conky\\mountpoint\foo\bar', 'mountpoint/foo/bar'),
14761476
(r'//?/C:/foo/bar', 'foo/bar'),
14771477
(r'\\?\C:\foo\bar', 'foo/bar'),
14781478
(r'C:/../C:/foo/bar', 'C_/foo/bar'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix handling of partial and invalid UNC drives in ``ntpath.splitdrive()``, and in
2+
``ntpath.normpath()`` on non-Windows systems. Paths such as '\\server' and '\\' are now considered
3+
by ``splitdrive()`` to contain only a drive, and consequently are not modified by ``normpath()`` on
4+
non-Windows systems. The behaviour of ``normpath()`` on Windows systems is unaffected, as native
5+
OS APIs are used. Patch by Eryk Sun, with contributions by Barney Gale.

0 commit comments

Comments
 (0)