use std::ffi::OsStr;
use std::os::unix::fs::PermissionsExt;
use std::path::{Component, Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outcome {
PrefixFirst,
ExistingFirst,
PrefixAbsent,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Shadow {
pub bin: String,
pub existing: PathBuf,
pub outcome: Outcome,
}
pub fn is_executable(path: &Path) -> bool {
path.metadata()
.is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
}
fn anchor(entry: PathBuf, cwd: &Path) -> PathBuf {
let entry = if entry.as_os_str().is_empty() {
cwd.to_path_buf()
} else if entry.is_relative() {
cwd.join(entry)
} else {
entry
};
let mut out = PathBuf::new();
for component in entry.components() {
match component {
Component::ParentDir => {
out.pop();
}
Component::CurDir => {}
other => out.push(other),
}
}
out
}
pub fn find_shadows(
path_var: &OsStr,
prefix_bin: &Path,
bins: &[String],
cwd: &Path,
exists: impl Fn(&Path) -> bool,
) -> Vec<Shadow> {
let dirs: Vec<PathBuf> = std::env::split_paths(path_var)
.map(|d| anchor(d, cwd))
.collect();
let prefix_bin = anchor(prefix_bin.to_path_buf(), cwd);
let prefix_pos = dirs.iter().position(|d| *d == prefix_bin);
bins.iter()
.filter_map(|bin| {
dirs.iter()
.enumerate()
.filter(|(_, d)| **d != prefix_bin)
.map(|(i, d)| (i, d.join(bin)))
.find(|(_, candidate)| exists(candidate))
.map(|(i, existing)| Shadow {
bin: bin.clone(),
existing,
outcome: match prefix_pos {
Some(p) if p < i => Outcome::PrefixFirst,
Some(_) => Outcome::ExistingFirst,
None => Outcome::PrefixAbsent,
},
})
})
.collect()
}
pub fn owner_of(path: &Path) -> Option<String> {
let queries: [(&str, &[&str]); 3] = [
("/usr/bin/pacman", &["-Qo"]),
("/usr/bin/rpm", &["-qf"]),
("/usr/bin/dpkg", &["-S"]),
];
for (tool, args) in queries {
let Ok(output) = Command::new(tool).args(args).arg(path).output() else {
continue;
};
if !output.status.success() {
continue;
}
let line = String::from_utf8_lossy(&output.stdout)
.lines()
.next()
.unwrap_or_default()
.trim()
.to_owned();
if !line.is_empty() {
return Some(line);
}
}
None
}
fn terminal_text(text: &str) -> String {
text.chars()
.map(|c| if c.is_control() { ' ' } else { c })
.collect::<String>()
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
}
pub fn describe(shadow: &Shadow, prefix_bin: &Path, owner: Option<&str>) -> String {
let prefix_bin = terminal_text(&prefix_bin.to_string_lossy());
let existing_dir = shadow
.existing
.parent()
.map(|d| terminal_text(&d.to_string_lossy()))
.unwrap_or_default();
let who = owner.map_or(String::new(), |o| format!(" ({})", terminal_text(o)));
let verdict = match shadow.outcome {
Outcome::PrefixFirst => format!("{prefix_bin} precedes {existing_dir} in PATH"),
Outcome::ExistingFirst => format!("{existing_dir} precedes {prefix_bin} in PATH"),
Outcome::PrefixAbsent => format!("{prefix_bin} is not on PATH"),
};
format!(
"`{}` already exists as {}{who}; {verdict}",
terminal_text(&shadow.bin),
terminal_text(&shadow.existing.to_string_lossy())
)
}
#[cfg(test)]
mod tests {
use super::*;
fn bins(names: &[&str]) -> Vec<String> {
names.iter().map(|s| (*s).to_owned()).collect()
}
#[test]
fn first_hit_outside_prefix_wins_and_order_decides() {
let path = OsStr::new("/usr/local/bin:/usr/bin:/bin");
let prefix_bin = Path::new("/usr/local/bin");
let exists = |p: &Path| {
matches!(
p.to_str().unwrap(),
"/usr/local/bin/rg" | "/usr/bin/rg" | "/bin/rg" | "/usr/bin/fd"
)
};
let cwd = Path::new("/home/u");
let found = find_shadows(path, prefix_bin, &bins(&["rg", "fd", "bat"]), cwd, exists);
assert_eq!(
found,
[
Shadow {
bin: "rg".to_owned(),
existing: PathBuf::from("/usr/bin/rg"),
outcome: Outcome::PrefixFirst,
},
Shadow {
bin: "fd".to_owned(),
existing: PathBuf::from("/usr/bin/fd"),
outcome: Outcome::PrefixFirst,
},
]
);
}
#[test]
fn prefix_later_and_prefix_absent_are_different_outcomes() {
let exists = |p: &Path| p == Path::new("/usr/bin/rg");
let cwd = Path::new("/home/u");
let path = OsStr::new("/usr/bin:/usr/local/bin");
let found = find_shadows(
path,
Path::new("/usr/local/bin"),
&bins(&["rg"]),
cwd,
exists,
);
assert_eq!(found[0].outcome, Outcome::ExistingFirst);
let path = OsStr::new("/usr/bin:/bin");
let found = find_shadows(
path,
Path::new("/opt/tools/bin"),
&bins(&["rg"]),
cwd,
exists,
);
assert_eq!(found[0].outcome, Outcome::PrefixAbsent);
}
#[test]
fn path_entries_are_normalized_before_comparison() {
let exists = |p: &Path| p == Path::new("/usr/local/bin/rg");
let cwd = Path::new("/home/u");
let path = OsStr::new("/usr/local/bin/:/usr/./local/bin");
let found = find_shadows(
path,
Path::new("/usr/local/bin"),
&bins(&["rg"]),
cwd,
exists,
);
assert_eq!(found, Vec::<Shadow>::new());
}
#[test]
fn parent_components_fold_lexically() {
let cwd = Path::new("/home/u");
let exists = |p: &Path| p == Path::new("/usr/local/bin/rg");
let path = OsStr::new("/usr/local/../local/bin:/usr/bin");
let found = find_shadows(
path,
Path::new("/usr/local/bin"),
&bins(&["rg"]),
cwd,
exists,
);
assert_eq!(found, Vec::<Shadow>::new());
assert_eq!(
anchor(PathBuf::from("/../usr/bin"), cwd),
PathBuf::from("/usr/bin")
);
assert_eq!(
anchor(PathBuf::from("../x"), Path::new("/a/b")),
PathBuf::from("/a/x")
);
}
#[test]
fn relative_and_empty_path_entries_resolve_against_cwd() {
let cwd = Path::new("/home/u");
let exists = |p: &Path| p == Path::new("/home/u/local/bin/rg");
let path = OsStr::new("/home/u/local/bin:/usr/bin");
let found = find_shadows(path, Path::new("local/bin"), &bins(&["rg"]), cwd, exists);
assert_eq!(found, Vec::<Shadow>::new());
let exists = |p: &Path| p == Path::new("/home/u/rg");
let path = OsStr::new("/usr/bin::/usr/local/bin");
let found = find_shadows(
path,
Path::new("/usr/local/bin"),
&bins(&["rg"]),
cwd,
exists,
);
assert_eq!(found[0].existing, PathBuf::from("/home/u/rg"));
assert_eq!(found[0].outcome, Outcome::ExistingFirst);
}
#[test]
fn description_names_file_owner_and_outcome() {
let shadow = Shadow {
bin: "rg".to_owned(),
existing: PathBuf::from("/usr/bin/rg"),
outcome: Outcome::PrefixFirst,
};
let text = describe(
&shadow,
Path::new("/usr/local/bin"),
Some("/usr/bin/rg is owned by ripgrep 14.1.1-1"),
);
assert!(
text.starts_with(
"`rg` already exists as /usr/bin/rg (/usr/bin/rg is owned by ripgrep 14.1.1-1);"
),
"{text}"
);
assert!(
text.ends_with("/usr/local/bin precedes /usr/bin in PATH"),
"{text}"
);
let prefix = Path::new("/usr/local/bin");
let later = Shadow {
outcome: Outcome::ExistingFirst,
..shadow
};
let text = describe(&later, prefix, None);
assert!(!text.contains('('), "{text}");
assert!(
text.ends_with("/usr/bin precedes /usr/local/bin in PATH"),
"{text}"
);
let absent = Shadow {
outcome: Outcome::PrefixAbsent,
..later
};
let text = describe(&absent, prefix, None);
assert!(text.ends_with("/usr/local/bin is not on PATH"), "{text}");
for verb in ["run", "found", "execut"] {
assert!(!text.contains(verb), "{verb}: {text}");
}
}
#[test]
fn external_text_never_carries_terminal_controls() {
let shadow = Shadow {
bin: "rg".to_owned(),
existing: PathBuf::from("/opt/\u{1b}[2Jevil/bin/rg"),
outcome: Outcome::PrefixFirst,
};
let text = describe(
&shadow,
Path::new("/usr/local/bin"),
Some("owned by\u{7} ripgrep\r\n14.1.1"),
);
assert!(!text.chars().any(char::is_control), "{text}");
assert!(text.contains("/opt/ [2Jevil/bin/rg"), "{text}");
assert!(text.contains("(owned by ripgrep 14.1.1)"), "{text}");
}
}