Skip to content

Commit 2ba0fd5

Browse files
authored
gh-81790: support "UNC" device paths in ntpath.splitdrive() (GH-91882)
1 parent 53a8b17 commit 2ba0fd5

File tree

5 files changed

+39
-66
lines changed

5 files changed

+39
-66
lines changed

Doc/library/os.path.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ the :mod:`glob` module.)
469469
("c:", "/dir")
470470

471471
If the path contains a UNC path, drive will contain the host name
472-
and share, up to but not including the fourth separator::
472+
and share::
473473

474474
>>> splitdrive("//host/computer/dir")
475475
("//host/computer", "/dir")

Lib/ntpath.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -172,17 +172,23 @@ def splitdrive(p):
172172
sep = b'\\'
173173
altsep = b'/'
174174
colon = b':'
175+
unc_prefix = b'\\\\?\\UNC'
175176
else:
176177
sep = '\\'
177178
altsep = '/'
178179
colon = ':'
180+
unc_prefix = '\\\\?\\UNC'
179181
normp = p.replace(altsep, sep)
180182
if (normp[0:2] == sep*2) and (normp[2:3] != sep):
181183
# is a UNC path:
182184
# vvvvvvvvvvvvvvvvvvvv drive letter or UNC path
183185
# \\machine\mountpoint\directory\etc\...
184186
# directory ^^^^^^^^^^^^^^^
185-
index = normp.find(sep, 2)
187+
if normp[:8].upper().rstrip(sep) == unc_prefix:
188+
start = 8
189+
else:
190+
start = 2
191+
index = normp.find(sep, start)
186192
if index == -1:
187193
return p[:0], p
188194
index2 = normp.find(sep, index + 1)

Lib/pathlib.py

+4-64
Original file line numberDiff line numberDiff line change
@@ -120,68 +120,18 @@ class _WindowsFlavour(_Flavour):
120120

121121
is_supported = (os.name == 'nt')
122122

123-
drive_letters = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
124-
ext_namespace_prefix = '\\\\?\\'
125-
126123
reserved_names = (
127124
{'CON', 'PRN', 'AUX', 'NUL', 'CONIN$', 'CONOUT$'} |
128125
{'COM%s' % c for c in '123456789\xb9\xb2\xb3'} |
129126
{'LPT%s' % c for c in '123456789\xb9\xb2\xb3'}
130127
)
131128

132-
# Interesting findings about extended paths:
133-
# * '\\?\c:\a' is an extended path, which bypasses normal Windows API
134-
# path processing. Thus relative paths are not resolved and slash is not
135-
# translated to backslash. It has the native NT path limit of 32767
136-
# characters, but a bit less after resolving device symbolic links,
137-
# such as '\??\C:' => '\Device\HarddiskVolume2'.
138-
# * '\\?\c:/a' looks for a device named 'C:/a' because slash is a
139-
# regular name character in the object namespace.
140-
# * '\\?\c:\foo/bar' is invalid because '/' is illegal in NT filesystems.
141-
# The only path separator at the filesystem level is backslash.
142-
# * '//?/c:\a' and '//?/c:/a' are effectively equivalent to '\\.\c:\a' and
143-
# thus limited to MAX_PATH.
144-
# * Prior to Windows 8, ANSI API bytes paths are limited to MAX_PATH,
145-
# even with the '\\?\' prefix.
146-
147129
def splitroot(self, part, sep=sep):
148-
first = part[0:1]
149-
second = part[1:2]
150-
if (second == sep and first == sep):
151-
# XXX extended paths should also disable the collapsing of "."
152-
# components (according to MSDN docs).
153-
prefix, part = self._split_extended_path(part)
154-
first = part[0:1]
155-
second = part[1:2]
130+
drv, rest = self.pathmod.splitdrive(part)
131+
if drv[:1] == sep or rest[:1] == sep:
132+
return drv, sep, rest.lstrip(sep)
156133
else:
157-
prefix = ''
158-
third = part[2:3]
159-
if (second == sep and first == sep and third != sep):
160-
# is a UNC path:
161-
# vvvvvvvvvvvvvvvvvvvvv root
162-
# \\machine\mountpoint\directory\etc\...
163-
# directory ^^^^^^^^^^^^^^
164-
index = part.find(sep, 2)
165-
if index != -1:
166-
index2 = part.find(sep, index + 1)
167-
# a UNC path can't have two slashes in a row
168-
# (after the initial two)
169-
if index2 != index + 1:
170-
if index2 == -1:
171-
index2 = len(part)
172-
if prefix:
173-
return prefix + part[1:index2], sep, part[index2+1:]
174-
else:
175-
return part[:index2], sep, part[index2+1:]
176-
drv = root = ''
177-
if second == ':' and first in self.drive_letters:
178-
drv = part[:2]
179-
part = part[2:]
180-
first = third
181-
if first == sep:
182-
root = first
183-
part = part.lstrip(sep)
184-
return prefix + drv, root, part
134+
return drv, '', rest
185135

