sqlx_mysql/options/
parse.rs

1use std::str::FromStr;
2
3use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
4use sqlx_core::Url;
5
6use crate::{error::Error, MySqlSslMode};
7
8use super::MySqlConnectOptions;
9
10impl MySqlConnectOptions {
11    pub(crate) fn parse_from_url(url: &Url) -> Result<Self, Error> {
12        let mut options = Self::new();
13
14        if let Some(host) = url.host_str() {
15            options = options.host(host);
16        }
17
18        if let Some(port) = url.port() {
19            options = options.port(port);
20        }
21
22        let username = url.username();
23        if !username.is_empty() {
24            options = options.username(
25                &percent_decode_str(username)
26                    .decode_utf8()
27                    .map_err(Error::config)?,
28            );
29        }
30
31        if let Some(password) = url.password() {
32            options = options.password(
33                &percent_decode_str(password)
34                    .decode_utf8()
35                    .map_err(Error::config)?,
36            );
37        }
38
39        let path = url.path().trim_start_matches('/');
40        if !path.is_empty() {
41            options = options.database(
42                &percent_decode_str(path)
43                    .decode_utf8()
44                    .map_err(Error::config)?,
45            );
46        }
47
48        for (key, value) in url.query_pairs().into_iter() {
49            match &*key {
50                "sslmode" | "ssl-mode" => {
51                    options = options.ssl_mode(value.parse().map_err(Error::config)?);
52                }
53
54                "sslca" | "ssl-ca" => {
55                    options = options.ssl_ca(&*value);
56                }
57
58                "charset" => {
59                    options = options.charset(&value);
60                }
61
62                "collation" => {
63                    options = options.collation(&value);
64                }
65
66                "sslcert" | "ssl-cert" => options = options.ssl_client_cert(&*value),
67
68                "sslkey" | "ssl-key" => options = options.ssl_client_key(&*value),
69
70                "statement-cache-capacity" => {
71                    options =
72                        options.statement_cache_capacity(value.parse().map_err(Error::config)?);
73                }
74
75                "socket" => {
76                    options = options.socket(&*value);
77                }
78
79                "timezone" | "time-zone" => {
80                    options = options.timezone(Some(value.to_string()));
81                }
82
83                _ => {}
84            }
85        }
86
87        Ok(options)
88    }
89
90    pub(crate) fn build_url(&self) -> Url {
91        let mut url = Url::parse(&format!(
92            "mysql://{}@{}:{}",
93            self.username, self.host, self.port
94        ))
95        .expect("BUG: generated un-parseable URL");
96
97        if let Some(password) = &self.password {
98            let password = utf8_percent_encode(password, NON_ALPHANUMERIC).to_string();
99            let _ = url.set_password(Some(&password));
100        }
101
102        if let Some(database) = &self.database {
103            url.set_path(database);
104        }
105
106        let ssl_mode = match self.ssl_mode {
107            MySqlSslMode::Disabled => "DISABLED",
108            MySqlSslMode::Preferred => "PREFERRED",
109            MySqlSslMode::Required => "REQUIRED",
110            MySqlSslMode::VerifyCa => "VERIFY_CA",
111            MySqlSslMode::VerifyIdentity => "VERIFY_IDENTITY",
112        };
113        url.query_pairs_mut().append_pair("ssl-mode", ssl_mode);
114
115        if let Some(ssl_ca) = &self.ssl_ca {
116            url.query_pairs_mut()
117                .append_pair("ssl-ca", &ssl_ca.to_string());
118        }
119
120        url.query_pairs_mut().append_pair("charset", &self.charset);
121
122        if let Some(collation) = &self.collation {
123            url.query_pairs_mut().append_pair("charset", collation);
124        }
125
126        if let Some(ssl_client_cert) = &self.ssl_client_cert {
127            url.query_pairs_mut()
128                .append_pair("ssl-cert", &ssl_client_cert.to_string());
129        }
130
131        if let Some(ssl_client_key) = &self.ssl_client_key {
132            url.query_pairs_mut()
133                .append_pair("ssl-key", &ssl_client_key.to_string());
134        }
135
136        url.query_pairs_mut().append_pair(
137            "statement-cache-capacity",
138            &self.statement_cache_capacity.to_string(),
139        );
140
141        if let Some(socket) = &self.socket {
142            url.query_pairs_mut()
143                .append_pair("socket", &socket.to_string_lossy());
144        }
145
146        url
147    }
148}
149
150impl FromStr for MySqlConnectOptions {
151    type Err = Error;
152
153    fn from_str(s: &str) -> Result<Self, Error> {
154        let url: Url = s.parse().map_err(Error::config)?;
155        Self::parse_from_url(&url)
156    }
157}
158
159#[test]
160fn it_parses_username_with_at_sign_correctly() {
161    let url = "mysql://user@hostname:password@hostname:5432/database";
162    let opts = MySqlConnectOptions::from_str(url).unwrap();
163
164    assert_eq!("user@hostname", &opts.username);
165}
166
167#[test]
168fn it_parses_password_with_non_ascii_chars_correctly() {
169    let url = "mysql://username:p@ssw0rd@hostname:5432/database";
170    let opts = MySqlConnectOptions::from_str(url).unwrap();
171
172    assert_eq!(Some("p@ssw0rd".into()), opts.password);
173}
174
175#[test]
176fn it_returns_the_parsed_url() {
177    let url = "mysql://username:p@ssw0rd@hostname:3306/database";
178    let opts = MySqlConnectOptions::from_str(url).unwrap();
179
180    let mut expected_url = Url::parse(url).unwrap();
181    // MySqlConnectOptions defaults
182    let query_string = "ssl-mode=PREFERRED&charset=utf8mb4&statement-cache-capacity=100";
183    expected_url.set_query(Some(query_string));
184
185    assert_eq!(expected_url, opts.build_url());
186}
187
188#[test]
189fn it_parses_timezone() {
190    let opts: MySqlConnectOptions = "mysql://user:password@hostname/database?timezone=%2B08:00"
191        .parse()
192        .unwrap();
193    assert_eq!(opts.timezone.as_deref(), Some("+08:00"));
194
195    let opts: MySqlConnectOptions = "mysql://user:password@hostname/database?time-zone=%2B08:00"
196        .parse()
197        .unwrap();
198    assert_eq!(opts.timezone.as_deref(), Some("+08:00"));
199}