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