use crate::Env;
use crate::builtin::Builtin;
use crate::builtin::Type::{Special, Substitutive};
use crate::function::Function;
use crate::path::PathBuf;
use crate::system::IsExecutableFile;
use crate::variable::Expansion;
use crate::variable::PATH;
use std::ffi::CStr;
use std::ffi::CString;
use std::rc::Rc;
pub enum Target<S> {
Builtin {
builtin: Builtin<S>,
path: CString,
},
Function(Rc<Function<S>>),
External {
path: CString,
},
}
impl<S> Clone for Target<S> {
fn clone(&self) -> Self {
match self {
Self::Builtin { builtin, path } => Self::Builtin {
builtin: *builtin,
path: path.clone(),
},
Self::Function(f) => Self::Function(f.clone()),
Self::External { path } => Self::External { path: path.clone() },
}
}
}
impl<S> PartialEq for Target<S> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(
Self::Builtin {
builtin: l_builtin,
path: l_path,
},
Self::Builtin {
builtin: r_builtin,
path: r_path,
},
) => l_builtin == r_builtin && l_path == r_path,
(Self::Function(l), Self::Function(r)) => l == r,
(Self::External { path: l_path }, Self::External { path: r_path }) => l_path == r_path,
_ => false,
}
}
}
impl<S> Eq for Target<S> {}
impl<S> std::fmt::Debug for Target<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Builtin { builtin, path } => f
.debug_struct("Builtin")
.field("builtin", builtin)
.field("path", path)
.finish(),
Self::Function(func) => f.debug_tuple("Function").field(func).finish(),
Self::External { path } => f.debug_struct("External").field("path", path).finish(),
}
}
}
impl<S> From<Rc<Function<S>>> for Target<S> {
#[inline]
fn from(function: Rc<Function<S>>) -> Target<S> {
Target::Function(function)
}
}
impl<S> From<Function<S>> for Target<S> {
#[inline]
fn from(function: Function<S>) -> Target<S> {
Target::Function(function.into())
}
}
pub trait ClassifyEnv<S> {
#[must_use]
fn builtin(&self, name: &str) -> Option<Builtin<S>>;
#[must_use]
fn function(&self, name: &str) -> Option<&Rc<Function<S>>>;
}
pub trait PathEnv {
#[must_use]
fn path(&self) -> Expansion<'_>;
#[must_use]
fn is_executable_file(&self, path: &CStr) -> bool;
}
impl<S: IsExecutableFile> PathEnv for Env<S> {
fn path(&self) -> Expansion<'_> {
self.variables
.get(PATH)
.and_then(|var| {
assert_eq!(var.quirk, None, "PATH does not support quirks");
var.value.as_ref()
})
.into()
}
fn is_executable_file(&self, path: &CStr) -> bool {
self.system.is_executable_file(path)
}
}
impl<S> ClassifyEnv<S> for Env<S> {
fn builtin(&self, name: &str) -> Option<Builtin<S>> {
self.builtins.get(name).copied()
}
#[inline]
fn function(&self, name: &str) -> Option<&Rc<Function<S>>> {
self.functions.get(name)
}
}
#[must_use]
pub fn search<S, E: ClassifyEnv<S> + PathEnv>(env: &mut E, name: &str) -> Option<Target<S>> {
let mut target = classify(env, name);
'fill_path: {
let path = match &mut target {
Target::Builtin { builtin, path } if builtin.r#type == Substitutive => {
path
}
Target::External { path } => {
if name.contains('/') {
*path = CString::new(name).ok()?;
break 'fill_path;
} else {
path
}
}
Target::Builtin { .. } | Target::Function(_) => {
break 'fill_path;
}
};
if let Some(real_path) = search_path(env, name) {
*path = real_path;
} else {
return None;
}
}
Some(target)
}
#[must_use]
pub fn classify<S, E: ClassifyEnv<S>>(env: &E, name: &str) -> Target<S> {
if name.contains('/') {
return Target::External {
path: CString::default(),
};
}
let builtin = env.builtin(name);
if let Some(builtin) = builtin {
if builtin.r#type == Special {
let path = CString::default();
return Target::Builtin { builtin, path };
}
}
if let Some(function) = env.function(name) {
return Rc::clone(function).into();
}
if let Some(builtin) = builtin {
let path = CString::default();
return Target::Builtin { builtin, path };
}
Target::External {
path: CString::default(),
}
}
#[must_use]
pub fn search_path<E: PathEnv>(env: &mut E, name: &str) -> Option<CString> {
env.path()
.split()
.filter_map(|dir| {
let candidate = PathBuf::from_iter([dir, name])
.into_unix_string()
.into_vec();
CString::new(candidate).ok()
})
.find(|path| env.is_executable_file(path))
}
#[allow(clippy::field_reassign_with_default)]
#[cfg(test)]
mod tests {
use super::*;
use crate::builtin::Type::{Elective, Extension, Mandatory};
use crate::function::{FunctionBody, FunctionBodyObject, FunctionSet};
use crate::source::Location;
use crate::variable::Value;
use assert_matches::assert_matches;
use std::collections::HashMap;
use std::collections::HashSet;
#[derive(Default)]
struct DummyEnv {
builtins: HashMap<&'static str, Builtin<()>>,
functions: FunctionSet<()>,
path: Expansion<'static>,
executables: HashSet<String>,
}
impl PathEnv for DummyEnv {
fn path(&self) -> Expansion<'_> {
self.path.as_ref()
}
fn is_executable_file(&self, path: &CStr) -> bool {
if let Ok(path) = path.to_str() {
self.executables.contains(path)
} else {
false
}
}
}
impl ClassifyEnv<()> for DummyEnv {
fn builtin(&self, name: &str) -> Option<Builtin<()>> {
self.builtins.get(name).copied()
}
fn function(&self, name: &str) -> Option<&Rc<Function<()>>> {
self.functions.get(name)
}
}
#[derive(Clone, Debug)]
struct FunctionBodyStub;
impl std::fmt::Display for FunctionBodyStub {
fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
unreachable!()
}
}
impl<S> FunctionBody<S> for FunctionBodyStub {
async fn execute(&self, _: &mut Env<S>) -> crate::semantics::Result {
unreachable!()
}
}
fn function_body_stub<S>() -> Rc<dyn FunctionBodyObject<S>> {
Rc::new(FunctionBodyStub)
}
#[test]
fn nothing_is_found_in_empty_env() {
let mut env = DummyEnv::default();
let target = search(&mut env, "foo");
assert!(target.is_none(), "target = {target:?}");
}
#[test]
fn nothing_is_found_with_name_unmatched() {
let mut env = DummyEnv::default();
env.builtins
.insert("foo", Builtin::new(Special, |_, _| unreachable!()));
let function = Function::new("foo", function_body_stub(), Location::dummy(""));
env.functions.define(function).unwrap();
let target = search(&mut env, "bar");
assert!(target.is_none(), "target = {target:?}");
}
#[test]
fn classify_defaults_to_external() {
let env = DummyEnv::default();
let target = classify(&env, "foo");
assert_eq!(
target,
Target::External {
path: CString::default()
}
);
}
#[test]
fn special_builtin_is_found() {
let mut env = DummyEnv::default();
let builtin = Builtin::new(Special, |_, _| unreachable!());
env.builtins.insert("foo", builtin);
assert_matches!(
search(&mut env, "foo"),
Some(Target::Builtin { builtin: result, path }) => {
assert_eq!(result.r#type, builtin.r#type);
assert_eq!(*path, *c"");
}
);
assert_matches!(
classify(&env, "foo"),
Target::Builtin { builtin: result, path } => {
assert_eq!(result.r#type, builtin.r#type);
assert_eq!(*path, *c"");
}
);
}
#[test]
fn function_is_found_if_not_hidden_by_special_builtin() {
let mut env = DummyEnv::default();
let function = Rc::new(Function::new(
"foo",
function_body_stub(),
Location::dummy("location"),
));
env.functions.define(function.clone()).unwrap();
assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
assert_eq!(result, function);
});
assert_matches!(classify(&env, "foo"), Target::Function(result) => {
assert_eq!(result, function);
});
}
#[test]
fn special_builtin_takes_priority_over_function() {
let mut env = DummyEnv::default();
let builtin = Builtin::new(Special, |_, _| unreachable!());
env.builtins.insert("foo", builtin);
let function = Function::new("foo", function_body_stub(), Location::dummy("location"));
env.functions.define(function).unwrap();
assert_matches!(
search(&mut env, "foo"),
Some(Target::Builtin { builtin: result, path }) => {
assert_eq!(result.r#type, builtin.r#type);
assert_eq!(*path, *c"");
}
);
assert_matches!(
classify(&env, "foo"),
Target::Builtin { builtin: result, path } => {
assert_eq!(result.r#type, builtin.r#type);
assert_eq!(*path, *c"");
}
);
}
#[test]
fn mandatory_builtin_is_found_if_not_hidden_by_function() {
let mut env = DummyEnv::default();
let builtin = Builtin::new(Mandatory, |_, _| unreachable!());
env.builtins.insert("foo", builtin);
assert_matches!(
search(&mut env, "foo"),
Some(Target::Builtin { builtin: result, path }) => {
assert_eq!(result.r#type, builtin.r#type);
assert_eq!(*path, *c"");
}
);
assert_matches!(
classify(&env, "foo"),
Target::Builtin { builtin: result, path } => {
assert_eq!(result.r#type, builtin.r#type);
assert_eq!(*path, *c"");
}
);
}
#[test]
fn elective_builtin_is_found_if_not_hidden_by_function() {
let mut env = DummyEnv::default();
let builtin = Builtin::new(Elective, |_, _| unreachable!());
env.builtins.insert("foo", builtin);
assert_matches!(
search(&mut env, "foo"),
Some(Target::Builtin { builtin: result, path }) => {
assert_eq!(result.r#type, builtin.r#type);
assert_eq!(*path, *c"");
}
);
assert_matches!(
classify(&env, "foo"),
Target::Builtin { builtin: result, path } => {
assert_eq!(result.r#type, builtin.r#type);
assert_eq!(*path, *c"");
}
);
}
#[test]
fn extension_builtin_is_found_if_not_hidden_by_function_or_option() {
let mut env = DummyEnv::default();
let builtin = Builtin::new(Extension, |_, _| unreachable!());
env.builtins.insert("foo", builtin);
assert_matches!(
search(&mut env, "foo"),
Some(Target::Builtin { builtin: result, path }) => {
assert_eq!(result.r#type, builtin.r#type);
assert_eq!(*path, *c"");
}
);
assert_matches!(
classify(&env, "foo"),
Target::Builtin { builtin: result, path } => {
assert_eq!(result.r#type, builtin.r#type);
assert_eq!(*path, *c"");
}
);
}
#[test]
fn function_takes_priority_over_mandatory_builtin() {
let mut env = DummyEnv::default();
env.builtins
.insert("foo", Builtin::new(Mandatory, |_, _| unreachable!()));
let function = Rc::new(Function::new(
"foo",
function_body_stub(),
Location::dummy("location"),
));
env.functions.define(function.clone()).unwrap();
assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
assert_eq!(result, function);
});
assert_matches!(classify(&env, "foo"), Target::Function(result) => {
assert_eq!(result, function);
});
}
#[test]
fn function_takes_priority_over_elective_builtin() {
let mut env = DummyEnv::default();
env.builtins
.insert("foo", Builtin::new(Elective, |_, _| unreachable!()));
let function = Rc::new(Function::new(
"foo",
function_body_stub(),
Location::dummy("location"),
));
env.functions.define(function.clone()).unwrap();
assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
assert_eq!(result, function);
});
assert_matches!(classify(&env, "foo"), Target::Function(result) => {
assert_eq!(result, function);
});
}
#[test]
fn function_takes_priority_over_extension_builtin() {
let mut env = DummyEnv::default();
env.builtins
.insert("foo", Builtin::new(Extension, |_, _| unreachable!()));
let function = Rc::new(Function::new(
"foo",
function_body_stub(),
Location::dummy("location"),
));
env.functions.define(function.clone()).unwrap();
assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
assert_eq!(result, function);
});
assert_matches!(classify(&env, "foo"), Target::Function(result) => {
assert_eq!(result, function);
});
}
#[test]
fn substitutive_builtin_is_found_if_external_executable_exists() {
let mut env = DummyEnv::default();
let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
env.builtins.insert("foo", builtin);
env.path = Expansion::from("/bin");
env.executables.insert("/bin/foo".to_string());
assert_matches!(
search(&mut env, "foo"),
Some(Target::Builtin { builtin: result, path }) => {
assert_eq!(result.r#type, builtin.r#type);
assert_eq!(*path, *c"/bin/foo");
}
);
assert_matches!(
classify(&env, "foo"),
Target::Builtin { builtin: result, path } => {
assert_eq!(result.r#type, builtin.r#type);
assert_eq!(*path, *c"");
}
);
}
#[test]
fn substitutive_builtin_is_not_found_without_external_executable() {
let mut env = DummyEnv::default();
let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
env.builtins.insert("foo", builtin);
let target = search(&mut env, "foo");
assert!(target.is_none(), "target = {target:?}");
}
#[test]
fn substitutive_builtin_is_classified_even_without_external_executable() {
let mut env = DummyEnv::default();
let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
env.builtins.insert("foo", builtin);
assert_matches!(
classify(&env, "foo"),
Target::Builtin { builtin: result, path } => {
assert_eq!(result.r#type, builtin.r#type);
assert_eq!(*path, *c"");
}
);
}
#[test]
fn function_takes_priority_over_substitutive_builtin() {
let mut env = DummyEnv::default();
let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
env.builtins.insert("foo", builtin);
env.path = Expansion::from("/bin");
env.executables.insert("/bin/foo".to_string());
let function = Rc::new(Function::new(
"foo",
function_body_stub(),
Location::dummy("location"),
));
env.functions.define(function.clone()).unwrap();
assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
assert_eq!(result, function);
});
assert_matches!(classify(&env, "foo"), Target::Function(result) => {
assert_eq!(result, function);
});
}
#[test]
fn external_utility_is_found_if_external_executable_exists() {
let mut env = DummyEnv::default();
env.path = Expansion::from("/bin");
env.executables.insert("/bin/foo".to_string());
assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
assert_eq!(*path, *c"/bin/foo");
});
assert_matches!(classify(&env, "foo"), Target::External { path } => {
assert_eq!(*path, *c"");
});
}
#[test]
fn returns_external_utility_if_name_contains_slash() {
let mut env = DummyEnv::default();
let builtin = Builtin::new(Special, |_, _| unreachable!());
env.builtins.insert("bar/baz", builtin);
assert_matches!(search(&mut env, "bar/baz"), Some(Target::External { path }) => {
assert_eq!(*path, *c"bar/baz");
});
assert_matches!(classify(&env, "bar/baz"), Target::External { path } => {
assert_eq!(*path, *c"");
});
}
#[test]
fn external_target_is_first_executable_found_in_path_scalar() {
let mut env = DummyEnv::default();
env.path = Expansion::from("/usr/local/bin:/usr/bin:/bin");
env.executables.insert("/usr/bin/foo".to_string());
env.executables.insert("/bin/foo".to_string());
assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
assert_eq!(*path, *c"/usr/bin/foo");
});
env.executables.insert("/usr/local/bin/foo".to_string());
assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
assert_eq!(*path, *c"/usr/local/bin/foo");
});
}
#[test]
fn external_target_is_first_executable_found_in_path_array() {
let mut env = DummyEnv::default();
env.path = Expansion::from(Value::array(["/usr/local/bin", "/usr/bin", "/bin"]));
env.executables.insert("/usr/bin/foo".to_string());
env.executables.insert("/bin/foo".to_string());
assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
assert_eq!(*path, *c"/usr/bin/foo");
});
env.executables.insert("/usr/local/bin/foo".to_string());
assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
assert_eq!(*path, *c"/usr/local/bin/foo");
});
}
#[test]
fn empty_string_in_path_names_current_directory() {
let mut env = DummyEnv::default();
env.path = Expansion::from("/x::/y");
env.executables.insert("foo".to_string());
assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
assert_eq!(*path, *c"foo");
});
}
}