From 2266240a77798b3150939542b33aa1c5e64cd4f7 Mon Sep 17 00:00:00 2001 From: "Kai A. Hiller" Date: Fri, 21 Mar 2025 19:41:56 +0100 Subject: [PATCH 1/5] Escape PostgreSQL options --- sqlx-postgres/src/options/mod.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs index a0b222606a..8726c3d27c 100644 --- a/sqlx-postgres/src/options/mod.rs +++ b/sqlx-postgres/src/options/mod.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; use std::env::var; -use std::fmt::{Display, Write}; +use std::fmt::Display; use std::path::{Path, PathBuf}; pub use ssl_mode::PgSslMode; @@ -515,7 +515,16 @@ impl PgConnectOptions { options_str.push(' '); } - write!(options_str, "-c {k}={v}").expect("failed to write an option to the string"); + // Escape options per + // https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-OPTIONS + options_str.push_str("-c "); + for c in format!("{k}={v}").chars() { + match c { + '\\' => options_str.push_str("\\\\"), + ' ' => options_str.push_str("\\ "), + c => options_str.push(c), + }; + } } self } @@ -683,6 +692,12 @@ fn test_options_formatting() { options.options, Some("-c geqo=off -c statement_timeout=5min".to_string()) ); + // https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-OPTIONS + let options = PgConnectOptions::new().options([("passfile", "/back\\slash/ and\\ spaces")]); + assert_eq!( + options.options, + Some("-c passfile=/back\\\\slash/\\ and\\\\\\ spaces".to_string()) + ); let options = PgConnectOptions::new(); assert_eq!(options.options, None); } From 72697b3184416b87b9fb7b58a75db39aff37290a Mon Sep 17 00:00:00 2001 From: V02460 Date: Sun, 23 Mar 2025 14:08:21 +0100 Subject: [PATCH 2/5] Use raw string literals in test case Co-authored-by: Austin Bonander --- sqlx-postgres/src/options/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs index 8726c3d27c..c9b0d9f43c 100644 --- a/sqlx-postgres/src/options/mod.rs +++ b/sqlx-postgres/src/options/mod.rs @@ -693,10 +693,10 @@ fn test_options_formatting() { Some("-c geqo=off -c statement_timeout=5min".to_string()) ); // https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-OPTIONS - let options = PgConnectOptions::new().options([("passfile", "/back\\slash/ and\\ spaces")]); + let options = PgConnectOptions::new().options([("passfile", r"/back\slash/ and\ spaces")]); assert_eq!( options.options, - Some("-c passfile=/back\\\\slash/\\ and\\\\\\ spaces".to_string()) + Some(r"-c passfile=/back\\slash/\ and\\\ spaces".to_string()) ); let options = PgConnectOptions::new(); assert_eq!(options.options, None); From 8d24fd833cdc4830290cc0ee15340ca891dfbad1 Mon Sep 17 00:00:00 2001 From: "Kai A. Hiller" Date: Sun, 23 Mar 2025 14:12:28 +0100 Subject: [PATCH 3/5] Document escaping behavior for options --- sqlx-postgres/src/options/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs index c9b0d9f43c..406c52937c 100644 --- a/sqlx-postgres/src/options/mod.rs +++ b/sqlx-postgres/src/options/mod.rs @@ -495,6 +495,9 @@ impl PgConnectOptions { /// Set additional startup options for the connection as a list of key-value pairs. /// + /// Escapes the options’ backslash and space characters as per + /// https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-OPTIONS + /// /// # Example /// /// ```rust From bc06c4efc0f6dddbf8cbcd2c9e1d18c66591cd82 Mon Sep 17 00:00:00 2001 From: "Kai A. Hiller" Date: Mon, 24 Mar 2025 21:14:38 +0100 Subject: [PATCH 4/5] Remove heap allocations for options formatting --- sqlx-postgres/src/options/mod.rs | 58 ++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs index 406c52937c..ada25f5c1c 100644 --- a/sqlx-postgres/src/options/mod.rs +++ b/sqlx-postgres/src/options/mod.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; use std::env::var; -use std::fmt::Display; +use std::fmt::{self, Display, Write}; use std::path::{Path, PathBuf}; pub use ssl_mode::PgSslMode; @@ -518,16 +518,8 @@ impl PgConnectOptions { options_str.push(' '); } - // Escape options per - // https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-OPTIONS options_str.push_str("-c "); - for c in format!("{k}={v}").chars() { - match c { - '\\' => options_str.push_str("\\\\"), - ' ' => options_str.push_str("\\ "), - c => options_str.push(c), - }; - } + write!(PgOptionsWriteEscaped(options_str), "{k}={v}").ok(); } self } @@ -681,6 +673,39 @@ fn default_host(port: u16) -> String { "localhost".to_owned() } +/// Writer that escapes passed-in PostgreSQL options. +/// +/// Escapes backslashes and spaces with an additional backslash according to +/// https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-OPTIONS +#[derive(Debug)] +struct PgOptionsWriteEscaped<'a>(&'a mut String); + +impl Write for PgOptionsWriteEscaped<'_> { + fn write_str(&mut self, s: &str) -> fmt::Result { + let mut span_start = 0; + + for (span_end, matched) in s.match_indices([' ', '\\']) { + write!(self.0, r"{}\{matched}", &s[span_start..span_end])?; + span_start = span_end + matched.len(); + } + + // Write the rest of the string after the last match, or all of it if no matches + self.0.push_str(&s[span_start..]); + + Ok(()) + } + + fn write_char(&mut self, ch: char) -> fmt::Result { + if matches!(ch, ' ' | '\\') { + self.0.push('\\'); + } + + self.0.push(ch); + + Ok(()) + } +} + #[test] fn test_options_formatting() { let options = PgConnectOptions::new().options([("geqo", "off")]); @@ -704,3 +729,16 @@ fn test_options_formatting() { let options = PgConnectOptions::new(); assert_eq!(options.options, None); } + +#[test] +fn test_pg_write_escaped() { + let mut buf = String::new(); + let mut x = PgOptionsWriteEscaped(&mut buf); + x.write_str("x").unwrap(); + x.write_str("").unwrap(); + x.write_char('\\').unwrap(); + x.write_str("y \\").unwrap(); + x.write_char(' ').unwrap(); + x.write_char('z').unwrap(); + assert_eq!(buf, r"x\\y\ \\\ z"); +} From 6da7b211ebfb1f674a68d392046c70bd3f0a2466 Mon Sep 17 00:00:00 2001 From: "Kai A. Hiller" Date: Tue, 15 Apr 2025 11:46:44 +0200 Subject: [PATCH 5/5] Use an actual config option for the test case --- sqlx-postgres/src/options/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs index ada25f5c1c..6ec872f569 100644 --- a/sqlx-postgres/src/options/mod.rs +++ b/sqlx-postgres/src/options/mod.rs @@ -721,10 +721,11 @@ fn test_options_formatting() { Some("-c geqo=off -c statement_timeout=5min".to_string()) ); // https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-OPTIONS - let options = PgConnectOptions::new().options([("passfile", r"/back\slash/ and\ spaces")]); + let options = + PgConnectOptions::new().options([("application_name", r"/back\slash/ and\ spaces")]); assert_eq!( options.options, - Some(r"-c passfile=/back\\slash/\ and\\\ spaces".to_string()) + Some(r"-c application_name=/back\\slash/\ and\\\ spaces".to_string()) ); let options = PgConnectOptions::new(); assert_eq!(options.options, None);