|
| 1 | +use std::ffi::OsString; |
| 2 | +use std::path::{Path, PathBuf}; |
| 3 | + |
| 4 | +use once_cell::sync::Lazy; |
| 5 | + |
| 6 | +/// `usr`-like directory component names that MSYS2 may provide, other than for `/usr` itself. |
| 7 | +/// |
| 8 | +/// These are the values of the "Prefix" column of the "Environments" and "Legacy Environments" |
| 9 | +/// tables in the [MSYS2 Environments](https://www.msys2.org/docs/environments/) documentation, |
| 10 | +/// with the leading `/` separator removed, except that this does not list `usr` itself. |
| 11 | +/// |
| 12 | +/// On Windows, we prefer to use `sh` as provided by Git for Windows, when present. To find it, we |
| 13 | +/// run `git --exec-path` to get a path that is usually `<platform>/libexec/git-core` in the Git |
| 14 | +/// for Windows installation, where `<platform>` is something like `mingw64`. It is also acceptable |
| 15 | +/// to find `sh` in an environment not provided by Git for Windows, such as an independent MSYS2 |
| 16 | +/// environment in which a `git` package has been installed. However, in an unusual installation, |
| 17 | +/// or if the user has set a custom value of `GIT_EXEC_PATH`, the output of `git --exec-path` may |
| 18 | +/// take a form other than `<platform>/libexec/git-core`, such that finding shell at a location |
| 19 | +/// like `../../../bin/sh.exe` relative to it should not be attempted. We lower the risk by |
| 20 | +/// checking that `<platform>` is a plausible value that is not likely to have any other meaning. |
| 21 | +/// |
| 22 | +/// This involves two tradeoffs. First, it may be reasonable to find `sh.exe` in an environment |
| 23 | +/// that is not MSYS2 at all, for which in principle the prefix could be different. But listing |
| 24 | +/// more prefixes or matching a broad pattern of platform-like strings might be too broad. So only |
| 25 | +/// prefixes that have been used in MSYS2 are considered. |
| 26 | +/// |
| 27 | +/// Second, we don't recognize `usr` itself here, even though it is a plausible prefix. In MSYS2, |
| 28 | +/// it is the prefix for MSYS2 non-native programs, i.e. those that use `msys-2.0.dll`. But unlike |
| 29 | +/// the `<platform>` names we recognize, `usr` also has an effectively unbounded range of plausible |
| 30 | +/// meanings on non-Unix systems (for example, what should we take `Z:\usr` to mean?), which might |
| 31 | +/// occasionally relate to subdirectories with contents controlled by different *user accounts*. |
| 32 | +/// |
| 33 | +/// If we start with a `libexec/git-core` directory that we already use and trust, and it is in a |
| 34 | +/// directory with a name like `mingw64`, we infer that this `mingw64` directory has the expected |
| 35 | +/// meaning and accordingly infer that its `usr` sibling, if present, is acceptable to treat as |
| 36 | +/// though it is a first-level directory inside an MSYS2-like tree. So we are willing to traverse |
| 37 | +/// down to `usr/sh.exe` and try to use it. But if the `libexec/git-core` we use and trust is in a |
| 38 | +/// directory named `usr`, that `usr` directory may still not have the meaning we expect of `usr`. |
| 39 | +/// |
| 40 | +/// Conditions for a privilege escalation attack or other serious malfunction seem far-fetched. If |
| 41 | +/// further research finds the risk is low enough, `usr` may be added. But for now it is omitted. |
| 42 | +const MSYS_USR_VARIANTS: &[&str] = &["mingw64", "mingw32", "clangarm64", "clang64", "clang32", "ucrt64"]; |
| 43 | + |
| 44 | +/// Find a Git for Windows installation directory based on `git --exec-path` output. |
| 45 | +/// |
| 46 | +/// Currently this is used only for finding the path to an `sh.exe` associated with Git. This is |
| 47 | +/// separate from `installation_config()` and `installation_config_prefix()` in `gix_path::env`. |
| 48 | +/// This is *not* suitable for finding the highest-scoped configuration file, because that could be |
| 49 | +/// installed in an unusual place, or customized via `GIT_CONFIG_SYSTEM` or `GIT_CONFIG_NOSYSTEM`, |
| 50 | +/// all of which `installation_config()` should reflect. Likewise, `installation_config_prefix()` |
| 51 | +/// has strong uses, such as to find a directory inside `ProgramData` containing configuration. |
| 52 | +/// But it is possible that some marginal uses of `installation_config_prefix()`, if they do not |
| 53 | +/// really relate to configuration, could be replaced with `git_for_windows_root()` in the future. |
| 54 | +fn git_for_windows_root() -> Option<&'static Path> { |
| 55 | + static GIT_ROOT: Lazy<Option<PathBuf>> = Lazy::new(|| { |
| 56 | + super::core_dir() |
| 57 | + .filter(|core| { |
| 58 | + // Only use this if the directory structure resembles a Git installation. This |
| 59 | + // accepts installations of common types that are not broken when `GIT_EXEC_PATH` |
| 60 | + // is unset, as well as values of `GIT_EXEC_PATH` that are likely to be usable. |
| 61 | + core.is_absolute() && core.ends_with("libexec/git-core") |
| 62 | + }) |
| 63 | + .and_then(|core| core.ancestors().nth(2)) |
| 64 | + .filter(|prefix| { |
| 65 | + // Only use `libexec/git-core` from inside something `usr`-like, such as `mingw64`. |
| 66 | + // See `MSYS_USR_VARIANTS` for details and the rationale for this restriction. |
| 67 | + MSYS_USR_VARIANTS.iter().any(|name| prefix.ends_with(name)) |
| 68 | + }) |
| 69 | + .and_then(|prefix| prefix.parent()) |
| 70 | + .map(Into::into) |
| 71 | + }); |
| 72 | + GIT_ROOT.as_deref() |
| 73 | +} |
| 74 | + |
| 75 | +/// `bin` directory paths to try relative to the root of a Git for Windows or MSYS2 installation. |
| 76 | +/// |
| 77 | +/// These are ordered so that a shim is preferred over a non-shim when they are tried in order. |
| 78 | +const BIN_DIR_FRAGMENTS: &[&str] = &["bin", "usr/bin"]; |
| 79 | + |
| 80 | +/// Obtain a path to an executable command on Windows associated with Git, if one can be found. |
| 81 | +/// |
| 82 | +/// The resulting path uses only `/` separators so long as the path obtained from `git --exec-path` |
| 83 | +/// does, which is the case unless it is overridden by setting `GIT_EXEC_PATH` to an unusual value. |
| 84 | +/// |
| 85 | +/// This is currently only used (and only heavily exercised in tests) for finding `sh.exe`. It may |
| 86 | +/// be used to find other executables in the future, but may need adjustment. In particular, |
| 87 | +/// depending on desired semantics, it should possibly also check a `cmd` directory; directories |
| 88 | +/// like `<platform>/bin`, for any applicable variants (such as `mingw64`); and `super::core_dir()` |
| 89 | +/// itself, which it could safely check even if its value is not safe for inferring other paths. |
| 90 | +fn find_git_associated_windows_executable(stem: &str) -> Option<OsString> { |
| 91 | + let git_root = git_for_windows_root()?; |
| 92 | + |
| 93 | + BIN_DIR_FRAGMENTS |
| 94 | + .iter() |
| 95 | + .map(|bin_dir_fragment| { |
| 96 | + // Perform explicit raw concatenation with `/` to avoid introducing any `\` separators. |
| 97 | + let mut raw_path = OsString::from(git_root); |
| 98 | + raw_path.push("/"); |
| 99 | + raw_path.push(bin_dir_fragment); |
| 100 | + raw_path.push("/"); |
| 101 | + raw_path.push(stem); |
| 102 | + raw_path.push(".exe"); |
| 103 | + raw_path |
| 104 | + }) |
| 105 | + .find(|raw_path| Path::new(raw_path).is_file()) |
| 106 | +} |
| 107 | + |
| 108 | +/// Like `find_associated_windows_executable`, but if not found, fall back to a simple filename. |
| 109 | +pub(super) fn find_git_associated_windows_executable_with_fallback(stem: &str) -> OsString { |
| 110 | + find_git_associated_windows_executable(stem).unwrap_or_else(|| { |
| 111 | + let mut raw_path = OsString::from(stem); |
| 112 | + raw_path.push(".exe"); |
| 113 | + raw_path |
| 114 | + }) |
| 115 | +} |
| 116 | + |
| 117 | +#[cfg(test)] |
| 118 | +mod tests { |
| 119 | + use std::path::Path; |
| 120 | + |
| 121 | + /// Some commands with `.exe` files in `bin` and `usr/bin` that should be found. |
| 122 | + /// |
| 123 | + /// Tests are expected to run with a full Git for Windows installation (not MinGit). |
| 124 | + const SHOULD_FIND: &[&str] = &[ |
| 125 | + "sh", "bash", "dash", "diff", "tar", "less", "sed", "awk", "perl", "cygpath", |
| 126 | + ]; |
| 127 | + |
| 128 | + /// Shouldn't find anything nonexistent, or only in PATH or in `bin`s we don't mean to search. |
| 129 | + /// |
| 130 | + /// If dirs like `mingsw64/bin` are added, `git-credential-manager` should be moved to `SHOULD_FIND`. |
| 131 | + /// Likewise, if `super::core_dir()` is added, `git-daemon` should be moved to `SHOULD_FIND`. |
| 132 | + const SHOULD_NOT_FIND: &[&str] = &[ |
| 133 | + "nonexistent-command", |
| 134 | + "cmd", |
| 135 | + "powershell", |
| 136 | + "explorer", |
| 137 | + "git-credential-manager", |
| 138 | + "git-daemon", |
| 139 | + ]; |
| 140 | + |
| 141 | + #[test] |
| 142 | + #[cfg_attr(not(windows), ignore)] |
| 143 | + fn find_git_associated_windows_executable() { |
| 144 | + for stem in SHOULD_FIND { |
| 145 | + let path = super::find_git_associated_windows_executable(stem); |
| 146 | + assert!(path.is_some(), "should find {stem:?}"); |
| 147 | + } |
| 148 | + } |
| 149 | + |
| 150 | + #[test] |
| 151 | + #[cfg_attr(not(windows), ignore)] |
| 152 | + fn find_git_associated_windows_executable_no_extra() { |
| 153 | + for stem in SHOULD_NOT_FIND { |
| 154 | + let path = super::find_git_associated_windows_executable(stem); |
| 155 | + assert_eq!(path, None, "should not find {stem:?}"); |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + #[test] |
| 160 | + #[cfg_attr(not(windows), ignore)] |
| 161 | + fn find_git_associated_windows_executable_with_fallback() { |
| 162 | + for stem in SHOULD_FIND { |
| 163 | + let path = super::find_git_associated_windows_executable_with_fallback(stem); |
| 164 | + assert!(Path::new(&path).is_absolute(), "should find {stem:?}"); |
| 165 | + } |
| 166 | + } |
| 167 | + |
| 168 | + #[test] |
| 169 | + #[cfg_attr(not(windows), ignore)] |
| 170 | + fn find_git_associated_windows_executable_with_fallback_falls_back() { |
| 171 | + for stem in SHOULD_NOT_FIND { |
| 172 | + let path = super::find_git_associated_windows_executable_with_fallback(stem) |
| 173 | + .to_str() |
| 174 | + .expect("valid Unicode") |
| 175 | + .to_owned(); |
| 176 | + assert_eq!(path, format!("{stem}.exe"), "should fall back for {stem:?}"); |
| 177 | + } |
| 178 | + } |
| 179 | +} |
0 commit comments