1use crate::request::Request;
43use crate::response::Response;
44use serde::{Deserialize, Serialize};
45use std::cmp::Reverse;
46use std::fs;
47use std::path::{Path, PathBuf};
48use thiserror::Error;
49
50#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
53pub struct Route {
54 pub name: String,
56 #[serde(default)]
59 pub host: Option<String>,
60 pub path_prefix: String,
63 #[serde(default)]
65 pub methods: Vec<String>,
66 #[serde(default)]
69 pub headers: Vec<(String, String)>,
70 #[serde(default)]
72 pub priority: i32,
73 pub document_root: PathBuf,
75 #[serde(default)]
79 pub compression: Option<bool>,
80}
81
82impl Route {
83 pub fn new(
85 name: &str,
86 path_prefix: &str,
87 document_root: impl Into<PathBuf>,
88 ) -> Self {
89 Self {
90 name: name.to_string(),
91 host: None,
92 path_prefix: path_prefix.to_string(),
93 methods: Vec::new(),
94 headers: Vec::new(),
95 priority: 0,
96 document_root: document_root.into(),
97 compression: None,
98 }
99 }
100
101 #[must_use]
103 pub fn host(mut self, host: &str) -> Self {
104 self.host = Some(host.to_string());
105 self
106 }
107
108 #[must_use]
110 pub fn method(mut self, method: &str) -> Self {
111 self.methods.push(method.to_string());
112 self
113 }
114
115 #[must_use]
117 pub fn header(mut self, name: &str, value: &str) -> Self {
118 self.headers.push((name.to_string(), value.to_string()));
119 self
120 }
121
122 #[must_use]
124 pub fn priority(mut self, priority: i32) -> Self {
125 self.priority = priority;
126 self
127 }
128
129 #[must_use]
131 pub fn compression(mut self, enabled: bool) -> Self {
132 self.compression = Some(enabled);
133 self
134 }
135
136 pub fn matches(&self, request: &Request) -> bool {
138 self.host_matches(request)
139 && prefix_matches(
140 &self.path_prefix,
141 path_only(request.path()),
142 )
143 && (self.methods.is_empty()
144 || self.methods.iter().any(|m| m == request.method()))
145 && self.headers.iter().all(|(name, value)| {
146 request.header(name) == Some(value)
147 })
148 }
149
150 fn host_matches(&self, request: &Request) -> bool {
151 let Some(expected) = self.host.as_deref() else {
152 return true;
153 };
154 request
155 .header("host")
156 .map(host_without_port)
157 .is_some_and(|actual| actual.eq_ignore_ascii_case(expected))
158 }
159
160 fn rank(&self) -> Reverse<(i32, bool, usize, usize, bool)> {
162 Reverse((
163 self.priority,
164 self.host.is_some(),
165 self.path_prefix.len(),
166 self.headers.len(),
167 !self.methods.is_empty(),
168 ))
169 }
170
171 fn normalised(mut self) -> Self {
173 self.host = self.host.map(|h| h.to_ascii_lowercase());
174 if self.path_prefix.len() > 1 {
175 let trimmed = self.path_prefix.trim_end_matches('/');
176 self.path_prefix =
177 if trimmed.is_empty() { "/" } else { trimmed }.into();
178 }
179 for method in &mut self.methods {
180 method.make_ascii_uppercase();
181 }
182 self.methods.sort();
183 self.methods.dedup();
184 for (name, _) in &mut self.headers {
185 name.make_ascii_lowercase();
186 }
187 self.headers.sort();
188 self
189 }
190
191 fn constraints(&self) -> impl PartialEq + '_ {
193 (
194 self.priority,
195 &self.host,
196 &self.path_prefix,
197 &self.methods,
198 &self.headers,
199 )
200 }
201}
202
203#[derive(Clone, Debug, PartialEq, Eq, Error)]
205pub enum RouteError {
206 #[error("route name must not be empty")]
208 EmptyName,
209 #[error("duplicate route name `{0}`")]
211 DuplicateName(String),
212 #[error(
214 "route `{route}`: path prefix `{prefix}` must start with `/`"
215 )]
216 InvalidPathPrefix {
217 route: String,
219 prefix: String,
221 },
222 #[error("route `{route}`: invalid {field} `{value}`")]
224 InvalidConstraint {
225 route: String,
227 field: &'static str,
229 value: String,
231 },
232 #[error(
234 "route `{route}`: document root `{path}` is not a directory"
235 )]
236 InvalidDocumentRoot {
237 route: String,
239 path: String,
241 },
242 #[error("routes `{first}` and `{second}` are ambiguous")]
245 Ambiguous {
246 first: String,
248 second: String,
250 },
251}
252
253#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
258#[serde(try_from = "Vec<Route>", into = "Vec<Route>")]
259pub struct Router {
260 routes: Vec<Route>,
261 canonical_roots: Vec<PathBuf>,
262}
263
264#[derive(Debug)]
266pub(crate) struct RouteTarget<'a> {
267 pub(crate) request: Request,
269 pub(crate) document_root: &'a Path,
271 pub(crate) canonical_root: &'a Path,
273 pub(crate) compression: Option<bool>,
275}
276
277impl Router {
278 pub fn new(routes: Vec<Route>) -> Result<Self, RouteError> {
287 let mut routes: Vec<Route> =
288 routes.into_iter().map(Route::normalised).collect();
289 for (index, route) in routes.iter().enumerate() {
290 validate_route(route)?;
291 if let Some(other) = routes[..index]
292 .iter()
293 .find(|other| other.name == route.name)
294 {
295 return Err(RouteError::DuplicateName(
296 other.name.clone(),
297 ));
298 }
299 if let Some(other) = routes[..index].iter().find(|other| {
300 other.constraints() == route.constraints()
301 }) {
302 return Err(RouteError::Ambiguous {
303 first: other.name.clone(),
304 second: route.name.clone(),
305 });
306 }
307 }
308 routes.sort_by_key(Route::rank);
310 let canonical_roots = routes
311 .iter()
312 .map(canonical_root)
313 .collect::<Result<_, _>>()?;
314 Ok(Self {
315 routes,
316 canonical_roots,
317 })
318 }
319
320 pub fn routes(&self) -> &[Route] {
322 &self.routes
323 }
324
325 pub fn select(&self, request: &Request) -> Option<&Route> {
327 self.routes.iter().find(|route| route.matches(request))
328 }
329
330 pub(crate) fn resolve(
332 &self,
333 request: &Request,
334 ) -> Option<RouteTarget<'_>> {
335 let index = self
336 .routes
337 .iter()
338 .position(|route| route.matches(request))?;
339 let route = &self.routes[index];
340 let mut routed = request.clone();
341 routed.path = strip_prefix(&route.path_prefix, request.path());
342 Some(RouteTarget {
343 request: routed,
344 document_root: &route.document_root,
345 canonical_root: &self.canonical_roots[index],
346 compression: route.compression,
347 })
348 }
349}
350
351impl TryFrom<Vec<Route>> for Router {
352 type Error = RouteError;
353
354 fn try_from(routes: Vec<Route>) -> Result<Self, Self::Error> {
355 Self::new(routes)
356 }
357}
358
359impl From<Router> for Vec<Route> {
360 fn from(router: Router) -> Self {
361 router.routes
362 }
363}
364
365pub(crate) fn no_route_response() -> Response {
367 let mut response =
368 Response::new(404, "NOT FOUND", b"404 Not Found".to_vec());
369 response.add_header("Content-Type", "text/plain");
370 response
371}
372
373fn validate_route(route: &Route) -> Result<(), RouteError> {
374 if route.name.is_empty() {
375 return Err(RouteError::EmptyName);
376 }
377 if !route.path_prefix.starts_with('/') {
378 return Err(RouteError::InvalidPathPrefix {
379 route: route.name.clone(),
380 prefix: route.path_prefix.clone(),
381 });
382 }
383 let invalid = |field, value: &str| RouteError::InvalidConstraint {
384 route: route.name.clone(),
385 field,
386 value: value.to_string(),
387 };
388 if let Some(host) = route.host.as_deref().filter(|h| h.is_empty()) {
389 return Err(invalid("host", host));
390 }
391 if let Some(method) = route.methods.iter().find(|m| !is_token(m)) {
392 return Err(invalid("method", method));
393 }
394 Ok(())
395}
396
397fn canonical_root(route: &Route) -> Result<PathBuf, RouteError> {
398 fs::canonicalize(&route.document_root)
399 .ok()
400 .filter(|path| path.is_dir())
401 .ok_or_else(|| RouteError::InvalidDocumentRoot {
402 route: route.name.clone(),
403 path: route.document_root.display().to_string(),
404 })
405}
406
407fn is_token(value: &str) -> bool {
408 !value.is_empty() && value.bytes().all(|b| b.is_ascii_uppercase())
409}
410
411fn path_only(path: &str) -> &str {
412 path.split_once('?').map_or(path, |(path, _)| path)
413}
414
415fn prefix_matches(prefix: &str, path: &str) -> bool {
416 prefix == "/"
417 || path.strip_prefix(prefix).is_some_and(|rest| {
418 rest.is_empty() || rest.starts_with('/')
419 })
420}
421
422fn strip_prefix(prefix: &str, path: &str) -> String {
424 if prefix == "/" {
425 return path.to_string();
426 }
427 let rest = path.strip_prefix(prefix).unwrap_or(path);
428 if rest.starts_with('/') {
429 rest.to_string()
430 } else {
431 format!("/{rest}")
432 }
433}
434
435fn host_without_port(host: &str) -> &str {
437 let host = host.trim();
438 if host.starts_with('[') {
439 return host.split_inclusive(']').next().unwrap_or(host);
440 }
441 match host.rsplit_once(':') {
442 Some((name, port))
443 if !port.is_empty()
444 && port.bytes().all(|b| b.is_ascii_digit()) =>
445 {
446 name
447 }
448 _ => host,
449 }
450}