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