askar_storage/
options.rs

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)]
9/// Parsed representation of database connection URI
10pub struct Options<'a> {
11    /// The URI schema
12    pub scheme: Cow<'a, str>,
13    /// The authenticating user name
14    pub user: Cow<'a, str>,
15    /// The authenticating user password
16    pub password: Cow<'a, str>,
17    /// The host name
18    pub host: Cow<'a, str>,
19    /// The path component
20    pub path: Cow<'a, str>,
21    /// The query component
22    pub query: HashMap<String, String>,
23    /// The fragment component
24    pub fragment: Cow<'a, str>,
25}
26
27impl Options<'_> {
28    /// Parse a URI string into an Options structure
29    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    /// Convert an options structure back into a string
92    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
139/// A trait implemented by types that can be converted into Options
140pub trait IntoOptions<'a> {
141    /// Try to convert self into an Options structure
142    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}