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}