1use std::{
4 future::Future,
5 io::{self, Write as _},
6 pin::Pin,
7 task::{Context, Poll},
8};
9
10use bytes::Bytes;
11#[cfg(feature = "compress-gzip")]
12use flate2::write::{GzDecoder, ZlibDecoder};
13use futures_core::{ready, Stream};
14use pin_project_lite::pin_project;
15use tokio::task::{spawn_blocking, JoinHandle};
16#[cfg(feature = "compress-zstd")]
17use zstd::stream::write::Decoder as ZstdDecoder;
18
19use crate::{
20 encoding::Writer,
21 error::PayloadError,
22 header::{ContentEncoding, HeaderMap, CONTENT_ENCODING},
23};
24
25const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049;
26
27pin_project! {
28 pub struct Decoder<S> {
37 decoder: Option<ContentDecoder>,
38 #[pin]
39 stream: S,
40 eof: bool,
41 fut: Option<JoinHandle<io::Result<(Option<Bytes>, ContentDecoder)>>>,
42 }
43}
44
45impl<S> Decoder<S>
46where
47 S: Stream<Item = Result<Bytes, PayloadError>>,
48{
49 #[inline]
54 pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> {
55 #[cfg(feature = "compress-brotli")]
56 const DEFAULT_DECODER_CAPACITY: usize = 8 * 1024; let decoder = match encoding {
59 #[cfg(feature = "compress-brotli")]
60 ContentEncoding::Brotli => {
61 let mut br =
62 brotli::DecompressorWriter::new(Writer::new(), DEFAULT_DECODER_CAPACITY);
63 br.set_parameter(
64 brotli_decompressor::BrotliDecoderParameter::BROTLI_DECODER_PARAM_LARGE_WINDOW,
65 0,
66 );
67
68 Some(ContentDecoder::Brotli(Box::new(br)))
69 }
70
71 #[cfg(feature = "compress-gzip")]
72 ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(ZlibDecoder::new(
73 Writer::new(),
74 )))),
75
76 #[cfg(feature = "compress-gzip")]
77 ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(GzDecoder::new(
78 Writer::new(),
79 )))),
80
81 #[cfg(feature = "compress-zstd")]
82 ContentEncoding::Zstd => Some(ContentDecoder::Zstd(Box::new(
83 ZstdDecoder::new(Writer::new()).expect(
84 "Failed to create zstd decoder. This is a bug. \
85 Please report it to the actix-web repository.",
86 ),
87 ))),
88
89 _ => None,
90 };
91
92 Decoder {
93 decoder,
94 stream,
95 fut: None,
96 eof: false,
97 }
98 }
99
100 pub fn limit(mut self, limit: usize) -> Self {
108 if let Some(ref mut decoder) = self.decoder {
109 decoder.set_limit(limit);
110 }
111
112 self
113 }
114
115 #[inline]
124 pub fn from_headers(stream: S, headers: &HeaderMap) -> Decoder<S> {
125 let encoding = headers
127 .get(&CONTENT_ENCODING)
128 .and_then(|val| val.to_str().ok())
129 .and_then(|x| x.parse().ok())
130 .unwrap_or(ContentEncoding::Identity);
131
132 Self::new(stream, encoding)
133 }
134}
135
136impl<S> Stream for Decoder<S>
137where
138 S: Stream<Item = Result<Bytes, PayloadError>>,
139{
140 type Item = Result<Bytes, PayloadError>;
141
142 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
143 let mut this = self.project();
144
145 loop {
146 if let Some(ref mut fut) = this.fut {
148 let result = ready!(Pin::new(fut).poll(cx))
149 .map_err(|_| {
150 PayloadError::Io(io::Error::other(
151 "Blocking task was cancelled unexpectedly",
152 ))
153 })
154 .and_then(|result| result.map_err(from_decoder_error));
155
156 this.fut.take();
158
159 let (chunk, decoder) = match result {
160 Ok(result) => result,
161 Err(err) => {
162 *this.eof = true;
163 return Poll::Ready(Some(Err(err)));
164 }
165 };
166
167 *this.decoder = Some(decoder);
168
169 if let Some(chunk) = chunk {
170 return Poll::Ready(Some(Ok(chunk)));
171 }
172 }
173
174 if *this.eof {
175 return Poll::Ready(None);
176 }
177
178 match ready!(this.stream.as_mut().poll_next(cx)) {
179 Some(Err(err)) => return Poll::Ready(Some(Err(err))),
180
181 Some(Ok(chunk)) => {
182 if let Some(mut decoder) = this.decoder.take() {
183 if chunk.len() < MAX_CHUNK_SIZE_DECODE_IN_PLACE {
184 let chunk = match decoder.feed_data(chunk) {
185 Ok(chunk) => chunk,
186 Err(err) => {
187 *this.eof = true;
188 return Poll::Ready(Some(Err(from_decoder_error(err))));
189 }
190 };
191
192 *this.decoder = Some(decoder);
193
194 if let Some(chunk) = chunk {
195 return Poll::Ready(Some(Ok(chunk)));
196 }
197 } else {
198 *this.fut = Some(spawn_blocking(move || {
199 let chunk = decoder.feed_data(chunk)?;
200 Ok((chunk, decoder))
201 }));
202 }
203
204 continue;
205 } else {
206 return Poll::Ready(Some(Ok(chunk)));
207 }
208 }
209
210 None => {
211 *this.eof = true;
212
213 return if let Some(mut decoder) = this.decoder.take() {
214 match decoder.feed_eof() {
215 Ok(Some(res)) => Poll::Ready(Some(Ok(res))),
216 Ok(None) => Poll::Ready(None),
217 Err(err) => Poll::Ready(Some(Err(from_decoder_error(err)))),
218 }
219 } else {
220 Poll::Ready(None)
221 };
222 }
223 }
224 }
225 }
226}
227
228fn from_decoder_error(err: io::Error) -> PayloadError {
229 match err.kind() {
230 io::ErrorKind::FileTooLarge => PayloadError::Overflow,
231 _ => err.into(),
232 }
233}
234
235enum ContentDecoder {
236 #[cfg(feature = "compress-gzip")]
237 Deflate(Box<ZlibDecoder<Writer>>),
238
239 #[cfg(feature = "compress-gzip")]
240 Gzip(Box<GzDecoder<Writer>>),
241
242 #[cfg(feature = "compress-brotli")]
243 Brotli(Box<brotli::DecompressorWriter<Writer>>),
244
245 #[cfg(feature = "compress-zstd")]
249 Zstd(Box<ZstdDecoder<'static, Writer>>),
250}
251
252impl ContentDecoder {
253 fn set_limit(&mut self, limit: usize) {
254 match self {
255 #[cfg(feature = "compress-brotli")]
256 ContentDecoder::Brotli(ref mut decoder) => decoder.get_mut().set_limit(limit),
257
258 #[cfg(feature = "compress-gzip")]
259 ContentDecoder::Gzip(ref mut decoder) => decoder.get_mut().set_limit(limit),
260
261 #[cfg(feature = "compress-gzip")]
262 ContentDecoder::Deflate(ref mut decoder) => decoder.get_mut().set_limit(limit),
263
264 #[cfg(feature = "compress-zstd")]
265 ContentDecoder::Zstd(ref mut decoder) => decoder.get_mut().set_limit(limit),
266 }
267 }
268
269 fn feed_eof(&mut self) -> io::Result<Option<Bytes>> {
270 match self {
271 #[cfg(feature = "compress-brotli")]
272 ContentDecoder::Brotli(ref mut decoder) => match decoder.flush() {
273 Ok(()) => {
274 let b = decoder.get_mut().take();
275
276 if !b.is_empty() {
277 Ok(Some(b))
278 } else {
279 Ok(None)
280 }
281 }
282 Err(err) => Err(err),
283 },
284
285 #[cfg(feature = "compress-gzip")]
286 ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() {
287 Ok(_) => {
288 let b = decoder.get_mut().take();
289
290 if !b.is_empty() {
291 Ok(Some(b))
292 } else {
293 Ok(None)
294 }
295 }
296 Err(err) => Err(err),
297 },
298
299 #[cfg(feature = "compress-gzip")]
300 ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() {
301 Ok(_) => {
302 let b = decoder.get_mut().take();
303 if !b.is_empty() {
304 Ok(Some(b))
305 } else {
306 Ok(None)
307 }
308 }
309 Err(err) => Err(err),
310 },
311
312 #[cfg(feature = "compress-zstd")]
313 ContentDecoder::Zstd(ref mut decoder) => match decoder.flush() {
314 Ok(_) => {
315 let b = decoder.get_mut().take();
316 if !b.is_empty() {
317 Ok(Some(b))
318 } else {
319 Ok(None)
320 }
321 }
322 Err(err) => Err(err),
323 },
324 }
325 }
326
327 fn feed_data(&mut self, data: Bytes) -> io::Result<Option<Bytes>> {
328 match self {
329 #[cfg(feature = "compress-brotli")]
330 ContentDecoder::Brotli(ref mut decoder) => match decoder.write_all(&data) {
331 Ok(_) => {
332 decoder.flush()?;
333 let b = decoder.get_mut().take();
334
335 if !b.is_empty() {
336 Ok(Some(b))
337 } else {
338 Ok(None)
339 }
340 }
341 Err(err) => Err(err),
342 },
343
344 #[cfg(feature = "compress-gzip")]
345 ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) {
346 Ok(_) => {
347 decoder.flush()?;
348 let b = decoder.get_mut().take();
349
350 if !b.is_empty() {
351 Ok(Some(b))
352 } else {
353 Ok(None)
354 }
355 }
356 Err(err) => Err(err),
357 },
358
359 #[cfg(feature = "compress-gzip")]
360 ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) {
361 Ok(_) => {
362 decoder.flush()?;
363
364 let b = decoder.get_mut().take();
365 if !b.is_empty() {
366 Ok(Some(b))
367 } else {
368 Ok(None)
369 }
370 }
371 Err(err) => Err(err),
372 },
373
374 #[cfg(feature = "compress-zstd")]
375 ContentDecoder::Zstd(ref mut decoder) => match decoder.write_all(&data) {
376 Ok(_) => {
377 decoder.flush()?;
378
379 let b = decoder.get_mut().take();
380 if !b.is_empty() {
381 Ok(Some(b))
382 } else {
383 Ok(None)
384 }
385 }
386 Err(err) => Err(err),
387 },
388 }
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use futures_util::{stream, StreamExt as _};
395 use rand::RngExt as _;
396
397 use super::*;
398
399 const ENCODINGS: &[ContentEncoding] = &[
400 #[cfg(feature = "compress-brotli")]
401 ContentEncoding::Brotli,
402 #[cfg(feature = "compress-gzip")]
403 ContentEncoding::Gzip,
404 #[cfg(feature = "compress-gzip")]
405 ContentEncoding::Deflate,
406 #[cfg(feature = "compress-zstd")]
407 ContentEncoding::Zstd,
408 ];
409
410 #[cfg(feature = "compress-brotli")]
411 #[actix_rt::test]
412 async fn brotli_large_window_is_rejected() {
413 let data = vec![b'a'; 4096];
414 let params = brotli::enc::BrotliEncoderParams {
415 large_window: true,
416 lgwin: 25,
417 ..Default::default()
418 };
419 let mut compressed = Vec::new();
420 brotli::BrotliCompress(&mut &data[..], &mut compressed, ¶ms).unwrap();
421
422 assert_eq!(compressed[0], 0x11);
423
424 let mut decoded = Vec::new();
425 brotli::BrotliDecompress(&mut &compressed[..], &mut decoded).unwrap();
426 assert_eq!(decoded, data);
427
428 let stream = stream::iter([Ok(Bytes::from(compressed))]);
429 let mut decoder = Decoder::new(stream, ContentEncoding::Brotli);
430
431 assert!(matches!(
432 decoder.next().await,
433 Some(Err(PayloadError::Incomplete(Some(_))))
434 ));
435 assert!(decoder.next().await.is_none());
436 }
437
438 fn encode(data: &[u8], encoding: ContentEncoding) -> Bytes {
439 match encoding {
440 #[cfg(feature = "compress-brotli")]
441 ContentEncoding::Brotli => {
442 let mut encoder = brotli::CompressorWriter::new(Vec::new(), 4096, 3, 22);
443 encoder.write_all(data).unwrap();
444 encoder.into_inner().into()
445 }
446
447 #[cfg(feature = "compress-gzip")]
448 ContentEncoding::Gzip => {
449 let mut encoder =
450 flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
451 encoder.write_all(data).unwrap();
452 encoder.finish().unwrap().into()
453 }
454
455 #[cfg(feature = "compress-gzip")]
456 ContentEncoding::Deflate => {
457 let mut encoder =
458 flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
459 encoder.write_all(data).unwrap();
460 encoder.finish().unwrap().into()
461 }
462
463 #[cfg(feature = "compress-zstd")]
464 ContentEncoding::Zstd => zstd::stream::encode_all(data, 3).unwrap().into(),
465
466 ContentEncoding::Identity => Bytes::copy_from_slice(data),
467 }
468 }
469
470 #[actix_rt::test]
471 async fn decoded_output_is_bounded() {
472 let data = vec![b'a'; 1024 * 1024];
473
474 for &encoding in ENCODINGS {
475 let compressed = encode(&data, encoding);
476 let stream = stream::iter([Ok(compressed)]);
477 let mut decoder = Decoder::new(stream, encoding).limit(1024);
478
479 assert!(
480 matches!(decoder.next().await, Some(Err(PayloadError::Overflow))),
481 "{encoding:?} should report overflow"
482 );
483 assert!(
484 decoder.next().await.is_none(),
485 "{encoding:?} should end after overflow"
486 );
487 }
488 }
489
490 #[actix_rt::test]
491 async fn blocking_decode_reports_overflow() {
492 let data_that_compresses_poorly = rand::rng()
493 .sample_iter(rand::distr::StandardUniform)
494 .take(65_536)
495 .collect::<Vec<u8>>();
496
497 for &encoding in ENCODINGS {
498 let compressed = encode(&data_that_compresses_poorly, encoding);
499
500 assert!(
501 compressed.len() >= MAX_CHUNK_SIZE_DECODE_IN_PLACE,
502 "{encoding:?} should use the blocking decode path"
503 );
504
505 let stream = stream::iter([Ok(compressed)]);
506 let mut decoder = Decoder::new(stream, encoding).limit(1024);
507
508 assert!(
509 matches!(decoder.next().await, Some(Err(PayloadError::Overflow))),
510 "{encoding:?} should report overflow"
511 );
512 assert!(
513 decoder.next().await.is_none(),
514 "{encoding:?} should end after overflow"
515 );
516 }
517 }
518
519 #[actix_rt::test]
520 async fn decoded_output_at_limit_is_complete() {
521 let data = vec![b'a'; 1024];
522
523 for &encoding in ENCODINGS {
524 let compressed = encode(&data, encoding);
525 let stream = stream::iter(
526 compressed
527 .chunks(8)
528 .map(|chunk| Ok(Bytes::copy_from_slice(chunk)))
529 .collect::<Vec<_>>(),
530 );
531
532 let mut decoder = Decoder::new(stream, encoding).limit(data.len());
533 let mut output = Vec::new();
534
535 while let Some(chunk) = decoder.next().await {
536 output.extend_from_slice(&chunk.unwrap());
537 }
538
539 assert_eq!(
540 output, data,
541 "{encoding:?} should decode the complete body at the limit"
542 );
543 }
544 }
545
546 #[actix_rt::test]
547 async fn decoded_limit_applies_across_chunks() {
548 let data = vec![b'a'; 1024 * 1024];
549
550 for &encoding in ENCODINGS {
551 let compressed = encode(&data, encoding);
552 let chunks = compressed
553 .chunks(16)
554 .map(|chunk| Ok(Bytes::copy_from_slice(chunk)))
555 .collect::<Vec<_>>();
556
557 let mut decoder = Decoder::new(stream::iter(chunks), encoding).limit(200_000);
558 let mut size = 0;
559
560 loop {
561 match decoder.next().await {
562 Some(Ok(chunk)) => {
563 size += chunk.len();
564 assert!(
565 size <= 200_000,
566 "{encoding:?} should keep total decoded output within the limit"
567 );
568 }
569 Some(Err(PayloadError::Overflow)) => break,
570 result => panic!("{encoding:?} should report overflow, got {result:?}"),
571 }
572 }
573
574 assert!(
575 decoder.next().await.is_none(),
576 "{encoding:?} should end after overflow"
577 );
578 }
579 }
580
581 #[actix_rt::test]
582 async fn zero_decoded_limit_rejects_nonempty_body() {
583 for &encoding in ENCODINGS {
584 let compressed = encode(b"a", encoding);
585 let mut decoder = Decoder::new(stream::iter([Ok(compressed)]), encoding).limit(0);
586 assert!(
587 matches!(decoder.next().await, Some(Err(PayloadError::Overflow))),
588 "{encoding:?} should report overflow"
589 );
590 }
591 }
592
593 #[test]
594 fn io_errors_are_not_classified_as_overflow() {
595 assert!(matches!(
596 from_decoder_error(io::Error::other("codec error")),
597 PayloadError::Incomplete(Some(_))
598 ));
599 }
600}