use std::collections::HashMap;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use crate::builtin::{BuiltinKind, classify_builtin};
use crate::env::aliases::AliasStore;
pub struct CheckerEnv<'a> {
pub path: &'a str,
pub aliases: &'a AliasStore,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandExistence {
Valid,
Invalid,
}
pub struct CommandChecker {
path_cache: HashMap<String, bool>,
cached_path: String,
}
impl CommandChecker {
pub fn new() -> Self {
Self {
path_cache: HashMap::new(),
cached_path: String::new(),
}
}
pub fn check(&mut self, name: &str, env: &CheckerEnv) -> CommandExistence {
if classify_builtin(name) != BuiltinKind::NotBuiltin {
return CommandExistence::Valid;
}
if env.aliases.get(name).is_some() {
return CommandExistence::Valid;
}
if name.contains('/') {
return if is_executable(Path::new(name)) {
CommandExistence::Valid
} else {
CommandExistence::Invalid
};
}
if env.path != self.cached_path {
self.path_cache.clear();
self.cached_path = env.path.to_string();
}
let found = self
.path_cache
.entry(name.to_string())
.or_insert_with(|| search_path(name, env.path));
if *found {
CommandExistence::Valid
} else {
CommandExistence::Invalid
}
}
}
fn search_path(name: &str, path_var: &str) -> bool {
for dir in path_var.split(':') {
if dir.is_empty() {
continue;
}
let candidate = Path::new(dir).join(name);
if is_executable(&candidate) {
return true;
}
}
false
}
fn is_executable(path: &Path) -> bool {
match std::fs::metadata(path) {
Ok(meta) => meta.is_file() && (meta.permissions().mode() & 0o111 != 0),
Err(_) => false,
}
}