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