io_http/rfc9112/
chunk_stream.rs1use 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#[derive(Debug, Error)]
22pub enum Http11ReadChunksStreamError {
23 #[error("HTTP/1.1 read chunks failed: invalid chunk size `{0}`")]
24 InvalidChunkSize(String),
25}
26
27#[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#[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 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}