mant_engine/
executable.rs1use std::{collections::BTreeMap, env, ffi::OsStr, path::PathBuf};
4
5#[cfg(unix)]
6use std::os::unix::fs::PermissionsExt;
7
8pub(crate) fn environment_value<'a>(
11 environment: &'a BTreeMap<String, String>,
12 name: &str,
13) -> Option<&'a str> {
14 if let Some(value) = environment.get(name) {
15 return Some(value.as_str());
16 }
17
18 #[cfg(windows)]
19 {
20 environment
21 .iter()
22 .find(|(candidate, _)| candidate.eq_ignore_ascii_case(name))
23 .map(|(_, value)| value.as_str())
24 }
25 #[cfg(not(windows))]
26 {
27 None
28 }
29}
30
31pub(crate) fn find_executable(
33 name: &str,
34 environment: &BTreeMap<String, String>,
35) -> Option<PathBuf> {
36 let path = environment_value(environment, "PATH")?;
37 let names = executable_names(name, environment);
38 env::split_paths(OsStr::new(path))
39 .flat_map(|directory| names.iter().map(move |name| directory.join(name)))
40 .find(|candidate| is_executable(candidate))
41}
42
43#[must_use]
46pub fn find_host_executable(name: &str) -> Option<PathBuf> {
47 let environment = env::vars().collect::<BTreeMap<_, _>>();
48 find_executable(name, &environment)
49}
50
51#[cfg(unix)]
52fn executable_names(name: &str, _environment: &BTreeMap<String, String>) -> Vec<String> {
53 vec![name.to_owned()]
54}
55
56#[cfg(windows)]
57fn executable_names(name: &str, environment: &BTreeMap<String, String>) -> Vec<String> {
58 if std::path::Path::new(name).extension().is_some() {
59 return vec![name.to_owned()];
60 }
61 windows_name_candidates(name, environment_value(environment, "PATHEXT"))
62 .into_iter()
63 .skip(1)
64 .collect()
65}
66
67#[cfg(not(any(unix, windows)))]
68fn executable_names(name: &str, _environment: &BTreeMap<String, String>) -> Vec<String> {
69 vec![name.to_owned()]
70}
71
72#[cfg(unix)]
73fn is_executable(path: &std::path::Path) -> bool {
74 path.metadata()
75 .is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
76}
77
78#[cfg(windows)]
79fn is_executable(path: &std::path::Path) -> bool {
80 path.is_file()
81}
82
83pub(crate) fn query_name_candidates(name: &str) -> Vec<String> {
86 #[cfg(windows)]
87 {
88 windows_name_candidates(name, env::var("PATHEXT").ok().as_deref())
89 }
90 #[cfg(not(windows))]
91 {
92 vec![name.to_owned()]
93 }
94}
95
96#[cfg(any(windows, test))]
101fn windows_name_candidates(name: &str, pathext: Option<&str>) -> Vec<String> {
102 let mut candidates = vec![name.to_owned()];
103 if std::path::Path::new(name).extension().is_some() {
104 return candidates;
105 }
106
107 let extensions = pathext
108 .filter(|value| !value.trim().is_empty())
109 .unwrap_or(".COM;.EXE;.BAT;.CMD");
110 for extension in extensions.split(';').map(str::trim) {
111 if extension.is_empty() || extension.contains(['/', '\\']) {
112 continue;
113 }
114 let extension = if extension.starts_with('.') {
115 extension.to_owned()
116 } else {
117 format!(".{extension}")
118 };
119 let candidate = format!("{name}{extension}");
120 if !candidates
121 .iter()
122 .any(|existing| existing.eq_ignore_ascii_case(&candidate))
123 {
124 candidates.push(candidate);
125 }
126 }
127 candidates
128}
129
130#[cfg(test)]
131mod tests {
132 use super::windows_name_candidates;
133
134 #[test]
135 fn windows_candidates_keep_exact_names_before_pathext_order() {
136 assert_eq!(
137 windows_name_candidates("cargo", Some(".EXE;.CMD;.PS1")),
138 ["cargo", "cargo.EXE", "cargo.CMD", "cargo.PS1"]
139 );
140 }
141
142 #[test]
143 fn windows_candidates_do_not_expand_an_explicit_suffix() {
144 assert_eq!(
145 windows_name_candidates("where.exe", Some(".EXE;.CMD")),
146 ["where.exe"]
147 );
148 }
149
150 #[test]
151 fn windows_candidates_use_the_native_default_when_pathext_is_absent() {
152 assert_eq!(
153 windows_name_candidates("tool", None),
154 ["tool", "tool.COM", "tool.EXE", "tool.BAT", "tool.CMD"]
155 );
156 }
157
158 #[test]
159 fn windows_candidates_normalise_and_deduplicate_extensions() {
160 assert_eq!(
161 windows_name_candidates("tool", Some(" EXE ;.exe;.CMD;bad/path;.PS1 ")),
162 ["tool", "tool.EXE", "tool.CMD", "tool.PS1"]
163 );
164 }
165
166 #[cfg(not(windows))]
167 #[test]
168 fn non_windows_queries_never_elide_executable_suffixes() {
169 assert_eq!(super::query_name_candidates("tool"), ["tool"]);
170 }
171}
172
173#[cfg(not(any(unix, windows)))]
174fn is_executable(path: &std::path::Path) -> bool {
175 path.is_file()
176}