1use std::collections::BTreeMap;
5use std::collections::BTreeSet;
6use std::ffi::OsString;
7use std::path::Path;
8use std::path::PathBuf;
9use std::sync::Arc;
10
11use compact_str::CompactString;
12use compact_str::format_compact;
13use thiserror::Error;
14use zerotrie::ZeroTrieBuildError;
15use zerotrie::ZeroTrieSimpleAscii;
16
17use crate::response::Response;
18use crate::response::StatusCode;
19use crate::settings::ExposeHiddenFiles;
20use crate::settings::HandlerSettings;
21
22mod mime;
23pub struct Handler {
24 file_data: Vec<HandlerFileData>,
25 mime_types: Vec<&'static str>,
26 path_data: Vec<HandlerPathData>,
27 fallback_idx: Option<usize>,
28 path_trie: ZeroTrieSimpleAscii<Vec<u8>>,
29}
30
31struct HandlerFileData {
32 resolved_path: Arc<Path>,
33 len: u64,
34}
35
36enum HandlerPathData {
37 Found {
38 file_idx: usize,
39 mime_idx: Option<usize>,
40 },
41 DirRedirect(Arc<str>),
42}
43
44#[derive(Debug, Error)]
45pub enum HandlerBuildError {
46 #[error("Loop while walking paths")]
47 PathLoop,
48
49 #[error(transparent)]
50 IoError(#[from] std::io::Error),
51
52 #[error("Non-Utf8 path component")]
53 NonUtf8Component(OsString),
54
55 #[error("Non-ascii path component: {0}")]
56 NonAsciiComponent(CompactString),
57
58 #[error(transparent)]
59 ZeroTrieBuildError(#[from] ZeroTrieBuildError),
60}
61
62impl Handler {
63 pub fn build_from_root(
64 mut root: PathBuf,
65 index_file: &str,
66 fallback_path: Option<&str>,
67 expose_hidden: ExposeHiddenFiles,
68 ) -> Result<Self, HandlerBuildError> {
69 let mut file_data: Vec<HandlerFileData> = Vec::new();
70 let mut mime_types: Vec<&'static str> = Vec::new();
71 let mut path_data: Vec<HandlerPathData> = Vec::new();
72
73 let mut file_index: BTreeMap<PathBuf, usize> = BTreeMap::new();
74 let mut mime_index: BTreeMap<&'static str, usize> = BTreeMap::new();
75
76 let mut entries: Vec<(CompactString, usize)> = Vec::new();
77
78 let mut dirs_searched: BTreeSet<PathBuf> = BTreeSet::new();
79 let mut search: Vec<(CompactString, PathBuf)> = Vec::new();
80
81 {
82 root = root.canonicalize()?;
83 let metadata = root.metadata()?;
84
85 if !metadata.is_dir() {
86 return Err(HandlerBuildError::IoError(
87 std::io::ErrorKind::NotADirectory.into(),
88 ));
89 }
90
91 dirs_searched.insert(root.clone());
92 search.push(("/".into(), root));
93 }
94
95 while let Some((http_prefix, dir_path)) = search.pop() {
96 'entries: for entry in std::fs::read_dir(dir_path)? {
97 let entry = entry?;
98 let original_path = entry.path();
99 let resolved_path = original_path.canonicalize()?;
100
101 if dirs_searched.contains(resolved_path.as_path()) {
102 return Err(HandlerBuildError::PathLoop);
103 }
104
105 let file_name = entry.file_name();
106
107 let http_name = if let Some(s) = file_name.to_str() {
108 if s.is_ascii() {
109 s
110 } else {
111 return Err(HandlerBuildError::NonAsciiComponent(s.into()));
112 }
113 } else {
114 return Err(HandlerBuildError::NonUtf8Component(file_name));
115 };
116
117 if http_name.starts_with('.') {
118 match expose_hidden {
119 ExposeHiddenFiles::OnlyWellKnown => {
120 if http_prefix == "/" && http_name == ".well-known" {
121 } else {
123 continue;
124 }
125 }
126 ExposeHiddenFiles::Hide => continue 'entries,
127 ExposeHiddenFiles::Expose => (),
128 }
129 }
130
131 let http_path = format_compact!("{http_prefix}{http_name}");
132
133 let file_idx = match file_index.get(&resolved_path) {
134 Some(&file_idx) => file_idx,
135 None => {
136 let metadata = entry.metadata()?;
137
138 if metadata.is_dir() {
139 dirs_searched.insert(resolved_path.clone());
140 search.push((
141 format_compact!("{http_prefix}{http_name}/"),
142 resolved_path,
143 ));
144 continue;
145 }
146
147 let file_idx = file_data.len();
148
149 file_data.push(HandlerFileData {
150 resolved_path: Arc::from(resolved_path.as_path()),
151 len: metadata.len(),
152 });
153
154 file_index.insert(resolved_path, file_idx);
155
156 file_idx
157 }
158 };
159
160 let mime_idx = mime::mime_from_path(&original_path).map(|mime_type| {
161 *mime_index.entry(mime_type).or_insert_with(|| {
162 let idx = mime_types.len();
163 mime_types.push(mime_type);
164 idx
165 })
166 });
167
168 let path_idx = path_data.len();
169
170 path_data.push(HandlerPathData::Found { file_idx, mime_idx });
171
172 entries.push((http_path, path_idx));
173
174 if http_name == index_file {
175 entries.push((http_prefix.clone(), path_idx));
176
177 path_data.push(HandlerPathData::DirRedirect(Arc::from(
178 http_prefix.as_ref(),
179 )));
180 entries.push((http_prefix.trim_end_matches('/').into(), path_idx + 1));
181 }
182 }
183 }
184
185 let path_trie: ZeroTrieSimpleAscii<Vec<u8>> = entries.into_iter().collect();
186
187 let fallback_idx = fallback_path.and_then(|path| path_trie.get(path));
188
189 Ok(Self {
190 path_trie,
191 path_data,
192 file_data,
193 mime_types,
194 fallback_idx,
195 })
196 }
197
198 pub fn handle(&self, method: Option<&str>, path: Option<&str>) -> Result<Response, Response> {
199 let send_body = match method {
200 Some("HEAD") => false,
201 Some("GET") => true,
202 Some(_) | None => {
203 return Ok(Response::StatusStr(StatusCode::METHOD_NOT_ALLOWED));
204 }
205 };
206
207 let (path, query_sep, query) = match path {
208 Some(path) => {
209 if let Some((path, query)) = path.split_once('?') {
210 (path, "?", query)
211 } else {
212 (path, "", "")
213 }
214 }
215 None => {
216 return Ok(Response::StatusStr(StatusCode::BAD_REQUEST));
217 }
218 };
219
220 let path_idx = match self.path_trie.get(path).or(self.fallback_idx) {
221 Some(idx) => idx,
222 None => return Ok(Response::NotFound),
223 };
224
225 let path_data = match self.path_data.get(path_idx) {
226 Some(data) => data,
227 None => {
228 return Ok(Response::StatusStr(StatusCode::INTERNAL_SERVER_ERROR));
229 }
230 };
231
232 let (file_data, mime_idx) = match path_data {
233 HandlerPathData::Found { file_idx, mime_idx } => match self.file_data.get(*file_idx) {
234 Some(data) => (data, mime_idx),
235 None => return Ok(Response::NotFound),
236 },
237 HandlerPathData::DirRedirect(path) => {
238 let len = path.len() + query_sep.len() + query.len();
239
240 if len > 256 {
241 return Ok(Response::StatusStr(StatusCode::URI_TOO_LONG));
242 } else {
243 return Ok(Response::Redirect {
244 path: Arc::clone(path),
245 query: format_compact!("{query_sep}{query}"),
246 });
247 }
248 }
249 };
250
251 let mime_type = mime_idx.and_then(|mime_idx| self.mime_types.get(mime_idx).cloned());
252
253 let resolved_path = if send_body {
254 Some(Arc::clone(&file_data.resolved_path))
255 } else {
256 None
257 };
258
259 Ok(Response::Found {
260 resolved_path,
261 len: file_data.len,
262 mime_type,
263 })
264 }
265}
266
267impl TryFrom<HandlerSettings> for Handler {
268 type Error = HandlerBuildError;
269
270 fn try_from(settings: HandlerSettings) -> Result<Self, Self::Error> {
271 Self::build_from_root(
272 settings.root,
273 settings.index_file.as_str(),
274 settings.fallback_path.as_deref(),
275 settings.expose_hidden,
276 )
277 }
278}