Skip to main content

sim_lib_net_core/
body.rs

1//! HTTP response body framing helpers.
2
3use crate::NetError;
4
5/// Decode a complete `Transfer-Encoding: chunked` body.
6///
7/// The decoded body is capped at `cap` bytes. Chunk extensions and trailer
8/// lines are accepted, but the body must contain the terminal empty trailer
9/// line (`0\r\n\r\n` for a response without trailers).
10///
11/// # Examples
12///
13/// ```
14/// use sim_lib_net_core::decode_chunked;
15///
16/// let decoded = decode_chunked(b"4\r\nWiki\r\n5\r\npedia\r\n0\r\n\r\n", 32).unwrap();
17/// assert_eq!(decoded, b"Wikipedia");
18/// ```
19pub fn decode_chunked(mut input: &[u8], cap: usize) -> Result<Vec<u8>, NetError> {
20    let mut decoded = Vec::new();
21    loop {
22        let line_end = find_crlf(input).ok_or(NetError::TruncatedChunk)?;
23        let size = parse_chunk_size(&input[..line_end])?;
24        input = &input[line_end + 2..];
25
26        if size == 0 {
27            validate_trailers(input)?;
28            return Ok(decoded);
29        }
30
31        let next_len = decoded
32            .len()
33            .checked_add(size)
34            .ok_or(NetError::OversizeBody(cap))?;
35        if next_len > cap {
36            return Err(NetError::OversizeBody(cap));
37        }
38
39        let needed = size.checked_add(2).ok_or(NetError::InvalidChunkSize(
40            "size overflows frame length".to_owned(),
41        ))?;
42        if input.len() < needed {
43            return Err(NetError::TruncatedChunk);
44        }
45        if &input[size..needed] != b"\r\n" {
46            return Err(NetError::InvalidChunkDelimiter);
47        }
48
49        decoded.extend_from_slice(&input[..size]);
50        input = &input[needed..];
51    }
52}
53
54fn find_crlf(input: &[u8]) -> Option<usize> {
55    input.windows(2).position(|window| window == b"\r\n")
56}
57
58fn parse_chunk_size(line: &[u8]) -> Result<usize, NetError> {
59    let text = std::str::from_utf8(line)
60        .map_err(|err| NetError::InvalidChunkSize(format!("size line is not UTF-8: {err}")))?;
61    let size_text = text.split(';').next().unwrap_or_default().trim();
62    if size_text.is_empty() {
63        return Err(NetError::InvalidChunkSize("size line is empty".to_owned()));
64    }
65    usize::from_str_radix(size_text, 16)
66        .map_err(|err| NetError::InvalidChunkSize(format!("invalid size: {err}")))
67}
68
69fn validate_trailers(mut input: &[u8]) -> Result<(), NetError> {
70    loop {
71        let line_end = find_crlf(input).ok_or(NetError::TruncatedChunk)?;
72        input = &input[line_end + 2..];
73        if line_end == 0 {
74            if input.is_empty() {
75                return Ok(());
76            }
77            return Err(NetError::InvalidChunkDelimiter);
78        }
79    }
80}