mirror of https://github.com/python/cpython.git
[3.13] gh-135034: Normalize link targets in tarfile, add `os.path.realpath(strict='allow_missing')` (GH-135037) (GH-135064)
Addresses CVEs 2024-12718, 2025-4138, 2025-4330, and 2025-4517.
(cherry picked from commit 3612d8f517
)
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
Signed-off-by: Łukasz Langa <lukasz@langa.pl>
Co-authored-by: Petr Viktorin <encukou@gmail.com>
Co-authored-by: Seth Michael Larson <seth@python.org>
Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
parent
9f3d99967c
commit
aa9eb5f757
|
@ -408,9 +408,26 @@ the :mod:`glob` module.)
|
||||||
system). On Windows, this function will also resolve MS-DOS (also called 8.3)
|
system). On Windows, this function will also resolve MS-DOS (also called 8.3)
|
||||||
style names such as ``C:\\PROGRA~1`` to ``C:\\Program Files``.
|
style names such as ``C:\\PROGRA~1`` to ``C:\\Program Files``.
|
||||||
|
|
||||||
If a path doesn't exist or a symlink loop is encountered, and *strict* is
|
By default, the path is evaluated up to the first component that does not
|
||||||
``True``, :exc:`OSError` is raised. If *strict* is ``False`` these errors
|
exist, is a symlink loop, or whose evaluation raises :exc:`OSError`.
|
||||||
are ignored, and so the result might be missing or otherwise inaccessible.
|
All such components are appended unchanged to the existing part of the path.
|
||||||
|
|
||||||
|
Some errors that are handled this way include "access denied", "not a
|
||||||
|
directory", or "bad argument to internal function". Thus, the
|
||||||
|
resulting path may be missing or inaccessible, may still contain
|
||||||
|
links or loops, and may traverse non-directories.
|
||||||
|
|
||||||
|
This behavior can be modified by keyword arguments:
|
||||||
|
|
||||||
|
If *strict* is ``True``, the first error encountered when evaluating the path is
|
||||||
|
re-raised.
|
||||||
|
In particular, :exc:`FileNotFoundError` is raised if *path* does not exist,
|
||||||
|
or another :exc:`OSError` if it is otherwise inaccessible.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This function emulates the operating system's procedure for making a path
|
This function emulates the operating system's procedure for making a path
|
||||||
|
@ -429,6 +446,15 @@ the :mod:`glob` module.)
|
||||||
.. versionchanged:: 3.10
|
.. versionchanged:: 3.10
|
||||||
The *strict* parameter was added.
|
The *strict* parameter was added.
|
||||||
|
|
||||||
|
.. versionchanged:: next
|
||||||
|
The :py:data:`~os.path.ALLOW_MISSING` value for the *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)
|
.. function:: relpath(path, start=os.curdir)
|
||||||
|
|
||||||
|
|
|
@ -249,6 +249,15 @@ The :mod:`tarfile` module defines the following exceptions:
|
||||||
Raised to refuse extracting a symbolic link pointing outside the destination
|
Raised to refuse extracting a symbolic link pointing outside the destination
|
||||||
directory.
|
directory.
|
||||||
|
|
||||||
|
.. exception:: LinkFallbackError
|
||||||
|
|
||||||
|
Raised to refuse emulating a link (hard or symbolic) by extracting another
|
||||||
|
archive member, when that member would be rejected by the filter location.
|
||||||
|
The exception that was raised to reject the replacement member is available
|
||||||
|
as :attr:`!BaseException.__context__`.
|
||||||
|
|
||||||
|
.. versionadded:: next
|
||||||
|
|
||||||
|
|
||||||
The following constants are available at the module level:
|
The following constants are available at the module level:
|
||||||
|
|
||||||
|
@ -1052,6 +1061,12 @@ reused in custom filters:
|
||||||
Implements the ``'data'`` filter.
|
Implements the ``'data'`` filter.
|
||||||
In addition to what ``tar_filter`` does:
|
In addition to what ``tar_filter`` does:
|
||||||
|
|
||||||
|
- Normalize link targets (:attr:`TarInfo.linkname`) using
|
||||||
|
:func:`os.path.normpath`.
|
||||||
|
Note that this removes internal ``..`` components, which may change the
|
||||||
|
meaning of the link if the path in :attr:`!TarInfo.linkname` traverses
|
||||||
|
symbolic links.
|
||||||
|
|
||||||
- :ref:`Refuse <tarfile-extraction-refuse>` to extract links (hard or soft)
|
- :ref:`Refuse <tarfile-extraction-refuse>` to extract links (hard or soft)
|
||||||
that link to absolute paths, or ones that link outside the destination.
|
that link to absolute paths, or ones that link outside the destination.
|
||||||
|
|
||||||
|
@ -1080,6 +1095,10 @@ reused in custom filters:
|
||||||
|
|
||||||
Return the modified ``TarInfo`` member.
|
Return the modified ``TarInfo`` member.
|
||||||
|
|
||||||
|
.. versionchanged:: next
|
||||||
|
|
||||||
|
Link targets are now normalized.
|
||||||
|
|
||||||
|
|
||||||
.. _tarfile-extraction-refuse:
|
.. _tarfile-extraction-refuse:
|
||||||
|
|
||||||
|
@ -1106,6 +1125,7 @@ Here is an incomplete list of things to consider:
|
||||||
* Extract to a :func:`new temporary directory <tempfile.mkdtemp>`
|
* Extract to a :func:`new temporary directory <tempfile.mkdtemp>`
|
||||||
to prevent e.g. exploiting pre-existing links, and to make it easier to
|
to prevent e.g. exploiting pre-existing links, and to make it easier to
|
||||||
clean up after a failed extraction.
|
clean up after a failed extraction.
|
||||||
|
* Disallow symbolic links if you do not need the functionality.
|
||||||
* When working with untrusted data, use external (e.g. OS-level) limits on
|
* When working with untrusted data, use external (e.g. OS-level) limits on
|
||||||
disk, memory and CPU usage.
|
disk, memory and CPU usage.
|
||||||
* Check filenames against an allow-list of characters
|
* Check filenames against an allow-list of characters
|
||||||
|
|
|
@ -2829,3 +2829,36 @@ sys
|
||||||
* The previously undocumented special function :func:`sys.getobjects`,
|
* The previously undocumented special function :func:`sys.getobjects`,
|
||||||
which only exists in specialized builds of Python, may now return objects
|
which only exists in specialized builds of Python, may now return objects
|
||||||
from other interpreters than the one it's called in.
|
from other interpreters than the one it's called in.
|
||||||
|
|
||||||
|
Notable changes in 3.13.4
|
||||||
|
=========================
|
||||||
|
|
||||||
|
os.path
|
||||||
|
-------
|
||||||
|
|
||||||
|
* The *strict* parameter to :func:`os.path.realpath` accepts a new value,
|
||||||
|
: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`.)
|
||||||
|
|
||||||
|
tarfile
|
||||||
|
-------
|
||||||
|
|
||||||
|
* :func:`~tarfile.data_filter` now normalizes symbolic link targets in order to
|
||||||
|
avoid path traversal attacks.Add commentMore actions
|
||||||
|
(Contributed by Petr Viktorin in :gh:`127987` and :cve:`2025-4138`.)
|
||||||
|
* :func:`~tarfile.TarFile.extractall` now skips fixing up directory attributes
|
||||||
|
when a directory was removed or replaced by another kind of file.
|
||||||
|
(Contributed by Petr Viktorin in :gh:`127987` and :cve:`2024-12718`.)
|
||||||
|
* :func:`~tarfile.TarFile.extract` and :func:`~tarfile.TarFile.extractall`
|
||||||
|
now (re-)apply the extraction filter when substituting a link (hard or
|
||||||
|
symbolic) with a copy of another archive member, and when fixing up
|
||||||
|
directory attributes.
|
||||||
|
The former raises a new exception, :exc:`~tarfile.LinkFallbackError`.
|
||||||
|
(Contributed by Petr Viktorin for :cve:`2025-4330` and :cve:`2024-12718`.)
|
||||||
|
* :func:`~tarfile.TarFile.extract` and :func:`~tarfile.TarFile.extractall`
|
||||||
|
no longer extract rejected members when
|
||||||
|
:func:`~tarfile.TarFile.errorlevel` is zero.
|
||||||
|
(Contributed by Matt Prodani and Petr Viktorin in :gh:`112887`
|
||||||
|
and :cve:`2025-4435`.)
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
__all__ = ['commonprefix', 'exists', 'getatime', 'getctime', 'getmtime',
|
__all__ = ['commonprefix', 'exists', 'getatime', 'getctime', 'getmtime',
|
||||||
'getsize', 'isdevdrive', 'isdir', 'isfile', 'isjunction', 'islink',
|
'getsize', 'isdevdrive', 'isdir', 'isfile', 'isjunction', 'islink',
|
||||||
'lexists', 'samefile', 'sameopenfile', 'samestat']
|
'lexists', 'samefile', 'sameopenfile', 'samestat', 'ALLOW_MISSING']
|
||||||
|
|
||||||
|
|
||||||
# Does a path exist?
|
# Does a path exist?
|
||||||
|
@ -189,3 +189,12 @@ def _check_arg_types(funcname, *args):
|
||||||
f'os.PathLike object, not {s.__class__.__name__!r}') from None
|
f'os.PathLike object, not {s.__class__.__name__!r}') from None
|
||||||
if hasstr and hasbytes:
|
if hasstr and hasbytes:
|
||||||
raise TypeError("Can't mix strings and bytes in path components") from None
|
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__
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
"abspath","curdir","pardir","sep","pathsep","defpath","altsep",
|
"abspath","curdir","pardir","sep","pathsep","defpath","altsep",
|
||||||
"extsep","devnull","realpath","supports_unicode_filenames","relpath",
|
"extsep","devnull","realpath","supports_unicode_filenames","relpath",
|
||||||
"samefile", "sameopenfile", "samestat", "commonpath", "isjunction",
|
"samefile", "sameopenfile", "samestat", "commonpath", "isjunction",
|
||||||
"isdevdrive"]
|
"isdevdrive", "ALLOW_MISSING"]
|
||||||
|
|
||||||
def _get_bothseps(path):
|
def _get_bothseps(path):
|
||||||
if isinstance(path, bytes):
|
if isinstance(path, bytes):
|
||||||
|
@ -601,9 +601,10 @@ def abspath(path):
|
||||||
from nt import _findfirstfile, _getfinalpathname, readlink as _nt_readlink
|
from nt import _findfirstfile, _getfinalpathname, readlink as _nt_readlink
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# realpath is a no-op on systems without _getfinalpathname support.
|
# realpath is a no-op on systems without _getfinalpathname support.
|
||||||
realpath = abspath
|
def realpath(path, *, strict=False):
|
||||||
|
return abspath(path)
|
||||||
else:
|
else:
|
||||||
def _readlink_deep(path):
|
def _readlink_deep(path, ignored_error=OSError):
|
||||||
# These error codes indicate that we should stop reading links and
|
# These error codes indicate that we should stop reading links and
|
||||||
# return the path we currently have.
|
# return the path we currently have.
|
||||||
# 1: ERROR_INVALID_FUNCTION
|
# 1: ERROR_INVALID_FUNCTION
|
||||||
|
@ -636,7 +637,7 @@ def _readlink_deep(path):
|
||||||
path = old_path
|
path = old_path
|
||||||
break
|
break
|
||||||
path = normpath(join(dirname(old_path), path))
|
path = normpath(join(dirname(old_path), path))
|
||||||
except OSError as ex:
|
except ignored_error as ex:
|
||||||
if ex.winerror in allowed_winerror:
|
if ex.winerror in allowed_winerror:
|
||||||
break
|
break
|
||||||
raise
|
raise
|
||||||
|
@ -645,7 +646,7 @@ def _readlink_deep(path):
|
||||||
break
|
break
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def _getfinalpathname_nonstrict(path):
|
def _getfinalpathname_nonstrict(path, ignored_error=OSError):
|
||||||
# These error codes indicate that we should stop resolving the path
|
# These error codes indicate that we should stop resolving the path
|
||||||
# and return the value we currently have.
|
# and return the value we currently have.
|
||||||
# 1: ERROR_INVALID_FUNCTION
|
# 1: ERROR_INVALID_FUNCTION
|
||||||
|
@ -673,17 +674,18 @@ def _getfinalpathname_nonstrict(path):
|
||||||
try:
|
try:
|
||||||
path = _getfinalpathname(path)
|
path = _getfinalpathname(path)
|
||||||
return join(path, tail) if tail else path
|
return join(path, tail) if tail else path
|
||||||
except OSError as ex:
|
except ignored_error as ex:
|
||||||
if ex.winerror not in allowed_winerror:
|
if ex.winerror not in allowed_winerror:
|
||||||
raise
|
raise
|
||||||
try:
|
try:
|
||||||
# The OS could not resolve this path fully, so we attempt
|
# The OS could not resolve this path fully, so we attempt
|
||||||
# to follow the link ourselves. If we succeed, join the tail
|
# to follow the link ourselves. If we succeed, join the tail
|
||||||
# and return.
|
# and return.
|
||||||
new_path = _readlink_deep(path)
|
new_path = _readlink_deep(path,
|
||||||
|
ignored_error=ignored_error)
|
||||||
if new_path != path:
|
if new_path != path:
|
||||||
return join(new_path, tail) if tail else new_path
|
return join(new_path, tail) if tail else new_path
|
||||||
except OSError:
|
except ignored_error:
|
||||||
# If we fail to readlink(), let's keep traversing
|
# If we fail to readlink(), let's keep traversing
|
||||||
pass
|
pass
|
||||||
# If we get these errors, try to get the real name of the file without accessing it.
|
# If we get these errors, try to get the real name of the file without accessing it.
|
||||||
|
@ -691,7 +693,7 @@ def _getfinalpathname_nonstrict(path):
|
||||||
try:
|
try:
|
||||||
name = _findfirstfile(path)
|
name = _findfirstfile(path)
|
||||||
path, _ = split(path)
|
path, _ = split(path)
|
||||||
except OSError:
|
except ignored_error:
|
||||||
path, name = split(path)
|
path, name = split(path)
|
||||||
else:
|
else:
|
||||||
path, name = split(path)
|
path, name = split(path)
|
||||||
|
@ -721,6 +723,15 @@ def realpath(path, *, strict=False):
|
||||||
if normcase(path) == devnull:
|
if normcase(path) == devnull:
|
||||||
return '\\\\.\\NUL'
|
return '\\\\.\\NUL'
|
||||||
had_prefix = path.startswith(prefix)
|
had_prefix = path.startswith(prefix)
|
||||||
|
|
||||||
|
if strict is ALLOW_MISSING:
|
||||||
|
ignored_error = FileNotFoundError
|
||||||
|
strict = True
|
||||||
|
elif strict:
|
||||||
|
ignored_error = ()
|
||||||
|
else:
|
||||||
|
ignored_error = OSError
|
||||||
|
|
||||||
if not had_prefix and not isabs(path):
|
if not had_prefix and not isabs(path):
|
||||||
path = join(cwd, path)
|
path = join(cwd, path)
|
||||||
try:
|
try:
|
||||||
|
@ -728,17 +739,16 @@ def realpath(path, *, strict=False):
|
||||||
initial_winerror = 0
|
initial_winerror = 0
|
||||||
except ValueError as ex:
|
except ValueError as ex:
|
||||||
# gh-106242: Raised for embedded null characters
|
# gh-106242: Raised for embedded null characters
|
||||||
# In strict mode, we convert into an OSError.
|
# In strict modes, we convert into an OSError.
|
||||||
# Non-strict mode returns the path as-is, since we've already
|
# Non-strict mode returns the path as-is, since we've already
|
||||||
# made it absolute.
|
# made it absolute.
|
||||||
if strict:
|
if strict:
|
||||||
raise OSError(str(ex)) from None
|
raise OSError(str(ex)) from None
|
||||||
path = normpath(path)
|
path = normpath(path)
|
||||||
except OSError as ex:
|
except ignored_error as ex:
|
||||||
if strict:
|
|
||||||
raise
|
|
||||||
initial_winerror = ex.winerror
|
initial_winerror = ex.winerror
|
||||||
path = _getfinalpathname_nonstrict(path)
|
path = _getfinalpathname_nonstrict(path,
|
||||||
|
ignored_error=ignored_error)
|
||||||
# The path returned by _getfinalpathname will always start with \\?\ -
|
# The path returned by _getfinalpathname will always start with \\?\ -
|
||||||
# strip off that prefix unless it was already provided on the original
|
# strip off that prefix unless it was already provided on the original
|
||||||
# path.
|
# path.
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
"samefile","sameopenfile","samestat",
|
"samefile","sameopenfile","samestat",
|
||||||
"curdir","pardir","sep","pathsep","defpath","altsep","extsep",
|
"curdir","pardir","sep","pathsep","defpath","altsep","extsep",
|
||||||
"devnull","realpath","supports_unicode_filenames","relpath",
|
"devnull","realpath","supports_unicode_filenames","relpath",
|
||||||
"commonpath", "isjunction","isdevdrive"]
|
"commonpath", "isjunction","isdevdrive","ALLOW_MISSING"]
|
||||||
|
|
||||||
|
|
||||||
def _get_sep(path):
|
def _get_sep(path):
|
||||||
|
@ -402,6 +402,15 @@ def realpath(filename, *, strict=False):
|
||||||
curdir = '.'
|
curdir = '.'
|
||||||
pardir = '..'
|
pardir = '..'
|
||||||
getcwd = os.getcwd
|
getcwd = os.getcwd
|
||||||
|
if strict is ALLOW_MISSING:
|
||||||
|
ignored_error = FileNotFoundError
|
||||||
|
strict = True
|
||||||
|
elif strict:
|
||||||
|
ignored_error = ()
|
||||||
|
else:
|
||||||
|
ignored_error = OSError
|
||||||
|
|
||||||
|
maxlinks = None
|
||||||
|
|
||||||
# The stack of unresolved path parts. When popped, a special value of None
|
# The stack of unresolved path parts. When popped, a special value of None
|
||||||
# indicates that a symlink target has been resolved, and that the original
|
# indicates that a symlink target has been resolved, and that the original
|
||||||
|
@ -462,25 +471,28 @@ def realpath(filename, *, strict=False):
|
||||||
path = newpath
|
path = newpath
|
||||||
continue
|
continue
|
||||||
target = os.readlink(newpath)
|
target = os.readlink(newpath)
|
||||||
except OSError:
|
except ignored_error:
|
||||||
if strict:
|
pass
|
||||||
raise
|
else:
|
||||||
path = newpath
|
# Resolve the symbolic link
|
||||||
|
if target.startswith(sep):
|
||||||
|
# Symlink target is absolute; reset resolved path.
|
||||||
|
path = sep
|
||||||
|
if maxlinks is None:
|
||||||
|
# Mark this symlink as seen but not fully resolved.
|
||||||
|
seen[newpath] = None
|
||||||
|
# Push the symlink path onto the stack, and signal its specialness
|
||||||
|
# by also pushing None. When these entries are popped, we'll
|
||||||
|
# record the fully-resolved symlink target in the 'seen' mapping.
|
||||||
|
rest.append(newpath)
|
||||||
|
rest.append(None)
|
||||||
|
# Push the unresolved symlink target parts onto the stack.
|
||||||
|
target_parts = target.split(sep)[::-1]
|
||||||
|
rest.extend(target_parts)
|
||||||
|
part_count += len(target_parts)
|
||||||
continue
|
continue
|
||||||
# Resolve the symbolic link
|
# An error occurred and was ignored.
|
||||||
seen[newpath] = None # not resolved symlink
|
path = newpath
|
||||||
if target.startswith(sep):
|
|
||||||
# Symlink target is absolute; reset resolved path.
|
|
||||||
path = sep
|
|
||||||
# Push the symlink path onto the stack, and signal its specialness by
|
|
||||||
# also pushing None. When these entries are popped, we'll record the
|
|
||||||
# fully-resolved symlink target in the 'seen' mapping.
|
|
||||||
rest.append(newpath)
|
|
||||||
rest.append(None)
|
|
||||||
# Push the unresolved symlink target parts onto the stack.
|
|
||||||
target_parts = target.split(sep)[::-1]
|
|
||||||
rest.extend(target_parts)
|
|
||||||
part_count += len(target_parts)
|
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
169
Lib/tarfile.py
169
Lib/tarfile.py
|
@ -68,7 +68,7 @@
|
||||||
"DEFAULT_FORMAT", "open","fully_trusted_filter", "data_filter",
|
"DEFAULT_FORMAT", "open","fully_trusted_filter", "data_filter",
|
||||||
"tar_filter", "FilterError", "AbsoluteLinkError",
|
"tar_filter", "FilterError", "AbsoluteLinkError",
|
||||||
"OutsideDestinationError", "SpecialFileError", "AbsolutePathError",
|
"OutsideDestinationError", "SpecialFileError", "AbsolutePathError",
|
||||||
"LinkOutsideDestinationError"]
|
"LinkOutsideDestinationError", "LinkFallbackError"]
|
||||||
|
|
||||||
|
|
||||||
#---------------------------------------------------------
|
#---------------------------------------------------------
|
||||||
|
@ -755,10 +755,22 @@ def __init__(self, tarinfo, path):
|
||||||
super().__init__(f'{tarinfo.name!r} would link to {path!r}, '
|
super().__init__(f'{tarinfo.name!r} would link to {path!r}, '
|
||||||
+ 'which is outside the destination')
|
+ 'which is outside the destination')
|
||||||
|
|
||||||
|
class LinkFallbackError(FilterError):
|
||||||
|
def __init__(self, tarinfo, path):
|
||||||
|
self.tarinfo = tarinfo
|
||||||
|
self._path = path
|
||||||
|
super().__init__(f'link {tarinfo.name!r} would be extracted as a '
|
||||||
|
+ f'copy of {path!r}, which was rejected')
|
||||||
|
|
||||||
|
# Errors caused by filters -- both "fatal" and "non-fatal" -- that
|
||||||
|
# we consider to be issues with the argument, rather than a bug in the
|
||||||
|
# filter function
|
||||||
|
_FILTER_ERRORS = (FilterError, OSError, ExtractError)
|
||||||
|
|
||||||
def _get_filtered_attrs(member, dest_path, for_data=True):
|
def _get_filtered_attrs(member, dest_path, for_data=True):
|
||||||
new_attrs = {}
|
new_attrs = {}
|
||||||
name = member.name
|
name = member.name
|
||||||
dest_path = os.path.realpath(dest_path)
|
dest_path = os.path.realpath(dest_path, strict=os.path.ALLOW_MISSING)
|
||||||
# Strip leading / (tar's directory separator) from filenames.
|
# Strip leading / (tar's directory separator) from filenames.
|
||||||
# Include os.sep (target OS directory separator) as well.
|
# Include os.sep (target OS directory separator) as well.
|
||||||
if name.startswith(('/', os.sep)):
|
if name.startswith(('/', os.sep)):
|
||||||
|
@ -768,7 +780,8 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
|
||||||
# For example, 'C:/foo' on Windows.
|
# For example, 'C:/foo' on Windows.
|
||||||
raise AbsolutePathError(member)
|
raise AbsolutePathError(member)
|
||||||
# Ensure we stay in the destination
|
# Ensure we stay in the destination
|
||||||
target_path = os.path.realpath(os.path.join(dest_path, name))
|
target_path = os.path.realpath(os.path.join(dest_path, name),
|
||||||
|
strict=os.path.ALLOW_MISSING)
|
||||||
if os.path.commonpath([target_path, dest_path]) != dest_path:
|
if os.path.commonpath([target_path, dest_path]) != dest_path:
|
||||||
raise OutsideDestinationError(member, target_path)
|
raise OutsideDestinationError(member, target_path)
|
||||||
# Limit permissions (no high bits, and go-w)
|
# Limit permissions (no high bits, and go-w)
|
||||||
|
@ -806,6 +819,9 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
|
||||||
if member.islnk() or member.issym():
|
if member.islnk() or member.issym():
|
||||||
if os.path.isabs(member.linkname):
|
if os.path.isabs(member.linkname):
|
||||||
raise AbsoluteLinkError(member)
|
raise AbsoluteLinkError(member)
|
||||||
|
normalized = os.path.normpath(member.linkname)
|
||||||
|
if normalized != member.linkname:
|
||||||
|
new_attrs['linkname'] = normalized
|
||||||
if member.issym():
|
if member.issym():
|
||||||
target_path = os.path.join(dest_path,
|
target_path = os.path.join(dest_path,
|
||||||
os.path.dirname(name),
|
os.path.dirname(name),
|
||||||
|
@ -813,7 +829,8 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
|
||||||
else:
|
else:
|
||||||
target_path = os.path.join(dest_path,
|
target_path = os.path.join(dest_path,
|
||||||
member.linkname)
|
member.linkname)
|
||||||
target_path = os.path.realpath(target_path)
|
target_path = os.path.realpath(target_path,
|
||||||
|
strict=os.path.ALLOW_MISSING)
|
||||||
if os.path.commonpath([target_path, dest_path]) != dest_path:
|
if os.path.commonpath([target_path, dest_path]) != dest_path:
|
||||||
raise LinkOutsideDestinationError(member, target_path)
|
raise LinkOutsideDestinationError(member, target_path)
|
||||||
return new_attrs
|
return new_attrs
|
||||||
|
@ -2323,30 +2340,58 @@ def extractall(self, path=".", members=None, *, numeric_owner=False,
|
||||||
members = self
|
members = self
|
||||||
|
|
||||||
for member in members:
|
for member in members:
|
||||||
tarinfo = self._get_extract_tarinfo(member, filter_function, path)
|
tarinfo, unfiltered = self._get_extract_tarinfo(
|
||||||
|
member, filter_function, path)
|
||||||
if tarinfo is None:
|
if tarinfo is None:
|
||||||
continue
|
continue
|
||||||
if tarinfo.isdir():
|
if tarinfo.isdir():
|
||||||
# For directories, delay setting attributes until later,
|
# For directories, delay setting attributes until later,
|
||||||
# since permissions can interfere with extraction and
|
# since permissions can interfere with extraction and
|
||||||
# extracting contents can reset mtime.
|
# extracting contents can reset mtime.
|
||||||
directories.append(tarinfo)
|
directories.append(unfiltered)
|
||||||
self._extract_one(tarinfo, path, set_attrs=not tarinfo.isdir(),
|
self._extract_one(tarinfo, path, set_attrs=not tarinfo.isdir(),
|
||||||
numeric_owner=numeric_owner)
|
numeric_owner=numeric_owner,
|
||||||
|
filter_function=filter_function)
|
||||||
|
|
||||||
# Reverse sort directories.
|
# Reverse sort directories.
|
||||||
directories.sort(key=lambda a: a.name, reverse=True)
|
directories.sort(key=lambda a: a.name, reverse=True)
|
||||||
|
|
||||||
|
|
||||||
# Set correct owner, mtime and filemode on directories.
|
# Set correct owner, mtime and filemode on directories.
|
||||||
for tarinfo in directories:
|
for unfiltered in directories:
|
||||||
dirpath = os.path.join(path, tarinfo.name)
|
|
||||||
try:
|
try:
|
||||||
|
# Need to re-apply any filter, to take the *current* filesystem
|
||||||
|
# state into account.
|
||||||
|
try:
|
||||||
|
tarinfo = filter_function(unfiltered, path)
|
||||||
|
except _FILTER_ERRORS as exc:
|
||||||
|
self._log_no_directory_fixup(unfiltered, repr(exc))
|
||||||
|
continue
|
||||||
|
if tarinfo is None:
|
||||||
|
self._log_no_directory_fixup(unfiltered,
|
||||||
|
'excluded by filter')
|
||||||
|
continue
|
||||||
|
dirpath = os.path.join(path, tarinfo.name)
|
||||||
|
try:
|
||||||
|
lstat = os.lstat(dirpath)
|
||||||
|
except FileNotFoundError:
|
||||||
|
self._log_no_directory_fixup(tarinfo, 'missing')
|
||||||
|
continue
|
||||||
|
if not stat.S_ISDIR(lstat.st_mode):
|
||||||
|
# This is no longer a directory; presumably a later
|
||||||
|
# member overwrote the entry.
|
||||||
|
self._log_no_directory_fixup(tarinfo, 'not a directory')
|
||||||
|
continue
|
||||||
self.chown(tarinfo, dirpath, numeric_owner=numeric_owner)
|
self.chown(tarinfo, dirpath, numeric_owner=numeric_owner)
|
||||||
self.utime(tarinfo, dirpath)
|
self.utime(tarinfo, dirpath)
|
||||||
self.chmod(tarinfo, dirpath)
|
self.chmod(tarinfo, dirpath)
|
||||||
except ExtractError as e:
|
except ExtractError as e:
|
||||||
self._handle_nonfatal_error(e)
|
self._handle_nonfatal_error(e)
|
||||||
|
|
||||||
|
def _log_no_directory_fixup(self, member, reason):
|
||||||
|
self._dbg(2, "tarfile: Not fixing up directory %r (%s)" %
|
||||||
|
(member.name, reason))
|
||||||
|
|
||||||
def extract(self, member, path="", set_attrs=True, *, numeric_owner=False,
|
def extract(self, member, path="", set_attrs=True, *, numeric_owner=False,
|
||||||
filter=None):
|
filter=None):
|
||||||
"""Extract a member from the archive to the current working directory,
|
"""Extract a member from the archive to the current working directory,
|
||||||
|
@ -2362,41 +2407,56 @@ def extract(self, member, path="", set_attrs=True, *, numeric_owner=False,
|
||||||
String names of common filters are accepted.
|
String names of common filters are accepted.
|
||||||
"""
|
"""
|
||||||
filter_function = self._get_filter_function(filter)
|
filter_function = self._get_filter_function(filter)
|
||||||
tarinfo = self._get_extract_tarinfo(member, filter_function, path)
|
tarinfo, unfiltered = self._get_extract_tarinfo(
|
||||||
|
member, filter_function, path)
|
||||||
if tarinfo is not None:
|
if tarinfo is not None:
|
||||||
self._extract_one(tarinfo, path, set_attrs, numeric_owner)
|
self._extract_one(tarinfo, path, set_attrs, numeric_owner)
|
||||||
|
|
||||||
def _get_extract_tarinfo(self, member, filter_function, path):
|
def _get_extract_tarinfo(self, member, filter_function, path):
|
||||||
"""Get filtered TarInfo (or None) from member, which might be a str"""
|
"""Get (filtered, unfiltered) TarInfos from *member*
|
||||||
if isinstance(member, str):
|
|
||||||
tarinfo = self.getmember(member)
|
|
||||||
else:
|
|
||||||
tarinfo = member
|
|
||||||
|
|
||||||
unfiltered = tarinfo
|
*member* might be a string.
|
||||||
|
|
||||||
|
Return (None, None) if not found.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(member, str):
|
||||||
|
unfiltered = self.getmember(member)
|
||||||
|
else:
|
||||||
|
unfiltered = member
|
||||||
|
|
||||||
|
filtered = None
|
||||||
try:
|
try:
|
||||||
tarinfo = filter_function(tarinfo, path)
|
filtered = filter_function(unfiltered, path)
|
||||||
except (OSError, UnicodeEncodeError, FilterError) as e:
|
except (OSError, UnicodeEncodeError, FilterError) as e:
|
||||||
self._handle_fatal_error(e)
|
self._handle_fatal_error(e)
|
||||||
except ExtractError as e:
|
except ExtractError as e:
|
||||||
self._handle_nonfatal_error(e)
|
self._handle_nonfatal_error(e)
|
||||||
if tarinfo is None:
|
if filtered is None:
|
||||||
self._dbg(2, "tarfile: Excluded %r" % unfiltered.name)
|
self._dbg(2, "tarfile: Excluded %r" % unfiltered.name)
|
||||||
return None
|
return None, None
|
||||||
# Prepare the link target for makelink().
|
|
||||||
if tarinfo.islnk():
|
|
||||||
tarinfo = copy.copy(tarinfo)
|
|
||||||
tarinfo._link_target = os.path.join(path, tarinfo.linkname)
|
|
||||||
return tarinfo
|
|
||||||
|
|
||||||
def _extract_one(self, tarinfo, path, set_attrs, numeric_owner):
|
# Prepare the link target for makelink().
|
||||||
"""Extract from filtered tarinfo to disk"""
|
if filtered.islnk():
|
||||||
|
filtered = copy.copy(filtered)
|
||||||
|
filtered._link_target = os.path.join(path, filtered.linkname)
|
||||||
|
return filtered, unfiltered
|
||||||
|
|
||||||
|
def _extract_one(self, tarinfo, path, set_attrs, numeric_owner,
|
||||||
|
filter_function=None):
|
||||||
|
"""Extract from filtered tarinfo to disk.
|
||||||
|
|
||||||
|
filter_function is only used when extracting a *different*
|
||||||
|
member (e.g. as fallback to creating a symlink)
|
||||||
|
"""
|
||||||
self._check("r")
|
self._check("r")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._extract_member(tarinfo, os.path.join(path, tarinfo.name),
|
self._extract_member(tarinfo, os.path.join(path, tarinfo.name),
|
||||||
set_attrs=set_attrs,
|
set_attrs=set_attrs,
|
||||||
numeric_owner=numeric_owner)
|
numeric_owner=numeric_owner,
|
||||||
|
filter_function=filter_function,
|
||||||
|
extraction_root=path)
|
||||||
except (OSError, UnicodeEncodeError) as e:
|
except (OSError, UnicodeEncodeError) as e:
|
||||||
self._handle_fatal_error(e)
|
self._handle_fatal_error(e)
|
||||||
except ExtractError as e:
|
except ExtractError as e:
|
||||||
|
@ -2454,9 +2514,13 @@ def extractfile(self, member):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _extract_member(self, tarinfo, targetpath, set_attrs=True,
|
def _extract_member(self, tarinfo, targetpath, set_attrs=True,
|
||||||
numeric_owner=False):
|
numeric_owner=False, *, filter_function=None,
|
||||||
"""Extract the TarInfo object tarinfo to a physical
|
extraction_root=None):
|
||||||
|
"""Extract the filtered TarInfo object tarinfo to a physical
|
||||||
file called targetpath.
|
file called targetpath.
|
||||||
|
|
||||||
|
filter_function is only used when extracting a *different*
|
||||||
|
member (e.g. as fallback to creating a symlink)
|
||||||
"""
|
"""
|
||||||
# Fetch the TarInfo object for the given name
|
# Fetch the TarInfo object for the given name
|
||||||
# and build the destination pathname, replacing
|
# and build the destination pathname, replacing
|
||||||
|
@ -2485,7 +2549,10 @@ def _extract_member(self, tarinfo, targetpath, set_attrs=True,
|
||||||
elif tarinfo.ischr() or tarinfo.isblk():
|
elif tarinfo.ischr() or tarinfo.isblk():
|
||||||
self.makedev(tarinfo, targetpath)
|
self.makedev(tarinfo, targetpath)
|
||||||
elif tarinfo.islnk() or tarinfo.issym():
|
elif tarinfo.islnk() or tarinfo.issym():
|
||||||
self.makelink(tarinfo, targetpath)
|
self.makelink_with_filter(
|
||||||
|
tarinfo, targetpath,
|
||||||
|
filter_function=filter_function,
|
||||||
|
extraction_root=extraction_root)
|
||||||
elif tarinfo.type not in SUPPORTED_TYPES:
|
elif tarinfo.type not in SUPPORTED_TYPES:
|
||||||
self.makeunknown(tarinfo, targetpath)
|
self.makeunknown(tarinfo, targetpath)
|
||||||
else:
|
else:
|
||||||
|
@ -2568,10 +2635,18 @@ def makedev(self, tarinfo, targetpath):
|
||||||
os.makedev(tarinfo.devmajor, tarinfo.devminor))
|
os.makedev(tarinfo.devmajor, tarinfo.devminor))
|
||||||
|
|
||||||
def makelink(self, tarinfo, targetpath):
|
def makelink(self, tarinfo, targetpath):
|
||||||
|
return self.makelink_with_filter(tarinfo, targetpath, None, None)
|
||||||
|
|
||||||
|
def makelink_with_filter(self, tarinfo, targetpath,
|
||||||
|
filter_function, extraction_root):
|
||||||
"""Make a (symbolic) link called targetpath. If it cannot be created
|
"""Make a (symbolic) link called targetpath. If it cannot be created
|
||||||
(platform limitation), we try to make a copy of the referenced file
|
(platform limitation), we try to make a copy of the referenced file
|
||||||
instead of a link.
|
instead of a link.
|
||||||
|
|
||||||
|
filter_function is only used when extracting a *different*
|
||||||
|
member (e.g. as fallback to creating a link).
|
||||||
"""
|
"""
|
||||||
|
keyerror_to_extracterror = False
|
||||||
try:
|
try:
|
||||||
# For systems that support symbolic and hard links.
|
# For systems that support symbolic and hard links.
|
||||||
if tarinfo.issym():
|
if tarinfo.issym():
|
||||||
|
@ -2579,18 +2654,38 @@ def makelink(self, tarinfo, targetpath):
|
||||||
# Avoid FileExistsError on following os.symlink.
|
# Avoid FileExistsError on following os.symlink.
|
||||||
os.unlink(targetpath)
|
os.unlink(targetpath)
|
||||||
os.symlink(tarinfo.linkname, targetpath)
|
os.symlink(tarinfo.linkname, targetpath)
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
if os.path.exists(tarinfo._link_target):
|
if os.path.exists(tarinfo._link_target):
|
||||||
os.link(tarinfo._link_target, targetpath)
|
os.link(tarinfo._link_target, targetpath)
|
||||||
else:
|
return
|
||||||
self._extract_member(self._find_link_target(tarinfo),
|
|
||||||
targetpath)
|
|
||||||
except symlink_exception:
|
except symlink_exception:
|
||||||
|
keyerror_to_extracterror = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
unfiltered = self._find_link_target(tarinfo)
|
||||||
|
except KeyError:
|
||||||
|
if keyerror_to_extracterror:
|
||||||
|
raise ExtractError(
|
||||||
|
"unable to resolve link inside archive") from None
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if filter_function is None:
|
||||||
|
filtered = unfiltered
|
||||||
|
else:
|
||||||
|
if extraction_root is None:
|
||||||
|
raise ExtractError(
|
||||||
|
"makelink_with_filter: if filter_function is not None, "
|
||||||
|
+ "extraction_root must also not be None")
|
||||||
try:
|
try:
|
||||||
self._extract_member(self._find_link_target(tarinfo),
|
filtered = filter_function(unfiltered, extraction_root)
|
||||||
targetpath)
|
except _FILTER_ERRORS as cause:
|
||||||
except KeyError:
|
raise LinkFallbackError(tarinfo, unfiltered.name) from cause
|
||||||
raise ExtractError("unable to resolve link inside archive") from None
|
if filtered is not None:
|
||||||
|
self._extract_member(filtered, targetpath,
|
||||||
|
filter_function=filter_function,
|
||||||
|
extraction_root=extraction_root)
|
||||||
|
|
||||||
def chown(self, tarinfo, targetpath, numeric_owner):
|
def chown(self, tarinfo, targetpath, numeric_owner):
|
||||||
"""Set owner of targetpath according to tarinfo. If numeric_owner
|
"""Set owner of targetpath according to tarinfo. If numeric_owner
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
import warnings
|
||||||
|
from ntpath import ALLOW_MISSING
|
||||||
from test.support import cpython_only, os_helper
|
from test.support import cpython_only, os_helper
|
||||||
from test.support import TestFailed, is_emscripten
|
from test.support import TestFailed, is_emscripten
|
||||||
from test.support.os_helper import FakePath
|
from test.support.os_helper import FakePath
|
||||||
|
@ -77,6 +78,27 @@ def tester(fn, wantResult):
|
||||||
%(str(fn), str(wantResult), repr(gotResult)))
|
%(str(fn), str(wantResult), repr(gotResult)))
|
||||||
|
|
||||||
|
|
||||||
|
def _parameterize(*parameters):
|
||||||
|
"""Simplistic decorator to parametrize a test
|
||||||
|
|
||||||
|
Runs the decorated test multiple times in subTest, with a value from
|
||||||
|
'parameters' passed as an extra positional argument.
|
||||||
|
Calls doCleanups() after each run.
|
||||||
|
|
||||||
|
Not for general use. Intended to avoid indenting for easier backports.
|
||||||
|
|
||||||
|
See https://discuss.python.org/t/91827 for discussing generalizations.
|
||||||
|
"""
|
||||||
|
def _parametrize_decorator(func):
|
||||||
|
def _parameterized(self, *args, **kwargs):
|
||||||
|
for parameter in parameters:
|
||||||
|
with self.subTest(parameter):
|
||||||
|
func(self, *args, parameter, **kwargs)
|
||||||
|
self.doCleanups()
|
||||||
|
return _parameterized
|
||||||
|
return _parametrize_decorator
|
||||||
|
|
||||||
|
|
||||||
class NtpathTestCase(unittest.TestCase):
|
class NtpathTestCase(unittest.TestCase):
|
||||||
def assertPathEqual(self, path1, path2):
|
def assertPathEqual(self, path1, path2):
|
||||||
if path1 == path2 or _norm(path1) == _norm(path2):
|
if path1 == path2 or _norm(path1) == _norm(path2):
|
||||||
|
@ -475,6 +497,27 @@ def test_realpath_curdir(self):
|
||||||
tester("ntpath.realpath('.\\.')", expected)
|
tester("ntpath.realpath('.\\.')", expected)
|
||||||
tester("ntpath.realpath('\\'.join(['.'] * 100))", expected)
|
tester("ntpath.realpath('\\'.join(['.'] * 100))", expected)
|
||||||
|
|
||||||
|
def test_realpath_curdir_strict(self):
|
||||||
|
expected = ntpath.normpath(os.getcwd())
|
||||||
|
tester("ntpath.realpath('.', strict=True)", expected)
|
||||||
|
tester("ntpath.realpath('./.', strict=True)", expected)
|
||||||
|
tester("ntpath.realpath('/'.join(['.'] * 100), strict=True)", expected)
|
||||||
|
tester("ntpath.realpath('.\\.', strict=True)", expected)
|
||||||
|
tester("ntpath.realpath('\\'.join(['.'] * 100), strict=True)", expected)
|
||||||
|
|
||||||
|
def test_realpath_curdir_missing_ok(self):
|
||||||
|
expected = ntpath.normpath(os.getcwd())
|
||||||
|
tester("ntpath.realpath('.', strict=ALLOW_MISSING)",
|
||||||
|
expected)
|
||||||
|
tester("ntpath.realpath('./.', strict=ALLOW_MISSING)",
|
||||||
|
expected)
|
||||||
|
tester("ntpath.realpath('/'.join(['.'] * 100), strict=ALLOW_MISSING)",
|
||||||
|
expected)
|
||||||
|
tester("ntpath.realpath('.\\.', strict=ALLOW_MISSING)",
|
||||||
|
expected)
|
||||||
|
tester("ntpath.realpath('\\'.join(['.'] * 100), strict=ALLOW_MISSING)",
|
||||||
|
expected)
|
||||||
|
|
||||||
def test_realpath_pardir(self):
|
def test_realpath_pardir(self):
|
||||||
expected = ntpath.normpath(os.getcwd())
|
expected = ntpath.normpath(os.getcwd())
|
||||||
tester("ntpath.realpath('..')", ntpath.dirname(expected))
|
tester("ntpath.realpath('..')", ntpath.dirname(expected))
|
||||||
|
@ -487,24 +530,59 @@ def test_realpath_pardir(self):
|
||||||
tester("ntpath.realpath('\\'.join(['..'] * 50))",
|
tester("ntpath.realpath('\\'.join(['..'] * 50))",
|
||||||
ntpath.splitdrive(expected)[0] + '\\')
|
ntpath.splitdrive(expected)[0] + '\\')
|
||||||
|
|
||||||
|
def test_realpath_pardir_strict(self):
|
||||||
|
expected = ntpath.normpath(os.getcwd())
|
||||||
|
tester("ntpath.realpath('..', strict=True)", ntpath.dirname(expected))
|
||||||
|
tester("ntpath.realpath('../..', strict=True)",
|
||||||
|
ntpath.dirname(ntpath.dirname(expected)))
|
||||||
|
tester("ntpath.realpath('/'.join(['..'] * 50), strict=True)",
|
||||||
|
ntpath.splitdrive(expected)[0] + '\\')
|
||||||
|
tester("ntpath.realpath('..\\..', strict=True)",
|
||||||
|
ntpath.dirname(ntpath.dirname(expected)))
|
||||||
|
tester("ntpath.realpath('\\'.join(['..'] * 50), strict=True)",
|
||||||
|
ntpath.splitdrive(expected)[0] + '\\')
|
||||||
|
|
||||||
|
def test_realpath_pardir_missing_ok(self):
|
||||||
|
expected = ntpath.normpath(os.getcwd())
|
||||||
|
tester("ntpath.realpath('..', strict=ALLOW_MISSING)",
|
||||||
|
ntpath.dirname(expected))
|
||||||
|
tester("ntpath.realpath('../..', strict=ALLOW_MISSING)",
|
||||||
|
ntpath.dirname(ntpath.dirname(expected)))
|
||||||
|
tester("ntpath.realpath('/'.join(['..'] * 50), strict=ALLOW_MISSING)",
|
||||||
|
ntpath.splitdrive(expected)[0] + '\\')
|
||||||
|
tester("ntpath.realpath('..\\..', strict=ALLOW_MISSING)",
|
||||||
|
ntpath.dirname(ntpath.dirname(expected)))
|
||||||
|
tester("ntpath.realpath('\\'.join(['..'] * 50), strict=ALLOW_MISSING)",
|
||||||
|
ntpath.splitdrive(expected)[0] + '\\')
|
||||||
|
|
||||||
@os_helper.skip_unless_symlink
|
@os_helper.skip_unless_symlink
|
||||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||||
def test_realpath_basic(self):
|
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
|
||||||
|
def test_realpath_basic(self, kwargs):
|
||||||
ABSTFN = ntpath.abspath(os_helper.TESTFN)
|
ABSTFN = ntpath.abspath(os_helper.TESTFN)
|
||||||
open(ABSTFN, "wb").close()
|
open(ABSTFN, "wb").close()
|
||||||
self.addCleanup(os_helper.unlink, ABSTFN)
|
self.addCleanup(os_helper.unlink, ABSTFN)
|
||||||
self.addCleanup(os_helper.unlink, ABSTFN + "1")
|
self.addCleanup(os_helper.unlink, ABSTFN + "1")
|
||||||
|
|
||||||
os.symlink(ABSTFN, ABSTFN + "1")
|
os.symlink(ABSTFN, ABSTFN + "1")
|
||||||
self.assertPathEqual(ntpath.realpath(ABSTFN + "1"), ABSTFN)
|
self.assertPathEqual(ntpath.realpath(ABSTFN + "1", **kwargs), ABSTFN)
|
||||||
self.assertPathEqual(ntpath.realpath(os.fsencode(ABSTFN + "1")),
|
self.assertPathEqual(ntpath.realpath(os.fsencode(ABSTFN + "1"), **kwargs),
|
||||||
os.fsencode(ABSTFN))
|
os.fsencode(ABSTFN))
|
||||||
|
|
||||||
# gh-88013: call ntpath.realpath with binary drive name may raise a
|
# gh-88013: call ntpath.realpath with binary drive name may raise a
|
||||||
# TypeError. The drive should not exist to reproduce the bug.
|
# TypeError. The drive should not exist to reproduce the bug.
|
||||||
drives = {f"{c}:\\" for c in string.ascii_uppercase} - set(os.listdrives())
|
drives = {f"{c}:\\" for c in string.ascii_uppercase} - set(os.listdrives())
|
||||||
d = drives.pop().encode()
|
d = drives.pop().encode()
|
||||||
self.assertEqual(ntpath.realpath(d), d)
|
self.assertEqual(ntpath.realpath(d, strict=False), d)
|
||||||
|
|
||||||
|
# gh-106242: Embedded nulls and non-strict fallback to abspath
|
||||||
|
if kwargs:
|
||||||
|
with self.assertRaises(OSError):
|
||||||
|
ntpath.realpath(os_helper.TESTFN + "\0spam",
|
||||||
|
**kwargs)
|
||||||
|
else:
|
||||||
|
self.assertEqual(ABSTFN + "\0spam",
|
||||||
|
ntpath.realpath(os_helper.TESTFN + "\0spam", **kwargs))
|
||||||
|
|
||||||
@os_helper.skip_unless_symlink
|
@os_helper.skip_unless_symlink
|
||||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||||
|
@ -527,51 +605,66 @@ def test_realpath_invalid_paths(self):
|
||||||
self.assertEqual(realpath(path, strict=False), path)
|
self.assertEqual(realpath(path, strict=False), path)
|
||||||
# gh-106242: Embedded nulls should raise OSError (not ValueError)
|
# gh-106242: Embedded nulls should raise OSError (not ValueError)
|
||||||
self.assertRaises(OSError, realpath, path, strict=True)
|
self.assertRaises(OSError, realpath, path, strict=True)
|
||||||
|
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
|
||||||
path = ABSTFNb + b'\x00'
|
path = ABSTFNb + b'\x00'
|
||||||
self.assertEqual(realpath(path, strict=False), path)
|
self.assertEqual(realpath(path, strict=False), path)
|
||||||
self.assertRaises(OSError, realpath, path, strict=True)
|
self.assertRaises(OSError, realpath, path, strict=True)
|
||||||
|
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
|
||||||
path = ABSTFN + '\\nonexistent\\x\x00'
|
path = ABSTFN + '\\nonexistent\\x\x00'
|
||||||
self.assertEqual(realpath(path, strict=False), path)
|
self.assertEqual(realpath(path, strict=False), path)
|
||||||
self.assertRaises(OSError, realpath, path, strict=True)
|
self.assertRaises(OSError, realpath, path, strict=True)
|
||||||
|
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
|
||||||
path = ABSTFNb + b'\\nonexistent\\x\x00'
|
path = ABSTFNb + b'\\nonexistent\\x\x00'
|
||||||
self.assertEqual(realpath(path, strict=False), path)
|
self.assertEqual(realpath(path, strict=False), path)
|
||||||
self.assertRaises(OSError, realpath, path, strict=True)
|
self.assertRaises(OSError, realpath, path, strict=True)
|
||||||
|
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
|
||||||
path = ABSTFN + '\x00\\..'
|
path = ABSTFN + '\x00\\..'
|
||||||
self.assertEqual(realpath(path, strict=False), os.getcwd())
|
self.assertEqual(realpath(path, strict=False), os.getcwd())
|
||||||
self.assertEqual(realpath(path, strict=True), os.getcwd())
|
self.assertEqual(realpath(path, strict=True), os.getcwd())
|
||||||
|
self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwd())
|
||||||
path = ABSTFNb + b'\x00\\..'
|
path = ABSTFNb + b'\x00\\..'
|
||||||
self.assertEqual(realpath(path, strict=False), os.getcwdb())
|
self.assertEqual(realpath(path, strict=False), os.getcwdb())
|
||||||
self.assertEqual(realpath(path, strict=True), os.getcwdb())
|
self.assertEqual(realpath(path, strict=True), os.getcwdb())
|
||||||
|
self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwdb())
|
||||||
path = ABSTFN + '\\nonexistent\\x\x00\\..'
|
path = ABSTFN + '\\nonexistent\\x\x00\\..'
|
||||||
self.assertEqual(realpath(path, strict=False), ABSTFN + '\\nonexistent')
|
self.assertEqual(realpath(path, strict=False), ABSTFN + '\\nonexistent')
|
||||||
self.assertRaises(OSError, realpath, path, strict=True)
|
self.assertRaises(OSError, realpath, path, strict=True)
|
||||||
|
self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFN + '\\nonexistent')
|
||||||
path = ABSTFNb + b'\\nonexistent\\x\x00\\..'
|
path = ABSTFNb + b'\\nonexistent\\x\x00\\..'
|
||||||
self.assertEqual(realpath(path, strict=False), ABSTFNb + b'\\nonexistent')
|
self.assertEqual(realpath(path, strict=False), ABSTFNb + b'\\nonexistent')
|
||||||
self.assertRaises(OSError, realpath, path, strict=True)
|
self.assertRaises(OSError, realpath, path, strict=True)
|
||||||
|
self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFNb + b'\\nonexistent')
|
||||||
|
|
||||||
|
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||||
|
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
|
||||||
|
def test_realpath_invalid_unicode_paths(self, kwargs):
|
||||||
|
realpath = ntpath.realpath
|
||||||
|
ABSTFN = ntpath.abspath(os_helper.TESTFN)
|
||||||
|
ABSTFNb = os.fsencode(ABSTFN)
|
||||||
path = ABSTFNb + b'\xff'
|
path = ABSTFNb + b'\xff'
|
||||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=False)
|
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
|
||||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=True)
|
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
|
||||||
path = ABSTFNb + b'\\nonexistent\\\xff'
|
path = ABSTFNb + b'\\nonexistent\\\xff'
|
||||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=False)
|
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
|
||||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=True)
|
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
|
||||||
path = ABSTFNb + b'\xff\\..'
|
path = ABSTFNb + b'\xff\\..'
|
||||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=False)
|
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
|
||||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=True)
|
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
|
||||||
path = ABSTFNb + b'\\nonexistent\\\xff\\..'
|
path = ABSTFNb + b'\\nonexistent\\\xff\\..'
|
||||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=False)
|
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
|
||||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=True)
|
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
|
||||||
|
|
||||||
@os_helper.skip_unless_symlink
|
@os_helper.skip_unless_symlink
|
||||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||||
def test_realpath_relative(self):
|
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
|
||||||
|
def test_realpath_relative(self, kwargs):
|
||||||
ABSTFN = ntpath.abspath(os_helper.TESTFN)
|
ABSTFN = ntpath.abspath(os_helper.TESTFN)
|
||||||
open(ABSTFN, "wb").close()
|
open(ABSTFN, "wb").close()
|
||||||
self.addCleanup(os_helper.unlink, ABSTFN)
|
self.addCleanup(os_helper.unlink, ABSTFN)
|
||||||
self.addCleanup(os_helper.unlink, ABSTFN + "1")
|
self.addCleanup(os_helper.unlink, ABSTFN + "1")
|
||||||
|
|
||||||
os.symlink(ABSTFN, ntpath.relpath(ABSTFN + "1"))
|
os.symlink(ABSTFN, ntpath.relpath(ABSTFN + "1"))
|
||||||
self.assertPathEqual(ntpath.realpath(ABSTFN + "1"), ABSTFN)
|
self.assertPathEqual(ntpath.realpath(ABSTFN + "1", **kwargs), ABSTFN)
|
||||||
|
|
||||||
@os_helper.skip_unless_symlink
|
@os_helper.skip_unless_symlink
|
||||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||||
|
@ -723,7 +816,62 @@ def test_realpath_symlink_loops_strict(self):
|
||||||
|
|
||||||
@os_helper.skip_unless_symlink
|
@os_helper.skip_unless_symlink
|
||||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||||
def test_realpath_symlink_prefix(self):
|
def test_realpath_symlink_loops_raise(self):
|
||||||
|
# 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")
|
||||||
|
self.addCleanup(os_helper.unlink, ABSTFN + "2")
|
||||||
|
self.addCleanup(os_helper.unlink, ABSTFN + "y")
|
||||||
|
self.addCleanup(os_helper.unlink, ABSTFN + "c")
|
||||||
|
self.addCleanup(os_helper.unlink, ABSTFN + "a")
|
||||||
|
self.addCleanup(os_helper.unlink, ABSTFN + "x")
|
||||||
|
|
||||||
|
os.symlink(ABSTFN, ABSTFN)
|
||||||
|
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)
|
||||||
|
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "2",
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
|
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\\x",
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
|
|
||||||
|
# Windows eliminates '..' components before resolving links;
|
||||||
|
# realpath is not expected to raise if this removes the loop.
|
||||||
|
self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\.."),
|
||||||
|
ntpath.dirname(ABSTFN))
|
||||||
|
self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\..\\x"),
|
||||||
|
ntpath.dirname(ABSTFN) + "\\x")
|
||||||
|
|
||||||
|
os.symlink(ABSTFN + "x", ABSTFN + "y")
|
||||||
|
self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\..\\"
|
||||||
|
+ ntpath.basename(ABSTFN) + "y"),
|
||||||
|
ABSTFN + "x")
|
||||||
|
self.assertRaises(
|
||||||
|
OSError, ntpath.realpath,
|
||||||
|
ABSTFN + "1\\..\\" + ntpath.basename(ABSTFN) + "1",
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
|
|
||||||
|
os.symlink(ntpath.basename(ABSTFN) + "a\\b", ABSTFN + "a")
|
||||||
|
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "a",
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Test using relative path as well.
|
||||||
|
self.assertRaises(OSError, ntpath.realpath, ntpath.basename(ABSTFN),
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
|
|
||||||
|
@os_helper.skip_unless_symlink
|
||||||
|
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||||
|
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
|
||||||
|
def test_realpath_symlink_prefix(self, kwargs):
|
||||||
ABSTFN = ntpath.abspath(os_helper.TESTFN)
|
ABSTFN = ntpath.abspath(os_helper.TESTFN)
|
||||||
self.addCleanup(os_helper.unlink, ABSTFN + "3")
|
self.addCleanup(os_helper.unlink, ABSTFN + "3")
|
||||||
self.addCleanup(os_helper.unlink, "\\\\?\\" + ABSTFN + "3.")
|
self.addCleanup(os_helper.unlink, "\\\\?\\" + ABSTFN + "3.")
|
||||||
|
@ -738,9 +886,9 @@ def test_realpath_symlink_prefix(self):
|
||||||
f.write(b'1')
|
f.write(b'1')
|
||||||
os.symlink("\\\\?\\" + ABSTFN + "3.", ABSTFN + "3.link")
|
os.symlink("\\\\?\\" + ABSTFN + "3.", ABSTFN + "3.link")
|
||||||
|
|
||||||
self.assertPathEqual(ntpath.realpath(ABSTFN + "3link"),
|
self.assertPathEqual(ntpath.realpath(ABSTFN + "3link", **kwargs),
|
||||||
ABSTFN + "3")
|
ABSTFN + "3")
|
||||||
self.assertPathEqual(ntpath.realpath(ABSTFN + "3.link"),
|
self.assertPathEqual(ntpath.realpath(ABSTFN + "3.link", **kwargs),
|
||||||
"\\\\?\\" + ABSTFN + "3.")
|
"\\\\?\\" + ABSTFN + "3.")
|
||||||
|
|
||||||
# Resolved paths should be usable to open target files
|
# Resolved paths should be usable to open target files
|
||||||
|
@ -750,14 +898,17 @@ def test_realpath_symlink_prefix(self):
|
||||||
self.assertEqual(f.read(), b'1')
|
self.assertEqual(f.read(), b'1')
|
||||||
|
|
||||||
# When the prefix is included, it is not stripped
|
# When the prefix is included, it is not stripped
|
||||||
self.assertPathEqual(ntpath.realpath("\\\\?\\" + ABSTFN + "3link"),
|
self.assertPathEqual(ntpath.realpath("\\\\?\\" + ABSTFN + "3link", **kwargs),
|
||||||
"\\\\?\\" + ABSTFN + "3")
|
"\\\\?\\" + ABSTFN + "3")
|
||||||
self.assertPathEqual(ntpath.realpath("\\\\?\\" + ABSTFN + "3.link"),
|
self.assertPathEqual(ntpath.realpath("\\\\?\\" + ABSTFN + "3.link", **kwargs),
|
||||||
"\\\\?\\" + ABSTFN + "3.")
|
"\\\\?\\" + ABSTFN + "3.")
|
||||||
|
|
||||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||||
def test_realpath_nul(self):
|
def test_realpath_nul(self):
|
||||||
tester("ntpath.realpath('NUL')", r'\\.\NUL')
|
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')
|
||||||
|
|
||||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||||
@unittest.skipUnless(HAVE_GETSHORTPATHNAME, 'need _getshortpathname')
|
@unittest.skipUnless(HAVE_GETSHORTPATHNAME, 'need _getshortpathname')
|
||||||
|
@ -781,12 +932,20 @@ def test_realpath_cwd(self):
|
||||||
|
|
||||||
self.assertPathEqual(test_file_long, ntpath.realpath(test_file_short))
|
self.assertPathEqual(test_file_long, ntpath.realpath(test_file_short))
|
||||||
|
|
||||||
with os_helper.change_cwd(test_dir_long):
|
for kwargs in {}, {'strict': True}, {'strict': ALLOW_MISSING}:
|
||||||
self.assertPathEqual(test_file_long, ntpath.realpath("file.txt"))
|
with self.subTest(**kwargs):
|
||||||
with os_helper.change_cwd(test_dir_long.lower()):
|
with os_helper.change_cwd(test_dir_long):
|
||||||
self.assertPathEqual(test_file_long, ntpath.realpath("file.txt"))
|
self.assertPathEqual(
|
||||||
with os_helper.change_cwd(test_dir_short):
|
test_file_long,
|
||||||
self.assertPathEqual(test_file_long, ntpath.realpath("file.txt"))
|
ntpath.realpath("file.txt", **kwargs))
|
||||||
|
with os_helper.change_cwd(test_dir_long.lower()):
|
||||||
|
self.assertPathEqual(
|
||||||
|
test_file_long,
|
||||||
|
ntpath.realpath("file.txt", **kwargs))
|
||||||
|
with os_helper.change_cwd(test_dir_short):
|
||||||
|
self.assertPathEqual(
|
||||||
|
test_file_long,
|
||||||
|
ntpath.realpath("file.txt", **kwargs))
|
||||||
|
|
||||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||||
def test_realpath_permission(self):
|
def test_realpath_permission(self):
|
||||||
|
@ -807,12 +966,15 @@ def test_realpath_permission(self):
|
||||||
# Automatic generation of short names may be disabled on
|
# Automatic generation of short names may be disabled on
|
||||||
# NTFS volumes for the sake of performance.
|
# NTFS volumes for the sake of performance.
|
||||||
# They're not supported at all on ReFS and exFAT.
|
# They're not supported at all on ReFS and exFAT.
|
||||||
subprocess.run(
|
p = subprocess.run(
|
||||||
# Try to set the short name manually.
|
# Try to set the short name manually.
|
||||||
['fsutil.exe', 'file', 'setShortName', test_file, 'LONGFI~1.TXT'],
|
['fsutil.exe', 'file', 'setShortName', test_file, 'LONGFI~1.TXT'],
|
||||||
creationflags=subprocess.DETACHED_PROCESS
|
creationflags=subprocess.DETACHED_PROCESS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if p.returncode:
|
||||||
|
raise unittest.SkipTest('failed to set short name')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.assertPathEqual(test_file, ntpath.realpath(test_file_short))
|
self.assertPathEqual(test_file, ntpath.realpath(test_file_short))
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
from posixpath import realpath, abspath, dirname, basename
|
from functools import partial
|
||||||
|
from posixpath import realpath, abspath, dirname, basename, ALLOW_MISSING
|
||||||
from test import support
|
from test import support
|
||||||
from test import test_genericpath
|
from test import test_genericpath
|
||||||
from test.support import import_helper
|
from test.support import import_helper
|
||||||
|
@ -33,6 +34,27 @@ def skip_if_ABSTFN_contains_backslash(test):
|
||||||
msg = "ABSTFN is not a posix path - tests fail"
|
msg = "ABSTFN is not a posix path - tests fail"
|
||||||
return [test, unittest.skip(msg)(test)][found_backslash]
|
return [test, unittest.skip(msg)(test)][found_backslash]
|
||||||
|
|
||||||
|
|
||||||
|
def _parameterize(*parameters):
|
||||||
|
"""Simplistic decorator to parametrize a test
|
||||||
|
|
||||||
|
Runs the decorated test multiple times in subTest, with a value from
|
||||||
|
'parameters' passed as an extra positional argument.
|
||||||
|
Does *not* call doCleanups() after each run.
|
||||||
|
|
||||||
|
Not for general use. Intended to avoid indenting for easier backports.
|
||||||
|
|
||||||
|
See https://discuss.python.org/t/91827 for discussing generalizations.
|
||||||
|
"""
|
||||||
|
def _parametrize_decorator(func):
|
||||||
|
def _parameterized(self, *args, **kwargs):
|
||||||
|
for parameter in parameters:
|
||||||
|
with self.subTest(parameter):
|
||||||
|
func(self, *args, parameter, **kwargs)
|
||||||
|
return _parameterized
|
||||||
|
return _parametrize_decorator
|
||||||
|
|
||||||
|
|
||||||
class PosixPathTest(unittest.TestCase):
|
class PosixPathTest(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -442,32 +464,35 @@ def test_normpath(self):
|
||||||
self.assertEqual(result, expected)
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
@skip_if_ABSTFN_contains_backslash
|
@skip_if_ABSTFN_contains_backslash
|
||||||
def test_realpath_curdir(self):
|
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
|
||||||
self.assertEqual(realpath('.'), os.getcwd())
|
def test_realpath_curdir(self, kwargs):
|
||||||
self.assertEqual(realpath('./.'), os.getcwd())
|
self.assertEqual(realpath('.', **kwargs), os.getcwd())
|
||||||
self.assertEqual(realpath('/'.join(['.'] * 100)), os.getcwd())
|
self.assertEqual(realpath('./.', **kwargs), os.getcwd())
|
||||||
|
self.assertEqual(realpath('/'.join(['.'] * 100), **kwargs), os.getcwd())
|
||||||
|
|
||||||
self.assertEqual(realpath(b'.'), os.getcwdb())
|
self.assertEqual(realpath(b'.', **kwargs), os.getcwdb())
|
||||||
self.assertEqual(realpath(b'./.'), os.getcwdb())
|
self.assertEqual(realpath(b'./.', **kwargs), os.getcwdb())
|
||||||
self.assertEqual(realpath(b'/'.join([b'.'] * 100)), os.getcwdb())
|
self.assertEqual(realpath(b'/'.join([b'.'] * 100), **kwargs), os.getcwdb())
|
||||||
|
|
||||||
@skip_if_ABSTFN_contains_backslash
|
@skip_if_ABSTFN_contains_backslash
|
||||||
def test_realpath_pardir(self):
|
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
|
||||||
self.assertEqual(realpath('..'), dirname(os.getcwd()))
|
def test_realpath_pardir(self, kwargs):
|
||||||
self.assertEqual(realpath('../..'), dirname(dirname(os.getcwd())))
|
self.assertEqual(realpath('..', **kwargs), dirname(os.getcwd()))
|
||||||
self.assertEqual(realpath('/'.join(['..'] * 100)), '/')
|
self.assertEqual(realpath('../..', **kwargs), dirname(dirname(os.getcwd())))
|
||||||
|
self.assertEqual(realpath('/'.join(['..'] * 100), **kwargs), '/')
|
||||||
|
|
||||||
self.assertEqual(realpath(b'..'), dirname(os.getcwdb()))
|
self.assertEqual(realpath(b'..', **kwargs), dirname(os.getcwdb()))
|
||||||
self.assertEqual(realpath(b'../..'), dirname(dirname(os.getcwdb())))
|
self.assertEqual(realpath(b'../..', **kwargs), dirname(dirname(os.getcwdb())))
|
||||||
self.assertEqual(realpath(b'/'.join([b'..'] * 100)), b'/')
|
self.assertEqual(realpath(b'/'.join([b'..'] * 100), **kwargs), b'/')
|
||||||
|
|
||||||
@os_helper.skip_unless_symlink
|
@os_helper.skip_unless_symlink
|
||||||
@skip_if_ABSTFN_contains_backslash
|
@skip_if_ABSTFN_contains_backslash
|
||||||
def test_realpath_basic(self):
|
@_parameterize({}, {'strict': ALLOW_MISSING})
|
||||||
|
def test_realpath_basic(self, kwargs):
|
||||||
# Basic operation.
|
# Basic operation.
|
||||||
try:
|
try:
|
||||||
os.symlink(ABSTFN+"1", ABSTFN)
|
os.symlink(ABSTFN+"1", ABSTFN)
|
||||||
self.assertEqual(realpath(ABSTFN), ABSTFN+"1")
|
self.assertEqual(realpath(ABSTFN, **kwargs), ABSTFN+"1")
|
||||||
finally:
|
finally:
|
||||||
os_helper.unlink(ABSTFN)
|
os_helper.unlink(ABSTFN)
|
||||||
|
|
||||||
|
@ -487,90 +512,115 @@ def test_realpath_invalid_paths(self):
|
||||||
path = '/\x00'
|
path = '/\x00'
|
||||||
self.assertRaises(ValueError, realpath, path, strict=False)
|
self.assertRaises(ValueError, realpath, path, strict=False)
|
||||||
self.assertRaises(ValueError, realpath, path, strict=True)
|
self.assertRaises(ValueError, realpath, path, strict=True)
|
||||||
|
self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
|
||||||
path = b'/\x00'
|
path = b'/\x00'
|
||||||
self.assertRaises(ValueError, realpath, path, strict=False)
|
self.assertRaises(ValueError, realpath, path, strict=False)
|
||||||
self.assertRaises(ValueError, realpath, path, strict=True)
|
self.assertRaises(ValueError, realpath, path, strict=True)
|
||||||
|
self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
|
||||||
path = '/nonexistent/x\x00'
|
path = '/nonexistent/x\x00'
|
||||||
self.assertRaises(ValueError, realpath, path, strict=False)
|
self.assertRaises(ValueError, realpath, path, strict=False)
|
||||||
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
||||||
|
self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
|
||||||
path = b'/nonexistent/x\x00'
|
path = b'/nonexistent/x\x00'
|
||||||
self.assertRaises(ValueError, realpath, path, strict=False)
|
self.assertRaises(ValueError, realpath, path, strict=False)
|
||||||
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
||||||
|
self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
|
||||||
path = '/\x00/..'
|
path = '/\x00/..'
|
||||||
self.assertRaises(ValueError, realpath, path, strict=False)
|
self.assertRaises(ValueError, realpath, path, strict=False)
|
||||||
self.assertRaises(ValueError, realpath, path, strict=True)
|
self.assertRaises(ValueError, realpath, path, strict=True)
|
||||||
|
self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
|
||||||
path = b'/\x00/..'
|
path = b'/\x00/..'
|
||||||
self.assertRaises(ValueError, realpath, path, strict=False)
|
self.assertRaises(ValueError, realpath, path, strict=False)
|
||||||
self.assertRaises(ValueError, realpath, path, strict=True)
|
self.assertRaises(ValueError, realpath, path, strict=True)
|
||||||
|
self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
|
||||||
|
|
||||||
path = '/nonexistent/x\x00/..'
|
path = '/nonexistent/x\x00/..'
|
||||||
self.assertRaises(ValueError, realpath, path, strict=False)
|
self.assertRaises(ValueError, realpath, path, strict=False)
|
||||||
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
||||||
|
self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
|
||||||
path = b'/nonexistent/x\x00/..'
|
path = b'/nonexistent/x\x00/..'
|
||||||
self.assertRaises(ValueError, realpath, path, strict=False)
|
self.assertRaises(ValueError, realpath, path, strict=False)
|
||||||
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
||||||
|
self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
|
||||||
|
|
||||||
path = '/\udfff'
|
path = '/\udfff'
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
self.assertEqual(realpath(path, strict=False), path)
|
self.assertEqual(realpath(path, strict=False), path)
|
||||||
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
||||||
|
self.assertEqual(realpath(path, strict=ALLOW_MISSING), path)
|
||||||
else:
|
else:
|
||||||
self.assertRaises(UnicodeEncodeError, realpath, path, strict=False)
|
self.assertRaises(UnicodeEncodeError, realpath, path, strict=False)
|
||||||
self.assertRaises(UnicodeEncodeError, realpath, path, strict=True)
|
self.assertRaises(UnicodeEncodeError, realpath, path, strict=True)
|
||||||
|
self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING)
|
||||||
path = '/nonexistent/\udfff'
|
path = '/nonexistent/\udfff'
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
self.assertEqual(realpath(path, strict=False), path)
|
self.assertEqual(realpath(path, strict=False), path)
|
||||||
|
self.assertEqual(realpath(path, strict=ALLOW_MISSING), path)
|
||||||
else:
|
else:
|
||||||
self.assertRaises(UnicodeEncodeError, realpath, path, strict=False)
|
self.assertRaises(UnicodeEncodeError, realpath, path, strict=False)
|
||||||
|
self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING)
|
||||||
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
||||||
path = '/\udfff/..'
|
path = '/\udfff/..'
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
self.assertEqual(realpath(path, strict=False), '/')
|
self.assertEqual(realpath(path, strict=False), '/')
|
||||||
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
||||||
|
self.assertEqual(realpath(path, strict=ALLOW_MISSING), '/')
|
||||||
else:
|
else:
|
||||||
self.assertRaises(UnicodeEncodeError, realpath, path, strict=False)
|
self.assertRaises(UnicodeEncodeError, realpath, path, strict=False)
|
||||||
self.assertRaises(UnicodeEncodeError, realpath, path, strict=True)
|
self.assertRaises(UnicodeEncodeError, realpath, path, strict=True)
|
||||||
|
self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING)
|
||||||
path = '/nonexistent/\udfff/..'
|
path = '/nonexistent/\udfff/..'
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
self.assertEqual(realpath(path, strict=False), '/nonexistent')
|
self.assertEqual(realpath(path, strict=False), '/nonexistent')
|
||||||
|
self.assertEqual(realpath(path, strict=ALLOW_MISSING), '/nonexistent')
|
||||||
else:
|
else:
|
||||||
self.assertRaises(UnicodeEncodeError, realpath, path, strict=False)
|
self.assertRaises(UnicodeEncodeError, realpath, path, strict=False)
|
||||||
|
self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING)
|
||||||
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
||||||
|
|
||||||
path = b'/\xff'
|
path = b'/\xff'
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=False)
|
self.assertRaises(UnicodeDecodeError, realpath, path, strict=False)
|
||||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=True)
|
self.assertRaises(UnicodeDecodeError, realpath, path, strict=True)
|
||||||
|
self.assertRaises(UnicodeDecodeError, realpath, path, strict=ALLOW_MISSING)
|
||||||
else:
|
else:
|
||||||
self.assertEqual(realpath(path, strict=False), path)
|
self.assertEqual(realpath(path, strict=False), path)
|
||||||
if support.is_wasi:
|
if support.is_wasi:
|
||||||
self.assertRaises(OSError, realpath, path, strict=True)
|
self.assertRaises(OSError, realpath, path, strict=True)
|
||||||
|
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
|
||||||
else:
|
else:
|
||||||
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
||||||
|
self.assertEqual(realpath(path, strict=ALLOW_MISSING), path)
|
||||||
path = b'/nonexistent/\xff'
|
path = b'/nonexistent/\xff'
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=False)
|
self.assertRaises(UnicodeDecodeError, realpath, path, strict=False)
|
||||||
|
self.assertRaises(UnicodeDecodeError, realpath, path, strict=ALLOW_MISSING)
|
||||||
else:
|
else:
|
||||||
self.assertEqual(realpath(path, strict=False), path)
|
self.assertEqual(realpath(path, strict=False), path)
|
||||||
if support.is_wasi:
|
if support.is_wasi:
|
||||||
self.assertRaises(OSError, realpath, path, strict=True)
|
self.assertRaises(OSError, realpath, path, strict=True)
|
||||||
|
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
|
||||||
else:
|
else:
|
||||||
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
|
||||||
|
|
||||||
@os_helper.skip_unless_symlink
|
@os_helper.skip_unless_symlink
|
||||||
@skip_if_ABSTFN_contains_backslash
|
@skip_if_ABSTFN_contains_backslash
|
||||||
def test_realpath_relative(self):
|
@_parameterize({}, {'strict': ALLOW_MISSING})
|
||||||
|
def test_realpath_relative(self, kwargs):
|
||||||
try:
|
try:
|
||||||
os.symlink(posixpath.relpath(ABSTFN+"1"), ABSTFN)
|
os.symlink(posixpath.relpath(ABSTFN+"1"), ABSTFN)
|
||||||
self.assertEqual(realpath(ABSTFN), ABSTFN+"1")
|
self.assertEqual(realpath(ABSTFN, **kwargs), ABSTFN+"1")
|
||||||
finally:
|
finally:
|
||||||
os_helper.unlink(ABSTFN)
|
os_helper.unlink(ABSTFN)
|
||||||
|
|
||||||
@os_helper.skip_unless_symlink
|
@os_helper.skip_unless_symlink
|
||||||
@skip_if_ABSTFN_contains_backslash
|
@skip_if_ABSTFN_contains_backslash
|
||||||
def test_realpath_missing_pardir(self):
|
@_parameterize({}, {'strict': ALLOW_MISSING})
|
||||||
|
def test_realpath_missing_pardir(self, kwargs):
|
||||||
try:
|
try:
|
||||||
os.symlink(TESTFN + "1", TESTFN)
|
os.symlink(TESTFN + "1", TESTFN)
|
||||||
self.assertEqual(realpath("nonexistent/../" + TESTFN), ABSTFN + "1")
|
self.assertEqual(
|
||||||
|
realpath("nonexistent/../" + TESTFN, **kwargs), ABSTFN + "1")
|
||||||
finally:
|
finally:
|
||||||
os_helper.unlink(TESTFN)
|
os_helper.unlink(TESTFN)
|
||||||
|
|
||||||
|
@ -617,37 +667,38 @@ def test_realpath_symlink_loops(self):
|
||||||
|
|
||||||
@os_helper.skip_unless_symlink
|
@os_helper.skip_unless_symlink
|
||||||
@skip_if_ABSTFN_contains_backslash
|
@skip_if_ABSTFN_contains_backslash
|
||||||
def test_realpath_symlink_loops_strict(self):
|
@_parameterize({'strict': True}, {'strict': ALLOW_MISSING})
|
||||||
|
def test_realpath_symlink_loops_strict(self, kwargs):
|
||||||
# Bug #43757, raise OSError if we get into an infinite symlink loop in
|
# Bug #43757, raise OSError if we get into an infinite symlink loop in
|
||||||
# strict mode.
|
# the strict modes.
|
||||||
try:
|
try:
|
||||||
os.symlink(ABSTFN, ABSTFN)
|
os.symlink(ABSTFN, ABSTFN)
|
||||||
self.assertRaises(OSError, realpath, ABSTFN, strict=True)
|
self.assertRaises(OSError, realpath, ABSTFN, **kwargs)
|
||||||
|
|
||||||
os.symlink(ABSTFN+"1", ABSTFN+"2")
|
os.symlink(ABSTFN+"1", ABSTFN+"2")
|
||||||
os.symlink(ABSTFN+"2", ABSTFN+"1")
|
os.symlink(ABSTFN+"2", ABSTFN+"1")
|
||||||
self.assertRaises(OSError, realpath, ABSTFN+"1", strict=True)
|
self.assertRaises(OSError, realpath, ABSTFN+"1", **kwargs)
|
||||||
self.assertRaises(OSError, realpath, ABSTFN+"2", strict=True)
|
self.assertRaises(OSError, realpath, ABSTFN+"2", **kwargs)
|
||||||
|
|
||||||
self.assertRaises(OSError, realpath, ABSTFN+"1/x", strict=True)
|
self.assertRaises(OSError, realpath, ABSTFN+"1/x", **kwargs)
|
||||||
self.assertRaises(OSError, realpath, ABSTFN+"1/..", strict=True)
|
self.assertRaises(OSError, realpath, ABSTFN+"1/..", **kwargs)
|
||||||
self.assertRaises(OSError, realpath, ABSTFN+"1/../x", strict=True)
|
self.assertRaises(OSError, realpath, ABSTFN+"1/../x", **kwargs)
|
||||||
os.symlink(ABSTFN+"x", ABSTFN+"y")
|
os.symlink(ABSTFN+"x", ABSTFN+"y")
|
||||||
self.assertRaises(OSError, realpath,
|
self.assertRaises(OSError, realpath,
|
||||||
ABSTFN+"1/../" + basename(ABSTFN) + "y", strict=True)
|
ABSTFN+"1/../" + basename(ABSTFN) + "y", **kwargs)
|
||||||
self.assertRaises(OSError, realpath,
|
self.assertRaises(OSError, realpath,
|
||||||
ABSTFN+"1/../" + basename(ABSTFN) + "1", strict=True)
|
ABSTFN+"1/../" + basename(ABSTFN) + "1", **kwargs)
|
||||||
|
|
||||||
os.symlink(basename(ABSTFN) + "a/b", ABSTFN+"a")
|
os.symlink(basename(ABSTFN) + "a/b", ABSTFN+"a")
|
||||||
self.assertRaises(OSError, realpath, ABSTFN+"a", strict=True)
|
self.assertRaises(OSError, realpath, ABSTFN+"a", **kwargs)
|
||||||
|
|
||||||
os.symlink("../" + basename(dirname(ABSTFN)) + "/" +
|
os.symlink("../" + basename(dirname(ABSTFN)) + "/" +
|
||||||
basename(ABSTFN) + "c", ABSTFN+"c")
|
basename(ABSTFN) + "c", ABSTFN+"c")
|
||||||
self.assertRaises(OSError, realpath, ABSTFN+"c", strict=True)
|
self.assertRaises(OSError, realpath, ABSTFN+"c", **kwargs)
|
||||||
|
|
||||||
# Test using relative path as well.
|
# Test using relative path as well.
|
||||||
with os_helper.change_cwd(dirname(ABSTFN)):
|
with os_helper.change_cwd(dirname(ABSTFN)):
|
||||||
self.assertRaises(OSError, realpath, basename(ABSTFN), strict=True)
|
self.assertRaises(OSError, realpath, basename(ABSTFN), **kwargs)
|
||||||
finally:
|
finally:
|
||||||
os_helper.unlink(ABSTFN)
|
os_helper.unlink(ABSTFN)
|
||||||
os_helper.unlink(ABSTFN+"1")
|
os_helper.unlink(ABSTFN+"1")
|
||||||
|
@ -658,13 +709,14 @@ def test_realpath_symlink_loops_strict(self):
|
||||||
|
|
||||||
@os_helper.skip_unless_symlink
|
@os_helper.skip_unless_symlink
|
||||||
@skip_if_ABSTFN_contains_backslash
|
@skip_if_ABSTFN_contains_backslash
|
||||||
def test_realpath_repeated_indirect_symlinks(self):
|
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
|
||||||
|
def test_realpath_repeated_indirect_symlinks(self, kwargs):
|
||||||
# Issue #6975.
|
# Issue #6975.
|
||||||
try:
|
try:
|
||||||
os.mkdir(ABSTFN)
|
os.mkdir(ABSTFN)
|
||||||
os.symlink('../' + basename(ABSTFN), ABSTFN + '/self')
|
os.symlink('../' + basename(ABSTFN), ABSTFN + '/self')
|
||||||
os.symlink('self/self/self', ABSTFN + '/link')
|
os.symlink('self/self/self', ABSTFN + '/link')
|
||||||
self.assertEqual(realpath(ABSTFN + '/link'), ABSTFN)
|
self.assertEqual(realpath(ABSTFN + '/link', **kwargs), ABSTFN)
|
||||||
finally:
|
finally:
|
||||||
os_helper.unlink(ABSTFN + '/self')
|
os_helper.unlink(ABSTFN + '/self')
|
||||||
os_helper.unlink(ABSTFN + '/link')
|
os_helper.unlink(ABSTFN + '/link')
|
||||||
|
@ -672,14 +724,15 @@ def test_realpath_repeated_indirect_symlinks(self):
|
||||||
|
|
||||||
@os_helper.skip_unless_symlink
|
@os_helper.skip_unless_symlink
|
||||||
@skip_if_ABSTFN_contains_backslash
|
@skip_if_ABSTFN_contains_backslash
|
||||||
def test_realpath_deep_recursion(self):
|
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
|
||||||
|
def test_realpath_deep_recursion(self, kwargs):
|
||||||
depth = 10
|
depth = 10
|
||||||
try:
|
try:
|
||||||
os.mkdir(ABSTFN)
|
os.mkdir(ABSTFN)
|
||||||
for i in range(depth):
|
for i in range(depth):
|
||||||
os.symlink('/'.join(['%d' % i] * 10), ABSTFN + '/%d' % (i + 1))
|
os.symlink('/'.join(['%d' % i] * 10), ABSTFN + '/%d' % (i + 1))
|
||||||
os.symlink('.', ABSTFN + '/0')
|
os.symlink('.', ABSTFN + '/0')
|
||||||
self.assertEqual(realpath(ABSTFN + '/%d' % depth), ABSTFN)
|
self.assertEqual(realpath(ABSTFN + '/%d' % depth, **kwargs), ABSTFN)
|
||||||
|
|
||||||
# Test using relative path as well.
|
# Test using relative path as well.
|
||||||
with os_helper.change_cwd(ABSTFN):
|
with os_helper.change_cwd(ABSTFN):
|
||||||
|
@ -691,7 +744,8 @@ def test_realpath_deep_recursion(self):
|
||||||
|
|
||||||
@os_helper.skip_unless_symlink
|
@os_helper.skip_unless_symlink
|
||||||
@skip_if_ABSTFN_contains_backslash
|
@skip_if_ABSTFN_contains_backslash
|
||||||
def test_realpath_resolve_parents(self):
|
@_parameterize({}, {'strict': ALLOW_MISSING})
|
||||||
|
def test_realpath_resolve_parents(self, kwargs):
|
||||||
# We also need to resolve any symlinks in the parents of a relative
|
# We also need to resolve any symlinks in the parents of a relative
|
||||||
# path passed to realpath. E.g.: current working directory is
|
# path passed to realpath. E.g.: current working directory is
|
||||||
# /usr/doc with 'doc' being a symlink to /usr/share/doc. We call
|
# /usr/doc with 'doc' being a symlink to /usr/share/doc. We call
|
||||||
|
@ -702,7 +756,8 @@ def test_realpath_resolve_parents(self):
|
||||||
os.symlink(ABSTFN + "/y", ABSTFN + "/k")
|
os.symlink(ABSTFN + "/y", ABSTFN + "/k")
|
||||||
|
|
||||||
with os_helper.change_cwd(ABSTFN + "/k"):
|
with os_helper.change_cwd(ABSTFN + "/k"):
|
||||||
self.assertEqual(realpath("a"), ABSTFN + "/y/a")
|
self.assertEqual(realpath("a", **kwargs),
|
||||||
|
ABSTFN + "/y/a")
|
||||||
finally:
|
finally:
|
||||||
os_helper.unlink(ABSTFN + "/k")
|
os_helper.unlink(ABSTFN + "/k")
|
||||||
os_helper.rmdir(ABSTFN + "/y")
|
os_helper.rmdir(ABSTFN + "/y")
|
||||||
|
@ -710,7 +765,8 @@ def test_realpath_resolve_parents(self):
|
||||||
|
|
||||||
@os_helper.skip_unless_symlink
|
@os_helper.skip_unless_symlink
|
||||||
@skip_if_ABSTFN_contains_backslash
|
@skip_if_ABSTFN_contains_backslash
|
||||||
def test_realpath_resolve_before_normalizing(self):
|
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
|
||||||
|
def test_realpath_resolve_before_normalizing(self, kwargs):
|
||||||
# Bug #990669: Symbolic links should be resolved before we
|
# Bug #990669: Symbolic links should be resolved before we
|
||||||
# normalize the path. E.g.: if we have directories 'a', 'k' and 'y'
|
# normalize the path. E.g.: if we have directories 'a', 'k' and 'y'
|
||||||
# in the following hierarchy:
|
# in the following hierarchy:
|
||||||
|
@ -725,10 +781,10 @@ def test_realpath_resolve_before_normalizing(self):
|
||||||
os.symlink(ABSTFN + "/k/y", ABSTFN + "/link-y")
|
os.symlink(ABSTFN + "/k/y", ABSTFN + "/link-y")
|
||||||
|
|
||||||
# Absolute path.
|
# Absolute path.
|
||||||
self.assertEqual(realpath(ABSTFN + "/link-y/.."), ABSTFN + "/k")
|
self.assertEqual(realpath(ABSTFN + "/link-y/..", **kwargs), ABSTFN + "/k")
|
||||||
# Relative path.
|
# Relative path.
|
||||||
with os_helper.change_cwd(dirname(ABSTFN)):
|
with os_helper.change_cwd(dirname(ABSTFN)):
|
||||||
self.assertEqual(realpath(basename(ABSTFN) + "/link-y/.."),
|
self.assertEqual(realpath(basename(ABSTFN) + "/link-y/..", **kwargs),
|
||||||
ABSTFN + "/k")
|
ABSTFN + "/k")
|
||||||
finally:
|
finally:
|
||||||
os_helper.unlink(ABSTFN + "/link-y")
|
os_helper.unlink(ABSTFN + "/link-y")
|
||||||
|
@ -738,7 +794,8 @@ def test_realpath_resolve_before_normalizing(self):
|
||||||
|
|
||||||
@os_helper.skip_unless_symlink
|
@os_helper.skip_unless_symlink
|
||||||
@skip_if_ABSTFN_contains_backslash
|
@skip_if_ABSTFN_contains_backslash
|
||||||
def test_realpath_resolve_first(self):
|
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
|
||||||
|
def test_realpath_resolve_first(self, kwargs):
|
||||||
# Bug #1213894: The first component of the path, if not absolute,
|
# Bug #1213894: The first component of the path, if not absolute,
|
||||||
# must be resolved too.
|
# must be resolved too.
|
||||||
|
|
||||||
|
@ -748,8 +805,8 @@ def test_realpath_resolve_first(self):
|
||||||
os.symlink(ABSTFN, ABSTFN + "link")
|
os.symlink(ABSTFN, ABSTFN + "link")
|
||||||
with os_helper.change_cwd(dirname(ABSTFN)):
|
with os_helper.change_cwd(dirname(ABSTFN)):
|
||||||
base = basename(ABSTFN)
|
base = basename(ABSTFN)
|
||||||
self.assertEqual(realpath(base + "link"), ABSTFN)
|
self.assertEqual(realpath(base + "link", **kwargs), ABSTFN)
|
||||||
self.assertEqual(realpath(base + "link/k"), ABSTFN + "/k")
|
self.assertEqual(realpath(base + "link/k", **kwargs), ABSTFN + "/k")
|
||||||
finally:
|
finally:
|
||||||
os_helper.unlink(ABSTFN + "link")
|
os_helper.unlink(ABSTFN + "link")
|
||||||
os_helper.rmdir(ABSTFN + "/k")
|
os_helper.rmdir(ABSTFN + "/k")
|
||||||
|
@ -767,12 +824,67 @@ def test_realpath_unreadable_symlink(self):
|
||||||
self.assertEqual(realpath(ABSTFN + '/foo'), ABSTFN + '/foo')
|
self.assertEqual(realpath(ABSTFN + '/foo'), ABSTFN + '/foo')
|
||||||
self.assertEqual(realpath(ABSTFN + '/../foo'), dirname(ABSTFN) + '/foo')
|
self.assertEqual(realpath(ABSTFN + '/../foo'), dirname(ABSTFN) + '/foo')
|
||||||
self.assertEqual(realpath(ABSTFN + '/foo/..'), ABSTFN)
|
self.assertEqual(realpath(ABSTFN + '/foo/..'), ABSTFN)
|
||||||
with self.assertRaises(PermissionError):
|
|
||||||
realpath(ABSTFN, strict=True)
|
|
||||||
finally:
|
finally:
|
||||||
os.chmod(ABSTFN, 0o755, follow_symlinks=False)
|
os.chmod(ABSTFN, 0o755, follow_symlinks=False)
|
||||||
os_helper.unlink(ABSTFN)
|
os_helper.unlink(ABSTFN)
|
||||||
|
|
||||||
|
@os_helper.skip_unless_symlink
|
||||||
|
@skip_if_ABSTFN_contains_backslash
|
||||||
|
@unittest.skipIf(os.chmod not in os.supports_follow_symlinks, "Can't set symlink permissions")
|
||||||
|
@unittest.skipIf(sys.platform != "darwin", "only macOS requires read permission to readlink()")
|
||||||
|
@_parameterize({'strict': True}, {'strict': ALLOW_MISSING})
|
||||||
|
def test_realpath_unreadable_symlink_strict(self, kwargs):
|
||||||
|
try:
|
||||||
|
os.symlink(ABSTFN+"1", ABSTFN)
|
||||||
|
os.chmod(ABSTFN, 0o000, follow_symlinks=False)
|
||||||
|
with self.assertRaises(PermissionError):
|
||||||
|
realpath(ABSTFN, **kwargs)
|
||||||
|
with self.assertRaises(PermissionError):
|
||||||
|
realpath(ABSTFN + '/foo', **kwargs),
|
||||||
|
with self.assertRaises(PermissionError):
|
||||||
|
realpath(ABSTFN + '/../foo', **kwargs)
|
||||||
|
with self.assertRaises(PermissionError):
|
||||||
|
realpath(ABSTFN + '/foo/..', **kwargs)
|
||||||
|
finally:
|
||||||
|
os.chmod(ABSTFN, 0o755, follow_symlinks=False)
|
||||||
|
os.unlink(ABSTFN)
|
||||||
|
|
||||||
|
@skip_if_ABSTFN_contains_backslash
|
||||||
|
@os_helper.skip_unless_symlink
|
||||||
|
def test_realpath_unreadable_directory(self):
|
||||||
|
try:
|
||||||
|
os.mkdir(ABSTFN)
|
||||||
|
os.mkdir(ABSTFN + '/k')
|
||||||
|
os.chmod(ABSTFN, 0o000)
|
||||||
|
self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN)
|
||||||
|
self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN)
|
||||||
|
self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.stat(ABSTFN)
|
||||||
|
except PermissionError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.skipTest('Cannot block permissions')
|
||||||
|
|
||||||
|
self.assertEqual(realpath(ABSTFN + '/k', strict=False),
|
||||||
|
ABSTFN + '/k')
|
||||||
|
self.assertRaises(PermissionError, realpath, ABSTFN + '/k',
|
||||||
|
strict=True)
|
||||||
|
self.assertRaises(PermissionError, realpath, ABSTFN + '/k',
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
|
|
||||||
|
self.assertEqual(realpath(ABSTFN + '/missing', strict=False),
|
||||||
|
ABSTFN + '/missing')
|
||||||
|
self.assertRaises(PermissionError, realpath, ABSTFN + '/missing',
|
||||||
|
strict=True)
|
||||||
|
self.assertRaises(PermissionError, realpath, ABSTFN + '/missing',
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
|
finally:
|
||||||
|
os.chmod(ABSTFN, 0o755)
|
||||||
|
os_helper.rmdir(ABSTFN + '/k')
|
||||||
|
os_helper.rmdir(ABSTFN)
|
||||||
|
|
||||||
@skip_if_ABSTFN_contains_backslash
|
@skip_if_ABSTFN_contains_backslash
|
||||||
def test_realpath_nonterminal_file(self):
|
def test_realpath_nonterminal_file(self):
|
||||||
try:
|
try:
|
||||||
|
@ -780,14 +892,27 @@ def test_realpath_nonterminal_file(self):
|
||||||
f.write('test_posixpath wuz ere')
|
f.write('test_posixpath wuz ere')
|
||||||
self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN)
|
self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN)
|
||||||
self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN)
|
self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN)
|
||||||
|
self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN)
|
||||||
|
|
||||||
self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN)
|
self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN)
|
||||||
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True)
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True)
|
||||||
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/",
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
|
|
||||||
self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN)
|
self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN)
|
||||||
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True)
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True)
|
||||||
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.",
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
|
|
||||||
self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN))
|
self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN))
|
||||||
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True)
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True)
|
||||||
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..",
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
|
|
||||||
self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "/subdir")
|
self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "/subdir")
|
||||||
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True)
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True)
|
||||||
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir",
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
finally:
|
finally:
|
||||||
os_helper.unlink(ABSTFN)
|
os_helper.unlink(ABSTFN)
|
||||||
|
|
||||||
|
@ -800,14 +925,27 @@ def test_realpath_nonterminal_symlink_to_file(self):
|
||||||
os.symlink(ABSTFN + "1", ABSTFN)
|
os.symlink(ABSTFN + "1", ABSTFN)
|
||||||
self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN + "1")
|
self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN + "1")
|
||||||
self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "1")
|
self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "1")
|
||||||
|
self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN + "1")
|
||||||
|
|
||||||
self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN + "1")
|
self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN + "1")
|
||||||
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True)
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True)
|
||||||
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/",
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
|
|
||||||
self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN + "1")
|
self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN + "1")
|
||||||
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True)
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True)
|
||||||
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.",
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
|
|
||||||
self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN))
|
self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN))
|
||||||
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True)
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True)
|
||||||
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..",
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
|
|
||||||
self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "1/subdir")
|
self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "1/subdir")
|
||||||
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True)
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True)
|
||||||
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir",
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
finally:
|
finally:
|
||||||
os_helper.unlink(ABSTFN)
|
os_helper.unlink(ABSTFN)
|
||||||
os_helper.unlink(ABSTFN + "1")
|
os_helper.unlink(ABSTFN + "1")
|
||||||
|
@ -822,14 +960,27 @@ def test_realpath_nonterminal_symlink_to_symlinks_to_file(self):
|
||||||
os.symlink(ABSTFN + "1", ABSTFN)
|
os.symlink(ABSTFN + "1", ABSTFN)
|
||||||
self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN + "2")
|
self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN + "2")
|
||||||
self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "2")
|
self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "2")
|
||||||
|
self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "2")
|
||||||
|
|
||||||
self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN + "2")
|
self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN + "2")
|
||||||
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True)
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True)
|
||||||
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/",
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
|
|
||||||
self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN + "2")
|
self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN + "2")
|
||||||
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True)
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True)
|
||||||
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.",
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
|
|
||||||
self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN))
|
self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN))
|
||||||
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True)
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True)
|
||||||
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..",
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
|
|
||||||
self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "2/subdir")
|
self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "2/subdir")
|
||||||
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True)
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True)
|
||||||
|
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir",
|
||||||
|
strict=ALLOW_MISSING)
|
||||||
finally:
|
finally:
|
||||||
os_helper.unlink(ABSTFN)
|
os_helper.unlink(ABSTFN)
|
||||||
os_helper.unlink(ABSTFN + "1")
|
os_helper.unlink(ABSTFN + "1")
|
||||||
|
@ -1017,9 +1168,12 @@ def test_path_normpath(self):
|
||||||
def test_path_abspath(self):
|
def test_path_abspath(self):
|
||||||
self.assertPathEqual(self.path.abspath)
|
self.assertPathEqual(self.path.abspath)
|
||||||
|
|
||||||
def test_path_realpath(self):
|
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
|
||||||
|
def test_path_realpath(self, kwargs):
|
||||||
self.assertPathEqual(self.path.realpath)
|
self.assertPathEqual(self.path.realpath)
|
||||||
|
|
||||||
|
self.assertPathEqual(partial(self.path.realpath, **kwargs))
|
||||||
|
|
||||||
def test_path_relpath(self):
|
def test_path_relpath(self):
|
||||||
self.assertPathEqual(self.path.relpath)
|
self.assertPathEqual(self.path.relpath)
|
||||||
|
|
||||||
|
|
|
@ -2682,6 +2682,31 @@ def test_useful_error_message_when_modules_missing(self):
|
||||||
str(excinfo.exception),
|
str(excinfo.exception),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@unittest.skipUnless(os_helper.can_symlink(), 'requires symlink support')
|
||||||
|
@unittest.skipUnless(hasattr(os, 'chmod'), "missing os.chmod")
|
||||||
|
@unittest.mock.patch('os.chmod')
|
||||||
|
def test_deferred_directory_attributes_update(self, mock_chmod):
|
||||||
|
# Regression test for gh-127987: setting attributes on arbitrary files
|
||||||
|
tempdir = os.path.join(TEMPDIR, 'test127987')
|
||||||
|
def mock_chmod_side_effect(path, mode, **kwargs):
|
||||||
|
target_path = os.path.realpath(path)
|
||||||
|
if os.path.commonpath([target_path, tempdir]) != tempdir:
|
||||||
|
raise Exception("should not try to chmod anything outside the destination", target_path)
|
||||||
|
mock_chmod.side_effect = mock_chmod_side_effect
|
||||||
|
|
||||||
|
outside_tree_dir = os.path.join(TEMPDIR, 'outside_tree_dir')
|
||||||
|
with ArchiveMaker() as arc:
|
||||||
|
arc.add('x', symlink_to='.')
|
||||||
|
arc.add('x', type=tarfile.DIRTYPE, mode='?rwsrwsrwt')
|
||||||
|
arc.add('x', symlink_to=outside_tree_dir)
|
||||||
|
|
||||||
|
os.makedirs(outside_tree_dir)
|
||||||
|
try:
|
||||||
|
arc.open().extractall(path=tempdir, filter='tar')
|
||||||
|
finally:
|
||||||
|
os_helper.rmtree(outside_tree_dir)
|
||||||
|
os_helper.rmtree(tempdir)
|
||||||
|
|
||||||
|
|
||||||
class CommandLineTest(unittest.TestCase):
|
class CommandLineTest(unittest.TestCase):
|
||||||
|
|
||||||
|
@ -3242,6 +3267,10 @@ def check_files_present(self, directory):
|
||||||
got_paths = set(
|
got_paths = set(
|
||||||
p.relative_to(directory)
|
p.relative_to(directory)
|
||||||
for p in pathlib.Path(directory).glob('**/*'))
|
for p in pathlib.Path(directory).glob('**/*'))
|
||||||
|
if self.extraction_filter == 'data':
|
||||||
|
# The 'data' filter is expected to reject special files
|
||||||
|
for path in 'ustar/fifotype', 'ustar/blktype', 'ustar/chrtype':
|
||||||
|
got_paths.discard(pathlib.Path(path))
|
||||||
self.assertEqual(self.control_paths, got_paths)
|
self.assertEqual(self.control_paths, got_paths)
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
|
@ -3471,12 +3500,28 @@ def __exit__(self, *exc):
|
||||||
self.bio = None
|
self.bio = None
|
||||||
|
|
||||||
def add(self, name, *, type=None, symlink_to=None, hardlink_to=None,
|
def add(self, name, *, type=None, symlink_to=None, hardlink_to=None,
|
||||||
mode=None, size=None, **kwargs):
|
mode=None, size=None, content=None, **kwargs):
|
||||||
"""Add a member to the test archive. Call within `with`."""
|
"""Add a member to the test archive. Call within `with`.
|
||||||
|
|
||||||
|
Provides many shortcuts:
|
||||||
|
- default `type` is based on symlink_to, hardlink_to, and trailing `/`
|
||||||
|
in name (which is stripped)
|
||||||
|
- size & content defaults are based on each other
|
||||||
|
- content can be str or bytes
|
||||||
|
- mode should be textual ('-rwxrwxrwx')
|
||||||
|
|
||||||
|
(add more! this is unstable internal test-only API)
|
||||||
|
"""
|
||||||
name = str(name)
|
name = str(name)
|
||||||
tarinfo = tarfile.TarInfo(name).replace(**kwargs)
|
tarinfo = tarfile.TarInfo(name).replace(**kwargs)
|
||||||
|
if content is not None:
|
||||||
|
if isinstance(content, str):
|
||||||
|
content = content.encode()
|
||||||
|
size = len(content)
|
||||||
if size is not None:
|
if size is not None:
|
||||||
tarinfo.size = size
|
tarinfo.size = size
|
||||||
|
if content is None:
|
||||||
|
content = bytes(tarinfo.size)
|
||||||
if mode:
|
if mode:
|
||||||
tarinfo.mode = _filemode_to_int(mode)
|
tarinfo.mode = _filemode_to_int(mode)
|
||||||
if symlink_to is not None:
|
if symlink_to is not None:
|
||||||
|
@ -3490,7 +3535,7 @@ def add(self, name, *, type=None, symlink_to=None, hardlink_to=None,
|
||||||
if type is not None:
|
if type is not None:
|
||||||
tarinfo.type = type
|
tarinfo.type = type
|
||||||
if tarinfo.isreg():
|
if tarinfo.isreg():
|
||||||
fileobj = io.BytesIO(bytes(tarinfo.size))
|
fileobj = io.BytesIO(content)
|
||||||
else:
|
else:
|
||||||
fileobj = None
|
fileobj = None
|
||||||
self.tar_w.addfile(tarinfo, fileobj)
|
self.tar_w.addfile(tarinfo, fileobj)
|
||||||
|
@ -3524,7 +3569,7 @@ class TestExtractionFilters(unittest.TestCase):
|
||||||
destdir = outerdir / 'dest'
|
destdir = outerdir / 'dest'
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def check_context(self, tar, filter):
|
def check_context(self, tar, filter, *, check_flag=True):
|
||||||
"""Extracts `tar` to `self.destdir` and allows checking the result
|
"""Extracts `tar` to `self.destdir` and allows checking the result
|
||||||
|
|
||||||
If an error occurs, it must be checked using `expect_exception`
|
If an error occurs, it must be checked using `expect_exception`
|
||||||
|
@ -3533,27 +3578,40 @@ def check_context(self, tar, filter):
|
||||||
except the destination directory itself and parent directories of
|
except the destination directory itself and parent directories of
|
||||||
other files.
|
other files.
|
||||||
When checking directories, do so before their contents.
|
When checking directories, do so before their contents.
|
||||||
|
|
||||||
|
A file called 'flag' is made in outerdir (i.e. outside destdir)
|
||||||
|
before extraction; it should not be altered nor should its contents
|
||||||
|
be read/copied.
|
||||||
"""
|
"""
|
||||||
with os_helper.temp_dir(self.outerdir):
|
with os_helper.temp_dir(self.outerdir):
|
||||||
|
flag_path = self.outerdir / 'flag'
|
||||||
|
flag_path.write_text('capture me')
|
||||||
try:
|
try:
|
||||||
tar.extractall(self.destdir, filter=filter)
|
tar.extractall(self.destdir, filter=filter)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.raised_exception = exc
|
self.raised_exception = exc
|
||||||
|
self.reraise_exception = True
|
||||||
self.expected_paths = set()
|
self.expected_paths = set()
|
||||||
else:
|
else:
|
||||||
self.raised_exception = None
|
self.raised_exception = None
|
||||||
|
self.reraise_exception = False
|
||||||
self.expected_paths = set(self.outerdir.glob('**/*'))
|
self.expected_paths = set(self.outerdir.glob('**/*'))
|
||||||
self.expected_paths.discard(self.destdir)
|
self.expected_paths.discard(self.destdir)
|
||||||
|
self.expected_paths.discard(flag_path)
|
||||||
try:
|
try:
|
||||||
yield
|
yield self
|
||||||
finally:
|
finally:
|
||||||
tar.close()
|
tar.close()
|
||||||
if self.raised_exception:
|
if self.reraise_exception:
|
||||||
raise self.raised_exception
|
raise self.raised_exception
|
||||||
self.assertEqual(self.expected_paths, set())
|
self.assertEqual(self.expected_paths, set())
|
||||||
|
if check_flag:
|
||||||
|
self.assertEqual(flag_path.read_text(), 'capture me')
|
||||||
|
else:
|
||||||
|
assert filter == 'fully_trusted'
|
||||||
|
|
||||||
def expect_file(self, name, type=None, symlink_to=None, mode=None,
|
def expect_file(self, name, type=None, symlink_to=None, mode=None,
|
||||||
size=None):
|
size=None, content=None):
|
||||||
"""Check a single file. See check_context."""
|
"""Check a single file. See check_context."""
|
||||||
if self.raised_exception:
|
if self.raised_exception:
|
||||||
raise self.raised_exception
|
raise self.raised_exception
|
||||||
|
@ -3572,26 +3630,45 @@ def expect_file(self, name, type=None, symlink_to=None, mode=None,
|
||||||
# The symlink might be the same (textually) as what we expect,
|
# The symlink might be the same (textually) as what we expect,
|
||||||
# but some systems change the link to an equivalent path, so
|
# but some systems change the link to an equivalent path, so
|
||||||
# we fall back to samefile().
|
# we fall back to samefile().
|
||||||
if expected != got:
|
try:
|
||||||
self.assertTrue(got.samefile(expected))
|
if expected != got:
|
||||||
|
self.assertTrue(got.samefile(expected))
|
||||||
|
except Exception as e:
|
||||||
|
# attach a note, so it's shown even if `samefile` fails
|
||||||
|
e.add_note(f'{expected=}, {got=}')
|
||||||
|
raise
|
||||||
elif type == tarfile.REGTYPE or type is None:
|
elif type == tarfile.REGTYPE or type is None:
|
||||||
self.assertTrue(path.is_file())
|
self.assertTrue(path.is_file())
|
||||||
elif type == tarfile.DIRTYPE:
|
elif type == tarfile.DIRTYPE:
|
||||||
self.assertTrue(path.is_dir())
|
self.assertTrue(path.is_dir())
|
||||||
elif type == tarfile.FIFOTYPE:
|
elif type == tarfile.FIFOTYPE:
|
||||||
self.assertTrue(path.is_fifo())
|
self.assertTrue(path.is_fifo())
|
||||||
|
elif type == tarfile.SYMTYPE:
|
||||||
|
self.assertTrue(path.is_symlink())
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(type)
|
raise NotImplementedError(type)
|
||||||
if size is not None:
|
if size is not None:
|
||||||
self.assertEqual(path.stat().st_size, size)
|
self.assertEqual(path.stat().st_size, size)
|
||||||
|
if content is not None:
|
||||||
|
self.assertEqual(path.read_text(), content)
|
||||||
for parent in path.parents:
|
for parent in path.parents:
|
||||||
self.expected_paths.discard(parent)
|
self.expected_paths.discard(parent)
|
||||||
|
|
||||||
|
def expect_any_tree(self, name):
|
||||||
|
"""Check a directory; forget about its contents."""
|
||||||
|
tree_path = (self.destdir / name).resolve()
|
||||||
|
self.expect_file(tree_path, type=tarfile.DIRTYPE)
|
||||||
|
self.expected_paths = {
|
||||||
|
p for p in self.expected_paths
|
||||||
|
if tree_path not in p.parents
|
||||||
|
}
|
||||||
|
|
||||||
def expect_exception(self, exc_type, message_re='.'):
|
def expect_exception(self, exc_type, message_re='.'):
|
||||||
with self.assertRaisesRegex(exc_type, message_re):
|
with self.assertRaisesRegex(exc_type, message_re):
|
||||||
if self.raised_exception is not None:
|
if self.raised_exception is not None:
|
||||||
raise self.raised_exception
|
raise self.raised_exception
|
||||||
self.raised_exception = None
|
self.reraise_exception = False
|
||||||
|
return self.raised_exception
|
||||||
|
|
||||||
def test_benign_file(self):
|
def test_benign_file(self):
|
||||||
with ArchiveMaker() as arc:
|
with ArchiveMaker() as arc:
|
||||||
|
@ -3676,6 +3753,80 @@ def test_parent_symlink(self):
|
||||||
with self.check_context(arc.open(), 'data'):
|
with self.check_context(arc.open(), 'data'):
|
||||||
self.expect_file('parent/evil')
|
self.expect_file('parent/evil')
|
||||||
|
|
||||||
|
@symlink_test
|
||||||
|
@os_helper.skip_unless_symlink
|
||||||
|
def test_realpath_limit_attack(self):
|
||||||
|
# (CVE-2025-4517)
|
||||||
|
|
||||||
|
with ArchiveMaker() as arc:
|
||||||
|
# populate the symlinks and dirs that expand in os.path.realpath()
|
||||||
|
# The component length is chosen so that in common cases, the unexpanded
|
||||||
|
# path fits in PATH_MAX, but it overflows when the final symlink
|
||||||
|
# is expanded
|
||||||
|
steps = "abcdefghijklmnop"
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
component = 'd' * 25
|
||||||
|
elif 'PC_PATH_MAX' in os.pathconf_names:
|
||||||
|
max_path_len = os.pathconf(self.outerdir.parent, "PC_PATH_MAX")
|
||||||
|
path_sep_len = 1
|
||||||
|
dest_len = len(str(self.destdir)) + path_sep_len
|
||||||
|
component_len = (max_path_len - dest_len) // (len(steps) + path_sep_len)
|
||||||
|
component = 'd' * component_len
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("Need to guess component length for {sys.platform}")
|
||||||
|
path = ""
|
||||||
|
step_path = ""
|
||||||
|
for i in steps:
|
||||||
|
arc.add(os.path.join(path, component), type=tarfile.DIRTYPE,
|
||||||
|
mode='drwxrwxrwx')
|
||||||
|
arc.add(os.path.join(path, i), symlink_to=component)
|
||||||
|
path = os.path.join(path, component)
|
||||||
|
step_path = os.path.join(step_path, i)
|
||||||
|
# create the final symlink that exceeds PATH_MAX and simply points
|
||||||
|
# to the top dir.
|
||||||
|
# this link will never be expanded by
|
||||||
|
# os.path.realpath(strict=False), nor anything after it.
|
||||||
|
linkpath = os.path.join(*steps, "l"*254)
|
||||||
|
parent_segments = [".."] * len(steps)
|
||||||
|
arc.add(linkpath, symlink_to=os.path.join(*parent_segments))
|
||||||
|
# make a symlink outside to keep the tar command happy
|
||||||
|
arc.add("escape", symlink_to=os.path.join(linkpath, ".."))
|
||||||
|
# use the symlinks above, that are not checked, to create a hardlink
|
||||||
|
# to a file outside of the destination path
|
||||||
|
arc.add("flaglink", hardlink_to=os.path.join("escape", "flag"))
|
||||||
|
# now that we have the hardlink we can overwrite the file
|
||||||
|
arc.add("flaglink", content='overwrite')
|
||||||
|
# we can also create new files as well!
|
||||||
|
arc.add("escape/newfile", content='new')
|
||||||
|
|
||||||
|
with (self.subTest('fully_trusted'),
|
||||||
|
self.check_context(arc.open(), filter='fully_trusted',
|
||||||
|
check_flag=False)):
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
self.expect_exception((FileNotFoundError, FileExistsError))
|
||||||
|
elif self.raised_exception:
|
||||||
|
# Cannot symlink/hardlink: tarfile falls back to getmember()
|
||||||
|
self.expect_exception(KeyError)
|
||||||
|
# Otherwise, this block should never enter.
|
||||||
|
else:
|
||||||
|
self.expect_any_tree(component)
|
||||||
|
self.expect_file('flaglink', content='overwrite')
|
||||||
|
self.expect_file('../newfile', content='new')
|
||||||
|
self.expect_file('escape', type=tarfile.SYMTYPE)
|
||||||
|
self.expect_file('a', symlink_to=component)
|
||||||
|
|
||||||
|
for filter in 'tar', 'data':
|
||||||
|
with self.subTest(filter), self.check_context(arc.open(), filter=filter):
|
||||||
|
exc = self.expect_exception((OSError, KeyError))
|
||||||
|
if isinstance(exc, OSError):
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
# 3: ERROR_PATH_NOT_FOUND
|
||||||
|
# 5: ERROR_ACCESS_DENIED
|
||||||
|
# 206: ERROR_FILENAME_EXCED_RANGE
|
||||||
|
self.assertIn(exc.winerror, (3, 5, 206))
|
||||||
|
else:
|
||||||
|
self.assertEqual(exc.errno, errno.ENAMETOOLONG)
|
||||||
|
|
||||||
@symlink_test
|
@symlink_test
|
||||||
def test_parent_symlink2(self):
|
def test_parent_symlink2(self):
|
||||||
# Test interplaying symlinks
|
# Test interplaying symlinks
|
||||||
|
@ -3898,8 +4049,8 @@ def test_chains(self):
|
||||||
arc.add('symlink2', symlink_to=os.path.join(
|
arc.add('symlink2', symlink_to=os.path.join(
|
||||||
'linkdir', 'hardlink2'))
|
'linkdir', 'hardlink2'))
|
||||||
arc.add('targetdir/target', size=3)
|
arc.add('targetdir/target', size=3)
|
||||||
arc.add('linkdir/hardlink', hardlink_to='targetdir/target')
|
arc.add('linkdir/hardlink', hardlink_to=os.path.join('targetdir', 'target'))
|
||||||
arc.add('linkdir/hardlink2', hardlink_to='linkdir/symlink')
|
arc.add('linkdir/hardlink2', hardlink_to=os.path.join('linkdir', 'symlink'))
|
||||||
|
|
||||||
for filter in 'tar', 'data', 'fully_trusted':
|
for filter in 'tar', 'data', 'fully_trusted':
|
||||||
with self.check_context(arc.open(), filter):
|
with self.check_context(arc.open(), filter):
|
||||||
|
@ -3915,6 +4066,129 @@ def test_chains(self):
|
||||||
self.expect_file('linkdir/symlink', size=3)
|
self.expect_file('linkdir/symlink', size=3)
|
||||||
self.expect_file('symlink2', size=3)
|
self.expect_file('symlink2', size=3)
|
||||||
|
|
||||||
|
@symlink_test
|
||||||
|
def test_sneaky_hardlink_fallback(self):
|
||||||
|
# (CVE-2025-4330)
|
||||||
|
# Test that when hardlink extraction falls back to extracting members
|
||||||
|
# from the archive, the extracted member is (re-)filtered.
|
||||||
|
with ArchiveMaker() as arc:
|
||||||
|
# Create a directory structure so the c/escape symlink stays
|
||||||
|
# inside the path
|
||||||
|
arc.add("a/t/dummy")
|
||||||
|
# Create b/ directory
|
||||||
|
arc.add("b/")
|
||||||
|
# Point "c" to the bottom of the tree in "a"
|
||||||
|
arc.add("c", symlink_to=os.path.join("a", "t"))
|
||||||
|
# link to non-existant location under "a"
|
||||||
|
arc.add("c/escape", symlink_to=os.path.join("..", "..",
|
||||||
|
"link_here"))
|
||||||
|
# Move "c" to point to "b" ("c/escape" no longer exists)
|
||||||
|
arc.add("c", symlink_to="b")
|
||||||
|
# Attempt to create a hard link to "c/escape". Since it doesn't
|
||||||
|
# exist it will attempt to extract "cescape" but at "boom".
|
||||||
|
arc.add("boom", hardlink_to=os.path.join("c", "escape"))
|
||||||
|
|
||||||
|
with self.check_context(arc.open(), 'data'):
|
||||||
|
if not os_helper.can_symlink():
|
||||||
|
# When 'c/escape' is extracted, 'c' is a regular
|
||||||
|
# directory, and 'c/escape' *would* point outside
|
||||||
|
# the destination if symlinks were allowed.
|
||||||
|
self.expect_exception(
|
||||||
|
tarfile.LinkOutsideDestinationError)
|
||||||
|
elif sys.platform == "win32":
|
||||||
|
# On Windows, 'c/escape' points outside the destination
|
||||||
|
self.expect_exception(tarfile.LinkOutsideDestinationError)
|
||||||
|
else:
|
||||||
|
e = self.expect_exception(
|
||||||
|
tarfile.LinkFallbackError,
|
||||||
|
"link 'boom' would be extracted as a copy of "
|
||||||
|
+ "'c/escape', which was rejected")
|
||||||
|
self.assertIsInstance(e.__cause__,
|
||||||
|
tarfile.LinkOutsideDestinationError)
|
||||||
|
for filter in 'tar', 'fully_trusted':
|
||||||
|
with self.subTest(filter), self.check_context(arc.open(), filter):
|
||||||
|
if not os_helper.can_symlink():
|
||||||
|
self.expect_file("a/t/dummy")
|
||||||
|
self.expect_file("b/")
|
||||||
|
self.expect_file("c/")
|
||||||
|
else:
|
||||||
|
self.expect_file("a/t/dummy")
|
||||||
|
self.expect_file("b/")
|
||||||
|
self.expect_file("a/t/escape", symlink_to='../../link_here')
|
||||||
|
self.expect_file("boom", symlink_to='../../link_here')
|
||||||
|
self.expect_file("c", symlink_to='b')
|
||||||
|
|
||||||
|
@symlink_test
|
||||||
|
def test_exfiltration_via_symlink(self):
|
||||||
|
# (CVE-2025-4138)
|
||||||
|
# Test changing symlinks that result in a symlink pointing outside
|
||||||
|
# the extraction directory, unless prevented by 'data' filter's
|
||||||
|
# normalization.
|
||||||
|
with ArchiveMaker() as arc:
|
||||||
|
arc.add("escape", symlink_to=os.path.join('link', 'link', '..', '..', 'link-here'))
|
||||||
|
arc.add("link", symlink_to='./')
|
||||||
|
|
||||||
|
for filter in 'tar', 'data', 'fully_trusted':
|
||||||
|
with self.check_context(arc.open(), filter):
|
||||||
|
if os_helper.can_symlink():
|
||||||
|
self.expect_file("link", symlink_to='./')
|
||||||
|
if filter == 'data':
|
||||||
|
self.expect_file("escape", symlink_to='link-here')
|
||||||
|
else:
|
||||||
|
self.expect_file("escape",
|
||||||
|
symlink_to='link/link/../../link-here')
|
||||||
|
else:
|
||||||
|
# Nothing is extracted.
|
||||||
|
pass
|
||||||
|
|
||||||
|
@symlink_test
|
||||||
|
def test_chmod_outside_dir(self):
|
||||||
|
# (CVE-2024-12718)
|
||||||
|
# Test that members used for delayed updates of directory metadata
|
||||||
|
# are (re-)filtered.
|
||||||
|
with ArchiveMaker() as arc:
|
||||||
|
# "pwn" is a veeeery innocent symlink:
|
||||||
|
arc.add("a/pwn", symlink_to='.')
|
||||||
|
# But now "pwn" is also a directory, so it's scheduled to have its
|
||||||
|
# metadata updated later:
|
||||||
|
arc.add("a/pwn/", mode='drwxrwxrwx')
|
||||||
|
# Oops, "pwn" is not so innocent any more:
|
||||||
|
arc.add("a/pwn", symlink_to='x/../')
|
||||||
|
# Newly created symlink points to the dest dir,
|
||||||
|
# so it's OK for the "data" filter.
|
||||||
|
arc.add('a/x', symlink_to=('../'))
|
||||||
|
# But now "pwn" points outside the dest dir
|
||||||
|
|
||||||
|
for filter in 'tar', 'data', 'fully_trusted':
|
||||||
|
with self.check_context(arc.open(), filter) as cc:
|
||||||
|
if not os_helper.can_symlink():
|
||||||
|
self.expect_file("a/pwn/")
|
||||||
|
elif filter == 'data':
|
||||||
|
self.expect_file("a/x", symlink_to='../')
|
||||||
|
self.expect_file("a/pwn", symlink_to='.')
|
||||||
|
else:
|
||||||
|
self.expect_file("a/x", symlink_to='../')
|
||||||
|
self.expect_file("a/pwn", symlink_to='x/../')
|
||||||
|
if sys.platform != "win32":
|
||||||
|
st_mode = cc.outerdir.stat().st_mode
|
||||||
|
self.assertNotEqual(st_mode & 0o777, 0o777)
|
||||||
|
|
||||||
|
def test_link_fallback_normalizes(self):
|
||||||
|
# Make sure hardlink fallbacks work for non-normalized paths for all
|
||||||
|
# filters
|
||||||
|
with ArchiveMaker() as arc:
|
||||||
|
arc.add("dir/")
|
||||||
|
arc.add("dir/../afile")
|
||||||
|
arc.add("link1", hardlink_to='dir/../afile')
|
||||||
|
arc.add("link2", hardlink_to='dir/../dir/../afile')
|
||||||
|
|
||||||
|
for filter in 'tar', 'data', 'fully_trusted':
|
||||||
|
with self.check_context(arc.open(), filter) as cc:
|
||||||
|
self.expect_file("dir/")
|
||||||
|
self.expect_file("afile")
|
||||||
|
self.expect_file("link1")
|
||||||
|
self.expect_file("link2")
|
||||||
|
|
||||||
def test_modes(self):
|
def test_modes(self):
|
||||||
# Test how file modes are extracted
|
# Test how file modes are extracted
|
||||||
# (Note that the modes are ignored on platforms without working chmod)
|
# (Note that the modes are ignored on platforms without working chmod)
|
||||||
|
@ -4039,7 +4313,7 @@ def test_tar_filter(self):
|
||||||
# The 'tar' filter returns TarInfo objects with the same name/type.
|
# The 'tar' filter returns TarInfo objects with the same name/type.
|
||||||
# (It can also fail for particularly "evil" input, but we don't have
|
# (It can also fail for particularly "evil" input, but we don't have
|
||||||
# that in the test archive.)
|
# that in the test archive.)
|
||||||
with tarfile.TarFile.open(tarname) as tar:
|
with tarfile.TarFile.open(tarname, encoding="iso8859-1") as tar:
|
||||||
for tarinfo in tar.getmembers():
|
for tarinfo in tar.getmembers():
|
||||||
try:
|
try:
|
||||||
filtered = tarfile.tar_filter(tarinfo, '')
|
filtered = tarfile.tar_filter(tarinfo, '')
|
||||||
|
@ -4051,7 +4325,7 @@ def test_tar_filter(self):
|
||||||
def test_data_filter(self):
|
def test_data_filter(self):
|
||||||
# The 'data' filter either raises, or returns TarInfo with the same
|
# The 'data' filter either raises, or returns TarInfo with the same
|
||||||
# name/type.
|
# name/type.
|
||||||
with tarfile.TarFile.open(tarname) as tar:
|
with tarfile.TarFile.open(tarname, encoding="iso8859-1") as tar:
|
||||||
for tarinfo in tar.getmembers():
|
for tarinfo in tar.getmembers():
|
||||||
try:
|
try:
|
||||||
filtered = tarfile.data_filter(tarinfo, '')
|
filtered = tarfile.data_filter(tarinfo, '')
|
||||||
|
@ -4218,13 +4492,13 @@ def valueerror_filter(tarinfo, path):
|
||||||
# If errorlevel is 0, errors affected by errorlevel are ignored
|
# If errorlevel is 0, errors affected by errorlevel are ignored
|
||||||
|
|
||||||
with self.check_context(arc.open(errorlevel=0), extracterror_filter):
|
with self.check_context(arc.open(errorlevel=0), extracterror_filter):
|
||||||
self.expect_file('file')
|
pass
|
||||||
|
|
||||||
with self.check_context(arc.open(errorlevel=0), filtererror_filter):
|
with self.check_context(arc.open(errorlevel=0), filtererror_filter):
|
||||||
self.expect_file('file')
|
pass
|
||||||
|
|
||||||
with self.check_context(arc.open(errorlevel=0), oserror_filter):
|
with self.check_context(arc.open(errorlevel=0), oserror_filter):
|
||||||
self.expect_file('file')
|
pass
|
||||||
|
|
||||||
with self.check_context(arc.open(errorlevel=0), tarerror_filter):
|
with self.check_context(arc.open(errorlevel=0), tarerror_filter):
|
||||||
self.expect_exception(tarfile.TarError)
|
self.expect_exception(tarfile.TarError)
|
||||||
|
@ -4235,7 +4509,7 @@ def valueerror_filter(tarinfo, path):
|
||||||
# If 1, all fatal errors are raised
|
# If 1, all fatal errors are raised
|
||||||
|
|
||||||
with self.check_context(arc.open(errorlevel=1), extracterror_filter):
|
with self.check_context(arc.open(errorlevel=1), extracterror_filter):
|
||||||
self.expect_file('file')
|
pass
|
||||||
|
|
||||||
with self.check_context(arc.open(errorlevel=1), filtererror_filter):
|
with self.check_context(arc.open(errorlevel=1), filtererror_filter):
|
||||||
self.expect_exception(tarfile.FilterError)
|
self.expect_exception(tarfile.FilterError)
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
Fixes multiple issues that allowed ``tarfile`` extraction filters
|
||||||
|
(``filter="data"`` and ``filter="tar"``) to be bypassed using crafted
|
||||||
|
symlinks and hard links.
|
||||||
|
|
||||||
|
Addresses :cve:`2024-12718`, :cve:`2025-4138`, :cve:`2025-4330`, and :cve:`2025-4517`.
|
||||||
|
|
Loading…
Reference in New Issue