Skip to content

BZ2Decompressor and zlib._ZlibDecompressor set needs_input=True while output is still buffered #159999

Open

Description

@pganssle

Bug description

When max_length cuts the output short and the call also consumes the whole input buffer, bz2.BZ2Decompressor and zlib._ZlibDecompressor set needs_input to True even though a decompress(b'') call would return more data. The documented meaning of the flag is the opposite: False means "there is buffered output, call again with b''"; True means "all output has been produced, feed more input".

>>> import bz2, zlib
>>> stream = bz2.compress(b"\x00\x10 0@" * 2)
>>> d = bz2.BZ2Decompressor()
>>> d.decompress(stream[:40], max_length=0)
b''
>>> d.needs_input, d.eof
(True, False)
>>> d.decompress(b"")   # Expecting: b'' if needs_input is to be believed
b'\x00\x10 0@\x00\x10 0@'
>>> d = zlib._ZlibDecompressor()
>>> d.decompress(zlib.compress(b"\x00")[:4], max_length=0)
b''
>>> d.needs_input
True
>>> d.decompress(b"")
b'\x00'

lzma.LZMADecompressor and compression.zstd.ZstdDecompressor get this right. Compare lzma on the same kind of input:

>>> import lzma
>>> d = lzma.LZMADecompressor()
>>> d.decompress(lzma.compress(b"\x00" * 21), max_length=2)
b'\x00\x00'
>>> d.needs_input
False

The cause is that both modules decide needs_input from the input buffer alone. In _bz2module.c the branch that handles "input exhausted" unconditionally stores 1: Modules/_bz2module.c lines 539-542. zlibmodule.c has the same branch for _ZlibDecompressor: Modules/zlibmodule.c lines 1646-1649. _lzmamodule.c checks whether the output buffer was also full before it decides, with a comment explaining why: Modules/_lzmamodule.c lines 1069-1082. Porting that check to the other two modules looks like the whole fix.

As a practical matter I don't see a way for this to lose data for a caller following the documented protocol: the stream's trailer has not been consumed yet in these states, so the caller still has input to feed and will get the buffered output then. It does make needs_input unreliable as a "no more buffered output" signal, which is what max_length users (including DecompressReader in compression/_common/_streams.py, which raises EOFError rather than call decompress(b'') when needs_input is true at end of file) rely on. So it is worth fixing, but it is not urgent.

Found by the Hypothesis tests in #159960, which record it as test_needs_input_means_drained under expectedFailure for the bz2 and zlib backends. The reproducers depend on where the compressor places block boundaries; the ones above are for zlib 1.3.2 and libbz2 1.0.8.

CPython versions tested on

CPython main (4e3d5ec)

Operating systems tested on

Linux

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-modulesC modules in the Modules dirtype-bugAn unexpected behavior, bug, or error

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions