Skip to main content

oxihuman_core/
http_parser.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! HTTP/1.1 request/response parser stub.
6
7/// An HTTP method.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum HttpMethod {
10    Get,
11    Post,
12    Put,
13    Delete,
14    Head,
15    Options,
16    Patch,
17    Other(String),
18}
19
20impl HttpMethod {
21    /// Parse from a string slice.
22    #[allow(clippy::should_implement_trait)]
23    pub fn from_str(s: &str) -> Self {
24        match s.to_ascii_uppercase().as_str() {
25            "GET" => Self::Get,
26            "POST" => Self::Post,
27            "PUT" => Self::Put,
28            "DELETE" => Self::Delete,
29            "HEAD" => Self::Head,
30            "OPTIONS" => Self::Options,
31            "PATCH" => Self::Patch,
32            other => Self::Other(other.to_string()),
33        }
34    }
35}
36
37/// An HTTP header (name + value pair).
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct HttpHeader {
40    pub name: String,
41    pub value: String,
42}
43
44/// A parsed HTTP/1.1 request.
45#[derive(Debug, Clone)]
46pub struct HttpRequest {
47    pub method: HttpMethod,
48    pub path: String,
49    pub version: String,
50    pub headers: Vec<HttpHeader>,
51    pub body: Vec<u8>,
52}
53
54/// A parsed HTTP/1.1 response.
55#[derive(Debug, Clone)]
56pub struct HttpResponse {
57    pub version: String,
58    pub status_code: u16,
59    pub reason: String,
60    pub headers: Vec<HttpHeader>,
61    pub body: Vec<u8>,
62}
63
64/// HTTP parse error.
65#[derive(Debug, Clone, PartialEq)]
66pub enum HttpError {
67    MalformedRequestLine,
68    MalformedStatusLine,
69    MalformedHeader(String),
70    InvalidStatusCode(String),
71    UnexpectedEnd,
72}
73
74impl std::fmt::Display for HttpError {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        match self {
77            Self::MalformedRequestLine => write!(f, "malformed HTTP request line"),
78            Self::MalformedStatusLine => write!(f, "malformed HTTP status line"),
79            Self::MalformedHeader(s) => write!(f, "malformed HTTP header: {s}"),
80            Self::InvalidStatusCode(s) => write!(f, "invalid HTTP status code: {s}"),
81            Self::UnexpectedEnd => write!(f, "unexpected end of HTTP message"),
82        }
83    }
84}
85
86/// Parse an HTTP/1.1 request from raw bytes.
87pub fn parse_request(raw: &[u8]) -> Result<HttpRequest, HttpError> {
88    let text = std::str::from_utf8(raw).map_err(|_| HttpError::MalformedRequestLine)?;
89    let mut lines = text.split("\r\n");
90    let request_line = lines.next().ok_or(HttpError::UnexpectedEnd)?;
91    let mut parts = request_line.splitn(3, ' ');
92    let method = parts
93        .next()
94        .ok_or(HttpError::MalformedRequestLine)
95        .map(HttpMethod::from_str)?;
96    let path = parts
97        .next()
98        .ok_or(HttpError::MalformedRequestLine)?
99        .to_string();
100    let version = parts
101        .next()
102        .ok_or(HttpError::MalformedRequestLine)?
103        .to_string();
104    let mut headers = vec![];
105    for line in lines.by_ref() {
106        if line.is_empty() {
107            break;
108        }
109        let mut h = line.splitn(2, ':');
110        let name = h.next().unwrap_or("").trim().to_string();
111        let value = h.next().unwrap_or("").trim().to_string();
112        headers.push(HttpHeader { name, value });
113    }
114    Ok(HttpRequest {
115        method,
116        path,
117        version,
118        headers,
119        body: vec![],
120    })
121}
122
123/// Parse an HTTP/1.1 response from raw bytes.
124pub fn parse_response(raw: &[u8]) -> Result<HttpResponse, HttpError> {
125    let text = std::str::from_utf8(raw).map_err(|_| HttpError::MalformedStatusLine)?;
126    let mut lines = text.split("\r\n");
127    let status_line = lines.next().ok_or(HttpError::UnexpectedEnd)?;
128    let mut parts = status_line.splitn(3, ' ');
129    let version = parts
130        .next()
131        .ok_or(HttpError::MalformedStatusLine)?
132        .to_string();
133    let code_str = parts.next().ok_or(HttpError::MalformedStatusLine)?;
134    let status_code = code_str
135        .parse::<u16>()
136        .map_err(|_| HttpError::InvalidStatusCode(code_str.to_string()))?;
137    let reason = parts.next().unwrap_or("").to_string();
138    let mut headers = vec![];
139    for line in lines.by_ref() {
140        if line.is_empty() {
141            break;
142        }
143        let mut h = line.splitn(2, ':');
144        let name = h.next().unwrap_or("").trim().to_string();
145        let value = h.next().unwrap_or("").trim().to_string();
146        headers.push(HttpHeader { name, value });
147    }
148    Ok(HttpResponse {
149        version,
150        status_code,
151        reason,
152        headers,
153        body: vec![],
154    })
155}
156
157/// Look up a header value by name (case-insensitive).
158pub fn find_header<'a>(headers: &'a [HttpHeader], name: &str) -> Option<&'a str> {
159    headers
160        .iter()
161        .find(|h| h.name.eq_ignore_ascii_case(name))
162        .map(|h| h.value.as_str())
163}
164
165/// Return `true` if the request uses HTTP/1.1.
166pub fn is_http11(req: &HttpRequest) -> bool {
167    req.version == "HTTP/1.1"
168}
169
170/// Return the Content-Length header value if present.
171pub fn content_length(headers: &[HttpHeader]) -> Option<usize> {
172    find_header(headers, "content-length").and_then(|v| v.trim().parse().ok())
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_parse_get_request() {
181        /* basic GET request parsed */
182        let raw = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
183        let req = parse_request(raw).expect("should succeed");
184        assert_eq!(req.method, HttpMethod::Get);
185        assert_eq!(req.path, "/");
186    }
187
188    #[test]
189    fn test_parse_response_200() {
190        /* 200 OK response parsed */
191        let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
192        let resp = parse_response(raw).expect("should succeed");
193        assert_eq!(resp.status_code, 200);
194    }
195
196    #[test]
197    fn test_find_header_case_insensitive() {
198        /* header lookup is case-insensitive */
199        let headers = vec![HttpHeader {
200            name: "Content-Type".to_string(),
201            value: "text/html".to_string(),
202        }];
203        assert_eq!(find_header(&headers, "content-type"), Some("text/html"));
204    }
205
206    #[test]
207    fn test_is_http11_true() {
208        /* HTTP/1.1 version detected */
209        let raw = b"GET / HTTP/1.1\r\n\r\n";
210        let req = parse_request(raw).expect("should succeed");
211        assert!(is_http11(&req));
212    }
213
214    #[test]
215    fn test_content_length_header() {
216        /* content_length parses header value */
217        let headers = vec![HttpHeader {
218            name: "Content-Length".to_string(),
219            value: "42".to_string(),
220        }];
221        assert_eq!(content_length(&headers), Some(42));
222    }
223
224    #[test]
225    fn test_method_post() {
226        /* POST method parsed */
227        let raw = b"POST /data HTTP/1.1\r\n\r\n";
228        let req = parse_request(raw).expect("should succeed");
229        assert_eq!(req.method, HttpMethod::Post);
230    }
231
232    #[test]
233    fn test_invalid_status_code() {
234        /* non-numeric status code returns error */
235        let raw = b"HTTP/1.1 OK notanumber\r\n\r\n";
236        assert!(parse_response(raw).is_err());
237    }
238
239    #[test]
240    fn test_multiple_headers() {
241        /* multiple headers parsed */
242        let raw = b"GET / HTTP/1.1\r\nHost: x\r\nAccept: */*\r\n\r\n";
243        let req = parse_request(raw).expect("should succeed");
244        assert_eq!(req.headers.len(), 2);
245    }
246
247    #[test]
248    fn test_find_header_missing() {
249        /* missing header returns None */
250        let headers: Vec<HttpHeader> = vec![];
251        assert!(find_header(&headers, "x-custom").is_none());
252    }
253
254    #[test]
255    fn test_parse_response_404() {
256        /* 404 status code parsed */
257        let raw = b"HTTP/1.1 404 Not Found\r\n\r\n";
258        let resp = parse_response(raw).expect("should succeed");
259        assert_eq!(resp.status_code, 404);
260        assert_eq!(resp.reason, "Not Found");
261    }
262}