winx_code_agent/utils/
executable.rs1use std::path::{Path, PathBuf};
10
11pub fn find_on_path(program: &str) -> Option<PathBuf> {
15 if program.is_empty() {
16 return None;
17 }
18 let direct = Path::new(program);
19 if direct.components().count() > 1 {
20 return is_executable_file(direct).then(|| direct.to_path_buf());
21 }
22 let path = std::env::var_os("PATH")?;
23 std::env::split_paths(&path)
24 .filter(|dir| !dir.as_os_str().is_empty())
25 .flat_map(|dir| candidate_names(program).into_iter().map(move |name| dir.join(name)))
26 .find(|candidate| is_executable_file(candidate))
27}
28
29pub fn is_available(program: &str) -> bool {
32 find_on_path(program).is_some()
33}
34
35pub fn bash_executable() -> Option<PathBuf> {
40 #[cfg(windows)]
41 {
42 windows_bash_executable()
43 }
44 #[cfg(not(windows))]
45 {
46 find_on_path("bash")
47 }
48}
49
50#[cfg(windows)]
51fn windows_bash_executable() -> Option<PathBuf> {
52 let mut candidates = Vec::new();
53 for root in ["ProgramFiles", "ProgramFiles(x86)", "ProgramW6432"] {
54 if let Some(base) = std::env::var_os(root) {
55 candidates.push(PathBuf::from(&base).join("Git").join("bin").join("bash.exe"));
56 candidates
57 .push(PathBuf::from(base).join("Git").join("usr").join("bin").join("bash.exe"));
58 }
59 }
60 if let Some(base) = std::env::var_os("LOCALAPPDATA") {
61 let user_git = PathBuf::from(base).join("Programs").join("Git");
62 candidates.push(user_git.join("bin").join("bash.exe"));
63 candidates.push(user_git.join("usr").join("bin").join("bash.exe"));
64 }
65 candidates
66 .into_iter()
67 .find(|candidate| is_executable_file(candidate))
68 .or_else(|| find_on_path("bash").filter(|found| !is_windows_system_launcher(found)))
69}
70
71#[cfg(windows)]
74fn is_windows_system_launcher(path: &Path) -> bool {
75 let mut launcher_dirs = Vec::new();
76 if let Some(system_root) = std::env::var_os("SystemRoot") {
77 launcher_dirs.push(PathBuf::from(system_root).join("System32"));
78 }
79 if let Some(local) = std::env::var_os("LOCALAPPDATA") {
80 launcher_dirs.push(PathBuf::from(local).join("Microsoft").join("WindowsApps"));
81 }
82 path.ancestors().any(|ancestor| {
83 launcher_dirs.iter().any(|dir| ancestor.as_os_str().eq_ignore_ascii_case(dir.as_os_str()))
84 })
85}
86
87#[cfg(windows)]
88fn candidate_names(program: &str) -> Vec<String> {
89 let has_extension = Path::new(program).extension().is_some();
90 let mut names = vec![program.to_string()];
91 if !has_extension {
92 let pathext =
93 std::env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".to_string());
94 names.extend(
95 pathext
96 .split(';')
97 .filter(|extension| !extension.is_empty())
98 .map(|extension| format!("{program}{}", extension.to_ascii_lowercase())),
99 );
100 }
101 names
102}
103
104#[cfg(not(windows))]
105fn candidate_names(program: &str) -> Vec<String> {
106 vec![program.to_string()]
107}
108
109#[cfg(unix)]
110fn is_executable_file(path: &Path) -> bool {
111 use std::os::unix::fs::PermissionsExt as _;
112 std::fs::metadata(path)
113 .is_ok_and(|meta| meta.is_file() && meta.permissions().mode() & 0o111 != 0)
114}
115
116#[cfg(not(unix))]
117fn is_executable_file(path: &Path) -> bool {
118 std::fs::metadata(path).is_ok_and(|meta| meta.is_file())
119}
120
121#[cfg(test)]
122mod tests {
123 #![allow(clippy::expect_used)]
124 use super::{find_on_path, is_available};
125
126 #[test]
127 fn empty_and_missing_programs_are_not_found() {
128 assert!(find_on_path("").is_none());
129 assert!(!is_available("winx-definitely-not-a-real-program-9f8e7d"));
130 }
131
132 #[cfg(unix)]
133 #[test]
134 fn resolves_a_standard_unix_tool_and_rejects_non_executables() {
135 assert!(is_available("sh"));
136 let temp = tempfile::tempdir().expect("temp dir");
137 let plain = temp.path().join("not-executable");
138 std::fs::write(&plain, b"data").expect("write");
139 assert!(find_on_path(plain.to_str().expect("utf-8 path")).is_none());
140 }
141}