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> {
17 let program = program.as_ref();
18 if program.is_empty() {
19 return Err(io::Error::new(
20 io::ErrorKind::InvalidInput,
21 "executable name must not be empty",
22 ));
23 }
24 let current_dir = std::env::current_dir()?;
25 let path = Path::new(program);
26 if path.is_absolute() || path.components().count() > 1 {
27 return canonical_executable(¤t_dir.join(path));
28 }
29
30 let search_path = std::env::var_os("PATH").ok_or_else(|| {
31 io::Error::new(
32 io::ErrorKind::NotFound,
33 format!(
34 "cannot resolve executable `{}` because PATH is unset",
35 program.to_string_lossy()
36 ),
37 )
38 })?;
39 resolve_executable_in(program, &search_path, ¤t_dir)
40}
41
42fn resolve_executable_in(
43 program: &OsStr,
44 search_path: &OsStr,
45 current_dir: &Path,
46) -> io::Result<PathBuf> {
47 for directory in std::env::split_paths(search_path) {
48 let directory = if directory.as_os_str().is_empty() {
49 current_dir.to_owned()
50 } else if directory.is_absolute() {
51 directory
52 } else {
53 current_dir.join(directory)
54 };
55 for candidate in executable_candidates(&directory, program) {
56 match canonical_executable(&candidate) {
57 Ok(path) => return Ok(path),
58 Err(error)
59 if matches!(
60 error.kind(),
61 io::ErrorKind::NotFound | io::ErrorKind::PermissionDenied
62 ) => {}
63 Err(error) => return Err(error),
64 }
65 }
66 }
67 Err(io::Error::new(
68 io::ErrorKind::NotFound,
69 format!(
70 "executable `{}` was not found in PATH",
71 program.to_string_lossy()
72 ),
73 ))
74}
75
76fn canonical_executable(path: &Path) -> io::Result<PathBuf> {
77 let canonical = path.canonicalize()?;
78 let metadata = fs::metadata(&canonical)?;
79 if !metadata.is_file() {
80 return Err(io::Error::new(
81 io::ErrorKind::NotFound,
82 format!("executable path is not a regular file: {}", path.display()),
83 ));
84 }
85 if !is_executable(&metadata) {
86 return Err(io::Error::new(
87 io::ErrorKind::PermissionDenied,
88 format!("file is not executable: {}", path.display()),
89 ));
90 }
91 Ok(canonical)
92}
93
94#[cfg(unix)]
95fn is_executable(metadata: &fs::Metadata) -> bool {
96 use std::os::unix::fs::PermissionsExt as _;
97 metadata.permissions().mode() & 0o111 != 0
98}
99
100#[cfg(not(unix))]
101fn is_executable(_metadata: &fs::Metadata) -> bool {
102 true
103}
104
105#[cfg(windows)]
106fn executable_candidates(directory: &Path, program: &OsStr) -> Vec<PathBuf> {
107 let program_path = Path::new(program);
108 if program_path.extension().is_some() {
109 return vec![directory.join(program_path)];
110 }
111 let extensions =
112 std::env::var_os("PATHEXT").unwrap_or_else(|| OsString::from(".COM;.EXE;.BAT;.CMD"));
113 extensions
114 .to_string_lossy()
115 .split(';')
116 .filter(|extension| !extension.is_empty())
117 .map(|extension| {
118 let mut name = program.to_os_string();
119 name.push(extension);
120 directory.join(name)
121 })
122 .collect()
123}
124
125#[cfg(not(windows))]
126fn executable_candidates(directory: &Path, program: &OsStr) -> Vec<PathBuf> {
127 vec![directory.join(program)]
128}
129
130#[cfg(test)]
131mod tests {
132 use super::resolve_executable_in;
133 use crate::artifacts::test_support::unique_temp_directory;
134 use std::{ffi::OsStr, fs};
135
136 #[cfg(unix)]
137 use crate::artifacts::test_support::write_executable_script;
138
139 #[test]
140 #[cfg(unix)]
141 fn path_resolution_returns_one_canonical_executable_file() {
142 let root = unique_temp_directory("resolve-executable");
143 let bin = root.join("bin");
144 fs::create_dir_all(&bin).expect("create executable search directory");
145 let tool = bin.join("optimizer");
146 write_executable_script(&tool, b"#!/bin/sh\nexit 0\n");
147
148 let resolved = resolve_executable_in(OsStr::new("optimizer"), bin.as_os_str(), &root)
149 .expect("resolve executable from supplied PATH");
150
151 assert_eq!(
152 resolved,
153 tool.canonicalize()
154 .expect("canonicalize executable fixture")
155 );
156 fs::remove_dir_all(root).expect("remove executable-resolution fixture");
157 }
158}