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