Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-backports.zstd for 
openSUSE:Factory checked in at 2026-09-12 21:15:22
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-backports.zstd (Old)
 and      /work/SRC/openSUSE:Factory/.python-backports.zstd.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-backports.zstd"

Sat Sep 12 21:15:22 2026 rev:6 rq:1377344 version:1.7.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-backports.zstd/python-backports.zstd.changes  
    2026-07-02 20:15:13.369366127 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-backports.zstd.new.1265/python-backports.zstd.changes
    2026-09-12 21:15:59.572078187 +0200
@@ -1,0 +2,6 @@
+Thu Sep 10 07:22:43 UTC 2026 - Markéta Machová <[email protected]>
+
+- update to 1.7.0
+  * Update code with CPython 3.14.7 version
+
+-------------------------------------------------------------------

Old:
----
  backports_zstd-1.6.0.tar.gz

New:
----
  backports_zstd-1.7.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-backports.zstd.spec ++++++
--- /var/tmp/diff_new_pack.p12HPa/_old  2026-09-12 21:16:00.698125018 +0200
+++ /var/tmp/diff_new_pack.p12HPa/_new  2026-09-12 21:16:00.699125059 +0200
@@ -21,7 +21,7 @@
 %define skip_python315 1
 %{?sle15_python_module_pythons}
 Name:           python-backports.zstd
-Version:        1.6.0
+Version:        1.7.0
 Release:        0
 Summary:        Backport of compressionzstd
 License:        BSD-3-Clause

++++++ backports_zstd-1.6.0.tar.gz -> backports_zstd-1.7.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/backports_zstd-1.6.0/PKG-INFO 
new/backports_zstd-1.7.0/PKG-INFO
--- old/backports_zstd-1.6.0/PKG-INFO   2026-06-14 11:30:21.372915700 +0200
+++ new/backports_zstd-1.7.0/PKG-INFO   2026-08-15 18:14:35.708436300 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: backports.zstd
-Version: 1.6.0
+Version: 1.7.0
 Summary: Backport of compression.zstd
 Author-email: Rogdham <[email protected]>
 License-Expression: PSF-2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/backports_zstd-1.6.0/pyproject.toml 
new/backports_zstd-1.7.0/pyproject.toml
--- old/backports_zstd-1.6.0/pyproject.toml     2026-06-14 11:30:13.000000000 
+0200
+++ new/backports_zstd-1.7.0/pyproject.toml     2026-08-15 18:14:26.003348400 
+0200
@@ -1,6 +1,6 @@
 [project]
 name = "backports.zstd"
-version = "1.6.0"
+version = "1.7.0"
 authors = [{ name = "Rogdham", email = "[email protected]" }]
 description = "Backport of compression.zstd"
 readme = { file = "README.md", content-type = "text/markdown" }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/backports_zstd-1.6.0/src/c/compression_zstd/compressor.c 
new/backports_zstd-1.7.0/src/c/compression_zstd/compressor.c
--- old/backports_zstd-1.6.0/src/c/compression_zstd/compressor.c        
2026-06-14 11:30:13.000000000 +0200
+++ new/backports_zstd-1.7.0/src/c/compression_zstd/compressor.c        
2026-08-15 18:14:26.004533800 +0200
@@ -349,6 +349,7 @@
     }
 
     self->use_multithread = 0;
+    self->compression_level = ZSTD_CLEVEL_DEFAULT;
     self->dict = NULL;
     self->lock = BACKPORTSZSTD_LOCK_allocate();
     if(BACKPORTSZSTD_LOCK_isError(self->lock)) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/backports_zstd-1.6.0/src/c/compression_zstd/decompressor.c 
new/backports_zstd-1.7.0/src/c/compression_zstd/decompressor.c
--- old/backports_zstd-1.6.0/src/c/compression_zstd/decompressor.c      
2026-06-14 11:30:13.000000000 +0200
+++ new/backports_zstd-1.7.0/src/c/compression_zstd/decompressor.c      
2026-08-15 18:14:26.004533800 +0200
@@ -605,9 +605,14 @@
     }
     else {
         if (self->unused_data == NULL) {
-            self->unused_data = PyBytes_FromStringAndSize(
-                                    self->input_buffer + self->in_begin,
-                                    self->in_end - self->in_begin);
+            if (self->input_buffer == NULL) {
+                self->unused_data = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
+            }
+            else {
+                self->unused_data = PyBytes_FromStringAndSize(
+                                        self->input_buffer + self->in_begin,
+                                        self->in_end - self->in_begin);
+            }
             ret = self->unused_data;
             Py_XINCREF(ret);
         }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/backports_zstd-1.6.0/src/python/backports/zstd/__init__.pyi 
new/backports_zstd-1.7.0/src/python/backports/zstd/__init__.pyi
--- old/backports_zstd-1.6.0/src/python/backports/zstd/__init__.pyi     
2026-06-14 11:30:13.000000000 +0200
+++ new/backports_zstd-1.7.0/src/python/backports/zstd/__init__.pyi     
2026-08-15 18:14:26.005311300 +0200
@@ -76,6 +76,7 @@
 def decompress(
     data: ReadableBuffer, zstd_dict: ZstdDict | tuple[ZstdDict, int] | None = 
None, options: Mapping[int, int] | None = None
 ) -> bytes: ...
+
 @final
 class CompressionParameter(enum.IntEnum):
     compression_level = ...
@@ -165,6 +166,7 @@
         options: Mapping[int, int] | None = None,
         zstd_dict: ZstdDict | tuple[ZstdDict, int] | None = None,
     ) -> None: ...
