hardware_enclave/internal/core/
bin_discovery.rs1#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
40
41use std::path::PathBuf;
42
43#[cfg(windows)]
44use std::io::Read;
45
46#[derive(Debug, Clone, Default)]
51pub struct BinaryDiscoveryContext {
52 pub current_exe: Option<PathBuf>,
53 #[cfg(not(windows))]
54 pub home_dir: Option<PathBuf>,
55 #[cfg(windows)]
56 pub local_app_data: Option<PathBuf>,
57 #[cfg(windows)]
58 pub program_files: Option<PathBuf>,
59 #[cfg(windows)]
60 pub program_files_x86: Option<PathBuf>,
61}
62
63impl BinaryDiscoveryContext {
64 #[must_use]
68 pub fn current() -> Self {
69 Self {
70 current_exe: std::env::current_exe().ok(),
71 #[cfg(not(windows))]
72 home_dir: dirs::home_dir(),
73 #[cfg(windows)]
74 local_app_data: std::env::var_os("LOCALAPPDATA").map(PathBuf::from),
75 #[cfg(windows)]
76 program_files: std::env::var_os("ProgramFiles").map(PathBuf::from),
77 #[cfg(windows)]
78 program_files_x86: std::env::var_os("ProgramFiles(x86)").map(PathBuf::from),
79 }
80 }
81}
82
83fn candidate_dirs(context: &BinaryDiscoveryContext, app_name: &str) -> Vec<PathBuf> {
84 let mut dirs = Vec::new();
85
86 #[cfg(windows)]
87 {
88 if let Some(current_exe) = context.current_exe.as_ref() {
91 if let Some(parent) = current_exe.parent() {
92 dirs.push(parent.to_path_buf());
93 }
94 }
95 if let Some(local_app_data) = context.local_app_data.as_ref() {
96 dirs.push(local_app_data.join(app_name).join("bin"));
97 }
98 if let Some(program_files) = context.program_files.as_ref() {
99 dirs.push(program_files.join(app_name));
100 dirs.push(program_files.join(app_name).join("bin"));
101 }
102 if let Some(program_files_x86) = context.program_files_x86.as_ref() {
103 dirs.push(program_files_x86.join(app_name));
104 dirs.push(program_files_x86.join(app_name).join("bin"));
105 }
106 }
107
108 #[cfg(not(windows))]
109 {
110 let _ = app_name;
111 if let Some(home_dir) = context.home_dir.as_ref() {
122 dirs.push(home_dir.join(".local").join("bin"));
123 }
124 dirs.push(PathBuf::from("/opt/homebrew/bin"));
125 dirs.push(PathBuf::from("/usr/local/bin"));
126 dirs.push(PathBuf::from("/usr/bin"));
127 if let Some(current_exe) = context.current_exe.as_ref() {
130 if let Some(parent) = current_exe.parent() {
131 dirs.push(parent.to_path_buf());
132 }
133 }
134 }
135
136 let mut unique_dirs = Vec::new();
137 for dir in dirs {
138 if !unique_dirs.iter().any(|existing| existing == &dir) {
139 unique_dirs.push(dir);
140 }
141 }
142 unique_dirs
143}
144
145#[must_use]
154pub fn find_trusted_binary_with_context(
155 binary_name: &str,
156 app_name: &str,
157 context: &BinaryDiscoveryContext,
158) -> Option<PathBuf> {
159 candidate_dirs(context, app_name)
160 .into_iter()
161 .map(|dir| dir.join(binary_name))
162 .find_map(|candidate| resolve_trusted_binary_candidate(&candidate))
163}
164
165#[must_use]
171pub fn find_trusted_binary(binary_name: &str, app_name: &str) -> Option<PathBuf> {
172 find_trusted_binary_with_context(binary_name, app_name, &BinaryDiscoveryContext::current())
173}
174
175fn resolve_trusted_binary_candidate(path: &std::path::Path) -> Option<PathBuf> {
182 let resolved = path.canonicalize().ok()?;
183 if resolved.is_file() && candidate_looks_executable(&resolved) {
184 Some(path.to_path_buf())
185 } else {
186 None
187 }
188}
189
190#[cfg(unix)]
191fn candidate_looks_executable(path: &std::path::Path) -> bool {
192 use std::os::unix::fs::PermissionsExt;
193 std::fs::metadata(path)
194 .map(|metadata| metadata.permissions().mode() & 0o111 != 0)
195 .unwrap_or(false)
196}
197
198#[cfg(windows)]
199fn candidate_looks_executable(path: &std::path::Path) -> bool {
200 path.extension()
201 .is_some_and(|extension| extension.eq_ignore_ascii_case("exe"))
202 && has_pe_header(path)
203}
204
205#[cfg(windows)]
206fn has_pe_header(path: &std::path::Path) -> bool {
207 let Ok(mut file) = std::fs::File::open(path) else {
208 return false;
209 };
210 let mut header = [0_u8; 2];
211 file.read_exact(&mut header).is_ok() && header == *b"MZ"
212}
213
214#[cfg(test)]
215#[allow(clippy::unwrap_used, clippy::panic)]
216mod tests {
217 use super::*;
218 use std::sync::atomic::{AtomicU64, Ordering};
219
220 #[cfg(unix)]
221 use std::os::unix::fs::PermissionsExt;
222
223 static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
224
225 fn test_dir(name: &str) -> PathBuf {
226 let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
227 let dir = std::env::temp_dir().join(format!(
228 "enclaveapp-bin-discovery-test-{}-{}-{name}",
229 std::process::id(),
230 id
231 ));
232 let _unused = std::fs::remove_dir_all(&dir);
233 std::fs::create_dir_all(&dir).unwrap();
234 dir
235 }
236
237 fn write_test_binary(path: &std::path::Path) {
238 #[cfg(unix)]
239 {
240 std::fs::write(path, b"#!/bin/sh\nexit 0\n").unwrap();
241 let mut permissions = std::fs::metadata(path).unwrap().permissions();
242 permissions.set_mode(0o755);
243 std::fs::set_permissions(path, permissions).unwrap();
244 }
245 #[cfg(windows)]
246 {
247 std::fs::write(path, b"MZtest-binary").unwrap();
248 }
249 }
250
251 #[test]
252 fn finds_current_exe_sibling_as_fallback() {
253 let root = test_dir("sibling");
257 let bin_dir = root.join("bin");
258 std::fs::create_dir_all(&bin_dir).unwrap();
259 #[cfg(not(windows))]
260 let (current_exe, sibling) = (bin_dir.join("myapp"), bin_dir.join("myapp-helper"));
261 #[cfg(windows)]
262 let (current_exe, sibling) = (bin_dir.join("myapp.exe"), bin_dir.join("myapp-helper.exe"));
263 write_test_binary(¤t_exe);
264 write_test_binary(&sibling);
265
266 let context = BinaryDiscoveryContext {
267 current_exe: Some(current_exe),
268 #[cfg(not(windows))]
269 home_dir: Some(root.join("home")),
270 #[cfg(windows)]
271 local_app_data: Some(root.join("LocalAppData")),
272 #[cfg(windows)]
273 program_files: Some(root.join("ProgramFiles")),
274 #[cfg(windows)]
275 program_files_x86: Some(root.join("ProgramFilesX86")),
276 };
277
278 #[cfg(not(windows))]
279 let binary_name = "myapp-helper";
280 #[cfg(windows)]
281 let binary_name = "myapp-helper.exe";
282
283 let found =
284 find_trusted_binary_with_context(binary_name, "myapp", &context).expect("found");
285 assert_eq!(found, sibling);
287
288 std::fs::remove_dir_all(&root).unwrap();
289 }
290
291 #[test]
292 fn app_name_parameterizes_windows_install_dir() {
293 #[cfg(windows)]
297 {
298 let root = test_dir("app-name");
299 let pf_a = root.join("pf").join("appA");
300 let pf_b = root.join("pf").join("appB");
301 std::fs::create_dir_all(&pf_a).unwrap();
302 std::fs::create_dir_all(&pf_b).unwrap();
303 let bin_a = pf_a.join("helper.exe");
304 let bin_b = pf_b.join("helper.exe");
305 write_test_binary(&bin_a);
306 write_test_binary(&bin_b);
307
308 let context = BinaryDiscoveryContext {
309 current_exe: None,
310 local_app_data: None,
311 program_files: Some(root.join("pf")),
312 program_files_x86: None,
313 };
314
315 let a_found = find_trusted_binary_with_context("helper.exe", "appA", &context)
316 .expect("find appA's helper");
317 assert_eq!(a_found, bin_a);
318
319 let b_found = find_trusted_binary_with_context("helper.exe", "appB", &context)
320 .expect("find appB's helper");
321 assert_eq!(b_found, bin_b);
322
323 std::fs::remove_dir_all(&root).unwrap();
324 }
325 #[cfg(not(windows))]
328 {
329 drop(find_trusted_binary("some-binary-name", "my-app"));
330 }
331 }
332
333 #[cfg(not(windows))]
334 #[test]
335 fn candidate_dirs_no_home_no_exe_includes_fixed_unix_dirs() {
336 let ctx = BinaryDiscoveryContext {
337 current_exe: None,
338 home_dir: None,
339 };
340 let dirs = candidate_dirs(&ctx, "myapp");
341 assert!(dirs.contains(&PathBuf::from("/opt/homebrew/bin")));
342 assert!(dirs.contains(&PathBuf::from("/usr/local/bin")));
343 assert!(dirs.contains(&PathBuf::from("/usr/bin")));
344 }
345
346 #[cfg(not(windows))]
347 #[test]
348 fn candidate_dirs_with_home_prepends_user_dirs() {
349 let ctx = BinaryDiscoveryContext {
350 current_exe: None,
351 home_dir: Some(PathBuf::from("/home/user")),
352 };
353 let dirs = candidate_dirs(&ctx, "myapp");
354 assert!(dirs.contains(&PathBuf::from("/home/user/.local/bin")));
355 assert!(!dirs.contains(&PathBuf::from("/home/user/.cargo/bin")));
356 let local_bin_pos = dirs
357 .iter()
358 .position(|d| d == &PathBuf::from("/home/user/.local/bin"))
359 .unwrap();
360 let homebrew_pos = dirs
361 .iter()
362 .position(|d| d == &PathBuf::from("/opt/homebrew/bin"))
363 .unwrap();
364 assert!(local_bin_pos < homebrew_pos);
365 }
366
367 #[cfg(not(windows))]
368 #[test]
369 fn candidate_dirs_exe_sibling_appended_as_fallback() {
370 let ctx = BinaryDiscoveryContext {
371 current_exe: Some(PathBuf::from("/opt/myapp/bin/myapp")),
372 home_dir: None,
373 };
374 let dirs = candidate_dirs(&ctx, "myapp");
375 assert!(dirs.contains(&PathBuf::from("/opt/myapp/bin")));
376 let usr_bin_pos = dirs
377 .iter()
378 .position(|d| d == &PathBuf::from("/usr/bin"))
379 .unwrap();
380 let sibling_pos = dirs
381 .iter()
382 .position(|d| d == &PathBuf::from("/opt/myapp/bin"))
383 .unwrap();
384 assert!(sibling_pos > usr_bin_pos);
385 }
386
387 #[cfg(not(windows))]
388 #[test]
389 fn candidate_dirs_deduplicates_exe_sibling_matching_stable_dir() {
390 let ctx = BinaryDiscoveryContext {
391 current_exe: Some(PathBuf::from("/usr/local/bin/myapp")),
392 home_dir: None,
393 };
394 let dirs = candidate_dirs(&ctx, "myapp");
395 let count = dirs
396 .iter()
397 .filter(|d| *d == &PathBuf::from("/usr/local/bin"))
398 .count();
399 assert_eq!(count, 1);
400 }
401
402 #[cfg(not(windows))]
403 #[test]
404 fn find_trusted_binary_with_context_returns_none_for_absent_binary() {
405 let root = test_dir("no-match");
406 let ctx = BinaryDiscoveryContext {
407 current_exe: None,
408 home_dir: Some(root.clone()),
409 };
410 let result = find_trusted_binary_with_context("definitely-nonexistent-zz99", "myapp", &ctx);
411 assert!(result.is_none());
412 std::fs::remove_dir_all(&root).unwrap();
413 }
414
415 #[cfg(not(windows))]
416 #[test]
417 fn binary_discovery_context_default_all_none() {
418 let ctx = BinaryDiscoveryContext::default();
419 assert!(ctx.current_exe.is_none());
420 assert!(ctx.home_dir.is_none());
421 }
422
423 #[cfg(unix)]
424 #[test]
425 fn non_executable_file_is_not_discovered() {
426 let root = test_dir("non-exec");
427 let bin_dir = root.join("bin");
428 std::fs::create_dir_all(&bin_dir).unwrap();
429 let non_exec = bin_dir.join("myapp-helper");
430 std::fs::write(&non_exec, b"#!/bin/sh\n").unwrap();
432 let mut perms = std::fs::metadata(&non_exec).unwrap().permissions();
433 perms.set_mode(0o644);
434 std::fs::set_permissions(&non_exec, perms).unwrap();
435
436 let current_exe = bin_dir.join("myapp");
437 write_test_binary(¤t_exe);
438
439 let ctx = BinaryDiscoveryContext {
440 current_exe: Some(current_exe),
441 home_dir: Some(root.join("home")),
442 };
443 let result = find_trusted_binary_with_context("myapp-helper", "myapp", &ctx);
444 assert!(result.is_none(), "non-executable should not be found");
445 std::fs::remove_dir_all(&root).unwrap();
446 }
447
448 #[cfg(not(windows))]
449 #[test]
450 fn find_trusted_binary_with_context_prefers_stable_dir_over_exe_sibling() {
451 let root = test_dir("priority");
452 let stable = root.join(".local").join("bin");
453 let sibling_dir = root.join("sibling");
454 std::fs::create_dir_all(&stable).unwrap();
455 std::fs::create_dir_all(&sibling_dir).unwrap();
456
457 let stable_bin = stable.join("mytool");
458 let sibling_bin = sibling_dir.join("mytool");
459 write_test_binary(&stable_bin);
460 write_test_binary(&sibling_bin);
461
462 let ctx = BinaryDiscoveryContext {
463 current_exe: Some(sibling_dir.join("myapp")),
464 home_dir: Some(root.clone()),
465 };
466 let found = find_trusted_binary_with_context("mytool", "myapp", &ctx).unwrap();
467 assert_eq!(found, stable_bin);
468 std::fs::remove_dir_all(&root).unwrap();
469 }
470}