diff --git a/Cargo.lock b/Cargo.lock index b9d06485ebf..aa20bb07a24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2219,9 +2219,12 @@ dependencies = [ "bstr", "gix-trace 0.1.9", "home", + "known-folders", "once_cell", "tempfile", "thiserror", + "windows 0.58.0", + "winreg 0.52.0", ] [[package]] @@ -2472,6 +2475,7 @@ dependencies = [ "gix-path 0.10.8", "gix-pathspec", "gix-worktree 0.34.0", + "portable-atomic", "thiserror", ] @@ -3204,6 +3208,15 @@ dependencies = [ "rayon", ] +[[package]] +name = "known-folders" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4397c789f2709d23cfcb703b316e0766a8d4b17db2d47b0ab096ef6047cae1d8" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "kstring" version = "2.0.0" @@ -3747,6 +3760,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + [[package]] name = "powerfmt" version = "0.2.0" @@ -3946,7 +3965,7 @@ dependencies = [ "wasm-bindgen-futures", "web-sys", "webpki-roots", - "winreg", + "winreg 0.50.0", ] [[package]] @@ -5109,10 +5128,20 @@ version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" dependencies = [ - "windows-core", + "windows-core 0.51.1", "windows-targets 0.48.5", ] +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.51.1" @@ -5122,6 +5151,60 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -5137,7 +5220,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.6", ] [[package]] @@ -5172,17 +5255,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -5199,9 +5283,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -5217,9 +5301,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -5235,9 +5319,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -5253,9 +5343,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -5271,9 +5361,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -5289,9 +5379,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -5307,9 +5397,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" @@ -5339,6 +5429,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "xattr" version = "1.2.0" diff --git a/gix-path/Cargo.toml b/gix-path/Cargo.toml index fb942e41965..bc084e8341e 100644 --- a/gix-path/Cargo.toml +++ b/gix-path/Cargo.toml @@ -23,3 +23,8 @@ home = "0.5.5" [dev-dependencies] tempfile = "3.3.0" + +[target.'cfg(windows)'.dev-dependencies] +known-folders = "1.1.0" +windows = { version = "0.58.0", features = ["Win32_System_Threading"] } +winreg = "0.52.0" diff --git a/gix-path/src/env/git.rs b/gix-path/src/env/git.rs deleted file mode 100644 index 59996827961..00000000000 --- a/gix-path/src/env/git.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::path::PathBuf; -use std::{ - path::Path, - process::{Command, Stdio}, -}; - -use bstr::{BStr, BString, ByteSlice}; - -/// Other places to find Git in. -#[cfg(windows)] -pub(super) static ALTERNATIVE_LOCATIONS: &[&str] = &[ - "C:/Program Files/Git/mingw64/bin", - "C:/Program Files (x86)/Git/mingw32/bin", -]; -#[cfg(not(windows))] -pub(super) static ALTERNATIVE_LOCATIONS: &[&str] = &[]; - -#[cfg(windows)] -pub(super) static EXE_NAME: &str = "git.exe"; -#[cfg(not(windows))] -pub(super) static EXE_NAME: &str = "git"; - -/// Invoke the git executable in PATH to obtain the origin configuration, which is cached and returned. -pub(super) static EXE_INFO: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { - let git_cmd = |executable: PathBuf| { - let mut cmd = Command::new(executable); - cmd.args(["config", "-l", "--show-origin"]) - .stdin(Stdio::null()) - .stderr(Stdio::null()); - cmd - }; - let mut cmd = git_cmd(EXE_NAME.into()); - gix_trace::debug!(cmd = ?cmd, "invoking git for installation config path"); - let cmd_output = match cmd.output() { - Ok(out) => out.stdout, - #[cfg(windows)] - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - let executable = ALTERNATIVE_LOCATIONS.into_iter().find_map(|prefix| { - let candidate = Path::new(prefix).join(EXE_NAME); - candidate.is_file().then_some(candidate) - })?; - gix_trace::debug!(cmd = ?cmd, "invoking git for installation config path in alternate location"); - git_cmd(executable).output().ok()?.stdout - } - Err(_) => return None, - }; - - first_file_from_config_with_origin(cmd_output.as_slice().into()).map(ToOwned::to_owned) -}); - -/// Returns the file that contains git configuration coming with the installation of the `git` file in the current `PATH`, or `None` -/// if no `git` executable was found or there were other errors during execution. -pub(super) fn install_config_path() -> Option<&'static BStr> { - let _span = gix_trace::detail!("gix_path::git::install_config_path()"); - static PATH: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { - // Shortcut: in Msys shells this variable is set which allows to deduce the installation directory, - // so we can save the `git` invocation. - #[cfg(windows)] - if let Some(mut exec_path) = std::env::var_os("EXEPATH").map(std::path::PathBuf::from) { - exec_path.push("etc"); - exec_path.push("gitconfig"); - return crate::os_string_into_bstring(exec_path.into()).ok(); - } - EXE_INFO.clone() - }); - PATH.as_ref().map(AsRef::as_ref) -} - -fn first_file_from_config_with_origin(source: &BStr) -> Option<&BStr> { - let file = source.strip_prefix(b"file:")?; - let end_pos = file.find_byte(b'\t')?; - file[..end_pos].trim_with(|c| c == '"').as_bstr().into() -} - -/// Given `config_path` as obtained from `install_config_path()`, return the path of the git installation base. -pub(super) fn config_to_base_path(config_path: &Path) -> &Path { - config_path - .parent() - .expect("config file paths always have a file name to pop") -} - -#[cfg(test)] -mod tests { - use std::path::Path; - - #[test] - fn config_to_base_path() { - for (input, expected) in [ - ( - "/Applications/Xcode.app/Contents/Developer/usr/share/git-core/gitconfig", - "/Applications/Xcode.app/Contents/Developer/usr/share/git-core", - ), - ("C:/git-sdk-64/etc/gitconfig", "C:/git-sdk-64/etc"), - ("C:\\ProgramData/Git/config", "C:\\ProgramData/Git"), - ("C:/Program Files/Git/etc/gitconfig", "C:/Program Files/Git/etc"), - ] { - assert_eq!(super::config_to_base_path(Path::new(input)), Path::new(expected)); - } - } - #[test] - fn first_file_from_config_with_origin() { - let macos = "file:/Applications/Xcode.app/Contents/Developer/usr/share/git-core/gitconfig credential.helper=osxkeychain\nfile:/Users/byron/.gitconfig push.default=simple\n"; - let win_msys = - "file:C:/git-sdk-64/etc/gitconfig core.symlinks=false\r\nfile:C:/git-sdk-64/etc/gitconfig core.autocrlf=true"; - let win_cmd = "file:C:/Program Files/Git/etc/gitconfig diff.astextplain.textconv=astextplain\r\nfile:C:/Program Files/Git/etc/gitconfig filter.lfs.clean=gix-lfs clean -- %f\r\n"; - let win_msys_old = "file:\"C:\\ProgramData/Git/config\" diff.astextplain.textconv=astextplain\r\nfile:\"C:\\ProgramData/Git/config\" filter.lfs.clean=git-lfs clean -- %f\r\n"; - let linux = "file:/home/parallels/.gitconfig core.excludesfile=~/.gitignore\n"; - let bogus = "something unexpected"; - let empty = ""; - - for (source, expected) in [ - ( - macos, - Some("/Applications/Xcode.app/Contents/Developer/usr/share/git-core/gitconfig"), - ), - (win_msys, Some("C:/git-sdk-64/etc/gitconfig")), - (win_msys_old, Some("C:\\ProgramData/Git/config")), - (win_cmd, Some("C:/Program Files/Git/etc/gitconfig")), - (linux, Some("/home/parallels/.gitconfig")), - (bogus, None), - (empty, None), - ] { - assert_eq!( - super::first_file_from_config_with_origin(source.into()), - expected.map(Into::into) - ); - } - } -} diff --git a/gix-path/src/env/git/mod.rs b/gix-path/src/env/git/mod.rs new file mode 100644 index 00000000000..9ba82d6c027 --- /dev/null +++ b/gix-path/src/env/git/mod.rs @@ -0,0 +1,144 @@ +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +use bstr::{BStr, BString, ByteSlice}; +use once_cell::sync::Lazy; + +/// Other places to find Git in. +#[cfg(windows)] +pub(super) static ALTERNATIVE_LOCATIONS: Lazy> = + Lazy::new(|| locations_under_program_files(|key| std::env::var_os(key))); +#[cfg(not(windows))] +pub(super) static ALTERNATIVE_LOCATIONS: Lazy> = Lazy::new(Vec::new); + +#[cfg(windows)] +fn locations_under_program_files(var_os_func: F) -> Vec +where + F: Fn(&str) -> Option, +{ + // Should give a 64-bit program files path from a 32-bit or 64-bit process on a 64-bit system. + let varname_64bit = "ProgramW6432"; + + // Should give a 32-bit program files path from a 32-bit or 64-bit process on a 64-bit system. + // This variable is x86-specific, but neither Git nor Rust target 32-bit ARM on Windows. + let varname_x86 = "ProgramFiles(x86)"; + + // Should give a 32-bit program files path on a 32-bit system. We also check this on a 64-bit + // system, even though it *should* equal the process's architecture specific variable, so that + // we cover the case of a parent process that passes down an overly sanitized environment that + // lacks the architecture-specific variable. On a 64-bit system, because parent and child + // processes' architectures can be different, Windows sets the child's ProgramFiles variable + // from the ProgramW6432 or ProgramFiles(x86) variable applicable to the child's architecture. + // Only if the parent does not pass that down is the passed-down ProgramFiles variable even + // used. But this behavior is not well known, so that situation does sometimes happen. + let varname_current = "ProgramFiles"; + + // 64-bit relative bin dir. So far, this is always mingw64, not ucrt64, clang64, or clangarm64. + let suffix_64 = Path::new(r"Git\mingw64\bin"); + + // 32-bit relative bin dir. So far, this is always mingw32, not clang32. + let suffix_32 = Path::new(r"Git\mingw32\bin"); + + // Whichever of the 64-bit or 32-bit relative bin better matches this process's architecture. + // Unlike the system architecture, the process architecture is always known at compile time. + #[cfg(target_pointer_width = "64")] + let suffix_current = suffix_64; + #[cfg(target_pointer_width = "32")] + let suffix_current = suffix_32; + + let rules = [ + (varname_64bit, suffix_64), + (varname_x86, suffix_32), + (varname_current, suffix_current), + ]; + + let mut locations = vec![]; + + for (name, suffix) in rules { + let Some(pf) = var_os_func(name) else { continue }; + let pf = Path::new(&pf); + if pf.is_relative() { + // This shouldn't happen, but if it does then don't use the path. This is mainly in + // case we are accidentally invoked with the environment variable set but empty. + continue; + } + let location = pf.join(suffix); + if !locations.contains(&location) { + locations.push(location); + } + } + + locations +} + +#[cfg(windows)] +pub(super) static EXE_NAME: &str = "git.exe"; +#[cfg(not(windows))] +pub(super) static EXE_NAME: &str = "git"; + +/// Invoke the git executable to obtain the origin configuration, which is cached and returned. +/// +/// The git executable is the one found in PATH or an alternative location. +pub(super) static EXE_INFO: Lazy> = Lazy::new(|| { + let git_cmd = |executable: PathBuf| { + let mut cmd = Command::new(executable); + cmd.args(["config", "-l", "--show-origin"]) + .stdin(Stdio::null()) + .stderr(Stdio::null()); + cmd + }; + let mut cmd = git_cmd(EXE_NAME.into()); + gix_trace::debug!(cmd = ?cmd, "invoking git for installation config path"); + let cmd_output = match cmd.output() { + Ok(out) => out.stdout, + #[cfg(windows)] + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + let executable = ALTERNATIVE_LOCATIONS.iter().find_map(|prefix| { + let candidate = prefix.join(EXE_NAME); + candidate.is_file().then_some(candidate) + })?; + gix_trace::debug!(cmd = ?cmd, "invoking git for installation config path in alternate location"); + git_cmd(executable).output().ok()?.stdout + } + Err(_) => return None, + }; + + first_file_from_config_with_origin(cmd_output.as_slice().into()).map(ToOwned::to_owned) +}); + +/// Try to find the file that contains git configuration coming with the git installation. +/// +/// This returns the configuration associated with the `git` executable found in the current `PATH` +/// or an alternative location, or `None` if no `git` executable was found or there were other +/// errors during execution. +pub(super) fn install_config_path() -> Option<&'static BStr> { + let _span = gix_trace::detail!("gix_path::git::install_config_path()"); + static PATH: Lazy> = Lazy::new(|| { + // Shortcut: Specifically in Git for Windows 'Git Bash' shells, this variable is set. It + // may let us deduce the installation directory, so we can save the `git` invocation. + #[cfg(windows)] + if let Some(mut exec_path) = std::env::var_os("EXEPATH").map(PathBuf::from) { + exec_path.push("etc"); + exec_path.push("gitconfig"); + return crate::os_string_into_bstring(exec_path.into()).ok(); + } + EXE_INFO.clone() + }); + PATH.as_ref().map(AsRef::as_ref) +} + +fn first_file_from_config_with_origin(source: &BStr) -> Option<&BStr> { + let file = source.strip_prefix(b"file:")?; + let end_pos = file.find_byte(b'\t')?; + file[..end_pos].trim_with(|c| c == '"').as_bstr().into() +} + +/// Given `config_path` as obtained from `install_config_path()`, return the path of the git installation base. +pub(super) fn config_to_base_path(config_path: &Path) -> &Path { + config_path + .parent() + .expect("config file paths always have a file name to pop") +} + +#[cfg(test)] +mod tests; diff --git a/gix-path/src/env/git/tests.rs b/gix-path/src/env/git/tests.rs new file mode 100644 index 00000000000..033b6f0b983 --- /dev/null +++ b/gix-path/src/env/git/tests.rs @@ -0,0 +1,403 @@ +use std::path::Path; + +#[test] +fn config_to_base_path() { + for (input, expected) in [ + ( + "/Applications/Xcode.app/Contents/Developer/usr/share/git-core/gitconfig", + "/Applications/Xcode.app/Contents/Developer/usr/share/git-core", + ), + ("C:/git-sdk-64/etc/gitconfig", "C:/git-sdk-64/etc"), + ("C:\\ProgramData/Git/config", "C:\\ProgramData/Git"), + ("C:/Program Files/Git/etc/gitconfig", "C:/Program Files/Git/etc"), + ] { + assert_eq!(super::config_to_base_path(Path::new(input)), Path::new(expected)); + } +} + +#[test] +fn first_file_from_config_with_origin() { + let macos = "file:/Applications/Xcode.app/Contents/Developer/usr/share/git-core/gitconfig credential.helper=osxkeychain\nfile:/Users/byron/.gitconfig push.default=simple\n"; + let win_msys = + "file:C:/git-sdk-64/etc/gitconfig core.symlinks=false\r\nfile:C:/git-sdk-64/etc/gitconfig core.autocrlf=true"; + let win_cmd = "file:C:/Program Files/Git/etc/gitconfig diff.astextplain.textconv=astextplain\r\nfile:C:/Program Files/Git/etc/gitconfig filter.lfs.clean=gix-lfs clean -- %f\r\n"; + let win_msys_old = "file:\"C:\\ProgramData/Git/config\" diff.astextplain.textconv=astextplain\r\nfile:\"C:\\ProgramData/Git/config\" filter.lfs.clean=git-lfs clean -- %f\r\n"; + let linux = "file:/home/parallels/.gitconfig core.excludesfile=~/.gitignore\n"; + let bogus = "something unexpected"; + let empty = ""; + + for (source, expected) in [ + ( + macos, + Some("/Applications/Xcode.app/Contents/Developer/usr/share/git-core/gitconfig"), + ), + (win_msys, Some("C:/git-sdk-64/etc/gitconfig")), + (win_msys_old, Some("C:\\ProgramData/Git/config")), + (win_cmd, Some("C:/Program Files/Git/etc/gitconfig")), + (linux, Some("/home/parallels/.gitconfig")), + (bogus, None), + (empty, None), + ] { + assert_eq!( + super::first_file_from_config_with_origin(source.into()), + expected.map(Into::into) + ); + } +} + +#[cfg(windows)] +mod locations { + use std::ffi::{OsStr, OsString}; + use std::io::ErrorKind; + use std::path::{Path, PathBuf}; + + use known_folders::{get_known_folder_path, KnownFolder}; + use windows::core::Result as WindowsResult; + use windows::Win32::Foundation::BOOL; + use windows::Win32::System::Threading::{GetCurrentProcess, IsWow64Process}; + use winreg::enums::{HKEY_LOCAL_MACHINE, KEY_QUERY_VALUE}; + use winreg::RegKey; + + macro_rules! var_os_stub { + { $($name:expr => $value:expr),* $(,)? } => { + |key| { + match key { + $( + $name => Some(OsString::from($value)), + )* + _ => None, + } + } + } + } + + macro_rules! locations_from { + ($($name:expr => $value:expr),* $(,)?) => { + super::super::locations_under_program_files(var_os_stub! { + $( + $name => $value, + )* + }) + } + } + + macro_rules! pathbuf_vec { + [$($path:expr),* $(,)?] => { + vec![$( + PathBuf::from($path), + )*] + } + } + + #[test] + fn locations_under_program_files_ordinary() { + assert_eq!( + locations_from!( + "ProgramFiles" => r"C:\Program Files", + ), + if cfg!(target_pointer_width = "64") { + pathbuf_vec![r"C:\Program Files\Git\mingw64\bin"] + } else { + pathbuf_vec![r"C:\Program Files\Git\mingw32\bin"] + }, + ); + assert_eq!( + locations_from!( + "ProgramFiles" => { + if cfg!(target_pointer_width = "64") { + r"C:\Program Files" + } else { + r"C:\Program Files (x86)" + } + }, + "ProgramFiles(x86)" => r"C:\Program Files (x86)", + "ProgramW6432" => r"C:\Program Files", + ), + pathbuf_vec![ + r"C:\Program Files\Git\mingw64\bin", + r"C:\Program Files (x86)\Git\mingw32\bin", + ], + ); + assert_eq!(locations_from!(), Vec::::new()); + } + + #[test] + fn locations_under_program_files_strange() { + assert_eq!( + locations_from!( + "ProgramFiles" => r"X:\cur\rent", + "ProgramFiles(x86)" => r"Y:\nar\row", + "ProgramW6432" => r"Z:\wi\de", + ), + pathbuf_vec![ + r"Z:\wi\de\Git\mingw64\bin", + r"Y:\nar\row\Git\mingw32\bin", + if cfg!(target_pointer_width = "64") { + r"X:\cur\rent\Git\mingw64\bin" + } else { + r"X:\cur\rent\Git\mingw32\bin" + }, + ], + ); + assert_eq!( + locations_from!( + "ProgramW6432" => r"Z:\wi\de", + ), + pathbuf_vec![r"Z:\wi\de\Git\mingw64\bin"], + ); + assert_eq!( + locations_from!( + "ProgramFiles" => r"Z:/wi//de/", + "ProgramFiles(x86)" => r"Y:/\nar\/row", + "ProgramW6432" => r"Z:\wi\.\de", + ), + if cfg!(target_pointer_width = "64") { + pathbuf_vec![r"Z:\wi\de\Git\mingw64\bin", r"Y:\nar\row\Git\mingw32\bin"] + } else { + pathbuf_vec![ + r"Z:\wi\de\Git\mingw64\bin", + r"Y:\nar\row\Git\mingw32\bin", + r"Z:\wi\de\Git\mingw32\bin", + ] + }, + ); + assert_eq!( + locations_from!( + "ProgramFiles" => r"foo\bar", + "ProgramFiles(x86)" => r"\\host\share\subdir", + "ProgramW6432" => r"", + ), + pathbuf_vec![r"\\host\share\subdir\Git\mingw32\bin"], + ); + } + + #[derive(Clone, Copy, Debug)] + enum PlatformArchitecture { + Is32on32, + Is32on64, + Is64on64, + } + + impl PlatformArchitecture { + fn current() -> WindowsResult { + // Ordinarily, we would check the target pointer width first to avoid doing extra work, + // because if this is a 64-bit executable then the operating system is 64-bit. But this + // is for the test suite, and doing it this way allows problems to be caught earlier if + // a change made on a 64-bit development machine breaks the IsWow64Process() call. + let mut wow64process = BOOL::default(); + unsafe { IsWow64Process(GetCurrentProcess(), &mut wow64process)? }; + + let platform_architecture = if wow64process.as_bool() { + Self::Is32on64 + } else if cfg!(target_pointer_width = "32") { + Self::Is32on32 + } else { + assert!(cfg!(target_pointer_width = "64")); + Self::Is64on64 + }; + Ok(platform_architecture) + } + } + + fn ends_with_case_insensitive(full_text: &OsStr, literal_pattern: &str) -> Option { + let folded_text = full_text.to_str()?.to_lowercase(); + let folded_pattern = literal_pattern.to_lowercase(); + Some(folded_text.ends_with(&folded_pattern)) + } + + /// The common global program files paths on this system, by process and system architecture. + #[derive(Clone, Debug)] + struct ProgramFilesPaths { + /// The program files directory used for whatever architecture this program was built for. + current: PathBuf, + + /// The x86 program files directory regardless of the architecture of the program. + /// + /// If Rust gains Windows targets like ARMv7 where this is unavailable, this could fail. + x86: PathBuf, + + /// The 64-bit program files directory if there is one. + /// + /// This is present on x64 and also ARM64 systems. On an ARM64 system, ARM64 and AMD64 + /// programs use the same program files directory while 32-bit x86 and ARM programs use + /// two others. Only a 32-bit has no 64-bit program files directory. + maybe_64bit: Option, + } + + impl ProgramFilesPaths { + /// Gets the three common kinds of global program files paths without environment variables. + /// + /// The idea here is to obtain this information, which the `alternative_locations()` unit + /// test uses to learn the expected alternative locations, without duplicating *any* of the + /// approach used for `ALTERNATIVE_LOCATIONS`, so it can be used to test that. The approach + /// here is also more reliable than using environment variables, but it is a bit more + /// complex, and it requires either additional dependencies or the use of unsafe code. + /// + /// This gets `pf_current` and `pf_x86` by the [known folders][known-folders] system. But + /// it gets `maybe_pf_64bit` from the registry, as the corresponding known folder is not + /// available to 32-bit processes. See the [`KNOWNFOLDDERID`][knownfolderid] documentation. + /// + /// If in the future the implementation of `ALTERNATIVE_LOCATIONS` uses these techniques, + /// then this function can be changed to use environment variables and renamed accordingly. + /// + /// [known-folders]: https://learn.microsoft.com/en-us/windows/win32/shell/known-folders + /// [knownfolderid]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#remarks + fn obtain_envlessly() -> Self { + let pf_current = get_known_folder_path(KnownFolder::ProgramFiles) + .expect("The process architecture specific program files folder is always available"); + + let pf_x86 = get_known_folder_path(KnownFolder::ProgramFilesX86) + .expect("The x86 program files folder will in practice always be available"); + + let maybe_pf_64bit = RegKey::predef(HKEY_LOCAL_MACHINE) + .open_subkey_with_flags(r"SOFTWARE\Microsoft\Windows\CurrentVersion", KEY_QUERY_VALUE) + .expect("The `CurrentVersion` registry key exists and allows reading") + .get_value::("ProgramW6432Dir") + .map(PathBuf::from) + .map_err(|error| { + assert_eq!(error.kind(), ErrorKind::NotFound); + error + }) + .ok(); + + Self { + current: pf_current, + x86: pf_x86, + maybe_64bit: maybe_pf_64bit, + } + } + + /// Checks that the paths we got for testing are reasonable. + /// + /// This checks that `obtain_envlessly()` returned paths that are likely to be correct and + /// that satisfy the most important properties based on the current system and process. + fn validated(self) -> Self { + match PlatformArchitecture::current().expect("Process and system 'bitness' should be available") { + PlatformArchitecture::Is32on32 => { + assert_eq!( + self.current.as_os_str(), + self.x86.as_os_str(), + "Our program files path is exactly identical to the 32-bit one.", + ); + for trailing_arch in [" (x86)", " (Arm)"] { + let is_adorned = ends_with_case_insensitive(self.current.as_os_str(), trailing_arch) + .expect("Assume the test system's important directories are valid Unicode"); + assert!( + !is_adorned, + "The 32-bit program files directory name on a 32-bit system mentions no architecture.", + ); + } + assert_eq!( + self.maybe_64bit, None, + "A 32-bit system has no 64-bit program files directory.", + ); + } + PlatformArchitecture::Is32on64 => { + assert_eq!( + self.current.as_os_str(), + self.x86.as_os_str(), + "Our program files path is exactly identical to the 32-bit one.", + ); + let pf_64bit = self + .maybe_64bit + .as_ref() + .expect("The 64-bit program files directory exists"); + assert_ne!( + &self.x86, pf_64bit, + "The 32-bit and 64-bit program files directories have different locations.", + ); + } + PlatformArchitecture::Is64on64 => { + let pf_64bit = self + .maybe_64bit + .as_ref() + .expect("The 64-bit program files directory exists"); + assert_eq!( + self.current.as_os_str(), + pf_64bit.as_os_str(), + "Our program files path is exactly identical to the 64-bit one.", + ); + assert_ne!( + &self.x86, pf_64bit, + "The 32-bit and 64-bit program files directories have different locations.", + ); + } + } + + self + } + } + + /// Paths relative to process architecture specific program files directories. + #[derive(Clone, Debug)] + struct RelativeGitBinPaths<'a> { + x86: &'a Path, + maybe_64bit: Option<&'a Path>, + } + + impl<'a> RelativeGitBinPaths<'a> { + /// Assert that `locations` has the given path prefixes, and extract the suffixes. + fn assert_from(pf: &'a ProgramFilesPaths, locations: &'static [PathBuf]) -> Self { + match locations { + [primary, secondary] => { + let prefix_64bit = pf + .maybe_64bit + .as_ref() + .expect("It gives two paths only if one can be 64-bit"); + let suffix_64bit = primary + .strip_prefix(prefix_64bit) + .expect("It gives the 64-bit path and lists it first"); + let suffix_x86 = secondary + .strip_prefix(pf.x86.as_path()) + .expect("It gives the 32-bit path and lists it second"); + Self { + x86: suffix_x86, + maybe_64bit: Some(suffix_64bit), + } + } + [only] => { + assert_eq!(pf.maybe_64bit, None, "It gives one path only if none can be 64-bit."); + let suffix_x86 = only + .strip_prefix(pf.x86.as_path()) + .expect("The one path it gives is the 32-bit path"); + Self { + x86: suffix_x86, + maybe_64bit: None, + } + } + other => panic!("{:?} has length {}, expected 1 or 2.", other, other.len()), + } + } + + /// Assert that the suffixes (relative subdirectories) are the common per-architecture Git install locations. + fn assert_architectures(&self) { + assert_eq!(self.x86, Path::new("Git/mingw32/bin")); + + if let Some(suffix_64bit) = self.maybe_64bit { + // When Git for Windows releases ARM64 builds, there will be another 64-bit suffix, + // likely clangarm64. In that case, this and other assertions will need updating, + // as there will be two separate paths to check under the same 64-bit program files + // directory. (See the definition of ProgramFilesPaths::maybe_64bit for details.) + assert_eq!(suffix_64bit, Path::new("Git/mingw64/bin")); + } + } + } + + #[test] + fn alternative_locations() { + // Obtain program files directory paths by other means and check that they seem correct. + let pf = ProgramFilesPaths::obtain_envlessly().validated(); + + // Check that `ALTERNATIVE_LOCATIONS` correspond to them, with the correct subdirectories. + let locations = super::super::ALTERNATIVE_LOCATIONS.as_slice(); + RelativeGitBinPaths::assert_from(&pf, locations).assert_architectures(); + } +} + +#[cfg(not(windows))] +mod locations { + #[test] + fn alternative_locations() { + assert!(super::super::ALTERNATIVE_LOCATIONS.is_empty()); + } +} diff --git a/gix-path/src/env/mod.rs b/gix-path/src/env/mod.rs index 06baff53db2..0e528058e9b 100644 --- a/gix-path/src/env/mod.rs +++ b/gix-path/src/env/mod.rs @@ -1,10 +1,10 @@ -use std::{ - ffi::OsString, - path::{Path, PathBuf}, -}; +use std::ffi::OsString; +use std::path::{Path, PathBuf}; -use crate::env::git::EXE_NAME; use bstr::{BString, ByteSlice}; +use once_cell::sync::Lazy; + +use crate::env::git::EXE_NAME; mod git; @@ -37,7 +37,7 @@ pub fn exe_invocation() -> &'static Path { if cfg!(windows) { /// The path to the Git executable as located in the `PATH` or in other locations that it's known to be installed to. /// It's `None` if environment variables couldn't be read or if no executable could be found. - static EXECUTABLE_PATH: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { + static EXECUTABLE_PATH: Lazy> = Lazy::new(|| { std::env::split_paths(&std::env::var_os("PATH")?) .chain(git::ALTERNATIVE_LOCATIONS.iter().map(Into::into)) .find_map(|prefix| { @@ -98,7 +98,7 @@ pub fn xdg_config(file: &str, env_var: &mut dyn FnMut(&str) -> Option) /// wasn't built with a well-known directory structure or environment. pub fn system_prefix() -> Option<&'static Path> { if cfg!(windows) { - static PREFIX: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { + static PREFIX: Lazy> = Lazy::new(|| { if let Some(root) = std::env::var_os("EXEPATH").map(PathBuf::from) { for candidate in ["mingw64", "mingw32"] { let candidate = root.join(candidate); diff --git a/gix-path/src/lib.rs b/gix-path/src/lib.rs index 7f913042fac..c433e48d9f8 100644 --- a/gix-path/src/lib.rs +++ b/gix-path/src/lib.rs @@ -47,7 +47,7 @@ //! ever get into a code-path which does panic though. //! #![deny(missing_docs, rust_2018_idioms)] -#![forbid(unsafe_code)] +#![cfg_attr(not(test), forbid(unsafe_code))] /// A dummy type to represent path specs and help finding all spots that take path specs once it is implemented. mod convert;