Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b66728d

Browse files
authored
[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 fd4b545)
1 parent ad1b23b commit b66728d

File tree

3 files changed

+76
-4
lines changed

3 files changed

+76
-4
lines changed

Lib/posixpath.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
altsep = None
2323
devnull = '/dev/null'
2424

25+
import errno
2526
import os
2627
import sys
2728
import stat
@@ -408,6 +409,10 @@ def realpath(filename, *, strict=False):
408409
# very fast way of spelling list(reversed(...)).
409410
rest = filename.split(sep)[::-1]
410411

412+
# Number of unprocessed parts in 'rest'. This can differ from len(rest)
413+
# later, because 'rest' might contain markers for unresolved symlinks.
414+
part_count = len(rest)
415+
411416
# The resolved path, which is absolute throughout this function.
412417
# Note: getcwd() returns a normalized and symlink-free path.
413418
path = sep if filename.startswith(sep) else getcwd()
@@ -418,12 +423,13 @@ def realpath(filename, *, strict=False):
418423
# the same links.
419424
seen = {}
420425

421-
while rest:
426+
while part_count:
422427
name = rest.pop()
423428
if name is None:
424429
# resolved symlink target
425430
seen[rest.pop()] = path
426431
continue
432+
part_count -= 1
427433
if not name or name == curdir:
428434
# current dir
429435
continue
@@ -436,8 +442,11 @@ def realpath(filename, *, strict=False):
436442
else:
437443
newpath = path + sep + name
438444
try:
439-
st = os.lstat(newpath)
440-
if not stat.S_ISLNK(st.st_mode):
445+
st_mode = os.lstat(newpath).st_mode
446+
if not stat.S_ISLNK(st_mode):
447+
if strict and part_count and not stat.S_ISDIR(st_mode):
448+
raise OSError(errno.ENOTDIR, os.strerror(errno.ENOTDIR),
449+
newpath)
441450
path = newpath
442451
continue
443452
if newpath in seen:
@@ -469,7 +478,9 @@ def realpath(filename, *, strict=False):
469478
rest.append(newpath)
470479
rest.append(None)
471480
# Push the unresolved symlink target parts onto the stack.
472-
rest.extend(target.split(sep)[::-1])
481+
target_parts = target.split(sep)[::-1]
482+
rest.extend(target_parts)
483+
part_count += len(target_parts)
473484

474485
return path
475486

Lib/test/test_posixpath.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,65 @@ def test_realpath_unreadable_symlink(self):
695695
os.chmod(ABSTFN, 0o755, follow_symlinks=False)
696696
os.unlink(ABSTFN)
697697

698+
@skip_if_ABSTFN_contains_backslash
699+
def test_realpath_nonterminal_file(self):
700+
try:
701+
with open(ABSTFN, 'w') as f:
702+
f.write('test_posixpath wuz ere')
703+
self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN)
704+
self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN)
705+
self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN)
706+
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True)
707+
self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN)
708+
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True)
709+
self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN))
710+
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True)
711+
self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "/subdir")
712+
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True)
713+
finally:
714+
os_helper.unlink(ABSTFN)
715+
716+
@os_helper.skip_unless_symlink
717+
@skip_if_ABSTFN_contains_backslash
718+
def test_realpath_nonterminal_symlink_to_file(self):
719+
try:
720+
with open(ABSTFN + "1", 'w') as f:
721+
f.write('test_posixpath wuz ere')
722+
os.symlink(ABSTFN + "1", ABSTFN)
723+
self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN + "1")
724+
self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "1")
725+
self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN + "1")
726+
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True)
727+
self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN + "1")
728+
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True)
729+
self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN))
730+
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True)
731+
self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "1/subdir")
732+
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True)
733+
finally:
734+
os_helper.unlink(ABSTFN)
735+
736+
@os_helper.skip_unless_symlink
737+
@skip_if_ABSTFN_contains_backslash
738+
def test_realpath_nonterminal_symlink_to_symlinks_to_file(self):
739+
try:
740+
with open(ABSTFN + "2", 'w') as f:
741+
f.write('test_posixpath wuz ere')
742+
os.symlink(ABSTFN + "2", ABSTFN + "1")
743+
os.symlink(ABSTFN + "1", ABSTFN)
744+
self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN + "2")
745+
self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "2")
746+
self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN + "2")
747+
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True)
748+
self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN + "2")
749+
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True)
750+
self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN))
751+
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True)
752+
self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "2/subdir")
753+
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True)
754+
finally:
755+
os_helper.unlink(ABSTFN)
756+
698757
def test_relpath(self):
699758
(real_getcwd, os.getcwd) = (os.getcwd, lambda: r"/home/user/bar")
700759
try:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:func:`!posixpath.realpath` now raises :exc:`NotADirectoryError` when *strict*
2+
mode is enabled and a non-directory path with a trailing slash is supplied.

0 commit comments

Comments
 (0)