88
99import codecs
1010import io
11+ import itertools
1112import sys
1213import typing
1314import zlib
4849 _zstandard_installed = False
4950
5051
52+ MAX_DECODE_CHUNK_SIZE = 2 ** 20 # 1 MiB
53+
54+
55+ class Decompressor (typing .Protocol ):
56+ @property
57+ def unconsumed_tail (self ) -> bytes : ...
58+
59+ def decompress (self , data : bytes , max_length : int ) -> bytes : ...
60+
61+ def flush (self ) -> bytes : ...
62+
63+
64+ class ZlibDecompressor :
65+ """
66+ Drain a `zlib`/`gzip` decompressor in bounded pieces so a small compressed
67+ input cannot inflate to an unbounded buffer in a single call.
68+ """
69+
70+ def __init__ (self , decompressor : Decompressor ) -> None :
71+ self .decompressor = decompressor
72+
73+ def decompress (self , data : bytes ) -> typing .Iterator [bytes ]:
74+ decompressed = self .decompressor .decompress (data , MAX_DECODE_CHUNK_SIZE )
75+ while decompressed :
76+ yield decompressed
77+ decompressed = self .decompressor .decompress (self .decompressor .unconsumed_tail , MAX_DECODE_CHUNK_SIZE )
78+
79+ def flush (self ) -> bytes :
80+ return self .decompressor .flush ()
81+
82+
5183class ContentDecoder :
52- def decode (self , data : bytes ) -> bytes :
84+ def decode (self , data : bytes ) -> typing . Iterator [ bytes ] :
5385 raise NotImplementedError () # pragma: no cover
5486
55- def flush (self ) -> bytes :
87+ def flush (self ) -> typing . Iterator [ bytes ] :
5688 raise NotImplementedError () # pragma: no cover
5789
5890
@@ -61,11 +93,11 @@ class IdentityDecoder(ContentDecoder):
6193 Handle unencoded data.
6294 """
6395
64- def decode (self , data : bytes ) -> bytes :
65- return data
96+ def decode (self , data : bytes ) -> typing . Iterator [ bytes ] :
97+ yield data
6698
67- def flush (self ) -> bytes :
68- return b""
99+ def flush (self ) -> typing . Iterator [ bytes ] :
100+ yield from ()
69101
70102
71103class DeflateDecoder (ContentDecoder ):
@@ -77,22 +109,23 @@ class DeflateDecoder(ContentDecoder):
77109
78110 def __init__ (self ) -> None :
79111 self .first_attempt = True
80- self .decompressor = zlib .decompressobj ()
112+ self .decompressor = ZlibDecompressor ( zlib .decompressobj () )
81113
82- def decode (self , data : bytes ) -> bytes :
114+ def decode (self , data : bytes ) -> typing . Iterator [ bytes ] :
83115 was_first_attempt = self .first_attempt
84116 self .first_attempt = False
85117 try :
86- return self .decompressor .decompress (data )
118+ yield from self .decompressor .decompress (data )
87119 except zlib .error as exc :
88120 if was_first_attempt :
89- self .decompressor = zlib .decompressobj (- zlib .MAX_WBITS )
90- return self .decode (data )
91- raise DecodingError (str (exc )) from exc
121+ self .decompressor = ZlibDecompressor (zlib .decompressobj (- zlib .MAX_WBITS ))
122+ yield from self .decode (data )
123+ else :
124+ raise DecodingError (str (exc )) from exc
92125
93- def flush (self ) -> bytes :
126+ def flush (self ) -> typing . Iterator [ bytes ] :
94127 try :
95- return self .decompressor .flush ()
128+ yield self .decompressor .flush ()
96129 except zlib .error as exc : # pragma: no cover
97130 raise DecodingError (str (exc )) from exc
98131
@@ -105,17 +138,17 @@ class GZipDecoder(ContentDecoder):
105138 """
106139
107140 def __init__ (self ) -> None :
108- self .decompressor = zlib .decompressobj (zlib .MAX_WBITS | 16 )
141+ self .decompressor = ZlibDecompressor ( zlib .decompressobj (zlib .MAX_WBITS | 16 ) )
109142
110- def decode (self , data : bytes ) -> bytes :
143+ def decode (self , data : bytes ) -> typing . Iterator [ bytes ] :
111144 try :
112- return self .decompressor .decompress (data )
145+ yield from self .decompressor .decompress (data )
113146 except zlib .error as exc :
114147 raise DecodingError (str (exc )) from exc
115148
116- def flush (self ) -> bytes :
149+ def flush (self ) -> typing . Iterator [ bytes ] :
117150 try :
118- return self .decompressor .flush ()
151+ yield self .decompressor .flush ()
119152 except zlib .error as exc : # pragma: no cover
120153 raise DecodingError (str (exc )) from exc
121154
@@ -140,26 +173,31 @@ def __init__(self) -> None:
140173
141174 self .decompressor = brotli .Decompressor ()
142175 self .seen_data = False
143- self ._decompress : typing .Callable [[ bytes ] , bytes ]
176+ self ._decompress : typing .Callable [... , bytes ]
144177 if hasattr (self .decompressor , "decompress" ):
145178 # The 'brotlicffi' package.
146179 self ._decompress = self .decompressor .decompress # pragma: no cover
147180 else :
148181 # The 'brotli' package.
149- self ._decompress = self .decompressor .process # pragma: no cover
182+ self ._decompress = self .decompressor .process
150183
151- def decode (self , data : bytes ) -> bytes :
184+ def decode (self , data : bytes ) -> typing . Iterator [ bytes ] :
152185 if not data :
153- return b""
186+ return
154187 self .seen_data = True
155188 try :
156- return self ._decompress (data )
189+ # The C backend may allocate nearly twice the requested threshold.
190+ output_buffer_limit = MAX_DECODE_CHUNK_SIZE // 2
191+ decompressed = self ._decompress (data , output_buffer_limit = output_buffer_limit )
192+ while decompressed :
193+ yield decompressed
194+ decompressed = self ._decompress (b"" , output_buffer_limit = output_buffer_limit )
157195 except brotli .error as exc :
158196 raise DecodingError (str (exc )) from exc
159197
160- def flush (self ) -> bytes :
198+ def flush (self ) -> typing . Iterator [ bytes ] :
161199 if not self .seen_data :
162- return b""
200+ return
163201 try :
164202 if hasattr (self .decompressor , "finish" ):
165203 # Only available in the 'brotlicffi' package.
@@ -168,9 +206,9 @@ def flush(self) -> bytes:
168206 # will never actually emit any data. However, it will potentially throw
169207 # errors if a truncated or damaged data stream has been used.
170208 self .decompressor .finish () # pragma: no cover
171- return b""
172209 except brotli .error as exc : # pragma: no cover
173210 raise DecodingError (str (exc )) from exc
211+ yield from ()
174212
175213
176214class ZStandardDecoder (ContentDecoder ):
@@ -189,30 +227,34 @@ def __init__(self) -> None:
189227 self .decompressor = ZstdDecompressor ()
190228 self .seen_data = False
191229
192- def decode (self , data : bytes ) -> bytes :
230+ def decode (self , data : bytes ) -> typing . Iterator [ bytes ] :
193231 if not data :
194- return b""
232+ return
195233 self .seen_data = True
196- output = io .BytesIO ()
197234 try :
198235 if self .decompressor .eof :
199236 data = self .decompressor .unused_data + data
200237 self .decompressor = ZstdDecompressor ()
201- output .write (self .decompressor .decompress (data ))
202- while self .decompressor .eof and self .decompressor .unused_data :
203- unused_data = self .decompressor .unused_data
238+ while True :
239+ decompressed = self .decompressor .decompress (data , MAX_DECODE_CHUNK_SIZE )
240+ while decompressed :
241+ yield decompressed
242+ if self .decompressor .needs_input or self .decompressor .eof :
243+ break
244+ decompressed = self .decompressor .decompress (b"" , MAX_DECODE_CHUNK_SIZE )
245+ if not (self .decompressor .eof and self .decompressor .unused_data ):
246+ break
247+ data = self .decompressor .unused_data
204248 self .decompressor = ZstdDecompressor ()
205- output .write (self .decompressor .decompress (unused_data ))
206249 except ZstdError as exc :
207250 raise DecodingError (str (exc )) from exc
208- return output .getvalue ()
209251
210- def flush (self ) -> bytes :
252+ def flush (self ) -> typing . Iterator [ bytes ] :
211253 if not self .seen_data :
212- return b""
254+ return
213255 if not self .decompressor .eof :
214256 raise DecodingError ("Zstandard data is incomplete" ) # pragma: no cover
215- return b""
257+ yield from ()
216258
217259
218260class MultiDecoder (ContentDecoder ):
@@ -233,16 +275,25 @@ def __init__(self, encodings: typing.Sequence[str]) -> None:
233275 # Note that we reverse the order for decoding.
234276 self .children : list [ContentDecoder ] = [SUPPORTED_DECODERS [coding ]() for coding in reversed (codings )]
235277
236- def decode (self , data : bytes ) -> bytes :
278+ def decode (self , data : bytes ) -> typing .Iterator [bytes ]:
279+ streams : typing .Iterator [bytes ] = iter ((data ,))
237280 for child in self .children :
238- data = child .decode ( data )
239- return data
281+ streams = self . _pipe ( child .decode , streams )
282+ yield from streams
240283
241- def flush (self ) -> bytes :
242- data = b""
284+ def flush (self ) -> typing . Iterator [ bytes ] :
285+ streams : typing . Iterator [ bytes ] = iter (())
243286 for child in self .children :
244- data = child .decode (data ) + child .flush ()
245- return data
287+ streams = itertools .chain (self ._pipe (child .decode , streams ), child .flush ())
288+ yield from streams
289+
290+ @staticmethod
291+ def _pipe (
292+ decode : typing .Callable [[bytes ], typing .Iterator [bytes ]],
293+ upstream : typing .Iterator [bytes ],
294+ ) -> typing .Iterator [bytes ]:
295+ for chunk in upstream :
296+ yield from decode (chunk )
246297
247298
248299class ByteChunker :
0 commit comments