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