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::trace;
15use memchr::{memchr, memmem};
16use thiserror::Error;
17
18use crate::{coroutine::*, rfc9110::chars::CRLF};
19
20/// Failure causes during the HTTP/1.1 chunked-body streaming flow.
21#[derive(Debug, Error)]
22pub enum Http11ReadChunksStreamError {
23    #[error("HTTP/1.1 read chunks failed: invalid chunk size `{0}`")]
24    InvalidChunkSize(String),
25}
26
27/// Per-step yield emitted by [`Http11ReadChunksStream`]; adds
28/// [`Self::Frame`] to the standard [`HttpYield`] shape.
29#[derive(Debug)]
30pub enum Http11ReadChunksStreamYield {
31    WantsRead,
32    Frame { body: Vec<u8> },
33}
34
35#[derive(Debug, Default)]
36enum State {
37    #[default]
38    ChunkSize,
39    ChunkData(usize),
40}
41
42/// I/O-free streaming chunked-body decoder. `Complete(Ok(remaining))`
43/// carries bytes already buffered past the zero-length terminator.
44#[derive(Debug, Default)]
45pub struct Http11ReadChunksStream {
46    state: State,
47    wants_read: bool,
48    done: bool,
49    buf: Vec<u8>,
50}
51
52impl HttpCoroutine for Http11ReadChunksStream {
53    type Yield = Http11ReadChunksStreamYield;
54    type Return = Result<Vec<u8>, Http11ReadChunksStreamError>;
55
56    fn resume(&mut self, arg: Option<&[u8]>) -> HttpCoroutineState<Self::Yield, Self::Return> {
57        if let Some(data) = arg {
58            self.buf.extend_from_slice(data);
59        }
60
61        loop {
62            if self.wants_read {
63                self.wants_read = false;
64                return HttpCoroutineState::Yielded(Http11ReadChunksStreamYield::WantsRead);
65            }
66
67            if self.done {
68                let remaining = mem::take(&mut self.buf);
69                return HttpCoroutineState::Complete(Ok(remaining));
70            }
71
72            match self.state {
73                State::ChunkSize => {
74                    let Some(crlf) = memmem::find(&self.buf, &CRLF) else {
75                        self.wants_read = true;
76                        continue;
77                    };
78
79                    let ext = match memchr(b';', &self.buf[..crlf]) {
80                        None => crlf,
81                        Some(ext) => {
82                            let exts = String::from_utf8_lossy(self.buf[ext..crlf].trim_ascii());
83                            trace!("ignore extension(s) `{exts}`");
84                            ext
85                        }
86                    };
87
88                    let chunk_size = String::from_utf8_lossy(self.buf[..ext].trim_ascii());
89
90                    let Ok(n) = usize::from_str_radix(&chunk_size, 16) else {
91                        let chunk_size = chunk_size.to_string();
92                        let err = Http11ReadChunksStreamError::InvalidChunkSize(chunk_size);
93                        return HttpCoroutineState::Complete(Err(err));
94                    };
95
96                    self.buf.drain(..crlf + CRLF.len());
97                    trace!("reading chunk of {n} bytes");
98                    self.state = State::ChunkData(n);
99                }
100                State::ChunkData(size) if self.buf.len() < size + CRLF.len() => {
101                    trace!("received incomplete chunk data {}/{size}", self.buf.len());
102                    self.wants_read = true;
103                    continue;
104                }
105                State::ChunkData(0) => {
106                    self.buf.drain(..CRLF.len());
107                    self.state = State::ChunkSize;
108                    self.done = true;
109                }
110                State::ChunkData(size) => {
111                    let body: Vec<u8> = self.buf.drain(..size).collect();
112                    self.buf.drain(..CRLF.len());
113                    self.state = State::ChunkSize;
114                    return HttpCoroutineState::Yielded(Http11ReadChunksStreamYield::Frame {
115                        body,
116                    });
117                }
118            }
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use alloc::vec;
126
127    use super::*;
128
129    #[test]
130    fn single_chunk() {
131        let frames = collect_all(b"5\r\nhello\r\n0\r\n\r\n");
132        assert_eq!(frames, vec![b"hello".to_vec()]);
133    }
134
135    #[test]
136    fn two_chunks_yielded_separately() {
137        let frames = collect_all(b"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n");
138        assert_eq!(frames, vec![b"hello".to_vec(), b" world".to_vec()]);
139    }
140
141    #[test]
142    fn empty_body() {
143        let frames = collect_all(b"0\r\n\r\n");
144        assert!(frames.is_empty());
145    }
146
147    #[test]
148    fn ignored_extension() {
149        let frames = collect_all(b"5;ext\r\nHello\r\n0\r\n\r\n");
150        assert_eq!(frames, vec![b"Hello".to_vec()]);
151    }
152
153    #[test]
154    fn invalid_chunk_size() {
155        let mut coroutine = Http11ReadChunksStream::default();
156        let err = expect_complete_err(&mut coroutine, Some(b":\r\n0\r\n\r\n"));
157        let Http11ReadChunksStreamError::InvalidChunkSize(s) = err;
158        assert_eq!(s, ":");
159    }
160
161    #[test]
162    fn incomplete_chunk_size_then_resume() {
163        let mut coroutine = Http11ReadChunksStream::default();
164        expect_wants_read(&mut coroutine, Some(b"5\r"));
165        let body = expect_frame(&mut coroutine, Some(b"\nHello\r\n0\r\n\r\n"));
166        assert_eq!(body, b"Hello");
167        let remaining = expect_complete_ok(&mut coroutine, None);
168        assert!(remaining.is_empty());
169    }
170
171    #[test]
172    fn remaining_bytes_after_terminator() {
173        let mut coroutine = Http11ReadChunksStream::default();
174        let body = expect_frame(&mut coroutine, Some(b"5\r\nhello\r\n0\r\n\r\nNEXT"));
175        assert_eq!(body, b"hello");
176        let remaining = expect_complete_ok(&mut coroutine, None);
177        assert_eq!(remaining, b"NEXT");
178    }
179
180    // --- utils
181
182    fn collect_all(encoded: &[u8]) -> Vec<Vec<u8>> {
183        let mut coroutine = Http11ReadChunksStream::default();
184        let mut arg: Option<&[u8]> = Some(encoded);
185        let mut frames = Vec::new();
186
187        loop {
188            match coroutine.resume(arg.take()) {
189                HttpCoroutineState::Yielded(Http11ReadChunksStreamYield::Frame { body }) => {
190                    frames.push(body);
191                }
192                HttpCoroutineState::Complete(Ok(remaining)) => {
193                    assert!(remaining.is_empty(), "unexpected remaining bytes");
194                    return frames;
195                }
196                state => panic!("expected Frame or Complete, got {state:?}"),
197            }
198        }
199    }
200
201    fn expect_wants_read(cor: &mut Http11ReadChunksStream, arg: Option<&[u8]>) {
202        match cor.resume(arg) {
203            HttpCoroutineState::Yielded(Http11ReadChunksStreamYield::WantsRead) => {}
204            state => panic!("expected WantsRead, got {state:?}"),
205        }
206    }
207
208    fn expect_frame(cor: &mut Http11ReadChunksStream, arg: Option<&[u8]>) -> Vec<u8> {
209        match cor.resume(arg) {
210            HttpCoroutineState::Yielded(Http11ReadChunksStreamYield::Frame { body }) => body,
211            state => panic!("expected Frame, got {state:?}"),
212        }
213    }
214
215    fn expect_complete_ok(cor: &mut Http11ReadChunksStream, arg: Option<&[u8]>) -> Vec<u8> {
216        match cor.resume(arg) {
217            HttpCoroutineState::Complete(Ok(remaining)) => remaining,
218            state => panic!("expected Complete(Ok), got {state:?}"),
219        }
220    }
221
222    fn expect_complete_err(
223        cor: &mut Http11ReadChunksStream,
224        arg: Option<&[u8]>,
225    ) -> Http11ReadChunksStreamError {
226        match cor.resume(arg) {
227            HttpCoroutineState::Complete(Err(err)) => err,
228            state => panic!("expected Complete(Err), got {state:?}"),
229        }
230    }
231}