376 lines · 16.5 KB · Rust
RawDownload
use std::{    borrow::Cow,    ffi::{OsStr, OsString},    path::{Path, PathBuf},    process::{Command, Stdio},}; use bstr::ByteSlice; use crate::{Context, Prepare, extract_interpreter, is_bare_command, split_paths, win_path_lookup}; /// Builderimpl Prepare {    /// If called, the command will be checked for characters that are typical for shell    /// scripts, and if found will use `sh` to execute it or whatever is set as    /// [`with_shell_program()`](Self::with_shell_program()).    ///    /// Commands are inspected as bytes, including non-UTF-8 commands on Unix. If the platform    /// cannot represent a command as bytes, it is invoked directly.    ///    /// If a shell is used, then arguments given here with [arg()](Self::arg) or    /// [args()](Self::args) will be substituted via `"$@"` if it's not already present in the    /// command.    ///    ///    /// The [`command_may_be_shell_script_allow_manual_argument_splitting()`](Self::command_may_be_shell_script_allow_manual_argument_splitting())    /// and [`command_may_be_shell_script_disallow_manual_argument_splitting()`](Self::command_may_be_shell_script_disallow_manual_argument_splitting())    /// methods also call this method.    ///    /// If neither this method nor [`with_shell()`](Self::with_shell()) is called, commands are    /// always executed verbatim and directly, without the use of a shell.    pub fn command_may_be_shell_script(mut self) -> Self {        self.use_shell = gix_path::os_str_into_bstr(&self.command)            .is_ok_and(|cmd| cmd.find_byteset(b"|&;<>()$`\\\"' \t\n*?[#~=%").is_some());        self    }     /// If called, unconditionally use a shell to execute the command and its arguments.    ///    /// This uses `sh` to execute it, or whatever is set as    /// [`with_shell_program()`](Self::with_shell_program()).    ///    /// Arguments given here with [arg()](Self::arg) or [args()](Self::args) will be    /// substituted via `"$@"` if it's not already present in the command.    ///    /// If neither this method nor    /// [`command_may_be_shell_script()`](Self::command_may_be_shell_script()) is called,    /// commands are always executed verbatim and directly, without the use of a shell. (But    /// see [`command_may_be_shell_script()`](Self::command_may_be_shell_script()) on other    /// methods that call that method.)    ///    /// We also disallow manual argument splitting    /// (see [`command_may_be_shell_script_disallow_manual_argument_splitting`](Self::command_may_be_shell_script_disallow_manual_argument_splitting()))    /// to assure a shell is indeed used, no matter what.    pub fn with_shell(mut self) -> Self {        self.use_shell = true;        self.allow_manual_arg_splitting = false;        self    }     /// Quote the command if it is run in a shell, so its path is left intact.    ///    /// This is only meaningful if the command has been arranged to run in a shell, either    /// unconditionally with [`with_shell()`](Self::with_shell()), or conditionally with    /// [`command_may_be_shell_script()`](Self::command_may_be_shell_script()).    ///    /// Note that this should not be used if the command is a script - quoting is only the    /// right choice if it's known to be a program path.    ///    /// Note also that this does not affect arguments passed with [arg()](Self::arg) or    /// [args()](Self::args), which do not have to be quoted by the *caller* because they are    /// passed as `"$@"` positional parameters (`"$1"`, `"$2"`, and so on).    pub fn with_quoted_command(mut self) -> Self {        self.quote_command = true;        self    }     /// Set the name or path to the shell `program` to use if a shell is to be used, to avoid    /// using the default shell which is `sh`.    ///    /// Note that shells that are not Bourne-style cannot be expected to work correctly,    /// because POSIX shell syntax is assumed when searching for and conditionally adding    /// `"$@"` to receive arguments, where applicable (and in the behaviour of    /// [`with_quoted_command()`](Self::with_quoted_command()), if called).    pub fn with_shell_program(mut self, program: impl Into<OsString>) -> Self {        self.shell_program = Some(program.into());        self    }     /// Unconditionally turn off using the shell when spawning the command.    ///    /// Note that not using the shell is the default. So an effective use of this method    /// is some time after [`command_may_be_shell_script()`](Self::command_may_be_shell_script())    /// or [`with_shell()`](Self::with_shell()) was called.    pub fn without_shell(mut self) -> Self {        self.use_shell = false;        self    }     /// Set additional `ctx` to be used when spawning the process.    ///    /// Note that this is a must for most kind of commands that `git` usually spawns, as at    /// least they need to know the correct Git repository to function.    pub fn with_context(mut self, ctx: Context) -> Self {        self.context = Some(ctx);        self    }     /// Like [`command_may_be_shell_script()`](Self::command_may_be_shell_script()), but try to    /// split arguments by hand if this can be safely done without a shell.    ///    /// This is useful on platforms where spawning processes is slow, or where many processes    /// have to be spawned in a row which should be sped up. Manual argument splitting is    /// enabled by default on Windows only.    ///    /// Note that this does *not* check for the use of possible shell builtins. Commands may    /// fail or behave differently if they are available as shell builtins and no corresponding    /// external command exists, or the external command behaves differently.    /// Leading shell assignment words are applied to the environment when followed by a command.    pub fn command_may_be_shell_script_allow_manual_argument_splitting(mut self) -> Self {        self.allow_manual_arg_splitting = true;        self.command_may_be_shell_script()    }     /// Like [`command_may_be_shell_script()`](Self::command_may_be_shell_script()), but don't    /// allow to bypass the shell even if manual argument splitting can be performed safely.    pub fn command_may_be_shell_script_disallow_manual_argument_splitting(mut self) -> Self {        self.allow_manual_arg_splitting = false;        self.command_may_be_shell_script()    }     /// Configure the process to use `stdio` for _stdin_.    pub fn stdin(mut self, stdio: Stdio) -> Self {        self.stdin = stdio;        self    }    /// Configure the process to use `stdio` for _stdout_.    pub fn stdout(mut self, stdio: Stdio) -> Self {        self.stdout = stdio;        self    }    /// Configure the process to use `stdio` for _stderr_.    pub fn stderr(mut self, stdio: Stdio) -> Self {        self.stderr = stdio;        self    }     /// Add `arg` to the list of arguments to call the command with.    pub fn arg(mut self, arg: impl Into<OsString>) -> Self {        self.args.push(arg.into());        self    }     /// Add `args` to the list of arguments to call the command with.    pub fn args(mut self, args: impl IntoIterator<Item = impl Into<OsString>>) -> Self {        self.args            .append(&mut args.into_iter().map(Into::into).collect::<Vec<_>>());        self    }     /// Add `key` with `value` to the environment of the spawned command.    pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {        self.env.push((key.into(), value.into()));        self    }} /// Finalizationimpl Prepare {    /// Spawn the command as configured.    pub fn spawn(self) -> std::io::Result<std::process::Child> {        let mut cmd = Command::from(self);        gix_trace::debug!(cmd = ?cmd);        cmd.spawn()    }} impl From<Prepare> for Command {    fn from(mut prep: Prepare) -> Command {        let mut inline_env = Vec::new();        let mut cmd = if prep.use_shell {            let split_args = prep                .allow_manual_arg_splitting                .then(|| {                    let command = gix_path::os_str_into_bstr(&prep.command).ok()?;                    if command.find_byteset(b"\\|&;<>()$`\n*?[#~%").is_none() {                        crate::parse::command_line(command).ok()                    } else {                        None                    }                })                .flatten();            match split_args {                Some(parsed) => {                    let mut cmd = if cfg!(windows) {                        windows_command(parsed.command, &prep.env, &parsed.env)                    } else {                        Command::new(parsed.command)                    };                    cmd.args(parsed.args);                    inline_env = parsed.env;                    cmd                }                None => {                    let mut cmd = match prep.shell_program {                        Some(shell) => Command::new(shell),                        None => gix_path::env::shell_command(),                    };                    // Passed as `command_name` after `-c <script>`; the shell uses it                    // as `$0`, which prefixes its own diagnostic messages. If the                    // shell path has no extractable basename — reachable only via                    // degenerate input like `""` or `/` — fall back to `_`, the                    // conventional placeholder for an unused `$0`, rather than                    // making a false claim about which shell is running.                    let arg0 = std::path::Path::new(cmd.get_program())                        .file_name()                        .unwrap_or(std::ffi::OsStr::new("_"))                        .to_os_string();                    cmd.arg("-c");                    if !prep.args.is_empty() {                        if !gix_path::os_str_into_bstr(&prep.command).is_ok_and(|cmd| cmd.contains_str("$@")) {                            if prep.quote_command                                && let Ok(command) = gix_path::os_str_into_bstr(&prep.command)                            {                                prep.command = gix_path::from_bstring(gix_quote::single(command)).into();                            }                            prep.command.push(r#" "$@""#);                        } else {                            gix_trace::debug!(                                r#"Will not add '"$@"' to '{:?}' as it seems to contain '$@' already"#,                                prep.command                            );                        }                    }                    cmd.arg(prep.command);                    cmd.arg(arg0);                    cmd                }            }        } else if cfg!(windows) {            windows_command(prep.command, &prep.env, &[])        } else {            Command::new(prep.command)        };        // We never want to have terminals pop-up on Windows if this runs from a GUI application.        #[cfg(windows)]        {            use std::os::windows::process::CommandExt;            const CREATE_NO_WINDOW: u32 = 0x08000000;            cmd.creation_flags(CREATE_NO_WINDOW);        }        cmd.stdin(prep.stdin)            .stdout(prep.stdout)            .stderr(prep.stderr)            .envs(prep.env)            .args(prep.args);        if let Some(ctx) = prep.context {            if let Some(git_dir) = ctx.git_dir {                cmd.env("GIT_DIR", &git_dir);            }            if let Some(worktree_dir) = ctx.worktree_dir {                cmd.env("GIT_WORK_TREE", worktree_dir);            }            if let Some(value) = ctx.no_replace_objects {                cmd.env("GIT_NO_REPLACE_OBJECTS", usize::from(value).to_string());            }            if let Some(namespace) = ctx.ref_namespace {                cmd.env("GIT_NAMESPACE", gix_path::from_bstring(namespace));            }            if let Some(value) = ctx.literal_pathspecs {                cmd.env("GIT_LITERAL_PATHSPECS", usize::from(value).to_string());            }            if let Some(value) = ctx.glob_pathspecs {                cmd.env(                    if value {                        "GIT_GLOB_PATHSPECS"                    } else {                        "GIT_NOGLOB_PATHSPECS"                    },                    "1",                );            }            if let Some(value) = ctx.icase_pathspecs {                cmd.env("GIT_ICASE_PATHSPECS", usize::from(value).to_string());            }            if let Some(stderr) = ctx.stderr {                cmd.stderr(if stderr { Stdio::inherit() } else { Stdio::null() });            }        }        cmd.envs(inline_env);        cmd    }} /// Create a Windows command using Git-compatible `PATH` lookup and shebang dispatch.////// The last `PATH` in `inline_env` overrides the last one in `env`, which overrides the inherited value (of this process)./// The selected `PATH` is searched in order. A resolved shebang script is launched through its interpreter, ignoring shebang/// arguments. If an explicit `PATH` does not resolve a bare command, the missing program remains anchored in its first entry/// so Rust's broader Windows lookup cannot find it elsewhere.fn windows_command(command: OsString, env: &[(OsString, OsString)], inline_env: &[(String, OsString)]) -> Command {    let explicit_joined_paths = inline_env        .iter()        .rev()        .find(|(name, _)| name.eq_ignore_ascii_case("PATH"))        .map(|(_, value)| value.as_os_str())        .or_else(|| {            env.iter()                .rev()                .find(|(name, _)| name.to_str().is_some_and(|name| name.eq_ignore_ascii_case("PATH")))                .map(|(_, value)| value.as_os_str())        });    let joined_paths = explicit_joined_paths        .map(Cow::Borrowed)        .or_else(|| std::env::var_os("PATH").map(Cow::Owned));    let looked_up = joined_paths        .as_deref()        .and_then(|joined_paths| win_path_lookup(command.as_ref(), joined_paths));    let program: Cow<'_, Path> = match (looked_up, explicit_joined_paths) {        // Use the manually resolved path.        (Some(program), _) => Cow::Owned(program),        // An explicit `PATH` miss must not fall back to `std::process::Command` broader Windows search.        (None, Some(explicit_joined_paths)) if is_bare_command(Path::new(&command)) => {            Cow::Owned(prevent_further_path_lookup(command.as_ref(), explicit_joined_paths))        }        // Preserve non-bare commands and let `std::process::Command` resolve bare commands without an explicit `PATH`.        (None, _) => Cow::Borrowed(command.as_ref()),    };    if let Some(shebang) = extract_interpreter(program.as_ref()) {        let mut cmd = Command::new(shebang.interpreter);        // Git for Windows ignores shebang arguments and passes only the script path.        cmd.arg(program.as_ref());        cmd    } else {        match program {            // Process lookup happens before the child's environment is installed, so an explicitly            // configured PATH must be handled here for ordinary executables as well.            Cow::Owned(program) if explicit_joined_paths.is_some() => Command::new(program),            _ => Command::new(command),        }    }} /// Represent the failed lookup of `command` in an explicitly assigned `PATH` without permitting another search.////// `joined_paths` is the complete value of the explicit `PATH`, not one of its entries. The first non-empty entry is/// joined with `command`, producing a path that Rust's Windows resolver will not look up elsewhere. If there is no such/// entry, a trailing separator makes `command` invalid instead.fn prevent_further_path_lookup(command: &Path, joined_paths: &OsStr) -> PathBuf {    if let Some(mut root) = split_paths(joined_paths).next() {        root.push(command);        root    } else {        let mut missing = command.to_owned();        // A trailing separator makes the program invalid without allowing Rust to search ambient locations.        missing.push("");        missing    }} #[cfg(test)]mod tests {    use super::*;     #[test]    fn explicit_path_lookup_failure_stays_within_that_path() -> gix_testtools::Result {        let joined_paths = std::env::join_paths(["", "not/a/real/path", "also/not/real"])?;        let cmd = windows_command("missing.exe".into(), &[], &[("PATH".into(), joined_paths)]);        assert_eq!(            cmd.get_program(),            std::path::Path::new("not/a/real/path/missing.exe"),            "an inline PATH miss must not leave a bare program for Rust to resolve elsewhere"        );        Ok(())    }}