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()