Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-httplib2 for openSUSE:Factory 
checked in at 2026-07-07 21:01:45
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-httplib2 (Old)
 and      /work/SRC/openSUSE:Factory/.python-httplib2.new.1982 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-httplib2"

Tue Jul  7 21:01:45 2026 rev:61 rq:1364066 version:0.32.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-httplib2/python-httplib2.changes  
2026-04-10 18:02:55.710400709 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-httplib2.new.1982/python-httplib2.changes    
    2026-07-07 21:03:43.343342655 +0200
@@ -1,0 +2,8 @@
+Mon Jul  6 15:28:14 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 0.32.0:
+  * Python support 3.8+ only
+  * decompression limited by size and ratio
+  * decoder foundation to support more compression algorithms
+
+-------------------------------------------------------------------

Old:
----
  httplib2-0.31.2.tar.gz

New:
----
  httplib2-0.32.0.tar.gz

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

Other differences:
------------------
++++++ python-httplib2.spec ++++++
--- /var/tmp/diff_new_pack.UINcYB/_old  2026-07-07 21:03:44.047367031 +0200
+++ /var/tmp/diff_new_pack.UINcYB/_new  2026-07-07 21:03:44.047367031 +0200
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-httplib2
-Version:        0.31.2
+Version:        0.32.0
 Release:        0
 Summary:        A Python HTTP client library
 License:        Apache-2.0 AND MIT AND (GPL-2.0-or-later OR MPL-1.1 OR 
LGPL-2.1-or-later)

++++++ httplib2-0.31.2.tar.gz -> httplib2-0.32.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httplib2-0.31.2/CHANGELOG 
new/httplib2-0.32.0/CHANGELOG
--- old/httplib2-0.31.2/CHANGELOG       2026-01-23 12:00:25.000000000 +0100
+++ new/httplib2-0.32.0/CHANGELOG       2026-06-26 12:11:05.000000000 +0200
@@ -1,3 +1,11 @@
+0.32.0
+
+  Python support 3.8+ only
+
+  decompression limited by size and ratio
+
+  decoder foundation to support more compression algorithms
+
 0.31.2
 
   build(deps): pp.DelimitedList (camel case) only available in pyparsing>=3.1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httplib2-0.31.2/PKG-INFO new/httplib2-0.32.0/PKG-INFO
--- old/httplib2-0.31.2/PKG-INFO        2026-01-23 12:00:42.607651200 +0100
+++ new/httplib2-0.32.0/PKG-INFO        2026-06-26 12:11:20.274066200 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: httplib2
-Version: 0.31.2
+Version: 0.32.0
 Summary: A comprehensive HTTP client library.
 Home-page: https://github.com/httplib2/httplib2
 Author: Joe Gregorio
@@ -15,7 +15,7 @@
 Classifier: Programming Language :: Python :: 3
 Classifier: Topic :: Internet :: WWW/HTTP
 Classifier: Topic :: Software Development :: Libraries
-Requires-Python: >=3.6
+Requires-Python: >=3.8
 License-File: LICENSE
 Requires-Dist: pyparsing<4,>=3.1
 Dynamic: author
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httplib2-0.31.2/README.md 
new/httplib2-0.32.0/README.md
--- old/httplib2-0.31.2/README.md       2026-01-23 12:00:25.000000000 +0100
+++ new/httplib2-0.32.0/README.md       2026-06-26 12:11:05.000000000 +0200
@@ -10,13 +10,13 @@
 
 HTTPS support is only available if the socket module was
 compiled with SSL support.
-    
+
 ### Keep-Alive
 
 Supports HTTP 1.1 Keep-Alive, keeping the socket open and
 performing multiple requests over the same connection if
 possible.
-    
+
 ### Authentication
 
 The following three types of HTTP Authentication are
@@ -31,26 +31,26 @@
 The module can optionally operate with a private cache that
 understands the Cache-Control: header and uses both the ETag
 and Last-Modified cache validators.
-    
+
 ### All Methods
 
 The module can handle any HTTP request method, not just GET
 and POST.
-    
+
 ### Redirects
 
 Automatically follows 3XX redirects on GETs.
-    
+
 ### Compression
 
 Handles both 'deflate' and 'gzip' types of compression.
