sqlx_build_trust_postgres/options/
parse.rs

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        let mut options = Self::new_without_pgpass();
11
12        if let Some(host) = url.host_str() {
13            let host_decoded = percent_decode_str(host);
14            options = match host_decoded.clone().next() {
15                Some(b'/') => options.socket(&*host_decoded.decode_utf8().map_err(Error::config)?),
16                _ => options.host(host),
17            }
18        }
19
20        if let Some(port) = url.port() {
21            options = options.port(port);
22        }
23
24        let username = url.username();
25        if !username.is_empty() {
26            options = options.username(
27                &*percent_decode_str(username)
28                    .decode_utf8()
29                    .map_err(Error::config)?,
30            );
31        }
32
33        if let Some(password) = url.password() {
34            options = options.password(
35                &*percent_decode_str(password)
36                    .decode_utf8()
37                    .map_err(Error::config)?,
38            );
39        }
40
41        let path = url.path().trim_start_matches('/');
42        if !path.is_empty() {
43            options = options.database(path);
44        }
45
46        for (key, value) in url.query_pairs().into_iter() {
47            match &*key {
48                "sslmode" | "ssl-mode" => {
49                    options = options.ssl_mode(value.parse().map_err(Error::config)?);
50                }
51
52                "sslrootcert" | "ssl-root-cert" | "ssl-ca" => {
53                    options = options.ssl_root_cert(&*value);
54                }
55
56                "sslcert" | "ssl-cert" => options = options.ssl_client_cert(&*value),
57
58                "sslkey" | "ssl-key" => options = options.ssl_client_key(&*value),
59
60                "statement-cache-capacity" => {
61                    options =
62                        options.statement_cache_capacity(value.parse().map_err(Error::config)?);
63                }
64
65                "host" => {
66                    if value.starts_with("/") {
67                        options = options.socket(&*value);
68                    } else {
69                        options = options.host(&*value);
70                    }
71                }
72
73                "hostaddr" => {
74                    value.parse::<IpAddr>().map_err(Error::config)?;
75                    options = options.host(&*value)
76                }
77
78                "port" => options = options.port(value.parse().map_err(Error::config)?),
79
80                "dbname" => options = options.database(&*value),
81
82                "user" => options = options.username(&*value),
83
84                "password" => options = options.password(&*value),
85
86                "application_name" => options = options.application_name(&*value),
87
88                "options" => {
89                    if let Some(options) = options.options.as_mut() {
90                        options.push(' ');
91                        options.push_str(&*value);
92                    } else {
93                        options.options = Some(value.to_string());
94                    }
95                }
96
97                k if k.starts_with("options[") => {
98                    if let Some(key) = k.strip_prefix("options[").unwrap().strip_suffix(']') {
99                        options = options.options([(key, &*value)]);
100                    }
101                }
102
103                _ => tracing::warn!(%key, %value, "ignoring unrecognized connect parameter"),
104            }
105        }
106
107        let options = options.apply_pgpass();
108
109        Ok(options)
110    }
111
112    pub(crate) fn build_url(&self) -> Url {
113        let host = match &self.socket {
114            Some(socket) => {
115                utf8_percent_encode(&*socket.to_string_lossy(), NON_ALPHANUMERIC).to_string()
116            }
117            None => self.host.to_owned(),
118        };
119
120        let mut url = Url::parse(&format!(
121            "postgres://{}@{}:{}",
122            self.username, host, self.port
123        ))
124        .expect("BUG: generated un-parseable URL");
125
126        if let Some(password) = &self.password {
127            let password = utf8_percent_encode(&password, NON_ALPHANUMERIC).to_string();
128            let _ = url.set_password(Some(&password));
129        }
130
131        if let Some(database) = &self.database {
132            url.set_path(&database);
133        }
134
135        let ssl_mode = match self.ssl_mode {
136            PgSslMode::Allow => "ALLOW",
137            PgSslMode::Disable => "DISABLED",
138            PgSslMode::Prefer => "PREFERRED",
139            PgSslMode::Require => "REQUIRED",
140            PgSslMode::VerifyCa => "VERIFY_CA",
141            PgSslMode::VerifyFull => "VERIFY_FULL",
142        };
143        url.query_pairs_mut().append_pair("ssl-mode", ssl_mode);
144
145        if let Some(ssl_root_cert) = &self.ssl_root_cert {
146            url.query_pairs_mut()
147                .append_pair("ssl-root-cert", &ssl_root_cert.to_string());
148        }
149
150        if let Some(ssl_client_cert) = &self.ssl_client_cert {
151            url.query_pairs_mut()
152                .append_pair("ssl-cert", &ssl_client_cert.to_string());
153        }
154
155        if let Some(ssl_client_key) = &self.ssl_client_key {
156            url.query_pairs_mut()
157                .append_pair("ssl-key", &ssl_client_key.to_string());
158        }
159
160        url.query_pairs_mut().append_pair(
161            "statement-cache-capacity",
162            &self.statement_cache_capacity.to_string(),
163        );
164
165        url
166    }
167}
168
169impl FromStr for PgConnectOptions {
170    type Err = Error;
171
172    fn from_str(s: &str) -> Result<Self, Error> {
173        let url: Url = s.parse().map_err(Error::config)?;
174
175        Self::parse_from_url(&url)
176    }
177}
178
179#[test]
180fn it_parses_socket_correctly_from_parameter() {
181    let url = "postgres:///?host=/var/run/postgres/";
182    let opts = PgConnectOptions::from_str(url).unwrap();
183
184    assert_eq!(Some("/var/run/postgres/".into()), opts.socket);
185}
186
187#[test]
188fn it_parses_host_correctly_from_parameter() {
189    let url = "postgres:///?host=google.database.com";
190    let opts = PgConnectOptions::from_str(url).unwrap();
191
192    assert_eq!(None, opts.socket);
193    assert_eq!("google.database.com", &opts.host);
194}
195
196#[test]
197fn it_parses_hostaddr_correctly_from_parameter() {
198    let url = "postgres:///?hostaddr=8.8.8.8";
199    let opts = PgConnectOptions::from_str(url).unwrap();
200
201    assert_eq!(None, opts.socket);
202    assert_eq!("8.8.8.8", &opts.host);
203}
204
205#[test]
206fn it_parses_port_correctly_from_parameter() {
207    let url = "postgres:///?port=1234";
208    let opts = PgConnectOptions::from_str(url).unwrap();
209
210    assert_eq!(None, opts.socket);
211    assert_eq!(1234, opts.port);
212}
213
214#[test]
215fn it_parses_dbname_correctly_from_parameter() {
216    let url = "postgres:///?dbname=some_db";
217    let opts = PgConnectOptions::from_str(url).unwrap();
218
219    assert_eq!(None, opts.socket);
220    assert_eq!(Some("some_db"), opts.database.as_deref());
221}
222
223#[test]
224fn it_parses_user_correctly_from_parameter() {
225    let url = "postgres:///?user=some_user";
226    let opts = PgConnectOptions::from_str(url).unwrap();
227
228    assert_eq!(None, opts.socket);
229    assert_eq!("some_user", opts.username);
230}
231
232#[test]
233fn it_parses_password_correctly_from_parameter() {
234    let url = "postgres:///?password=some_pass";
235    let opts = PgConnectOptions::from_str(url).unwrap();
236
237    assert_eq!(None, opts.socket);
238    assert_eq!(Some("some_pass"), opts.password.as_deref());
239}
240
241#[test]
242fn it_parses_application_name_correctly_from_parameter() {
243    let url = "postgres:///?application_name=some_name";
244    let opts = PgConnectOptions::from_str(url).unwrap();
245
246    assert_eq!(Some("some_name"), opts.application_name.as_deref());
247}
248
249#[test]
250fn it_parses_username_with_at_sign_correctly() {
251    let url = "postgres://user@hostname:password@hostname:5432/database";
252    let opts = PgConnectOptions::from_str(url).unwrap();
253
254    assert_eq!("user@hostname", &opts.username);
255}
256
257#[test]
258fn it_parses_password_with_non_ascii_chars_correctly() {
259    let url = "postgres://username:p@ssw0rd@hostname:5432/database";
260    let opts = PgConnectOptions::from_str(url).unwrap();
261
262    assert_eq!(Some("p@ssw0rd".into()), opts.password);
263}
264
265#[test]
266fn it_parses_socket_correctly_percent_encoded() {
267    let url = "postgres://%2Fvar%2Flib%2Fpostgres/database";
268    let opts = PgConnectOptions::from_str(url).unwrap();
269
270    assert_eq!(Some("/var/lib/postgres/".into()), opts.socket);
271}
272#[test]
273fn it_parses_socket_correctly_with_username_percent_encoded() {
274    let url = "postgres://some_user@%2Fvar%2Flib%2Fpostgres/database";
275    let opts = PgConnectOptions::from_str(url).unwrap();
276
277    assert_eq!("some_user", opts.username);
278    assert_eq!(Some("/var/lib/postgres/".into()), opts.socket);
279    assert_eq!(Some("database"), opts.database.as_deref());
280}
281#[test]
282fn it_parses_libpq_options_correctly() {
283    let url = "postgres:///?options=-c%20synchronous_commit%3Doff%20--search_path%3Dpostgres";
284    let opts = PgConnectOptions::from_str(url).unwrap();
285
286    assert_eq!(
287        Some("-c synchronous_commit=off --search_path=postgres".into()),
288        opts.options
289    );
290}
291#[test]
292fn it_parses_sqlx_options_correctly() {
293    let url = "postgres:///?options[synchronous_commit]=off&options[search_path]=postgres";
294    let opts = PgConnectOptions::from_str(url).unwrap();
295
296    assert_eq!(
297        Some("-c synchronous_commit=off -c search_path=postgres".into()),
298        opts.options
299    );
300}
301
302#[test]
303fn it_returns_the_parsed_url_when_socket() {
304    let url = "postgres://username@%2Fvar%2Flib%2Fpostgres/database";
305    let opts = PgConnectOptions::from_str(url).unwrap();
306
307    let mut expected_url = Url::parse(url).unwrap();
308    // PgConnectOptions defaults
309    let query_string = "ssl-mode=PREFERRED&statement-cache-capacity=100";
310    let port = 5432;
311    expected_url.set_query(Some(query_string));
312    let _ = expected_url.set_port(Some(port));
313
314    assert_eq!(expected_url, opts.build_url());
315}
316
317#[test]
318fn it_returns_the_parsed_url_when_host() {
319    let url = "postgres://username:p@ssw0rd@hostname:5432/database";
320    let opts = PgConnectOptions::from_str(url).unwrap();
321
322    let mut expected_url = Url::parse(url).unwrap();
323    // PgConnectOptions defaults
324    let query_string = "ssl-mode=PREFERRED&statement-cache-capacity=100";
325    expected_url.set_query(Some(query_string));
326
327    assert_eq!(expected_url, opts.build_url());
328}