Skip to main content

gix_credentials/program/
mod.rs

1use std::{
2    path::Path,
3    process::{Command, Stdio},
4};
5
6use bstr::{BString, ByteSlice, ByteVec};
7
8use crate::{Program, helper};
9
10/// Correctly quotes `git_progarm`, while assuming `name_and_args` are good to be put into a script.
11fn 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/// The kind of helper program to use.
27#[derive(Debug, Clone, Eq, PartialEq)]
28pub enum Kind {
29    /// The built-in `git credential` helper program, part of any `git` distribution.
30    Builtin,
31    /// A custom credentials helper, as identified just by the name with optional arguments
32    ExternalName {
33        /// The name like `foo` along with optional args, like `foo --arg --bar="a b"`, with arguments using `sh` shell quoting rules.
34        /// The program executed will be `git-credential-foo [args]` if `name_and_args` starts with `foo [args]`.
35        /// Note that a shell is only used if it's needed.
36        name_and_args: BString,
37    },
38    /// A custom credentials helper, as identified just by the absolute path to the program and optional arguments. The program is executed through a shell.
39    ExternalPath {
40        /// The absolute path to the executable, like `/path/to/exe` along with optional args, like `/path/to/exe --arg --bar="a b"`, with arguments using `sh`
41        /// shell quoting rules.
42        path_and_args: BString,
43    },
44    /// A script to execute with `sh`.
45    ExternalShellScript(BString),
46}
47
48/// Initialization
49impl Program {
50    /// Create a new program of the given `kind`.
51    pub fn from_kind(kind: Kind) -> Self {
52        Program {
53            kind,
54            child: None,
55            stderr: true,
56        }
57    }
58
59    /// Parse the given input as per the custom helper definition, supporting `!<script>`, `name` and `/absolute/name`, the latter two
60    /// also support arguments which are ignored here.
61    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    /// Convert the program into the respective command, suitable to invoke `action`.
89    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
117/// Builder
118impl Program {
119    /// By default `stderr` of programs is inherited and typically displayed in the terminal.
120    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
156///
157pub 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}