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 memchr::{memchr, memmem};
16use thiserror::Error;
17
18use crate::{coroutine::*, rfc9110::chars::CRLF};
19
20/// Failure causes during the HTTP/1.1 chunked-body read flow.
21#[derive(Debug, Error)]
22pub enum Http11ChunksReadError {
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/// Terminal output of [`Http11ChunksRead`].
29#[derive(Debug)]
30pub struct Http11ChunksReadOutput {
31    /// The decoded body, all chunks concatenated.
32    pub body: Vec<u8>,
33    /// Bytes already buffered past the zero-length terminator.
34    pub remaining: Vec<u8>,
35}
36
37#[derive(Debug, Default)]
38enum State {
39    #[default]
40    ChunkSize,
41    ChunkData(usize),
42}
43
44/// I/O-free coroutine to read an HTTP response body using chunked
45/// transfer coding.
46#[derive(Debug, Default)]
47pub struct Http11ChunksRead {
48    state: State,
49    wants_read: bool,
50    last_chunk: bool,
51    buf: Vec<u8>,
52    body: Vec<u8>,
53}
54
55impl HttpCoroutine for Http11ChunksRead {
56    type Yield = HttpYield;
57    type Return = Result<Http11ChunksReadOutput, Http11ChunksReadError>;
58
59    fn resume(&mut self, arg: Option<&[u8]>) -> HttpCoroutineState<Self::Yield, Self::Return> {
60        if let Some(data) = arg {
61            self.buf.extend_from_slice(data);
62        }
63
64        loop {
65            if self.wants_read {
66                self.wants_read = false;
67                return HttpCoroutineState::Yielded(HttpYield::WantsRead);
68            }
69
70            if self.last_chunk {
71                let body = mem::take(&mut self.body);
72                let remaining = mem::take(&mut self.buf);
73                return HttpCoroutineState::Complete(Ok(Http11ChunksReadOutput {
74                    body,
75                    remaining,
76                }));
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 = Http11ChunksReadError::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(size) => {
113                    self.body.extend(self.buf.drain(..size));
114                    self.buf.drain(..CRLF.len());
115                    self.state = State::ChunkSize;
116                    self.last_chunk = size == 0;
117                }
118            }
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use crate::{coroutine::*, rfc9112::chunk::*};
126
127    #[test]
128    fn single_chunk() {
129        let mut coroutine = Http11ChunksRead::default();
130        let out = expect_complete_ok(&mut coroutine, Some(b"5\r\nhello\r\n0\r\n\r\n"));
131        assert_eq!(out.body, b"hello");
132        assert_eq!(out.remaining, b"");
133    }
134
135    #[test]
136    fn empty_body() {
137        let mut coroutine = Http11ChunksRead::default();
138        let out = expect_complete_ok(&mut coroutine, Some(b"0\r\n\r\n"));
139        assert!(out.body.is_empty());
140    }
141
142    #[test]
143    fn ignored_extension() {
144        let mut coroutine = Http11ChunksRead::default();
145        let out = expect_complete_ok(&mut coroutine, Some(b"5;ext\r\nHello\r\n0\r\n\r\n"));
146        assert_eq!(out.body, b"Hello");
147    }
148
149    #[test]
150    fn invalid_chunk_size() {
151        let mut coroutine = Http11ChunksRead::default();
152        let err = expect_complete_err(&mut coroutine, Some(b":\r\n0\r\n\r\n"));
153        let Http11ChunksReadError::InvalidChunkSize(s) = err;
154        assert_eq!(s, ":");
155    }
156
157    #[test]
158    fn incomplete_chunk_size_then_resume() {
159        let mut coroutine = Http11ChunksRead::default();
160        expect_wants_read(&mut coroutine, Some(b"5\r"));
161        let out = expect_complete_ok(&mut coroutine, Some(b"\nHello\r\n0\r\n\r\n"));
162        assert_eq!(out.body, b"Hello");
163    }
164
165    #[test]
166    fn incomplete_chunk_data_then_resume() {
167        let mut coroutine = Http11ChunksRead::default();
168        expect_wants_read(&mut coroutine, Some(b"5\r\nHell"));
169        let out = expect_complete_ok(&mut coroutine, Some(b"o\r\n0\r\n\r\n"));
170        assert_eq!(out.body, b"Hello");
171    }
172
173    #[test]
174    fn wiki_ru_multi_chunk() {
175        let encoded = "9\r\nchunk 1, \r\n7\r\nchunk 2\r\n0\r\n\r\n";
176        let mut coroutine = Http11ChunksRead::default();
177        let out = expect_complete_ok(&mut coroutine, Some(encoded.as_bytes()));
178        assert_eq!(out.body, b"chunk 1, chunk 2");
179    }
180
181    #[test]
182    fn github_frewsxcv_test_vector() {
183        let encoded = "3\r\nhel\r\nb\r\nlo world!!!\r\n0\r\n\r\n";
184        let mut coroutine = Http11ChunksRead::default();
185        let out = expect_complete_ok(&mut coroutine, Some(encoded.as_bytes()));
186        assert_eq!(out.body, b"hello world!!!");
187    }
188
189    fn expect_wants_read(cor: &mut Http11ChunksRead, arg: Option<&[u8]>) {
190        match cor.resume(arg) {
191            HttpCoroutineState::Yielded(HttpYield::WantsRead) => {}
192            state => panic!("expected WantsRead, got {state:?}"),
193        }
194    }
195
196    fn expect_complete_ok(
197        cor: &mut Http11ChunksRead,
198        arg: Option<&[u8]>,
199    ) -> Http11ChunksReadOutput {
200        match cor.resume(arg) {
201            HttpCoroutineState::Complete(Ok(out)) => out,
202            state => panic!("expected Complete(Ok), got {state:?}"),
203        }
204    }
205
206    fn expect_complete_err(
207        cor: &mut Http11ChunksRead,
208        arg: Option<&[u8]>,
209    ) -> Http11ChunksReadError {
210        match cor.resume(arg) {
211            HttpCoroutineState::Complete(Err(err)) => err,
212            state => panic!("expected Complete(Err), got {state:?}"),
213        }
214    }
215}