+
     def write(self, data: ReadableBuffer, /) -> int: ...
     def flush(self, mode: _ZstdCompressorFlushBlock | 
_ZstdCompressorFlushFrame = 1) -> bytes: ...  # type: ignore[override]
     def read(self, size: int | None = -1) -> bytes: ...
@@ -274,7 +276,7 @@
 
 @final
 class ZstdDict:
-    def __new__(cls, dict_content: bytes, /, *, is_raw: bool = False) -> Self: 
...
+    def __new__(cls, dict_content: ReadableBuffer, /, *, is_raw: bool = False) 
-> Self: ...
     def __len__(self, /) -> int: ...
     @property
     def as_digested_dict(self) -> tuple[Self, int]: ...
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/backports_zstd-1.6.0/src/python/backports/zstd/_cffi/compressor.py 
new/backports_zstd-1.7.0/src/python/backports/zstd/_cffi/compressor.py
--- old/backports_zstd-1.6.0/src/python/backports/zstd/_cffi/compressor.py      
2026-06-14 11:30:13.000000000 +0200
+++ new/backports_zstd-1.7.0/src/python/backports/zstd/_cffi/compressor.py      
2026-08-15 18:14:26.005464600 +0200
@@ -55,7 +55,7 @@
     FLUSH_FRAME = _lib.ZSTD_e_end
 
     def __init__(self, level=None, options=None, zstd_dict=None):
-        self._compression_level = 0
+        self._compression_level = _lib.ZSTD_defaultCLevel()
         self._use_multithread = False
         self._lock = Lock()
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/backports_zstd-1.6.0/src/python/backports/zstd/_zstdfile.py 
new/backports_zstd-1.7.0/src/python/backports/zstd/_zstdfile.py
--- old/backports_zstd-1.6.0/src/python/backports/zstd/_zstdfile.py     
2026-06-14 11:30:13.000000000 +0200
+++ new/backports_zstd-1.7.0/src/python/backports/zstd/_zstdfile.py     
2026-08-15 18:14:26.005464600 +0200
@@ -34,7 +34,7 @@
                  level=None, options=None, zstd_dict=None):
         """Open a Zstandard compressed file in binary mode.
 
-        *file* can be either an file-like object, or a file name to open.
+        *file* can be either a file-like object, or a file name to open.
 
         *mode* can be 'r' for reading (default), 'w' for (over)writing, 'x'
         for creating exclusively, or 'a' for appending.  These can
@@ -246,6 +246,7 @@
         return self._buffer.peek(size)
 
     def __next__(self):
+        self._check_can_read()
         if ret := self._buffer.readline():
             return ret
         raise StopIteration
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/backports_zstd-1.6.0/src/python/backports/zstd/tarfile.py 
new/backports_zstd-1.7.0/src/python/backports/zstd/tarfile.py
--- old/backports_zstd-1.6.0/src/python/backports/zstd/tarfile.py       
2026-06-14 11:30:13.000000000 +0200
+++ new/backports_zstd-1.7.0/src/python/backports/zstd/tarfile.py       
2026-08-15 18:14:26.005464600 +0200
@@ -269,6 +269,32 @@
         dst.write(buf)
     return
 
+# Maximum number of bytes read in a single call when reading a member's
+# extended header (a GNU long name/link or a pax header).  The size of such
+# a header is taken from the archive and is not trustworthy, so it is read in
+# bounded chunks to avoid a huge up-front allocation when a crafted or
+# truncated archive claims far more data than the file actually contains
+# (gh-151497).
+_EXTHEADER_READ_CHUNK = 1024 * 1024  # 1 MiB
+
+def _safe_read(fileobj, size):
+    """Read up to *size* bytes from *fileobj* in bounded chunks.
+
+    Returns the same bytes as ``fileobj.read(size)`` would (including a short
+    result at end of file), but limits pre-allocation, so an
+    oversized size field in a crafted header cannot force a huge allocation.
+    """
+    if size <= _EXTHEADER_READ_CHUNK:
+        return fileobj.read(size)
+    chunks = []
+    while size > 0:
+        chunk = fileobj.read(min(size, _EXTHEADER_READ_CHUNK))
+        if not chunk:
+            break
+        chunks.append(chunk)
+        size -= len(chunk)
+    return b"".join(chunks)
+
 def _safe_print(s):
     encoding = getattr(sys.stdout, 'encoding', None)
     if encoding is not None:
@@ -532,7 +558,9 @@
         if pos - self.pos >= 0:
             blocks, remainder = divmod(pos - self.pos, self.bufsize)
             for i in range(blocks):
-                self.read(self.bufsize)
+                data = self.read(self.bufsize)
+                if not data:
+                    break
             self.read(remainder)
         else:
             raise StreamError("seeking backwards is not allowed")
@@ -1435,7 +1463,7 @@
         """Process the blocks that hold a GNU longname
            or longlink member.
         """
-        buf = tarfile.fileobj.read(self._block(self.size))
+        buf = _safe_read(tarfile.fileobj, self._block(self.size))
 
         # Fetch the next header and process it.
         try:
@@ -1491,7 +1519,7 @@
            POSIX.1-2008.
         """
         # Read the header information.
