1use crate::error::Error;
2use crate::{PgConnectOptions, PgSslMode};
3use sqlx_core::percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
4use sqlx_core::Url;
5use std::net::IpAddr;
6use std::str::FromStr;
7
8impl PgConnectOptions {
9 pub(crate) fn parse_from_url(url: &Url) -> Result<Self, Error> {
10 #[allow(deprecated)]
11 let options = Self::new_without_pgpass();
12
13 Self::apply_url(options, url).map(|opts| opts.apply_pgpass())
14 }
15
16 pub(crate) fn parse_from_url_without_env(url: &Url) -> Result<Self, Error> {
17 Self::apply_url(Self::default_without_env(), url)
18 }
19
20 fn apply_url(mut options: Self, url: &Url) -> Result<Self, Error> {
21 if let Some(host) = url.host_str() {
22 let host_decoded = percent_decode_str(host);
23 options = match host_decoded.clone().next() {
24 Some(b'/') => options.socket(&*host_decoded.decode_utf8().map_err(Error::config)?),
25 _ => options.host(host),
26 }
27 }
28
29 if let Some(port) = url.port() {
30 options = options.port(port);
31 }
32
33 let username = url.username();
34 if !username.is_empty() {
35 options = options.username(
36 &percent_decode_str(username)
37 .decode_utf8()
38 .map_err(Error::config)?,
39 );
40 }
41
42 if let Some(password) = url.password() {
43 options = options.password(
44 &percent_decode_str(password)
45 .decode_utf8()
46 .map_err(Error::config)?,
47 );
48 }
49
50 let path = url.path().trim_start_matches('/');
51 if !path.is_empty() {
52 options = options.database(
53 &percent_decode_str(path)
54 .decode_utf8()
55 .map_err(Error::config)?,
56 );
57 }
58
59 for (key, value) in url.query_pairs().into_iter() {
60 match &*key {
61 "sslmode" | "ssl-mode" => {
62 options = options.ssl_mode(value.parse().map_err(Error::config)?);
63 }
64
65 "sslrootcert" | "ssl-root-cert" | "ssl-ca" => {
66 options = options.ssl_root_cert(&*value);
67 }
68
69 "sslcert" | "ssl-cert" => options = options.ssl_client_cert(&*value),
70
71 "sslkey" | "ssl-key" => options = options.ssl_client_key(&*value),
72
73 "statement-cache-capacity" => {
74 options =
75 options.statement_cache_capacity(value.parse().map_err(Error::config)?);
76 }
77
78 "host" => {
79 if value.starts_with('/') {
80 options = options.socket(&*value);
81 } else {
82 options = options.host(&value);
83 }
84 }
85
86 "hostaddr" => {
87 value.parse::<IpAddr>().map_err(Error::config)?;
88 options = options.host_addr(&value)
89 }
90
91 "port" => options = options.port(value.parse().map_err(Error::config)?),
92
93 "dbname" => options = options.database(&value),
94
95 "user" => options = options.username(&value),
96
97 "password" => options = options.password(&value),
98
99 "application_name" => options = options.application_name(&value),
100
101 "options" => {
102 if let Some(options) = options.options.as_mut() {
103 options.push(' ');
104 options.push_str(&value);
105 } else {
106 options.options = Some(value.to_string());
107 }
108 }
109
110 k if k.starts_with("options[") => {
111 if let Some(key) = k.strip_prefix("options[").unwrap().strip_suffix(']') {
112 options = options.options([(key, &*value)]);
113 }
114 }
115
116 _ => tracing::warn!(%key, %value, "ignoring unrecognized connect parameter"),
117 }
118 }
119
120 Ok(options)
121 }
122
123 pub fn from_url_without_env(url: &str) -> Result<Self, Error> {
139 let url: Url = url.parse().map_err(Error::config)?;
140 Self::parse_from_url_without_env(&url)
141 }
142
143 pub(crate) fn build_url(&self) -> Url {
144 let host = match &self.socket {
145 Some(socket) => {
146 utf8_percent_encode(&socket.to_string_lossy(), NON_ALPHANUMERIC).to_string()
147 }
148 None => self.host.to_owned(),
149 };
150
151 let mut url = Url::parse(&format!(
152 "postgres://{}@{}:{}",
153 self.username, host, self.port
154 ))
155 .expect("BUG: generated un-parseable URL");
156
157 if let Some(password) = &self.password {
158 let password = utf8_percent_encode(password, NON_ALPHANUMERIC).to_string();
159 let _ = url.set_password(Some(&password));
160 }
161
162 if let Some(database) = &self.database {
163 url.set_path(database);
164 }
165
166 let ssl_mode = match self.ssl_mode {
167 PgSslMode::Allow => "allow",
168 PgSslMode::Disable => "disable",
169 PgSslMode::Prefer => "prefer",
170 PgSslMode::Require => "require",
171 PgSslMode::VerifyCa => "verify-ca",
172 PgSslMode::VerifyFull => "verify-full",
173 };
174 url.query_pairs_mut().append_pair("sslmode", ssl_mode);
175
176 if let Some(ssl_root_cert) = &self.ssl_root_cert {
177 url.query_pairs_mut()
178 .append_pair("sslrootcert", &ssl_root_cert.to_string());
179 }
180
181 if let Some(ssl_client_cert) = &self.ssl_client_cert {
182 url.query_pairs_mut()
183 .append_pair("sslcert", &ssl_client_cert.to_string());
184 }
185
186 if let Some(ssl_client_key) = &self.ssl_client_key {
187 url.query_pairs_mut()
188 .append_pair("sslkey", &ssl_client_key.to_string());
189 }
190
191 url.query_pairs_mut().append_pair(
192 "statement-cache-capacity",
193 &self.statement_cache_capacity.to_string(),
194 );
195
196 url
197 }
198}
199
200impl FromStr for PgConnectOptions {
201 type Err = Error;
202
203 fn from_str(s: &str) -> Result<Self, Error> {
204 let url: Url = s.parse().map_err(Error::config)?;
205
206 Self::parse_from_url(&url)
207 }
208}
209
210#[test]
211fn it_parses_socket_correctly_from_parameter() {
212 let url = "postgres:///?host=/var/run/postgres/";
213 let opts = PgConnectOptions::from_str(url).unwrap();
214
215 assert_eq!(Some("/var/run/postgres/".into()), opts.socket);
216}
217
218#[test]
219fn it_parses_host_correctly_from_parameter() {
220 let url = "postgres:///?host=google.database.com";
221 let opts = PgConnectOptions::from_str(url).unwrap();
222
223 assert_eq!(None, opts.socket);
224 assert_eq!("google.database.com", &opts.host);
225}
226
227#[test]
228fn it_parses_hostaddr_correctly_from_parameter() {
229 let url = "postgres:///?hostaddr=8.8.8.8";
230 let opts = PgConnectOptions::from_str(url).unwrap();
231
232 assert_eq!(None, opts.socket);
233 assert_eq!("localhost", &opts.host);
234 assert_eq!(Some("8.8.8.8"), opts.host_addr.as_deref());
235}
236
237#[test]
238fn it_parses_hostaddr_host_separately_from_parameter() {
239 let url = "postgres://example.com/?hostaddr=8.8.8.8";
240 let opts = PgConnectOptions::from_str(url).unwrap();
241
242 assert_eq!(None, opts.socket);
243 assert_eq!("example.com", &opts.host);
244 assert_eq!(Some("8.8.8.8"), opts.host_addr.as_deref());
245}
246
247#[test]
248fn it_parses_hostaddr_host_host_overwrite_from_query_from_parameter() {
249 let url = "postgres://example.com/?hostaddr=8.8.8.8&host=sqlx.rs";
250 let opts = PgConnectOptions::from_str(url).unwrap();
251
252 assert_eq!(None, opts.socket);
253 assert_eq!("sqlx.rs", &opts.host);
254 assert_eq!(Some("8.8.8.8"), opts.host_addr.as_deref());
255}
256
257#[test]
258fn it_parses_port_correctly_from_parameter() {
259 let url = "postgres:///?port=1234";
260 let opts = PgConnectOptions::from_str(url).unwrap();
261
262 assert_eq!(None, opts.socket);
263 assert_eq!(1234, opts.port);
264}
265
266#[test]
267fn it_parses_dbname_correctly_from_parameter() {
268 let url = "postgres:///?dbname=some_db";
269 let opts = PgConnectOptions::from_str(url).unwrap();
270
271 assert_eq!(None, opts.socket);
272 assert_eq!(Some("some_db"), opts.database.as_deref());
273}
274
275#[test]
276fn it_parses_user_correctly_from_parameter() {
277 let url = "postgres:///?user=some_user";
278 let opts = PgConnectOptions::from_str(url).unwrap();
279
280 assert_eq!(None, opts.socket);
281 assert_eq!("some_user", opts.username);
282}
283
284#[test]
285fn it_parses_password_correctly_from_parameter() {
286 let url = "postgres:///?password=some_pass";
287 let opts = PgConnectOptions::from_str(url).unwrap();
288
289 assert_eq!(None, opts.socket);
290 assert_eq!(Some("some_pass"), opts.password.as_deref());
291}
292
293#[test]
294fn it_parses_application_name_correctly_from_parameter() {
295 let url = "postgres:///?application_name=some_name";
296 let opts = PgConnectOptions::from_str(url).unwrap();
297
298 assert_eq!(Some("some_name"), opts.application_name.as_deref());
299}
300
301#[test]
302fn it_parses_username_with_at_sign_correctly() {
303 let url = "postgres://user@hostname:password@hostname:5432/database";
304 let opts = PgConnectOptions::from_str(url).unwrap();
305
306 assert_eq!("user@hostname", &opts.username);
307}
308
309#[test]
310fn it_parses_password_with_non_ascii_chars_correctly() {
311 let url = "postgres://username:p@ssw0rd@hostname:5432/database";
312 let opts = PgConnectOptions::from_str(url).unwrap();
313
314 assert_eq!(Some("p@ssw0rd".into()), opts.password);
315}
316
317#[test]
318fn it_parses_socket_correctly_percent_encoded() {
319 let url = "postgres://%2Fvar%2Flib%2Fpostgres/database";
320 let opts = PgConnectOptions::from_str(url).unwrap();
321
322 assert_eq!(Some("/var/lib/postgres/".into()), opts.socket);
323}
324#[test]
325fn it_parses_socket_correctly_with_username_percent_encoded() {
326 let url = "postgres://some_user@%2Fvar%2Flib%2Fpostgres/database";
327 let opts = PgConnectOptions::from_str(url).unwrap();
328
329 assert_eq!("some_user", opts.username);
330 assert_eq!(Some("/var/lib/postgres/".into()), opts.socket);
331 assert_eq!(Some("database"), opts.database.as_deref());
332}
333#[test]
334fn it_parses_libpq_options_correctly() {
335 let url = "postgres:///?options=-c%20synchronous_commit%3Doff%20--search_path%3Dpostgres";
336 let opts = PgConnectOptions::from_str(url).unwrap();
337
338 assert_eq!(
339 Some("-c synchronous_commit=off --search_path=postgres".into()),
340 opts.options
341 );
342}
343#[test]
344fn it_parses_sqlx_options_correctly() {
345 let url = "postgres:///?options[synchronous_commit]=off&options[search_path]=postgres";
346 let opts = PgConnectOptions::from_str(url).unwrap();
347
348 assert_eq!(
349 Some("-c synchronous_commit=off -c search_path=postgres".into()),
350 opts.options
351 );
352}
353
354#[test]
355fn it_returns_the_parsed_url_when_socket() {
356 let url = "postgres://username@%2Fvar%2Flib%2Fpostgres/database";
357 let opts = PgConnectOptions::from_str(url).unwrap();
358
359 let mut expected_url = Url::parse(url).unwrap();
360 let query_string = "sslmode=prefer&statement-cache-capacity=100";
362 let port = 5432;
363 expected_url.set_query(Some(query_string));
364 let _ = expected_url.set_port(Some(port));
365
366 assert_eq!(expected_url, opts.build_url());
367}
368
369#[test]
370fn it_returns_the_parsed_url_when_host() {
371 let url = "postgres://username:p@ssw0rd@hostname:5432/database";
372 let opts = PgConnectOptions::from_str(url).unwrap();
373
374 let mut expected_url = Url::parse(url).unwrap();
375 let query_string = "sslmode=prefer&statement-cache-capacity=100";
377 expected_url.set_query(Some(query_string));
378
379 assert_eq!(expected_url, opts.build_url());
380}
381
382#[test]
383fn built_url_can_be_parsed() {
384 let url = "postgres://username:p@ssw0rd@hostname:5432/database";
385 let opts = PgConnectOptions::from_str(url).unwrap();
386
387 let parsed = PgConnectOptions::from_str(opts.build_url().as_ref());
388
389 assert!(parsed.is_ok());
390}
391
392#[test]
393fn test_from_url_without_env() {
394 let url = "postgres://testuser:testpass@testhost:5433/testdb";
396 let opts = PgConnectOptions::from_url_without_env(url).unwrap();
397
398 assert_eq!(opts.get_host(), "testhost");
399 assert_eq!(opts.get_port(), 5433);
400 assert_eq!(opts.get_username(), "testuser");
401 assert_eq!(opts.get_database(), Some("testdb"));
402
403 let url = "postgres://";
405 let opts = PgConnectOptions::from_url_without_env(url).unwrap();
406
407 assert_eq!(opts.get_port(), 5432);
409 assert_eq!(opts.get_username(), "postgres"); assert_eq!(opts.get_ssl_mode(), PgSslMode::Prefer);
411
412 let url = "postgres://user@host/db?sslmode=require&application_name=myapp";
414 let opts = PgConnectOptions::from_url_without_env(url).unwrap();
415
416 assert_eq!(opts.get_username(), "user");
417 assert_eq!(opts.get_host(), "host");
418 assert_eq!(opts.get_database(), Some("db"));
419 assert_eq!(opts.get_ssl_mode(), PgSslMode::Require);
420 assert_eq!(opts.get_application_name(), Some("myapp"));
421}