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
10 changes: 8 additions & 2 deletions Doc/library/os.path.rst
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ the :mod:`glob` module.)
In particular, :exc:`FileNotFoundError` is raised if *path* does not exist,
or another :exc:`OSError` if it is otherwise inaccessible.

If *strict* is the string ``'allow_missing'``, errors other than
If *strict* is :py:data:`os.path.ALLOW_MISSING`, errors other than
:exc:`FileNotFoundError` are re-raised (as with ``strict=True``).
Thus, the returned path will not contain any symbolic links, but the named
file and some of its parent directories may be missing.
Expand All @@ -447,7 +447,13 @@ the :mod:`glob` module.)
The *strict* parameter was added.

.. versionchanged:: next
The ``'allow_missing'`` value for *strict* parameter was added.
The :py:data:`~ntpath.ALLOW_MISSING` value for *strict* parameter was added.

.. data:: ALLOW_MISSING

Special value used for the *strict* argument in :func:`realpath`.

.. versionadded:: next

.. function:: relpath(path, start=os.curdir)

Expand Down
2 changes: 1 addition & 1 deletion Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ os.path
-------

* The *strict* parameter to :func:`os.path.realpath` accepts a new value,
``'allow_missing'``.
:data:`os.path.ALLOW_MISSING`.
If used, errors other than :exc:`FileNotFoundError` will be re-raised;
the resulting path can be missing but it will be free of symlinks.
(Contributed by Petr Viktorin for :cve:`2025-4517`.)
Expand Down
11 changes: 10 additions & 1 deletion Lib/genericpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

__all__ = ['commonprefix', 'exists', 'getatime', 'getctime', 'getmtime',
'getsize', 'isdevdrive', 'isdir', 'isfile', 'isjunction', 'islink',
'lexists', 'samefile', 'sameopenfile', 'samestat']
'lexists', 'samefile', 'sameopenfile', 'samestat', 'ALLOW_MISSING']


# Does a path exist?
Expand Down Expand Up @@ -189,3 +189,12 @@ def _check_arg_types(funcname, *args):
f'os.PathLike object, not {s.__class__.__name__!r}') from None
if hasstr and hasbytes:
raise TypeError("Can't mix strings and bytes in path components") from None

# A singleton with a true boolean value.
@object.__new__
class ALLOW_MISSING:
"""Special value for use in realpath()."""
def __repr__(self):
return 'os.path.ALLOW_MISSING'
def __reduce__(self):
return self.__class__.__name__
4 changes: 2 additions & 2 deletions Lib/ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"abspath","curdir","pardir","sep","pathsep","defpath","altsep",
"extsep","devnull","realpath","supports_unicode_filenames","relpath",
"samefile", "sameopenfile", "samestat", "commonpath", "isjunction",
"isdevdrive"]
"isdevdrive", "ALLOW_MISSING"]

def _get_bothseps(path):
if isinstance(path, bytes):
Expand Down Expand Up @@ -724,7 +724,7 @@ def realpath(path, *, strict=False):
return '\\\\.\\NUL'
had_prefix = path.startswith(prefix)

if strict == 'allow_missing':
if strict is ALLOW_MISSING:
ignored_error = FileNotFoundError
strict = True
elif strict:
Expand Down
4 changes: 2 additions & 2 deletions Lib/posixpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"samefile","sameopenfile","samestat",
"curdir","pardir","sep","pathsep","defpath","altsep","extsep",
"devnull","realpath","supports_unicode_filenames","relpath",
"commonpath", "isjunction","isdevdrive"]
"commonpath", "isjunction","isdevdrive","ALLOW_MISSING"]


def _get_sep(path):
Expand Down Expand Up @@ -402,7 +402,7 @@ def realpath(filename, *, strict=False):
curdir = '.'
pardir = '..'
getcwd = os.getcwd
if strict == 'allow_missing':
if strict is ALLOW_MISSING:
ignored_error = FileNotFoundError
strict = True
elif strict:
Expand Down
7 changes: 4 additions & 3 deletions Lib/tarfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -781,7 +781,7 @@ def __init__(self, tarinfo, path):
def _get_filtered_attrs(member, dest_path, for_data=True):
new_attrs = {}
name = member.name
dest_path = os.path.realpath(dest_path, strict='allow_missing')
dest_path = os.path.realpath(dest_path, strict=os.path.ALLOW_MISSING)
# Strip leading / (tar's directory separator) from filenames.
# Include os.sep (target OS directory separator) as well.
if name.startswith(('/', os.sep)):
Expand All @@ -792,7 +792,7 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
raise AbsolutePathError(member)
# Ensure we stay in the destination
target_path = os.path.realpath(os.path.join(dest_path, name),
strict='allow_missing')
strict=os.path.ALLOW_MISSING)
if os.path.commonpath([target_path, dest_path]) != dest_path:
raise OutsideDestinationError(member, target_path)
# Limit permissions (no high bits, and go-w)
Expand Down Expand Up @@ -840,7 +840,8 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
else:
target_path = os.path.join(dest_path,
member.linkname)
target_path = os.path.realpath(target_path, strict='allow_missing')
target_path = os.path.realpath(target_path,
strict=os.path.ALLOW_MISSING)
if os.path.commonpath([target_path, dest_path]) != dest_path:
raise LinkOutsideDestinationError(member, target_path)
return new_attrs
Expand Down
67 changes: 34 additions & 33 deletions Lib/test/test_ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import sys
import unittest
import warnings
from ntpath import ALLOW_MISSING
from test.support import TestFailed, cpython_only, os_helper
from test.support.os_helper import FakePath
from test import test_genericpath
Expand Down Expand Up @@ -505,15 +506,15 @@ def test_realpath_curdir_strict(self):

