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 let file = root.path().join(directory).join("shim.cmd");
113 std::fs::write(&file, "fixture").unwrap();
114 crate::test_os::make_executable(&file);
115 }
116 let path = env::join_paths(["second", "first"]).unwrap();
117 assert_eq!(
118 resolve_from(OsStr::new("shim.cmd"), root.path(), Some(&path), None).unwrap(),
119 root.path().join("second/shim.cmd")
120 );
121 assert!(resolve_from(OsStr::new("shim.cmd"), root.path(), None, None).is_err());
122 }
123
124 #[test]
125 fn pathext_candidates_keep_the_variable_order_and_skip_unsafe_suffixes() {
126 assert_eq!(
127 pathext_candidates(
128 Path::new("dir/shim"),
129 Some(OsStr::new(".CMD;bad;.a/b;.EXE"))
130 ),
131 [PathBuf::from("dir/shim.CMD"), PathBuf::from("dir/shim.EXE")]
132 );
133 assert_eq!(
134 pathext_candidates(Path::new("shim"), None),
135 [".COM", ".EXE", ".BAT", ".CMD"].map(|suffix| PathBuf::from(format!("shim{suffix}")))
136 );
137 }
138
139 #[cfg(windows)]
141 #[test]
142 fn extensionless_names_follow_pathext_order_not_exe_preference() {
143 let root = crate::test_os::tempdir();
144 for extension in ["cmd", "exe", "bat"] {
145 std::fs::write(root.path().join(format!("shim.{extension}")), "fixture").unwrap();
146 }
147 let path = env::join_paths([root.path()]).unwrap();
148 assert_eq!(
149 resolve_from(
150 OsStr::new("shim"),
151 root.path(),
152 Some(&path),
153 Some(OsStr::new(".CMD;.EXE"))
154 )
155 .unwrap(),
156 root.path().join("shim.CMD")
157 );
158 assert_eq!(
159 resolve_from(
160 OsStr::new("shim"),
161 root.path(),
162 Some(&path),
163 Some(OsStr::new(".BAT;.CMD"))
164 )
165 .unwrap(),
166 root.path().join("shim.BAT")
167 );
168 }
169}