mod build;
mod custom;
mod dispatch;
mod docs;
mod policy;
pub(crate) mod types;
use std::collections::HashMap;
use std::sync::LazyLock;
use crate::parse::Token;
use crate::verdict::Verdict;
pub use build::{build_registry, load_toml};
pub(crate) use custom::user_config_level;
pub use dispatch::dispatch_spec;
pub use types::{CommandSpec, OwnedPolicy};
use types::DispatchKind;
type HandlerFn = fn(&[Token]) -> Verdict;
static CMD_HANDLERS: LazyLock<HashMap<&'static str, HandlerFn>> =
LazyLock::new(crate::handlers::custom_cmd_handlers);
static SUB_HANDLERS: LazyLock<HashMap<&'static str, HandlerFn>> =
LazyLock::new(crate::handlers::custom_sub_handlers);
static TOML_REGISTRY: LazyLock<HashMap<String, CommandSpec>> = LazyLock::new(||
include!(concat!(env!("OUT_DIR"), "/toml_includes.rs"))
);
static CUSTOM_REGISTRY: LazyLock<HashMap<String, CommandSpec>> = LazyLock::new(|| {
let mut map = HashMap::new();
custom::apply_custom(&mut map);
map
});
pub fn toml_dispatch(tokens: &[Token]) -> Option<Verdict> {
let cmd = tokens[0].command_name();
TOML_REGISTRY.get(cmd).map(|spec| dispatch_spec(tokens, spec))
}
pub fn custom_dispatch(tokens: &[Token]) -> Option<Verdict> {
let cmd = tokens[0].command_name();
CUSTOM_REGISTRY.get(cmd).map(|spec| dispatch_spec(tokens, spec))
}
pub fn canonical_name(cmd: &str) -> &str {
CUSTOM_REGISTRY
.get(cmd)
.or_else(|| TOML_REGISTRY.get(cmd))
.map_or(cmd, |spec| spec.name.as_str())
}
pub(crate) fn command_path_gate(cmd: &str) -> Option<&'static crate::pathgate::RoleSpec> {
CUSTOM_REGISTRY
.get(cmd)
.or_else(|| TOML_REGISTRY.get(cmd))
.and_then(|spec| spec.path_gate.as_ref())
}
pub(crate) fn command_behavior(cmd: &str) -> Option<&'static crate::registry::types::BehaviorSpec> {
CUSTOM_REGISTRY
.get(cmd)
.or_else(|| TOML_REGISTRY.get(cmd))
.and_then(|spec| spec.behavior.as_ref())
}
pub(crate) fn sub_archetypes(tokens: &[Token]) -> Option<Vec<&'static str>> {
let cmd = canonical_name(tokens.first()?.command_name());
let spec = CUSTOM_REGISTRY.get(cmd).or_else(|| TOML_REGISTRY.get(cmd))?;
let sub = walk_to_profiled_sub(&tokens[1..], &spec.kind)?;
let mut out = vec![sub.profile.as_deref()?];
for flag in &sub.flags {
if flag_escalates(tokens, flag) {
out.push(flag.classifies.as_str());
}
}
Some(out)
}
pub(crate) fn command_flag_archetypes(tokens: &[Token]) -> Option<Vec<&'static str>> {
let cmd = canonical_name(tokens.first()?.command_name());
let spec = CUSTOM_REGISTRY.get(cmd).or_else(|| TOML_REGISTRY.get(cmd))?;
let present: Vec<&'static str> = spec
.archetype_flags
.iter()
.filter(|f| flag_escalates(tokens, f))
.map(|f| f.classifies.as_str())
.collect();
(!present.is_empty()).then_some(present)
}
fn flag_escalates(tokens: &[Token], flag: &types::FlagProvenance) -> bool {
if flag.when_absent {
return !flag_is_affirmatively_set(tokens, &flag.name);
}
let Some(prefix) = flag.value_prefix.as_deref() else {
return flag_present(tokens, &flag.name);
};
tokens.windows(2).any(|w| w[0].as_str() == flag.name && w[1].as_str().starts_with(prefix))
|| tokens.iter().any(|t| {
t.as_str()
.strip_prefix(flag.name.as_str())
.and_then(|r| r.strip_prefix('='))
.is_some_and(|v| v.starts_with(prefix))
})
}
fn flag_is_affirmatively_set(tokens: &[Token], flag: &str) -> bool {
let neg = format!("--no-{}", flag.trim_start_matches('-'));
let mut set = false;
for t in tokens {
let s = t.as_str();
if s == flag {
set = true;
} else if let Some(v) = s.strip_prefix(flag).and_then(|r| r.strip_prefix('=')) {
set = !matches!(v.to_ascii_lowercase().as_str(), "false" | "0" | "no" | "off" | "");
} else if s == neg {
set = false;
}
}
set
}
fn walk_to_profiled_sub(
remaining: &[Token],
kind: &'static DispatchKind,
) -> Option<&'static types::SubSpec> {
let subs = match kind {
DispatchKind::Branching { subs, .. } | DispatchKind::Custom { subs, .. } => subs,
_ => return None,
};
let arg = remaining.first()?;
let sub = subs.iter().find(|s| s.name == arg.as_str())?;
walk_to_profiled_sub(&remaining[1..], &sub.kind).or_else(|| sub.profile.is_some().then_some(sub))
}
fn walk_to_profiled_sub_rest<'a>(
remaining: &'a [Token],
kind: &'static DispatchKind,
) -> Option<(&'static types::SubSpec, &'a [Token])> {
let subs = match kind {
DispatchKind::Branching { subs, .. } | DispatchKind::Custom { subs, .. } => subs,
_ => return None,
};
let arg = remaining.first()?;
let sub = subs.iter().find(|s| s.name == arg.as_str())?;
let rest = &remaining[1..];
walk_to_profiled_sub_rest(rest, &sub.kind).or_else(|| sub.profile.is_some().then_some((sub, rest)))
}
pub(crate) fn sub_destination_token(tokens: &[Token]) -> Option<Option<&str>> {
let cmd = canonical_name(tokens.first()?.command_name());
let spec = CUSTOM_REGISTRY.get(cmd).or_else(|| TOML_REGISTRY.get(cmd))?;
let (sub, rest) = walk_to_profiled_sub_rest(&tokens[1..], &spec.kind)?;
if !sub.network_destination {
return None;
}
if let Some(flag) = sub.destination_flag.as_deref()
&& let Some(v) = flag_value(tokens, flag)
{
return Some(Some(v));
}
Some(rest.iter().map(Token::as_str).find(|t| !t.starts_with('-')))
}
fn flag_value<'a>(tokens: &'a [Token], flag: &str) -> Option<&'a str> {
if let Some(v) = tokens.iter().find_map(|t| t.as_str().strip_prefix(flag).and_then(|r| r.strip_prefix('='))) {
return Some(v);
}
tokens.windows(2).find(|w| w[0].as_str() == flag).map(|w| w[1].as_str())
}
pub(crate) fn sub_output_path_token(tokens: &[Token]) -> Option<&str> {
let cmd = canonical_name(tokens.first()?.command_name());
let spec = CUSTOM_REGISTRY.get(cmd).or_else(|| TOML_REGISTRY.get(cmd))?;
let (sub, _rest) = walk_to_profiled_sub_rest(&tokens[1..], &spec.kind)?;
sub.output_path_flags.iter().find_map(|f| output_flag_value(tokens, f))
}
fn output_flag_value<'a>(tokens: &'a [Token], flag: &'a str) -> Option<&'a str> {
if let Some(v) = flag_value(tokens, flag) {
return Some(v);
}
if flag.len() == 2 && flag.starts_with('-') && !flag.starts_with("--") {
return tokens
.iter()
.find_map(|t| t.as_str().strip_prefix(flag).filter(|r| !r.is_empty()));
}
None
}
fn flag_present(tokens: &[Token], flag: &str) -> bool {
let short_char = (flag.len() == 2 && flag.as_bytes()[0] == b'-').then(|| flag.as_bytes()[1]);
tokens.iter().any(|t| {
let s = t.as_str();
if s == flag || s.strip_prefix(flag).is_some_and(|rest| rest.starts_with('=')) {
return true;
}
matches!(short_char, Some(c)
if s.len() > 1 && s.as_bytes()[0] == b'-' && s.as_bytes()[1] != b'-'
&& s[1..].split('=').next().is_some_and(|run| run.as_bytes().contains(&c)))
})
}
pub fn toml_command_names() -> Vec<&'static str> {
TOML_REGISTRY
.keys()
.map(|k| k.as_str())
.collect()
}
#[cfg(test)]
pub(crate) fn corpus_examples()
-> Vec<(&'static str, &'static [String], &'static [String])> {
TOML_REGISTRY
.iter()
.map(|(name, spec)| {
(name.as_str(), spec.examples_safe.as_slice(), spec.examples_denied.as_slice())
})
.collect()
}
pub fn try_sub_dispatch(cmd_name: &str, tokens: &[Token]) -> Option<Verdict> {
let spec = handler_spec(cmd_name)?;
let DispatchKind::Custom { subs, .. } = &spec.kind else {
return None;
};
let arg = tokens.get(1)?.as_str();
let sub = subs.iter().find(|s| s.name == arg)?;
Some(dispatch::dispatch_sub_kind(&tokens[1..], &sub.kind))
}
pub fn try_fallback_grammar(cmd_name: &str, tokens: &[Token]) -> Option<Verdict> {
let spec = handler_spec(cmd_name)?;
let DispatchKind::Custom { fallback, .. } = &spec.kind else {
return None;
};
let f = fallback.as_ref()?;
Some(dispatch::dispatch_fallback(tokens, f))
}
pub fn try_matrix_dispatch(cmd_name: &str, tokens: &[Token]) -> Option<Verdict> {
let spec = handler_spec(cmd_name)?;
let DispatchKind::Custom { matrices, handler_policies, .. } = &spec.kind else {
return None;
};
let parent = tokens.get(1)?.as_str();
let action = tokens.get(2)?.as_str();
for matrix in matrices {
if !matrix.parents.iter().any(|p| p == parent) {
continue;
}
let Some(action_spec) = matrix.actions.get(action) else { continue; };
if let Some(long) = action_spec.guard.as_deref()
&& !crate::parse::has_flag(&tokens[2..], action_spec.guard_short.as_deref(), Some(long))
{
return Some(Verdict::Denied);
}
let Some(policy) = handler_policies.get(&action_spec.policy_key) else {
return Some(Verdict::Denied);
};
return Some(dispatch::dispatch_matrix_action(&tokens[2..], policy, matrix.level));
}
None
}
pub fn check_handler_policy(cmd_name: &str, key: &str, tokens: &[Token]) -> bool {
let Some(spec) = handler_spec(cmd_name) else { return false; };
let DispatchKind::Custom { handler_policies, .. } = &spec.kind else {
return false;
};
let Some(policy) = handler_policies.get(key) else { return false; };
dispatch::check_handler_policy_owned(tokens, policy)
}
fn handler_spec(cmd_name: &str) -> Option<&'static CommandSpec> {
CUSTOM_REGISTRY
.get(cmd_name)
.or_else(|| TOML_REGISTRY.get(cmd_name))
}
pub fn is_eval_safe_invocation(tokens: &[Token]) -> bool {
if tokens.is_empty() {
return false;
}
let cmd = tokens[0].command_name();
let Some(spec) = CUSTOM_REGISTRY.get(cmd).or_else(|| TOML_REGISTRY.get(cmd)) else {
return false;
};
is_eval_safe_for_spec(spec, tokens)
}
pub(crate) fn is_eval_safe_for_spec(spec: &CommandSpec, tokens: &[Token]) -> bool {
if tokens.is_empty() {
return false;
}
walk_to_eval_safe_leaf(
&tokens[1..],
&spec.kind,
spec.eval_safe,
&spec.eval_safe_flags,
&spec.eval_safe_flag_values,
&spec.eval_safe_required_flags,
)
}
fn walk_to_eval_safe_leaf(
remaining: &[Token],
kind: &DispatchKind,
eval_safe: bool,
eval_safe_flags: &[String],
eval_safe_flag_values: &std::collections::HashMap<String, Vec<String>>,
eval_safe_required_flags: &[String],
) -> bool {
let subs_opt = match kind {
DispatchKind::Branching { subs, .. } | DispatchKind::Custom { subs, .. } => Some(subs),
_ => None,
};
if let Some(subs) = subs_opt
&& let Some(arg) = remaining.first()
&& let Some(sub) = subs.iter().find(|s| s.name == arg.as_str())
{
return walk_to_eval_safe_leaf(
&remaining[1..],
&sub.kind,
sub.eval_safe,
&sub.eval_safe_flags,
&sub.eval_safe_flag_values,
&sub.eval_safe_required_flags,
);
}
if !eval_safe {
return false;
}
let mut i = 0;
let mut seen_required = false;
while i < remaining.len() {
let s = remaining[i].as_str();
if !s.starts_with('-') {
i += 1;
continue;
}
let (bare, eq_value) = match s.split_once('=') {
Some((k, v)) => (k, Some(v)),
None => (s, None),
};
if !eval_safe_flags.iter().any(|f| f == bare) {
return false;
}
if eval_safe_required_flags.iter().any(|f| f == bare) {
seen_required = true;
}
if let Some(allowed) = eval_safe_flag_values.get(bare) {
let value: &str = if let Some(v) = eq_value {
v
} else if let Some(next) = remaining.get(i + 1) {
let v = next.as_str();
i += 1;
v
} else {
return false;
};
if value.is_empty() {
return false;
}
if !allowed.is_empty() && !allowed.iter().any(|av| av == value) {
return false;
}
}
i += 1;
}
if !eval_safe_required_flags.is_empty() && !seen_required {
return false;
}
true
}
pub fn toml_command_docs() -> Vec<crate::docs::CommandDoc> {
TOML_REGISTRY
.iter()
.filter(|(key, spec)| *key == &spec.name)
.map(|(_, spec)| spec.to_command_doc())
.collect()
}
#[cfg(test)]
mod tests;