Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 22 additions & 16 deletions Lib/ntpath.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -666,6 +666,7 @@ def realpath(path, /, *, strict=False):
prefix = b'\\\\?\\'
unc_prefix = b'\\\\?\\UNC\\'
new_unc_prefix = b'\\\\'
colon_sep = b':\\'
cwd = os.getcwdb()
# bpo-38081: Special case for realpath(b'nul')
devnull = b'nul'
Expand All@@ -675,6 +676,7 @@ def realpath(path, /, *, strict=False):
prefix = '\\\\?\\'
unc_prefix = '\\\\?\\UNC\\'
new_unc_prefix = '\\\\'
colon_sep = ':\\'
cwd = os.getcwd()
# bpo-38081: Special case for realpath('nul')
devnull = 'nul'
Expand DownExpand Up@@ -718,25 +720,29 @@ def realpath(path, /, *, strict=False):
# strip off that prefix unless it was already provided on the original
# path.
if not had_prefix and path.startswith(prefix):
# For UNC paths, the prefix will actually be \\?\UNC\
# Handle that case as well.
# For UNC drives, the path starts with \\?\UNC\.
if path.startswith(unc_prefix):
spath = new_unc_prefix + path[len(unc_prefix):]
else:
# For drive-letter drives, the path starts with \\?\<letter>:\.
elif path.startswith(colon_sep, len(prefix) + 1):
spath = path[len(prefix):]
# Ensure that the non-prefixed path resolves to the same path
try:
if _getfinalpathname(spath) == path:
path = spath
except ValueError:
# Unexpected, as an invalid path should not have gained a prefix
# at any point, but we ignore this error just in case.
pass
except OSError as ex:
# If the path does not exist and originally did not exist, then
# strip the prefix anyway.
if ex.winerror == initial_winerror:
path = spath
# For all others, e.g. volume GUID paths, it cannot be stripped.
else:
spath = None
if spath is not None:
# Ensure that the non-prefixed path resolves to the same path
try:
if _getfinalpathname(spath) == path:
path = spath
except ValueError:
# Unexpected, as an invalid path should not have gained a
# prefix at any point, but we ignore this error just in case.
pass
except OSError as ex:
# If the path does not exist and originally did not exist,
# then strip the prefix anyway.
if ex.winerror == initial_winerror:
path = spath
return path


Expand Down
27 changes: 27 additions & 0 deletions Lib/test/test_ntpath.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1535,6 +1535,33 @@ def test_isjunction(self):
self.assertFalse(ntpath.isjunction('tmpdir'))
self.assertPathEqual(ntpath.realpath('testjunc'), ntpath.realpath('tmpdir'))

@unittest.skipIf(sys.platform != 'win32', "Can only test junctions with creation on win32.")
def test_realpath_volume_guid_path(self):
# gh-89760: the \\?\ prefix cannot be stripped from a volume GUID path.
# Find a volume which is not mounted as a drive.
for volume in os.listvolumes():
if not os.listmounts(volume):
break
else:
raise unittest.SkipTest('no volume without a mount point')

with os_helper.temp_dir() as d:
with os_helper.change_cwd(d):
# _winapi.CreateJunction() adds the \\??\\ prefix to a path
# which already has a prefix.
try:
subprocess.run(['cmd', '/c', 'mklink', '/j',
'testjunc', volume],
check=True, capture_output=True)
except (OSError, subprocess.CalledProcessError):
raise unittest.SkipTest('creating the test junction failed')

for path in 'testjunc', 'testjunc/spam', 'testjunc/spam/eggs':
with self.subTest(path=path):
realpath = ntpath.realpath(path)
self.assertStartsWith(realpath, '\\\\?\\Volume{')
self.assertTrue(ntpath.isabs(realpath), realpath)

def test_isfile_invalid_paths(self):
isfile = ntpath.isfile
self.assertIs(isfile('/tmp\udfffabcds'), False)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
Fix :func:`os.path.realpath` on Windows: the ``\\?\`` prefix is no longer
stripped from a volume GUID path, which made the result invalid.
Loading