Skip to main content

wireforge_app/http/
response.rs

1//! HTTP/1.1 response line parser.
2
3use super::headers::HeaderIter;
4
5/// Parsed HTTP response (version, status code, reason, headers, body).
6#[derive(Debug, Clone)]
7pub struct HttpResponse<'a> {
8    buf: &'a [u8],
9    version_end: usize,
10    status_start: usize,
11    status_end: usize,
12    reason_end: usize,
13    headers_start: usize,
14    body_start: usize,
15}
16
17impl<'a> HttpResponse<'a> {
18    pub fn new(buf: &'a [u8]) -> Option<Self> {
19        let response_line_end = buf.windows(2).position(|w| w == b"\r\n")?;
20        let line = &buf[..response_line_end];
21
22        let sp1 = line.iter().position(|&b| b == b' ')?;
23        let sp2 = line[sp1 + 1..].iter().position(|&b| b == b' ')?;
24        let sp2 = sp1 + 1 + sp2;
25
26        let headers_start = response_line_end + 2;
27        let body_start = find_body_start(&buf[headers_start..]).map(|p| headers_start + p);
28
29        Some(Self {
30            buf,
31            version_end: sp1,
32            status_start: sp1 + 1,
33            status_end: sp2,
34            reason_end: response_line_end,
35            headers_start,
36            body_start: body_start.unwrap_or(buf.len()),
37        })
38    }
39
40    pub fn version(&self) -> &str { core::str::from_utf8(&self.buf[..self.version_end]).unwrap_or("") }
41    pub fn status_code(&self) -> u16 {
42        core::str::from_utf8(&self.buf[self.status_start..self.status_end])
43            .ok()
44            .and_then(|s| s.parse().ok())
45            .unwrap_or(0)
46    }
47    pub fn reason(&self) -> &str { core::str::from_utf8(&self.buf[self.status_end + 1..self.reason_end]).unwrap_or("") }
48    pub fn headers(&self) -> HeaderIter<'a> { HeaderIter::new(&self.buf[self.headers_start..self.body_start]) }
49    pub fn body(&self) -> &[u8] { &self.buf[self.body_start..] }
50}
51
52fn find_body_start(data: &[u8]) -> Option<usize> {
53    data.windows(4).position(|w| w == b"\r\n\r\n").map(|p| p + 4)
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn parse_200_ok() {
62        let data = b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<body>hello</body>";
63        let resp = HttpResponse::new(data).unwrap();
64        assert_eq!(resp.version(), "HTTP/1.1");
65        assert_eq!(resp.status_code(), 200);
66        assert_eq!(resp.reason(), "OK");
67        assert_eq!(resp.body(), b"<body>hello</body>");
68
69        let headers: Vec<_> = resp.headers().collect();
70        assert_eq!(headers.len(), 1);
71        assert_eq!(headers[0].name, "Content-Type");
72    }
73
74    #[test]
75    fn parse_404() {
76        let data = b"HTTP/1.0 404 Not Found\r\n\r\n";
77        let resp = HttpResponse::new(data).unwrap();
78        assert_eq!(resp.status_code(), 404);
79        assert_eq!(resp.reason(), "Not Found");
80        assert_eq!(resp.body(), b"");
81    }
82
83    #[test]
84    fn parse_get_request() {
85        let data = b"GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n";
86        let req = crate::http::HttpRequest::new(data).unwrap();
87        assert_eq!(req.method(), "GET");
88        assert_eq!(req.uri(), "/index.html");
89        assert_eq!(req.version(), "HTTP/1.1");
90        assert_eq!(req.body(), b"");
91
92        let headers: Vec<_> = req.headers().collect();
93        assert_eq!(headers.len(), 1);
94        assert_eq!(headers[0].name, "Host");
95    }
96}