Commit 75e06496 authored by Steve Dower's avatar Steve Dower Committed by GitHub

bpo-9949: Enable symlink traversal for ntpath.realpath (GH-15287)

parent e1c638da
......@@ -350,11 +350,19 @@ the :mod:`glob` module.)
.. function:: realpath(path)
Return the canonical path of the specified filename, eliminating any symbolic
links encountered in the path (if they are supported by the operating system).
links encountered in the path (if they are supported by the operating
system).
.. note::
When symbolic link cycles occur, the returned path will be one member of
the cycle, but no guarantee is made about which member that will be.
.. versionchanged:: 3.6
Accepts a :term:`path-like object`.
.. versionchanged:: 3.8
Symbolic links and junctions are now resolved on Windows.
.. function:: relpath(path, start=os.curdir)
......
......@@ -824,6 +824,9 @@ characters or bytes unrepresentable at the OS level.
environment variable and does not use :envvar:`HOME`, which is not normally set
for regular user accounts.
:func:`~os.path.realpath` on Windows now resolves reparse points, including
symlinks and directory junctions.
ncurses
-------
......
......@@ -519,8 +519,94 @@ else: # use native Windows method on Windows
except (OSError, ValueError):
return _abspath_fallback(path)
# realpath is a no-op on systems without islink support
realpath = abspath
try:
from nt import _getfinalpathname, readlink as _nt_readlink
except ImportError:
# realpath is a no-op on systems without _getfinalpathname support.
realpath = abspath
else:
def _readlink_deep(path, seen=None):
if seen is None:
seen = set()
while normcase(path) not in seen:
seen.add(normcase(path))
try:
path = _nt_readlink(path)
except OSError as ex:
# Stop on file (2) or directory (3) not found, or
# paths that are not reparse points (4390)
if ex.winerror in (2, 3, 4390):
break
raise
except ValueError:
# Stop on reparse points that are not symlinks
break
return path
def _getfinalpathname_nonstrict(path):
# Fast path to get the final path name. If this succeeds, there
# is no need to go any further.
try:
return _getfinalpathname(path)
except OSError:
pass
# Allow file (2) or directory (3) not found, invalid syntax (123),
# and symlinks that cannot be followed (1921)
allowed_winerror = 2, 3, 123, 1921
# Non-strict algorithm is to find as much of the target directory
# as we can and join the rest.
tail = ''
seen = set()
while path:
try:
path = _readlink_deep(path, seen)
path = _getfinalpathname(path)
return join(path, tail) if tail else path
except OSError as ex:
if ex.winerror not in allowed_winerror:
raise
path, name = split(path)
if path and not name:
return abspath(path + tail)
tail = join(name, tail) if tail else name
return abspath(tail)
def realpath(path):
path = os.fspath(path)
if isinstance(path, bytes):
prefix = b'\\\\?\\'
unc_prefix = b'\\\\?\\UNC\\'
new_unc_prefix = b'\\\\'
cwd = os.getcwdb()
else:
prefix = '\\\\?\\'
unc_prefix = '\\\\?\\UNC\\'
new_unc_prefix = '\\\\'
cwd = os.getcwd()
had_prefix = path.startswith(prefix)
path = _getfinalpathname_nonstrict(path)
# The path returned by _getfinalpathname will always start with \\?\ -
# 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.
if path.startswith(unc_prefix):
spath = new_unc_prefix + path[len(unc_prefix):]
else:
spath = path[len(prefix):]
# Ensure that the non-prefixed path resolves to the same path
try:
if _getfinalpathname(spath) == path:
path = spath
except OSError as ex:
pass
return path
# Win9x family and earlier have no Unicode filename support.
supports_unicode_filenames = (hasattr(sys, "getwindowsversion") and
sys.getwindowsversion()[3] >= 2)
......@@ -633,23 +719,6 @@ def commonpath(paths):
raise
# determine if two files are in fact the same file
try:
# GetFinalPathNameByHandle is available starting with Windows 6.0.
# Windows XP and non-Windows OS'es will mock _getfinalpathname.
if sys.getwindowsversion()[:2] >= (6, 0):
from nt import _getfinalpathname
else:
raise ImportError
except (AttributeError, ImportError):
# On Windows XP and earlier, two files are the same if their absolute
# pathnames are the same.
# Non-Windows operating systems fake this method with an XP
# approximation.
def _getfinalpathname(f):
return normcase(abspath(f))
try:
# The genericpath.isdir implementation uses os.stat and checks the mode
# attribute to tell whether or not the path is a directory.
......
This diff is collapsed.
......@@ -3358,10 +3358,7 @@ class OSErrorTests(unittest.TestCase):
if hasattr(os, "lchmod"):
funcs.append((self.filenames, os.lchmod, 0o777))
if hasattr(os, "readlink"):
if sys.platform == "win32":
funcs.append((self.unicode_filenames, os.readlink,))
else:
funcs.append((self.filenames, os.readlink,))
funcs.append((self.filenames, os.readlink,))
for filenames, func, *func_args in funcs:
......
......@@ -1871,11 +1871,7 @@ class TestMove(unittest.TestCase):
dst_link = os.path.join(self.dst_dir, 'quux')
shutil.move(dst, dst_link)
self.assertTrue(os.path.islink(dst_link))
# On Windows, os.path.realpath does not follow symlinks (issue #9949)
if os.name == 'nt':
self.assertEqual(os.path.realpath(src), os.readlink(dst_link))
else:
self.assertEqual(os.path.realpath(src), os.path.realpath(dst_link))
self.assertEqual(os.path.realpath(src), os.path.realpath(dst_link))
@support.skip_unless_symlink
@mock_rename
......
......@@ -723,11 +723,13 @@ class TestDiscovery(unittest.TestCase):
original_listdir = os.listdir
original_isfile = os.path.isfile
original_isdir = os.path.isdir
original_realpath = os.path.realpath
def cleanup():
os.listdir = original_listdir
os.path.isfile = original_isfile
os.path.isdir = original_isdir
os.path.realpath = original_realpath
del sys.modules['foo']
if full_path in sys.path:
sys.path.remove(full_path)
......@@ -742,6 +744,10 @@ class TestDiscovery(unittest.TestCase):
os.listdir = listdir
os.path.isfile = isfile
os.path.isdir = isdir
if os.name == 'nt':
# ntpath.realpath may inject path prefixes when failing to
# resolve real files, so we substitute abspath() here instead.
os.path.realpath = os.path.abspath
return full_path
def test_detect_module_clash(self):
......
Enable support for following symlinks in :func:`os.realpath`.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment