Skip to main content

actix_web/
rmap.rs

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 resources within the tree or, for external resources, it points to isolated nodes
23    /// outside the tree.
24    named: FoldHashMap<String, Rc<ResourceMap>>,
25
26    parent: RefCell<Weak<ResourceMap>>,
27
28    /// Must be `None` for "edge" nodes.
29    nodes: Option<Vec<Rc<ResourceMap>>>,
30}
31
32impl ResourceMap {
33    /// Creates a _container_ node in the `ResourceMap` tree.
34    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    /// Format resource map as tree structure (unfinished).
44    #[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    /// Adds a (possibly nested) resource.
73    ///
74    /// To add a non-prefix pattern, `nested` must be `None`.
75    /// To add external resource, supply a pattern without a leading `/`.
76    /// The root pattern of `nested`, if present, should match `pattern`.
77    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            // parents absorb references to the named resources of children
86            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            // don't add external resources to the tree
106            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    /// Generate URL for named resource.
120    ///
121    /// Check [`HttpRequest::url_for`] for detailed information.
122    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    /// Generate URL for named resource using map of dynamic segment values.
149    ///
150    /// Check [`HttpRequest::url_for_map`] for detailed information.
151    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    /// Generate URL for named resource using an iterator of key-value pairs.
177    ///
178    /// Check [`HttpRequest::url_for_iter`] for detailed information.
179    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            // build full URL from connection info parts and resource path
197            let conn = req.connection_info();
198            let base = format!("{}://{}", conn.scheme(), conn.host());
199            (Cow::Owned(base), path.as_str())
200        } else {
201            // external resource; third slash would be the root slash in the path
202            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    /// Returns true if there is a resource that would match `path`.
220    pub fn has_resource(&self, path: &str) -> bool {
221        self.find_matching_node(path).is_some()
222    }
223
224    /// Returns the name of the route that matches the given path or None if no full match
225    /// is possible or the matching resource is not named.
226    pub fn match_name(&self, path: &str) -> Option<&str> {
227        self.find_matching_node(path)?.pattern.name()
228    }
229
230    /// Returns the full resource pattern matched against a path or None if no full match
231    /// is possible.
232    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    fn find_matching_node(&self, path: &str) -> Option<&ResourceMap> {
244        self._find_matching_node(path).flatten()
245    }
246
247    /// Returns `None` if root pattern doesn't match;
248    /// `Some(None)` if root pattern matches but there is no matching child pattern.
249    /// Don't search sideways when `Some(none)` is returned.
250    fn _find_matching_node(&self, path: &str) -> Option<Option<&ResourceMap>> {
251        let matched_len = self.pattern.find_match(path)?;
252        let path = &path[matched_len..];
253
254        Some(match &self.nodes {
255            // find first sub-node to match remaining path
256            Some(nodes) => nodes
257                .iter()
258                .filter_map(|node| node._find_matching_node(path))
259                .next()
260                .flatten(),
261
262            // only terminate at edge nodes
263            None => Some(self),
264        })
265    }
266
267    /// Find `self`'s highest ancestor and then run `F`, providing `B`, in that rmap context.
268    fn root_rmap_fn<F, B>(&self, init: B, mut f: F) -> Option<B>
269    where
270        F: FnMut(B, &ResourceMap) -> Option<B>,
271    {
272        self._root_rmap_fn(init, &mut f)
273    }
274
275    /// Run `F`, providing `B`, if `self` is top-level resource map, else recurse to parent map.
276    fn _root_rmap_fn<F, B>(&self, init: B, f: &mut F) -> Option<B>
277    where
278        F: FnMut(B, &ResourceMap) -> Option<B>,
279    {
280        let data = match self.parent.borrow().upgrade() {
281            Some(ref parent) => parent._root_rmap_fn(init, f)?,
282            None => init,
283        };
284
285        f(data, self)
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    #[test]
294    fn extract_matched_pattern() {
295        let mut root = ResourceMap::new(ResourceDef::root_prefix(""));
296
297        let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}"));
298        user_map.add(&mut ResourceDef::new("/"), None);
299        user_map.add(&mut ResourceDef::new("/profile"), None);
300        user_map.add(&mut ResourceDef::new("/article/{id}"), None);
301        user_map.add(&mut ResourceDef::new("/post/{post_id}"), None);
302        user_map.add(
303            &mut ResourceDef::new("/post/{post_id}/comment/{comment_id}"),
304            None,
305        );
306
307        root.add(&mut ResourceDef::new("/info"), None);
308        root.add(&mut ResourceDef::new("/v{version:[[:digit:]]{1}}"), None);
309        root.add(
310            &mut ResourceDef::root_prefix("/user/{id}"),
311            Some(Rc::new(user_map)),
312        );
313        root.add(&mut ResourceDef::new("/info"), None);
314
315        let root = Rc::new(root);
316        ResourceMap::finish(&root);
317
318        // sanity check resource map setup
319
320        assert!(root.has_resource("/info"));
321        assert!(!root.has_resource("/bar"));
322
323        assert!(root.has_resource("/v1"));
324        assert!(root.has_resource("/v2"));
325        assert!(!root.has_resource("/v33"));
326
327        assert!(!root.has_resource("/user/22"));
328        assert!(root.has_resource("/user/22/"));
329        assert!(root.has_resource("/user/22/profile"));
330
331        // extract patterns from paths
332
333        assert!(root.match_pattern("/bar").is_none());
334        assert!(root.match_pattern("/v44").is_none());
335
336        assert_eq!(root.match_pattern("/info"), Some("/info".to_owned()));
337        assert_eq!(
338            root.match_pattern("/v1"),
339            Some("/v{version:[[:digit:]]{1}}".to_owned())
340        );
341        assert_eq!(
342            root.match_pattern("/v2"),
343            Some("/v{version:[[:digit:]]{1}}".to_owned())
344        );
345        assert_eq!(
346            root.match_pattern("/user/22/profile"),
347            Some("/user/{id}/profile".to_owned())
348        );
349        assert_eq!(
350            root.match_pattern("/user/602CFB82-7709-4B17-ADCF-4C347B6F2203/profile"),
351            Some("/user/{id}/profile".to_owned())
352        );
353        assert_eq!(
354            root.match_pattern("/user/22/article/44"),
355            Some("/user/{id}/article/{id}".to_owned())
356        );
357        assert_eq!(
358            root.match_pattern("/user/22/post/my-post"),
359            Some("/user/{id}/post/{post_id}".to_owned())
360        );
361        assert_eq!(
362            root.match_pattern("/user/22/post/other-post/comment/42"),
363            Some("/user/{id}/post/{post_id}/comment/{comment_id}".to_owned())
364        );
365    }
366
367    #[test]
368    fn extract_matched_name() {
369        let mut root = ResourceMap::new(ResourceDef::root_prefix(""));
370
371        let mut rdef = ResourceDef::new("/info");
372        rdef.set_name("root_info");
373        root.add(&mut rdef, None);
374
375        let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}"));
376        let mut rdef = ResourceDef::new("/");
377        user_map.add(&mut rdef, None);
378
379        let mut rdef = ResourceDef::new("/post/{post_id}");
380        rdef.set_name("user_post");
381        user_map.add(&mut rdef, None);
382
383        root.add(
384            &mut ResourceDef::root_prefix("/user/{id}"),
385            Some(Rc::new(user_map)),
386        );
387
388        let root = Rc::new(root);
389        ResourceMap::finish(&root);
390
391        // sanity check resource map setup
392
393        assert!(root.has_resource("/info"));
394        assert!(!root.has_resource("/bar"));
395
396        assert!(!root.has_resource("/user/22"));
397        assert!(root.has_resource("/user/22/"));
398        assert!(root.has_resource("/user/22/post/55"));
399
400        // extract patterns from paths
401
402        assert!(root.match_name("/bar").is_none());
403        assert!(root.match_name("/v44").is_none());
404
405        assert_eq!(root.match_name("/info"), Some("root_info"));
406        assert_eq!(root.match_name("/user/22"), None);
407        assert_eq!(root.match_name("/user/22/"), None);
408        assert_eq!(root.match_name("/user/22/post/55"), Some("user_post"));
409    }
410
411    #[test]
412    fn bug_fix_issue_1582_debug_print_exits() {
413        // ref: https://github.com/actix/actix-web/issues/1582
414        let mut root = ResourceMap::new(ResourceDef::root_prefix(""));
415
416        let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}"));
417        user_map.add(&mut ResourceDef::new("/"), None);
418        user_map.add(&mut ResourceDef::new("/profile"), None);
419        user_map.add(&mut ResourceDef::new("/article/{id}"), None);
420        user_map.add(&mut ResourceDef::new("/post/{post_id}"), None);
421        user_map.add(
422            &mut ResourceDef::new("/post/{post_id}/comment/{comment_id}"),
423            None,
424        );
425
426        root.add(
427            &mut ResourceDef::root_prefix("/user/{id}"),
428            Some(Rc::new(user_map)),
429        );
430
431        let root = Rc::new(root);
432        ResourceMap::finish(&root);
433
434        // check root has no parent
435        assert!(root.parent.borrow().upgrade().is_none());
436        // check child has parent reference
437        assert!(root.nodes.as_ref().unwrap()[0]
438            .parent
439            .borrow()
440            .upgrade()
441            .is_some());
442        // check child's parent root id matches root's root id
443        assert!(Rc::ptr_eq(
444            &root.nodes.as_ref().unwrap()[0]
445                .parent
446                .borrow()
447                .upgrade()
448                .unwrap(),
449            &root
450        ));
451
452        let output = format!("{:?}", root);
453        assert!(output.starts_with("ResourceMap {"));
454        assert!(output.ends_with(" }"));
455    }
456
457    #[test]
458    fn short_circuit() {
459        let mut root = ResourceMap::new(ResourceDef::prefix(""));
460
461        let mut user_root = ResourceDef::prefix("/user");
462        let mut user_map = ResourceMap::new(user_root.clone());
463        user_map.add(&mut ResourceDef::new("/u1"), None);
464        user_map.add(&mut ResourceDef::new("/u2"), None);
465
466        root.add(&mut ResourceDef::new("/user/u3"), None);
467        root.add(&mut user_root, Some(Rc::new(user_map)));
468        root.add(&mut ResourceDef::new("/user/u4"), None);
469
470        let rmap = Rc::new(root);
471        ResourceMap::finish(&rmap);
472
473        assert!(rmap.has_resource("/user/u1"));
474        assert!(rmap.has_resource("/user/u2"));
475        assert!(rmap.has_resource("/user/u3"));
476        assert!(!rmap.has_resource("/user/u4"));
477    }
478
479    #[test]
480    fn url_for() {
481        let mut root = ResourceMap::new(ResourceDef::prefix(""));
482
483        let mut user_scope_rdef = ResourceDef::prefix("/user");
484        let mut user_scope_map = ResourceMap::new(user_scope_rdef.clone());
485
486        let mut user_rdef = ResourceDef::new("/{user_id}");
487        let mut user_map = ResourceMap::new(user_rdef.clone());
488
489        let mut post_rdef = ResourceDef::new("/post/{sub_id}");
490        post_rdef.set_name("post");
491
492        user_map.add(&mut post_rdef, None);
493        user_scope_map.add(&mut user_rdef, Some(Rc::new(user_map)));
494        root.add(&mut user_scope_rdef, Some(Rc::new(user_scope_map)));
495
496        let rmap = Rc::new(root);
497        ResourceMap::finish(&rmap);
498
499        let mut req = crate::test::TestRequest::default();
500        req.set_server_hostname("localhost:8888");
501        let req = req.to_http_request();
502
503        let url = rmap
504            .url_for(&req, "post", ["u123", "foobar"])
505            .unwrap()
506            .to_string();
507        assert_eq!(url, "http://localhost:8888/user/u123/post/foobar");
508
509        assert!(rmap.url_for(&req, "missing", ["u123"]).is_err());
510    }
511
512    #[test]
513    fn url_for_parser() {
514        let mut root = ResourceMap::new(ResourceDef::prefix(""));
515
516        let mut rdef_1 = ResourceDef::new("/{var}");
517        rdef_1.set_name("internal");
518
519        let mut rdef_2 = ResourceDef::new("http://host.dom/{var}");
520        rdef_2.set_name("external.1");
521
522        let mut rdef_3 = ResourceDef::new("{var}");
523        rdef_3.set_name("external.2");
524
525        root.add(&mut rdef_1, None);
526        root.add(&mut rdef_2, None);
527        root.add(&mut rdef_3, None);
528        let rmap = Rc::new(root);
529        ResourceMap::finish(&rmap);
530
531        let mut req = crate::test::TestRequest::default();
532        req.set_server_hostname("localhost:8888");
533        let req = req.to_http_request();
534
535        const INPUT: &[&str] = &["a/../quick brown%20fox/%nan?query#frag"];
536        const OUTPUT: &str = "/quick%20brown%20fox/%nan%3Fquery%23frag";
537
538        let url = rmap.url_for(&req, "internal", INPUT).unwrap();
539        assert_eq!(url.path(), OUTPUT);
540
541        let url = rmap.url_for(&req, "external.1", INPUT).unwrap();
542        assert_eq!(url.path(), OUTPUT);
543
544        assert!(rmap.url_for(&req, "external.2", INPUT).is_err());
545        assert!(rmap.url_for(&req, "external.2", [""]).is_err());
546    }
547
548    #[test]
549    fn external_resource_with_no_name() {
550        let mut root = ResourceMap::new(ResourceDef::prefix(""));
551
552        let mut rdef = ResourceDef::new("https://duck.com/{query}");
553        root.add(&mut rdef, None);
554
555        let rmap = Rc::new(root);
556        ResourceMap::finish(&rmap);
557
558        assert!(!rmap.has_resource("https://duck.com/abc"));
559    }
560
561    #[test]
562    fn external_resource_with_name() {
563        let mut root = ResourceMap::new(ResourceDef::prefix(""));
564
565        let mut rdef = ResourceDef::new("https://duck.com/{query}");
566        rdef.set_name("duck");
567        root.add(&mut rdef, None);
568
569        let rmap = Rc::new(root);
570        ResourceMap::finish(&rmap);
571
572        assert!(!rmap.has_resource("https://duck.com/abc"));
573
574        let mut req = crate::test::TestRequest::default();
575        req.set_server_hostname("localhost:8888");
576        let req = req.to_http_request();
577
578        assert_eq!(
579            rmap.url_for(&req, "duck", ["abcd"]).unwrap().to_string(),
580            "https://duck.com/abcd"
581        );
582    }
583
584    #[test]
585    fn url_for_override_within_map() {
586        let mut root = ResourceMap::new(ResourceDef::prefix(""));
587
588        let mut foo_rdef = ResourceDef::prefix("/foo");
589        let mut foo_map = ResourceMap::new(foo_rdef.clone());
590        let mut nested_rdef = ResourceDef::new("/nested");
591        nested_rdef.set_name("nested");
592        foo_map.add(&mut nested_rdef, None);
593        root.add(&mut foo_rdef, Some(Rc::new(foo_map)));
594
595        let mut foo_rdef = ResourceDef::prefix("/bar");
596        let mut foo_map = ResourceMap::new(foo_rdef.clone());
597        let mut nested_rdef = ResourceDef::new("/nested");
598        nested_rdef.set_name("nested");
599        foo_map.add(&mut nested_rdef, None);
600        root.add(&mut foo_rdef, Some(Rc::new(foo_map)));
601
602        let rmap = Rc::new(root);
603        ResourceMap::finish(&rmap);
604
605        let req = crate::test::TestRequest::default().to_http_request();
606
607        let url = rmap.url_for(&req, "nested", [""; 0]).unwrap().to_string();
608        assert_eq!(url, "http://localhost:8080/bar/nested");
609
610        assert!(rmap.url_for(&req, "missing", ["u123"]).is_err());
611    }
612}