LCOV - differential code coverage report
Current view: top level - libs/postgres_connection/src - lib.rs (source / functions) Coverage Total Hit UBC CBC
Current: cd44433dd675caa99df17a61b18949c8387e2242.info Lines: 95.9 % 194 186 8 186
Current Date: 2024-01-09 02:06:09 Functions: 87.2 % 47 41 6 41
Baseline: 66c52a629a0f4a503e193045e0df4c77139e344b.info
Baseline Date: 2024-01-08 15:34:46

           TLA  Line data    Source code
       1                 : #![deny(unsafe_code)]
       2                 : #![deny(clippy::undocumented_unsafe_blocks)]
       3                 : use anyhow::{bail, Context};
       4                 : use itertools::Itertools;
       5                 : use std::borrow::Cow;
       6                 : use std::fmt;
       7                 : use url::Host;
       8                 : 
       9                 : /// Parses a string of format either `host:port` or `host` into a corresponding pair.
      10                 : /// The `host` part should be a correct `url::Host`, while `port` (if present) should be
      11                 : /// a valid decimal u16 of digits only.
      12 CBC      852167 : pub fn parse_host_port<S: AsRef<str>>(host_port: S) -> Result<(Host, Option<u16>), anyhow::Error> {
      13          852167 :     let (host, port) = match host_port.as_ref().rsplit_once(':') {
      14          852152 :         Some((host, port)) => (
      15          852152 :             host,
      16          852152 :             // +80 is a valid u16, but not a valid port
      17         4260752 :             if port.chars().all(|c| c.is_ascii_digit()) {
      18          852151 :                 Some(port.parse::<u16>().context("Unable to parse port")?)
      19                 :             } else {
      20               1 :                 bail!("Port contains a non-ascii-digit")
      21                 :             },
      22                 :         ),
      23              15 :         None => (host_port.as_ref(), None), // No colons, no port specified
      24                 :     };
      25          852166 :     let host = Host::parse(host).context("Unable to parse host")?;
      26          852165 :     Ok((host, port))
      27          852167 : }
      28                 : 
      29                 : #[cfg(test)]
      30                 : mod tests_parse_host_port {
      31                 :     use crate::parse_host_port;
      32                 :     use url::Host;
      33                 : 
      34               1 :     #[test]
      35               1 :     fn test_normal() {
      36               1 :         let (host, port) = parse_host_port("hello:123").unwrap();
      37               1 :         assert_eq!(host, Host::Domain("hello".to_owned()));
      38               1 :         assert_eq!(port, Some(123));
      39               1 :     }
      40                 : 
      41               1 :     #[test]
      42               1 :     fn test_no_port() {
      43               1 :         let (host, port) = parse_host_port("hello").unwrap();
      44               1 :         assert_eq!(host, Host::Domain("hello".to_owned()));
      45               1 :         assert_eq!(port, None);
      46               1 :     }
      47                 : 
      48               1 :     #[test]
      49               1 :     fn test_ipv6() {
      50               1 :         let (host, port) = parse_host_port("[::1]:123").unwrap();
      51               1 :         assert_eq!(host, Host::<String>::Ipv6(std::net::Ipv6Addr::LOCALHOST));
      52               1 :         assert_eq!(port, Some(123));
      53               1 :     }
      54                 : 
      55               1 :     #[test]
      56               1 :     fn test_invalid_host() {
      57               1 :         assert!(parse_host_port("hello world").is_err());
      58               1 :     }
      59                 : 
      60               1 :     #[test]
      61               1 :     fn test_invalid_port() {
      62               1 :         assert!(parse_host_port("hello:+80").is_err());
      63               1 :     }
      64                 : }
      65                 : 
      66          565876 : #[derive(Clone)]
      67                 : pub struct PgConnectionConfig {
      68                 :     host: Host,
      69                 :     port: u16,
      70                 :     password: Option<String>,
      71                 :     options: Vec<String>,
      72                 : }
      73                 : 
      74                 : /// A simplified PostgreSQL connection configuration. Supports only a subset of possible
      75                 : /// settings for simplicity. A password getter or `to_connection_string` methods are not
      76                 : /// added by design to avoid accidentally leaking password through logging, command line
      77                 : /// arguments to a child process, or likewise.
      78                 : impl PgConnectionConfig {
      79          853171 :     pub fn new_host_port(host: Host, port: u16) -> Self {
      80          853171 :         PgConnectionConfig {
      81          853171 :             host,
      82          853171 :             port,
      83          853171 :             password: None,
      84          853171 :             options: vec![],
      85          853171 :         }
      86          853171 :     }
      87                 : 
      88            3703 :     pub fn host(&self) -> &Host {
      89            3703 :         &self.host
      90            3703 :     }
      91                 : 
      92            2161 :     pub fn port(&self) -> u16 {
      93            2161 :         self.port
      94            2161 :     }
      95                 : 
      96 UBC           0 :     pub fn set_host(mut self, h: Host) -> Self {
      97               0 :         self.host = h;
      98               0 :         self
      99               0 :     }
     100                 : 
     101               0 :     pub fn set_port(mut self, p: u16) -> Self {
     102               0 :         self.port = p;
     103               0 :         self
     104               0 :     }
     105                 : 
     106 CBC      845551 :     pub fn set_password(mut self, s: Option<String>) -> Self {
     107          845551 :         self.password = s;
     108          845551 :         self
     109          845551 :     }
     110                 : 
     111          846441 :     pub fn extend_options<I: IntoIterator<Item = S>, S: Into<String>>(mut self, i: I) -> Self {
     112         2537544 :         self.options.extend(i.into_iter().map(|s| s.into()));
     113          846441 :         self
     114          846441 :     }
     115                 : 
     116                 :     /// Return a `<host>:<port>` string.
     117            1395 :     pub fn raw_address(&self) -> String {
     118            1395 :         format!("{}:{}", self.host(), self.port())
     119            1395 :     }
     120                 : 
     121                 :     /// Build a client library-specific connection configuration.
     122                 :     /// Used for testing and when we need to add some obscure configuration
     123                 :     /// elements at the last moment.
     124            1536 :     pub fn to_tokio_postgres_config(&self) -> tokio_postgres::Config {
     125            1536 :         // Use `tokio_postgres::Config` instead of `postgres::Config` because
     126            1536 :         // the former supports more options to fiddle with later.
     127            1536 :         let mut config = tokio_postgres::Config::new();
     128            1536 :         config.host(&self.host().to_string()).port(self.port);
     129            1536 :         if let Some(password) = &self.password {
     130              56 :             config.password(password);
     131            1480 :         }
     132            1536 :         if !self.options.is_empty() {
     133            1531 :             // These options are command-line options and should be escaped before being passed
     134            1531 :             // as an 'options' connection string parameter, see
     135            1531 :             // https://www.postgresql.org/docs/15/libpq-connect.html#LIBPQ-CONNECT-OPTIONS
     136            1531 :             //
     137            1531 :             // They will be space-separated, so each space inside an option should be escaped,
     138            1531 :             // and all backslashes should be escaped before that. Although we don't expect options
     139            1531 :             // with spaces at the moment, they're supported by PostgreSQL. Hence we support them
     140            1531 :             // in this typesafe interface.
     141            1531 :             //
     142            1531 :             // We use `Cow` to avoid allocations in the best case (no escaping). A fully imperative
     143            1531 :             // solution would require 1-2 allocations in the worst case as well, but it's harder to
     144            1531 :             // implement and this function is hardly a bottleneck. The function is only called around
     145            1531 :             // establishing a new connection.
     146            1531 :             #[allow(unstable_name_collisions)]
     147            1531 :             config.options(
     148            1531 :                 &self
     149            1531 :                     .options
     150            1531 :                     .iter()
     151            4612 :                     .map(|s| {
     152            4612 :                         if s.contains(['\\', ' ']) {
     153               2 :                             Cow::Owned(s.replace('\\', "\\\\").replace(' ', "\\ "))
     154                 :                         } else {
     155            4610 :                             Cow::Borrowed(s.as_str())
     156                 :                         }
     157            4612 :                     })
     158            1531 :                     .intersperse(Cow::Borrowed(" ")) // TODO: use impl from std once it's stabilized
     159            1531 :                     .collect::<String>(),
     160            1531 :             );
     161            1531 :         }
     162            1536 :         config
     163            1536 :     }
     164                 : 
     165                 :     /// Connect using postgres protocol with TLS disabled.
     166               5 :     pub async fn connect_no_tls(
     167               5 :         &self,
     168               5 :     ) -> Result<
     169               5 :         (
     170               5 :             tokio_postgres::Client,
     171               5 :             tokio_postgres::Connection<tokio_postgres::Socket, tokio_postgres::tls::NoTlsStream>,
     172               5 :         ),
     173               5 :         postgres::Error,
     174               5 :     > {
     175               5 :         self.to_tokio_postgres_config()
     176               5 :             .connect(postgres::NoTls)
     177              15 :             .await
     178               5 :     }
     179                 : }
     180                 : 
     181                 : impl fmt::Debug for PgConnectionConfig {
     182            4914 :     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
     183            4914 :         // We want `password: Some(REDACTED-STRING)`, not `password: Some("REDACTED-STRING")`
     184            4914 :         // so even if the password is `REDACTED-STRING` (quite unlikely) there is no confusion.
     185            4914 :         // Hence `format_args!()`, it returns a "safe" string which is not escaped by `Debug`.
     186            4914 :         f.debug_struct("PgConnectionConfig")
     187            4914 :             .field("host", &self.host)
     188            4914 :             .field("port", &self.port)
     189            4914 :             .field(
     190            4914 :                 "password",
     191            4914 :                 &self
     192            4914 :                     .password
     193            4914 :                     .as_ref()
     194            4914 :                     .map(|_| format_args!("REDACTED-STRING")),
     195            4914 :             )
     196            4914 :             .finish()
     197            4914 :     }
     198                 : }
     199                 : 
     200                 : #[cfg(test)]
     201                 : mod tests_pg_connection_config {
     202                 :     use crate::PgConnectionConfig;
     203                 :     use once_cell::sync::Lazy;
     204                 :     use url::Host;
     205                 : 
     206               3 :     static STUB_HOST: Lazy<Host> = Lazy::new(|| Host::Domain("stub.host.example".to_owned()));
     207                 : 
     208               1 :     #[test]
     209               1 :     fn test_no_password() {
     210               1 :         let cfg = PgConnectionConfig::new_host_port(STUB_HOST.clone(), 123);
     211               1 :         assert_eq!(cfg.host(), &*STUB_HOST);
     212               1 :         assert_eq!(cfg.port(), 123);
     213               1 :         assert_eq!(cfg.raw_address(), "stub.host.example:123");
     214               1 :         assert_eq!(
     215               1 :             format!("{:?}", cfg),
     216               1 :             "PgConnectionConfig { host: Domain(\"stub.host.example\"), port: 123, password: None }"
     217               1 :         );
     218               1 :     }
     219                 : 
     220               1 :     #[test]
     221               1 :     fn test_ipv6() {
     222               1 :         // May be a special case because hostname contains a colon.
     223               1 :         let cfg = PgConnectionConfig::new_host_port(Host::parse("[::1]").unwrap(), 123);
     224               1 :         assert_eq!(
     225               1 :             cfg.host(),
     226               1 :             &Host::<String>::Ipv6(std::net::Ipv6Addr::LOCALHOST)
     227               1 :         );
     228               1 :         assert_eq!(cfg.port(), 123);
     229               1 :         assert_eq!(cfg.raw_address(), "[::1]:123");
     230               1 :         assert_eq!(
     231               1 :             format!("{:?}", cfg),
     232               1 :             "PgConnectionConfig { host: Ipv6(::1), port: 123, password: None }"
     233               1 :         );
     234               1 :     }
     235                 : 
     236               1 :     #[test]
     237               1 :     fn test_with_password() {
     238               1 :         let cfg = PgConnectionConfig::new_host_port(STUB_HOST.clone(), 123)
     239               1 :             .set_password(Some("password".to_owned()));
     240               1 :         assert_eq!(cfg.host(), &*STUB_HOST);
     241               1 :         assert_eq!(cfg.port(), 123);
     242               1 :         assert_eq!(cfg.raw_address(), "stub.host.example:123");
     243               1 :         assert_eq!(
     244               1 :             format!("{:?}", cfg),
     245               1 :             "PgConnectionConfig { host: Domain(\"stub.host.example\"), port: 123, password: Some(REDACTED-STRING) }"
     246               1 :         );
     247               1 :     }
     248                 : 
     249               1 :     #[test]
     250               1 :     fn test_with_options() {
     251               1 :         let cfg = PgConnectionConfig::new_host_port(STUB_HOST.clone(), 123).extend_options([
     252               1 :             "hello",
     253               1 :             "world",
     254               1 :             "with space",
     255               1 :             "and \\ backslashes",
     256               1 :         ]);
     257               1 :         assert_eq!(cfg.host(), &*STUB_HOST);
     258               1 :         assert_eq!(cfg.port(), 123);
     259               1 :         assert_eq!(cfg.raw_address(), "stub.host.example:123");
     260               1 :         assert_eq!(
     261               1 :             cfg.to_tokio_postgres_config().get_options(),
     262               1 :             Some("hello world with\\ space and\\ \\\\\\ backslashes")
     263               1 :         );
     264               1 :     }
     265                 : }
        

Generated by: LCOV version 2.1-beta