1use super::path::split_path;
10use std::ffi::OsStr;
11use std::os::unix::fs::PermissionsExt;
12use std::path::{Path, PathBuf};
13
14pub struct ResolveEnv<'a> {
15 pub path: &'a OsStr,
16 pub shim_dir: &'a Path,
17 pub dejavu_dir: &'a Path,
18}
19
20pub fn resolve_real(name: &str, env: &ResolveEnv) -> Option<PathBuf> {
22 let shim_canon = std::fs::canonicalize(env.shim_dir).ok();
23 let dejavu_canon = std::fs::canonicalize(env.dejavu_dir).ok();
24
25 for entry in split_path(env.path) {
26 if let Ok(canon) = std::fs::canonicalize(&entry) {
27 if shim_canon.as_ref() == Some(&canon) || dejavu_canon.as_ref() == Some(&canon) {
28 continue;
29 }
30 }
31 let candidate = entry.join(name);
32 if is_executable(&candidate) && !is_dejavu_shim(&candidate) {
33 return Some(candidate);
34 }
35 }
36 None
37}
38
39fn is_dejavu_shim(path: &Path) -> bool {
45 let Ok(mut file) = std::fs::File::open(path) else {
46 return false;
47 };
48 let mut buf = [0u8; 256];
49 let n = std::io::Read::read(&mut file, &mut buf).unwrap_or(0);
50 let head = &buf[..n];
51 head.starts_with(b"#!/bin/sh") && head.windows(10).any(|w| w == b"DEJAVU_BIN")
52}
53
54fn is_executable(path: &Path) -> bool {
55 match std::fs::metadata(path) {
57 Ok(meta) => meta.is_file() && (meta.permissions().mode() & 0o111 != 0),
58 Err(_) => false,
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use std::ffi::OsString;
66 use std::fs;
67 use std::io::Write;
68
69 fn make_exec(dir: &Path, name: &str) {
70 let p = dir.join(name);
71 let mut f = fs::File::create(&p).unwrap();
72 writeln!(f, "#!/bin/sh\ntrue").unwrap();
73 let mut perms = f.metadata().unwrap().permissions();
74 perms.set_mode(0o755);
75 fs::set_permissions(&p, perms).unwrap();
76 }
77
78 #[test]
79 fn skips_shim_dir_and_finds_real() {
80 let tmp = tempfile::tempdir().unwrap();
81 let shim = tmp.path().join("shim");
82 let real = tmp.path().join("real");
83 let dejavu = tmp.path().join("dejavu");
84 for d in [&shim, &real, &dejavu] {
85 fs::create_dir_all(d).unwrap();
86 }
87 make_exec(&shim, "pnpm");
89 make_exec(&real, "pnpm");
90
91 let path = std::env::join_paths([&shim, &real]).unwrap();
92 let env = ResolveEnv {
93 path: &path,
94 shim_dir: &shim,
95 dejavu_dir: &dejavu,
96 };
97 let found = resolve_real("pnpm", &env).unwrap();
98 assert_eq!(found, real.join("pnpm"));
99 }
100
101 #[test]
102 fn skips_shim_by_content_even_outside_known_shim_dir() {
103 let tmp = tempfile::tempdir().unwrap();
104 let rogue = tmp.path().join("rogue"); let real = tmp.path().join("real");
106 let other = tmp.path().join("other");
107 for d in [&rogue, &real, &other] {
108 fs::create_dir_all(d).unwrap();
109 }
110 let p = rogue.join("git");
112 fs::write(
113 &p,
114 "#!/bin/sh\nexec \"${DEJAVU_BIN:-/usr/local/bin/dejavu}\" run --shim-name git -- \"$@\"\n",
115 )
116 .unwrap();
117 fs::set_permissions(&p, fs::Permissions::from_mode(0o755)).unwrap();
118 make_exec(&real, "git");
119
120 let path = std::env::join_paths([&rogue, &real]).unwrap();
121 let env = ResolveEnv {
122 path: &path,
123 shim_dir: &other, dejavu_dir: &other,
125 };
126 let found = resolve_real("git", &env).unwrap();
127 assert_eq!(found, real.join("git"));
128 }
129
130 #[test]
131 fn returns_none_when_absent() {
132 let tmp = tempfile::tempdir().unwrap();
133 let env = ResolveEnv {
134 path: &OsString::from(tmp.path()),
135 shim_dir: tmp.path(),
136 dejavu_dir: tmp.path(),
137 };
138 assert!(resolve_real("definitely-not-a-binary", &env).is_none());
139 }
140}