def test_realpath_curdir_missing_ok(self):
expected = ntpath.normpath(os.getcwd())
tester("ntpath.realpath('.', strict='allow_missing')",
tester("ntpath.realpath('.', strict=ALLOW_MISSING)",
expected)
tester("ntpath.realpath('./.', strict='allow_missing')",
tester("ntpath.realpath('./.', strict=ALLOW_MISSING)",
expected)
tester("ntpath.realpath('/'.join(['.'] * 100), strict='allow_missing')",
tester("ntpath.realpath('/'.join(['.'] * 100), strict=ALLOW_MISSING)",
expected)
tester("ntpath.realpath('.\\.', strict='allow_missing')",
tester("ntpath.realpath('.\\.', strict=ALLOW_MISSING)",
expected)
tester("ntpath.realpath('\\'.join(['.'] * 100), strict='allow_missing')",
tester("ntpath.realpath('\\'.join(['.'] * 100), strict=ALLOW_MISSING)",
expected)

def test_realpath_pardir(self):
Expand Down Expand Up @@ -542,20 +543,20 @@ def test_realpath_pardir_strict(self):

def test_realpath_pardir_missing_ok(self):
expected = ntpath.normpath(os.getcwd())
tester("ntpath.realpath('..', strict='allow_missing')",
tester("ntpath.realpath('..', strict=ALLOW_MISSING)",
ntpath.dirname(expected))
tester("ntpath.realpath('../..', strict='allow_missing')",
tester("ntpath.realpath('../..', strict=ALLOW_MISSING)",
ntpath.dirname(ntpath.dirname(expected)))
tester("ntpath.realpath('/'.join(['..'] * 50), strict='allow_missing')",
tester("ntpath.realpath('/'.join(['..'] * 50), strict=ALLOW_MISSING)",
ntpath.splitdrive(expected)[0] + '\\')
tester("ntpath.realpath('..\\..', strict='allow_missing')",
tester("ntpath.realpath('..\\..', strict=ALLOW_MISSING)",
ntpath.dirname(ntpath.dirname(expected)))
tester("ntpath.realpath('\\'.join(['..'] * 50), strict='allow_missing')",
tester("ntpath.realpath('\\'.join(['..'] * 50), strict=ALLOW_MISSING)",
ntpath.splitdrive(expected)[0] + '\\')

@os_helper.skip_unless_symlink
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
@_parameterize({}, {'strict': True}, {'strict': 'allow_missing'})
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
def test_realpath_basic(self, kwargs):
ABSTFN = ntpath.abspath(os_helper.TESTFN)
open(ABSTFN, "wb").close()
Expand Down Expand Up @@ -603,38 +604,38 @@ def test_realpath_invalid_paths(self):
self.assertEqual(realpath(path, strict=False), path)
# gh-106242: Embedded nulls should raise OSError (not ValueError)
self.assertRaises(OSError, realpath, path, strict=True)
self.assertRaises(OSError, realpath, path, strict='allow_missing')
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
path = ABSTFNb + b'\x00'
self.assertEqual(realpath(path, strict=False), path)
self.assertRaises(OSError, realpath, path, strict=True)
self.assertRaises(OSError, realpath, path, strict='allow_missing')
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
path = ABSTFN + '\\nonexistent\\x\x00'
self.assertEqual(realpath(path, strict=False), path)
self.assertRaises(OSError, realpath, path, strict=True)
self.assertRaises(OSError, realpath, path, strict='allow_missing')
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
path = ABSTFNb + b'\\nonexistent\\x\x00'
self.assertEqual(realpath(path, strict=False), path)
self.assertRaises(OSError, realpath, path, strict=True)
self.assertRaises(OSError, realpath, path, strict='allow_missing')
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
path = ABSTFN + '\x00\\..'
self.assertEqual(realpath(path, strict=False), os.getcwd())
self.assertEqual(realpath(path, strict=True), os.getcwd())
self.assertEqual(realpath(path, strict='allow_missing'), os.getcwd())
self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwd())
path = ABSTFNb + b'\x00\\..'
self.assertEqual(realpath(path, strict=False), os.getcwdb())
self.assertEqual(realpath(path, strict=True), os.getcwdb())
self.assertEqual(realpath(path, strict='allow_missing'), os.getcwdb())
self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwdb())
path = ABSTFN + '\\nonexistent\\x\x00\\..'
self.assertEqual(realpath(path, strict=False), ABSTFN + '\\nonexistent')
self.assertRaises(OSError, realpath, path, strict=True)
self.assertEqual(realpath(path, strict='allow_missing'), ABSTFN + '\\nonexistent')
self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFN + '\\nonexistent')
path = ABSTFNb + b'\\nonexistent\\x\x00\\..'
self.assertEqual(realpath(path, strict=False), ABSTFNb + b'\\nonexistent')
self.assertRaises(OSError, realpath, path, strict=True)
self.assertEqual(realpath(path, strict='allow_missing'), ABSTFNb + b'\\nonexistent')
self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFNb + b'\\nonexistent')

@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
@_parameterize({}, {'strict': True}, {'strict': 'allow_missing'})
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
def test_realpath_invalid_unicode_paths(self, kwargs):
realpath = ntpath.realpath
ABSTFN = ntpath.abspath(os_helper.TESTFN)
Expand All @@ -654,7 +655,7 @@ def test_realpath_invalid_unicode_paths(self, kwargs):

@os_helper.skip_unless_symlink
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
@_parameterize({}, {'strict': True}, {'strict': 'allow_missing'})
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
def test_realpath_relative(self, kwargs):
ABSTFN = ntpath.abspath(os_helper.TESTFN)
open(ABSTFN, "wb").close()
Expand Down Expand Up @@ -815,7 +816,7 @@ def test_realpath_symlink_loops_strict(self):
@os_helper.skip_unless_symlink
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
def test_realpath_symlink_loops_raise(self):
# Symlink loops raise OSError in 'allow_missing' mode
# Symlink loops raise OSError in ALLOW_MISSING mode
ABSTFN = ntpath.abspath(os_helper.TESTFN)
self.addCleanup(os_helper.unlink, ABSTFN)
self.addCleanup(os_helper.unlink, ABSTFN + "1")
Expand All @@ -826,16 +827,16 @@ def test_realpath_symlink_loops_raise(self):
self.addCleanup(os_helper.unlink, ABSTFN + "x")

os.symlink(ABSTFN, ABSTFN)
self.assertRaises(OSError, ntpath.realpath, ABSTFN, strict='allow_missing')
self.assertRaises(OSError, ntpath.realpath, ABSTFN, strict=ALLOW_MISSING)

os.symlink(ABSTFN + "1", ABSTFN + "2")
os.symlink(ABSTFN + "2", ABSTFN + "1")
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1",
strict='allow_missing')
strict=ALLOW_MISSING)
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "2",
strict='allow_missing')
strict=ALLOW_MISSING)
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\\x",
strict='allow_missing')
strict=ALLOW_MISSING)

