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