Skip to content

[1.2] .decompress(..., output_buffer_limit=None) no longer works (which is the default value) #225

Description

@Dreamsorcerer

Repro:

import brotlicffi

# Highly compressible data over a SMALL window (lgwin=12 = 4 KiB): a tiny
# compressed prefix expands hugely, forcing the many-tiny-buffer decode path.
original = b"".join(bytes([65 + (i % 26)]) * 4096 for i in range(16))  # 64 KiB
compressed = brotlicffi.compress(original, lgwin=12)
assert len(compressed) > 64  # 64 is a genuine mid-stream prefix

d = brotlicffi.Decompressor()
d.decompress(compressed[:64])  # feed a mid-stream prefix, NO output_buffer_limit

# The prefix was fully consumed and nothing is buffered, yet the decoder claims
# it cannot accept more input:
assert not d.can_accept_more_data()           # BUG: should be True
d.decompress(compressed[64:])                 # raises: can_accept_more_data() is False

Output:

Traceback (most recent call last):
  File "/home/ubuntu/hacking/aiohttp/test.py", line 15, in <module>
    d.decompress(compressed[64:])                 # raises: can_accept_more_data() is False
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/home/ubuntu/.local/lib/python3.14/site-packages/brotlicffi/_api.py", line 437, in decompress
    chunks = self._decompress(data, output_buffer_limit)
  File "/home/ubuntu/.local/lib/python3.14/site-packages/brotlicffi/_api.py", line 444, in _decompress
    raise error(
    ...<2 lines>...
    )
brotlicffi._api.error: brotli: decoder process called with data when 'can_accept_more_data()' is False

Description (AI summary)

  1. No limit → tiny output buffers. With no limit and non-empty input, the buffer for each decode iteration is sized at just 5 * len(input):
    _calculate_buffer_size, no-limit branch → 5 * input_data_len (
    else:
    # Allocate a buffer that's hopefully overlarge, but if it's not we
    # don't mind: we'll spin around again.
    return 5 * input_data_len
    )

For a high-expansion chunk (e.g. 1 KiB compressed → ~1 MiB out, especially with a small lgwin), that buffer fills long before the input is exhausted, so the loop spins many times on NEEDS_MORE_OUTPUT.

  1. On every iteration where input remains, it stashes the remainder. Whenever available_in > 0 after a decode step, the leftover compressed bytes are copied into self._unconsumed_data:
    the save-unconsumed block (

    if available_in[0] > 0:
    remaining_input = ffi.buffer(next_in[0], available_in[0])[:]
    self._unconsumed_data = remaining_input
    )

  2. The final iteration consumes the rest but never clears the stash. When a later iteration consumes the last bytes and the decoder returns NEEDS_MORE_INPUT, the loop breaks — and that break asserts available_in
    == 0, so the save-block in step 2 does not run on this pass:
    the NEEDS_MORE_INPUT break (

    if rc == lib.BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:
    assert available_in[0] == 0
    break
    )

Nothing ever resets self._unconsumed_data, so it keeps a stale copy of bytes that were already decoded into the output. (My trace: 1024-byte prefix → 205 iterations of 5120-byte buffers; iter 204 had
available_in=4 and stashed those 4 bytes, iter 205 consumed them to 0 and broke — leaving the stale 4 bytes behind, with HasMoreOutput False.)

  1. can_accept_more_data() now lies. It reports "not ready" purely because the stash is non-empty, even though the decoder actually finished the input and has no buffered output:
    can_accept_more_data → False when _unconsumed_data is non-empty (

    if len(self._unconsumed_data) > 0:
    ret = False
    )

  2. The next call blows up. Either path fails:

  • Feeding new data hits the guard that assumes non-empty _unconsumed_data means the caller ignored the protocol, and raises can_accept_more_data() is False: the guard
    (
    if self._unconsumed_data and data:
    raise error(
    "brotli: decoder process called with data when "
    "'can_accept_more_data()' is False"
    )
    )
  • Feeding b"" (to "drain") re-feeds the stale, already-consumed bytes into the decoder, which is now past that stream position → BrotliDecoderDecompressStream returns an error (ERROR_FORMAT*).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions