LibCompress: Tolerate Z_SYNC_FLUSH as final packet in stream

The stream in the PNG in #26595 contains a compressed block followed by
an uncompressed block of size 0, followed by EOF. Such a block is
apparently written for Z_SYNC_FLUSH.

zlib is very lenient about data ending abruptly. Since it's enough for
the PNGs written by ghostscript, allow an empty uncompressed packet at
stream end to act as if it's the final packet, even if that's still
stricter than zlib -- it's lenient enough for the current problem. We
can always become even more lenient in the future if it we find
something else that requires that.

Fixes #26595.
This commit is contained in:
Nico Weber authored and Tim Schumacher committed 2026-02-08 08:08:18 +01:00
1 parent 838284f83a
commit 6497968ac7
2 files changed
+29

No files matched your search

+23
View File
@@ -83,6 +83,29 @@ TEST_CASE(deflate_decompress_compressed_block)
EXPECT(decompressed == uncompressed.bytes());
}
TEST_CASE(deflate_decompress_stream_ending_with_sync_flush)
{
// This is the output of compressing "This is a simple text file :)" with
// Z_SYNC_FLUSH instead of Z_FINISH. The stream has bfinal=0 on all blocks
// and ends with an empty uncompressed block (the sync flush marker).
// Real-world PNG files can contain such streams. This shows that zlib is fine decompressing such streams:
// python3 -c "import sys, zlib
// c = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15)
// sys.stdout.buffer.write(c.compress(b'This is a simple text file :)') + c.flush(zlib.Z_SYNC_FLUSH))" |
// python3 -c "import sys, zlib; print(zlib.decompressobj(-15).decompress(sys.stdin.buffer.read()))"
// To get the contents of `compressed`, pipe the first command into `xxd -i` instead.
Array<u8, 32> const compressed {
0x0A, 0xC9, 0xC8, 0x2C, 0x56, 0x00, 0xA2, 0x44, 0x85, 0xE2, 0xCC, 0xDC,
0x82, 0x9C, 0x54, 0x85, 0x92, 0xD4, 0x8A, 0x12, 0x85, 0xB4, 0x4C, 0x20,
0xCB, 0x4A, 0x13, 0x00, 0x00, 0x00, 0xFF, 0xFF
};
auto const uncompressed = "This is a simple text file :)"sv;
auto const decompressed = TRY_OR_FAIL(Compress::DeflateDecompressor::decompress_all(compressed));
EXPECT(decompressed == uncompressed.bytes());
}
TEST_CASE(deflate_decompress_uncompressed_block)
{
Array<u8, 18> const compressed {
@@ -290,6 +290,12 @@ ErrorOr<Bytes> DeflateDecompressor::read_some(Bytes bytes)
if ((length ^ 0xffff) != negated_length)
return Error::from_string_literal("Calculated negated length does not equal stored negated length");
// Hack: Some deflate streams, for example in PNGs written by GhostScript 10.05.1's `gs`,
// end with an empty uncompressed block (Z_SYNC_FLUSH) but do not set the BFINAL bit on that block.
// Treat such a block at end of data as final, so we can decompress that data.
if (length == 0 && m_input_stream->is_eof())
m_read_final_block = true;
m_state = State::ReadingUncompressedBlock;
new (&m_uncompressed_block) UncompressedBlock(*this, length);