gh-96290: Support partial/invalid UNC drives in ntpath.normpath() and splitdrive() (GH-100351)

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>
This commit is contained in:
Barney Gale 2023-01-12 19:24:57 +00:00 committed by GitHub
parent eecd422d1b
commit 005e69403d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 55 additions and 46 deletions

View file

@ -87,16 +87,20 @@ except ImportError:
def isabs(s): def isabs(s):
"""Test whether a path is absolute""" """Test whether a path is absolute"""
s = os.fspath(s) s = os.fspath(s)
# Paths beginning with \\?\ are always absolute, but do not
# necessarily contain a drive.
if isinstance(s, bytes): if isinstance(s, bytes):
if s.replace(b'/', b'\\').startswith(b'\\\\?\\'): sep = b'\\'
return True altsep = b'/'
colon_sep = b':\\'
else: else:
if s.replace('/', '\\').startswith('\\\\?\\'): sep = '\\'
return True altsep = '/'
s = splitdrive(s)[1] colon_sep = ':\\'
return len(s) > 0 and s[0] and s[0] in _get_bothseps(s) 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. # Join two (or more) paths.
@ -172,34 +176,26 @@ def splitdrive(p):
sep = b'\\' sep = b'\\'
altsep = b'/' altsep = b'/'
colon = b':' colon = b':'
unc_prefix = b'\\\\?\\UNC' unc_prefix = b'\\\\?\\UNC\\'
else: else:
sep = '\\' sep = '\\'
altsep = '/' altsep = '/'
colon = ':' colon = ':'
unc_prefix = '\\\\?\\UNC' unc_prefix = '\\\\?\\UNC\\'
normp = p.replace(altsep, sep) normp = p.replace(altsep, sep)
if (normp[0:2] == sep*2) and (normp[2:3] != sep): if normp[0:2] == sep * 2:
# is a UNC path: # UNC drives, e.g. \\server\share or \\?\UNC\server\share
# vvvvvvvvvvvvvvvvvvvv drive letter or UNC path # Device drives, e.g. \\.\device or \\?\device
# \\machine\mountpoint\directory\etc\... start = 8 if normp[:8].upper() == unc_prefix else 2
# directory ^^^^^^^^^^^^^^^
if normp[:8].upper().rstrip(sep) == unc_prefix:
start = 8
else:
start = 2
index = normp.find(sep, start) index = normp.find(sep, start)
if index == -1: if index == -1:
return p[:0], p return p, p[:0]
index2 = normp.find(sep, index + 1) 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: if index2 == -1:
index2 = len(p) return p, p[:0]
return p[:index2], p[index2:] return p[:index2], p[index2:]
if normp[1:2] == colon: if normp[1:2] == colon:
# Drive-letter drives, e.g. X:
return p[:2], p[2:] return p[:2], p[2:]
return p[:0], p return p[:0], p
@ -523,20 +519,11 @@ except ImportError:
altsep = b'/' altsep = b'/'
curdir = b'.' curdir = b'.'
pardir = b'..' pardir = b'..'
special_prefixes = (b'\\\\.\\', b'\\\\?\\')
else: else:
sep = '\\' sep = '\\'
altsep = '/' altsep = '/'
curdir = '.' curdir = '.'
pardir = '..' 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) path = path.replace(altsep, sep)
prefix, path = splitdrive(path) prefix, path = splitdrive(path)

View file

