ic_testkit/artifacts/
tool.rs1use std::{
2 ffi::OsStr,
3 fs, io,
4 path::{Path, PathBuf},
5};
6
7#[cfg(windows)]
8use std::ffi::OsString;
9
10pub fn resolve_executable(program: impl AsRef<OsStr>) -> io::Result<PathBuf> {
20 let program = program.as_ref();
21 if program.is_empty() {
22 return Err(io::Error::new(
23 io::ErrorKind::InvalidInput,
24 "executable name must not be empty",
25 ));
26 }
27 let path = Path::new(program);
28 if path.is_absolute() {
29 return canonical_executable(path);
30 }
31 let current_dir = std::env::current_dir()?;
32 if path.components().count() > 1 {
33 return canonical_executable(¤t_dir.join(path));
34 }
35
36 let search_path = std::env::var_os("PATH").ok_or_else(|| {
37 io::Error::new(
38 io::ErrorKind::NotFound,
39 format!(
40 "cannot resolve executable `{}` because PATH is unset",
41 program.to_string_lossy()
42 ),
43 )
44 })?;
45 resolve_executable_in(program, &search_path, ¤t_dir)
46}
47
48fn resolve_executable_in(
49 program: &OsStr,
50 search_path: &OsStr,
51 current_dir: &Path,
52) -> io::Result<PathBuf> {
53 for directory in std::env::split_paths(search_path) {
54 let directory = if directory.as_os_str().is_empty() {
55 current_dir.to_owned()
56 } else if directory.is_absolute() {
57 directory
58 } else {
59 current_dir.join(directory)
60 };
61 for candidate in executable_candidates(&directory, program) {
62 match canonical_executable(&candidate) {
63 Ok(path) => return Ok(path),
64 Err(error)
65 if matches!(
66 error.kind(),
67 io::ErrorKind::NotFound
68 | io::ErrorKind::NotADirectory
69 | io::ErrorKind::PermissionDenied
70 ) => {}
71 Err(error) => return Err(error),
72 }
73 }
74 }
75 Err(io::Error::new(
76 io::ErrorKind::NotFound,
77 format!(
78 "executable `{}` was not found in PATH",
79 program.to_string_lossy()
80 ),
81 ))
82}
83
84fn canonical_executable(path: &Path) -> io::Result<PathBuf> {
85 let canonical = path.canonicalize()?;
86 let metadata = fs::metadata(&canonical)?;
87 if !metadata.is_file() {
88 return Err(io::Error::new(
89 io::ErrorKind::NotFound,
90 format!("executable path is not a regular file: {}", path.display()),
91 ));
92 }
93 if !is_executable(&metadata) {
94 return Err(io::Error::new(
95 io::ErrorKind::PermissionDenied,
96 format!("file is not executable: {}", path.display()),
97 ));
98 }
99 Ok(canonical)
100}
101
102#[cfg(unix)]
103fn is_executable(metadata: &fs::Metadata) -> bool {
104 use std::os::unix::fs::PermissionsExt as _;
105 metadata.permissions().mode() & 0o111 != 0
106}
107
108#[cfg(not(unix))]
109fn is_executable(_metadata: &fs::Metadata) -> bool {
110 true
111}
112
113#[cfg(windows)]
114fn executable_candidates(directory: &Path, program: &OsStr) -> Vec<PathBuf> {
115 let program_path = Path::new(program);
116 if program_path.extension().is_some() {
117 return vec![directory.join(program_path)];
118 }
119 let extensions =
120 std::env::var_os("PATHEXT").unwrap_or_else(|| OsString::from(".COM;.EXE;.BAT;.CMD"));
121 extensions
122 .to_string_lossy()
123 .split(';')
124 .filter(|extension| !extension.is_empty())
125 .map(|extension| {
126 let mut name = program.to_os_string();
127 name.push(extension);
128 directory.join(name)
129 })
130 .collect()
131}
132
133#[cfg(not(windows))]
134fn executable_candidates(directory: &Path, program: &OsStr) -> Vec<PathBuf> {
135 vec![directory.join(program)]
136}
137
138#[cfg(test)]
139mod tests {
140 use super::resolve_executable_in;
141 use crate::artifacts::test_support::unique_temp_directory;
142 use std::{ffi::OsStr, fs};
143
144 #[cfg(unix)]
145 use crate::artifacts::test_support::write_executable_script;
146
147 #[test]
148 #[cfg(unix)]
149 fn path_resolution_returns_one_canonical_executable_file() {
150 let root = unique_temp_directory("resolve-executable")
151 .canonicalize()
152 .unwrap();
153 let bin = root.join("bin");
154 fs::create_dir_all(&bin).expect("create executable search directory");
155 let tool = bin.join("optimizer");
156 write_executable_script(&tool, b"#!/bin/sh\nexit 0\n");
157
158 for (search, current) in [
159 (bin.as_os_str(), &root),
160 (OsStr::new("bin"), &root),
161 (OsStr::new(""), &bin),
162 ] {
163 let resolved = resolve_executable_in(OsStr::new("optimizer"), search, current)
164 .expect("resolve executable from supplied PATH");
165 assert_eq!(resolved, tool.canonicalize().unwrap());
166 }
167 fs::remove_dir_all(root).expect("remove executable-resolution fixture");
168 }
169
170 #[test]
171 #[cfg(unix)]
172 fn path_resolution_skips_unusable_search_entries() {
173 let root = unique_temp_directory("resolve-executable-search")
174 .canonicalize()
175 .unwrap();
176 let not_directory = root.join("not-directory");
177 fs::write(¬_directory, b"file").unwrap();
178 let not_executable = root.join("not-executable");
179 fs::create_dir(¬_executable).unwrap();
180 fs::write(not_executable.join("optimizer"), b"not executable").unwrap();
181 let directory_candidate = root.join("directory-candidate");
182 fs::create_dir_all(directory_candidate.join("optimizer")).unwrap();
183 let bin = root.join("bin");
184 fs::create_dir(&bin).unwrap();
185 let tool = bin.join("optimizer");
186 write_executable_script(&tool, b"#!/bin/sh\nexit 0\n");
187 let search = std::env::join_paths([
188 root.join("missing"),
189 not_directory.clone(),
190 not_executable.clone(),
191 directory_candidate,
192 bin,
193 ])
194 .unwrap();
195 assert_eq!(
196 resolve_executable_in(OsStr::new("optimizer"), &search, &root).unwrap(),
197 tool.canonicalize().unwrap()
198 );
199 assert_eq!(
200 super::resolve_executable(not_directory.join("optimizer"))
201 .unwrap_err()
202 .kind(),
203 std::io::ErrorKind::NotADirectory
204 );
205 assert_eq!(
206 super::resolve_executable(not_executable.join("optimizer"))
207 .unwrap_err()
208 .kind(),
209 std::io::ErrorKind::PermissionDenied
210 );
211 assert_eq!(
212 resolve_executable_in(OsStr::new("missing"), &search, &root)
213 .unwrap_err()
214 .kind(),
215 std::io::ErrorKind::NotFound
216 );
217 fs::remove_dir_all(root).unwrap();
218 }
219
220 #[test]
221 #[cfg(unix)]
222 fn absolute_executable_resolution_does_not_require_current_directory() {
223 const CHILD_ENV: &str = "IC_TESTKIT_EXECUTABLE_REMOVED_CWD_CHILD";
224 if std::env::var_os(CHILD_ENV).is_none() {
225 let child = std::process::Command::new(std::env::current_exe().unwrap())
227 .args([
228 "--exact",
229 "artifacts::tool::tests::absolute_executable_resolution_does_not_require_current_directory",
230 "--test-threads=1",
231 ])
232 .env(CHILD_ENV, "1")
233 .output()
234 .unwrap();
235 assert!(
236 child.status.success(),
237 "absolute resolution regression failed: {}{}",
238 String::from_utf8_lossy(&child.stdout),
239 String::from_utf8_lossy(&child.stderr)
240 );
241 return;
242 }
243 let root = unique_temp_directory("resolve-executable-removed-cwd")
244 .canonicalize()
245 .unwrap();
246 let tool = root.join("optimizer");
247 write_executable_script(&tool, b"#!/bin/sh\nexit 0\n");
248 let expected = tool.canonicalize().unwrap();
249 std::env::set_current_dir(&root).unwrap();
250 assert_eq!(super::resolve_executable("./optimizer").unwrap(), expected);
251 let removed = root.join("removed");
252 fs::create_dir(&removed).unwrap();
253 std::env::set_current_dir(&removed).unwrap();
254 fs::remove_dir(&removed).unwrap();
255 assert!(std::env::current_dir().is_err());
256 assert_eq!(super::resolve_executable(&tool).unwrap(), expected);
257 assert!(super::resolve_executable("optimizer").is_err());
258 std::env::set_current_dir(&root).unwrap();
259 fs::remove_dir_all(root).unwrap();
260 }
261}