picoserve/response/
fs.rs

1//! Static files and directories.
2
3use core::fmt;
4
5use crate::{
6    io::{Read, Write},
7    request::Path,
8    routing::{PathRouter, PathRouterService, RequestHandler, RequestHandlerService},
9    ResponseSent,
10};
11
12use super::{IntoResponse, StatusCode};
13
14#[derive(Clone, PartialEq, Eq)]
15struct ETag([u8; 20]);
16
17impl fmt::Debug for ETag {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        write!(f, "ETag({self})")
20    }
21}
22
23impl fmt::Display for ETag {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        write!(f, "\"")?;
26        for b in self.0 {
27            write!(f, "{b:02x}")?;
28        }
29        write!(f, "\"")?;
30
31        Ok(())
32    }
33}
34
35impl PartialEq<[u8]> for ETag {
36    fn eq(&self, other: &[u8]) -> bool {
37        struct Eq;
38
39        fn eq(self_bytes: &[u8], other_str_bytes: &[u8]) -> Option<Eq> {
40            fn decode_hex_nibble(c: u8) -> Option<u8> {
41                Some(match c {
42                    c @ b'0'..=b'9' => c - b'0',
43                    c @ b'a'..=b'f' => 10 + c - b'a',
44                    c @ b'A'..=b'F' => 10 + c - b'A',
45                    _ => return None,
46                })
47            }
48
49            let mut other_str_bytes = other_str_bytes
50                .strip_prefix(b"\"")?
51                .strip_suffix(b"\"")?
52                .iter()
53                .copied();
54
55            for &self_byte in self_bytes {
56                let other_byte0 = decode_hex_nibble(other_str_bytes.next()?)?;
57                let other_byte1 = decode_hex_nibble(other_str_bytes.next()?)?;
58
59                let other_byte = 0x10 * other_byte0 + other_byte1;
60
61                if self_byte != other_byte {
62                    return None;
63                }
64            }
65
66            other_str_bytes.next().is_none().then_some(Eq)
67        }
68
69        matches!(eq(&self.0, other), Some(Eq))
70    }
71}
72
73impl PartialEq<&[u8]> for ETag {
74    fn eq(&self, other: &&[u8]) -> bool {
75        *self == **other
76    }
77}
78
79impl super::HeadersIter for ETag {
80    async fn for_each_header<F: super::ForEachHeader>(
81        self,
82        mut f: F,
83    ) -> Result<F::Output, F::Error> {
84        f.call("ETag", self).await?;
85        f.finalize().await
86    }
87}
88
89/// [RequestHandlerService] that serves a single file.
90#[derive(Debug, Clone)]
91pub struct File {
92    content_type: &'static str,
93    body: &'static [u8],
94    etag: ETag,
95    headers: &'static [(&'static str, &'static str)],
96}
97
98impl File {
99    pub const MIME_HTML: &'static str = "text/html; charset=utf-8";
100    pub const MIME_CSS: &'static str = "text/css";
101    pub const MIME_JS: &'static str = "application/javascript; charset=utf-8";
102
103    /// Create a file with the given content type but no additional headers.
104    pub const fn with_content_type(content_type: &'static str, body: &'static [u8]) -> Self {
105        Self {
106            content_type,
107            body,
108            etag: ETag(const_sha1::sha1(body).as_bytes()),
109            headers: &[],
110        }
111    }
112
113    /// Create a file with the given content type and some additional headers.
114    pub const fn with_content_type_and_headers(
115        content_type: &'static str,
116        body: &'static [u8],
117        headers: &'static [(&'static str, &'static str)],
118    ) -> Self {
119        Self {
120            content_type,
121            body,
122            etag: ETag(const_sha1::sha1(body).as_bytes()),
123            headers,
124        }
125    }
126
127    /// A HyperText Markup Language file with a MIME type of "text/html; charset=utf-8"
128    pub const fn html(body: &'static str) -> Self {
129        Self::with_content_type(Self::MIME_HTML, body.as_bytes())
130    }
131
132    /// Cascading StyleSheets file with a MIME type of "text/css"
133    pub const fn css(body: &'static str) -> Self {
134        Self::with_content_type(Self::MIME_CSS, body.as_bytes())
135    }
136
137    /// A Javascript file with a MIME type of "application/javascript; charset=utf-8"
138    pub const fn javascript(body: &'static str) -> Self {
139        Self::with_content_type(Self::MIME_JS, body.as_bytes())
140    }
141}
142
143impl<State, PathParameters> crate::routing::RequestHandlerService<State, PathParameters> for File {
144    async fn call_request_handler_service<R: Read, W: super::ResponseWriter<Error = R::Error>>(
145        &self,
146        _state: &State,
147        _path_parameters: PathParameters,
148        request: crate::request::Request<'_, R>,
149        response_writer: W,
150    ) -> Result<ResponseSent, W::Error> {
151        if let Some(if_none_match) = request.parts.headers().get("If-None-Match") {
152            if if_none_match
153                .split(b',')
154                .any(|etag| self.etag == etag.as_raw())
155            {
156                return response_writer
157                    .write_response(
158                        request.body_connection.finalize().await?,
159                        super::Response {
160                            status_code: StatusCode::NOT_MODIFIED,
161                            headers: self.etag.clone(),
162                            body: super::NoBody,
163                        },
164                    )
165                    .await;
166            }
167        }
168
169        struct FileContent<'a>(&'a File);
170
171        impl super::Content for FileContent<'_> {
172            fn content_type(&self) -> &'static str {
173                self.0.content_type
174            }
175
176            fn content_length(&self) -> usize {
177                self.0.body.len()
178            }
179
180            async fn write_content<W: Write>(self, mut writer: W) -> Result<(), W::Error> {
181                writer.write_all(self.0.body).await
182            }
183        }
184
185        super::Response::ok(FileContent(self))
186            .with_headers(self.headers)
187            .with_headers(self.etag.clone())
188            .write_to(request.body_connection.finalize().await?, response_writer)
189            .await
190    }
191}
192
193/// [PathRouter] that serves a single file based on the request path.
194#[derive(Debug, Default)]
195pub struct Directory {
196    /// The files in the directory.
197    pub files: &'static [(&'static str, File)],
198
199    /// Subdirectories inside this directory.
200    pub sub_directories: &'static [(&'static str, Directory)],
201}
202
203impl Directory {
204    pub const DEFAULT: Self = Self {
205        files: &[],
206        sub_directories: &[],
207    };
208
209    fn matching_file(&self, path: crate::request::Path) -> Option<&File> {
210        for (name, file) in self.files.iter() {
211            if let Some(crate::request::Path(crate::url_encoded::UrlEncodedString(""))) =
212                path.strip_slash_and_prefix(name)
213            {
214                return Some(file);
215            } else {
216                continue;
217            }
218        }
219
220        for (name, sub_directory) in self.sub_directories.iter() {
221            if let Some(path) = path.strip_slash_and_prefix(name) {
222                return sub_directory.matching_file(path);
223            } else {
224                continue;
225            }
226        }
227
228        None
229    }
230}
231
232impl<State, CurrentPathParameters> PathRouterService<State, CurrentPathParameters> for Directory {
233    async fn call_request_handler_service<R: Read, W: super::ResponseWriter<Error = R::Error>>(
234        &self,
235        state: &State,
236        current_path_parameters: CurrentPathParameters,
237        path: Path<'_>,
238        request: crate::request::Request<'_, R>,
239        response_writer: W,
240    ) -> Result<ResponseSent, W::Error> {
241        if !request.parts.method().eq_ignore_ascii_case("get") {
242            return crate::routing::MethodNotAllowed
243                .call_request_handler(state, current_path_parameters, request, response_writer)
244                .await;
245        }
246
247        if let Some(file) = self.matching_file(path) {
248            file.call_request_handler_service(
249                state,
250                current_path_parameters,
251                request,
252                response_writer,
253            )
254            .await
255        } else {
256            crate::routing::NotFound
257                .call_path_router(
258                    state,
259                    current_path_parameters,
260                    path,
261                    request,
262                    response_writer,
263                )
264                .await
265        }
266    }
267}