sark_core/http/codec/decode/chunked/
mod.rs1mod parse;
2
3use http::{HeaderName, HeaderValue};
4use parse::Framing;
5
6use crate::error::{Error, Result};
7
8const MAX_BODY_SIZE: usize = 100 * 1024 * 1024;
9
10fn parse_chunk_size(bytes: &[u8]) -> Result<usize> {
11 if bytes.is_empty() {
12 return Err(Error::BadRequest("Empty chunk size".into()));
13 }
14 let mut size: usize = 0;
15 for &b in bytes {
16 let digit = match b {
17 b'0'..=b'9' => (b - b'0') as usize,
18 b'a'..=b'f' => (b - b'a' + 10) as usize,
19 b'A'..=b'F' => (b - b'A' + 10) as usize,
20 _ => return Err(Error::BadRequest("Invalid chunk size".into())),
21 };
22 size = size
23 .checked_mul(16)
24 .and_then(|s| s.checked_add(digit))
25 .ok_or_else(|| Error::BadRequest("Chunk size overflow".into()))?;
26 }
27 Ok(size)
28}
29
30pub(crate) struct DecodeResult {
31 pub(crate) body: Vec<u8>,
32 pub(crate) trailers: Vec<(HeaderName, HeaderValue)>,
33}
34
35pub enum DecodeEvent {
36 NeedMore,
37 Chunk(Vec<u8>),
38 Done(Vec<(HeaderName, HeaderValue)>),
39}
40
41enum State {
42 SizeLine,
43 Data(usize),
44 Trailers,
45 Done,
46}
47
48pub struct BodyDecoder {
49 state: State,
50 max_body: usize,
51 body_len: usize,
52}
53
54impl Default for BodyDecoder {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60impl BodyDecoder {
61 pub fn new() -> Self {
62 Self::with_limit(MAX_BODY_SIZE)
63 }
64
65 pub fn with_limit(max_body: usize) -> Self {
66 Self {
67 state: State::SizeLine,
68 max_body,
69 body_len: 0,
70 }
71 }
72
73 pub fn decode(&mut self, buf: &[u8]) -> Result<(usize, DecodeEvent)> {
74 let mut pos = 0;
75
76 loop {
77 match self.state {
78 State::SizeLine => {
79 let crlf = match Framing::find_crlf(&buf[pos..]) {
80 Some(offset) => pos + offset,
81 None => return Ok((pos, DecodeEvent::NeedMore)),
82 };
83
84 let line = &buf[pos..crlf];
85 let size_bytes = match line.iter().position(|&b| b == b';') {
86 Some(semi) => &line[..semi],
87 None => line,
88 };
89 let chunk_size = parse_chunk_size(size_bytes)?;
90
91 if chunk_size > self.max_body || self.body_len + chunk_size > self.max_body {
92 return Err(Error::PayloadTooLarge(
93 "Chunked body exceeds size limit".into(),
94 ));
95 }
96
97 pos = crlf + 2;
98 if chunk_size == 0 {
99 self.state = State::Trailers;
100 } else {
101 self.state = State::Data(chunk_size);
102 }
103 }
104 State::Data(chunk_size) => {
105 if buf.len() < pos + chunk_size + 2 {
106 return Ok((pos, DecodeEvent::NeedMore));
107 }
108
109 let chunk = buf[pos..pos + chunk_size].to_vec();
110 if &buf[pos + chunk_size..pos + chunk_size + 2] != b"\r\n" {
111 return Err(Error::BadRequest("Invalid chunk terminator".into()));
112 }
113
114 pos += chunk_size + 2;
115 self.body_len += chunk_size;
116 self.state = State::SizeLine;
117 return Ok((pos, DecodeEvent::Chunk(chunk)));
118 }
119 State::Trailers => match Framing::parse_trailers(&buf[pos..])? {
120 Some((trailers, consumed)) => {
121 pos += consumed;
122 self.state = State::Done;
123 return Ok((pos, DecodeEvent::Done(trailers)));
124 }
125 None => return Ok((pos, DecodeEvent::NeedMore)),
126 },
127 State::Done => return Ok((0, DecodeEvent::Done(Vec::new()))),
128 }
129 }
130 }
131}
132
133impl crate::http::codec::Parse {
134 pub(crate) fn try_decode_chunked(buf: &[u8]) -> Result<Option<DecodeResult>> {
135 Self::try_decode_chunked_limited(buf, MAX_BODY_SIZE)
136 }
137
138 pub(crate) fn try_decode_chunked_limited(
139 buf: &[u8],
140 max_body: usize,
141 ) -> Result<Option<DecodeResult>> {
142 let mut decoder = BodyDecoder::with_limit(max_body);
143 let mut body = Vec::new();
144 let mut input = buf;
145
146 loop {
147 let (consumed, event) = decoder.decode(input)?;
148 input = &input[consumed..];
149
150 match event {
151 DecodeEvent::NeedMore => return Ok(None),
152 DecodeEvent::Chunk(chunk) => body.extend_from_slice(&chunk),
153 DecodeEvent::Done(trailers) => {
154 return Ok(Some(DecodeResult { body, trailers }));
155 }
156 }
157 }
158 }
159
160 pub fn chunked_body(buf: &[u8]) -> Result<Option<Vec<u8>>> {
161 Ok(Self::try_decode_chunked(buf)?.map(|r| r.body))
162 }
163
164 pub fn chunked_body_consumed(buf: &[u8], max_body: usize) -> Result<Option<(usize, Vec<u8>)>> {
165 let mut decoder = BodyDecoder::with_limit(max_body);
166 let mut body = Vec::new();
167 let mut input = buf;
168 let total = buf.len();
169
170 loop {
171 let (consumed, event) = decoder.decode(input)?;
172 input = &input[consumed..];
173
174 match event {
175 DecodeEvent::NeedMore => return Ok(None),
176 DecodeEvent::Chunk(chunk) => body.extend_from_slice(&chunk),
177 DecodeEvent::Done(_) => {
178 let consumed_total = total - input.len();
179 return Ok(Some((consumed_total, body)));
180 }
181 }
182 }
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn chunk_size_plus_prefix_rejected() {
192 assert!(parse_chunk_size(b"+a").is_err());
193 }
194
195 #[test]
196 fn chunk_size_minus_prefix_rejected() {
197 assert!(parse_chunk_size(b"-a").is_err());
198 }
199
200 #[test]
201 fn chunk_size_surrounding_whitespace_rejected() {
202 assert!(parse_chunk_size(b" 5 ").is_err());
203 assert!(parse_chunk_size(b"\t5").is_err());
204 assert!(parse_chunk_size(b"5 ").is_err());
205 }
206
207 #[test]
208 fn chunk_size_empty_rejected() {
209 assert!(parse_chunk_size(b"").is_err());
210 }
211
212 #[test]
213 fn chunk_size_valid_hex_accepted() {
214 assert_eq!(parse_chunk_size(b"a").unwrap(), 10);
215 assert_eq!(parse_chunk_size(b"1F").unwrap(), 31);
216 assert_eq!(parse_chunk_size(b"0").unwrap(), 0);
217 }
218
219 #[test]
220 fn chunk_size_huge_hex_overflows() {
221 assert!(parse_chunk_size(b"ffffffffffffffffff").is_err());
222 }
223
224 #[test]
225 fn chunk_extension_after_size_accepted() {
226 let raw = b"5;name=value\r\nhello\r\n0\r\n\r\n";
227 let body = crate::http::codec::Parse::chunked_body(raw)
228 .unwrap()
229 .unwrap();
230 assert_eq!(body, b"hello");
231 }
232
233 #[test]
234 fn normal_chunked_body_accepted() {
235 let raw = b"5\r\nhello\r\n0\r\n\r\n";
236 let body = crate::http::codec::Parse::chunked_body(raw)
237 .unwrap()
238 .unwrap();
239 assert_eq!(body, b"hello");
240 }
241
242 #[test]
243 fn chunk_size_whitespace_in_stream_rejected() {
244 let raw = b" 5 \r\nhello\r\n0\r\n\r\n";
245 assert!(crate::http::codec::Parse::chunked_body(raw).is_err());
246 }
247}