use std::path::{Path, PathBuf};
fn find_in_path(filename: &str) -> Option<PathBuf> {
let path_var = std::env::var("PATH").ok()?;
for dir in std::env::split_paths(&path_var) {
let candidate = dir.join(filename);
if candidate.is_file() {
if let Ok(resolved) = candidate.canonicalize() {
return Some(resolved);
}
}
}
None
}
#[cfg(unix)]
pub(crate) fn resolve_git_binary() -> PathBuf {
find_in_path("git").unwrap_or_else(|| PathBuf::from("/usr/bin/git"))
}
#[cfg(not(unix))]
pub(crate) fn resolve_git_binary() -> PathBuf {
if let Some(p) = find_in_path("git.exe") {
return p;
}
for fallback in &[
r"C:\Program Files\Git\bin\git.exe",
r"C:\Program Files (x86)\Git\bin\git.exe",
] {
let p = PathBuf::from(fallback);
if p.is_file() {
return p;
}
}
PathBuf::from("git.exe")
}
#[cfg_attr(not(feature = "clap"), allow(dead_code))]
pub(crate) fn is_safe_git_path(path: &Path) -> bool {
if path.as_os_str().is_empty() {
return false;
}
use std::path::Component;
for component in path.components() {
match component {
Component::ParentDir | Component::RootDir | Component::Prefix(_) => return false,
Component::Normal(s) => {
#[cfg(windows)]
if s.to_string_lossy().contains(':') {
return false;
}
let _ = s; }
_ => {}
}
}
true
}
#[cfg(test)]
mod tests {
#[cfg(unix)]
use std::ffi::OsStr;
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
#[cfg(unix)]
use std::path::PathBuf;
use super::is_safe_git_path;
use super::resolve_git_binary;
#[test]
fn git_binary_resolves_to_absolute_path() {
let path = resolve_git_binary();
#[cfg(unix)]
assert!(
path.is_absolute(),
"git binary must resolve to absolute path on Unix, got: {:?}",
path
);
#[cfg(not(unix))]
assert!(
path.is_absolute() || path == std::path::Path::new("git.exe"),
"git binary must be absolute or the bare-name fallback, got: {:?}",
path
);
}
#[test]
fn is_safe_git_path_rejects_traversal_and_absolute() {
assert!(!is_safe_git_path(Path::new("../../etc/passwd")));
assert!(!is_safe_git_path(Path::new("/etc/passwd")));
assert!(!is_safe_git_path(Path::new("src/../../../etc/passwd")));
assert!(!is_safe_git_path(Path::new("")));
assert!(is_safe_git_path(Path::new("src/main.rs")));
assert!(is_safe_git_path(Path::new("foo/bar/baz.rs")));
assert!(is_safe_git_path(Path::new("Cargo.toml")));
}
#[cfg(unix)]
#[test]
fn is_safe_git_path_accepts_non_utf8_relative_paths() {
let path = PathBuf::from(OsStr::from_bytes(b"src/\xff.rs"));
assert!(is_safe_git_path(&path));
}
#[test]
fn resolve_git_binary_returns_a_path() {
let git = resolve_git_binary();
assert!(!git.as_os_str().is_empty());
}
#[cfg(windows)]
#[test]
fn is_safe_git_path_rejects_ntfs_alternate_data_streams() {
assert!(!is_safe_git_path(Path::new("src/main.rs::$DATA")));
assert!(!is_safe_git_path(Path::new("file.txt:hidden_stream")));
}
#[test]
fn nonexistent_git_path_is_not_a_file() {
let fake = std::path::PathBuf::from("/absolutely/does/not/exist/git");
assert!(!fake.is_file());
}
}