diff --git a/Cargo.lock b/Cargo.lock index e864cc123de..a5389b138c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2135,6 +2135,7 @@ dependencies = [ "git-hash 0.10.1", "git-pack", "git-packetline", + "git-quote 0.4.0", "git-sec 0.6.1", "git-url", "maybe-async", diff --git a/git-quote/src/ansi_c.rs b/git-quote/src/ansi_c.rs index d91f93b280e..f271a4a41bb 100644 --- a/git-quote/src/ansi_c.rs +++ b/git-quote/src/ansi_c.rs @@ -1,9 +1,12 @@ +/// pub mod undo { use bstr::{BStr, BString}; use quick_error::quick_error; quick_error! { + /// The error returned by [ansi_c][crate::ansi_c::undo()]. #[derive(Debug)] + #[allow(missing_docs)] pub enum Error { InvalidInput { message: String, input: BString } { display("{}: {:?}", message, input) diff --git a/git-quote/src/lib.rs b/git-quote/src/lib.rs index 72dbe996778..b86336b3b0b 100644 --- a/git-quote/src/lib.rs +++ b/git-quote/src/lib.rs @@ -1,5 +1,9 @@ -#![deny(rust_2018_idioms)] +//! Provides functions to quote and possibly unquote strings with different quoting styles. +#![deny(rust_2018_idioms, missing_docs)] #![forbid(unsafe_code)] /// pub mod ansi_c; + +mod single; +pub use single::single; diff --git a/git-quote/src/single.rs b/git-quote/src/single.rs new file mode 100644 index 00000000000..648b49f57e1 --- /dev/null +++ b/git-quote/src/single.rs @@ -0,0 +1,21 @@ +use bstr::{BStr, BString, ByteSlice}; + +/// Transforms the given `value` to be suitable for use as an argument for Bourne shells by wrapping it into single quotes. +/// +/// Every single-quote `'` is escaped with `\'`, every exclamation mark `!` is escaped with `\!`, and the entire string is enclosed +/// in single quotes. +pub fn single(mut value: &BStr) -> BString { + let mut quoted = BString::new(b"'".to_vec()); + + while let Some(pos) = value.find_byteset(b"!'") { + quoted.extend_from_slice(&value[..pos]); + quoted.push(b'\\'); + quoted.push(value[pos]); + + value = &value[pos + 1..]; + } + + quoted.extend_from_slice(value); + quoted.push(b'\''); + quoted +} diff --git a/git-quote/tests/quote.rs b/git-quote/tests/quote.rs index 370f5baa1d5..06464365aa0 100644 --- a/git-quote/tests/quote.rs +++ b/git-quote/tests/quote.rs @@ -1,3 +1,39 @@ +mod single { + use git_quote::single; + + #[test] + fn empty() { + assert_eq!(single("".into()), "''"); + } + + #[test] + fn unquoted_becomes_quoted() { + assert_eq!(single("a".into()), "'a'"); + assert_eq!(single("a b".into()), "'a b'"); + assert_eq!(single("a\nb".into()), "'a\nb'", "newlines play no role"); + } + + #[test] + fn existing_exclamation_mark_gets_escaped() { + assert_eq!(single(r"a!b".into()), r"'a\!b'"); + assert_eq!(single(r"!".into()), r"'\!'"); + assert_eq!(single(r"\!".into()), r"'\\!'"); + } + + #[test] + fn existing_quote_gets_escaped() { + assert_eq!(single(r"a'b".into()), r"'a\'b'"); + assert_eq!(single(r"'".into()), r"'\''"); + assert_eq!(single(r"'\''".into()), r"'\'\\'\''"); + } + + #[test] + fn complex() { + let expected = "'\0cmd `arg` $var\\\\'ring\\// arg \"quoted\\!\"'"; + assert_eq!(single("\0cmd `arg` $var\\'ring\\// arg \"quoted!\"".into()), expected); + } +} + mod ansi_c { mod undo { use bstr::ByteSlice; diff --git a/git-transport/Cargo.toml b/git-transport/Cargo.toml index 45ebe296dd4..4393eaf8f2c 100644 --- a/git-transport/Cargo.toml +++ b/git-transport/Cargo.toml @@ -59,6 +59,7 @@ git-url = { version = "^0.13.1", path = "../git-url" } git-sec = { version = "^0.6.1", path = "../git-sec" } git-packetline = { version = "^0.14.1", path = "../git-packetline" } git-credentials = { version = "^0.9.0", path = "../git-credentials", optional = true } +git-quote = { version = "^0.4.0", path = "../git-quote" } serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"]} bstr = { version = "1.0.1", default-features = false, features = ["std", "unicode"] } diff --git a/git-transport/src/client/blocking_io/file.rs b/git-transport/src/client/blocking_io/file.rs index c580cf9e28b..8f866de8f92 100644 --- a/git-transport/src/client/blocking_io/file.rs +++ b/git-transport/src/client/blocking_io/file.rs @@ -213,10 +213,13 @@ impl client::Transport for SpawnProcessOnDemand { }; cmd.stdin = Stdio::piped(); cmd.stdout = Stdio::piped(); - if self.ssh_cmd.is_some() { + let repo_path = if self.ssh_cmd.is_some() { cmd.args.push(service.as_str().into()); - } - cmd.args.push(self.path.to_os_str_lossy().into_owned()); + git_quote::single(self.path.as_ref()).to_os_str_lossy().into_owned() + } else { + self.path.to_os_str_lossy().into_owned() + }; + cmd.args.push(repo_path); let mut cmd = std::process::Command::from(cmd); for env_to_remove in ENV_VARS_TO_REMOVE {