Skip to main content

mini_serve/
router.rs

1use std::collections::HashMap;
2
3use hyper::Method;
4
5use crate::handler::Handler;
6
7/// Extracted path parameters from a matched route.
8///
9/// A request path, split on `/` and percent-decoded, exactly once per request.
10///
11/// Attached to the request before a [`crate::RouteBuilder::with_fallback`] handler runs,
12/// so the fallback reads the same segments the router matched against instead of parsing
13/// the raw URI a second time. Splitting happens *before* decoding, per RFC 3986 §3.3: a
14/// percent-encoded `%2F` is an ordinary character inside one segment, never a separator.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct PathSegments(pub Vec<String>);
17
18/// Contains a map of parameter names to their decoded string values
19/// (e.g., `id` → `"42"` from a route `/items/:id`). Populated by the router
20/// and stored in request extensions; extract via `path_params::<T>(req)`.
21#[derive(Clone, Debug, Default)]
22pub struct PathParams(pub HashMap<String, String>);
23
24/// Parsed query parameters from the request URL.
25///
26/// Contains a map of query parameter names to their values, with `+` decoded
27/// as space and percent-encoded sequences decoded. Populated by the app and
28/// stored in request extensions; extract via `query_params(&req)`, which reads a
29/// request with no query string as one with no parameters.
30#[derive(Clone, Debug, Default)]
31pub struct QueryParams(pub HashMap<String, String>);
32
33/// A trie-based HTTP router for matching requests to handlers.
34///
35/// Routes are matched by method and path, with support for dynamic path parameters
36/// (`:name`) and wildcards (`*`). Each request is matched in a single trie traversal.
37#[derive(Default)]
38pub struct Router<S> {
39	root: Node<S>,
40}
41
42struct Node<S> {
43	segment:    String,
44	param_name: String,
45	is_wildcard: bool,
46	/// Methods registered at this node, in registration order.
47	///
48	/// A `HashMap` here paid SipHash — a hasher chosen to make *insertion* of
49	/// attacker-chosen colliding keys expensive — on a map whose keys are the handful
50	/// of `Method` constants the router itself registered. An attacker controls only
51	/// the lookup key, never a key in the map, so there is no HashDoS primitive to
52	/// defend against, and a scan bounded by the number of methods on one route beats
53	/// hashing outright at this size. If handler registration ever becomes reachable
54	/// from request data, this trade has to be revisited.
55	handlers:   Vec<(Method, Handler<S>)>,
56	children:   Vec<Node<S>>,
57}
58
59impl<S> Default for Node<S> {
60	fn default() -> Self {
61		Node {
62			segment:    String::new(),
63			param_name: String::new(),
64			is_wildcard: false,
65			handlers:   Vec::new(),
66			children:   Vec::new(),
67		}
68	}
69}
70
71impl<S: Send + Sync + 'static> Router<S> {
72	/// Create a new empty router.
73	pub fn new() -> Self {
74		Router { root: Node::default() }
75	}
76
77	/// Register a handler for the given method and path.
78	///
79	/// Paths may contain static segments, dynamic parameters (`:name`), and a
80	/// wildcard (`*`) to match everything. For example: `/users/:id` or `/api/*`.
81	pub fn insert(&mut self, method: Method, path: &str, handler: Handler<S>) {
82		let segments = split_path(path);
83		let mut node = &mut self.root;
84
85		for seg in segments {
86			if seg == "*" {
87				if let Some(idx) = node.children.iter().position(|c| c.is_wildcard) {
88					node = &mut node.children[idx];
89				} else {
90					node.children.push(Node {
91						segment:    "*".to_string(),
92						param_name: String::new(),
93						is_wildcard: true,
94						handlers:   Vec::new(),
95						children:   Vec::new(),
96					});
97					node = node.children.last_mut().unwrap();
98				}
99			} else if let Some(param_name) = seg.strip_prefix(':') {
100				if let Some(idx) = node.children.iter().position(|c| c.param_name == param_name) {
101					node = &mut node.children[idx];
102				} else {
103					node.children.push(Node {
104						segment:    seg.to_string(),
105						param_name: param_name.to_string(),
106						is_wildcard: false,
107						handlers:   Vec::new(),
108						children:   Vec::new(),
109					});
110					node = node.children.last_mut().unwrap();
111				}
112			} else {
113				if let Some(idx) = node.children.iter().position(|c| c.segment == seg) {
114					node = &mut node.children[idx];
115				} else {
116					node.children.push(Node {
117						segment:    seg.to_string(),
118						param_name: String::new(),
119						is_wildcard: false,
120						handlers:   Vec::new(),
121						children:   Vec::new(),
122					});
123					node = node.children.last_mut().unwrap();
124				}
125			}
126		}
127
128		// Replace rather than push on re-registration, matching what `HashMap::insert`
129		// did: registering the same method twice on one path must leave one handler,
130		// not shadow the first with an unreachable second.
131		if let Some(slot) = node.handlers.iter_mut().find(|(m, _)| *m == method) {
132			slot.1 = handler;
133		} else {
134			node.handlers.push((method, handler));
135		}
136	}
137
138	/// Find and return the handler for the given method and path, along with extracted parameters.
139	///
140	/// Returns `None` if no route matches. Path parameters are percent-decoded and
141	/// included in the returned `PathParams`.
142	/// Test-only convenience: unit tests address routes the way a reader does.
143	/// Delegates to the segment form rather than being a second implementation.
144	#[cfg(test)]
145	pub fn match_route<'a>(
146		&'a self,
147		method: &Method,
148		path: &str,
149	) -> Option<(&'a Handler<S>, PathParams)> {
150		self.match_segments(method, &split_path(path))
151	}
152
153	/// [`match_route`](Self::match_route) against segments that have already been split
154	/// and decoded.
155	///
156	/// The app splits the path once per request and hands the result to every consumer —
157	/// the router, the method list, and the fallback — so there is exactly one answer in
158	/// the process to "what are this request's path segments". Two independent answers is
159	/// how `mini-static` came to treat `%2F` as a separator while the router did not.
160	pub(crate) fn match_segments<'a>(
161		&'a self,
162		method: &Method,
163		segments: &[String],
164	) -> Option<(&'a Handler<S>, PathParams)> {
165		let mut params = PathParams::default();
166		let node = Self::find_node(&self.root, segments, 0, method, &mut params)?;
167		node.handlers.iter().find(|(m, _)| m == method).map(|(_, h)| (h, params))
168	}
169
170	/// List all HTTP methods that have handlers registered for the given path.
171	///
172	/// Returns an empty vector if the path has no registered handlers.
173	/// Test-only convenience: unit tests address routes the way a reader does.
174	/// Delegates to the segment form rather than being a second implementation.
175	#[cfg(test)]
176	pub fn allowed_methods(&self, path: &str) -> Vec<Method> {
177		self.allowed_methods_for(&split_path(path))
178	}
179
180	/// [`allowed_methods`](Self::allowed_methods) against pre-split segments.
181	pub(crate) fn allowed_methods_for(&self, segments: &[String]) -> Vec<Method> {
182		let mut methods = std::collections::HashSet::new();
183		let mut params = PathParams::default();
184		Self::collect_allowed_methods(&self.root, segments, 0, &mut params, &mut methods);
185		methods.into_iter().collect()
186	}
187
188	/// Whether any handler is registered for these segments, for any method.
189	pub(crate) fn segments_exist(&self, segments: &[String]) -> bool {
190		!self.allowed_methods_for(segments).is_empty()
191	}
192
193	/// Recursive backtracking search for a trie node that is both
194	/// path-complete *and* has a handler for `method`.
195	///
196	/// Tries children in precedence order: static → param → wildcard.
197	/// Backtracks to the next-choice child on either kind of dead end: no
198	/// path match, or a path-complete node with no handler for `method` (so
199	/// e.g. a registered `POST /users/new` doesn't shadow `GET /users/:id`
200	/// for a `GET /users/new` request — the static branch is path-complete
201	/// but lacks GET, so the search falls back to the param branch).
202	///
203	/// Optimization: borrows params through static segments without cloning,
204	/// only cloning before modifying for param branches. On backtrack,
205	/// truncates params to remove any additions made in failed attempts.
206	fn find_node<'a>(
207		node: &'a Node<S>,
208		segments: &[String],
209		idx: usize,
210		method: &Method,
211		params: &mut PathParams,
212	) -> Option<&'a Node<S>> {
213		if idx == segments.len() {
214			return if node.handlers.iter().any(|(m, _)| m == method) { Some(node) } else { None };
215		}
216
217		let seg = &segments[idx];
218
219		for child in &node.children {
220			if !child.is_wildcard && child.param_name.is_empty() && child.segment == *seg {
221				// Static segment: pass params through without cloning.
222				if let Some(found) = Self::find_node(child, segments, idx + 1, method, params) {
223					return Some(found);
224				}
225			}
226		}
227
228		for child in &node.children {
229			if !child.is_wildcard && !child.param_name.is_empty() {
230				// Param segment: clone before modifying for backtrack safety.
231				let mut p = params.clone();
232				p.0.insert(child.param_name.clone(), seg.clone());
233				if let Some(found) = Self::find_node(child, segments, idx + 1, method, &mut p) {
234					*params = p;
235					return Some(found);
236				}
237				// Backtrack: remove the param we added in this failed attempt.
238				params.0.remove(&child.param_name);
239			}
240		}
241
242		for child in &node.children {
243			if child.is_wildcard && child.handlers.iter().any(|(m, _)| m == method) {
244				params.0.insert("*".to_string(), segments[idx..].join("/"));
245				return Some(child);
246			}
247		}
248
249		None
250	}
251
252	/// Exhaustive counterpart to [`find_node`] for computing the `Allow`
253	/// header: unions handler methods across *every* path-complete node
254	/// reachable, not just the first one the static→param→wildcard
255	/// precedence would settle on. Necessary because, per the same
256	/// ambiguity `find_node` backtracks around, more than one branch can be
257	/// path-complete for a given request path.
258	fn collect_allowed_methods(
259		node: &Node<S>,
260		segments: &[String],
261		idx: usize,
262		params: &mut PathParams,
263		methods: &mut std::collections::HashSet<Method>,
264	) {
265		if idx == segments.len() {
266			methods.extend(node.handlers.iter().map(|(m, _)| m.clone()));
267			return;
268		}
269
270		let seg = &segments[idx];
271
272		for child in &node.children {
273			if !child.is_wildcard && child.param_name.is_empty() && child.segment == *seg {
274				let mut p = params.clone();
275				Self::collect_allowed_methods(child, segments, idx + 1, &mut p, methods);
276			}
277		}
278
279		for child in &node.children {
280			if !child.is_wildcard && !child.param_name.is_empty() {
281				let mut p = params.clone();
282				p.0.insert(child.param_name.clone(), seg.clone());
283				Self::collect_allowed_methods(child, segments, idx + 1, &mut p, methods);
284			}
285		}
286
287		for child in &node.children {
288			if child.is_wildcard {
289				methods.extend(child.handlers.iter().map(|(m, _)| m.clone()));
290			}
291		}
292	}
293}
294
295pub(crate) fn split_path(path: &str) -> Vec<String> {
296	path.trim_start_matches('/')
297		.split('/')
298		.filter(|s| !s.is_empty())
299		.map(|s| percent_encoding::percent_decode_str(s).decode_utf8_lossy().into_owned())
300		.collect()
301}
302
303#[cfg(test)]
304#[path = "../tests/unit/router.rs"]
305mod tests;
306
307#[cfg(test)]
308#[path = "../tests/unit/router_properties.rs"]
309mod property_tests;