artifactize_tools/platform/
program.rs1use crate::platform;
4use std::{
5 env,
6 ffi::{OsStr, OsString},
7 io,
8 path::{Path, PathBuf},
9};
10
11const SEARCH_PATH: &str = "PATH";
13const EXTENSIONS: &str = "PATHEXT";
15
16pub fn resolve(program: &OsStr, cwd: &Path) -> io::Result<PathBuf> {
20 resolve_with(program, cwd, super::environment::var)
21}
22
23pub fn resolve_with(
26 program: &OsStr,
27 cwd: &Path,
28 variable: impl Fn(&str) -> Option<OsString>,
29) -> io::Result<PathBuf> {
30 resolve_from(
31 program,
32 cwd,
33 variable(SEARCH_PATH).as_deref(),
34 variable(EXTENSIONS).as_deref(),
35 )
36}
37
38fn resolve_from(
39 program: &OsStr,
40 cwd: &Path,
41 path: Option<&OsStr>,
42 pathext: Option<&OsStr>,
43) -> io::Result<PathBuf> {
44 let program = Path::new(program);
45 let candidates = candidates_from(program, pathext);
46 let explicit = program.is_absolute() || program.components().count() > 1;
47 let found = if explicit {
48 candidates
49 .into_iter()
50 .map(|candidate| cwd.join(candidate))
51 .find(|candidate| platform::is_executable(candidate))
52 } else {
53 env::split_paths(path.unwrap_or_default()).find_map(|directory| {
54 candidates
55 .iter()
56 .map(|candidate| cwd.join(&directory).join(candidate))
57 .find(|candidate| platform::is_executable(candidate))
58 })
59 };
60 found.ok_or_else(|| {
61 io::Error::new(
62 io::ErrorKind::NotFound,
63 format!(
64 "Program is unavailable: {}",
65 program.to_string_lossy().replace('\\', "/")
66 ),
67 )
68 })
69}
70
71pub fn candidates(program: &Path, variable: impl Fn(&str) -> Option<OsString>) -> Vec<PathBuf> {
75 candidates_from(program, variable(EXTENSIONS).as_deref())
76}
77
78fn candidates_from(program: &Path, pathext: Option<&OsStr>) -> Vec<PathBuf> {
79 if platform::USES_PATHEXT && program.extension().is_none() {
80 pathext_candidates(program, pathext)
81 } else {
82 vec![program.to_owned()]
83 }
84}
85
86const DEFAULT_PATHEXT: &str = ".COM;.EXE;.BAT;.CMD";
88
89fn pathext_candidates(program: &Path, pathext: Option<&OsStr>) -> Vec<PathBuf> {
91 pathext
92 .unwrap_or_else(|| OsStr::new(DEFAULT_PATHEXT))
93 .to_string_lossy()
94 .split(';')
95 .filter(|extension| extension.starts_with('.') && !extension.contains(['/', '\\']))
96 .map(|extension| {
97 let mut candidate = program.as_os_str().to_owned();
98 candidate.push(extension);
99 PathBuf::from(candidate)
100 })
101 .collect()
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn lookup_uses_path_order_relative_to_child_cwd_and_never_implicit_local_search() {
110 let root = crate::test_os::tempdir();
111 for directory in ["first", "second"] {
112 std::fs::create_dir(root.path().join(directory)).unwrap();
113 crate::test_os::lookup_program(&root.path().join(directory), "shim", None);
114 }
115 let program = crate::test_os::executable_name("shim");
116 let path = env::join_paths(["second", "first"]).unwrap();
117 assert_eq!(
118 resolve_with(OsStr::new(&program), root.path(), |name| (name
119 == SEARCH_PATH)
120 .then(|| path.clone()))
121 .unwrap(),
122 root.path().join("second").join(&program)
123 );
124 assert!(resolve_with(OsStr::new(&program), root.path(), |_| None).is_err());
125 }
126
127 #[cfg(windows)]
129 #[test]
130 fn pathext_candidates_keep_the_variable_order_and_skip_unsafe_suffixes() {
131 assert_eq!(
132 candidates(Path::new("dir/shim"), |_| Some(".CMD;bad;.a/b;.EXE".into())),
133 [PathBuf::from("dir/shim.CMD"), PathBuf::from("dir/shim.EXE")]
134 );
135 assert_eq!(
136 candidates(Path::new("shim"), |_| None),
137 [".COM", ".EXE", ".BAT", ".CMD"].map(|suffix| PathBuf::from(format!("shim{suffix}")))
138 );
139 }
140
141 #[cfg(windows)]
143 #[test]
144 fn extensionless_names_follow_pathext_order_not_exe_preference() {
145 let root = crate::test_os::tempdir();
146 for extension in ["cmd", "exe", "bat"] {
147 crate::test_os::lookup_program(root.path(), "shim", Some(extension));
148 }
149 let path = env::join_paths([root.path()]).unwrap();
150 assert_eq!(
151 resolve_with(OsStr::new("shim"), root.path(), |name| match name {
152 SEARCH_PATH => Some(path.clone()),
153 EXTENSIONS => Some(".CMD;.EXE".into()),
154 _ => None,
155 })
156 .unwrap(),
157 root.path().join("shim.CMD")
158 );
159 assert_eq!(
160 resolve_with(OsStr::new("shim"), root.path(), |name| match name {
161 SEARCH_PATH => Some(path.clone()),
162 EXTENSIONS => Some(".BAT;.CMD".into()),
163 _ => None,
164 })
165 .unwrap(),
166 root.path().join("shim.BAT")
167 );
168 }
169}