Skip to content
Merged
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
49 changes: 41 additions & 8 deletions Lib/ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ 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
else:
if s.replace('/', '\\').startswith('\\\\?\\'):
return True
s = splitdrive(s)[1]
return len(s) > 0 and s[0] in _get_bothseps(s)

Expand Down Expand Up @@ -526,10 +534,7 @@ def abspath(path):
# 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()

def _readlink_deep(path):
# These error codes indicate that we should stop reading links and
# return the path we currently have.
# 1: ERROR_INVALID_FUNCTION
Expand All @@ -546,10 +551,22 @@ def _readlink_deep(path, seen=None):
# 4393: ERROR_REPARSE_TAG_INVALID
allowed_winerror = 1, 2, 3, 5, 21, 32, 50, 67, 87, 4390, 4392, 4393

seen = set()
while normcase(path) not in seen:
seen.add(normcase(path))
try:
old_path = path
path = _nt_readlink(path)
# Links may be relative, so resolve them against their
# own location
if not isabs(path):
# If it's something other than a symlink, we don't know
# what it's actually going to be resolved against, so
# just return the old path.
if not islink(old_path):
path = old_path
break
path = normpath(join(dirname(old_path), path))
except OSError as ex:
if ex.winerror in allowed_winerror:
break
Expand Down Expand Up @@ -579,23 +596,31 @@ def _getfinalpathname_nonstrict(path):
# 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
try:
# The OS could not resolve this path fully, so we attempt
# to follow the link ourselves. If we succeed, join the tail
# and return.
new_path = _readlink_deep(path)
if new_path != path:
return join(new_path, tail) if tail else new_path
except OSError:
# If we fail to readlink(), let's keep traversing
pass
path, name = split(path)
# TODO (bpo-38186): Request the real file name from the directory
# entry using FindFirstFileW. For now, we will return the path
# as best we have it
if path and not name:
return abspath(path + tail)
return path + tail
tail = join(name, tail) if tail else name
return abspath(tail)
return tail

