1use std::collections::VecDeque;
4use std::path::{Path, PathBuf};
5
6use crate::sys;
7use crate::sys::fs::PathExt;
8
9pub struct ExecutablePathSearch<PI, N> {
11 paths: VecDeque<PI>,
12 filename: N,
13}
14
15impl<PI, N> Iterator for ExecutablePathSearch<PI, N>
16where
17 PI: AsRef<Path>,
18 N: AsRef<Path>,
19{
20 type Item = PathBuf;
21
22 fn next(&mut self) -> Option<Self::Item> {
23 while let Some(path) = self.paths.pop_front() {
24 let path = PathBuf::from(path.as_ref()).join(self.filename.as_ref());
25
26 if let Some(resolved) = sys::fs::resolve_executable(path)
38 && !resolved.is_dir()
39 {
40 return Some(resolved);
41 }
42 }
43
44 None
45 }
46}
47
48pub(crate) struct ExecutablePathPrefixSearch<PI> {
49 paths: VecDeque<PI>,
50 queued_items: VecDeque<PathBuf>,
51 filename_prefix: String,
52 case_insensitive: bool,
53}
54
55impl<PI> Iterator for ExecutablePathPrefixSearch<PI>
56where
57 PI: AsRef<Path>,
58{
59 type Item = PathBuf;
60
61 fn next(&mut self) -> Option<Self::Item> {
62 if let Some(item) = self.queued_items.pop_front() {
64 return Some(item);
65 }
66
67 while let Some(path) = self.paths.pop_front() {
68 let path = PathBuf::from(path.as_ref());
69
70 if let Ok(readdir) = path.read_dir() {
71 for entry in readdir.flatten() {
72 if let Ok(mut filename) = entry.file_name().into_string() {
73 if self.case_insensitive {
74 filename = filename.to_ascii_lowercase();
75 }
76
77 if !filename.starts_with(&self.filename_prefix) {
78 continue;
79 }
80 }
81
82 let entry_path = entry.path();
83 if let Ok(file_type) = entry.file_type() {
84 if file_type.is_file() && entry_path.executable() {
85 self.queued_items.push_back(entry_path);
86 continue;
87 }
88 if file_type.is_symlink() && entry_path.executable() {
89 self.queued_items.push_back(entry_path);
90 }
91 }
92 }
93 }
94 if let Some(item) = self.queued_items.pop_front() {
95 return Some(item);
96 }
97 }
98
99 None
100 }
101}
102
103pub fn search_for_executable<P, PI, N>(paths: P, filename: N) -> ExecutablePathSearch<PI, N>
110where
111 P: Iterator<Item = PI>,
112 PI: AsRef<Path>,
113 N: AsRef<Path>,
114{
115 ExecutablePathSearch {
116 paths: paths.collect(),
117 filename,
118 }
119}
120
121pub fn resolve_command<P, PI, N>(paths: P, filename: N) -> Option<PathBuf>
132where
133 P: IntoIterator<Item = PI>,
134 PI: AsRef<Path>,
135 N: AsRef<Path>,
136{
137 let mut first_non_executable = None;
138
139 for dir in paths {
140 let path = dir.as_ref().join(filename.as_ref());
141
142 if first_non_executable.is_none() && path.metadata().is_ok_and(|m| !m.is_dir()) {
145 first_non_executable = Some(path.clone());
146 }
147
148 if let Some(resolved) = sys::fs::resolve_executable(path)
150 && !resolved.is_dir()
151 {
152 return Some(resolved);
153 }
154 }
155
156 first_non_executable
157}
158
159pub(crate) fn search_for_executable_with_prefix<P, PI>(
160 paths: P,
161 filename_prefix: &str,
162 case_insensitive: bool,
163) -> ExecutablePathPrefixSearch<PI>
164where
165 P: Iterator<Item = PI>,
166 PI: AsRef<Path>,
167{
168 let stored_prefix = if case_insensitive {
169 filename_prefix.to_ascii_lowercase()
170 } else {
171 filename_prefix.into()
172 };
173
174 ExecutablePathPrefixSearch {
175 paths: paths.collect(),
176 queued_items: VecDeque::new(),
177 filename_prefix: stored_prefix,
178 case_insensitive,
179 }
180}
181
182#[cfg(test)]
183#[expect(clippy::panic_in_result_fn)]
184mod tests {
185 use anyhow::Result;
186
187 use super::*;
188
189 #[test]
193 fn directory_is_not_a_command() -> Result<()> {
194 let scratch = tempfile::tempdir()?;
195 let first = scratch.path().join("first");
196 let second = scratch.path().join("second");
197 std::fs::create_dir_all(first.join("prog"))?;
198 std::fs::create_dir_all(&second)?;
199 std::fs::write(second.join("prog"), "")?;
200
201 let paths = [first.as_path(), second.as_path()];
202
203 assert_eq!(resolve_command(paths, "prog"), Some(second.join("prog")));
206
207 assert_eq!(search_for_executable(paths.iter(), "prog").next(), None);
209
210 Ok(())
211 }
212
213 #[cfg(windows)] #[test]
218 fn same_named_directory_does_not_hide_a_pathext_match() -> Result<()> {
219 let scratch = tempfile::tempdir()?;
220 std::fs::create_dir_all(scratch.path().join("prog"))?;
221 std::fs::write(scratch.path().join("prog.bat"), "@echo off\r\n")?;
222
223 let paths = [scratch.path()];
224 for found in [
225 search_for_executable(paths.iter(), "prog").next(),
226 resolve_command(paths, "prog"),
227 ] {
228 assert!(
229 found.as_ref().is_some_and(|path| path
230 .extension()
231 .is_some_and(|ext| ext.eq_ignore_ascii_case("bat"))),
232 "unexpected resolution: {found:?}"
233 );
234 }
235
236 Ok(())
237 }
238}