-    
+
 ### Lost update support
 
 Automatically adds back ETags into PUT requests to resources
 we have already cached. This implements Section 3.2 of
 Detecting the Lost Update Problem Using Unreserved Checkout.
-    
+
 ### Unit Tested
 
 A large and growing set of unit tests.
@@ -121,3 +121,26 @@
 
  * https://github.com/httplib2/httplib2/wiki/Examples
  * https://github.com/httplib2/httplib2/wiki/Examples-Python3
+
+
+### Decompression Limits
+
+To mitigate denial-of-service risks from maliciously crafted compressed 
responses, the library enforces configurable limits during decompression. 
Limits are checked in fixed order: **hard limit** → **safe limit** → **ratio**.
+
+- **hard limit**
+  Absolute maximum decompressed output size (bytes). Exceeding it raises 
`DecodeLimitError`. Default: `10 GiB`.
+- **safe limit**
+  Output size below which the ratio check is skipped (avoids false positives 
on small payloads). Default: `10 MiB`.
+- **ratio**
+  Maximum allowed inflation factor (`output_bytes ÷ consumed_input_bytes`). 
Once output exceeds `safe_limit`, the ratio is enforced. Default: `100`.
+- **chunk size**
+  Internal processing chunk size in bytes (affects granularity of limit 
checks). Default: `65536` (64 KiB).
+
+Configuration priority (highest first):
+1. `Http()` constructor arguments: `decode_limit_hard`, `decode_limit_safe`, 
`decode_limit_ratio`, `decode_limit_chunk`
+2. Environment variables: `httplib2_decode_limit_hard`, 
`httplib2_decode_limit_safe`, `httplib2_decode_limit_ratio`, 
`httplib2_decode_limit_chunk` (case-insensitive, uppercase also accepted)
+3. Library defaults (listed above)
+
+Example:
+```python
+h = Http(decode_limit_hard=50_000_000, decode_limit_ratio=50)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httplib2-0.31.2/httplib2/__init__.py 
new/httplib2-0.32.0/httplib2/__init__.py
--- old/httplib2-0.31.2/httplib2/__init__.py    2026-01-23 12:00:25.000000000 
+0100
+++ new/httplib2-0.32.0/httplib2/__init__.py    2026-06-26 12:11:05.000000000 
+0200
@@ -1,6 +1,10 @@
 # -*- coding: utf-8 -*-
 """Small, fast HTTP client library for Python."""
 
+import functools
+
+from httplib2.decode import ZlibDecoder, DecoderProtocol, LimitDecoder, 
DeflateDecoder
+
 __author__ = "Joe Gregorio ([email protected])"
 __copyright__ = "Copyright 2006, Joe Gregorio"
 __contributors__ = [
@@ -16,7 +20,7 @@
     "Lai Han",
 ]
 __license__ = "MIT"
-__version__ = "0.31.2"
+__version__ = "0.32.0"
 
 import base64
 import calendar
@@ -385,26 +389,25 @@
     return retval
 
 
-def _decompressContent(response, new_content):
+def _decompressContent(response, new_content, limit_kwargs):
     content = new_content
+    encoding_header = "content-encoding"
+    encoding = response.get(encoding_header, None)
+    limit_wrap = functools.partial(LimitDecoder, **limit_kwargs)
     try:
-        encoding = response.get("content-encoding", None)
-        if encoding in ["gzip", "deflate"]:
-            if encoding == "gzip":
-                content = gzip.GzipFile(fileobj=io.BytesIO(new_content)).read()
-            if encoding == "deflate":
-                try:
-                    content = zlib.decompress(content, zlib.MAX_WBITS)
-                except (IOError, zlib.error):
-                    content = zlib.decompress(content, -zlib.MAX_WBITS)
+        if encoding in ["gzip", "deflate", "zlib"]:
+            try:
+                content = limit_wrap(ZlibDecoder()).consume_bytes(new_content, 
0)
+            except (IOError, zlib.error):
+                content = 
limit_wrap(DeflateDecoder()).consume_bytes(new_content, 0)
             response["content-length"] = str(len(content))
             # Record the historical presence of the encoding in a way the 
won't interfere.
-            response["-content-encoding"] = response["content-encoding"]
-            del response["content-encoding"]
+            response["-content-encoding"] = response.pop(encoding_header)
     except (IOError, zlib.error):
         content = ""
         raise FailedToDecompressContent(
-            _("Content purported to be compressed with %s but failed to 
decompress.") % response.get("content-encoding"),
+            _("Content purported to be compressed with %s but failed to 
decompress.")
+            % encoding,
             response,
             content,
         )
@@ -1235,6 +1238,10 @@
         disable_ssl_certificate_validation=False,
         tls_maximum_version=None,
         tls_minimum_version=None,
+        decode_limit_hard=None,
+        decode_limit_safe=None,
+        decode_limit_ratio=None,
+        decode_limit_chunk=None,
     ):
         """If 'cache' is a string then it is used as a directory name for
         a disk cache. Otherwise it must be an object that supports the
@@ -1261,6 +1268,11 @@
 
         tls_maximum_version / tls_minimum_version require Python 3.7+ /
         OpenSSL 1.1.0g+. A value of "TLSv1_3" requires OpenSSL 1.1.1+.
+
+        `decode_limit_{hard,safe,ratio,chunk}` options configure 
`httplib2.decode.LimitDecoder` in attempt order:
+          - Http() argument - top priority
+          - environment httplib2_decode_limit_{hard,safe,ratio,chunk}
+          - LimitDecoder defaults - least priority
         """
         self.proxy_info = proxy_info
         self.ca_certs = ca_certs
@@ -1309,6 +1321,22 @@
         # Keep Authorization: headers on a redirect.
         self.forward_authorization_headers = False
 
+        limit_kwargs = dict(
+            hard_limit=try_value_or_env(
+                int, decode_limit_hard, "httplib2_decode_limit_hard"
+            ),
+            safe_limit=try_value_or_env(
+                int, decode_limit_safe, "httplib2_decode_limit_safe"
+            ),
+            ratio=try_value_or_env(
+                float, decode_limit_ratio, "httplib2_decode_limit_ratio"
+            ),
+            chunk_size=try_value_or_env(
+                int, decode_limit_chunk, "httplib2_decode_limit_chunk"
+            ),
+        )
+        self.limit_kwargs = {k: v for k, v in limit_kwargs.items() if v is not 
None}
+
     def close(self):
         """Close persistent connections, clear sensitive data.
         Not thread-safe, requires external synchronization against concurrent 
requests.
@@ -1428,7 +1456,7 @@
                     content = response.read()
                 response = Response(response)
                 if method != "HEAD":
-                    content = _decompressContent(response, content)
+                    content = _decompressContent(response, content, 
self.limit_kwargs)
 
             break
         return (response, content)
@@ -1800,3 +1828,16 @@
             return self
         else:
             raise AttributeError(name)
+
+
+def try_value_or_env(to, value, env_key, default=None):
+    candidates = (value, os.environ.get(env_key), 
os.environ.get(env_key.upper()))
+    # same as `to(x1) or to(x2) or to(x3)` except accepting falsey values like 0
+    for x in candidates:
+        if x is None:
+            continue
+        try:
+            return to(x)
+        except ValueError:
+            pass
+    return default
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httplib2-0.31.2/httplib2/decode.py 
new/httplib2-0.32.0/httplib2/decode.py
--- old/httplib2-0.31.2/httplib2/decode.py      1970-01-01 01:00:00.000000000 
+0100
+++ new/httplib2-0.32.0/httplib2/decode.py      2026-06-26 12:11:05.000000000 
+0200
@@ -0,0 +1,183 @@
+from typing import Protocol
+import zlib
+
+
+class DecodeRatioError(Exception):
+    """Output-to-input amplification ratio exceeded the configured limit."""
+
+
+class DecodeLimitError(Exception):
+    """Total output length exceeded the hard limit."""
+
+
+class DecoderProtocol(Protocol):
+    @property
+    def needs_input(self) -> bool:
+        ...
+
+    def decode(self, b: bytes) -> bytes:
+        ...
+
+    def flush(self) -> bytes:
+        ...
+
+    def consume_bytes(self, data: bytes, chunk_size: int = 64 << 10) -> bytes:
+        out = bytearray()
+        if chunk_size == 0:
+            chunk_size = len(data)
+        for i in range(0, len(data), chunk_size):
+            chunk = data[i : i + chunk_size]
+            out.extend(self.decode(chunk))
+        out.extend(self.flush())
+        return bytes(out)
+
+
+class ZlibDecoder(DecoderProtocol):
+    """
+    Thin wrapper around zlib.Decompressor conforming to the Decoder interface.
+
+    Note: zlib pushes all available decompressed data immediately upon 
receiving
+    input. It never holds back output requiring `decode(b"")` to extract it.
+    Thus, `needs_input` naturally remains True.
+    """
+
+    __slots__ = ("_decoder",)
+
+    WBITS_DEFLATE = -15
+    WBITS_ZLIB = 15
+    WBITS_GZIP = 15 | 16
+    WBITS_AUTO_GZIP_ZLIB = 15 | 32  # but not deflate
+
+    def __init__(self, wbits: int = WBITS_AUTO_GZIP_ZLIB):
+        self._decoder: zlib._Decompress | None = zlib.decompressobj(wbits)
+
+    @property
+    def needs_input(self) -> bool:
+        if self._decoder is None:
+            raise RuntimeError("used after flush()")
+        return not self._decoder.eof
+
+    def decode(self, b: bytes) -> bytes:
+        if self._decoder is None:
+            raise RuntimeError("used after flush()")
+        return self._decoder.decompress(b)
+
+    def flush(self) -> bytes:
+        if self._decoder is None:
+            raise RuntimeError("used after flush()")
+        result = self._decoder.flush()
+        self._decoder = None
+        return result
+
+
+def DeflateDecoder() -> ZlibDecoder:
+    return ZlibDecoder(ZlibDecoder.WBITS_DEFLATE)
+
+
+class LimitDecoder(DecoderProtocol):
+    __slots__ = (
+        "_decoder",
+        "_ratio",
+        "_chunk_size",
+        "_safe_limit",
+        "_hard_limit",
+        "_consumed_length",
+        "_output_length",
+        "_input_buffer",
+        "_flushed",
+    )
+
+    def __init__(
+        self,
+        decoder: DecoderProtocol,
+        ratio: float = 100,
+        chunk_size: int = 64 << 10,
+        safe_limit: int = 10 << 20,
+        hard_limit: int = 10 << 30,
+    ) -> None:
+        if ratio < 0:
+            raise ValueError(f"LimitDecoder() ratio={ratio} expected >= 0")
+        if chunk_size < 0:
+            raise ValueError(f"LimitDecoder() chunk_size={chunk_size} expected 
>= 0")
+        if safe_limit < 0:
+            raise ValueError(f"LimitDecoder() safe_limit={safe_limit} expected 
>= 0")
+        if hard_limit < 0:
+            raise ValueError(f"LimitDecoder() safe_limit={safe_limit} expected 
>= 0")
+
+        self._decoder: DecoderProtocol = decoder
+        self._ratio: float = ratio
+        self._chunk_size: int = chunk_size
+        self._safe_limit: int = safe_limit
+        self._hard_limit: int = hard_limit
+        self._consumed_length: int = 0
+        self._output_length: int = 0
+        self._input_buffer: bytearray = bytearray()
+        self._flushed: bool = False
+
+    def _check_limits(self) -> None:
+        if (self._hard_limit > 0) and (self._output_length > self._hard_limit):
+            raise DecodeLimitError(f"Output length {self._output_length} 
exceeds hard limit {self._hard_limit}")
+        if (self._safe_limit > 0) and (self._output_length < self._safe_limit):
+            return
+        if (self._ratio > 0) and (self._output_length > self._consumed_length 
* self._ratio):
+            actual_ratio = self._output_length / self._consumed_length if 
self._consumed_length > 0 else float("inf")
+            raise DecodeRatioError(
+                f"Amplification ratio {actual_ratio:.1f} 
({self._output_length}/{self._consumed_length})"
+                f" exceeds limit {self._ratio}"
+            )
+
+    @property
+    def needs_input(self) -> bool:
+        return self._decoder.needs_input
+
+    def decode(self, b: bytes) -> bytes:
+        if self._flushed:
+            raise RuntimeError("decode() called after flush()")
+        output = self._pump(b)
+        return bytes(output)
+
+    def flush(self) -> bytes:
+        if self._flushed:
+            raise RuntimeError("flush() called more than once")
+        self._flushed = True
+
+        output = self._pump(b"")
+
+        data = self._decoder.flush()
+        output.extend(data)
+        self._output_length += len(data)
+        self._check_limits()
+
+        return bytes(output)
+
+    def _pump(self, b: bytes) -> bytearray:
+        self._input_buffer.extend(b)
+
+        output = bytearray()
+        while True:
+            if not self._decoder.needs_input:
+                data = self._decoder.decode(b"")
+                if data:
+                    output.extend(data)
+                    self._output_length += len(data)
+                    self._check_limits()
+                    continue
+
+            if self._input_buffer:
+                chunk = bytes(self._input_buffer[: self._chunk_size])
+                del self._input_buffer[: self._chunk_size]
+
+                data = self._decoder.decode(chunk)
+                self._consumed_length += len(chunk)
+
+                if data:
+                    output.extend(data)
+                    self._output_length += len(data)
+                    self._check_limits()
+
+                continue
+
+            # neither input nor decoder progress
+            break
+
+        return output
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httplib2-0.31.2/httplib2.egg-info/PKG-INFO 
new/httplib2-0.32.0/httplib2.egg-info/PKG-INFO
--- old/httplib2-0.31.2/httplib2.egg-info/PKG-INFO      2026-01-23 
12:00:42.000000000 +0100
+++ new/httplib2-0.32.0/httplib2.egg-info/PKG-INFO      2026-06-26 
12:11:20.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: httplib2
-Version: 0.31.2
+Version: 0.32.0
 Summary: A comprehensive HTTP client library.
 Home-page: https://github.com/httplib2/httplib2
 Author: Joe Gregorio
@@ -15,7 +15,7 @@
 Classifier: Programming Language :: Python :: 3
 Classifier: Topic :: Internet :: WWW/HTTP
 Classifier: Topic :: Software Development :: Libraries
-Requires-Python: >=3.6
+Requires-Python: >=3.8
 License-File: LICENSE
 Requires-Dist: pyparsing<4,>=3.1
 Dynamic: author
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httplib2-0.31.2/httplib2.egg-info/SOURCES.txt 
new/httplib2-0.32.0/httplib2.egg-info/SOURCES.txt
--- old/httplib2-0.31.2/httplib2.egg-info/SOURCES.txt   2026-01-23 
12:00:42.000000000 +0100
+++ new/httplib2-0.32.0/httplib2.egg-info/SOURCES.txt   2026-06-26 
12:11:20.000000000 +0200
@@ -43,6 +43,7 @@
 httplib2/auth.py
 httplib2/cacerts.txt
 httplib2/certs.py
+httplib2/decode.py
 httplib2/error.py
 httplib2/iri2uri.py
 httplib2.egg-info/PKG-INFO
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httplib2-0.31.2/setup.cfg 
new/httplib2-0.32.0/setup.cfg
--- old/httplib2-0.31.2/setup.cfg       2026-01-23 12:00:42.607651200 +0100
+++ new/httplib2-0.32.0/setup.cfg       2026-06-26 12:11:20.274374200 +0200
@@ -2,7 +2,7 @@
 
 [flake8]
 exclude = *.egg*,.env,.git,.tox,_*,build*,dist*,venv*
-ignore = E261,E731,W503
+ignore = E203,E261,E731,W503
 max-line-length = 121
 
 [tool:pytest]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httplib2-0.31.2/setup.py new/httplib2-0.32.0/setup.py
--- old/httplib2-0.31.2/setup.py        2026-01-23 12:00:25.000000000 +0100
+++ new/httplib2-0.32.0/setup.py        2026-06-26 12:11:05.000000000 +0200
@@ -3,7 +3,7 @@
 import setuptools.command.test
 import sys
 
-VERSION = "0.31.2"
+VERSION = "0.32.0"
 
 
 # `python setup.py test` uses existing Python environment, no virtualenv, no 
pip.
@@ -86,7 +86,7 @@
     package_data={"httplib2": ["*.txt"]},
     install_requires=read_requirements("requirements.txt"),
     tests_require=read_requirements("requirements-test.txt"),
-    python_requires=">=3.6",
+    python_requires=">=3.8",
     cmdclass={"test": TestCommand},
     classifiers=[
         "Development Status :: 4 - Beta",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httplib2-0.31.2/tests/__init__.py 
new/httplib2-0.32.0/tests/__init__.py
--- old/httplib2-0.31.2/tests/__init__.py       2026-01-23 12:00:25.000000000 
+0100
+++ new/httplib2-0.32.0/tests/__init__.py       2026-06-26 12:11:05.000000000 
+0200
@@ -794,3 +794,11 @@
         path = u.path
     new = (scheme, netloc, path) + u[3:]
     return urllib.parse.urlunsplit(new)
+
+
+# remove when python version is raised to 3.9+
+try:
+    randbytes = random.randbytes
+except AttributeError:
+    def randbytes(n):
+        return random.getrandbits(n * 8).to_bytes(n, "little")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httplib2-0.31.2/tests/test_encoding.py 
new/httplib2-0.32.0/tests/test_encoding.py
--- old/httplib2-0.31.2/tests/test_encoding.py  2026-01-23 12:00:25.000000000 
+0100
+++ new/httplib2-0.32.0/tests/test_encoding.py  2026-06-26 12:11:05.000000000 
+0200
@@ -1,13 +1,14 @@
+import pytest
+
 import httplib2
+from httplib2.decode import LimitDecoder, ZlibDecoder, DecodeLimitError, 
DecodeRatioError
 import tests
 
 
 def test_gzip_head():
     # Test that we don't try to decompress a HEAD response
     http = httplib2.Http()
-    response = tests.http_response_bytes(
-        headers={"content-encoding": "gzip", "content-length": 42}
-    )
+    response = tests.http_response_bytes(headers={"content-encoding": "gzip", 
"content-length": 42})
     with tests.server_const_bytes(response) as uri:
         response, content = http.request(uri, "HEAD")
         assert response.status == 200
@@ -48,9 +49,7 @@
     http = httplib2.Http()
     # Test that we raise a good exception when the gzip fails
     http.force_exception_to_status_code = False
-    response = tests.http_response_bytes(
-        headers={"content-encoding": "gzip"}, body=b"obviously not compressed"
-    )
+    response = tests.http_response_bytes(headers={"content-encoding": "gzip"}, 
body=b"obviously not compressed")
     with tests.server_const_bytes(response, request_count=2) as uri:
         with tests.assert_raises(httplib2.FailedToDecompressContent):
             http.request(uri, "GET")
@@ -82,9 +81,7 @@
     # Test that we raise a good exception when the deflate fails
     http = httplib2.Http()
     http.force_exception_to_status_code = False
-    response = tests.http_response_bytes(
-        headers={"content-encoding": "deflate"}, body=b"obviously not 
compressed"
-    )
+    response = tests.http_response_bytes(headers={"content-encoding": 
"deflate"}, body=b"obviously not compressed")
     with tests.server_const_bytes(response, request_count=2) as uri:
         with tests.assert_raises(httplib2.FailedToDecompressContent):
             http.request(uri, "GET")
@@ -110,3 +107,157 @@
         assert "content-encoding" not in response
         assert int(response["content-length"]) == len(b"properly compressed")
         assert content == b"properly compressed"
+
+
+def test_gzip_excess_ratio():
+    http = httplib2.Http()
+    original = b"\x00" * (50 << 20)  # 50 MiB to ~50 KiB
+    response = tests.http_response_bytes(
+        headers={"content-encoding": "gzip"},
+        body=tests.gzip_compress(original),
+    )
+    with tests.server_const_bytes(response) as uri:
+        try:
+            http.request(uri, "GET")
+            assert False, "expected DecodeRatioError"
+        except DecodeRatioError:
+            pass
+
+
[email protected]("safe_limit", (0, 1000, 20000))
+def test_limitdecoder_normal_decompression_no_limits(safe_limit):
+    """Standard decompression of random data should pass with any safe_limit"""
+    original = tests.randbytes(10 << 10)
+    compressed = tests.zlib_compress(original)
+
+    decoder = LimitDecoder(
+        ZlibDecoder(),
+        ratio=10,
+        safe_limit=safe_limit,
+        hard_limit=len(original) + 1,
+    )
+    result = decoder.consume_bytes(compressed)
+    assert result == original
+    assert decoder._output_length == len(original)
+
+
+def test_limitdecoder_normal_rechunking():
+    """Passing a massive single chunk should be re-chunked internally without 
error"""
+    original = b"\x00" * (10 << 20)
+    compressed = tests.zlib_compress(original)
+    assert len(compressed) > 2000
+
+    decoder = LimitDecoder(
+        ZlibDecoder(),
+        ratio=2000,
+        chunk_size=512,
+        safe_limit=0,
+        hard_limit=len(original) + 1,
+    )
+    result = decoder.consume_bytes(compressed, chunk_size=0)
+    assert result == original
+    assert decoder._consumed_length == len(compressed)
+
+
+def test_limitdecoder_amplification_ratio_exceeded():
+    """High ratio should trigger DecodeRatioError above safe_limit"""
+    original = b"\x00" * (1 << 20)
+    compressed = tests.zlib_compress(original)
+
+    decoder = LimitDecoder(
+        ZlibDecoder(),
+        ratio=10,
+        chunk_size=512,
+        safe_limit=0,
+        hard_limit=len(original) + 1,
+    )
+    try:
+        decoder.consume_bytes(compressed, chunk_size=0)
+        assert False, "expected DecodeRatioError"
+    except DecodeRatioError:
+        pass
+    assert decoder._consumed_length == 512, "expected ratio error on first 
chunk"
+
+
[email protected]("ratio", (0, 10, 1000))
+def test_limitdecoder_hard_limit_exceeded(ratio):
+    """Output exceeding hard_limit must trigger DecodeLimitError regardless of 
ratio"""
+    original = b"\x00" * (10 << 10)
+    compressed = tests.zlib_compress(original)
+
+    decoder = LimitDecoder(
+        ZlibDecoder(),
+        ratio=ratio,
+        safe_limit=0,
+        hard_limit=len(original) - 1,
+    )
+    try:
+        decoder.consume_bytes(compressed)
+        assert False, "expected DecodeLimitError"
+    except DecodeLimitError:
+        pass
+
+
[email protected]("ratio", (0, 10, 1000))
+def test_limitdecoder_safe_limit_bypass(ratio):
+    """Any ratio allowed if total output < safe_limit"""
+    original = b"\x00" * (10 << 10)
+    compressed = tests.zlib_compress(original)
+
+    decoder = LimitDecoder(
+        ZlibDecoder(),
+        ratio=ratio,
+        safe_limit=len(original) + 1,
+        hard_limit=len(original) + 1,
+    )
+    result = decoder.consume_bytes(compressed)
+    assert result == original
+    assert decoder._output_length == len(original)
+
+
+def test_limitdecoder_single_byte_feeding():
+    """Feeding compressed data 1 byte at a time should still decode 
correctly"""
+    original = tests.randbytes(10 << 10)
+    compressed = tests.zlib_compress(original)
+
+    decoder = LimitDecoder(
+        ZlibDecoder(),
+        ratio=10,
+        safe_limit=5 << 10,
+        hard_limit=len(original) + 1,
+    )
+    result = decoder.consume_bytes(compressed, chunk_size=1)
+    assert result == original
+
+
+def test_limitdecoder_invalid_argument():
+    checks = (
+        ("ratio", dict(ratio=-1)),
+        ("chunk_size", dict(chunk_size=-1)),
+        ("safe_limit", dict(safe_limit=-1)),
+        ("hard_limit", dict(hard_limit=-1)),
+    )
+    for name, check in checks:
+        zd = ZlibDecoder()
+        try:
+            LimitDecoder(zd, **check)
+            assert False, f"check={name} expected ValueError"
+        except ValueError as e:
+            assert "expected >= 0" in str(e).lower(), str(e)
+
+
+def test_zlibdecoder_invalid_after_flush():
+    checks = (
+        ("needs_input", lambda d: d.needs_input),
+        ("decode", lambda d: d.decode(b"")),
+        ("flush", lambda d: d.flush()),
+    )
+    for name, check in checks:
+        d = ZlibDecoder()
+        d.decode(tests.zlib_compress(b""))
+        d.flush()
+        try:
+            check(d)
+            assert False, f"check={name} expected RuntimeError"
+        except RuntimeError as e:
+            assert "used after flush" in str(e).lower(), str(e)

Reply via email to