Skip to main content

sark_core/http/codec/decode/chunked/
mod.rs

1mod 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}