186136
def casefold(self, s):
187137
return s.lower()
@@ -192,16 +142,6 @@ def casefold_parts(self, parts):
192142
def compile_pattern(self, pattern):
193143
return re.compile(fnmatch.translate(pattern), re.IGNORECASE).fullmatch
194144

195-
def _split_extended_path(self, s, ext_prefix=ext_namespace_prefix):
196-
prefix = ''
197-
if s.startswith(ext_prefix):
198-
prefix = s[:4]
199-
s = s[4:]
200-
if s.startswith('UNC\\'):
201-
prefix += s[:3]
202-
s = '\\' + s[3:]
203-
return prefix, s
204-
205145
def is_reserved(self, parts):
206146
# NOTE: the rules for reserved names seem somewhat complicated
207147
# (e.g. r"..\NUL" is reserved but not r"foo\NUL" if "foo" does not

Lib/test/test_ntpath.py

+25
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,31 @@ def test_splitdrive(self):
117117
# Issue #19911: UNC part containing U+0130
118118
self.assertEqual(ntpath.splitdrive('//conky/MOUNTPOİNT/foo/bar'),
119119
('//conky/MOUNTPOİNT', '/foo/bar'))
120+
# gh-81790: support device namespace, including UNC drives.
121+
tester('ntpath.splitdrive("//?/c:")', ("//?/c:", ""))
122+
tester('ntpath.splitdrive("//?/c:/")', ("//?/c:", "/"))
123+
tester('ntpath.splitdrive("//?/c:/dir")', ("//?/c:", "/dir"))
124+
tester('ntpath.splitdrive("//?/UNC")', ("", "//?/UNC"))
125+
tester('ntpath.splitdrive("//?/UNC/")', ("", "//?/UNC/"))
126+
tester('ntpath.splitdrive("//?/UNC/server/")', ("//?/UNC/server/", ""))
127+
tester('ntpath.splitdrive("//?/UNC/server/share")', ("//?/UNC/server/share", ""))
128+
tester('ntpath.splitdrive("//?/UNC/server/share/dir")', ("//?/UNC/server/share", "/dir"))
129+
tester('ntpath.splitdrive("//?/VOLUME{00000000-0000-0000-0000-000000000000}/spam")',
130+
('//?/VOLUME{00000000-0000-0000-0000-000000000000}', '/spam'))
131+
tester('ntpath.splitdrive("//?/BootPartition/")', ("//?/BootPartition", "/"))
132+
133+
tester('ntpath.splitdrive("\\\\?\\c:")', ("\\\\?\\c:", ""))
134+
tester('ntpath.splitdrive("\\\\?\\c:\\")', ("\\\\?\\c:", "\\"))
135+
tester('ntpath.splitdrive("\\\\?\\c:\\dir")', ("\\\\?\\c:", "\\dir"))
136+
tester('ntpath.splitdrive("\\\\?\\UNC")', ("", "\\\\?\\UNC"))
137+
tester('ntpath.splitdrive("\\\\?\\UNC\\")', ("", "\\\\?\\UNC\\"))
138+
tester('ntpath.splitdrive("\\\\?\\UNC\\server\\")', ("\\\\?\\UNC\\server\\", ""))
139+
tester('ntpath.splitdrive("\\\\?\\UNC\\server\\share")', ("\\\\?\\UNC\\server\\share", ""))
140+
tester('ntpath.splitdrive("\\\\?\\UNC\\server\\share\\dir")',
141+
("\\\\?\\UNC\\server\\share", "\\dir"))
142+
tester('ntpath.splitdrive("\\\\?\\VOLUME{00000000-0000-0000-0000-000000000000}\\spam")',
143+
('\\\\?\\VOLUME{00000000-0000-0000-0000-000000000000}', '\\spam'))
144+
tester('ntpath.splitdrive("\\\\?\\BootPartition\\")', ("\\\\?\\BootPartition", "\\"))
120145

121146
def test_split(self):
122147
tester('ntpath.split("c:\\foo\\bar")', ('c:\\foo', 'bar'))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:func:`os.path.splitdrive` now understands DOS device paths with UNC
2+
links (beginning ``\\?\UNC\``). Contributed by Barney Gale.

0 commit comments

Comments
 (0)