Skip to main content

spars_httpd/
response.rs

1// SPDX-FileCopyrightText: 2025 Cullen Walsh <ckwalsh@cullenwalsh.com>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use std::fmt::Display;
5use std::fs::File;
6use std::io::Write as _;
7use std::net::TcpStream;
8use std::path::Path;
9use std::sync::Arc;
10
11use arrayvec::ArrayVec;
12use compact_str::CompactString;
13
14#[allow(non_camel_case_types)]
15#[derive(Debug)]
16pub enum StatusCode {
17    BAD_REQUEST,                     // 400
18    METHOD_NOT_ALLOWED,              // 405
19    REQUEST_TIMEOUT,                 // 408
20    PAYLOAD_TOO_LARGE,               // 413
21    URI_TOO_LONG,                    // 414
22    REQUEST_HEADER_FIELDS_TOO_LARGE, // 431
23    INTERNAL_SERVER_ERROR,           // 500
24    NOT_IMPLEMENTED,                 // 501
25    HTTP_VERSION_NOT_SUPPORTED,      // 505
26}
27
28const STATUS_STRS: [&str; 9] = [
29    "400 Bad Request",
30    "405 Method Not Allowed",
31    "408 Request Timeout",
32    "413 Content Too Large",
33    "414 URI Too Long",
34    "431 Request Header Fields Too Large",
35    "500 Internal Server Error",
36    "501 Not Implemented",
37    "505 HTTP Version Not Supported",
38];
39
40impl Display for StatusCode {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        f.write_str(match self {
43            StatusCode::BAD_REQUEST => STATUS_STRS[0],
44            StatusCode::METHOD_NOT_ALLOWED => STATUS_STRS[1],
45            StatusCode::REQUEST_TIMEOUT => STATUS_STRS[2],
46            StatusCode::PAYLOAD_TOO_LARGE => STATUS_STRS[3],
47            StatusCode::URI_TOO_LONG => STATUS_STRS[4],
48            StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE => STATUS_STRS[5],
49            StatusCode::INTERNAL_SERVER_ERROR => STATUS_STRS[6],
50            StatusCode::NOT_IMPLEMENTED => STATUS_STRS[7],
51            StatusCode::HTTP_VERSION_NOT_SUPPORTED => STATUS_STRS[8],
52        })
53    }
54}
55
56#[derive(Debug)]
57pub enum Response {
58    Found {
59        resolved_path: Option<Arc<Path>>,
60        len: u64,
61        mime_type: Option<&'static str>,
62    },
63    Redirect {
64        path: Arc<str>,
65        query: CompactString,
66    },
67    NotFound,
68    StatusStr(StatusCode),
69}
70
71impl Response {
72    pub fn write_to(self, conn: &mut TcpStream, keep_alive: bool) -> std::io::Result<()> {
73        let mut buf = ArrayVec::<u8, 256>::new();
74        let mut body_path = None;
75
76        match self {
77            Response::Found {
78                resolved_path,
79                len,
80                mime_type,
81            } => {
82                write!(buf, "HTTP/1.1 200 OK\r\n")?; // 17
83                write!(buf, "Content-Length: {len}\r\n")?; // 18 + len
84
85                if let Some(mime_type) = mime_type {
86                    write!(buf, "Content-Type: {mime_type}\r\n")?; // 16 + mime_type
87                }
88
89                if len > 0 {
90                    body_path = resolved_path;
91                }
92            }
93            Response::Redirect { path, query } => {
94                write!(buf, "HTTP/1.1 302 Found\r\n")?; // 20
95                write!(buf, "Content-Length: 0\r\n")?; // 19
96                write!(buf, "Location: {path}{query}\r\n")?; // 12 + path + query
97            }
98            Response::NotFound => {
99                write!(buf, "HTTP/1.1 404 Not Found\r\n")?; // 24
100                write!(buf, "Content-Length: 0\r\n")?; // 19
101            }
102            Response::StatusStr(status) => {
103                write!(buf, "HTTP/1.1 {status}\r\n")?; // 11 + status
104                write!(buf, "Content-Length: 0\r\n")?; // 19
105            }
106        }
107
108        if keep_alive {
109            write!(buf, "Connection: keep-alive\r\n\r\n")?; // 26
110        } else {
111            write!(buf, "Connection: close\r\n\r\n")?;
112        };
113
114        conn.write_all(buf.as_mut_slice())?;
115
116        if let Some(path) = body_path {
117            let mut f = File::options().read(true).open(path)?;
118            std::io::copy(&mut f, conn)?;
119        }
120
121        Ok(())
122    }
123}
124
125pub struct Responses(ResponsesInner);
126
127impl Default for Responses {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl Responses {
134    pub fn new() -> Self {
135        Self(ResponsesInner::Single(None))
136    }
137
138    pub fn push(&mut self, response: Response) {
139        match &mut self.0 {
140            ResponsesInner::Single(existing) => match existing.take() {
141                None => {
142                    *existing = Some(response);
143                }
144                Some(existing) => {
145                    self.0 = ResponsesInner::Multiple(vec![existing, response]);
146                }
147            },
148            ResponsesInner::Multiple(v) => {
149                v.push(response);
150            }
151        }
152    }
153
154    pub fn pop(&mut self) -> Option<Response> {
155        match &mut self.0 {
156            ResponsesInner::Single(existing) => existing.take(),
157            ResponsesInner::Multiple(v) => v.pop(),
158        }
159    }
160
161    pub fn drain<'this>(&'this mut self) -> ResponsesDrain<'this> {
162        match &mut self.0 {
163            ResponsesInner::Single(response) => {
164                ResponsesDrain(ResponsesDrainInner::Single(response.take()))
165            }
166            ResponsesInner::Multiple(v) => {
167                ResponsesDrain(ResponsesDrainInner::Multiple(v.drain(..)))
168            }
169        }
170    }
171}
172
173enum ResponsesInner {
174    Single(Option<Response>),
175    Multiple(Vec<Response>),
176}
177
178pub struct ResponsesDrain<'resp>(ResponsesDrainInner<'resp>);
179
180enum ResponsesDrainInner<'resp> {
181    Single(Option<Response>),
182    Multiple(std::vec::Drain<'resp, Response>),
183}
184
185impl Iterator for ResponsesDrain<'_> {
186    type Item = Response;
187
188    fn next(&mut self) -> Option<Self::Item> {
189        match &mut self.0 {
190            ResponsesDrainInner::Single(response) => response.take(),
191            ResponsesDrainInner::Multiple(iter) => iter.next(),
192        }
193    }
194}