https://github.com/python/cpython/commit/b66728da05e2f959eafb46df94d240ac801e8a3e
commit: b66728da05e2f959eafb46df94d240ac801e8a3e
branch: 3.13
author: Barney Gale <[email protected]>
committer: barneygale <[email protected]>
date: 2024-11-13T23:32:56Z
summary:
[3.13] GH-118289: Fix handling of non-directories in `posixpath.realpath()`
(GH-120127) (#126815)
In strict mode, raise `NotADirectoryError` if we encounter a non-directory
while we still have path parts left to process.
We use a `part_count` variable rather than `len(rest)` because the `rest`
stack also contains markers for unresolved symlinks.
(cherry picked from commit fd4b5453df74e249987553b12c14ad75fafa4991)
files:
A Misc/NEWS.d/next/Library/2024-06-05-19-09-36.gh-issue-118289.moL9_d.rst
M Lib/posixpath.py
M Lib/test/test_posixpath.py
diff --git a/Lib/posixpath.py b/Lib/posixpath.py
index 47b2aa572e5c65..f2173032786311 100644
--- a/Lib/posixpath.py
+++ b/Lib/posixpath.py
@@ -22,6 +22,7 @@
altsep = None
devnull = '/dev/null'
+import errno
import os
import sys
import stat
@@ -408,6 +409,10 @@ def realpath(filename, *, strict=False):
# very fast way of spelling list(reversed(...)).
rest = filename.split(sep)[::-1]
+ # Number of unprocessed parts in 'rest'. This can differ from len(rest)
+ # later, because 'rest' might contain markers for unresolved symlinks.
+ part_count = len(rest)
+
# The resolved path, which is absolute throughout this function.
# Note: getcwd() returns a normalized and symlink-free path.
path = sep if filename.startswith(sep) else getcwd()
@@ -418,12 +423,13 @@ def realpath(filename, *, strict=False):
# the same links.
seen = {}
- while rest:
+ while part_count:
name = rest.pop()
if name is None:
# resolved symlink target
seen[rest.pop()] = path
continue
+ part_count -= 1
if not name or name == curdir:
# current dir
continue
@@ -436,8 +442,11 @@ def realpath(filename, *, strict=False):
else:
newpath = path + sep + name
try:
- st = os.lstat(newpath)
- if not stat.S_ISLNK(st.st_mode):
+ st_mode = os.lstat(newpath).st_mode
+ if not stat.S_ISLNK(st_mode):
+ if strict and part_count and not stat.S_ISDIR(st_mode):
+ raise OSError(errno.ENOTDIR, os.strerror(errno.ENOTDIR),
+ newpath)
path = newpath
continue
if newpath in seen:
@@ -469,7 +478,9 @@ def realpath(filename, *, strict=False):
rest.append(newpath)
rest.append(None)
# Push the unresolved symlink target parts onto the stack.
- rest.extend(target.split(sep)[::-1])
+ target_parts = target.split(sep)[::-1]
+ rest.extend(target_parts)
+ part_count += len(target_parts)
return path
diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py
index ca5cf42f8fcd71..b39255ebc79ac1 100644
--- a/Lib/test/test_posixpath.py
+++ b/Lib/test/test_posixpath.py
@@ -695,6 +695,65 @@ def test_realpath_unreadable_symlink(self):
os.chmod(ABSTFN, 0o755, follow_symlinks=False)
os.unlink(ABSTFN)
+ @skip_if_ABSTFN_contains_backslash
+ def test_realpath_nonterminal_file(self):
+ try:
+ with open(ABSTFN, 'w') as f:
+ f.write('test_posixpath wuz ere')
+ self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN)
+ self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN)
+ self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN)
+ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/",
strict=True)
+ self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN)
+ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.",
strict=True)
+ self.assertEqual(realpath(ABSTFN + "/..", strict=False),
dirname(ABSTFN))
+ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..",
strict=True)
+ self.assertEqual(realpath(ABSTFN + "/subdir", strict=False),
ABSTFN + "/subdir")
+ self.assertRaises(NotADirectoryError, realpath, ABSTFN +
"/subdir", strict=True)
+ finally:
+ os_helper.unlink(ABSTFN)
+
+ @os_helper.skip_unless_symlink
+ @skip_if_ABSTFN_contains_backslash
+ def test_realpath_nonterminal_symlink_to_file(self):
+ try:
+ with open(ABSTFN + "1", 'w') as f:
+ f.write('test_posixpath wuz ere')
+ os.symlink(ABSTFN + "1", ABSTFN)
+ self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN + "1")
+ self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "1")
+ self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN +
"1")
+ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/",
strict=True)
+ self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN +
"1")
+ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.",
strict=True)
+ self.assertEqual(realpath(ABSTFN + "/..", strict=False),
dirname(ABSTFN))
+ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..",
strict=True)
+ self.assertEqual(realpath(ABSTFN + "/subdir", strict=False),
ABSTFN + "1/subdir")
+ self.assertRaises(NotADirectoryError, realpath, ABSTFN +
"/subdir", strict=True)
+ finally:
+ os_helper.unlink(ABSTFN)
+
+ @os_helper.skip_unless_symlink
+ @skip_if_ABSTFN_contains_backslash
+ def test_realpath_nonterminal_symlink_to_symlinks_to_file(self):
+ try:
+ with open(ABSTFN + "2", 'w') as f:
+ f.write('test_posixpath wuz ere')
+ os.symlink(ABSTFN + "2", ABSTFN + "1")
+ os.symlink(ABSTFN + "1", ABSTFN)
+ self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN + "2")
+ self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "2")
+ self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN +
"2")
+ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/",
strict=True)
+ self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN +
"2")
+ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.",
strict=True)
+ self.assertEqual(realpath(ABSTFN + "/..", strict=False),
dirname(ABSTFN))
+ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..",
strict=True)
+ self.assertEqual(realpath(ABSTFN + "/subdir", strict=False),
ABSTFN + "2/subdir")
+ self.assertRaises(NotADirectoryError, realpath, ABSTFN +
"/subdir", strict=True)
+ finally:
+ os_helper.unlink(ABSTFN)
+
def test_relpath(self):
(real_getcwd, os.getcwd) = (os.getcwd, lambda: r"/home/user/bar")
try:
diff --git
a/Misc/NEWS.d/next/Library/2024-06-05-19-09-36.gh-issue-118289.moL9_d.rst
b/Misc/NEWS.d/next/Library/2024-06-05-19-09-36.gh-issue-118289.moL9_d.rst
new file mode 100644
index 00000000000000..522572e160ba7b
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-06-05-19-09-36.gh-issue-118289.moL9_d.rst
@@ -0,0 +1,2 @@
+:func:`!posixpath.realpath` now raises :exc:`NotADirectoryError` when *strict*
+mode is enabled and a non-directory path with a trailing slash is supplied.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]