Skip to main content

io_http/rfc9112/
chunk.rs

1//! I/O-free coroutine decoding a `Transfer-Encoding: chunked` body
2//! ([RFC 9112 §7.1]) into a single buffer. For incremental consumption
3//! use [`crate::rfc9112::chunk_stream::Http11ChunksReadStream`].
4//!
5//! [RFC 9112 §7.1]: https://www.rfc-editor.org/rfc/rfc9112#section-7.1
6
7use core::mem;
8
9use alloc::{
10    string::{String, ToString},
11    vec::Vec,
12};
13
14use log::{debug, trace};
15use thiserror::Error;
16
17use crate::{
18    coroutine::*,
19    rfc9110::{chars::CRLF, send::HTTP_DEFAULT_MAX_BODY_LEN},
20    scan::{crlf_position, position},
21};
22
23/// Maximum size in bytes of a chunk size line, extensions included
24/// and CRLF excluded: 4 KiB.
25pub const HTTP11_MAX_CHUNK_LINE_LEN: usize = 4 * 1024;
26
27/// Failure causes during the HTTP/1.1 chunked-body read flow.
28#[derive(Debug, Error)]
29pub enum Http11ChunksReadError {
30    /// The chunk size line is not a valid hexadecimal integer.
31    #[error("HTTP/1.1 read chunks failed: invalid chunk size `{0}`")]
32    InvalidChunkSize(String),
33    /// The stream reached EOF before the zero-length terminator.
34    #[error("HTTP/1.1 read chunks failed: reached unexpected EOF")]
35    Eof,
36    /// The chunk size line exceeds [`HTTP11_MAX_CHUNK_LINE_LEN`].
37    #[error("HTTP/1.1 read chunks failed: chunk size line exceeds {0} bytes")]
38    ChunkLineTooLarge(usize),
39    /// The decoded body would exceed [`Http11ChunksRead::max_body_len`].
40    #[error("HTTP/1.1 read chunks failed: body exceeds {0} bytes")]
41    BodyTooLarge(usize),
42}
43
44/// Terminal output of [`Http11ChunksRead`].
45#[derive(Debug)]
46pub struct Http11ChunksReadOutput {
47    /// The decoded body, all chunks concatenated.
48    pub body: Vec<u8>,
49    /// Bytes already buffered past the zero-length terminator.
50    pub remaining: Vec<u8>,
51}
52
53#[derive(Debug, Default)]
54enum State {
55    #[default]
56    ChunkSize,
57    ChunkData(usize),
58}
59
60/// I/O-free coroutine to read an HTTP response body using chunked
61/// transfer coding.
62#[derive(Debug)]
63pub struct Http11ChunksRead {
64    /// Maximum size in bytes of the decoded body, all chunks
65    /// concatenated. A chunk announcing more fails the read with
66    /// [`Http11ChunksReadError::BodyTooLarge`] before any of it is
67    /// buffered. Defaults to [`HTTP_DEFAULT_MAX_BODY_LEN`].
68    pub max_body_len: usize,
69    state: State,
70    wants_read: bool,
71    last_chunk: bool,
72    buf: Vec<u8>,
73    /// Where the bytes not consumed yet start in `buf`.
74    start: usize,
75    body: Vec<u8>,
76}
77
78impl Default for Http11ChunksRead {
79    fn default() -> Self {
80        Self {
81            max_body_len: HTTP_DEFAULT_MAX_BODY_LEN,
82            state: State::default(),
83            wants_read: false,
84            last_chunk: false,
85            buf: Vec::new(),
86            start: 0,
87            body: Vec::new(),
88        }
89    }
90}
91
92impl HttpCoroutine for Http11ChunksRead {
93    type Yield = HttpYield;
94    type Return = Result<Http11ChunksReadOutput, Http11ChunksReadError>;
95
96    fn resume(&mut self, arg: Option<&[u8]>) -> HttpCoroutineState<Self::Yield, Self::Return> {
97        match arg {
98            Some(&[]) => return HttpCoroutineState::Complete(Err(Http11ChunksReadError::Eof)),
99            Some(data) => {
100                self.buf.drain(..self.start);
101                self.start = 0;
102                self.buf.extend_from_slice(data);
103            }
104            None => {}
105        }
106
107        loop {
108            if self.wants_read {
109                self.wants_read = false;
110                return HttpCoroutineState::Yielded(HttpYield::WantsRead);
111            }
112
113            if self.last_chunk {
114                self.buf.drain(..self.start);
115                self.start = 0;
116
117                let body = mem::take(&mut self.body);
118                let remaining = mem::take(&mut self.buf);
119                return HttpCoroutineState::Complete(Ok(Http11ChunksReadOutput {
120                    body,
121                    remaining,
122                }));
123            }
124
125            let rest = &self.buf[self.start..];
126
127            match self.state {
128                State::ChunkSize => {
129                    let line_window = rest.len().min(HTTP11_MAX_CHUNK_LINE_LEN + CRLF.len());
130
131                    let Some(crlf) = crlf_position(&rest[..line_window]) else {
132                        if line_window == HTTP11_MAX_CHUNK_LINE_LEN + CRLF.len() {
133                            let err =
134                                Http11ChunksReadError::ChunkLineTooLarge(HTTP11_MAX_CHUNK_LINE_LEN);
135                            return HttpCoroutineState::Complete(Err(err));
136                        }
137
138                        self.wants_read = true;
139                        continue;
140                    };
141
142                    let ext = match position(&rest[..crlf], |b| b == b';') {
143                        None => crlf,
144                        Some(ext) => {
145                            let exts = String::from_utf8_lossy(rest[ext..crlf].trim_ascii());
146                            trace!("ignore extension(s) `{exts}`");
147                            ext
148                        }
149                    };
150
151                    let chunk_size = String::from_utf8_lossy(rest[..ext].trim_ascii());
152
153                    let is_hex = chunk_size.bytes().all(|b| b.is_ascii_hexdigit());
154
155                    let Some(n) = is_hex
156                        .then(|| usize::from_str_radix(&chunk_size, 16).ok())
157                        .flatten()
158                    else {
159                        let chunk_size = chunk_size.to_string();
160                        let err = Http11ChunksReadError::InvalidChunkSize(chunk_size);
161                        return HttpCoroutineState::Complete(Err(err));
162                    };
163
164                    // NOTE: the checked addition keeps `n + CRLF`
165                    // addressable even when the limit is `usize::MAX`.
166                    let room = self.max_body_len.saturating_sub(self.body.len());
167                    if n > room || n.checked_add(CRLF.len()).is_none() {
168                        let err = Http11ChunksReadError::BodyTooLarge(self.max_body_len);
169                        return HttpCoroutineState::Complete(Err(err));
170                    }
171
172                    self.start += crlf + CRLF.len();
173                    debug!("reading chunk of {n} bytes");
174                    self.state = State::ChunkData(n);
175                }
176                State::ChunkData(size) if rest.len() < size + CRLF.len() => {
177                    trace!("received incomplete chunk data {}/{size}", rest.len());
178                    self.wants_read = true;
179                    continue;
180                }
181                State::ChunkData(size) => {
182                    self.body.extend_from_slice(&rest[..size]);
183                    self.start += size + CRLF.len();
184                    self.state = State::ChunkSize;
185                    self.last_chunk = size == 0;
186                }
187            }
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use crate::rfc9112::chunk::*;
195
196    #[test]
197    fn single_chunk() {
198        let mut coroutine = Http11ChunksRead::default();
199        let out = expect_complete_ok(&mut coroutine, Some(b"5\r\nhello\r\n0\r\n\r\n"));
200        assert_eq!(out.body, b"hello");
201        assert_eq!(out.remaining, b"");
202    }
203
204    #[test]
205    fn empty_body() {
206        let mut coroutine = Http11ChunksRead::default();
207        let out = expect_complete_ok(&mut coroutine, Some(b"0\r\n\r\n"));
208        assert!(out.body.is_empty());
209    }
210
211    #[test]
212    fn ignored_extension() {
213        let mut coroutine = Http11ChunksRead::default();
214        let out = expect_complete_ok(&mut coroutine, Some(b"5;ext\r\nHello\r\n0\r\n\r\n"));
215        assert_eq!(out.body, b"Hello");
216    }
217
218    #[test]
219    fn invalid_chunk_size() {
220        let mut coroutine = Http11ChunksRead::default();
221        let err = expect_complete_err(&mut coroutine, Some(b":\r\n0\r\n\r\n"));
222        let Http11ChunksReadError::InvalidChunkSize(s) = err else {
223            panic!("expected InvalidChunkSize, got {err:?}");
224        };
225        assert_eq!(s, ":");
226    }
227
228    #[test]
229    fn signed_chunk_size_errors() {
230        let mut coroutine = Http11ChunksRead::default();
231        let err = expect_complete_err(&mut coroutine, Some(b"+5\r\nhello\r\n0\r\n\r\n"));
232        assert!(matches!(err, Http11ChunksReadError::InvalidChunkSize(_)));
233    }
234
235    #[test]
236    fn max_usize_chunk_size_errors() {
237        let mut coroutine = Http11ChunksRead {
238            max_body_len: usize::MAX,
239            ..Http11ChunksRead::default()
240        };
241        let err = expect_complete_err(&mut coroutine, Some(b"FFFFFFFFFFFFFFFF\r\n"));
242        assert!(matches!(err, Http11ChunksReadError::BodyTooLarge(_)));
243    }
244
245    #[test]
246    fn chunk_over_body_limit_errors() {
247        let mut coroutine = Http11ChunksRead {
248            max_body_len: 8,
249            ..Http11ChunksRead::default()
250        };
251        let err = expect_complete_err(&mut coroutine, Some(b"5\r\nhello\r\n4\r\n"));
252        assert!(matches!(err, Http11ChunksReadError::BodyTooLarge(8)));
253    }
254
255    #[test]
256    fn chunk_line_over_limit_errors() {
257        let mut coroutine = Http11ChunksRead::default();
258        let line = [b'0'; HTTP11_MAX_CHUNK_LINE_LEN + 2];
259        let err = expect_complete_err(&mut coroutine, Some(&line));
260        assert!(matches!(err, Http11ChunksReadError::ChunkLineTooLarge(_)));
261    }
262
263    #[test]
264    fn incomplete_chunk_size_then_resume() {
265        let mut coroutine = Http11ChunksRead::default();
266        expect_wants_read(&mut coroutine, Some(b"5\r"));
267        let out = expect_complete_ok(&mut coroutine, Some(b"\nHello\r\n0\r\n\r\n"));
268        assert_eq!(out.body, b"Hello");
269    }
270
271    #[test]
272    fn incomplete_chunk_data_then_resume() {
273        let mut coroutine = Http11ChunksRead::default();
274        expect_wants_read(&mut coroutine, Some(b"5\r\nHell"));
275        let out = expect_complete_ok(&mut coroutine, Some(b"o\r\n0\r\n\r\n"));
276        assert_eq!(out.body, b"Hello");
277    }
278
279    #[test]
280    fn wiki_ru_multi_chunk() {
281        let encoded = "9\r\nchunk 1, \r\n7\r\nchunk 2\r\n0\r\n\r\n";
282        let mut coroutine = Http11ChunksRead::default();
283        let out = expect_complete_ok(&mut coroutine, Some(encoded.as_bytes()));
284        assert_eq!(out.body, b"chunk 1, chunk 2");
285    }
286
287    #[test]
288    fn github_frewsxcv_test_vector() {
289        let encoded = "3\r\nhel\r\nb\r\nlo world!!!\r\n0\r\n\r\n";
290        let mut coroutine = Http11ChunksRead::default();
291        let out = expect_complete_ok(&mut coroutine, Some(encoded.as_bytes()));
292        assert_eq!(out.body, b"hello world!!!");
293    }
294
295    fn expect_wants_read(cor: &mut Http11ChunksRead, arg: Option<&[u8]>) {
296        match cor.resume(arg) {
297            HttpCoroutineState::Yielded(HttpYield::WantsRead) => {}
298            state => panic!("expected WantsRead, got {state:?}"),
299        }
300    }
301
302    fn expect_complete_ok(
303        cor: &mut Http11ChunksRead,
304        arg: Option<&[u8]>,
305    ) -> Http11ChunksReadOutput {
306        match cor.resume(arg) {
307            HttpCoroutineState::Complete(Ok(out)) => out,
308            state => panic!("expected Complete(Ok), got {state:?}"),
309        }
310    }
311
312    fn expect_complete_err(
313        cor: &mut Http11ChunksRead,
314        arg: Option<&[u8]>,
315    ) -> Http11ChunksReadError {
316        match cor.resume(arg) {
317            HttpCoroutineState::Complete(Err(err)) => err,
318            state => panic!("expected Complete(Err), got {state:?}"),
319        }
320    }
321}