From 5beddbe88363c93f2912320c18e31e55b2caaa49 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 14 Jun 2023 07:08:32 -0700 Subject: [PATCH] Add PEP 706 filters to tarfile (#10316) Fixes #10315 Co-authored-by: Sebastian Rittau --- stdlib/shutil.pyi | 5 +- stdlib/tarfile.pyi | 90 ++++++++++++++++++++++-- tests/stubtest_allowlists/py310.txt | 3 + tests/stubtest_allowlists/py311.txt | 3 + tests/stubtest_allowlists/py312.txt | 17 +---- tests/stubtest_allowlists/py38.txt | 3 + tests/stubtest_allowlists/py39.txt | 3 + tests/stubtest_allowlists/py3_common.txt | 15 ++++ 8 files changed, 116 insertions(+), 23 deletions(-) diff --git a/stdlib/shutil.pyi b/stdlib/shutil.pyi index ef716d404..38c50d51b 100644 --- a/stdlib/shutil.pyi +++ b/stdlib/shutil.pyi @@ -2,6 +2,7 @@ import os import sys from _typeshed import BytesPath, FileDescriptorOrPath, StrOrBytesPath, StrPath, SupportsRead, SupportsWrite from collections.abc import Callable, Iterable, Sequence +from tarfile import _TarfileFilter from typing import Any, AnyStr, NamedTuple, Protocol, TypeVar, overload from typing_extensions import TypeAlias @@ -192,9 +193,9 @@ def register_archive_format( ) -> None: ... def unregister_archive_format(name: str) -> None: ... -if sys.version_info >= (3, 12): +if sys.version_info >= (3, 8): def unpack_archive( - filename: StrPath, extract_dir: StrPath | None = None, format: str | None = None, *, filter: str | None = None + filename: StrPath, extract_dir: StrPath | None = None, format: str | None = None, *, filter: _TarfileFilter | None = None ) -> None: ... else: diff --git a/stdlib/tarfile.pyi b/stdlib/tarfile.pyi index 5cf1d55ca..d9d9641ac 100644 --- a/stdlib/tarfile.pyi +++ b/stdlib/tarfile.pyi @@ -7,7 +7,7 @@ from collections.abc import Callable, Iterable, Iterator, Mapping from gzip import _ReadableFileobj as _GzipReadableFileobj, _WritableFileobj as _GzipWritableFileobj from types import TracebackType from typing import IO, ClassVar, Protocol, overload -from typing_extensions import Literal, Self +from typing_extensions import Literal, Self, TypeAlias __all__ = [ "TarFile", @@ -26,6 +26,21 @@ __all__ = [ "DEFAULT_FORMAT", "open", ] +if sys.version_info >= (3, 12): + __all__ += [ + "fully_trusted_filter", + "data_filter", + "tar_filter", + "FilterError", + "AbsoluteLinkError", + "OutsideDestinationError", + "SpecialFileError", + "AbsolutePathError", + "LinkOutsideDestinationError", + ] + +_FilterFunction: TypeAlias = Callable[[TarInfo, str], TarInfo | None] +_TarfileFilter: TypeAlias = Literal["fully_trusted", "tar", "data"] | _FilterFunction class _Fileobj(Protocol): def read(self, __size: int) -> bytes: ... @@ -125,6 +140,7 @@ class TarFile: debug: int | None errorlevel: int | None offset: int # undocumented + extraction_filter: _FilterFunction | None def __init__( self, name: StrOrBytesPath | None = None, @@ -275,12 +291,32 @@ class TarFile: def getnames(self) -> _list[str]: ... def list(self, verbose: bool = True, *, members: _list[TarInfo] | None = None) -> None: ... def next(self) -> TarInfo | None: ... - def extractall( - self, path: StrOrBytesPath = ".", members: Iterable[TarInfo] | None = None, *, numeric_owner: bool = False - ) -> None: ... - def extract( - self, member: str | TarInfo, path: StrOrBytesPath = "", set_attrs: bool = True, *, numeric_owner: bool = False - ) -> None: ... + if sys.version_info >= (3, 8): + def extractall( + self, + path: StrOrBytesPath = ".", + members: Iterable[TarInfo] | None = None, + *, + numeric_owner: bool = False, + filter: _TarfileFilter | None = ..., + ) -> None: ... + def extract( + self, + member: str | TarInfo, + path: StrOrBytesPath = "", + set_attrs: bool = True, + *, + numeric_owner: bool = False, + filter: _TarfileFilter | None = ..., + ) -> None: ... + else: + def extractall( + self, path: StrOrBytesPath = ".", members: Iterable[TarInfo] | None = None, *, numeric_owner: bool = False + ) -> None: ... + def extract( + self, member: str | TarInfo, path: StrOrBytesPath = "", set_attrs: bool = True, *, numeric_owner: bool = False + ) -> None: ... + def _extract_member( self, tarinfo: TarInfo, targetpath: str, set_attrs: bool = True, numeric_owner: bool = False ) -> None: ... # undocumented @@ -324,6 +360,31 @@ class StreamError(TarError): ... class ExtractError(TarError): ... class HeaderError(TarError): ... +if sys.version_info >= (3, 8): + class FilterError(TarError): + # This attribute is only set directly on the subclasses, but the documentation guarantees + # that it is always present on FilterError. + tarinfo: TarInfo + + class AbsolutePathError(FilterError): + def __init__(self, tarinfo: TarInfo) -> None: ... + + class OutsideDestinationError(FilterError): + def __init__(self, tarinfo: TarInfo, path: str) -> None: ... + + class SpecialFileError(FilterError): + def __init__(self, tarinfo: TarInfo) -> None: ... + + class AbsoluteLinkError(FilterError): + def __init__(self, tarinfo: TarInfo) -> None: ... + + class LinkOutsideDestinationError(FilterError): + def __init__(self, tarinfo: TarInfo, path: str) -> None: ... + + def fully_trusted_filter(member: TarInfo, dest_path: str) -> TarInfo: ... + def tar_filter(member: TarInfo, dest_path: str) -> TarInfo: ... + def data_filter(member: TarInfo, dest_path: str) -> TarInfo: ... + class TarInfo: name: str path: str @@ -353,6 +414,21 @@ class TarInfo: def linkpath(self) -> str: ... @linkpath.setter def linkpath(self, linkname: str) -> None: ... + if sys.version_info >= (3, 8): + def replace( + self, + *, + name: str = ..., + mtime: int = ..., + mode: int = ..., + linkname: str = ..., + uid: int = ..., + gid: int = ..., + uname: str = ..., + gname: str = ..., + deep: bool = True, + ) -> Self: ... + def get_info(self) -> Mapping[str, str | int | bytes | Mapping[str, str]]: ... if sys.version_info >= (3, 8): def tobuf(self, format: int | None = 2, encoding: str | None = "utf-8", errors: str = "surrogateescape") -> bytes: ... diff --git a/tests/stubtest_allowlists/py310.txt b/tests/stubtest_allowlists/py310.txt index f68a49e4c..bb38e5691 100644 --- a/tests/stubtest_allowlists/py310.txt +++ b/tests/stubtest_allowlists/py310.txt @@ -208,3 +208,6 @@ asynchat.async_chat.use_encoding asynchat.find_prefix_at_end pkgutil.ImpImporter\..* pkgutil.ImpLoader\..* + +# Omit internal _KEEP argument +tarfile.TarInfo.replace diff --git a/tests/stubtest_allowlists/py311.txt b/tests/stubtest_allowlists/py311.txt index 2c54dae68..d6e1754b1 100644 --- a/tests/stubtest_allowlists/py311.txt +++ b/tests/stubtest_allowlists/py311.txt @@ -171,3 +171,6 @@ asynchat.async_chat.use_encoding asynchat.find_prefix_at_end pkgutil.ImpImporter\..* pkgutil.ImpLoader\..* + +# Omit internal _KEEP argument +tarfile.TarInfo.replace diff --git a/tests/stubtest_allowlists/py312.txt b/tests/stubtest_allowlists/py312.txt index 2d84be031..efbbe8c41 100644 --- a/tests/stubtest_allowlists/py312.txt +++ b/tests/stubtest_allowlists/py312.txt @@ -137,20 +137,6 @@ ssl.OP_LEGACY_SERVER_CONNECT ssl.Options.OP_LEGACY_SERVER_CONNECT ssl.RAND_pseudo_bytes ssl.wrap_socket -tarfile.AbsoluteLinkError -tarfile.AbsolutePathError -tarfile.FilterError -tarfile.LinkOutsideDestinationError -tarfile.OutsideDestinationError -tarfile.SpecialFileError -tarfile.TarFile.extract -tarfile.TarFile.extractall -tarfile.TarFile.extraction_filter -tarfile.TarInfo.replace -tarfile.__all__ -tarfile.data_filter -tarfile.fully_trusted_filter -tarfile.tar_filter turtle.RawTurtle.teleport turtle.TNavigator.teleport turtle.TPen.teleport @@ -336,3 +322,6 @@ typing_extensions\.Final typing\.NamedTuple typing\.LiteralString typing\.Annotated + +# Omit internal _KEEP argument +tarfile.TarInfo.replace diff --git a/tests/stubtest_allowlists/py38.txt b/tests/stubtest_allowlists/py38.txt index 1e9ca5638..1d164a9b9 100644 --- a/tests/stubtest_allowlists/py38.txt +++ b/tests/stubtest_allowlists/py38.txt @@ -202,3 +202,6 @@ asynchat.async_chat.use_encoding asynchat.find_prefix_at_end pkgutil.ImpImporter\..* pkgutil.ImpLoader\..* + +# Omit internal _KEEP argument +tarfile.TarInfo.replace diff --git a/tests/stubtest_allowlists/py39.txt b/tests/stubtest_allowlists/py39.txt index 277efd02f..def53fd29 100644 --- a/tests/stubtest_allowlists/py39.txt +++ b/tests/stubtest_allowlists/py39.txt @@ -200,3 +200,6 @@ asynchat.async_chat.use_encoding asynchat.find_prefix_at_end pkgutil.ImpImporter\..* pkgutil.ImpLoader\..* + +# Omit internal _KEEP argument +tarfile.TarInfo.replace diff --git a/tests/stubtest_allowlists/py3_common.txt b/tests/stubtest_allowlists/py3_common.txt index ccc4eda51..dd0b06e3c 100644 --- a/tests/stubtest_allowlists/py3_common.txt +++ b/tests/stubtest_allowlists/py3_common.txt @@ -613,3 +613,18 @@ typing.IO.__iter__ # See https://github.com/python/typeshed/commit/97bc450acd60 # but have yet to find their way to all GitHub Actions images (sys.get_int_max_str_digits)? (sys.set_int_max_str_digits)? + +# Added or modified in a patch release, backported to all security branches, +# but have yet to find their way to all GitHub Actions images +(tarfile.tar_filter)? +(tarfile.fully_trusted_filter)? +(tarfile.data_filter)? +(tarfile.TarFile.extractall)? +(tarfile.TarFile.extract)? +(tarfile.SpecialFileError)? +(tarfile.OutsideDestinationError)? +(tarfile.LinkOutsideDestinationError)? +(tarfile.FilterError)? +(tarfile.AbsolutePathError)? +(tarfile.AbsoluteLinkError)? +(shutil.unpack_archive)?