diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 97edfa52a..0444b0f65 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -87,16 +87,20 @@ except ImportError: 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] 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. @@ -172,28 +176,26 @@ def splitdrive(p): 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 ^^^^^^^^^^^^^^^ - index = normp.find(sep, 2) + if normp[0: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[:0], p + return p, p[:0] 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, p[:0] return p[:index2], p[index2:] if normp[1:2] == colon: + # Drive-letter drives, e.g. X: return p[:2], p[2:] return p[:0], p @@ -294,11 +296,13 @@ def ismount(path): root, rest = splitdrive(path) if root and root[0] in seps: return (not rest) or (rest in seps) - if rest in seps: + if rest and rest in seps: return True if _getvolumepathname: - return path.rstrip(seps) == _getvolumepathname(path).rstrip(seps) + x = path.rstrip(seps) + y =_getvolumepathname(path).rstrip(seps) + return x.casefold() == y.casefold() else: return False @@ -485,56 +489,59 @@ def expandvars(path): # Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B. # Previously, this function also truncated pathnames to 8+3 format, # but as this module is called "ntpath", that's obviously wrong! +try: + from nt import _path_normpath -def normpath(path): - """Normalize path, eliminating double slashes, etc.""" - path = os.fspath(path) - if isinstance(path, bytes): - sep = b'\\' - 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) +except ImportError: + def normpath(path): + """Normalize path, eliminating double slashes, etc.""" + path = os.fspath(path) + if isinstance(path, bytes): + sep = b'\\' + altsep = b'/' + curdir = b'.' + pardir = b'..' + else: + sep = '\\' + altsep = '/' + curdir = '.' + pardir = '..' + path = path.replace(altsep, sep) + prefix, path = splitdrive(path) - # collapse initial backslashes - if path.startswith(sep): - prefix += sep - path = path.lstrip(sep) + # collapse initial backslashes + if path.startswith(sep): + prefix += sep + path = path.lstrip(sep) - comps = path.split(sep) - i = 0 - while i < len(comps): - if not comps[i] or comps[i] == curdir: - del comps[i] - elif comps[i] == pardir: - if i > 0 and comps[i-1] != pardir: - del comps[i-1:i+1] - i -= 1 - elif i == 0 and prefix.endswith(sep): + comps = path.split(sep) + i = 0 + while i < len(comps): + if not comps[i] or comps[i] == curdir: del comps[i] + elif comps[i] == pardir: + if i > 0 and comps[i-1] != pardir: + del comps[i-1:i+1] + i -= 1 + elif i == 0 and prefix.endswith(sep): + del comps[i] + else: + i += 1 else: i += 1 - else: - i += 1 - # If the path is now empty, substitute '.' - if not prefix and not comps: - comps.append(curdir) - return prefix + sep.join(comps) + # If the path is now empty, substitute '.' + if not prefix and not comps: + comps.append(curdir) + return prefix + sep.join(comps) + +else: + def normpath(path): + """Normalize path, eliminating double slashes, etc.""" + path = os.fspath(path) + if isinstance(path, bytes): + return os.fsencode(_path_normpath(os.fsdecode(path))) or b"." + return _path_normpath(path) or "." + def _abspath_fallback(path): """Return the absolute version of a path as a fallback function in case @@ -563,7 +570,7 @@ else: # use native Windows method on Windows def abspath(path): """Return the absolute version of a path.""" try: - return normpath(_getfullpathname(path)) + return _getfullpathname(normpath(path)) except (OSError, ValueError): return _abspath_fallback(path) @@ -625,16 +632,19 @@ else: # 21: ERROR_NOT_READY (implies drive with no media) # 32: ERROR_SHARING_VIOLATION (probably an NTFS paging file) # 50: ERROR_NOT_SUPPORTED + # 53: ERROR_BAD_NETPATH + # 65: ERROR_NETWORK_ACCESS_DENIED # 67: ERROR_BAD_NET_NAME (implies remote server unavailable) # 87: ERROR_INVALID_PARAMETER # 123: ERROR_INVALID_NAME + # 161: ERROR_BAD_PATHNAME # 1920: ERROR_CANT_ACCESS_FILE # 1921: ERROR_CANT_RESOLVE_FILENAME (implies unfollowable symlink) - allowed_winerror = 1, 2, 3, 5, 21, 32, 50, 67, 87, 123, 1920, 1921 + allowed_winerror = 1, 2, 3, 5, 21, 32, 50, 53, 65, 67, 87, 123, 161, 1920, 1921 # Non-strict algorithm is to find as much of the target directory # as we can and join the rest. - tail = '' + tail = path[:0] while path: try: path = _getfinalpathname(path) diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 969a05030..f75fce6e0 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -1,10 +1,11 @@ import ntpath import os +import string import sys import unittest import warnings from test.support import os_helper -from test.support import TestFailed +from test.support import TestFailed, is_emscripten from test.support.os_helper import FakePath from test import test_genericpath from tempfile import TemporaryFile @@ -107,17 +108,50 @@ class TestNtpath(NtpathTestCase): 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/server/")', ("//?/UNC/server/", "")) + tester('ntpath.splitdrive("//?/UNC/server/share")', ("//?/UNC/server/share", "")) + tester('ntpath.splitdrive("//?/UNC/server/share/dir")', ("//?/UNC/server/share", "/dir")) + tester('ntpath.splitdrive("//?/VOLUME{00000000-0000-0000-0000-000000000000}/spam")', + ('//?/VOLUME{00000000-0000-0000-0000-000000000000}', '/spam')) + tester('ntpath.splitdrive("//?/BootPartition/")', ("//?/BootPartition", "/")) + + 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\\server\\")', ("\\\\?\\UNC\\server\\", "")) + tester('ntpath.splitdrive("\\\\?\\UNC\\server\\share")', ("\\\\?\\UNC\\server\\share", "")) + tester('ntpath.splitdrive("\\\\?\\UNC\\server\\share\\dir")', + ("\\\\?\\UNC\\server\\share", "\\dir")) + tester('ntpath.splitdrive("\\\\?\\VOLUME{00000000-0000-0000-0000-000000000000}\\spam")', + ('\\\\?\\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 & 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): tester('ntpath.split("c:\\foo\\bar")', ('c:\\foo', 'bar')) tester('ntpath.split("\\\\conky\\mountpoint\\foo\\bar")', @@ -136,6 +170,10 @@ class TestNtpath(NtpathTestCase): tester('ntpath.isabs("\\foo")', 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): tester('ntpath.commonprefix(["/home/swenson/spam", "/home/swen/spam"])', "/home/swen") @@ -235,6 +273,21 @@ class TestNtpath(NtpathTestCase): tester("ntpath.normpath('\\\\.\\NUL')", r'\\.\NUL') tester("ntpath.normpath('\\\\?\\D:/XY\\Z')", r'\\?\D:/XY\Z') + tester("ntpath.normpath('handbook/../../Tests/image.png')", r'..\Tests\image.png') + tester("ntpath.normpath('handbook/../../../Tests/image.png')", r'..\..\Tests\image.png') + tester("ntpath.normpath('handbook///../a/.././../b/c')", r'..\b\c') + tester("ntpath.normpath('handbook/a/../..///../../b/c')", r'..\..\b\c') + + 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): expected = ntpath.normpath(os.getcwd()) @@ -269,6 +322,16 @@ class TestNtpath(NtpathTestCase): self.assertPathEqual(ntpath.realpath(os.fsencode(ABSTFN + "1")), os.fsencode(ABSTFN)) + # gh-88013: call ntpath.realpath with binary drive name may raise a + # TypeError. The drive should not exist to reproduce the bug. + for c in string.ascii_uppercase: + d = f"{c}:\\" + if not ntpath.exists(d): + break + else: + raise OSError("No free drive letters available") + self.assertEqual(ntpath.realpath(d), d) + @os_helper.skip_unless_symlink @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') def test_realpath_strict(self): @@ -604,9 +667,45 @@ class TestNtpath(NtpathTestCase): tester('ntpath.expanduser("~test")', '~test') tester('ntpath.expanduser("~")', 'C:\\Users\\eric') + + @unittest.skipUnless(nt, "abspath requires 'nt' module") def test_abspath(self): tester('ntpath.abspath("C:\\")', "C:\\") + tester('ntpath.abspath("\\\\?\\C:////spam////eggs. . .")', "\\\\?\\C:\\spam\\eggs") + tester('ntpath.abspath("\\\\.\\C:////spam////eggs. . .")', "\\\\.\\C:\\spam\\eggs") + tester('ntpath.abspath("//spam//eggs. . .")', "\\\\spam\\eggs") + tester('ntpath.abspath("\\\\spam\\\\eggs. . .")', "\\\\spam\\eggs") + tester('ntpath.abspath("C:/spam. . .")', "C:\\spam") + tester('ntpath.abspath("C:\\spam. . .")', "C:\\spam") + tester('ntpath.abspath("C:/nul")', "\\\\.\\nul") + tester('ntpath.abspath("C:\\nul")', "\\\\.\\nul") + tester('ntpath.abspath("//..")', "\\\\") + tester('ntpath.abspath("//../")', "\\\\..\\") + tester('ntpath.abspath("//../..")', "\\\\..\\") + tester('ntpath.abspath("//../../")', "\\\\..\\..\\") + tester('ntpath.abspath("//../../../")', "\\\\..\\..\\") + tester('ntpath.abspath("//../../../..")', "\\\\..\\..\\") + tester('ntpath.abspath("//../../../../")', "\\\\..\\..\\") + tester('ntpath.abspath("//server")', "\\\\server") + tester('ntpath.abspath("//server/")', "\\\\server\\") + tester('ntpath.abspath("//server/..")', "\\\\server\\") + tester('ntpath.abspath("//server/../")', "\\\\server\\..\\") + tester('ntpath.abspath("//server/../..")', "\\\\server\\..\\") + tester('ntpath.abspath("//server/../../")', "\\\\server\\..\\") + tester('ntpath.abspath("//server/../../..")', "\\\\server\\..\\") + tester('ntpath.abspath("//server/../../../")', "\\\\server\\..\\") + tester('ntpath.abspath("//server/share")', "\\\\server\\share") + tester('ntpath.abspath("//server/share/")', "\\\\server\\share\\") + tester('ntpath.abspath("//server/share/..")', "\\\\server\\share\\") + tester('ntpath.abspath("//server/share/../")', "\\\\server\\share\\") + tester('ntpath.abspath("//server/share/../..")', "\\\\server\\share\\") + tester('ntpath.abspath("//server/share/../../")', "\\\\server\\share\\") + tester('ntpath.abspath("C:\\nul. . .")', "\\\\.\\nul") + tester('ntpath.abspath("//... . .")', "\\\\") + tester('ntpath.abspath("//.. . . .")', "\\\\") + tester('ntpath.abspath("//../... . .")', "\\\\..\\") + tester('ntpath.abspath("//../.. . . .")', "\\\\..\\") with os_helper.temp_cwd(os_helper.TESTFN) as cwd_dir: # bpo-31047 tester('ntpath.abspath("")', cwd_dir) tester('ntpath.abspath(" ")', cwd_dir + "\\ ") @@ -708,6 +807,7 @@ class TestNtpath(NtpathTestCase): ['Program Files', b'C:\\Program Files\\Foo']) @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") + @unittest.skipIf(is_emscripten, "Emscripten cannot fstat unnamed files.") def test_sameopenfile(self): with TemporaryFile() as tf1, TemporaryFile() as tf2: # Make sure the same file is really the same @@ -745,8 +845,9 @@ class TestNtpath(NtpathTestCase): # (or any other volume root). The drive-relative # locations below cannot then refer to mount points # - drive, path = ntpath.splitdrive(sys.executable) - with os_helper.change_cwd(ntpath.dirname(sys.executable)): + test_cwd = os.getenv("SystemRoot") + drive, path = ntpath.splitdrive(test_cwd) + with os_helper.change_cwd(test_cwd): self.assertFalse(ntpath.ismount(drive.lower())) self.assertFalse(ntpath.ismount(drive.upper()))