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 [`super::chunk_stream::Http11ReadChunksStream`].
4//!
5//! [RFC 9112 §7.1]: https://www.rfc-editor.org/rfc/rfc9112#section-7.1
6
7use core::{fmt, 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 read flow.
21#[derive(Debug, Error)]
22pub enum Http11ReadChunksError {
23    #[error("HTTP/1.1 read chunks failed: invalid chunk size `{0}`")]
24    InvalidChunkSize(String),
25}
26
27/// Terminal output of [`Http11ReadChunks`].
28#[derive(Debug)]
29pub struct Http11ReadChunksOutput {
30    pub body: Vec<u8>,
31    pub remaining: Vec<u8>,
32}
33
34#[derive(Debug, Default)]
35enum State {
36    #[default]
37    ChunkSize,
38    ChunkData(usize),
39}
40
41impl fmt::Display for State {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            Self::ChunkSize => f.write_str("read chunk size"),
45            Self::ChunkData(_) => f.write_str("read chunk data"),
46        }
47    }
48}
49
50/// I/O-free coroutine to read an HTTP response body using chunked
51/// transfer coding.
52#[derive(Debug, Default)]
53pub struct Http11ReadChunks {
54    state: State,
55    wants_read: bool,
56    last_chunk: bool,
57    buf: Vec<u8>,
58    body: Vec<u8>,
59}
60
61impl HttpCoroutine for Http11ReadChunks {
62    type Yield = HttpYield;
63    type Return = Result<Http11ReadChunksOutput, Http11ReadChunksError>;
64
65    fn resume(&mut self, arg: Option<&[u8]>) -> HttpCoroutineState<Self::Yield, Self::Return> {
66        if let Some(data) = arg {
67            self.buf.extend_from_slice(data);
68        }
69
70        loop {
71            trace!("http/1.1 read chunks: {}", self.state);
72
73            if self.wants_read {
74                self.wants_read = false;
75                return HttpCoroutineState::Yielded(HttpYield::WantsRead);
76            }
77
78            if self.last_chunk {
79                let body = mem::take(&mut self.body);
80                let remaining = mem::take(&mut self.buf);
81                return HttpCoroutineState::Complete(Ok(Http11ReadChunksOutput {
82                    body,
83                    remaining,
84                }));
85            }
86
87            match self.state {
88                State::ChunkSize => {
89                    let Some(crlf) = memmem::find(&self.buf, &CRLF) else {
90                        self.wants_read = true;
91                        continue;
92                    };
93
94                    let ext = match memchr(b';', &self.buf[..crlf]) {
95                        None => crlf,
96                        Some(ext) => {
97                            let exts = String::from_utf8_lossy(self.buf[ext..crlf].trim_ascii());
98                            trace!("ignore extension(s) `{exts}`");
99                            ext
100                        }
101                    };
102
103                    let chunk_size = String::from_utf8_lossy(self.buf[..ext].trim_ascii());
104
105                    let Ok(n) = usize::from_str_radix(&chunk_size, 16) else {
106                        let chunk_size = chunk_size.to_string();
107                        let err = Http11ReadChunksError::InvalidChunkSize(chunk_size);
108                        return HttpCoroutineState::Complete(Err(err));
109                    };
110
111                    self.buf.drain(..crlf + CRLF.len());
112                    self.state = State::ChunkData(n);
113                }
114                State::ChunkData(size) if self.buf.len() < size + CRLF.len() => {
115                    trace!("received incomplete chunk data {}/{size}", self.buf.len());
116                    self.wants_read = true;
117                    continue;
118                }
119                State::ChunkData(size) => {
120                    self.body.extend(self.buf.drain(..size));
121                    self.buf.drain(..CRLF.len());
122                    self.state = State::ChunkSize;
123                    self.last_chunk = size == 0;
124                }
125            }
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn single_chunk() {
136        let mut coroutine = Http11ReadChunks::default();
137        let out = expect_complete_ok(&mut coroutine, Some(b"5\r\nhello\r\n0\r\n\r\n"));
138        assert_eq!(out.body, b"hello");
139        assert_eq!(out.remaining, b"");
140    }
141
142    #[test]
143    fn empty_body() {
144        let mut coroutine = Http11ReadChunks::default();
145        let out = expect_complete_ok(&mut coroutine, Some(b"0\r\n\r\n"));
146        assert!(out.body.is_empty());
147    }
148
149    #[test]
150    fn ignored_extension() {
151        let mut coroutine = Http11ReadChunks::default();
152        let out = expect_complete_ok(&mut coroutine, Some(b"5;ext\r\nHello\r\n0\r\n\r\n"));
153        assert_eq!(out.body, b"Hello");
154    }
155
156    #[test]
157    fn invalid_chunk_size() {
158        let mut coroutine = Http11ReadChunks::default();
159        let err = expect_complete_err(&mut coroutine, Some(b":\r\n0\r\n\r\n"));
160        let Http11ReadChunksError::InvalidChunkSize(s) = err;
161        assert_eq!(s, ":");
162    }
163
164    #[test]
165    fn incomplete_chunk_size_then_resume() {
166        let mut coroutine = Http11ReadChunks::default();
167        expect_wants_read(&mut coroutine, Some(b"5\r"));
168        let out = expect_complete_ok(&mut coroutine, Some(b"\nHello\r\n0\r\n\r\n"));
169        assert_eq!(out.body, b"Hello");
170    }
171
172    #[test]
173    fn incomplete_chunk_data_then_resume() {
174        let mut coroutine = Http11ReadChunks::default();
175        expect_wants_read(&mut coroutine, Some(b"5\r\nHell"));
176        let out = expect_complete_ok(&mut coroutine, Some(b"o\r\n0\r\n\r\n"));
177        assert_eq!(out.body, b"Hello");
178    }
179
180    #[test]
181    fn wiki_ru_multi_chunk() {
182        let encoded = "9\r\nchunk 1, \r\n7\r\nchunk 2\r\n0\r\n\r\n";
183        let mut coroutine = Http11ReadChunks::default();
184        let out = expect_complete_ok(&mut coroutine, Some(encoded.as_bytes()));
185        assert_eq!(out.body, b"chunk 1, chunk 2");
186    }
187
188    #[test]
189    fn github_frewsxcv_test_vector() {
190        let encoded = "3\r\nhel\r\nb\r\nlo world!!!\r\n0\r\n\r\n";
191        let mut coroutine = Http11ReadChunks::default();
192        let out = expect_complete_ok(&mut coroutine, Some(encoded.as_bytes()));
193        assert_eq!(out.body, b"hello world!!!");
194    }
195
196    // --- utils
197
198    fn expect_wants_read(cor: &mut Http11ReadChunks, arg: Option<&[u8]>) {
199        match cor.resume(arg) {
200            HttpCoroutineState::Yielded(HttpYield::WantsRead) => {}
201            state => panic!("expected WantsRead, got {state:?}"),
202        }
203    }
204
205    fn expect_complete_ok(
206        cor: &mut Http11ReadChunks,
207        arg: Option<&[u8]>,
208    ) -> Http11ReadChunksOutput {
209        match cor.resume(arg) {
210            HttpCoroutineState::Complete(Ok(out)) => out,
211            state => panic!("expected Complete(Ok), got {state:?}"),
212        }
213    }
214
215    fn expect_complete_err(
216        cor: &mut Http11ReadChunks,
217        arg: Option<&[u8]>,
218    ) -> Http11ReadChunksError {
219        match cor.resume(arg) {
220            HttpCoroutineState::Complete(Err(err)) => err,
221            state => panic!("expected Complete(Err), got {state:?}"),
222        }
223    }
224}