Skip to main content

sim_lib_net_core/
http.rs

1//! Incremental parsing of HTTP response heads.
2
3use crate::NetError;
4
5/// A parsed HTTP response head: status line plus headers.
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct HttpHead {
8    /// Numeric status code from the status line.
9    pub status: u16,
10    /// Reason phrase (everything after the status code on the status line);
11    /// may be empty.
12    pub reason: String,
13    /// Header field name/value pairs in the order received. Values are trimmed.
14    pub headers: Vec<(String, String)>,
15}
16
17impl HttpHead {
18    /// Case-insensitive lookup of the first header with the given name.
19    pub fn header(&self, name: &str) -> Option<&str> {
20        self.headers
21            .iter()
22            .find(|(key, _)| key.eq_ignore_ascii_case(name))
23            .map(|(_, value)| value.as_str())
24    }
25}
26
27/// How the body following a response head should be read.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum HttpBodyMode {
30    /// Exactly `n` bytes follow.
31    ContentLength(usize),
32    /// `Transfer-Encoding: chunked` framing follows.
33    Chunked,
34    /// Read until the connection is closed (no length, no chunking).
35    UntilEof,
36    /// No body is expected. Not returned by [`body_mode`]; provided for callers
37    /// that classify bodies by status (e.g. 204/304) before reading.
38    Empty,
39}
40
41/// Parse a CRLF-delimited HTTP response head.
42///
43/// Extracted from `sim-lib-agent-runner-http`'s `read_response` head parsing.
44/// `head_text` is the head with or without the trailing `\r\n\r\n`; the status
45/// line is the first CRLF-delimited line and the remainder are `key: value`
46/// headers. The status code is the second whitespace-delimited token of the
47/// status line, matching the client's `version status` split.
48pub fn parse_http_head(head_text: &str) -> Result<HttpHead, NetError> {
49    let body = head_text.trim_end_matches("\r\n\r\n");
50    let mut lines = body.split("\r\n");
51
52    let status_line = lines
53        .next()
54        .ok_or_else(|| NetError::InvalidHead("missing status line".to_owned()))?;
55    let mut parts = status_line.split_whitespace();
56    let _version = parts
57        .next()
58        .ok_or_else(|| NetError::InvalidHead("missing version".to_owned()))?;
59    let status = parts
60        .next()
61        .ok_or_else(|| NetError::InvalidHead("missing status".to_owned()))?
62        .parse::<u16>()
63        .map_err(|_| NetError::InvalidHead("invalid status code".to_owned()))?;
64    let reason = parts.collect::<Vec<_>>().join(" ");
65
66    let mut headers = Vec::new();
67    for line in lines {
68        if line.is_empty() {
69            continue;
70        }
71        let (key, value) = line
72            .split_once(':')
73            .ok_or_else(|| NetError::InvalidHead("invalid header line".to_owned()))?;
74        headers.push((key.to_owned(), value.trim().to_owned()));
75    }
76
77    Ok(HttpHead {
78        status,
79        reason,
80        headers,
81    })
82}
83
84/// Classify how the body should be read, matching the client's precedence.
85///
86/// Extracted from `sim-lib-agent-runner-http`'s `read_response`:
87///
88/// 1. `Transfer-Encoding: chunked` -> [`HttpBodyMode::Chunked`].
89/// 2. otherwise a present, non-zero `Content-Length` -> [`HttpBodyMode::ContentLength`].
90/// 3. otherwise (no/zero length) -> [`HttpBodyMode::UntilEof`].
91///
92/// The client treated a missing or `0` `Content-Length` identically (read to
93/// EOF), so a literal `Content-Length: 0` maps to `UntilEof` here, not
94/// [`HttpBodyMode::Empty`]. An invalid `Content-Length` is rejected.
95pub fn body_mode(head: &HttpHead) -> Result<HttpBodyMode, NetError> {
96    if let Some(encoding) = head.header("Transfer-Encoding")
97        && encoding.eq_ignore_ascii_case("chunked")
98    {
99        return Ok(HttpBodyMode::Chunked);
100    }
101    match head.header("Content-Length") {
102        Some(raw) => {
103            let length = raw
104                .parse::<usize>()
105                .map_err(|_| NetError::InvalidHead("invalid content-length".to_owned()))?;
106            if length == 0 {
107                Ok(HttpBodyMode::UntilEof)
108            } else {
109                Ok(HttpBodyMode::ContentLength(length))
110            }
111        }
112        None => Ok(HttpBodyMode::UntilEof),
113    }
114}