@ -107,13 +107,13 @@ class TestNtpath(NtpathTestCase):
tester('ntpath.splitdrive("//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")', tester('ntpath.splitdrive("\\\\\\conky\\mountpoint\\foo\\bar")',
('', '\\\\\\conky\\mountpoint\\foo\\bar')) ('\\\\\\conky', '\\mountpoint\\foo\\bar'))
tester('ntpath.splitdrive("///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")', tester('ntpath.splitdrive("\\\\conky\\\\mountpoint\\foo\\bar")',
('', '\\\\conky\\\\mountpoint\\foo\\bar')) ('\\\\conky\\', '\\mountpoint\\foo\\bar'))
tester('ntpath.splitdrive("//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 # Issue #19911: UNC part containing U+0130
self.assertEqual(ntpath.splitdrive('//conky/MOUNTPOİNT/foo/bar'), self.assertEqual(ntpath.splitdrive('//conky/MOUNTPOİNT/foo/bar'),
('//conky/MOUNTPOİNT', '/foo/bar')) ('//conky/MOUNTPOİNT', '/foo/bar'))
@ -121,8 +121,8 @@ class TestNtpath(NtpathTestCase):
tester('ntpath.splitdrive("//?/c:")', ("//?/c:", "")) tester('ntpath.splitdrive("//?/c:")', ("//?/c:", ""))
tester('ntpath.splitdrive("//?/c:/")', ("//?/c:", "/")) tester('ntpath.splitdrive("//?/c:/")', ("//?/c:", "/"))
tester('ntpath.splitdrive("//?/c:/dir")', ("//?/c:", "/dir")) 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/")', ("//?/UNC/server/", ""))
tester('ntpath.splitdrive("//?/UNC/server/share")', ("//?/UNC/server/share", "")) tester('ntpath.splitdrive("//?/UNC/server/share")', ("//?/UNC/server/share", ""))
tester('ntpath.splitdrive("//?/UNC/server/share/dir")', ("//?/UNC/server/share", "/dir")) tester('ntpath.splitdrive("//?/UNC/server/share/dir")', ("//?/UNC/server/share", "/dir"))
@ -133,8 +133,8 @@ class TestNtpath(NtpathTestCase):
tester('ntpath.splitdrive("\\\\?\\c:")', ("\\\\?\\c:", "")) tester('ntpath.splitdrive("\\\\?\\c:")', ("\\\\?\\c:", ""))
tester('ntpath.splitdrive("\\\\?\\c:\\")', ("\\\\?\\c:", "\\")) tester('ntpath.splitdrive("\\\\?\\c:\\")', ("\\\\?\\c:", "\\"))
tester('ntpath.splitdrive("\\\\?\\c:\\dir")', ("\\\\?\\c:", "\\dir")) 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\\")', ("\\\\?\\UNC\\server\\", ""))
tester('ntpath.splitdrive("\\\\?\\UNC\\server\\share")', ("\\\\?\\UNC\\server\\share", "")) tester('ntpath.splitdrive("\\\\?\\UNC\\server\\share")', ("\\\\?\\UNC\\server\\share", ""))
tester('ntpath.splitdrive("\\\\?\\UNC\\server\\share\\dir")', tester('ntpath.splitdrive("\\\\?\\UNC\\server\\share\\dir")',
@ -143,6 +143,13 @@ class TestNtpath(NtpathTestCase):
('\\\\?\\VOLUME{00000000-0000-0000-0000-000000000000}', '\\spam')) ('\\\\?\\VOLUME{00000000-0000-0000-0000-000000000000}', '\\spam'))
tester('ntpath.splitdrive("\\\\?\\BootPartition\\")', ("\\\\?\\BootPartition", "\\")) 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 & non-empty share
tester('ntpath.splitdrive("//x")', ("//x", "")) # non-empty server & missing share
tester('ntpath.splitdrive("//x/")', ("//x/", "")) # non-empty server & empty share
def test_split(self): def test_split(self):
tester('ntpath.split("c:\\foo\\bar")', ('c:\\foo', 'bar')) tester('ntpath.split("c:\\foo\\bar")', ('c:\\foo', 'bar'))
tester('ntpath.split("\\\\conky\\mountpoint\\foo\\bar")', tester('ntpath.split("\\\\conky\\mountpoint\\foo\\bar")',
@ -161,6 +168,10 @@ class TestNtpath(NtpathTestCase):
tester('ntpath.isabs("\\foo")', 1) tester('ntpath.isabs("\\foo")', 1)
tester('ntpath.isabs("\\foo\\bar")', 1) tester('ntpath.isabs("\\foo\\bar")', 1)
# gh-96290: normal UNC paths and device paths without trailing backslashes
tester('ntpath.isabs("\\\\conky\\mountpoint")', 1)
tester('ntpath.isabs("\\\\.\\C:")', 1)
def test_commonprefix(self): def test_commonprefix(self):
tester('ntpath.commonprefix(["/home/swenson/spam", "/home/swen/spam"])', tester('ntpath.commonprefix(["/home/swenson/spam", "/home/swen/spam"])',
"/home/swen") "/home/swen")
@ -270,6 +281,12 @@ class TestNtpath(NtpathTestCase):
tester("ntpath.normpath('//server/share/../..')", '\\\\server\\share\\') tester("ntpath.normpath('//server/share/../..')", '\\\\server\\share\\')
tester("ntpath.normpath('//server/share/../../')", '\\\\server\\share\\') tester("ntpath.normpath('//server/share/../../')", '\\\\server\\share\\')
# gh-96290: don't normalize partial/invalid UNC drives as rooted paths.
tester("ntpath.normpath('\\\\foo\\\\')", '\\\\foo\\\\')
tester("ntpath.normpath('\\\\foo\\')", '\\\\foo\\')
tester("ntpath.normpath('\\\\foo')", '\\\\foo')
tester("ntpath.normpath('\\\\')", '\\\\')
def test_realpath_curdir(self): def test_realpath_curdir(self):
expected = ntpath.normpath(os.getcwd()) expected = ntpath.normpath(os.getcwd())
tester("ntpath.realpath('.')", expected) tester("ntpath.realpath('.')", expected)

View file

@ -1469,10 +1469,10 @@ class ExtractTests(unittest.TestCase):
(r'C:\foo\bar', 'foo/bar'), (r'C:\foo\bar', 'foo/bar'),
(r'//conky/mountpoint/foo/bar', 'foo/bar'), (r'//conky/mountpoint/foo/bar', 'foo/bar'),
(r'\\conky\mountpoint\foo\bar', 'foo/bar'), (r'\\conky\mountpoint\foo\bar', 'foo/bar'),
(r'///conky/mountpoint/foo/bar', 'conky/mountpoint/foo/bar'), (r'///conky/mountpoint/foo/bar', 'mountpoint/foo/bar'),
(r'\\\conky\mountpoint\foo\bar', 'conky/mountpoint/foo/bar'), (r'\\\conky\mountpoint\foo\bar', 'mountpoint/foo/bar'),
(r'//conky//mountpoint/foo/bar', 'conky/mountpoint/foo/bar'), (r'//conky//mountpoint/foo/bar', 'mountpoint/foo/bar'),
(r'\\conky\\mountpoint\foo\bar', 'conky/mountpoint/foo/bar'), (r'\\conky\\mountpoint\foo\bar', 'mountpoint/foo/bar'),
(r'//?/C:/foo/bar', 'foo/bar'), (r'//?/C:/foo/bar', 'foo/bar'),
(r'\\?\C:\foo\bar', 'foo/bar'), (r'\\?\C:\foo\bar', 'foo/bar'),
(r'C:/../C:/foo/bar', 'C_/foo/bar'), (r'C:/../C:/foo/bar', 'C_/foo/bar'),

View file

@ -0,0 +1,5 @@
Fix handling of partial and invalid UNC drives in ``ntpath.splitdrive()``, and in
``ntpath.normpath()`` on non-Windows systems. Paths such as '\\server' and '\\' are now considered
by ``splitdrive()`` to contain only a drive, and consequently are not modified by ``normpath()`` on
non-Windows systems. The behaviour of ``normpath()`` on Windows systems is unaffected, as native
OS APIs are used. Patch by Eryk Sun, with contributions by Barney Gale.