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