Skip to content

Commit aaf331e

Browse files
committed
Correct repeated calls to decompress() with max_length=0
1 parent f3c27a4 commit aaf331e

7 files changed

Lines changed: 106 additions & 0 deletions

File tree

‎Lib/test/test_bz2.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,29 @@ def testDecompressorChunksMaxsize(self):
967967
self.assertEqual(out, self.BIG_TEXT)
968968
self.assertEqual(bzd.unused_data, b"")
969969

970+
def test_decompressor_max_length_zero(self):
971+
# max_length=0 buffers the input and never produces output,
972+
# however many times it is repeated.
973+
bzd = BZ2Decompressor()
974+
for _ in range(20):
975+
self.assertEqual(bzd.decompress(b'', max_length=0), b'')
976+
self.assertEqual(bzd.decompress(self.DATA, max_length=0), b'')
977+
for _ in range(20):
978+
self.assertEqual(bzd.decompress(b'', max_length=0), b'')
979+
self.assertFalse(bzd.needs_input)
980+
self.assertFalse(bzd.eof)
981+
self.assertEqual(bzd.decompress(b''), self.TEXT)
982+
self.assertTrue(bzd.eof)
983+
984+
# Output pending from an earlier call is kept for later.
985+
bzd = BZ2Decompressor()
986+
out = bzd.decompress(self.DATA, max_length=1)
987+
for _ in range(20):
988+
self.assertEqual(bzd.decompress(b'', max_length=0), b'')
989+
self.assertFalse(bzd.needs_input)
990+
self.assertEqual(out + bzd.decompress(b''), self.TEXT)
991+
self.assertTrue(bzd.eof)
992+
970993
def test_decompressor_inputbuf_1(self):
971994
# Test reusing input buffer after moving existing
972995
# contents to beginning

‎Lib/test/test_zlib.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,29 @@ def testDecompressorChunksMaxsize(self):
12311231
self.assertEqual(out, self.BIG_TEXT)
12321232
self.assertEqual(zlibd.unused_data, b"")
12331233

1234+
def test_decompressor_max_length_zero(self):
1235+
# max_length=0 buffers the input and never produces output,
1236+
# however many times it is repeated.
1237+
zlibd = zlib._ZlibDecompressor()
1238+
for _ in range(20):
1239+
self.assertEqual(zlibd.decompress(b'', max_length=0), b'')
1240+
self.assertEqual(zlibd.decompress(self.DATA, max_length=0), b'')
1241+
for _ in range(20):
1242+
self.assertEqual(zlibd.decompress(b'', max_length=0), b'')
1243+
self.assertFalse(zlibd.needs_input)
1244+
self.assertFalse(zlibd.eof)
1245+
self.assertEqual(zlibd.decompress(b''), self.TEXT)
1246+
self.assertTrue(zlibd.eof)
1247+
1248+
# Output pending from an earlier call is kept for later.
1249+
zlibd = zlib._ZlibDecompressor()
1250+
out = zlibd.decompress(self.DATA, max_length=1)
1251+
for _ in range(20):
1252+
self.assertEqual(zlibd.decompress(b'', max_length=0), b'')
1253+
self.assertFalse(zlibd.needs_input)
1254+
self.assertEqual(out + zlibd.decompress(b''), self.TEXT)
1255+
self.assertTrue(zlibd.eof)
1256+
12341257
def test_decompressor_inputbuf_1(self):
12351258
# Test reusing input buffer after moving existing
12361259
# contents to beginning

‎Lib/test/test_zstd.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,29 @@ def test_decompress_empty_content_frame(self):
853853
self.assertEqual(d.unused_data, b'')
854854
self.assertEqual(d.unused_data, b'') # twice
855855

