Skip to main content

dejavu/exec/
resolve.rs

1//! Anti-recursion real-binary resolution. Walk `PATH` left→right, skipping the
2//! shim dir and dejavu's own dir, and return the first matching executable.
3//!
4//! We return the *path found* (e.g. `entry/pnpm`) and never canonicalize the
5//! exec target — version-manager wrappers (volta/asdf/nvm) dispatch on the name
6//! they are invoked as, so canonicalizing to the underlying `node` would break
7//! them. Canonicalization is only used for the directory-exclusion comparison.
8
9use 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
20/// Find the real binary named `name`, excluding the shim dir and dejavu's dir.
21pub 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
39/// Content-based self-identification: a generated shim always contains the
40/// `DEJAVU_BIN` marker in its first line. Directory exclusion alone is not
41/// enough — a shim can be invoked with no `DEJAVU_SHIM_DIR` in the environment
42/// (GUI apps, global PATH setups, stale shims), and resolving the shim itself
43/// as the "real" binary would recurse forever.
44fn 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    // metadata() follows symlinks — a wrapper symlink to a real file is fine.
56    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        // A shim named `pnpm` sits earlier; the real one is later.
88        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"); // a shim dir NOT excluded by env
105        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        // A real dejavu shim body in a dir the resolver does not know about.
111        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, // wrong exclusion — content check must save us
124            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}