def realpath(path):
path = normpath(path)
Expand All @@ -604,12 +629,20 @@ def realpath(path):
unc_prefix = b'\\\\?\\UNC\\'
new_unc_prefix = b'\\\\'
cwd = os.getcwdb()
# bpo-38081: Special case for realpath(b'nul')
if normcase(path) == normcase(os.fsencode(devnull)):
return b'\\\\.\\NUL'
else:
prefix = '\\\\?\\'
unc_prefix = '\\\\?\\UNC\\'
new_unc_prefix = '\\\\'
cwd = os.getcwd()
# bpo-38081: Special case for realpath('nul')
if normcase(path) == normcase(devnull):
return '\\\\.\\NUL'
had_prefix = path.startswith(prefix)
if not had_prefix and not isabs(path):
path = join(cwd, path)
try:
path = _getfinalpathname(path)
initial_winerror = 0
Expand Down
52 changes: 40 additions & 12 deletions Lib/test/test_ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,16 @@ def test_realpath_broken_symlinks(self):
ABSTFN + r"\missing")
self.assertPathEqual(ntpath.realpath(r"broken\foo"),
ABSTFN + r"\missing\foo")
# bpo-38453: We no longer recursively resolve segments of relative
# symlinks that the OS cannot resolve.
self.assertPathEqual(ntpath.realpath(r"broken1"),
ABSTFN + r"\missing\bar")
ABSTFN + r"\broken\bar")
self.assertPathEqual(ntpath.realpath(r"broken1\baz"),
ABSTFN + r"\missing\bar\baz")
ABSTFN + r"\broken\bar\baz")
self.assertPathEqual(ntpath.realpath("broken2"),
ABSTFN + r"\missing")
ABSTFN + r"\self\self\missing")
self.assertPathEqual(ntpath.realpath("broken3"),
ABSTFN + r"\missing")
ABSTFN + r"\subdir\parent\subdir\parent\missing")
self.assertPathEqual(ntpath.realpath("broken4"),
ABSTFN + r"\missing")
self.assertPathEqual(ntpath.realpath("broken5"),
Expand All @@ -304,13 +306,13 @@ def test_realpath_broken_symlinks(self):
self.assertPathEqual(ntpath.realpath(rb"broken\foo"),
os.fsencode(ABSTFN + r"\missing\foo"))
self.assertPathEqual(ntpath.realpath(rb"broken1"),
os.fsencode(ABSTFN + r"\missing\bar"))
os.fsencode(ABSTFN + r"\broken\bar"))
self.assertPathEqual(ntpath.realpath(rb"broken1\baz"),
os.fsencode(ABSTFN + r"\missing\bar\baz"))
os.fsencode(ABSTFN + r"\broken\bar\baz"))
self.assertPathEqual(ntpath.realpath(b"broken2"),
os.fsencode(ABSTFN + r"\missing"))
os.fsencode(ABSTFN + r"\self\self\missing"))
self.assertPathEqual(ntpath.realpath(rb"broken3"),
os.fsencode(ABSTFN + r"\missing"))
os.fsencode(ABSTFN + r"\subdir\parent\subdir\parent\missing"))
self.assertPathEqual(ntpath.realpath(b"broken4"),
os.fsencode(ABSTFN + r"\missing"))
self.assertPathEqual(ntpath.realpath(b"broken5"),
Expand All @@ -319,8 +321,8 @@ def test_realpath_broken_symlinks(self):
@support.skip_unless_symlink
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
def test_realpath_symlink_loops(self):
# Bug #930024, return the path unchanged if we get into an infinite
# symlink loop.
# Symlink loops are non-deterministic as to which path is returned, but
# it will always be the fully resolved path of one member of the cycle
ABSTFN = ntpath.abspath(support.TESTFN)
self.addCleanup(support.unlink, ABSTFN)
self.addCleanup(support.unlink, ABSTFN + "1")
Expand All @@ -332,8 +334,6 @@ def test_realpath_symlink_loops(self):
os.symlink(ABSTFN, ABSTFN)
self.assertPathEqual(ntpath.realpath(ABSTFN), ABSTFN)

# cycles are non-deterministic as to which path is returned, but
# it will always be the fully resolved path of one member of the cycle
os.symlink(ABSTFN + "1", ABSTFN + "2")
os.symlink(ABSTFN + "2", ABSTFN + "1")
expected = (ABSTFN + "1", ABSTFN + "2")
Expand Down Expand Up @@ -402,6 +402,34 @@ def test_realpath_symlink_prefix(self):
def test_realpath_nul(self):
tester("ntpath.realpath('NUL')", r'\\.\NUL')

@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
def test_realpath_cwd(self):
ABSTFN = ntpath.abspath(support.TESTFN)

support.unlink(ABSTFN)
support.rmtree(ABSTFN)
os.mkdir(ABSTFN)
self.addCleanup(support.rmtree, ABSTFN)

test_dir_long = ntpath.join(ABSTFN, "MyVeryLongDirectoryName")
test_dir_short = ntpath.join(ABSTFN, "MYVERY~1")
test_file_long = ntpath.join(test_dir_long, "file.txt")
test_file_short = ntpath.join(test_dir_short, "file.txt")

os.mkdir(test_dir_long)

with open(test_file_long, "wb") as f:
f.write(b"content")

self.assertPathEqual(test_file_long, ntpath.realpath(test_file_short))

with support.change_cwd(test_dir_long):
self.assertPathEqual(test_file_long, ntpath.realpath("file.txt"))
with support.change_cwd(test_dir_long.lower()):
self.assertPathEqual(test_file_long, ntpath.realpath("file.txt"))
with support.change_cwd(test_dir_short):
self.assertPathEqual(test_file_long, ntpath.realpath("file.txt"))

def test_expandvars(self):
with support.EnvironmentVarGuard() as env:
env.clear()
Expand Down
12 changes: 9 additions & 3 deletions Lib/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,10 @@ def mkdtemp(self):

Returns the path of the directory.
"""
d = tempfile.mkdtemp()
basedir = None
if sys.platform == "win32":
basedir = os.path.realpath(os.getcwd())
d = tempfile.mkdtemp(dir=basedir)
self.tempdirs.append(d)
return d

Expand Down Expand Up @@ -1788,8 +1791,11 @@ class TestMove(unittest.TestCase):

def setUp(self):
filename = "foo"
self.src_dir = tempfile.mkdtemp()
self.dst_dir = tempfile.mkdtemp()
basedir = None
if sys.platform == "win32":
basedir = os.path.realpath(os.getcwd())
self.src_dir = tempfile.mkdtemp(dir=basedir)
self.dst_dir = tempfile.mkdtemp(dir=basedir)
self.src_file = os.path.join(self.src_dir, filename)
self.dst_file = os.path.join(self.dst_dir, filename)
with open(self.src_file, "wb") as f:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Ensure ntpath.realpath() correctly resolves relative paths.