Skip to main content

actix_http/encoding/
decoder.rs

1//! Stream decoders.
2
3use 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    /// Payload stream that decompresses chunks using a content encoding.
29    ///
30    /// Yields decoded [`Bytes`] as they become available. Errors from the underlying stream are
31    /// passed through; decompression errors are returned as [`PayloadError::Io`].
32    ///
33    /// Exceeding the configured decoded byte limit returns [`PayloadError::Overflow`].
34    ///
35    /// See the [module-level docs](crate::encoding) for supported encodings and runtime requirements.
36    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    /// Constructs a decoder with the given content encoding.
50    ///
51    /// If `encoding` is [`ContentEncoding::Identity`] or its codec feature is disabled, the stream
52    /// is passed through unchanged.
53    #[inline]
54    pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> {
55        #[cfg(feature = "compress-brotli")]
56        const DEFAULT_DECODER_CAPACITY: usize = 8 * 1024; // 8 KiB
57
58        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    /// Set the maximum total number of decoded bytes.
101    ///
102    /// Decoding stops with [`PayloadError::Overflow`] if the limit is exceeded.
103    ///
104    /// Limit must be set before the stream is first polled.
105    ///
106    /// By default, decoded output is unlimited.
107    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    /// Constructs a decoder using the `Content-Encoding` header.
116    ///
117    /// If the header is missing, invalid, or unrecognized, the stream is passed through unchanged.
118    /// A recognized encoding whose codec feature is disabled also passes through unchanged.
119    ///
120    /// Only a single encoding is supported. Comma-separated encoding lists are treated as
121    /// unrecognized values. If there are multiple `Content-Encoding` header values, only the first
122    /// is used.
123    #[inline]
124    pub fn from_headers(stream: S, headers: &HeaderMap) -> Decoder<S> {
125        // check content-encoding
126        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            // Receive chunk and decoder state back from thread-offloaded decode task.
147            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                // Offload task completed; throw away join handle.
157                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    // We need 'static here because:
246    // - ZstdDecoder needs a lifetime argument;
247    // - we use `spawn_blocking` in `Decoder::poll_next` that requires `FnOnce() -> R + Send + 'static`.
248    #[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, &params).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}