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}