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