Skip to content

Commit

Permalink
pythonGH-118289: Fix handling of non-directories in os.path.realpath()
Browse files Browse the repository at this point in the history
In strict mode, raise `NotADirectoryError` if a file path is given with a
trailing slash, or subsequent dot segments.

We use a `part_count` variable rather than `len(rest)` because the `rest`
stack also contains markers for unresolved symlinks.
  • Loading branch information
barneygale committed Apr 25, 2024
1 parent 5865fa5 commit 59f7fde
Show file tree
Hide file tree
Showing 3 changed files with 29 additions and 4 deletions.
17 changes: 13 additions & 4 deletions Lib/posixpath.py
Expand Up @@ -22,6 +22,7 @@
altsep = None
devnull = '/dev/null'

import errno
import os
import sys
import stat
Expand Down Expand Up @@ -421,6 +422,7 @@ def realpath(filename, *, strict=False):
# symlink path can be retrieved by popping again. The [::-1] slice is a
# very fast way of spelling list(reversed(...)).
rest = filename.split(sep)[::-1]
part_count = len(rest)

# The resolved path, which is absolute throughout this function.
# Note: getcwd() returns a normalized and symlink-free path.
Expand All @@ -432,12 +434,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
Expand All @@ -450,8 +453,12 @@ 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 stat.S_ISLNK(st_mode):
pass
elif part_count and not stat.S_ISDIR(st_mode):
raise NotADirectoryError(errno.ENOTDIR, "Not a directory", newpath)
else:
path = newpath
continue
except OSError:
Expand All @@ -474,6 +481,7 @@ def realpath(filename, *, strict=False):
continue
seen[newpath] = None # not resolved symlink
target = os.readlink(newpath)
target_parts = target.split(sep)
if target.startswith(sep):
# Symlink target is absolute; reset resolved path.
path = sep
Expand All @@ -483,7 +491,8 @@ 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])
rest.extend(reversed(target_parts))
part_count += len(target_parts)

return path

Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_posixpath.py
Expand Up @@ -660,6 +660,20 @@ def test_realpath_resolve_first(self):
safe_rmdir(ABSTFN + "/k")
safe_rmdir(ABSTFN)

def test_realpath_strict_nondir(self):
try:
with open(ABSTFN, 'w') as f:
f.write('test_posixpath wuz ere')
self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN)
with self.assertRaises(NotADirectoryError):
realpath(ABSTFN + "/", strict=True)
with self.assertRaises(NotADirectoryError):
realpath(ABSTFN + "/.", strict=True)
with 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:
Expand Down
@@ -0,0 +1,2 @@
:func:`os.path.realpath` now raises :exc:`NotADirectoryError` when *strict*
mode is enabled and a non-directory path with a trailing slash is supplied.

0 comments on commit 59f7fde

Please sign in to comment.