actix_web/
rmap.rs

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 resources within the tree or, for external resources, it points to isolated nodes
21    /// outside the tree.
22    named: FoldHashMap<String, Rc<ResourceMap>>,
23
24    parent: RefCell<Weak<ResourceMap>>,
25
26    /// Must be `None` for "edge" nodes.
27    nodes: Option<Vec<Rc<ResourceMap>>>,
28}
29
30impl ResourceMap {
31    /// Creates a _container_ node in the `ResourceMap` tree.
32    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    /// Format resource map as tree structure (unfinished).
42    #[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    /// Adds a (possibly nested) resource.
71    ///
72    /// To add a non-prefix pattern, `nested` must be `None`.
73    /// To add external resource, supply a pattern without a leading `/`.
74    /// The root pattern of `nested`, if present, should match `pattern`.
75    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            // parents absorb references to the named resources of children
84            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            // don't add external resources to the tree
104            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    /// Generate URL for named resource.
118    ///
119    /// Check [`HttpRequest::url_for`] for detailed information.
120    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            // build full URL from connection info parts and resource path
145            let conn = req.connection_info();
146            let base = format!("{}://{}", conn.scheme(), conn.host());
147            (Cow::Owned(base), path.as_str())
148        } else {
149            // external resource; third slash would be the root slash in the path
150            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    /// Returns true if there is a resource that would match `path`.
168    pub fn has_resource(&self, path: &str) -> bool {
169        self.find_matching_node(path).is_some()
170    }
171
172    /// Returns the name of the route that matches the given path or None if no full match
173    /// is possible or the matching resource is not named.
174    pub fn match_name(&self, path: &str) -> Option<&str> {
175        self.find_matching_node(path)?.pattern.name()
176    }
177
178    /// Returns the full resource pattern matched against a path or None if no full match
179    /// is possible.
180    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    /// Returns `None` if root pattern doesn't match;
196    /// `Some(None)` if root pattern matches but there is no matching child pattern.
197    /// Don't search sideways when `Some(none)` is returned.
198    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            // find first sub-node to match remaining path
204            Some(nodes) => nodes
205                .iter()
206                .filter_map(|node| node._find_matching_node(path))
207                .next()
208                .flatten(),
209
210            // only terminate at edge nodes
211            None => Some(self),
212        })
213    }
214
215    /// Find `self`'s highest ancestor and then run `F`, providing `B`, in that rmap context.
216    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    /// Run `F`, providing `B`, if `self` is top-level resource map, else recurse to parent map.
224    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        // sanity check resource map setup
267
268        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        // extract patterns from paths
280
281        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        // sanity check resource map setup
340
341        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        // extract patterns from paths
349
350        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        // ref: https://github.com/actix/actix-web/issues/1582
362        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        // check root has no parent
383        assert!(root.parent.borrow().upgrade().is_none());
384        // check child has parent reference
385        assert!(root.nodes.as_ref().unwrap()[0]
386            .parent
387            .borrow()
388            .upgrade()
389            .is_some());
390        // check child's parent root id matches root's root id
391        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}