856+
def test_decompressor_max_length_zero(self):
857+
# max_length=0 buffers the input and never produces output,
858+
# however many times it is repeated.
859+
d = ZstdDecompressor()
860+
for _ in range(20):
861+
self.assertEqual(d.decompress(b'', max_length=0), b'')
862+
self.assertEqual(d.decompress(COMPRESSED_100_PLUS_32KB, max_length=0), b'')
863+
for _ in range(20):
864+
self.assertEqual(d.decompress(b'', max_length=0), b'')
865+
self.assertFalse(d.needs_input)
866+
self.assertFalse(d.eof)
867+
self.assertEqual(d.decompress(b''), DECOMPRESSED_100_PLUS_32KB)
868+
self.assertTrue(d.eof)
869+
870+
# Output pending from an earlier call is kept for later.
871+
d = ZstdDecompressor()
872+
out = d.decompress(COMPRESSED_100_PLUS_32KB, max_length=1)
873+
for _ in range(20):
874+
self.assertEqual(d.decompress(b'', max_length=0), b'')
875+
self.assertFalse(d.needs_input)
876+
self.assertEqual(out + d.decompress(b''), DECOMPRESSED_100_PLUS_32KB)
877+
self.assertTrue(d.eof)
878+
856879
class DecompressorFlagsTestCase(unittest.TestCase):
857880

858881
@classmethod
@@ -1167,6 +1190,18 @@ def test_decompressor_skippable(self):
11671190
d = ZstdDecompressor()
11681191
dat = d.decompress(SKIPPABLE_FRAME, 0)
11691192

1193+
self.assertEqual(dat, b'')
1194+
self.assertFalse(d.eof)
1195+
self.assertFalse(d.needs_input)
1196+
1197+
dat = d.decompress(b'', 0)
1198+
1199+
self.assertEqual(dat, b'')
1200+
self.assertFalse(d.eof)
1201+
self.assertFalse(d.needs_input)
1202+
1203+
dat = d.decompress(b'')
1204+
11701205
self.assertEqual(dat, b'')
11711206
self.assertTrue(d.eof)
11721207
self.assertFalse(d.needs_input)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix :meth:`compression.zstd.ZstdDecompressor.decompress` raising an error when
2+
called repeatedly with ``max_length=0``. Calling ``decompress()`` with
3+
``max_length=0`` on the Zstandard decompressor, :class:`bz2.BZ2Decompressor`
4+
and :class:`!zlib._ZlibDecompressor` now always buffers the input and returns
5+
``b''`` without decompressing anything.

‎Modules/_bz2module.c‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,12 @@ decompress_buf(BZ2Decompressor *d, Py_ssize_t max_length)
421421
goto error;
422422
}
423423

424+
/* With max_length == 0 there is no room for output. Don't call into
425+
libbz2: any input is kept in the input buffer for the next call. */
426+
if (max_length == 0) {
427+
goto finish;
428+
}
429+
424430
for (;;) {
425431
int bzret;
426432
/* On a 64-bit system, buffer length might not fit in avail_out, so we
@@ -456,6 +462,7 @@ decompress_buf(BZ2Decompressor *d, Py_ssize_t max_length)
456462
}
457463
}
458464

465+
finish:
459466
result = OutputBuffer_Finish(&buffer, bzs->avail_out);
460467
if (result != NULL) {
461468
return result;

‎Modules/_zstd/decompressor.c‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,12 @@ decompress_lock_held(ZstdDecompressor *self, ZSTD_inBuffer *in,
226226
}
227227
assert(out.pos == 0);
228228

229+
/* With max_length == 0 there is no room for output. Don't call into
230+
libzstd: any input is kept in the input buffer for the next call. */
231+
if (max_length == 0) {
232+
goto finish;
233+
}
234+
229235
while (1) {
230236
/* Decompress */
231237
Py_BEGIN_ALLOW_THREADS
@@ -269,6 +275,7 @@ decompress_lock_held(ZstdDecompressor *self, ZSTD_inBuffer *in,
269275
}
270276
}
271277

278+
finish:
272279
/* Return a bytes object */
273280
ret = _OutputBuffer_Finish(&buffer, &out);
274281
if (ret != NULL) {

‎Modules/zlibmodule.c‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1488,6 +1488,12 @@ decompress_buf(ZlibDecompressor *self, Py_ssize_t max_length)
14881488

14891489
int err = Z_OK;
14901490

1491+
/* With max_length == 0 there is no room for output. Don't call into
1492+
zlib: any input is kept in the input buffer for the next call. */
1493+
if (max_length == 0) {
1494+
return Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
1495+
}
1496+
14911497
/* When sys.maxsize is passed as default use DEF_BUF_SIZE as start buffer.
14921498
In this particular case the data may not necessarily be very big, so
14931499
it is better to grow dynamically.*/

0 commit comments

Comments
 (0)