use std::{
path::Path,
process::{Command, Stdio},
};
use bstr::{BString, ByteSlice, ByteVec};
use crate::{Program, helper};
fn external_name_command(
git_program: &Path,
name_and_args: &bstr::BStr,
action: &helper::Action,
) -> std::process::Command {
let git_program = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(git_program));
let mut args = gix_quote::single(git_program.as_ref());
args.push_str(" credential-");
args.push_str(name_and_args);
gix_command::prepare(gix_path::from_bstr(args.as_bstr()).into_owned())
.arg(action.as_arg(true))
.command_may_be_shell_script_allow_manual_argument_splitting()
.into()
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Kind {
Builtin,
ExternalName {
name_and_args: BString,
},
ExternalPath {
path_and_args: BString,
},
ExternalShellScript(BString),
}
impl Program {
pub fn from_kind(kind: Kind) -> Self {
Program {
kind,
child: None,
stderr: true,
}
}
pub fn from_custom_definition(input: impl Into<BString>) -> Self {
fn from_custom_definition_inner(mut input: BString) -> Program {
let kind = if input.starts_with(b"!") {
input.remove(0);
Kind::ExternalShellScript(input)
} else {
let path = gix_path::from_bstr(
input
.find_byte(b' ')
.map_or(input.as_slice(), |pos| &input[..pos])
.as_bstr(),
);
if gix_path::is_absolute(path) {
Kind::ExternalPath { path_and_args: input }
} else {
Kind::ExternalName { name_and_args: input }
}
};
Program {
kind,
child: None,
stderr: true,
}
}
from_custom_definition_inner(input.into())
}
pub fn to_command(&self, action: &helper::Action) -> std::process::Command {
let git_program = gix_path::env::exe_invocation();
let mut cmd = match &self.kind {
Kind::Builtin => {
let mut cmd = Command::from(gix_command::prepare(git_program));
cmd.arg("credential").arg(action.as_arg(false));
cmd
}
Kind::ExternalName { name_and_args } => external_name_command(git_program, name_and_args.as_bstr(), action),
Kind::ExternalShellScript(for_shell)
| Kind::ExternalPath {
path_and_args: for_shell,
} => gix_command::prepare(gix_path::from_bstr(for_shell.as_bstr()).as_ref())
.command_may_be_shell_script()
.arg(action.as_arg(true))
.into(),
};
cmd.stdin(Stdio::piped())
.stdout(if action.expects_output() {
Stdio::piped()
} else {
Stdio::null()
})
.stderr(if self.stderr { Stdio::inherit() } else { Stdio::null() });
cmd
}
}
impl Program {
pub fn suppress_stderr(mut self) -> Self {
self.stderr = false;
self
}
}
impl Program {
pub(crate) fn start(
&mut self,
action: &helper::Action,
) -> std::io::Result<(std::process::ChildStdin, Option<std::process::ChildStdout>)> {
assert!(self.child.is_none(), "BUG: must not call `start()` twice");
let mut cmd = self.to_command(action);
gix_trace::debug!(cmd = ?cmd, "launching credential helper");
let mut child = cmd.spawn()?;
let stdin = child.stdin.take().expect("stdin to be configured");
let stdout = child.stdout.take();
self.child = child.into();
Ok((stdin, stdout))
}
pub(crate) fn finish(&mut self) -> std::io::Result<()> {
let mut child = self.child.take().expect("Call `start()` before calling finish()");
let status = child.wait()?;
if status.success() {
Ok(())
} else {
Err(std::io::Error::other(format!(
"Credentials helper program failed with status code {:?}",
status.code()
)))
}
}
}
pub mod main;
pub use main::function::main;
#[cfg(test)]
mod tests {
use std::{ffi::OsStr, path::Path};
use crate::helper;
#[test]
fn git_program_with_spaces_is_quoted_in_external_name_shell_scripts() {
let cmd = super::external_name_command(
Path::new(r"C:\Program Files\Git\mingw64\bin\git.exe"),
"manager --config=~/credentials".into(),
&helper::Action::Get(Default::default()),
);
let script = cmd
.get_args()
.skip_while(|arg| *arg != OsStr::new("-c"))
.nth(1)
.expect("a shell invocation has its script after the '-c' argument");
assert_eq!(
script,
OsStr::new(if cfg!(windows) {
r#"'C:/Program Files/Git/mingw64/bin/git.exe' credential-manager --config=~/credentials "$@""#
} else {
r#"'C:\Program Files\Git\mingw64\bin\git.exe' credential-manager --config=~/credentials "$@""#
}),
"the Git executable is a single shell token even when its path contains spaces"
);
}
}