# Windows eliminates '..' components before resolving links;
# realpath is not expected to raise if this removes the loop.
Expand All @@ -851,24 +852,24 @@ def test_realpath_symlink_loops_raise(self):
self.assertRaises(
OSError, ntpath.realpath,
ABSTFN + "1\\..\\" + ntpath.basename(ABSTFN) + "1",
strict='allow_missing')
strict=ALLOW_MISSING)

os.symlink(ntpath.basename(ABSTFN) + "a\\b", ABSTFN + "a")
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "a",
strict='allow_missing')
strict=ALLOW_MISSING)

os.symlink("..\\" + ntpath.basename(ntpath.dirname(ABSTFN))
+ "\\" + ntpath.basename(ABSTFN) + "c", ABSTFN + "c")
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "c",
strict='allow_missing')
strict=ALLOW_MISSING)

# Test using relative path as well.
self.assertRaises(OSError, ntpath.realpath, ntpath.basename(ABSTFN),
strict='allow_missing')
strict=ALLOW_MISSING)

@os_helper.skip_unless_symlink
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
@_parameterize({}, {'strict': True}, {'strict': 'allow_missing'})
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
def test_realpath_symlink_prefix(self, kwargs):
ABSTFN = ntpath.abspath(os_helper.TESTFN)
self.addCleanup(os_helper.unlink, ABSTFN + "3")
Expand Down Expand Up @@ -906,7 +907,7 @@ def test_realpath_nul(self):
tester("ntpath.realpath('NUL')", r'\\.\NUL')
tester("ntpath.realpath('NUL', strict=False)", r'\\.\NUL')
tester("ntpath.realpath('NUL', strict=True)", r'\\.\NUL')
tester("ntpath.realpath('NUL', strict='allow_missing')", r'\\.\NUL')
tester("ntpath.realpath('NUL', strict=ALLOW_MISSING)", r'\\.\NUL')

@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
@unittest.skipUnless(HAVE_GETSHORTPATHNAME, 'need _getshortpathname')
Expand All @@ -930,7 +931,7 @@ def test_realpath_cwd(self):

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

for kwargs in {}, {'strict': True}, {'strict': 'allow_missing'}:
for kwargs in {}, {'strict': True}, {'strict': ALLOW_MISSING}:
with self.subTest(**kwargs):
with os_helper.change_cwd(test_dir_long):
self.assertPathEqual(
Expand Down
Loading