#[inline(always)]
pub(crate) fn version_to_str(version: http::Version) -> &'static str {
match version {
http::Version::HTTP_10 => "HTTP/1.0",
http::Version::HTTP_11 => "HTTP/1.1",
_ => unreachable!("unsupported HTTP version: only HTTP/1.0 and HTTP/1.1 are supported"),
}
}
#[inline]
pub(crate) fn chunked_body_len(buf: &[u8]) -> Option<usize> {
let mut pos = 0;
loop {
let crlf = buf[pos..].windows(2).position(|w| w == b"\r\n")?;
let size_field = buf[pos..pos + crlf].splitn(2, |&b| b == b';').next()?;
let chunk_size = usize::from_str_radix(
std::str::from_utf8(size_field.trim_ascii()).ok()?,
16,
)
.ok()?;
pos += crlf + 2;
if chunk_size == 0 {
return buf[pos - 2..]
.windows(4)
.position(|w| w == b"\r\n\r\n")
.map(|k| pos - 2 + k + 4);
}
pos = pos.checked_add(chunk_size)?.checked_add(2)?;
if pos > buf.len() {
return None;
}
}
}
#[inline]
pub(crate) fn is_chunked_slice(buf: &[u8]) -> bool {
let last = buf.rsplit(|&b| b == b',').next().unwrap_or(buf);
last.trim_ascii().eq_ignore_ascii_case(b"chunked")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_chunk() {
let buf = b"5\r\nhello\r\n0\r\n\r\n";
assert_eq!(chunked_body_len(buf), Some(buf.len()));
}
#[test]
fn multiple_chunks() {
let buf = b"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
assert_eq!(chunked_body_len(buf), Some(buf.len()));
}
#[test]
fn chunk_size_uppercase_hex() {
let buf = b"A\r\n0123456789\r\n0\r\n\r\n";
assert_eq!(chunked_body_len(buf), Some(buf.len()));
}
#[test]
fn chunk_size_mixed_case_hex() {
let mut full = b"1a\r\n".to_vec();
full.extend_from_slice(&vec![b'x'; 0x1a]);
full.extend_from_slice(b"\r\n0\r\n\r\n");
assert_eq!(chunked_body_len(&full), Some(full.len()));
}
#[test]
fn terminal_chunk_only() {
let buf = b"0\r\n\r\n";
assert_eq!(chunked_body_len(buf), Some(buf.len()));
}
#[test]
fn extra_data_after_terminal_chunk_not_included() {
let buf = b"5\r\nhello\r\n0\r\n\r\nGARBAGE";
assert_eq!(chunked_body_len(buf), Some(b"5\r\nhello\r\n0\r\n\r\n".len()));
}
#[test]
fn chunk_extension_is_ignored() {
let buf = b"5;name=value\r\nhello\r\n0\r\n\r\n";
assert_eq!(chunked_body_len(buf), Some(buf.len()));
}
#[test]
fn terminal_chunk_with_extension() {
let buf = b"5\r\nhello\r\n0;last\r\n\r\n";
assert_eq!(chunked_body_len(buf), Some(buf.len()));
}
#[test]
fn single_trailer() {
let buf = b"5\r\nhello\r\n0\r\nTrailer-Header: value\r\n\r\n";
assert_eq!(chunked_body_len(buf), Some(buf.len()));
}
#[test]
fn multiple_trailers() {
let buf = b"5\r\nhello\r\n0\r\nX-A: 1\r\nX-B: 2\r\n\r\n";
assert_eq!(chunked_body_len(buf), Some(buf.len()));
}
#[test]
fn incomplete_chunk_data() {
let buf = b"5\r\nhel";
assert_eq!(chunked_body_len(buf), None);
}
#[test]
fn incomplete_missing_terminal_chunk() {
let buf = b"5\r\nhello\r\n";
assert_eq!(chunked_body_len(buf), None);
}
#[test]
fn incomplete_terminal_chunk_missing_final_crlf() {
let buf = b"5\r\nhello\r\n0\r\n";
assert_eq!(chunked_body_len(buf), None);
}
#[test]
fn incomplete_trailer_no_closing_crlf() {
let buf = b"5\r\nhello\r\n0\r\nTrailer: val\r\n";
assert_eq!(chunked_body_len(buf), None);
}
#[test]
fn empty_buffer() {
assert_eq!(chunked_body_len(b""), None);
}
#[test]
fn invalid_hex_in_chunk_size() {
let buf = b"z\r\nhello\r\n0\r\n\r\n";
assert_eq!(chunked_body_len(buf), None);
}
#[test]
fn empty_chunk_size_field() {
let buf = b"\r\nhello\r\n0\r\n\r\n";
assert_eq!(chunked_body_len(buf), None);
}
#[test]
fn chunk_size_line_lf_only_rejected() {
let buf = b"5\nhello\n0\n\n";
assert_eq!(chunked_body_len(buf), None);
}
#[test]
fn chunk_size_with_space_in_middle_rejected() {
let buf = b"1 2\r\n";
let mut full = buf.to_vec();
full.extend_from_slice(&[b'x'; 18]); full.extend_from_slice(b"\r\n0\r\n\r\n");
assert_eq!(chunked_body_len(&full), None);
}
#[test]
fn chunk_size_overflow_rejected() {
let buf = b"ffffffffffffffffffffffff\r\n";
assert_eq!(chunked_body_len(buf), None);
}
#[test]
fn chunk_size_exactly_usize_max_rejected() {
let hex = format!("{:x}\r\n", usize::MAX);
assert_eq!(chunked_body_len(hex.as_bytes()), None);
}
}