-        buf = tarfile.fileobj.read(self._block(self.size))
+        buf = _safe_read(tarfile.fileobj, self._block(self.size))
 
         # A pax header stores supplemental information for either
         # the following file (extended) or all following files
@@ -2513,7 +2541,8 @@
         tarinfo, unfiltered = self._get_extract_tarinfo(
             member, filter_function, path)
         if tarinfo is not None:
-            self._extract_one(tarinfo, path, set_attrs, numeric_owner)
+            self._extract_one(tarinfo, path, set_attrs, numeric_owner,
+                              filter_function=filter_function)
 
     def _get_extract_tarinfo(self, member, filter_function, path):
         """Get (filtered, unfiltered) TarInfos from *member*
@@ -2785,6 +2814,9 @@
                     "makelink_with_filter: if filter_function is not None, "
                     + "extraction_root must also not be None")
             try:
+                filter_function(
+                    unfiltered.replace(name=tarinfo.name, deep=False),
+                    extraction_root)
                 filtered = filter_function(unfiltered, extraction_root)
             except _FILTER_ERRORS as cause:
                 raise LinkFallbackError(tarinfo, unfiltered.name) from cause
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/backports_zstd-1.6.0/src/python/backports/zstd/tarfile.pyi 
new/backports_zstd-1.7.0/src/python/backports/zstd/tarfile.pyi
--- old/backports_zstd-1.6.0/src/python/backports/zstd/tarfile.pyi      
2026-06-14 11:30:13.000000000 +0200
+++ new/backports_zstd-1.7.0/src/python/backports/zstd/tarfile.pyi      
2026-08-15 18:14:26.005656200 +0200
@@ -5,8 +5,8 @@
 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, Final, Literal, Protocol, overload, 
type_check_only
-from typing_extensions import Self, TypeAlias, deprecated
+from typing import IO, ClassVar, Final, Literal, Protocol, TypeAlias, 
overload, type_check_only
+from typing_extensions import Self, deprecated
 
 from backports.zstd import ZstdDict
 
@@ -126,6 +126,7 @@
     errorlevel: Literal[0, 1, 2]
     offset: int  # undocumented
     extraction_filter: _FilterFunction | None
+    stream: bool
     def __init__(
         self,
         name: StrOrBytesPath | None = None,
@@ -141,6 +142,7 @@
         debug: Literal[0, 1, 2, 3] | None = None,  # default 0
         errorlevel: Literal[0, 1, 2] | None = None,  # default 1
         copybufsize: int | None = None,  # undocumented
+        stream: bool = False,
     ) -> None: ...
 
     def __enter__(self) -> Self: ...
@@ -167,6 +169,7 @@
         debug: Literal[0, 1, 2, 3] | None = None,  # default 0
         errorlevel: Literal[0, 1, 2] | None = None,  # default 1
     ) -> Self: ...
+
     @overload
     @classmethod
     def open(
@@ -467,6 +470,7 @@
         errorlevel: Literal[0, 1, 2] | None = None,  # default 1
         compresslevel: int = 9,
     ) -> Self: ...
+
     @classmethod
     def taropen(
         cls,
@@ -484,6 +488,7 @@
         debug: Literal[0, 1, 2, 3] | None = None,  # default 0
         errorlevel: Literal[0, 1, 2] | None = None,  # default 1
     ) -> Self: ...
+
     @overload
     @classmethod
     def gzopen(
@@ -520,6 +525,7 @@
         debug: Literal[0, 1, 2, 3] | None = None,  # default 0
         errorlevel: Literal[0, 1, 2] | None = None,  # default 1
     ) -> Self: ...
+
     @overload
     @classmethod
     def bz2open(
@@ -556,6 +562,7 @@
         debug: Literal[0, 1, 2, 3] | None = None,  # default 0
         errorlevel: Literal[0, 1, 2] | None = None,  # default 1
     ) -> Self: ...
+
     @classmethod
     def xzopen(
         cls,
@@ -756,21 +763,24 @@
     gname: str
     pax_headers: Mapping[str, str]
     def __init__(self, name: str = "") -> None: ...
+
     @property
-    @deprecated("Deprecated since Python 3.13; will be removed in Python 
3.16.")
+    @deprecated("Deprecated; will be removed in Python 3.16.")
     def tarfile(self) -> TarFile | None: ...
     @tarfile.setter
-    @deprecated("Deprecated since Python 3.13; will be removed in Python 
3.16.")
+    @deprecated("Deprecated; will be removed in Python 3.16.")
     def tarfile(self, tarfile: TarFile | None) -> None: ...
 
     @classmethod
     def frombuf(cls, buf: bytes | bytearray, encoding: str, errors: str) -> 
Self: ...
     @classmethod
     def fromtarfile(cls, tarfile: TarFile) -> Self: ...
+
     @property
     def linkpath(self) -> str: ...
     @linkpath.setter
     def linkpath(self, linkname: str) -> None: ...
+
     def replace(
         self,
         *,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/backports_zstd-1.6.0/src/python/backports/zstd/zipfile/__init__.pyi 
new/backports_zstd-1.7.0/src/python/backports/zstd/zipfile/__init__.pyi
--- old/backports_zstd-1.6.0/src/python/backports/zstd/zipfile/__init__.pyi     
2026-06-14 11:30:13.000000000 +0200
+++ new/backports_zstd-1.7.0/src/python/backports/zstd/zipfile/__init__.pyi     
2026-08-15 18:14:26.007081700 +0200
@@ -4,8 +4,8 @@
 from io import TextIOWrapper
 from os import PathLike
 from types import TracebackType
-from typing import IO, Final, Literal, Protocol, overload, type_check_only
-from typing_extensions import Self, TypeAlias
+from typing import IO, Final, Literal, Protocol, TypeAlias, overload, 
type_check_only
+from typing_extensions import Self
 
 __all__ = [
     "BadZipFile",
@@ -64,6 +64,7 @@
     newlines: list[bytes] | None
     mode: _ReadWriteMode
     name: str
+
     @overload
     def __init__(
         self, fileobj: _ClosableZipStream, mode: _ReadWriteMode, zipinfo: 
ZipInfo, pwd: bytes | None, close_fileobj: Literal[True]
@@ -87,6 +88,7 @@
         pwd: bytes | None = None,
         close_fileobj: Literal[False] = False,
     ) -> None: ...
+
     def read(self, n: int | None = -1) -> bytes: ...
     def readline(self, limit: int = -1) -> bytes: ...  # type: ignore[override]
     def peek(self, n: int = 1) -> bytes: ...
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/backports_zstd-1.6.0/src/python/backports/zstd/zipfile/_path/__init__.pyi 
new/backports_zstd-1.7.0/src/python/backports/zstd/zipfile/_path/__init__.pyi
--- 
old/backports_zstd-1.6.0/src/python/backports/zstd/zipfile/_path/__init__.pyi   
    2026-06-14 11:30:13.000000000 +0200
+++ 
new/backports_zstd-1.7.0/src/python/backports/zstd/zipfile/_path/__init__.pyi   
    2026-08-15 18:14:26.007291600 +0200
@@ -17,12 +17,14 @@
 
 class CompleteDirs(InitializedState, ZipFile):
     def resolve_dir(self, name: str) -> str: ...
+
     @overload
     @classmethod
     def make(cls, source: ZipFile) -> CompleteDirs: ...
     @overload
     @classmethod
     def make(cls, source: StrPath | IO[bytes]) -> Self: ...
+
     @classmethod
     def inject(cls, zf: _ZF) -> _ZF: ...
 
@@ -42,6 +44,7 @@
     def suffixes(self) -> list[str]: ...
     @property
     def stem(self) -> str: ...
+
     @overload
     def open(
         self,
@@ -56,6 +59,7 @@
     ) -> TextIOWrapper: ...
     @overload
     def open(self, mode: Literal["rb", "wb"], *, pwd: bytes | None = None) -> 
IO[bytes]: ...
+
     def iterdir(self) -> Iterator[Self]: ...
     def is_dir(self) -> bool: ...
     def is_file(self) -> bool: ...
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/backports_zstd-1.6.0/src/python/backports.zstd.egg-info/PKG-INFO 
new/backports_zstd-1.7.0/src/python/backports.zstd.egg-info/PKG-INFO
--- old/backports_zstd-1.6.0/src/python/backports.zstd.egg-info/PKG-INFO        
2026-06-14 11:30:21.000000000 +0200
+++ new/backports_zstd-1.7.0/src/python/backports.zstd.egg-info/PKG-INFO        
2026-08-15 18:14:35.629517300 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: backports.zstd
-Version: 1.6.0
+Version: 1.7.0
 Summary: Backport of compression.zstd
 Author-email: Rogdham <[email protected]>
 License-Expression: PSF-2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/backports_zstd-1.6.0/tests/test/support/__init__.py 
new/backports_zstd-1.7.0/tests/test/support/__init__.py
--- old/backports_zstd-1.6.0/tests/test/support/__init__.py     2026-06-14 
11:30:13.000000000 +0200
+++ new/backports_zstd-1.7.0/tests/test/support/__init__.py     2026-08-15 
18:14:26.007819200 +0200
@@ -35,7 +35,7 @@
     "requires_gil_enabled", "requires_linux_version", "requires_mac_ver",
     "check_syntax_error",
     "requires_gzip", "requires_bz2", "requires_lzma", "requires_zstd",
-    "bigmemtest", "bigaddrspacetest", "cpython_only", "get_attribute",
+    "bigmemtest", "nomemtest", "bigaddrspacetest", "cpython_only", 
"get_attribute",
     "requires_IEEE_754", "requires_zlib",
     "has_fork_support", "requires_fork",
     "has_subprocess_support", "requires_subprocess",
@@ -44,7 +44,8 @@
     "check__all__", "skip_if_buggy_ucrt_strfptime",
     "check_disallow_instantiation", "check_sanitizer", "skip_if_sanitizer",
     "requires_limited_api", "requires_specialization", "thread_unsafe",
-    "skip_if_unlimited_stack_size",
+    "skip_if_unlimited_stack_size", "skip_if_huge_c_stack",
+    "run_with_limited_c_stack",
     # sys
     "MS_WINDOWS", "is_jython", "is_android", "is_emscripten", "is_wasi",
     "is_apple_mobile", "check_impl_detail", "unix_shell", "setswitchinterval",
@@ -1222,6 +1223,22 @@
         return wrapper
     return decorator
 
+def nomemtest(f):
+    """Check that we can use this test with `_testcapi.set_nomemory`."""
+    from .import_helper import import_module
+
+    @functools.wraps(f)
+    def internal(*args, **kwargs):
+        import_module('_testcapi')
+        return f(*args, **kwargs)
+
+    return unittest.skipIf(
+        # Python built with Py_TRACE_REFS fail with a fatal error in
+        # _PyRefchain_Trace() on memory allocation error.
+        Py_TRACE_REFS,
+        'cannot test Py_TRACE_REFS build',
+    )(cpython_only(internal))
+
 def bigaddrspacetest(f):
     """Decorator for tests that fill the address space."""
     def wrapper(self):
@@ -2723,6 +2740,96 @@
     return 150_000
 
 
+def _has_huge_c_stack(depth):
+    """Check that *depth* recursive calls cannot exhaust the C stack."""
+    try:
+        from _testinternalcapi import get_c_recursion_remaining
+    except ImportError:
+        # Fall back to checking for an unlimited stack size.
+        if is_emscripten or is_wasi or os.name == "nt":
+            return False
+        import resource
+        soft, hard = resource.getrlimit(resource.RLIMIT_STACK)
+        return soft == hard and soft in (-1, 0xFFFF_FFFF_FFFF_FFFF)
+    else:
+        remaining = get_c_recursion_remaining()
+        # A negative value means integer overflow in the estimate
+        # (e.g. with an unlimited RLIMIT_STACK).  The estimate is based on
+        # the size of the interpreter loop frame, so it is only a lower
+        # bound for recursion with smaller C frames.
+        return remaining >= depth or remaining < 0
+
+
+def skip_if_huge_c_stack(depth=150_000):
+    """Skip decorator for tests which cannot overflow the C stack.
+
+    Tests exhausting the C stack with *depth* recursive calls cannot
+    trigger the recursion protection if the C stack is too large (e.g.
+    with a large or unlimited RLIMIT_STACK), and either fail, or run
+    for a very long time, or crash, or consume all memory.
+
+    Prefer run_with_limited_c_stack() for tests recursing to a fixed depth.
+    """
+    return unittest.skipIf(
+        _has_huge_c_stack(depth),
+        f"the C stack is large enough for {depth} recursive calls")
+
+
+# Small enough to be exhausted by tens of thousands of recursive calls,
+# but not smaller than Py_C_STACK_SIZE (4 MiB) which the interpreter
+# assumes if it cannot query the thread stack size.
+C_STACK_SIZE = 8 * 1024 * 1024
+
+
+def run_with_limited_c_stack(depth=150_000, size=C_STACK_SIZE):
+    """Decorator for tests exhausting the C stack with *depth* recursive calls.
+
+    Run the test in a separate thread with the C stack of *size* bytes, so
+    that the outcome does not depend on the C stack size of the main thread
+    (which can be large or unlimited, see RLIMIT_STACK).
+
+    If a thread with the limited C stack cannot be created, run the test in
+    the current thread, but skip it if the C stack is too large.
+    """
+    reason = f"the C stack is large enough for {depth} recursive calls"
+    def decorator(test):
+        @functools.wraps(test)
+        def wrapper(*args, **kwargs):
+            def run_test():
+                # The C stack can still be too large if limiting it failed.
+                if _has_huge_c_stack(depth):
+                    raise unittest.SkipTest(reason)
+                test(*args, **kwargs)
+
+            try:
+                import threading
+                old_size = threading.stack_size(size)
+            except (ImportError, ValueError, RuntimeError):
+                # Setting the thread stack size is not supported.
+                return run_test()
+
+            exceptions = []
+            def run():
+                try:
+                    run_test()
+                except BaseException as exc:
+                    exceptions.append(exc)
+
+            thread = threading.Thread(target=run)
+            try:
+                thread.start()
+            except RuntimeError:
+                # Threads are not supported.
+                return run_test()
+            finally:
+                threading.stack_size(old_size)
+            thread.join()
+            if exceptions:
+                raise exceptions[0]
+        return wrapper
+    return decorator
+
+
 # Windows doesn't have os.uname() but it doesn't support s390x.
 is_s390x = hasattr(os, 'uname') and os.uname().machine == 's390x'
 skip_on_s390x = unittest.skipIf(is_s390x, 'skipped on s390x')
@@ -3072,7 +3179,7 @@
         with open("/run/systemd/container", "rb") as fp:
             if fp.read().rstrip() != b"systemd-nspawn":
                 return False
-    except FileNotFoundError:
+    except (FileNotFoundError, PermissionError):
         return False
 
     # If systemd-nspawn is used, O_SYNC flag will immediately
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/backports_zstd-1.6.0/tests/test/support/os_helper.py 
new/backports_zstd-1.7.0/tests/test/support/os_helper.py
--- old/backports_zstd-1.6.0/tests/test/support/os_helper.py    2026-06-14 
11:30:13.000000000 +0200
+++ new/backports_zstd-1.7.0/tests/test/support/os_helper.py    2026-08-15 
18:14:26.009343900 +0200
@@ -467,12 +467,23 @@
 
         def _rmtree_inner(path):
             from test.support import _force_run
+            # Clear file flags (e.g. UF_IMMUTABLE, UF_NOUNLINK on BSD).
+            if hasattr(os, 'chflags'):
+                try:
+                    os.chflags(path, 0)
+                except OSError:
+                    pass
             for name in _force_run(path, os.listdir, path):
                 fullname = os.path.join(path, name)
                 try:
                     mode = os.lstat(fullname).st_mode
                 except OSError:
                     mode = 0
+                if hasattr(os, 'lchflags'):
+                    try:
+                        os.lchflags(fullname, 0)
+                    except OSError:
+                        pass
                 if stat.S_ISDIR(mode):
                     _rmtree_inner(fullname)
                     _force_run(path, os.rmdir, fullname)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/backports_zstd-1.6.0/tests/test/test_tarfile.py 
new/backports_zstd-1.7.0/tests/test/test_tarfile.py
--- old/backports_zstd-1.6.0/tests/test/test_tarfile.py 2026-06-14 
11:30:13.000000000 +0200
+++ new/backports_zstd-1.7.0/tests/test/test_tarfile.py 2026-08-15 
18:14:26.009343900 +0200
@@ -560,6 +560,53 @@
             self.assertIs(fobj.seekable(), True)
 
 
+class ReadSizeRecorder(io.BytesIO):
+    # Records the largest size ever passed to read(), so a test can check
+    # that tarfile does not request far more data than the archive holds
+    # (which on a real file would pre-allocate it).
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.max_read_size = 0
+
+    def read(self, size=-1):
+        if size is not None and size >= 0:
+            self.max_read_size = max(self.max_read_size, size)
+        return super().read(size)
+
+
[email protected]_only
+class ExtendedHeaderMemoryTest(unittest.TestCase):
+    # gh-151497: the size of a GNU long name/link or a pax extended header is
+    # read from the archive and is untrusted.  A crafted header can claim a
+    # size far larger than the file actually contains; opening such an archive
+    # must not try to read (and so pre-allocate) the claimed size in one go.
+
+    def crafted_archive(self, hdrtype):
+        tarinfo = tarfile.TarInfo("A")
+        tarinfo.type = hdrtype
+        tarinfo.size = 0xFFFFFFFF  # ~4 GiB claimed in a 512-byte header
+        return tarinfo.tobuf(format=tarfile.GNU_FORMAT)
+
+    def check(self, hdrtype):
+        fobj = ReadSizeRecorder(self.crafted_archive(hdrtype))
+        try:
+            with tarfile.open(fileobj=fobj, mode="r:") as tar:
+                tar.getmembers()
+        except tarfile.ReadError:
+            pass  # a truncated header is fine; we only check the allocation
+        # The bogus ~4 GiB size must never reach a single read() call.
+        self.assertLessEqual(fobj.max_read_size, tarfile._EXTHEADER_READ_CHUNK)
+
+    def test_gnu_longname_oversized_size(self):
+        self.check(tarfile.GNUTYPE_LONGNAME)
+
+    def test_gnu_longlink_oversized_size(self):
+        self.check(tarfile.GNUTYPE_LONGLINK)
+
+    def test_pax_header_oversized_size(self):
+        self.check(tarfile.XHDTYPE)
+
+
 class MiscReadTestBase(CommonReadTest):
     is_stream = False
 
@@ -4363,6 +4410,30 @@
                     self.expect_file("c", symlink_to='b')
 
     @symlink_test
+    def test_sneaky_hardlink_fallback_deep(self):
+        # (CVE-2026-11940)
+        with ArchiveMaker() as arc:
+            arc.add("a/b/s", symlink_to=os.path.join("..", "escape"))
+            arc.add("s", hardlink_to=os.path.join("a", "b", "s"))
+
+        with self.check_context(arc.open(), 'data'):
+            e = self.expect_exception(
+                tarfile.LinkFallbackError,
+                "link 's' would be extracted as a copy of "
+                + "'a/b/s', 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/")
+                    self.expect_file("a/b/")
+                else:
+                    self.expect_file("a/b/s", symlink_to=os.path.join('..', 
'escape'))
+                    self.expect_file("s", symlink_to=os.path.join('..', 
'escape'))
+
+    @symlink_test
     def test_exfiltration_via_symlink(self):
         # (CVE-2025-4138)
         # Test changing symlinks that result in a symlink pointing outside
@@ -4417,6 +4488,98 @@
                     st_mode = cc.outerdir.stat().st_mode
                     self.assertNotEqual(st_mode & 0o777, 0o777)
 
+    @symlink_test
+    @unittest.skipUnless(hasattr(os, 'chown'), "missing os.chown")
+    @unittest.skipUnless(hasattr(os, 'lchown'), "missing os.lchown")
+    @unittest.skipUnless(hasattr(os, 'geteuid'), "missing os.geteuid")
+    @support.subTests('link_type', (tarfile.SYMTYPE, tarfile.LNKTYPE))
+    def test_chown_links_on_extract(self, link_type):
+        with ArchiveMaker() as arc:
+            arc.add("test.txt",
+                    uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
+            arc.add("link",
+                    type=link_type,
+                    linkname='test.txt',
+                    uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
+
+        with (
+            os_helper.temp_dir() as tmpdir,
+            arc.open() as tar,
+            unittest.mock.patch("os.chown") as mock_chown,
+            unittest.mock.patch("os.lchown") as mock_lchown,
+            unittest.mock.patch("os.geteuid") as mock_geteuid,
+        ):
+            # Set UID to 0 so chown() is attempted.
+            mock_geteuid.return_value = 0
+            tar.extract("link", path=tmpdir, filter='data')
+            extract_path = os.path.join(tmpdir, "link")
+
+            if link_type == tarfile.SYMTYPE:
+                mock_chown.assert_not_called()
+                mock_lchown.assert_called_once_with(extract_path, -1, -1)
+            else:
+                mock_chown.assert_has_calls([
+                    unittest.mock.call(extract_path, -1, -1),
+                    unittest.mock.call(extract_path, -1, -1)
+                ])
+                mock_lchown.assert_not_called()
+
+    @symlink_test
+    @unittest.skipUnless(hasattr(os, 'chown'), "missing os.chown")
+    @unittest.skipUnless(hasattr(os, 'lchown'), "missing os.lchown")
+    @unittest.skipUnless(hasattr(os, 'geteuid'), "missing os.geteuid")
+    @support.subTests('link_type', (tarfile.SYMTYPE, tarfile.LNKTYPE))
+    def test_chown_links_on_extractall(self, link_type):
+        with ArchiveMaker() as arc:
+            arc.add("test.txt",
+                    uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
+            arc.add("link",
+                    type=link_type,
+                    linkname='test.txt',
+                    uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
+
+        with (
+            os_helper.temp_dir() as tmpdir,
+            arc.open() as tar,
+            unittest.mock.patch("os.chown") as mock_chown,
+            unittest.mock.patch("os.lchown") as mock_lchown,
+            unittest.mock.patch("os.geteuid") as mock_geteuid,
+        ):
+            # Set UID to 0 so chown() is attempted.
+            mock_geteuid.return_value = 0
+            tar.extractall(path=tmpdir, filter='data')
+            extract_link_path = os.path.join(tmpdir, "link")
+            extract_file_path = os.path.join(tmpdir, "test.txt")
+
+            if link_type == tarfile.SYMTYPE:
+                mock_chown.assert_called_once_with(extract_file_path, -1, -1)
+                mock_lchown.assert_called_once_with(extract_link_path, -1, -1)
+            else:
+                mock_chown.assert_has_calls([
+                    unittest.mock.call(extract_file_path, -1, -1),
+                    unittest.mock.call(extract_link_path, -1, -1)
+                ])
+                mock_lchown.assert_not_called()
+
+    def test_extract_filters_target(self):
+        # Test that when extract() falls back to extracting (rather than
+        # linking) a hardlink target, it filters the target.
+        with ArchiveMaker() as arc:
+            arc.add("target")
+            arc.add("link", hardlink_to="target")
+        def testing_filter(member, path):
+            if member.name == 'target':
+                # target: set read-only
+                return member.replace(mode=stat.S_IRUSR)
+            # link: don't overwrite the mode
+            return member.replace(mode=None)
+        tempdir = pathlib.Path(TEMPDIR) / 'extract'
+        with os_helper.temp_dir(tempdir), arc.open() as tar:
+            tar.extract("link", path=tempdir, filter=testing_filter)
+            path = tempdir / 'link'
+            if os_helper.can_chmod():
+                self.assertFalse(path.stat().st_mode & stat.S_IWUSR)
+
     def test_link_fallback_normalizes(self):
         # Make sure hardlink fallbacks work for non-normalized paths for all
         # filters
@@ -4781,6 +4944,22 @@
         with self.check_context(arc.open(errorlevel='boo!'), 
filtererror_filter):
             self.expect_exception(TypeError)  # errorlevel is not int
 
+    @support.subTests('format', [tarfile.GNU_FORMAT, tarfile.PAX_FORMAT])
+    def test_getmembers_big_size(self, format):
+        # gh-151981: A loop in seek() for streaming files tried to read the
+        # declared number of blocks even at EOF
+        tinfo = tarfile.TarInfo("huge-file")
+        tinfo.size = 1 << 64
+        bio = io.BytesIO()
+        # Write header without data
+        bio.write(tinfo.tobuf(format))
+
+        # Reset & try to get contents
+        bio.seek(0)
+        with tarfile.open(fileobj=bio, mode="r|") as tar:
+            with self.assertRaises(tarfile.ReadError):
+                tar.getmembers()
+
 
 class OverwriteTests(archiver_tests.OverwriteTests, unittest.TestCase):
     testdir = os.path.join(TEMPDIR, "testoverwrite")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/backports_zstd-1.6.0/tests/test/test_zipfile/test_core.py 
new/backports_zstd-1.7.0/tests/test/test_zipfile/test_core.py
--- old/backports_zstd-1.6.0/tests/test/test_zipfile/test_core.py       
2026-06-14 11:30:13.000000000 +0200
+++ new/backports_zstd-1.7.0/tests/test/test_zipfile/test_core.py       
2026-08-15 18:14:26.010631600 +0200
@@ -1,6 +1,7 @@
 import _pyio
 import array
 import contextlib
+import errno
 import importlib.util
 import io
 import itertools
@@ -650,6 +651,12 @@
             os.utime(TESTFN, (ts, ts))
         except OverflowError:
             self.skipTest('Host fs cannot set timestamp to required value.')
+        except OSError as exc:
+            # Some file systems (e.g. UFS and ZFS on illumos) do not
+            # support timestamps that do not fit in 32 bits.
+            if exc.errno != errno.EOVERFLOW:
+                raise
+            self.skipTest('Host fs cannot set timestamp to required value.')
 
         mtime_ns = os.stat(TESTFN).st_mtime_ns
         if mtime_ns != (4386268800 * 10**9):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/backports_zstd-1.6.0/tests/test/test_zstd.py 
new/backports_zstd-1.7.0/tests/test/test_zstd.py
--- old/backports_zstd-1.6.0/tests/test/test_zstd.py    2026-06-14 
11:30:13.000000000 +0200
+++ new/backports_zstd-1.7.0/tests/test/test_zstd.py    2026-08-15 
18:14:26.010656400 +0200
@@ -2374,6 +2374,8 @@
                 f.read(100)
             with self.assertRaises(io.UnsupportedOperation):
                 f.seek(100)
+            with self.assertRaises(io.UnsupportedOperation):
+                next(iter(f))
         self.assertEqual(f.closed, True)
         with self.assertRaises(ValueError):
             f.readable()

Reply via email to