Skip to main content

http_net/
chunked.rs

1//! A standalone chunked-body decoder, for code that holds the bytes itself
2//! (a proxy streaming a body through, a client built on another buffer).
3//!
4//! It runs the connection's own decoder: the same chunk-size grammar (at
5//! most 16 hex digits, checked arithmetic), extension grammar and per-message
6//! extension budget, body budget checked before each chunk's data, CRLF
7//! after the data, and trailer validation with the trailer budgets.
8
9use crate::{
10    body::{Decoder, Step, TrailerMode},
11    error::HttpError,
12    head::{BodyFraming, HeadScanner},
13    limits::Limits,
14};
15
16/// One step of [`ChunkedDecoder::decode`], with explicit byte accounting so
17/// a caller can decode in place in its own buffer. Offsets are relative to
18/// the `input` of that call.
19///
20/// # Examples
21///
22/// See [`ChunkedDecoder`].
23#[non_exhaustive]
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
25pub enum ChunkStep {
26    /// Skip `skip` framing bytes, then `len` bytes are body data; consume
27    /// both (`skip + len`) and call again with what follows.
28    Data {
29        /// Framing bytes before the data.
30        skip: usize,
31        /// Body data bytes.
32        len: usize,
33    },
34    /// More input is needed: drop the first `consumed` bytes (framing that
35    /// was read), keep the rest, append more, and call again.
36    NeedMore {
37        /// Framing bytes consumed.
38        consumed: usize,
39    },
40    /// Skip `skip` framing bytes, then `len` bytes are the trailer section
41    /// (its field lines and the blank line), already validated; consume
42    /// both, and the next call reports [`ChunkStep::Done`]. Only reported for
43    /// a section that has at least one field.
44    ///
45    /// The bytes are validated, not filtered: a section may name
46    /// `content-length`, `transfer-encoding`, or `host`, which the
47    /// connections drop from their trailer events (RFC 9110 section 6.5.1).
48    /// A proxy must not relay these bytes as they are; re-emit the fields it
49    /// keeps with `end_body_with_trailers`, which refuses the names a
50    /// trailer may not carry.
51    Trailers {
52        /// Framing bytes before the section.
53        skip: usize,
54        /// Bytes of the trailer section, the final CRLF included.
55        len: usize,
56    },
57    /// The body is complete after `consumed` more bytes (the last chunk and
58    /// an empty trailer section); what follows belongs to the next message.
59    Done {
60        /// Bytes consumed.
61        consumed: usize,
62    },
63}
64
65/// Decodes a chunked body (RFC 9112 section 7.1) from input the caller owns,
66/// with the exact grammar and budgets of the connections. Nothing is
67/// allocated.
68///
69/// # Examples
70///
71/// ```
72/// use http_net::{ChunkStep, ChunkedDecoder, Limits};
73///
74/// let wire = b"4\r\nWiki\r\n5;ext=1\r\npedia\r\n0\r\nDigest: x\r\n\r\nNEXT";
75/// let mut decoder = ChunkedDecoder::new(&Limits::new());
76/// let (mut pos, mut body) = (0, Vec::new());
77/// loop {
78///     match decoder.decode(&wire[pos..], false)? {
79///         ChunkStep::Data { skip, len } => {
80///             body.extend_from_slice(&wire[pos + skip..pos + skip + len]);
81///             pos += skip + len;
82///         }
83///         ChunkStep::Trailers { skip, len } => {
84///             assert_eq!(&wire[pos + skip..pos + skip + len], b"Digest: x\r\n\r\n");
85///             pos += skip + len;
86///         }
87///         ChunkStep::Done { consumed } => {
88///             pos += consumed;
89///             break;
90///         }
91///         ChunkStep::NeedMore { .. } => unreachable!("the whole body is here"),
92///         _ => unreachable!(),
93///     }
94/// }
95/// assert_eq!(body, b"Wikipedia");
96/// assert_eq!(&wire[pos..], b"NEXT");
97/// assert!(decoder.is_done());
98/// assert_eq!(decoder.total(), 9);
99/// # Ok::<(), http_net::HttpError>(())
100/// ```
101pub struct ChunkedDecoder {
102    decoder: Decoder,
103    /// Scans trailer sections; with [`TrailerMode::Locate`] it stores no
104    /// fields, so its field vector never allocates.
105    trailers: HeadScanner,
106    limits: Limits,
107    total: u64,
108    /// The trailer section of the last step was handed to the caller, who
109    /// consumes it.
110    trailers_handed: bool,
111}
112
113/// Shows progress and limits; never body bytes.
114impl core::fmt::Debug for ChunkedDecoder {
115    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
116        f.debug_struct("ChunkedDecoder")
117            .field("total", &self.total)
118            .field("done", &self.is_done())
119            .field("limits", &self.limits)
120            .finish()
121    }
122}
123
124impl ChunkedDecoder {
125    /// A decoder for one chunked body, with `limits` (body size, chunk-size
126    /// line, extension budget, trailer budgets).
127    ///
128    /// # Examples
129    ///
130    /// ```
131    /// let decoder = http_net::ChunkedDecoder::new(&http_net::Limits::new().with_max_body(64));
132    /// assert!(!decoder.is_done());
133    /// ```
134    #[must_use]
135    pub fn new(limits: &Limits) -> Self {
136        let mut decoder = Decoder::new();
137        decoder.start(BodyFraming::Chunked);
138        Self {
139            decoder,
140            trailers: HeadScanner::new(),
141            limits: *limits,
142            total: 0,
143            trailers_handed: false,
144        }
145    }
146
147    /// Decodes from `input`, which starts where the previous call's consumed
148    /// bytes ended. `eof` says no more input will come.
149    ///
150    /// # Errors
151    ///
152    /// Every chunk error of the connections: [`HttpError::InvalidChunkSize`],
153    /// [`HttpError::InvalidChunkExtension`],
154    /// [`HttpError::InvalidChunkTerminator`], [`HttpError::ChunkLineTooLong`],
155    /// [`HttpError::ChunkExtensionsTooLarge`], [`HttpError::BareLf`],
156    /// [`HttpError::BodyTooLarge`]; the field errors of the trailer section;
157    /// [`HttpError::UnexpectedEof`] when `eof` comes before the end. After an
158    /// error the body cannot be resynchronised: drop the connection.
159    ///
160    /// # Examples
161    ///
162    /// ```
163    /// use http_net::{ChunkStep, ChunkedDecoder, HttpError, Limits};
164    ///
165    /// let mut d = ChunkedDecoder::new(&Limits::new());
166    /// // A partial chunk-size line: nothing consumed, more needed.
167    /// assert_eq!(d.decode(b"1", false)?, ChunkStep::NeedMore { consumed: 0 });
168    /// assert_eq!(d.decode(b"1", true), Err(HttpError::UnexpectedEof));
169    ///
170    /// let mut d = ChunkedDecoder::new(&Limits::new());
171    /// assert_eq!(d.decode(b"5\nhello", false), Err(HttpError::BareLf));
172    /// # Ok::<(), http_net::HttpError>(())
173    /// ```
174    pub fn decode(&mut self, input: &[u8], eof: bool) -> Result<ChunkStep, HttpError> {
175        if self.trailers_handed {
176            self.trailers_handed = false;
177            self.decoder.take_trailers();
178        }
179        let mut consumed = 0;
180        let step = self.decoder.step_with(
181            input,
182            eof,
183            &self.limits,
184            &mut self.trailers,
185            TrailerMode::Locate,
186            &mut consumed,
187        )?;
188        Ok(match step {
189            Step::Data(len) => {
190                self.total += len as u64;
191                ChunkStep::Data {
192                    skip: consumed,
193                    len,
194                }
195            }
196            Step::Trailers => {
197                self.trailers_handed = true;
198                ChunkStep::Trailers {
199                    skip: consumed,
200                    len: self.decoder.trailer_len(),
201                }
202            }
203            Step::Done => ChunkStep::Done { consumed },
204            Step::NeedMore => ChunkStep::NeedMore { consumed },
205        })
206    }
207
208    /// Whether the whole body, trailers included, has been decoded.
209    ///
210    /// # Examples
211    ///
212    /// ```
213    /// use http_net::{ChunkStep, ChunkedDecoder, Limits};
214    ///
215    /// let mut d = ChunkedDecoder::new(&Limits::new());
216    /// assert_eq!(d.decode(b"0\r\n\r\n", false)?, ChunkStep::Done { consumed: 5 });
217    /// assert!(d.is_done());
218    /// # Ok::<(), http_net::HttpError>(())
219    /// ```
220    #[must_use]
221    pub fn is_done(&self) -> bool {
222        !self.trailers_handed && self.decoder.is_done()
223    }
224
225    /// The body bytes delivered so far, after chunked decoding.
226    ///
227    /// # Examples
228    ///
229    /// ```
230    /// use http_net::{ChunkedDecoder, Limits};
231    ///
232    /// let mut d = ChunkedDecoder::new(&Limits::new());
233    /// let _ = d.decode(b"3\r\nabc\r\n", false)?;
234    /// assert_eq!(d.total(), 3);
235    /// # Ok::<(), http_net::HttpError>(())
236    /// ```
237    #[must_use]
238    pub fn total(&self) -> u64 {
239        self.total
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use alloc::vec::Vec;
246
247    use super::*;
248
249    /// The body, the trailer bytes, and what followed.
250    type Decoded = (Vec<u8>, Vec<u8>, Vec<u8>);
251
252    /// Feeds `wire` in `piece`-sized appends, as a caller with its own buffer
253    /// would.
254    fn run(wire: &[u8], piece: usize, limits: &Limits) -> Result<Decoded, HttpError> {
255        let mut d = ChunkedDecoder::new(limits);
256        let mut buf: Vec<u8> = Vec::new();
257        let mut fed = 0;
258        let (mut body, mut trailers) = (Vec::new(), Vec::new());
259        loop {
260            let eof = fed == wire.len();
261            match d.decode(&buf, eof)? {
262                ChunkStep::Data { skip, len } => {
263                    body.extend_from_slice(&buf[skip..skip + len]);
264                    let _ = buf.drain(..skip + len);
265                }
266                ChunkStep::Trailers { skip, len } => {
267                    trailers.extend_from_slice(&buf[skip..skip + len]);
268                    let _ = buf.drain(..skip + len);
269                }
270                ChunkStep::Done { consumed } => {
271                    let _ = buf.drain(..consumed);
272                    buf.extend_from_slice(&wire[fed..]);
273                    return Ok((body, trailers, buf));
274                }
275                ChunkStep::NeedMore { consumed } => {
276                    let _ = buf.drain(..consumed);
277                    let end = (fed + piece).min(wire.len());
278                    buf.extend_from_slice(&wire[fed..end]);
279                    fed = end;
280                }
281            }
282        }
283    }
284
285    #[test]
286    fn test_decode_any_split_gives_the_same_body_and_trailers() {
287        let wire = b"4;a=b\r\nWiki\r\n5\r\npedia\r\n0\r\nX-T: 1\r\nY: 2\r\n\r\nGET";
288        let whole = run(wire, wire.len(), &Limits::new()).unwrap();
289        assert_eq!(whole.0, b"Wikipedia");
290        assert_eq!(whole.1, b"X-T: 1\r\nY: 2\r\n\r\n");
291        assert_eq!(whole.2, b"GET");
292        for piece in 1..wire.len() {
293            assert_eq!(run(wire, piece, &Limits::new()).unwrap(), whole, "{piece}");
294        }
295    }
296
297    #[test]
298    fn test_decode_without_trailers_reports_done() {
299        let (body, trailers, rest) = run(b"1\r\nx\r\n0\r\n\r\n", 3, &Limits::new()).unwrap();
300        assert_eq!(body, b"x");
301        assert!(trailers.is_empty());
302        assert!(rest.is_empty());
303    }
304
305    #[test]
306    fn test_decode_errors_match_the_connection_grammar() {
307        let cases: &[(&[u8], HttpError)] = &[
308            (b"x\r\n", HttpError::InvalidChunkSize),
309            (b"00000000000000001\r\n", HttpError::InvalidChunkSize),
310            (b"5;\r\n", HttpError::InvalidChunkExtension),
311            (b"1\r\naX", HttpError::InvalidChunkTerminator),
312            (b"5;a=b\nX", HttpError::BareLf),
313            (b"0\r\nBad Name: x\r\n\r\n", HttpError::InvalidHeaderName),
314            (b"0\r\n folded\r\n\r\n", HttpError::ObsFold),
315            (b"3\r\nab", HttpError::UnexpectedEof),
316        ];
317        for (wire, err) in cases {
318            assert_eq!(
319                run(wire, wire.len(), &Limits::new()).err(),
320                Some(*err),
321                "{wire:?}"
322            );
323        }
324        let limits = Limits::new().with_max_body(4);
325        assert_eq!(
326            run(b"5\r\nhello\r\n0\r\n\r\n", 64, &limits).err(),
327            Some(HttpError::BodyTooLarge)
328        );
329        let limits = Limits::new().with_max_trailer_fields(1);
330        assert_eq!(
331            run(b"0\r\nA: 1\r\nB: 2\r\n\r\n", 64, &limits).err(),
332            Some(HttpError::TooManyHeaders)
333        );
334    }
335
336    #[test]
337    fn test_total_and_done_track_the_body() {
338        let mut d = ChunkedDecoder::new(&Limits::new());
339        assert_eq!(
340            d.decode(b"2\r\nab\r\n", false),
341            Ok(ChunkStep::Data { skip: 3, len: 2 })
342        );
343        assert_eq!(d.total(), 2);
344        assert!(!d.is_done());
345        assert_eq!(
346            d.decode(b"\r\n0\r\nT: v\r\n\r\n", false),
347            Ok(ChunkStep::Trailers { skip: 5, len: 8 })
348        );
349        assert!(!d.is_done());
350        assert_eq!(d.decode(b"", false), Ok(ChunkStep::Done { consumed: 0 }));
351        assert!(d.is_done());
352        assert_eq!(
353            d.decode(b"more", false),
354            Ok(ChunkStep::Done { consumed: 0 })
355        );
356    }
357}