Skip to content

Commit

Permalink
gh-96290: support partial/invalid UNC drives in normpath() and splitd…
Browse files Browse the repository at this point in the history
…rive()

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>
  • Loading branch information
barneygale and eryksun committed Dec 19, 2022
1 parent 797edb2 commit 471c47b
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 59 deletions.
88 changes: 37 additions & 51 deletions Lib/ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,20 @@ def normcase(s):
def isabs(s):
"""Test whether a path is absolute"""
s = os.fspath(s)
# Paths beginning with \\?\ are always absolute, but do not
# necessarily contain a drive.
if isinstance(s, bytes):
if s.replace(b'/', b'\\').startswith(b'\\\\?\\'):
return True
sep = b'\\'
altsep = b'/'
colon_sep = b':\\'
else:
if s.replace('/', '\\').startswith('\\\\?\\'):
return True
s = splitdrive(s)[1]
return len(s) > 0 and s[0] and s[0] in _get_bothseps(s)
sep = '\\'
altsep = '/'
colon_sep = ':\\'
s = s[:3].replace(altsep, sep)
# Absolute: UNC, device, and paths with a drive and root.
# LEGACY BUG: isabs("/x") should be false since the path has no drive.
if s.startswith(sep) or s.startswith(colon_sep, 1):
return True
return False


