actix_router/
url.rs

1use crate::{Quoter, ResourcePath};
2
3thread_local! {
4    static DEFAULT_QUOTER: Quoter = Quoter::new(b"", b"%/+");
5}
6
7#[derive(Debug, Clone, Default)]
8pub struct Url {
9    uri: http::Uri,
10    path: Option<String>,
11}
12
13impl Url {
14    #[inline]
15    pub fn new(uri: http::Uri) -> Url {
16        let path = DEFAULT_QUOTER.with(|q| q.requote_str_lossy(uri.path()));
17        Url { uri, path }
18    }
19
20    #[inline]
21    pub fn new_with_quoter(uri: http::Uri, quoter: &Quoter) -> Url {
22        Url {
23            path: quoter.requote_str_lossy(uri.path()),
24            uri,
25        }
26    }
27
28    /// Returns URI.
29    #[inline]
30    pub fn uri(&self) -> &http::Uri {
31        &self.uri
32    }
33
34    /// Returns path.
35    #[inline]
36    pub fn path(&self) -> &str {
37        match self.path {
38            Some(ref path) => path,
39            _ => self.uri.path(),
40        }
41    }
42
43    #[inline]
44    pub fn update(&mut self, uri: &http::Uri) {
45        self.uri = uri.clone();
46        self.path = DEFAULT_QUOTER.with(|q| q.requote_str_lossy(uri.path()));
47    }
48
49    #[inline]
50    pub fn update_with_quoter(&mut self, uri: &http::Uri, quoter: &Quoter) {
51        self.uri = uri.clone();
52        self.path = quoter.requote_str_lossy(uri.path());
53    }
54}
55
56impl ResourcePath for Url {
57    #[inline]
58    fn path(&self) -> &str {
59        self.path()
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use std::fmt::Write as _;
66
67    use http::Uri;
68
69    use super::*;
70    use crate::{Path, ResourceDef};
71
72    const PROTECTED: &[u8] = b"%/+";
73
74    fn match_url(pattern: &'static str, url: impl AsRef<str>) -> Path<Url> {
75        let re = ResourceDef::new(pattern);
76        let uri = Uri::try_from(url.as_ref()).unwrap();
77        let mut path = Path::new(Url::new(uri));
78        assert!(re.capture_match_info(&mut path));
79        path
80    }
81
82    fn percent_encode(data: &[u8]) -> String {
83        data.iter()
84            .fold(String::with_capacity(data.len() * 3), |mut buf, c| {
85                write!(&mut buf, "%{:02X}", c).unwrap();
86                buf
87            })
88    }
89
90    #[test]
91    fn parse_url() {
92        let re = "/user/{id}/test";
93
94        let path = match_url(re, "/user/2345/test");
95        assert_eq!(path.get("id").unwrap(), "2345");
96    }
97
98    #[test]
99    fn protected_chars() {
100        let re = "/user/{id}/test";
101
102        let encoded = percent_encode(PROTECTED);
103        let path = match_url(re, format!("/user/{}/test", encoded));
104        // characters in captured segment remain unencoded
105        assert_eq!(path.get("id").unwrap(), &encoded);
106
107        // "%25" should never be decoded into '%' to guarantee the output is a valid
108        // percent-encoded format
109        let path = match_url(re, "/user/qwe%25/test");
110        assert_eq!(path.get("id").unwrap(), "qwe%25");
111
112        let path = match_url(re, "/user/qwe%25rty/test");
113        assert_eq!(path.get("id").unwrap(), "qwe%25rty");
114    }
115
116    #[test]
117    fn non_protected_ascii() {
118        let non_protected_ascii = ('\u{0}'..='\u{7F}')
119            .filter(|&c| c.is_ascii() && !PROTECTED.contains(&(c as u8)))
120            .collect::<String>();
121        let encoded = percent_encode(non_protected_ascii.as_bytes());
122        let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded));
123        assert_eq!(path.get("id").unwrap(), &non_protected_ascii);
124    }
125
126    #[test]
127    fn valid_utf8_multi_byte() {
128        let test = ('\u{FF00}'..='\u{FFFF}').collect::<String>();
129        let encoded = percent_encode(test.as_bytes());
130        let path = match_url("/a/{id}/b", format!("/a/{}/b", &encoded));
131        assert_eq!(path.get("id").unwrap(), &test);
132    }
133
134    #[test]
135    fn invalid_utf8() {
136        let invalid_utf8 = percent_encode((0x80..=0xff).collect::<Vec<_>>().as_slice());
137        let uri = Uri::try_from(format!("/{}", invalid_utf8)).unwrap();
138        let path = Path::new(Url::new(uri));
139
140        // We should always get a valid utf8 string
141        assert!(String::from_utf8(path.as_str().as_bytes().to_owned()).is_ok());
142    }
143}