1use std::borrow::Cow;
2use std::collections::HashMap;
3
4use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
5
6use crate::error::Error;
7
8#[derive(Clone, Debug, Default, PartialEq, Eq)]
9pub struct Options<'a> {
11 pub scheme: Cow<'a, str>,
13 pub user: Cow<'a, str>,
15 pub password: Cow<'a, str>,
17 pub host: Cow<'a, str>,
19 pub path: Cow<'a, str>,
21 pub query: HashMap<String, String>,
23 pub fragment: Cow<'a, str>,
25}
26
27impl Options<'_> {
28 pub fn parse_uri(uri: &str) -> Result<Options<'_>, Error> {
30 let mut fragment_and_remain = uri.splitn(2, '#');
31 let uri = fragment_and_remain.next().unwrap_or_default();
32 let fragment = percent_decode(fragment_and_remain.next().unwrap_or_default());
33 let mut scheme_and_remain = uri.splitn(2, ':');
34 let scheme = scheme_and_remain.next().unwrap_or_default();
35
36 let (scheme, host_and_query) = if let Some(remain) = scheme_and_remain.next() {
37 if scheme.is_empty() {
38 ("", uri)
39 } else {
40 (scheme, remain.trim_start_matches("//"))
41 }
42 } else {
43 ("", uri)
44 };
45 let scheme = percent_decode(scheme);
46
47 let mut host_and_query = host_and_query.splitn(2, '?');
48 let (user, password, host) = {
49 let mut user_and_host = host_and_query.next().unwrap_or_default().splitn(2, '@');
50 let user_pass = user_and_host.next().unwrap_or_default();
51 if let Some(host) = user_and_host.next() {
52 let mut user_pass = user_pass.splitn(2, ':');
53 let user = percent_decode(user_pass.next().unwrap_or_default());
54 let pass = percent_decode(user_pass.next().unwrap_or_default());
55 (user, pass, host)
56 } else {
57 (Cow::Borrowed(""), Cow::Borrowed(""), user_pass)
58 }
59 };
60 let (host, path) = if let Some(path_pos) = host.find('/') {
61 (
62 percent_decode(&host[..path_pos]),
63 percent_decode(&host[path_pos..]),
64 )
65 } else {
66 (percent_decode(host), Cow::Borrowed(""))
67 };
68
69 let query = if let Some(query) = host_and_query.next() {
70 url::form_urlencoded::parse(query.as_bytes())
71 .into_owned()
72 .fold(HashMap::new(), |mut map, (k, v)| {
73 map.insert(k, v);
74 map
75 })
76 } else {
77 HashMap::new()
78 };
79
80 Ok(Options {
81 user,
82 password,
83 host,
84 path,
85 scheme,
86 query,
87 fragment,
88 })
89 }
90
91 pub fn into_uri(self) -> String {
93 let mut uri = String::new();
94 if !self.scheme.is_empty() {
95 percent_encode_into(&mut uri, &self.scheme);
96 uri.push_str("://");
97 }
98 if !self.user.is_empty() || !self.password.is_empty() {
99 percent_encode_into(&mut uri, &self.user);
100 uri.push(':');
101 percent_encode_into(&mut uri, &self.password);
102 uri.push('@');
103 }
104 uri.push_str(&self.host);
105 uri.push_str(&self.path);
106 if !self.query.is_empty() {
107 uri.push('?');
108 for (k, v) in self.query {
109 push_iter_str(&mut uri, url::form_urlencoded::byte_serialize(k.as_bytes()));
110 uri.push('=');
111 push_iter_str(&mut uri, url::form_urlencoded::byte_serialize(v.as_bytes()));
112 }
113 }
114 if !self.fragment.is_empty() {
115 uri.push('#');
116 percent_encode_into(&mut uri, &self.fragment);
117 }
118 uri
119 }
120}
121
122#[inline]
123fn push_iter_str<'a, I: Iterator<Item = &'a str>>(s: &mut String, iter: I) {
124 for item in iter {
125 s.push_str(item);
126 }
127}
128
129#[inline]
130fn percent_decode(s: &str) -> Cow<'_, str> {
131 percent_decode_str(s).decode_utf8_lossy()
132}
133
134#[inline]
135fn percent_encode_into(result: &mut String, s: &str) {
136 push_iter_str(result, utf8_percent_encode(s, NON_ALPHANUMERIC))
137}
138
139pub trait IntoOptions<'a> {
141 fn into_options(self) -> Result<Options<'a>, Error>;
143}
144
145impl<'a> IntoOptions<'a> for Options<'a> {
146 fn into_options(self) -> Result<Options<'a>, Error> {
147 Ok(self)
148 }
149}
150
151impl<'a> IntoOptions<'a> for &'a str {
152 fn into_options(self) -> Result<Options<'a>, Error> {
153 Options::parse_uri(self)
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn options_basic() {
163 let opts = Options::parse_uri("scheme://user%2E:pass@host/dbname?a+1=b#frag").unwrap();
164 let bs = Cow::Borrowed;
165 assert_eq!(
166 opts,
167 Options {
168 user: bs("user."),
169 password: bs("pass"),
170 scheme: bs("scheme"),
171 host: bs("host"),
172 path: bs("/dbname"),
173 query: HashMap::from_iter(vec![("a 1".to_owned(), "b".to_owned())]),
174 fragment: bs("frag"),
175 }
176 );
177 }
178
179 #[test]
180 fn options_no_schema() {
181 let opts = Options::parse_uri("dbname/path?a#frag").unwrap();
182 assert_eq!(
183 opts,
184 Options {
185 user: Default::default(),
186 password: Default::default(),
187 scheme: Default::default(),
188 host: Cow::Borrowed("dbname"),
189 path: Cow::Borrowed("/path"),
190 query: HashMap::from_iter(vec![("a".to_owned(), "".to_owned())]),
191 fragment: Cow::Borrowed("frag")
192 }
193 );
194 }
195
196 #[test]
197 fn options_round_trip() {
198 let opts_str = "schema://user%2F:pass@dbname?a+1=b#frag%2E";
199 let opts = Options::parse_uri(opts_str).unwrap();
200 assert_eq!(opts.into_uri(), opts_str);
201 }
202}