1use std::{
2 borrow::{Borrow, Cow},
3 cell::RefCell,
4 collections::HashMap,
5 fmt::Write as _,
6 hash::{BuildHasher, Hash},
7 rc::{Rc, Weak},
8};
9
10use actix_router::ResourceDef;
11use foldhash::HashMap as FoldHashMap;
12use url::Url;
13
14use crate::{error::UrlGenerationError, request::HttpRequest};
15
16const AVG_PATH_LEN: usize = 24;
17
18#[derive(Clone, Debug)]
19pub struct ResourceMap {
20 pattern: ResourceDef,
21
22 named: FoldHashMap<String, Rc<ResourceMap>>,
25
26 parent: RefCell<Weak<ResourceMap>>,
27
28 nodes: Option<Vec<Rc<ResourceMap>>>,
30}
31
32impl ResourceMap {
33 pub fn new(root: ResourceDef) -> Self {
35 ResourceMap {
36 pattern: root,
37 named: FoldHashMap::default(),
38 parent: RefCell::new(Weak::new()),
39 nodes: Some(Vec::new()),
40 }
41 }
42
43 #[allow(dead_code)]
45 pub(crate) fn tree(&self) -> String {
46 let mut buf = String::new();
47 self._tree(&mut buf, 0);
48 buf
49 }
50
51 pub(crate) fn _tree(&self, buf: &mut String, level: usize) {
52 if let Some(children) = &self.nodes {
53 for child in children {
54 writeln!(
55 buf,
56 "{}{} {}",
57 "--".repeat(level),
58 child.pattern.pattern().unwrap(),
59 child
60 .pattern
61 .name()
62 .map(|name| format!("({})", name))
63 .unwrap_or_else(|| "".to_owned())
64 )
65 .unwrap();
66
67 ResourceMap::_tree(child, buf, level + 1);
68 }
69 }
70 }
71
72 pub fn add(&mut self, pattern: &mut ResourceDef, nested: Option<Rc<ResourceMap>>) {
78 pattern.set_id(self.nodes.as_ref().unwrap().len() as u16);
79
80 if let Some(new_node) = nested {
81 debug_assert_eq!(
82 &new_node.pattern, pattern,
83 "`pattern` and `nested` mismatch"
84 );
85 self.named.extend(new_node.named.clone());
87 self.nodes.as_mut().unwrap().push(new_node);
88 } else {
89 let new_node = Rc::new(ResourceMap {
90 pattern: pattern.clone(),
91 named: FoldHashMap::default(),
92 parent: RefCell::new(Weak::new()),
93 nodes: None,
94 });
95
96 if let Some(name) = pattern.name() {
97 self.named.insert(name.to_owned(), Rc::clone(&new_node));
98 }
99
100 let is_external = match pattern.pattern() {
101 Some(p) => !p.is_empty() && !p.starts_with('/'),
102 None => false,
103 };
104
105 if !is_external {
107 self.nodes.as_mut().unwrap().push(new_node);
108 }
109 }
110 }
111
112 pub(crate) fn finish(self: &Rc<Self>) {
113 for node in self.nodes.iter().flatten() {
114 node.parent.replace(Rc::downgrade(self));
115 ResourceMap::finish(node);
116 }
117 }
118
119 pub fn url_for<U, I>(
123 &self,
124 req: &HttpRequest,
125 name: &str,
126 elements: U,
127 ) -> Result<Url, UrlGenerationError>
128 where
129 U: IntoIterator<Item = I>,
130 I: AsRef<str>,
131 {
132 let mut elements = elements.into_iter();
133
134 let path = self
135 .named
136 .get(name)
137 .ok_or(UrlGenerationError::ResourceNotFound)?
138 .root_rmap_fn(String::with_capacity(AVG_PATH_LEN), |mut acc, node| {
139 node.pattern
140 .resource_path_from_iter(&mut acc, &mut elements)
141 .then_some(acc)
142 })
143 .ok_or(UrlGenerationError::NotEnoughElements)?;
144
145 self.url_from_path(req, path)
146 }
147
148 pub fn url_for_map<K, V, S>(
152 &self,
153 req: &HttpRequest,
154 name: &str,
155 elements: &HashMap<K, V, S>,
156 ) -> Result<Url, UrlGenerationError>
157 where
158 K: Borrow<str> + Eq + Hash,
159 V: AsRef<str>,
160 S: BuildHasher,
161 {
162 let path = self
163 .named
164 .get(name)
165 .ok_or(UrlGenerationError::ResourceNotFound)?
166 .root_rmap_fn(String::with_capacity(AVG_PATH_LEN), |mut acc, node| {
167 node.pattern
168 .resource_path_from_map(&mut acc, elements)
169 .then_some(acc)
170 })
171 .ok_or(UrlGenerationError::NotEnoughElements)?;
172
173 self.url_from_path(req, path)
174 }
175
176 pub fn url_for_iter<K, V, I>(
180 &self,
181 req: &HttpRequest,
182 name: &str,
183 elements: I,
184 ) -> Result<Url, UrlGenerationError>
185 where
186 I: IntoIterator<Item = (K, V)>,
187 K: Borrow<str> + Eq + Hash,
188 V: AsRef<str>,
189 {
190 let elements = elements.into_iter().collect::<FoldHashMap<K, V>>();
191 self.url_for_map(req, name, &elements)
192 }
193
194 fn url_from_path(&self, req: &HttpRequest, path: String) -> Result<Url, UrlGenerationError> {
195 let (base, path): (Cow<'_, _>, _) = if path.starts_with('/') {
196 let conn = req.connection_info();
198 let base = format!("{}://{}", conn.scheme(), conn.host());
199 (Cow::Owned(base), path.as_str())
200 } else {
201 let third_slash_index = path
203 .char_indices()
204 .filter_map(|(i, c)| (c == '/').then_some(i))
205 .nth(2)
206 .unwrap_or(path.len());
207
208 (
209 Cow::Borrowed(&path[..third_slash_index]),
210 &path[third_slash_index..],
211 )
212 };
213
214 let mut url = Url::parse(&base)?;
215 url.set_path(path);
216 Ok(url)
217 }
218
219 pub fn has_resource(&self, path: &str) -> bool {
221 self.find_matching_node(path).is_some()
222 }
223
224 pub fn match_name(&self, path: &str) -> Option<&str> {
227 self.find_matching_node(path)?.pattern.name()
228 }
229
230 pub fn match_pattern(&self, path: &str) -> Option<String> {
233 self.find_matching_node(path)?.root_rmap_fn(
234 String::with_capacity(AVG_PATH_LEN),
235 |mut acc, node| {
236 let pattern = node.pattern.pattern()?;
237 acc.push_str(pattern);
238 Some(acc)
239 },
240 )
241 }
242
243 pub(crate) fn is_resource_path_match(&self, resource_path: &[u16]) -> bool {
244 self.find_node_by_resource_path(resource_path)
245 .is_some_and(|node| node.nodes.is_none())
246 }
247
248 pub(crate) fn match_name_by_resource_path(&self, resource_path: &[u16]) -> Option<&str> {
249 self.find_node_by_resource_path(resource_path)?
250 .pattern
251 .name()
252 }
253
254 pub(crate) fn match_pattern_by_resource_path(&self, resource_path: &[u16]) -> Option<String> {
255 self.find_node_by_resource_path(resource_path)?
256 .root_rmap_fn(String::with_capacity(AVG_PATH_LEN), |mut acc, node| {
257 let pattern = node.pattern.pattern()?;
258 acc.push_str(pattern);
259 Some(acc)
260 })
261 }
262
263 fn find_matching_node(&self, path: &str) -> Option<&ResourceMap> {
264 self._find_matching_node(path).flatten()
265 }
266
267 fn find_node_by_resource_path(&self, resource_path: &[u16]) -> Option<&ResourceMap> {
268 let mut node = self;
269
270 for id in resource_path {
271 node = node.nodes.as_ref()?.get(*id as usize)?;
272 }
273
274 Some(node)
275 }
276
277 fn _find_matching_node(&self, path: &str) -> Option<Option<&ResourceMap>> {
281 let matched_len = self.pattern.find_match(path)?;
282 let path = &path[matched_len..];
283
284 Some(match &self.nodes {
285 Some(nodes) => nodes
287 .iter()
288 .filter_map(|node| node._find_matching_node(path))
289 .next()
290 .flatten(),
291
292 None => Some(self),
294 })
295 }
296
297 fn root_rmap_fn<F, B>(&self, init: B, mut f: F) -> Option<B>
299 where
300 F: FnMut(B, &ResourceMap) -> Option<B>,
301 {
302 self._root_rmap_fn(init, &mut f)
303 }
304
305 fn _root_rmap_fn<F, B>(&self, init: B, f: &mut F) -> Option<B>
307 where
308 F: FnMut(B, &ResourceMap) -> Option<B>,
309 {
310 let data = match self.parent.borrow().upgrade() {
311 Some(ref parent) => parent._root_rmap_fn(init, f)?,
312 None => init,
313 };
314
315 f(data, self)
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn extract_matched_pattern() {
325 let mut root = ResourceMap::new(ResourceDef::root_prefix(""));
326
327 let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}"));
328 user_map.add(&mut ResourceDef::new("/"), None);
329 user_map.add(&mut ResourceDef::new("/profile"), None);
330 user_map.add(&mut ResourceDef::new("/article/{id}"), None);
331 user_map.add(&mut ResourceDef::new("/post/{post_id}"), None);
332 user_map.add(
333 &mut ResourceDef::new("/post/{post_id}/comment/{comment_id}"),
334 None,
335 );
336
337 root.add(&mut ResourceDef::new("/info"), None);
338 root.add(&mut ResourceDef::new("/v{version:[[:digit:]]{1}}"), None);
339 root.add(
340 &mut ResourceDef::root_prefix("/user/{id}"),
341 Some(Rc::new(user_map)),
342 );
343 root.add(&mut ResourceDef::new("/info"), None);
344
345 let root = Rc::new(root);
346 ResourceMap::finish(&root);
347
348 assert!(root.has_resource("/info"));
351 assert!(!root.has_resource("/bar"));
352
353 assert!(root.has_resource("/v1"));
354 assert!(root.has_resource("/v2"));
355 assert!(!root.has_resource("/v33"));
356
357 assert!(!root.has_resource("/user/22"));
358 assert!(root.has_resource("/user/22/"));
359 assert!(root.has_resource("/user/22/profile"));
360
361 assert!(root.match_pattern("/bar").is_none());
364 assert!(root.match_pattern("/v44").is_none());
365
366 assert_eq!(root.match_pattern("/info"), Some("/info".to_owned()));
367 assert_eq!(
368 root.match_pattern("/v1"),
369 Some("/v{version:[[:digit:]]{1}}".to_owned())
370 );
371 assert_eq!(
372 root.match_pattern("/v2"),
373 Some("/v{version:[[:digit:]]{1}}".to_owned())
374 );
375 assert_eq!(
376 root.match_pattern("/user/22/profile"),
377 Some("/user/{id}/profile".to_owned())
378 );
379 assert_eq!(
380 root.match_pattern("/user/602CFB82-7709-4B17-ADCF-4C347B6F2203/profile"),
381 Some("/user/{id}/profile".to_owned())
382 );
383 assert_eq!(
384 root.match_pattern("/user/22/article/44"),
385 Some("/user/{id}/article/{id}".to_owned())
386 );
387 assert_eq!(
388 root.match_pattern("/user/22/post/my-post"),
389 Some("/user/{id}/post/{post_id}".to_owned())
390 );
391 assert_eq!(
392 root.match_pattern("/user/22/post/other-post/comment/42"),
393 Some("/user/{id}/post/{post_id}/comment/{comment_id}".to_owned())
394 );
395 }
396
397 #[test]
398 fn extract_matched_name() {
399 let mut root = ResourceMap::new(ResourceDef::root_prefix(""));
400
401 let mut rdef = ResourceDef::new("/info");
402 rdef.set_name("root_info");
403 root.add(&mut rdef, None);
404
405 let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}"));
406 let mut rdef = ResourceDef::new("/");
407 user_map.add(&mut rdef, None);
408
409 let mut rdef = ResourceDef::new("/post/{post_id}");
410 rdef.set_name("user_post");
411 user_map.add(&mut rdef, None);
412
413 root.add(
414 &mut ResourceDef::root_prefix("/user/{id}"),
415 Some(Rc::new(user_map)),
416 );
417
418 let root = Rc::new(root);
419 ResourceMap::finish(&root);
420
421 assert!(root.has_resource("/info"));
424 assert!(!root.has_resource("/bar"));
425
426 assert!(!root.has_resource("/user/22"));
427 assert!(root.has_resource("/user/22/"));
428 assert!(root.has_resource("/user/22/post/55"));
429
430 assert!(root.match_name("/bar").is_none());
433 assert!(root.match_name("/v44").is_none());
434
435 assert_eq!(root.match_name("/info"), Some("root_info"));
436 assert_eq!(root.match_name("/user/22"), None);
437 assert_eq!(root.match_name("/user/22/"), None);
438 assert_eq!(root.match_name("/user/22/post/55"), Some("user_post"));
439 }
440
441 #[test]
442 fn bug_fix_issue_1582_debug_print_exits() {
443 let mut root = ResourceMap::new(ResourceDef::root_prefix(""));
445
446 let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}"));
447 user_map.add(&mut ResourceDef::new("/"), None);
448 user_map.add(&mut ResourceDef::new("/profile"), None);
449 user_map.add(&mut ResourceDef::new("/article/{id}"), None);
450 user_map.add(&mut ResourceDef::new("/post/{post_id}"), None);
451 user_map.add(
452 &mut ResourceDef::new("/post/{post_id}/comment/{comment_id}"),
453 None,
454 );
455
456 root.add(
457 &mut ResourceDef::root_prefix("/user/{id}"),
458 Some(Rc::new(user_map)),
459 );
460
461 let root = Rc::new(root);
462 ResourceMap::finish(&root);
463
464 assert!(root.parent.borrow().upgrade().is_none());
466 assert!(root.nodes.as_ref().unwrap()[0]
468 .parent
469 .borrow()
470 .upgrade()
471 .is_some());
472 assert!(Rc::ptr_eq(
474 &root.nodes.as_ref().unwrap()[0]
475 .parent
476 .borrow()
477 .upgrade()
478 .unwrap(),
479 &root
480 ));
481
482 let output = format!("{:?}", root);
483 assert!(output.starts_with("ResourceMap {"));
484 assert!(output.ends_with(" }"));
485 }
486
487 #[test]
488 fn short_circuit() {
489 let mut root = ResourceMap::new(ResourceDef::prefix(""));
490
491 let mut user_root = ResourceDef::prefix("/user");
492 let mut user_map = ResourceMap::new(user_root.clone());
493 user_map.add(&mut ResourceDef::new("/u1"), None);
494 user_map.add(&mut ResourceDef::new("/u2"), None);
495
496 root.add(&mut ResourceDef::new("/user/u3"), None);
497 root.add(&mut user_root, Some(Rc::new(user_map)));
498 root.add(&mut ResourceDef::new("/user/u4"), None);
499
500 let rmap = Rc::new(root);
501 ResourceMap::finish(&rmap);
502
503 assert!(rmap.has_resource("/user/u1"));
504 assert!(rmap.has_resource("/user/u2"));
505 assert!(rmap.has_resource("/user/u3"));
506 assert!(!rmap.has_resource("/user/u4"));
507 }
508
509 #[test]
510 fn url_for() {
511 let mut root = ResourceMap::new(ResourceDef::prefix(""));
512
513 let mut user_scope_rdef = ResourceDef::prefix("/user");
514 let mut user_scope_map = ResourceMap::new(user_scope_rdef.clone());
515
516 let mut user_rdef = ResourceDef::new("/{user_id}");
517 let mut user_map = ResourceMap::new(user_rdef.clone());
518
519 let mut post_rdef = ResourceDef::new("/post/{sub_id}");
520 post_rdef.set_name("post");
521
522 user_map.add(&mut post_rdef, None);
523 user_scope_map.add(&mut user_rdef, Some(Rc::new(user_map)));
524 root.add(&mut user_scope_rdef, Some(Rc::new(user_scope_map)));
525
526 let rmap = Rc::new(root);
527 ResourceMap::finish(&rmap);
528
529 let mut req = crate::test::TestRequest::default();
530 req.set_server_hostname("localhost:8888");
531 let req = req.to_http_request();
532
533 let url = rmap
534 .url_for(&req, "post", ["u123", "foobar"])
535 .unwrap()
536 .to_string();
537 assert_eq!(url, "http://localhost:8888/user/u123/post/foobar");
538
539 assert!(rmap.url_for(&req, "missing", ["u123"]).is_err());
540 }
541
542 #[test]
543 fn url_for_parser() {
544 let mut root = ResourceMap::new(ResourceDef::prefix(""));
545
546 let mut rdef_1 = ResourceDef::new("/{var}");
547 rdef_1.set_name("internal");
548
549 let mut rdef_2 = ResourceDef::new("http://host.dom/{var}");
550 rdef_2.set_name("external.1");
551
552 let mut rdef_3 = ResourceDef::new("{var}");
553 rdef_3.set_name("external.2");
554
555 root.add(&mut rdef_1, None);
556 root.add(&mut rdef_2, None);
557 root.add(&mut rdef_3, None);
558 let rmap = Rc::new(root);
559 ResourceMap::finish(&rmap);
560
561 let mut req = crate::test::TestRequest::default();
562 req.set_server_hostname("localhost:8888");
563 let req = req.to_http_request();
564
565 const INPUT: &[&str] = &["a/../quick brown%20fox/%nan?query#frag"];
566 const OUTPUT: &str = "/quick%20brown%20fox/%nan%3Fquery%23frag";
567
568 let url = rmap.url_for(&req, "internal", INPUT).unwrap();
569 assert_eq!(url.path(), OUTPUT);
570
571 let url = rmap.url_for(&req, "external.1", INPUT).unwrap();
572 assert_eq!(url.path(), OUTPUT);
573
574 assert!(rmap.url_for(&req, "external.2", INPUT).is_err());
575 assert!(rmap.url_for(&req, "external.2", [""]).is_err());
576 }
577
578 #[test]
579 fn external_resource_with_no_name() {
580 let mut root = ResourceMap::new(ResourceDef::prefix(""));
581
582 let mut rdef = ResourceDef::new("https://duck.com/{query}");
583 root.add(&mut rdef, None);
584
585 let rmap = Rc::new(root);
586 ResourceMap::finish(&rmap);
587
588 assert!(!rmap.has_resource("https://duck.com/abc"));
589 }
590
591 #[test]
592 fn external_resource_with_name() {
593 let mut root = ResourceMap::new(ResourceDef::prefix(""));
594
595 let mut rdef = ResourceDef::new("https://duck.com/{query}");
596 rdef.set_name("duck");
597 root.add(&mut rdef, None);
598
599 let rmap = Rc::new(root);
600 ResourceMap::finish(&rmap);
601
602 assert!(!rmap.has_resource("https://duck.com/abc"));
603
604 let mut req = crate::test::TestRequest::default();
605 req.set_server_hostname("localhost:8888");
606 let req = req.to_http_request();
607
608 assert_eq!(
609 rmap.url_for(&req, "duck", ["abcd"]).unwrap().to_string(),
610 "https://duck.com/abcd"
611 );
612 }
613
614 #[test]
615 fn url_for_override_within_map() {
616 let mut root = ResourceMap::new(ResourceDef::prefix(""));
617
618 let mut foo_rdef = ResourceDef::prefix("/foo");
619 let mut foo_map = ResourceMap::new(foo_rdef.clone());
620 let mut nested_rdef = ResourceDef::new("/nested");
621 nested_rdef.set_name("nested");
622 foo_map.add(&mut nested_rdef, None);
623 root.add(&mut foo_rdef, Some(Rc::new(foo_map)));
624
625 let mut foo_rdef = ResourceDef::prefix("/bar");
626 let mut foo_map = ResourceMap::new(foo_rdef.clone());
627 let mut nested_rdef = ResourceDef::new("/nested");
628 nested_rdef.set_name("nested");
629 foo_map.add(&mut nested_rdef, None);
630 root.add(&mut foo_rdef, Some(Rc::new(foo_map)));
631
632 let rmap = Rc::new(root);
633 ResourceMap::finish(&rmap);
634
635 let req = crate::test::TestRequest::default().to_http_request();
636
637 let url = rmap.url_for(&req, "nested", [""; 0]).unwrap().to_string();
638 assert_eq!(url, "http://localhost:8080/bar/nested");
639
640 assert!(rmap.url_for(&req, "missing", ["u123"]).is_err());
641 }
642}