Skip to main content

io_http/rfc9112/
chunk_stream.rs

1//! I/O-free coroutine decoding a `Transfer-Encoding: chunked` body
2//! ([RFC 9112 §7.1]) one chunk at a time; suitable for SSE and other
3//! long-lived streams.
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    rfc9112::chunk::HTTP11_MAX_CHUNK_LINE_LEN,
21    scan::{crlf_position, position},
22};
23
24/// Failure causes during the HTTP/1.1 chunked-body streaming flow.
25#[derive(Debug, Error)]
26pub enum Http11ChunksReadStreamError {
27    /// The chunk size line is not a valid hexadecimal integer.
28    #[error("HTTP/1.1 read chunks failed: invalid chunk size `{0}`")]
29    InvalidChunkSize(String),
30    /// The stream reached EOF before the zero-length terminator.
31    #[error("HTTP/1.1 read chunks failed: reached unexpected EOF")]
32    Eof,
33    /// The chunk size line exceeds [`HTTP11_MAX_CHUNK_LINE_LEN`].
34    #[error("HTTP/1.1 read chunks failed: chunk size line exceeds {0} bytes")]
35    ChunkLineTooLarge(usize),
36    /// A chunk exceeds [`Http11ChunksReadStream::max_chunk_len`].
37    #[error("HTTP/1.1 read chunks failed: chunk exceeds {0} bytes")]
38    ChunkTooLarge(usize),
39}
40
41/// Per-step yield emitted by [`Http11ChunksReadStream`]; adds
42/// [`Self::Frame`] to the standard [`HttpYield`] shape.
43#[derive(Debug)]
44pub enum Http11ChunksReadStreamYield {
45    /// The coroutine wants bytes read from the stream and handed back
46    /// on the next resume.
47    WantsRead,
48    /// One decoded chunk, yielded as soon as its bytes are available.
49    Frame {
50        /// The decoded chunk body.
51        body: Vec<u8>,
52    },
53}
54
55#[derive(Debug, Default)]
56enum State {
57    #[default]
58    ChunkSize,
59    ChunkData(usize),
60}
61
62/// I/O-free streaming chunked-body decoder. `Complete(Ok(remaining))`
63/// carries bytes already buffered past the zero-length terminator.
64#[derive(Debug)]
65pub struct Http11ChunksReadStream {
66    /// Maximum size in bytes of a single chunk. A chunk announcing more
67    /// fails the read with [`Http11ChunksReadStreamError::ChunkTooLarge`]
68    /// before any of it is buffered. The stream itself is unbounded.
69    /// Defaults to [`HTTP_DEFAULT_MAX_BODY_LEN`].
70    pub max_chunk_len: usize,
71    state: State,
72    wants_read: bool,
73    done: bool,
74    buf: Vec<u8>,
75    /// Where the bytes not consumed yet start in `buf`.
76    start: usize,
77}
78
79impl Default for Http11ChunksReadStream {
80    fn default() -> Self {
81        Self {
82            max_chunk_len: HTTP_DEFAULT_MAX_BODY_LEN,
83            state: State::default(),
84            wants_read: false,
85            done: false,
86            buf: Vec::new(),
87            start: 0,
88        }
89    }
90}
91
92impl HttpCoroutine for Http11ChunksReadStream {
93    type Yield = Http11ChunksReadStreamYield;
94    type Return = Result<Vec<u8>, Http11ChunksReadStreamError>;
95
96    fn resume(&mut self, arg: Option<&[u8]>) -> HttpCoroutineState<Self::Yield, Self::Return> {
97        match arg {
98            Some(&[]) => {
99                return HttpCoroutineState::Complete(Err(Http11ChunksReadStreamError::Eof));
100            }
101            Some(data) => {
102                self.buf.drain(..self.start);
103                self.start = 0;
104                self.buf.extend_from_slice(data);
105            }
106            None => {}
107        }
108
109        loop {
110            if self.wants_read {
111                self.wants_read = false;
112                return HttpCoroutineState::Yielded(Http11ChunksReadStreamYield::WantsRead);
113            }
114
115            if self.done {
116                self.buf.drain(..self.start);
117                self.start = 0;
118
119                let remaining = mem::take(&mut self.buf);
120                return HttpCoroutineState::Complete(Ok(remaining));
121            }
122
123            let rest = &self.buf[self.start..];
124
125            match self.state {
126                State::ChunkSize => {
127                    let line_window = rest.len().min(HTTP11_MAX_CHUNK_LINE_LEN + CRLF.len());
128
129                    let Some(crlf) = crlf_position(&rest[..line_window]) else {
130                        if line_window == HTTP11_MAX_CHUNK_LINE_LEN + CRLF.len() {
131                            let limit = HTTP11_MAX_CHUNK_LINE_LEN;
132                            let err = Http11ChunksReadStreamError::ChunkLineTooLarge(limit);
133                            return HttpCoroutineState::Complete(Err(err));
134                        }
135
136                        self.wants_read = true;
137                        continue;
138                    };
139
140                    let ext = match position(&rest[..crlf], |b| b == b';') {
141                        None => crlf,
142                        Some(ext) => {
143                            let exts = String::from_utf8_lossy(rest[ext..crlf].trim_ascii());
144                            trace!("ignore extension(s) `{exts}`");
145                            ext
146                        }
147                    };
148
149                    let chunk_size = String::from_utf8_lossy(rest[..ext].trim_ascii());
150
151                    let is_hex = chunk_size.bytes().all(|b| b.is_ascii_hexdigit());
152
153                    let Some(n) = is_hex
154                        .then(|| usize::from_str_radix(&chunk_size, 16).ok())
155                        .flatten()
156                    else {
157                        let chunk_size = chunk_size.to_string();
158                        let err = Http11ChunksReadStreamError::InvalidChunkSize(chunk_size);
159                        return HttpCoroutineState::Complete(Err(err));
160                    };
161
162                    // NOTE: the checked addition keeps `n + CRLF`
163                    // addressable even when the limit is `usize::MAX`.
164                    if n > self.max_chunk_len || n.checked_add(CRLF.len()).is_none() {
165                        let err = Http11ChunksReadStreamError::ChunkTooLarge(self.max_chunk_len);
166                        return HttpCoroutineState::Complete(Err(err));
167                    }
168
169                    self.start += crlf + CRLF.len();
170                    debug!("reading chunk of {n} bytes");
171                    self.state = State::ChunkData(n);
172                }
173                State::ChunkData(size) if rest.len() < size + CRLF.len() => {
174                    trace!("received incomplete chunk data {}/{size}", rest.len());
175                    self.wants_read = true;
176                    continue;
177                }
178                State::ChunkData(0) => {
179                    self.start += CRLF.len();
180                    self.state = State::ChunkSize;
181                    self.done = true;
182                }
183                State::ChunkData(size) => {
184                    let body = rest[..size].to_vec();
185                    self.start += size + CRLF.len();
186                    self.state = State::ChunkSize;
187                    return HttpCoroutineState::Yielded(Http11ChunksReadStreamYield::Frame {
188                        body,
189                    });
190                }
191            }
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use alloc::vec;
199
200    use crate::rfc9112::chunk_stream::*;
201
202    #[test]
203    fn single_chunk() {
204        let frames = collect_all(b"5\r\nhello\r\n0\r\n\r\n");
205        assert_eq!(frames, vec![b"hello".to_vec()]);
206    }
207
208    #[test]
209    fn two_chunks_yielded_separately() {
210        let frames = collect_all(b"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n");
211        assert_eq!(frames, vec![b"hello".to_vec(), b" world".to_vec()]);
212    }
213
214    #[test]
215    fn empty_body() {
216        let frames = collect_all(b"0\r\n\r\n");
217        assert!(frames.is_empty());
218    }
219
220    #[test]
221    fn ignored_extension() {
222        let frames = collect_all(b"5;ext\r\nHello\r\n0\r\n\r\n");
223        assert_eq!(frames, vec![b"Hello".to_vec()]);
224    }
225
226    #[test]
227    fn invalid_chunk_size() {
228        let mut coroutine = Http11ChunksReadStream::default();
229        let err = expect_complete_err(&mut coroutine, Some(b":\r\n0\r\n\r\n"));
230        let Http11ChunksReadStreamError::InvalidChunkSize(s) = err else {
231            panic!("expected InvalidChunkSize, got {err:?}");
232        };
233        assert_eq!(s, ":");
234    }
235
236    #[test]
237    fn max_usize_chunk_size_errors() {
238        let mut coroutine = Http11ChunksReadStream {
239            max_chunk_len: usize::MAX,
240            ..Http11ChunksReadStream::default()
241        };
242        let err = expect_complete_err(&mut coroutine, Some(b"FFFFFFFFFFFFFFFF\r\n"));
243        assert!(matches!(err, Http11ChunksReadStreamError::ChunkTooLarge(_)));
244    }
245
246    #[test]
247    fn chunk_over_limit_errors() {
248        let mut coroutine = Http11ChunksReadStream {
249            max_chunk_len: 4,
250            ..Http11ChunksReadStream::default()
251        };
252        let err = expect_complete_err(&mut coroutine, Some(b"5\r\nhello\r\n"));
253        assert!(matches!(err, Http11ChunksReadStreamError::ChunkTooLarge(4)));
254    }
255
256    #[test]
257    fn chunk_line_over_limit_errors() {
258        let mut coroutine = Http11ChunksReadStream::default();
259        let line = [b'0'; HTTP11_MAX_CHUNK_LINE_LEN + 2];
260        let err = expect_complete_err(&mut coroutine, Some(&line));
261        assert!(matches!(
262            err,
263            Http11ChunksReadStreamError::ChunkLineTooLarge(_)
264        ));
265    }
266
267    #[test]
268    fn incomplete_chunk_size_then_resume() {
269        let mut coroutine = Http11ChunksReadStream::default();
270        expect_wants_read(&mut coroutine, Some(b"5\r"));
271        let body = expect_frame(&mut coroutine, Some(b"\nHello\r\n0\r\n\r\n"));
272        assert_eq!(body, b"Hello");
273        let remaining = expect_complete_ok(&mut coroutine, None);
274        assert!(remaining.is_empty());
275    }
276
277    #[test]
278    fn remaining_bytes_after_terminator() {
279        let mut coroutine = Http11ChunksReadStream::default();
280        let body = expect_frame(&mut coroutine, Some(b"5\r\nhello\r\n0\r\n\r\nNEXT"));
281        assert_eq!(body, b"hello");
282        let remaining = expect_complete_ok(&mut coroutine, None);
283        assert_eq!(remaining, b"NEXT");
284    }
285
286    fn collect_all(encoded: &[u8]) -> Vec<Vec<u8>> {
287        let mut coroutine = Http11ChunksReadStream::default();
288        let mut arg: Option<&[u8]> = Some(encoded);
289        let mut frames = Vec::new();
290
291        loop {
292            match coroutine.resume(arg.take()) {
293                HttpCoroutineState::Yielded(Http11ChunksReadStreamYield::Frame { body }) => {
294                    frames.push(body);
295                }
296                HttpCoroutineState::Complete(Ok(remaining)) => {
297                    assert!(remaining.is_empty(), "unexpected remaining bytes");
298                    return frames;
299                }
300                state => panic!("expected Frame or Complete, got {state:?}"),
301            }
302        }
303    }
304
305    fn expect_wants_read(cor: &mut Http11ChunksReadStream, arg: Option<&[u8]>) {
306        match cor.resume(arg) {
307            HttpCoroutineState::Yielded(Http11ChunksReadStreamYield::WantsRead) => {}
308            state => panic!("expected WantsRead, got {state:?}"),
309        }
310    }
311
312    fn expect_frame(cor: &mut Http11ChunksReadStream, arg: Option<&[u8]>) -> Vec<u8> {
313        match cor.resume(arg) {
314            HttpCoroutineState::Yielded(Http11ChunksReadStreamYield::Frame { body }) => body,
315            state => panic!("expected Frame, got {state:?}"),
316        }
317    }
318
319    fn expect_complete_ok(cor: &mut Http11ChunksReadStream, arg: Option<&[u8]>) -> Vec<u8> {
320        match cor.resume(arg) {
321            HttpCoroutineState::Complete(Ok(remaining)) => remaining,
322            state => panic!("expected Complete(Ok), got {state:?}"),
323        }
324    }
325
326    fn expect_complete_err(
327        cor: &mut Http11ChunksReadStream,
328        arg: Option<&[u8]>,
329    ) -> Http11ChunksReadStreamError {
330        match cor.resume(arg) {
331            HttpCoroutineState::Complete(Err(err)) => err,
332            state => panic!("expected Complete(Err), got {state:?}"),
333        }
334    }
335}