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    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    /// Returns `None` if root pattern doesn't match;
278    /// `Some(None)` if root pattern matches but there is no matching child pattern.
279    /// Don't search sideways when `Some(none)` is returned.
280    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            // find first sub-node to match remaining path
286            Some(nodes) => nodes
287                .iter()
288                .filter_map(|node| node._find_matching_node(path))
289                .next()
290                .flatten(),
291
292            // only terminate at edge nodes
293            None => Some(self),
294        })
295    }
296
297    /// Find `self`'s highest ancestor and then run `F`, providing `B`, in that rmap context.
298    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    /// Run `F`, providing `B`, if `self` is top-level resource map, else recurse to parent map.
306    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        // sanity check resource map setup
349
350        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        // extract patterns from paths
362
363        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        // sanity check resource map setup
422
423        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        // extract patterns from paths
431
432        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        // ref: https://github.com/actix/actix-web/issues/1582
444        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        // check root has no parent
465        assert!(root.parent.borrow().upgrade().is_none());
466        // check child has parent reference
467        assert!(root.nodes.as_ref().unwrap()[0]
468            .parent
469            .borrow()
470            .upgrade()
471            .is_some());
472        // check child's parent root id matches root's root id
473        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}