diff --git a/Lib/test/test_zlib.py b/Lib/test/test_zlib.py index 3243448043b5c2..a2468ab0ae6a38 100644 --- a/Lib/test/test_zlib.py +++ b/Lib/test/test_zlib.py @@ -1153,6 +1153,45 @@ def testDecompress(self): text = zlibd.decompress(self.DATA) self.assertEqual(text, self.TEXT) + def testDecompressZdictWrapped(self): + # gh-160001: for a zlib-wrapped stream zlib only announces the + # dictionary (``Z_NEED_DICT``) after the header, so it has to be + # applied from the decompression loop rather than the constructor. + zdict = b"hello world " * 10 + compressor = zlib.compressobj(zdict=zdict) + data = compressor.compress(b"hello world hello") + compressor.flush() + + decompressor = zlib._ZlibDecompressor(zdict=zdict) + self.assertEqual(decompressor.decompress(data), b"hello world hello") + + def testDecompressZdictWrappedChunks(self): + zdict = b"hello world " * 10 + compressor = zlib.compressobj(zdict=zdict) + data = compressor.compress(b"hello world hello") + compressor.flush() + + decompressor = zlib._ZlibDecompressor(zdict=zdict) + result = b"" + for index in range(0, len(data), 3): + result += decompressor.decompress(data[index:index + 3]) + + self.assertEqual(result, b"hello world hello") + + def testDecompressZdictRaw(self): + zdict = b"hello world " * 10 + compressor = zlib.compressobj(wbits=-zlib.MAX_WBITS, zdict=zdict) + data = compressor.compress(b"hello world hello") + compressor.flush() + + decompressor = zlib._ZlibDecompressor(-zlib.MAX_WBITS, zdict=zdict) + self.assertEqual(decompressor.decompress(data), b"hello world hello") + + def testDecompressFdictHeaderWithoutZdict(self): + # A zlib header with the FDICT flag set asks for a dictionary that was + # never provided; that is a zlib error, not an internal error. + fdict_header = b"\x78\xbb" + b"\x00\x00\x00\x01\x03\x00" + + with self.assertRaises(zlib.error): + zlib._ZlibDecompressor().decompress(fdict_header) + def testDecompressChunks10(self): zlibd = zlib._ZlibDecompressor() text = b'' diff --git a/Misc/NEWS.d/next/Library/2026-11-05-07-30-00.gh-issue-160001.Zq4WnH.rst b/Misc/NEWS.d/next/Library/2026-11-05-07-30-00.gh-issue-160001.Zq4WnH.rst new file mode 100644 index 00000000000000..0e09743f828c9d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-11-05-07-30-00.gh-issue-160001.Zq4WnH.rst @@ -0,0 +1,4 @@ +:mod:`zlib`: Fix ``zlib._ZlibDecompressor`` failing with :exc:`SystemError` +instead of decompressing when a ``zdict`` is passed for a zlib-wrapped stream. +A stream that requires a dictionary which was not provided now raises +:exc:`zlib.error`, matching ``zlib.Decompress``. diff --git a/Modules/zlibmodule.c b/Modules/zlibmodule.c index 31533fa047af0b..8b5d188a801b8d 100644 --- a/Modules/zlibmodule.c +++ b/Modules/zlibmodule.c @@ -1534,13 +1534,21 @@ decompress_buf(ZlibDecompressor *self, Py_ssize_t max_length) break; default: if (err == Z_NEED_DICT) { - goto error; + if (self->zdict == NULL) { + zlib_error(state, self->zst, err, + "while decompressing data"); + goto error; + } + if (set_inflate_zdict_ZlibDecompressor(state, self) < 0) { + goto error; + } + break; } else { break; } } - } while (self->zst.avail_out == 0); + } while (self->zst.avail_out == 0 || err == Z_NEED_DICT); } while(err != Z_STREAM_END && self->avail_in_real != 0); if (err == Z_STREAM_END) {