1 extern crate url;
2 
3 use self::url::percent_encoding::percent_decode;
4 use self::url::{Host, Url};
5 use std::ffi::{CStr, CString};
6 
7 use result::{ConnectionError, ConnectionResult};
8 
9 pub struct ConnectionOptions {
10     host: Option<CString>,
11     user: CString,
12     password: Option<CString>,
13     database: Option<CString>,
14     port: Option<u16>,
15 }
16 
17 impl ConnectionOptions {
parse(database_url: &str) -> ConnectionResult<Self>18     pub fn parse(database_url: &str) -> ConnectionResult<Self> {
19         let url = match Url::parse(database_url) {
20             Ok(url) => url,
21             Err(_) => return Err(connection_url_error()),
22         };
23 
24         if url.scheme() != "mysql" {
25             return Err(connection_url_error());
26         }
27 
28         if url.path_segments().map(|x| x.count()).unwrap_or(0) > 1 {
29             return Err(connection_url_error());
30         }
31 
32         let host = match url.host() {
33             Some(Host::Ipv6(host)) => Some(CString::new(host.to_string())?),
34             Some(host) => Some(CString::new(host.to_string())?),
35             None => None,
36         };
37         let user = decode_into_cstring(url.username())?;
38         let password = match url.password() {
39             Some(password) => Some(decode_into_cstring(password)?),
40             None => None,
41         };
42         let database = match url.path_segments().and_then(|mut iter| iter.nth(0)) {
43             Some("") | None => None,
44             Some(segment) => Some(CString::new(segment.as_bytes())?),
45         };
46 
47         Ok(ConnectionOptions {
48             host: host,
49             user: user,
50             password: password,
51             database: database,
52             port: url.port(),
53         })
54     }
55 
host(&self) -> Option<&CStr>56     pub fn host(&self) -> Option<&CStr> {
57         self.host.as_ref().map(|x| &**x)
58     }
59 
user(&self) -> &CStr60     pub fn user(&self) -> &CStr {
61         &self.user
62     }
63 
password(&self) -> Option<&CStr>64     pub fn password(&self) -> Option<&CStr> {
65         self.password.as_ref().map(|x| &**x)
66     }
67 
database(&self) -> Option<&CStr>68     pub fn database(&self) -> Option<&CStr> {
69         self.database.as_ref().map(|x| &**x)
70     }
71 
port(&self) -> Option<u16>72     pub fn port(&self) -> Option<u16> {
73         self.port
74     }
75 }
76 
decode_into_cstring(s: &str) -> ConnectionResult<CString>77 fn decode_into_cstring(s: &str) -> ConnectionResult<CString> {
78     let decoded = percent_decode(s.as_bytes())
79         .decode_utf8()
80         .map_err(|_| connection_url_error())?;
81     CString::new(decoded.as_bytes()).map_err(Into::into)
82 }
83 
connection_url_error() -> ConnectionError84 fn connection_url_error() -> ConnectionError {
85     let msg = "MySQL connection URLs must be in the form \
86                `mysql://[[user]:[password]@]host[:port][/database]`";
87     ConnectionError::InvalidConnectionUrl(msg.into())
88 }
89 
90 #[test]
urls_with_schemes_other_than_mysql_are_errors()91 fn urls_with_schemes_other_than_mysql_are_errors() {
92     assert!(ConnectionOptions::parse("postgres://localhost").is_err());
93     assert!(ConnectionOptions::parse("http://localhost").is_err());
94     assert!(ConnectionOptions::parse("file:///tmp/mysql.sock").is_err());
95     assert!(ConnectionOptions::parse("socket:///tmp/mysql.sock").is_err());
96     assert!(ConnectionOptions::parse("mysql://localhost").is_ok());
97 }
98 
99 #[test]
urls_must_have_zero_or_one_path_segments()100 fn urls_must_have_zero_or_one_path_segments() {
101     assert!(ConnectionOptions::parse("mysql://localhost/foo/bar").is_err());
102     assert!(ConnectionOptions::parse("mysql://localhost/foo").is_ok());
103 }
104 
105 #[test]
first_path_segment_is_treated_as_database()106 fn first_path_segment_is_treated_as_database() {
107     let foo_cstr = CString::new("foo").unwrap();
108     let bar_cstr = CString::new("bar").unwrap();
109     assert_eq!(
110         Some(&*foo_cstr),
111         ConnectionOptions::parse("mysql://localhost/foo")
112             .unwrap()
113             .database()
114     );
115     assert_eq!(
116         Some(&*bar_cstr),
117         ConnectionOptions::parse("mysql://localhost/bar")
118             .unwrap()
119             .database()
120     );
121     assert_eq!(
122         None,
123         ConnectionOptions::parse("mysql://localhost")
124             .unwrap()
125             .database()
126     );
127 }
128 
129 #[test]
userinfo_should_be_percent_decode()130 fn userinfo_should_be_percent_decode() {
131     use self::url::percent_encoding::{utf8_percent_encode, USERINFO_ENCODE_SET};
132 
133     let username = "x#gfuL?4Zuj{n73m}eeJt0";
134     let encoded_username = utf8_percent_encode(username, USERINFO_ENCODE_SET);
135 
136     let password = "x/gfuL?4Zuj{n73m}eeJt1";
137     let encoded_password = utf8_percent_encode(password, USERINFO_ENCODE_SET);
138 
139     let db_url = format!(
140         "mysql://{}:{}@localhost/bar",
141         encoded_username, encoded_password
142     );
143     let db_url = Url::parse(&db_url).unwrap();
144 
145     let conn_opts = ConnectionOptions::parse(db_url.as_str()).unwrap();
146     let username = CString::new(username.as_bytes()).unwrap();
147     let password = CString::new(password.as_bytes()).unwrap();
148     assert_eq!(username, conn_opts.user);
149     assert_eq!(password, conn_opts.password.unwrap());
150 }
151 
152 #[test]
ipv6_host_not_wrapped_in_brackets()153 fn ipv6_host_not_wrapped_in_brackets() {
154     let host1 = CString::new("::1").unwrap();
155     let host2 = CString::new("2001:db8:85a3::8a2e:370:7334").unwrap();
156 
157     assert_eq!(
158         Some(&*host1),
159         ConnectionOptions::parse("mysql://[::1]").unwrap().host()
160     );
161     assert_eq!(
162         Some(&*host2),
163         ConnectionOptions::parse("mysql://[2001:db8:85a3::8a2e:370:7334]")
164             .unwrap()
165             .host()
166     );
167 }
168