1use crate::config::{Endpoint, Host, Password, Port, Session, SslMode, SslRootCert};
2use crate::{Config, Database, User};
3use fluent_uri::pct_enc::EStr;
4use std::collections::{BTreeMap, BTreeSet};
5use std::fmt;
6
7#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
8pub enum ParseError {
9 #[error("Invalid URL: {0}")]
10 InvalidUrl(#[from] ::fluent_uri::ParseError),
11 #[error("Invalid URL scheme: expected 'postgres' or 'postgresql', got '{0}'")]
12 InvalidScheme(String),
13 #[error("Invalid URL fragment: '{0}'")]
14 InvalidFragment(String),
15 #[error("Missing host in URL")]
16 MissingHost,
17 #[error("Missing required parameter '{0}' in URL")]
18 MissingParameter(&'static str),
19 #[error("Parameter '{0}' specified in both URL and query string")]
20 ConflictingParameter(&'static str),
21 #[error("Unknown query parameter: '{0}'")]
22 InvalidQueryParameter(String),
23 #[error("Invalid query parameter encoding: {0}")]
24 InvalidQueryParameterEncoding(std::str::Utf8Error),
25 #[error("{0}")]
26 Field(#[from] FieldError),
27 #[error("Unsupported parameter for socket path connection: '{0}'")]
28 UnsupportedSocketPathParameter(&'static str),
29 #[error("Invalid port: {0}")]
30 InvalidPort(#[from] std::num::ParseIntError),
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum FieldSource {
35 Authority,
36 Path,
37 QueryParam,
38}
39
40impl fmt::Display for FieldSource {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 FieldSource::Authority => f.write_str("authority"),
44 FieldSource::Path => f.write_str("path"),
45 FieldSource::QueryParam => f.write_str("query"),
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum Field {
52 User,
53 Password,
54 Database,
55 Host,
56 HostAddr,
57 SslMode,
58 SslRootCert,
59 ApplicationName,
60 ChannelBinding,
61}
62
63impl fmt::Display for Field {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 Field::User => f.write_str("user"),
67 Field::Password => f.write_str("password"),
68 Field::Database => f.write_str("dbname"),
69 Field::Host => f.write_str("host"),
70 Field::HostAddr => f.write_str("hostaddr"),
71 Field::SslMode => f.write_str("sslmode"),
72 Field::SslRootCert => f.write_str("sslrootcert"),
73 Field::ApplicationName => f.write_str("application_name"),
74 Field::ChannelBinding => f.write_str("channel_binding"),
75 }
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum FieldErrorCause {
81 InvalidUtf8(std::str::Utf8Error),
82 InvalidIdentifier(crate::identifier::ParseError),
83 InvalidValue(String),
84}
85
86impl fmt::Display for FieldErrorCause {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 match self {
89 FieldErrorCause::InvalidUtf8(error) => {
90 write!(f, "invalid utf-8 encoding: {error}")
91 }
92 FieldErrorCause::InvalidIdentifier(error) => {
93 write!(f, "invalid value: {error}")
94 }
95 FieldErrorCause::InvalidValue(error) if error.is_empty() => {
96 f.write_str("invalid value")
97 }
98 FieldErrorCause::InvalidValue(error) => write!(f, "invalid value: {error}"),
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
104#[error("Invalid {field} in {origin}: {cause}")]
105pub struct FieldError {
106 pub origin: FieldSource,
107 pub field: Field,
108 pub cause: FieldErrorCause,
109}
110
111pub fn parse(url: &str) -> Result<Config, ParseError> {
166 let uri = ::fluent_uri::Uri::parse(url)?;
167
168 let scheme = uri.scheme().as_str();
170 if scheme != "postgres" && scheme != "postgresql" {
171 return Err(ParseError::InvalidScheme(scheme.to_string()));
172 }
173
174 if let Some(fragment) = uri.fragment() {
175 return Err(ParseError::InvalidFragment(fragment.as_str().to_string()));
176 }
177
178 let query_map = parse_query(uri.query())?;
180 let mut query_params = QueryParams::new(&query_map);
181
182 let authority = uri.authority();
184 let (url_user, url_password) = extract_userinfo(authority.as_ref())?;
185
186 let url_database = decode_path_database(uri.path())?;
188
189 let query_host = query_params.take("host");
191
192 let endpoint = match authority.as_ref() {
193 Some(authority) if !authority.host().is_empty() => {
194 if query_host.is_some() {
195 return Err(ParseError::ConflictingParameter("host"));
196 }
197
198 let host = match authority.host_parsed() {
199 fluent_uri::component::Host::RegName(name) => {
200 let decoded = decode_to_string(name).map_err(|error| FieldError {
201 origin: FieldSource::Authority,
202 field: Field::Host,
203 cause: FieldErrorCause::InvalidUtf8(error),
204 })?;
205 decoded.parse::<Host>().map_err(|error: &str| FieldError {
206 origin: FieldSource::Authority,
207 field: Field::Host,
208 cause: FieldErrorCause::InvalidValue(error.to_string()),
209 })?
210 }
211 fluent_uri::component::Host::Ipv4(addr) => Host::IpAddr(addr.into()),
212 fluent_uri::component::Host::Ipv6(addr) => Host::IpAddr(addr.into()),
213 _ => {
214 let host = authority.host();
215 let message = if host.starts_with("[v") || host.starts_with("[V") {
216 "unsupported host type: ipvfuture"
217 } else {
218 "unsupported host type"
219 };
220 return Err(FieldError {
221 origin: FieldSource::Authority,
222 field: Field::Host,
223 cause: FieldErrorCause::InvalidValue(message.to_string()),
224 }
225 .into());
226 }
227 };
228
229 let host_addr = match query_params.take("hostaddr") {
230 Some(addr_str) => Some(addr_str.parse().map_err(|error: &str| FieldError {
231 origin: FieldSource::QueryParam,
232 field: Field::HostAddr,
233 cause: FieldErrorCause::InvalidValue(error.to_string()),
234 })?),
235 None => None,
236 };
237
238 let channel_binding = match query_params.take("channel_binding") {
239 Some(binding_str) => Some(binding_str.parse().map_err(|_| FieldError {
240 origin: FieldSource::QueryParam,
241 field: Field::ChannelBinding,
242 cause: FieldErrorCause::InvalidValue(binding_str.to_string()),
243 })?),
244 None => None,
245 };
246
247 let port = authority.port_to_u16()?.map(Port::new);
248
249 Endpoint::Network {
250 host,
251 channel_binding,
252 host_addr,
253 port,
254 }
255 }
256 _ => {
257 let host = query_host.ok_or(ParseError::MissingHost)?;
258
259 if !host.starts_with('/') && !host.starts_with('@') {
260 return Err(FieldError {
261 origin: FieldSource::QueryParam,
262 field: Field::Host,
263 cause: FieldErrorCause::InvalidValue(
264 "query host must be a socket path (start with / or @)".to_string(),
265 ),
266 }
267 .into());
268 }
269
270 for name in ["channel_binding", "hostaddr"] {
271 if query_params.take(name).is_some() {
272 return Err(ParseError::UnsupportedSocketPathParameter(name));
273 }
274 }
275
276 Endpoint::SocketPath(host.into())
277 }
278 };
279
280 let user_value = access_field(
281 "user",
282 url_user.map(|value| FieldValue::new(FieldSource::Authority, value)),
283 &mut query_params,
284 )?
285 .ok_or(ParseError::MissingParameter("user"))?;
286 if user_value.value.is_empty() {
287 return Err(ParseError::MissingParameter("user"));
288 }
289 let user: User = user_value.value.parse().map_err(|error| FieldError {
290 origin: user_value.origin,
291 field: Field::User,
292 cause: FieldErrorCause::InvalidIdentifier(error),
293 })?;
294
295 let password: Option<Password> = match access_field(
296 "password",
297 url_password.map(|value| FieldValue::new(FieldSource::Authority, value)),
298 &mut query_params,
299 )? {
300 Some(value) => Some(value.value.parse().map_err(|error: String| FieldError {
301 origin: value.origin,
302 field: Field::Password,
303 cause: FieldErrorCause::InvalidValue(error.to_string()),
304 })?),
305 None => None,
306 };
307
308 let database_value = access_field(
309 "dbname",
310 url_database.map(|value| FieldValue::new(FieldSource::Path, value)),
311 &mut query_params,
312 )?
313 .ok_or(ParseError::MissingParameter("dbname"))?;
314 let database: Database = database_value.value.parse().map_err(|error| FieldError {
315 origin: database_value.origin,
316 field: Field::Database,
317 cause: FieldErrorCause::InvalidIdentifier(error),
318 })?;
319
320 let ssl_mode = match query_params.take("sslmode") {
322 Some(mode_str) => mode_str.parse().map_err(|_| FieldError {
323 origin: FieldSource::QueryParam,
324 field: Field::SslMode,
325 cause: FieldErrorCause::InvalidValue(mode_str.to_string()),
326 })?,
327 None => SslMode::VerifyFull,
328 };
329
330 let ssl_root_cert = query_params.take("sslrootcert").map(|cert_str| {
332 if cert_str == "system" {
333 SslRootCert::System
334 } else {
335 SslRootCert::File(cert_str.to_string().into())
336 }
337 });
338
339 let application_name = match query_params.take("application_name") {
341 Some(name_str) => Some(name_str.parse().map_err(|error: String| FieldError {
342 origin: FieldSource::QueryParam,
343 field: Field::ApplicationName,
344 cause: FieldErrorCause::InvalidValue(error),
345 })?),
346 None => None,
347 };
348
349 if let Some(unknown) = query_params.unknown_param() {
350 return Err(ParseError::InvalidQueryParameter(unknown.to_string()));
351 }
352
353 Ok(Config {
354 endpoint,
355 session: Session {
356 application_name,
357 database,
358 password,
359 user,
360 },
361 ssl_mode,
362 ssl_root_cert,
363 #[cfg(feature = "sqlx")]
364 sqlx: Default::default(),
365 })
366}
367
368fn extract_userinfo(
369 authority: Option<&fluent_uri::component::Authority<'_>>,
370) -> Result<(Option<String>, Option<String>), ParseError> {
371 let userinfo = match authority.and_then(|authority| authority.userinfo()) {
372 Some(info) => info,
373 None => return Ok((None, None)),
374 };
375
376 match userinfo.split_once(':') {
377 Some((user_enc, pass_enc)) => {
378 let user = decode_to_string(user_enc).map_err(|error| FieldError {
379 origin: FieldSource::Authority,
380 field: Field::User,
381 cause: FieldErrorCause::InvalidUtf8(error),
382 })?;
383 let password = decode_to_string(pass_enc).map_err(|error| FieldError {
384 origin: FieldSource::Authority,
385 field: Field::Password,
386 cause: FieldErrorCause::InvalidUtf8(error),
387 })?;
388 let user = non_empty(user);
389 let password = non_empty(password);
390 Ok((user, password))
391 }
392 None => {
393 let user = decode_to_string(userinfo).map_err(|error| FieldError {
394 origin: FieldSource::Authority,
395 field: Field::User,
396 cause: FieldErrorCause::InvalidUtf8(error),
397 })?;
398 Ok((non_empty(user), None))
399 }
400 }
401}
402
403fn decode_to_string<E: fluent_uri::pct_enc::Encoder>(
404 estr: &EStr<E>,
405) -> Result<String, std::str::Utf8Error> {
406 let bytes = estr.decode().to_bytes();
407 String::from_utf8(bytes.into_owned()).map_err(|error| error.utf8_error())
408}
409
410fn non_empty(value: String) -> Option<String> {
411 if value.is_empty() { None } else { Some(value) }
412}
413
414fn decode_path_database(
415 path: &EStr<fluent_uri::pct_enc::encoder::Path>,
416) -> Result<Option<String>, ParseError> {
417 let decoded = decode_to_string(path).map_err(|error| FieldError {
418 origin: FieldSource::Path,
419 field: Field::Database,
420 cause: FieldErrorCause::InvalidUtf8(error),
421 })?;
422
423 let stripped = decoded.strip_prefix('/').unwrap_or(&decoded);
424
425 Ok(non_empty(stripped.to_string()))
426}
427
428fn parse_query(
429 query: Option<&EStr<fluent_uri::pct_enc::encoder::Query>>,
430) -> Result<BTreeMap<String, String>, ParseError> {
431 let query = match query {
432 Some(query) => query,
433 None => return Ok(BTreeMap::new()),
434 };
435
436 query
437 .split('&')
438 .map(|pair| {
439 let (key, value) = pair.split_once('=').unwrap_or((pair, EStr::EMPTY));
440 let key = decode_to_string(key).map_err(ParseError::InvalidQueryParameterEncoding)?;
441 let field = query_field(&key);
442 let value = decode_to_string(value).map_err(|error| match field {
443 Some(field) => FieldError {
444 origin: FieldSource::QueryParam,
445 field,
446 cause: FieldErrorCause::InvalidUtf8(error),
447 }
448 .into(),
449 None => ParseError::InvalidQueryParameterEncoding(error),
450 })?;
451 Ok((key, value))
452 })
453 .collect()
454}
455
456fn access_field(
457 name: &'static str,
458 url_value: Option<FieldValue>,
459 query_params: &mut QueryParams<'_>,
460) -> Result<Option<FieldValue>, ParseError> {
461 let query_value = query_params
462 .take(name)
463 .map(|value| FieldValue::new(FieldSource::QueryParam, value.to_string()));
464 match (url_value, query_value) {
465 (Some(_), Some(_)) => Err(ParseError::ConflictingParameter(name)),
466 (Some(value), None) => Ok(Some(value)),
467 (None, Some(value)) => Ok(Some(value)),
468 (None, None) => Ok(None),
469 }
470}
471
472#[derive(Debug, Clone, PartialEq, Eq)]
473struct FieldValue {
474 origin: FieldSource,
475 value: String,
476}
477
478impl FieldValue {
479 fn new(origin: FieldSource, value: String) -> Self {
480 Self { origin, value }
481 }
482}
483
484fn query_field(name: &str) -> Option<Field> {
485 match name {
486 "user" => Some(Field::User),
487 "password" => Some(Field::Password),
488 "dbname" => Some(Field::Database),
489 "host" => Some(Field::Host),
490 "hostaddr" => Some(Field::HostAddr),
491 "sslmode" => Some(Field::SslMode),
492 "sslrootcert" => Some(Field::SslRootCert),
493 "application_name" => Some(Field::ApplicationName),
494 "channel_binding" => Some(Field::ChannelBinding),
495 _ => None,
496 }
497}
498
499struct QueryParams<'a> {
500 params: &'a BTreeMap<String, String>,
501 remaining: BTreeSet<&'a str>,
502}
503
504impl<'a> QueryParams<'a> {
505 fn new(params: &'a BTreeMap<String, String>) -> Self {
506 let remaining = params.keys().map(|key| key.as_str()).collect();
507 Self { params, remaining }
508 }
509
510 fn take(&mut self, name: &str) -> Option<&'a str> {
511 let value = self.params.get(name).map(|value| value.as_str());
512 if value.is_some() {
513 self.remaining.remove(name);
514 }
515 value
516 }
517
518 fn unknown_param(&self) -> Option<&&'a str> {
519 self.remaining.iter().next()
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526 use crate::config::{ChannelBinding, SslMode};
527
528 fn network(host: &str, port: Option<u16>, host_addr: Option<&str>) -> Endpoint {
529 Endpoint::Network {
530 host: host.parse().unwrap(),
531 channel_binding: None,
532 port: port.map(Port::new),
533 host_addr: host_addr.map(|address| address.parse().unwrap()),
534 }
535 }
536
537 fn success(
538 user: &str,
539 password: Option<&str>,
540 database: &str,
541 endpoint: Endpoint,
542 ssl_mode: SslMode,
543 ssl_root_cert: Option<SslRootCert>,
544 application_name: Option<&str>,
545 ) -> Config {
546 Config {
547 endpoint,
548 session: Session {
549 user: user.parse().unwrap(),
550 password: password.map(|value| value.parse().unwrap()),
551 database: database.parse().unwrap(),
552 application_name: application_name.map(|value| value.parse().unwrap()),
553 },
554 ssl_mode,
555 ssl_root_cert,
556 #[cfg(feature = "sqlx")]
557 sqlx: Default::default(),
558 }
559 }
560
561 fn field_error(origin: FieldSource, field: Field, cause: FieldErrorCause) -> ParseError {
562 ParseError::Field(FieldError {
563 origin,
564 field,
565 cause,
566 })
567 }
568
569 #[test]
570 fn test_parse() {
571 type Expected = Result<Config, ParseError>;
572
573 let cases: Vec<(&str, &str, Expected)> = vec![
574 (
576 "basic_network",
577 "postgres://user@localhost:5432/mydb",
578 Ok(success(
579 "user",
580 None,
581 "mydb",
582 network("localhost", Some(5432), None),
583 SslMode::VerifyFull,
584 None,
585 None,
586 )),
587 ),
588 (
589 "with_password",
590 "postgres://user:secret@localhost/mydb",
591 Ok(success(
592 "user",
593 Some("secret"),
594 "mydb",
595 network("localhost", None, None),
596 SslMode::VerifyFull,
597 None,
598 None,
599 )),
600 ),
601 (
602 "percent_encoded_password",
603 "postgres://user:p%40ss%2Fword@localhost/mydb",
604 Ok(success(
605 "user",
606 Some("p@ss/word"),
607 "mydb",
608 network("localhost", None, None),
609 SslMode::VerifyFull,
610 None,
611 None,
612 )),
613 ),
614 (
615 "with_sslmode_disable",
616 "postgres://user@localhost/mydb?sslmode=disable",
617 Ok(success(
618 "user",
619 None,
620 "mydb",
621 network("localhost", None, None),
622 SslMode::Disable,
623 None,
624 None,
625 )),
626 ),
627 (
628 "with_sslmode_require",
629 "postgres://user@localhost/mydb?sslmode=require",
630 Ok(success(
631 "user",
632 None,
633 "mydb",
634 network("localhost", None, None),
635 SslMode::Require,
636 None,
637 None,
638 )),
639 ),
640 (
641 "with_channel_binding",
642 "postgres://user@localhost/mydb?channel_binding=require",
643 Ok(success(
644 "user",
645 None,
646 "mydb",
647 Endpoint::Network {
648 host: "localhost".parse().unwrap(),
649 channel_binding: Some(ChannelBinding::Require),
650 port: None,
651 host_addr: None,
652 },
653 SslMode::VerifyFull,
654 None,
655 None,
656 )),
657 ),
658 (
659 "with_application_name",
660 "postgres://user@localhost/mydb?application_name=myapp",
661 Ok(success(
662 "user",
663 None,
664 "mydb",
665 network("localhost", None, None),
666 SslMode::VerifyFull,
667 None,
668 Some("myapp"),
669 )),
670 ),
671 (
672 "with_hostaddr",
673 "postgres://user@example.com/mydb?hostaddr=192.168.1.1",
674 Ok(success(
675 "user",
676 None,
677 "mydb",
678 network("example.com", None, Some("192.168.1.1")),
679 SslMode::VerifyFull,
680 None,
681 None,
682 )),
683 ),
684 (
685 "with_sslrootcert_file",
686 "postgres://user@localhost/mydb?sslrootcert=/path/to/cert.pem",
687 Ok(success(
688 "user",
689 None,
690 "mydb",
691 network("localhost", None, None),
692 SslMode::VerifyFull,
693 Some(SslRootCert::File("/path/to/cert.pem".into())),
694 None,
695 )),
696 ),
697 (
698 "with_sslrootcert_system",
699 "postgres://user@localhost/mydb?sslrootcert=system",
700 Ok(success(
701 "user",
702 None,
703 "mydb",
704 network("localhost", None, None),
705 SslMode::VerifyFull,
706 Some(SslRootCert::System),
707 None,
708 )),
709 ),
710 (
711 "socket_path",
712 "postgres://?host=/var/run/postgresql&user=postgres&dbname=mydb",
713 Ok(success(
714 "postgres",
715 None,
716 "mydb",
717 Endpoint::SocketPath("/var/run/postgresql".into()),
718 SslMode::VerifyFull,
719 None,
720 None,
721 )),
722 ),
723 (
724 "socket_with_password",
725 "postgres://?host=/socket&user=user&password=pass&dbname=mydb",
726 Ok(success(
727 "user",
728 Some("pass"),
729 "mydb",
730 Endpoint::SocketPath("/socket".into()),
731 SslMode::VerifyFull,
732 None,
733 None,
734 )),
735 ),
736 (
737 "abstract_socket",
738 "postgres://?host=@abstract&user=postgres&dbname=mydb",
739 Ok(success(
740 "postgres",
741 None,
742 "mydb",
743 Endpoint::SocketPath("@abstract".into()),
744 SslMode::VerifyFull,
745 None,
746 None,
747 )),
748 ),
749 (
750 "postgresql_scheme",
751 "postgresql://user@localhost/mydb",
752 Ok(success(
753 "user",
754 None,
755 "mydb",
756 network("localhost", None, None),
757 SslMode::VerifyFull,
758 None,
759 None,
760 )),
761 ),
762 (
763 "ipv6_host",
764 "postgres://user@[::1]:5432/mydb",
765 Ok(success(
766 "user",
767 None,
768 "mydb",
769 network("::1", Some(5432), None),
770 SslMode::VerifyFull,
771 None,
772 None,
773 )),
774 ),
775 (
776 "ipv4_host",
777 "postgres://user@192.168.1.1:5432/mydb",
778 Ok(success(
779 "user",
780 None,
781 "mydb",
782 network("192.168.1.1", Some(5432), None),
783 SslMode::VerifyFull,
784 None,
785 None,
786 )),
787 ),
788 (
789 "no_port",
790 "postgres://user@localhost/mydb",
791 Ok(success(
792 "user",
793 None,
794 "mydb",
795 network("localhost", None, None),
796 SslMode::VerifyFull,
797 None,
798 None,
799 )),
800 ),
801 (
803 "cloud_sql_socket",
804 "postgres://user:secret@/main?host=/cloudsql/project:region:instance",
805 Ok(success(
806 "user",
807 Some("secret"),
808 "main",
809 Endpoint::SocketPath("/cloudsql/project:region:instance".into()),
810 SslMode::VerifyFull,
811 None,
812 None,
813 )),
814 ),
815 (
816 "cloud_sql_socket_no_password",
817 "postgres://user@/main?host=/cloudsql/project:region:instance",
818 Ok(success(
819 "user",
820 None,
821 "main",
822 Endpoint::SocketPath("/cloudsql/project:region:instance".into()),
823 SslMode::VerifyFull,
824 None,
825 None,
826 )),
827 ),
828 (
829 "cloud_sql_socket_sslmode_disable",
830 "postgres://user:secret@/main?host=/cloudsql/project:region:instance&sslmode=disable",
831 Ok(success(
832 "user",
833 Some("secret"),
834 "main",
835 Endpoint::SocketPath("/cloudsql/project:region:instance".into()),
836 SslMode::Disable,
837 None,
838 None,
839 )),
840 ),
841 (
842 "cloud_sql_socket_query_params",
843 "postgres://?host=/cloudsql/project:region:instance&user=user&password=secret&dbname=main",
844 Ok(success(
845 "user",
846 Some("secret"),
847 "main",
848 Endpoint::SocketPath("/cloudsql/project:region:instance".into()),
849 SslMode::VerifyFull,
850 None,
851 None,
852 )),
853 ),
854 (
856 "invalid_scheme",
857 "mysql://user@localhost/mydb",
858 Err(ParseError::InvalidScheme("mysql".to_string())),
859 ),
860 (
861 "missing_username",
862 "postgres://localhost/mydb",
863 Err(ParseError::MissingParameter("user")),
864 ),
865 (
866 "missing_database",
867 "postgres://user@localhost",
868 Err(ParseError::MissingParameter("dbname")),
869 ),
870 (
871 "missing_host",
872 "postgres://?user=user&dbname=mydb",
873 Err(ParseError::MissingHost),
874 ),
875 (
876 "conflicting_host",
877 "postgres://user@localhost/mydb?host=/socket",
878 Err(ParseError::ConflictingParameter("host")),
879 ),
880 (
881 "conflicting_user",
882 "postgres://user@localhost/mydb?user=other",
883 Err(ParseError::ConflictingParameter("user")),
884 ),
885 (
886 "conflicting_password",
887 "postgres://user:secret@localhost/mydb?password=other",
888 Err(ParseError::ConflictingParameter("password")),
889 ),
890 (
891 "conflicting_dbname",
892 "postgres://user@localhost/mydb?dbname=other",
893 Err(ParseError::ConflictingParameter("dbname")),
894 ),
895 (
896 "invalid_sslmode",
897 "postgres://user@localhost/mydb?sslmode=invalid",
898 Err(field_error(
899 FieldSource::QueryParam,
900 Field::SslMode,
901 FieldErrorCause::InvalidValue("invalid".to_string()),
902 )),
903 ),
904 (
905 "invalid_channel_binding",
906 "postgres://user@localhost/mydb?channel_binding=invalid",
907 Err(field_error(
908 FieldSource::QueryParam,
909 Field::ChannelBinding,
910 FieldErrorCause::InvalidValue("invalid".to_string()),
911 )),
912 ),
913 (
914 "invalid_hostaddr",
915 "postgres://user@localhost/mydb?hostaddr=not-an-ip",
916 Err(field_error(
917 FieldSource::QueryParam,
918 Field::HostAddr,
919 FieldErrorCause::InvalidValue("invalid IP address".to_string()),
920 )),
921 ),
922 (
923 "unsupported_ipvfuture_host",
924 "postgres://user@[v1.fe80]/mydb",
925 Err(field_error(
926 FieldSource::Authority,
927 Field::Host,
928 FieldErrorCause::InvalidValue("unsupported host type: ipvfuture".to_string()),
929 )),
930 ),
931 (
932 "unknown_parameter",
933 "postgres://user@localhost/mydb?unknown_parameter=1",
934 Err(ParseError::InvalidQueryParameter(
935 "unknown_parameter".to_string(),
936 )),
937 ),
938 (
939 "fragment",
940 "postgres://user@localhost/mydb#section",
941 Err(ParseError::InvalidFragment("section".to_string())),
942 ),
943 (
944 "socket_missing_user",
945 "postgres://?host=/socket&dbname=mydb",
946 Err(ParseError::MissingParameter("user")),
947 ),
948 (
949 "socket_missing_dbname",
950 "postgres://?host=/socket&user=user",
951 Err(ParseError::MissingParameter("dbname")),
952 ),
953 (
954 "socket_with_channel_binding",
955 "postgres://?host=/socket&user=user&dbname=mydb&channel_binding=require",
956 Err(ParseError::UnsupportedSocketPathParameter(
957 "channel_binding",
958 )),
959 ),
960 (
961 "socket_with_hostaddr",
962 "postgres://?host=/socket&user=user&dbname=mydb&hostaddr=127.0.0.1",
963 Err(ParseError::UnsupportedSocketPathParameter("hostaddr")),
964 ),
965 (
967 "cloud_sql_conflicting_user",
968 "postgres://user@/main?host=/cloudsql/project:region:instance&user=other",
969 Err(ParseError::ConflictingParameter("user")),
970 ),
971 (
972 "cloud_sql_conflicting_password",
973 "postgres://user:secret@/main?host=/cloudsql/project:region:instance&password=other",
974 Err(ParseError::ConflictingParameter("password")),
975 ),
976 (
977 "cloud_sql_conflicting_dbname",
978 "postgres://user@/main?host=/cloudsql/project:region:instance&dbname=other",
979 Err(ParseError::ConflictingParameter("dbname")),
980 ),
981 ];
982
983 for (name, url_str, expected) in cases {
984 let actual = parse(url_str);
985
986 assert_eq!(actual, expected, "{name}: {url_str}");
987
988 if let Ok(config) = actual {
989 let roundtrip_url = config.to_url_string();
990 let roundtrip_config = parse(&roundtrip_url).unwrap_or_else(|error| {
991 panic!("{name}: roundtrip parse failed: {error}, url: {roundtrip_url}")
992 });
993 assert_eq!(roundtrip_config, config, "{name}: roundtrip");
994 }
995 }
996 }
997}