From 03647fc4fce6a3204368ea41bd9662f236d4a799 Mon Sep 17 00:00:00 2001 From: kas Date: Wed, 1 Feb 2023 17:53:01 +0100 Subject: [PATCH 1/3] feat: new function `single` for single-quoting strings. This function takes a string and transforms it into a form safe for consumption by Bourne compatible shells. --- git-quote/src/lib.rs | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/git-quote/src/lib.rs b/git-quote/src/lib.rs index 72dbe996778..7a2574362ce 100644 --- a/git-quote/src/lib.rs +++ b/git-quote/src/lib.rs @@ -1,5 +1,50 @@ #![deny(rust_2018_idioms)] #![forbid(unsafe_code)] +use bstr::{BStr, BString, ByteSlice}; + /// pub mod ansi_c; + +/// Transforms the given value to be suitable for use as an argument for Bourne shells by wrapping in single quotes +pub fn to_single_quoted(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.extend_from_slice(b"'\\"); + quoted.push(value[pos]); + quoted.push(b'\''); + + value = &value[pos + 1..]; + } + + quoted.extend_from_slice(value); + quoted.push(b'\''); + quoted +} + +#[cfg(test)] +mod tests { + use crate::to_single_quoted; + use bstr::BStr; + + #[test] + fn quoted_strings() { + assert_eq!(to_single_quoted("my cool string".into()), "'my cool string'"); + assert_eq!(to_single_quoted(r"'\''".into()), BStr::new(r"''\''\'\'''\'''")); + assert_eq!( + to_single_quoted("my 'quoted' string".into()), + BStr::new(r"'my '\''quoted'\'' string'") + ); + assert_eq!(to_single_quoted(r"'\!'".into()), BStr::new(r"''\''\'\!''\'''")); + assert_eq!( + to_single_quoted("my excited string!!!".into()), + BStr::new(r"'my excited string'\!''\!''\!''") + ); + assert_eq!( + to_single_quoted("\0my `even` ~cooler~ $t\\'ring\\// with \"quotes!\"".into()), + BStr::new("'\0my `even` ~cooler~ $t\\'\\''ring\\// with \"quotes'\\!'\"'") + ); + } +} From acb4c170395779d1c34d74951121acd5c5b19c65 Mon Sep 17 00:00:00 2001 From: kas Date: Wed, 1 Feb 2023 17:55:32 +0100 Subject: [PATCH 2/3] fix(git-transport): Use single quotes for ssh path arg Git uses this method of quoting args for SSH transport too Some Git SSH servers require this method to be used (eg. BitBucket) --- Cargo.lock | 1 + git-transport/Cargo.toml | 1 + git-transport/src/client/blocking_io/file.rs | 7 ++++++- 3 files changed, 8 insertions(+), 1 deletion(-) 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-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..77e07e088d9 100644 --- a/git-transport/src/client/blocking_io/file.rs +++ b/git-transport/src/client/blocking_io/file.rs @@ -10,6 +10,8 @@ use std::{ use bstr::{io::BufReadExt, BStr, BString, ByteSlice}; +use git_quote::to_single_quoted; + use crate::{ client::{self, git, ssh, MessageKind, RequestWriter, SetServiceResponse, WriteMode}, Protocol, Service, @@ -215,8 +217,11 @@ impl client::Transport for SpawnProcessOnDemand { cmd.stdout = Stdio::piped(); if self.ssh_cmd.is_some() { cmd.args.push(service.as_str().into()); + cmd.args + .push(to_single_quoted(self.path.as_ref()).to_os_str_lossy().into_owned()); + } else { + cmd.args.push(self.path.to_os_str_lossy().into_owned()); } - cmd.args.push(self.path.to_os_str_lossy().into_owned()); let mut cmd = std::process::Command::from(cmd); for env_to_remove in ENV_VARS_TO_REMOVE { From deed1f1a81669c53475a88a504f593884d179363 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 2 Feb 2023 08:35:40 +0000 Subject: [PATCH 3/3] refactor - adapt to crate layout of git-quote - add more tests --- git-quote/src/ansi_c.rs | 3 ++ git-quote/src/lib.rs | 49 ++------------------ git-quote/src/single.rs | 21 +++++++++ git-quote/tests/quote.rs | 36 ++++++++++++++ git-transport/src/client/blocking_io/file.rs | 12 ++--- 5 files changed, 69 insertions(+), 52 deletions(-) create mode 100644 git-quote/src/single.rs 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 7a2574362ce..b86336b3b0b 100644 --- a/git-quote/src/lib.rs +++ b/git-quote/src/lib.rs @@ -1,50 +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)] -use bstr::{BStr, BString, ByteSlice}; - /// pub mod ansi_c; -/// Transforms the given value to be suitable for use as an argument for Bourne shells by wrapping in single quotes -pub fn to_single_quoted(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.extend_from_slice(b"'\\"); - quoted.push(value[pos]); - quoted.push(b'\''); - - value = &value[pos + 1..]; - } - - quoted.extend_from_slice(value); - quoted.push(b'\''); - quoted -} - -#[cfg(test)] -mod tests { - use crate::to_single_quoted; - use bstr::BStr; - - #[test] - fn quoted_strings() { - assert_eq!(to_single_quoted("my cool string".into()), "'my cool string'"); - assert_eq!(to_single_quoted(r"'\''".into()), BStr::new(r"''\''\'\'''\'''")); - assert_eq!( - to_single_quoted("my 'quoted' string".into()), - BStr::new(r"'my '\''quoted'\'' string'") - ); - assert_eq!(to_single_quoted(r"'\!'".into()), BStr::new(r"''\''\'\!''\'''")); - assert_eq!( - to_single_quoted("my excited string!!!".into()), - BStr::new(r"'my excited string'\!''\!''\!''") - ); - assert_eq!( - to_single_quoted("\0my `even` ~cooler~ $t\\'ring\\// with \"quotes!\"".into()), - BStr::new("'\0my `even` ~cooler~ $t\\'\\''ring\\// with \"quotes'\\!'\"'") - ); - } -} +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/src/client/blocking_io/file.rs b/git-transport/src/client/blocking_io/file.rs index 77e07e088d9..8f866de8f92 100644 --- a/git-transport/src/client/blocking_io/file.rs +++ b/git-transport/src/client/blocking_io/file.rs @@ -10,8 +10,6 @@ use std::{ use bstr::{io::BufReadExt, BStr, BString, ByteSlice}; -use git_quote::to_single_quoted; - use crate::{ client::{self, git, ssh, MessageKind, RequestWriter, SetServiceResponse, WriteMode}, Protocol, Service, @@ -215,13 +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(to_single_quoted(self.path.as_ref()).to_os_str_lossy().into_owned()); + git_quote::single(self.path.as_ref()).to_os_str_lossy().into_owned() } else { - cmd.args.push(self.path.to_os_str_lossy().into_owned()); - } + 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 {