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 Default for CommandChecker {
fn default() -> Self {
Self::new()
}
}
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,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::env::aliases::AliasStore;
fn make_aliases() -> AliasStore {
AliasStore::default()
}
fn checker_env<'a>(path: &'a str, aliases: &'a AliasStore) -> CheckerEnv<'a> {
CheckerEnv { path, aliases }
}
#[test]
fn test_checker_builtin_special() {
let mut checker = CommandChecker::new();
let aliases = make_aliases();
let env = checker_env("", &aliases);
assert_eq!(checker.check("export", &env), CommandExistence::Valid);
assert_eq!(checker.check("cd", &env), CommandExistence::Valid);
assert_eq!(checker.check("echo", &env), CommandExistence::Valid);
assert_eq!(checker.check("true", &env), CommandExistence::Valid);
}
#[test]
fn test_checker_alias() {
let mut checker = CommandChecker::new();
let mut aliases = make_aliases();
aliases.set("ll", "ls -l");
let env = checker_env("", &aliases);
assert_eq!(checker.check("ll", &env), CommandExistence::Valid);
assert_eq!(checker.check("zz", &env), CommandExistence::Invalid);
}
#[test]
fn test_checker_path_search() {
let mut checker = CommandChecker::new();
let aliases = make_aliases();
let path = "/usr/bin:/bin";
let env = checker_env(path, &aliases);
assert_eq!(checker.check("ls", &env), CommandExistence::Valid);
assert_eq!(
checker.check("xyzzy_nonexistent", &env),
CommandExistence::Invalid
);
}
#[test]
fn test_checker_path_cache_invalidation() {
let mut checker = CommandChecker::new();
let aliases = make_aliases();
let env1 = checker_env("/usr/bin:/bin", &aliases);
assert_eq!(checker.check("ls", &env1), CommandExistence::Valid);
let env2 = checker_env("", &aliases);
assert_eq!(checker.check("ls", &env2), CommandExistence::Invalid);
}
#[test]
fn test_checker_direct_path() {
let mut checker = CommandChecker::new();
let aliases = make_aliases();
let env = checker_env("", &aliases);
assert_eq!(checker.check("/bin/sh", &env), CommandExistence::Valid);
assert_eq!(
checker.check("./nonexistent_script_xyz", &env),
CommandExistence::Invalid
);
}
#[test]
fn test_checker_path_with_tempfile() {
use std::fs;
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().expect("create tempdir");
let bin_path = dir.path().join("my_test_cmd");
fs::write(&bin_path, "#!/bin/sh\n").expect("write temp executable");
let mut perms = fs::metadata(&bin_path).expect("metadata").permissions();
perms.set_mode(0o755);
fs::set_permissions(&bin_path, perms).expect("set permissions");
let mut checker = CommandChecker::new();
let aliases = make_aliases();
let path_val = dir.path().to_str().unwrap().to_string();
let env = checker_env(&path_val, &aliases);
assert_eq!(checker.check("my_test_cmd", &env), CommandExistence::Valid);
assert_eq!(
checker.check("nosuchthing", &env),
CommandExistence::Invalid
);
}
}