use std::path::{Path, PathBuf};
const SYSTEM_BIN_DIRS: &[&str] = &["/usr/bin", "/bin", "/usr/sbin", "/sbin"];
const ABSOLUTE_TOOL_DIRS: &[&str] = &["/opt/homebrew/bin", "/usr/local/bin"];
const HOME_RELATIVE_BIN_DIRS: &[&str] = &[".local/bin", ".cargo/bin"];
pub fn daemon_path_dirs() -> Vec<PathBuf> {
let mut dirs: Vec<PathBuf> = Vec::new();
let push = |p: PathBuf, acc: &mut Vec<PathBuf>| {
if !acc.contains(&p) {
acc.push(p);
}
};
for d in ABSOLUTE_TOOL_DIRS {
push(PathBuf::from(d), &mut dirs);
}
if let Some(home) = dirs::home_dir() {
for rel in HOME_RELATIVE_BIN_DIRS {
push(home.join(rel), &mut dirs);
}
}
for d in SYSTEM_BIN_DIRS {
push(PathBuf::from(d), &mut dirs);
}
dirs
}
pub fn daemon_path_env() -> String {
daemon_path_dirs()
.into_iter()
.filter_map(|p| p.to_str().map(str::to_owned))
.collect::<Vec<_>>()
.join(":")
}
pub fn resolve_binary(name: &str) -> Option<PathBuf> {
if name.contains(std::path::MAIN_SEPARATOR) {
let p = PathBuf::from(name);
return p.is_file().then_some(p);
}
if let Some(path_var) = std::env::var_os("PATH") {
for dir in std::env::split_paths(&path_var) {
if let Some(hit) = candidate(&dir, name) {
return Some(hit);
}
}
}
for dir in daemon_path_dirs() {
if let Some(hit) = candidate(&dir, name) {
return Some(hit);
}
}
None
}
fn candidate(dir: &Path, name: &str) -> Option<PathBuf> {
let p = dir.join(name);
if !p.is_file() {
return None;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
match std::fs::metadata(&p) {
Ok(meta) if meta.permissions().mode() & 0o111 != 0 => Some(p),
_ => None,
}
}
#[cfg(not(unix))]
{
Some(p)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn daemon_path_dirs_orders_user_before_system() {
let dirs = daemon_path_dirs();
let pos = |needle: &str| dirs.iter().position(|p| p == &PathBuf::from(needle));
let homebrew = pos("/opt/homebrew/bin").expect("homebrew dir present");
let usr_bin = pos("/usr/bin").expect("/usr/bin present");
assert!(
homebrew < usr_bin,
"Homebrew must precede /usr/bin so it shadows older system copies"
);
}
#[test]
fn daemon_path_dirs_expands_home() {
let home = dirs::home_dir().expect("home dir resolvable in test env");
let dirs = daemon_path_dirs();
assert!(
dirs.contains(&home.join(".local/bin")),
"~/.local/bin must be expanded to the real home"
);
assert!(
dirs.contains(&home.join(".cargo/bin")),
"~/.cargo/bin must be expanded to the real home"
);
assert!(
dirs.iter().all(|p| !p.starts_with("~")),
"launchd does not expand ~; paths must be absolute"
);
}
#[test]
fn daemon_path_dirs_dedupes() {
let dirs = daemon_path_dirs();
let mut sorted = dirs.clone();
sorted.sort();
sorted.dedup();
assert_eq!(
sorted.len(),
dirs.len(),
"daemon_path_dirs must not contain duplicates"
);
}
#[test]
fn daemon_path_env_contains_expected_dirs() {
let env = daemon_path_env();
let home = dirs::home_dir().expect("home dir resolvable in test env");
assert!(env.contains("/opt/homebrew/bin"), "PATH missing Homebrew");
assert!(
env.contains("/usr/local/bin"),
"PATH missing /usr/local/bin"
);
assert!(
env.contains(home.join(".local/bin").to_str().unwrap()),
"PATH missing expanded ~/.local/bin"
);
assert!(
env.contains(home.join(".cargo/bin").to_str().unwrap()),
"PATH missing expanded ~/.cargo/bin"
);
for sys in SYSTEM_BIN_DIRS {
assert!(env.contains(sys), "PATH missing system dir {sys}");
}
}
fn make_temp_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("bin_resolve_{tag}_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create temp dir");
dir
}
fn write_executable(path: &Path) {
std::fs::write(path, b"#!/bin/sh\n").expect("write fixture");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(path).expect("stat fixture").permissions();
perms.set_mode(0o755);
std::fs::set_permissions(path, perms).expect("chmod fixture");
}
}
#[test]
fn resolve_binary_finds_in_well_known_dir() {
let tmp = make_temp_dir("well_known");
let bin = tmp.join("fake-tool-xyz");
write_executable(&bin);
let hit = candidate(&tmp, "fake-tool-xyz");
assert_eq!(hit.as_deref(), Some(bin.as_path()));
let explicit = bin.to_str().expect("utf8 temp path");
assert_eq!(resolve_binary(explicit).as_deref(), Some(bin.as_path()));
std::fs::remove_dir_all(&tmp).ok();
}
#[cfg(unix)]
#[test]
fn candidate_requires_execute_bit_on_unix() {
use std::os::unix::fs::PermissionsExt;
let tmp = make_temp_dir("exec_bit");
let data = tmp.join("not-a-binary");
std::fs::write(&data, b"plain data\n").expect("write data file");
let mut perms = std::fs::metadata(&data).expect("stat data").permissions();
perms.set_mode(0o644);
std::fs::set_permissions(&data, perms).expect("chmod data");
assert_eq!(
candidate(&tmp, "not-a-binary"),
None,
"a non-executable regular file must not resolve as a runnable binary"
);
let exe = tmp.join("a-binary");
write_executable(&exe);
assert_eq!(
candidate(&tmp, "a-binary").as_deref(),
Some(exe.as_path()),
"an executable file must resolve"
);
std::fs::remove_dir_all(&tmp).ok();
}
#[test]
fn resolve_binary_returns_none_for_missing() {
assert!(
resolve_binary("definitely-not-a-real-binary-zzz-1298").is_none(),
"a nonexistent binary must resolve to None"
);
}
#[test]
fn resolve_binary_accepts_absolute_path() {
let sh = PathBuf::from("/bin/sh");
if sh.is_file() {
assert_eq!(resolve_binary("/bin/sh"), Some(sh));
}
assert!(
resolve_binary("/no/such/path/here-1298").is_none(),
"a non-existent explicit path must resolve to None"
);
}
}