# Join two (or more) paths.
Expand Down Expand Up @@ -167,40 +171,31 @@ def splitdrive(p):
"""
p = os.fspath(p)
if len(p) >= 2:
if isinstance(p, bytes):
sep = b'\\'
altsep = b'/'
colon = b':'
unc_prefix = b'\\\\?\\UNC'
else:
sep = '\\'
altsep = '/'
colon = ':'
unc_prefix = '\\\\?\\UNC'
normp = p.replace(altsep, sep)
if (normp[0:2] == sep*2) and (normp[2:3] != sep):
# is a UNC path:
# vvvvvvvvvvvvvvvvvvvv drive letter or UNC path
# \\machine\mountpoint\directory\etc\...
# directory ^^^^^^^^^^^^^^^
if normp[:8].upper().rstrip(sep) == unc_prefix:
start = 8
else:
start = 2
index = normp.find(sep, start)
if index == -1:
return p[:0], p
index2 = normp.find(sep, index + 1)
# a UNC path can't have two slashes in a row
# (after the initial two)
if index2 == index + 1:
return p[:0], p
if index2 == -1:
index2 = len(p)
return p[:index2], p[index2:]
if normp[1:2] == colon:
return p[:2], p[2:]
if isinstance(p, bytes):
sep = b'\\'
altsep = b'/'
colon = b':'
unc_prefix = b'\\\\?\\UNC\\'
else:
sep = '\\'
altsep = '/'
colon = ':'
unc_prefix = '\\\\?\\UNC\\'
normp = p.replace(altsep, sep)
if normp[1:2] == colon and normp[:1] != sep:
# Drive-letter logical drives, e.g. X:
return p[:2], p[2:]
if normp[:2] == sep * 2:
# UNC drives, e.g. \\server\share or \\?\UNC\server\share
# Device drives, e.g. \\.\device or \\?\device
start = 8 if normp[:8].upper() == unc_prefix else 2
index = normp.find(sep, start)
if index == -1:
return p, p[:0]
index = normp.find(sep, index + 1)
if index == -1:
return p, p[:0]
return p[:index], p[index:]
return p[:0], p


Expand Down Expand Up @@ -523,20 +518,11 @@ def normpath(path):
altsep = b'/'
curdir = b'.'
pardir = b'..'
special_prefixes = (b'\\\\.\\', b'\\\\?\\')
else:
sep = '\\'
altsep = '/'
curdir = '.'
pardir = '..'
special_prefixes = ('\\\\.\\', '\\\\?\\')
if path.startswith(special_prefixes):
# in the case of paths with these prefixes:
# \\.\ -> device names
# \\?\ -> literal paths
# do not do any normalization, but return the path
# unchanged apart from the call to os.fspath()
return path
path = path.replace(altsep, sep)
prefix, path = splitdrive(path)

Expand Down
23 changes: 15 additions & 8 deletions Lib/test/test_ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,22 +107,22 @@ def test_splitdrive(self):
tester('ntpath.splitdrive("//conky/mountpoint/foo/bar")',
('//conky/mountpoint', '/foo/bar'))
tester('ntpath.splitdrive("\\\\\\conky\\mountpoint\\foo\\bar")',
('', '\\\\\\conky\\mountpoint\\foo\\bar'))
('\\\\\\conky', '\\mountpoint\\foo\\bar'))
tester('ntpath.splitdrive("///conky/mountpoint/foo/bar")',
('', '///conky/mountpoint/foo/bar'))
('///conky', '/mountpoint/foo/bar'))
tester('ntpath.splitdrive("\\\\conky\\\\mountpoint\\foo\\bar")',
('', '\\\\conky\\\\mountpoint\\foo\\bar'))
('\\\\conky\\', '\\mountpoint\\foo\\bar'))
tester('ntpath.splitdrive("//conky//mountpoint/foo/bar")',
('', '//conky//mountpoint/foo/bar'))
('//conky/', '/mountpoint/foo/bar'))
# Issue #19911: UNC part containing U+0130
self.assertEqual(ntpath.splitdrive('//conky/MOUNTPOİNT/foo/bar'),
('//conky/MOUNTPOİNT', '/foo/bar'))
# gh-81790: support device namespace, including UNC drives.
tester('ntpath.splitdrive("//?/c:")', ("//?/c:", ""))
tester('ntpath.splitdrive("//?/c:/")', ("//?/c:", "/"))
tester('ntpath.splitdrive("//?/c:/dir")', ("//?/c:", "/dir"))
tester('ntpath.splitdrive("//?/UNC")', ("", "//?/UNC"))
tester('ntpath.splitdrive("//?/UNC/")', ("", "//?/UNC/"))
tester('ntpath.splitdrive("//?/UNC")', ("//?/UNC", ""))
tester('ntpath.splitdrive("//?/UNC/")', ("//?/UNC/", ""))
tester('ntpath.splitdrive("//?/UNC/server/")', ("//?/UNC/server/", ""))
tester('ntpath.splitdrive("//?/UNC/server/share")', ("//?/UNC/server/share", ""))
tester('ntpath.splitdrive("//?/UNC/server/share/dir")', ("//?/UNC/server/share", "/dir"))
Expand All @@ -133,8 +133,8 @@ def test_splitdrive(self):
tester('ntpath.splitdrive("\\\\?\\c:")', ("\\\\?\\c:", ""))
tester('ntpath.splitdrive("\\\\?\\c:\\")', ("\\\\?\\c:", "\\"))
tester('ntpath.splitdrive("\\\\?\\c:\\dir")', ("\\\\?\\c:", "\\dir"))
tester('ntpath.splitdrive("\\\\?\\UNC")', ("", "\\\\?\\UNC"))
tester('ntpath.splitdrive("\\\\?\\UNC\\")', ("", "\\\\?\\UNC\\"))
tester('ntpath.splitdrive("\\\\?\\UNC")', ("\\\\?\\UNC", ""))
tester('ntpath.splitdrive("\\\\?\\UNC\\")', ("\\\\?\\UNC\\", ""))
tester('ntpath.splitdrive("\\\\?\\UNC\\server\\")', ("\\\\?\\UNC\\server\\", ""))
tester('ntpath.splitdrive("\\\\?\\UNC\\server\\share")', ("\\\\?\\UNC\\server\\share", ""))
tester('ntpath.splitdrive("\\\\?\\UNC\\server\\share\\dir")',
Expand All @@ -143,6 +143,13 @@ def test_splitdrive(self):
('\\\\?\\VOLUME{00000000-0000-0000-0000-000000000000}', '\\spam'))
tester('ntpath.splitdrive("\\\\?\\BootPartition\\")', ("\\\\?\\BootPartition", "\\"))

# gh-96290: support partial/invalid UNC drives
tester('ntpath.splitdrive("//")', ("//", "")) # empty server & missing share
tester('ntpath.splitdrive("///")', ("///", "")) # empty server & empty share
tester('ntpath.splitdrive("///y")', ("///y", "")) # empty server & valid share
tester('ntpath.splitdrive("//x")', ("//x", "")) # valid server & missing share
tester('ntpath.splitdrive("//x/")', ("//x/", "")) # valid server & empty share

def test_split(self):
tester('ntpath.split("c:\\foo\\bar")', ('c:\\foo', 'bar'))
tester('ntpath.split("\\\\conky\\mountpoint\\foo\\bar")',
Expand Down

0 comments on commit 471c47b

Please sign in to comment.