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 #[inline]
30 pub fn uri(&self) -> &http::Uri {
31 &self.uri
32 }
33
34 #[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 assert_eq!(path.get("id").unwrap(), &encoded);
106
107 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 assert!(String::from_utf8(path.as_str().as_bytes().to_owned()).is_ok());
142 }
143}