gix_credentials/program/
mod.rs1use std::{
2 path::Path,
3 process::{Command, Stdio},
4};
5
6use bstr::{BString, ByteSlice, ByteVec};
7
8use crate::{Program, helper};
9
10fn external_name_command(
12 git_program: &Path,
13 name_and_args: &bstr::BStr,
14 action: &helper::Action,
15) -> std::process::Command {
16 let git_program = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(git_program));
17 let mut args = gix_quote::single(git_program.as_ref());
18 args.push_str(" credential-");
19 args.push_str(name_and_args);
20 gix_command::prepare(gix_path::from_bstr(args.as_bstr()).into_owned())
21 .arg(action.as_arg(true))
22 .command_may_be_shell_script_allow_manual_argument_splitting()
23 .into()
24}
25
26#[derive(Debug, Clone, Eq, PartialEq)]
28pub enum Kind {
29 Builtin,
31 ExternalName {
33 name_and_args: BString,
37 },
38 ExternalPath {
40 path_and_args: BString,
43 },
44 ExternalShellScript(BString),
46}
47
48impl Program {
50 pub fn from_kind(kind: Kind) -> Self {
52 Program {
53 kind,
54 child: None,
55 stderr: true,
56 }
57 }
58
59 pub fn from_custom_definition(input: impl Into<BString>) -> Self {
62 fn from_custom_definition_inner(mut input: BString) -> Program {
63 let kind = if input.starts_with(b"!") {
64 input.remove(0);
65 Kind::ExternalShellScript(input)
66 } else {
67 let path = gix_path::from_bstr(
68 input
69 .find_byte(b' ')
70 .map_or(input.as_slice(), |pos| &input[..pos])
71 .as_bstr(),
72 );
73 if gix_path::is_absolute(path) {
74 Kind::ExternalPath { path_and_args: input }
75 } else {
76 Kind::ExternalName { name_and_args: input }
77 }
78 };
79 Program {
80 kind,
81 child: None,
82 stderr: true,
83 }
84 }
85 from_custom_definition_inner(input.into())
86 }
87
88 pub fn to_command(&self, action: &helper::Action) -> std::process::Command {
90 let git_program = gix_path::env::exe_invocation();
91 let mut cmd = match &self.kind {
92 Kind::Builtin => {
93 let mut cmd = Command::from(gix_command::prepare(git_program));
94 cmd.arg("credential").arg(action.as_arg(false));
95 cmd
96 }
97 Kind::ExternalName { name_and_args } => external_name_command(git_program, name_and_args.as_bstr(), action),
98 Kind::ExternalShellScript(for_shell)
99 | Kind::ExternalPath {
100 path_and_args: for_shell,
101 } => gix_command::prepare(gix_path::from_bstr(for_shell.as_bstr()).as_ref())
102 .command_may_be_shell_script()
103 .arg(action.as_arg(true))
104 .into(),
105 };
106 cmd.stdin(Stdio::piped())
107 .stdout(if action.expects_output() {
108 Stdio::piped()
109 } else {
110 Stdio::null()
111 })
112 .stderr(if self.stderr { Stdio::inherit() } else { Stdio::null() });
113 cmd
114 }
115}
116
117impl Program {
119 pub fn suppress_stderr(mut self) -> Self {
121 self.stderr = false;
122 self
123 }
124}
125
126impl Program {
127 pub(crate) fn start(
128 &mut self,
129 action: &helper::Action,
130 ) -> std::io::Result<(std::process::ChildStdin, Option<std::process::ChildStdout>)> {
131 assert!(self.child.is_none(), "BUG: must not call `start()` twice");
132 let mut cmd = self.to_command(action);
133 gix_trace::debug!(cmd = ?cmd, "launching credential helper");
134 let mut child = cmd.spawn()?;
135 let stdin = child.stdin.take().expect("stdin to be configured");
136 let stdout = child.stdout.take();
137
138 self.child = child.into();
139 Ok((stdin, stdout))
140 }
141
142 pub(crate) fn finish(&mut self) -> std::io::Result<()> {
143 let mut child = self.child.take().expect("Call `start()` before calling finish()");
144 let status = child.wait()?;
145 if status.success() {
146 Ok(())
147 } else {
148 Err(std::io::Error::other(format!(
149 "Credentials helper program failed with status code {:?}",
150 status.code()
151 )))
152 }
153 }
154}
155
156pub mod main;
158pub use main::function::main;
159
160#[cfg(test)]
161mod tests {
162 use std::{ffi::OsStr, path::Path};
163
164 use crate::helper;
165
166 #[test]
167 fn git_program_with_spaces_is_quoted_in_external_name_shell_scripts() {
168 let cmd = super::external_name_command(
169 Path::new(r"C:\Program Files\Git\mingw64\bin\git.exe"),
170 "manager --config=~/credentials".into(),
171 &helper::Action::Get(Default::default()),
172 );
173 let script = cmd
174 .get_args()
175 .skip_while(|arg| *arg != OsStr::new("-c"))
176 .nth(1)
177 .expect("a shell invocation has its script after the '-c' argument");
178
179 assert_eq!(
180 script,
181 OsStr::new(if cfg!(windows) {
182 r#"'C:/Program Files/Git/mingw64/bin/git.exe' credential-manager --config=~/credentials "$@""#
183 } else {
184 r#"'C:\Program Files\Git\mingw64\bin\git.exe' credential-manager --config=~/credentials "$@""#
185 }),
186 "the Git executable is a single shell token even when its path contains spaces"
187 );
188 }
189}