1mod build;
2mod custom;
3mod dispatch;
4mod docs;
5mod policy;
6pub(crate) mod types;
7
8use std::collections::HashMap;
9use std::sync::LazyLock;
10
11use crate::parse::Token;
12use crate::verdict::Verdict;
13
14pub use build::{build_registry, load_toml};
15pub(crate) use custom::user_config_level;
16pub use dispatch::dispatch_spec;
17pub use types::{CommandSpec, OwnedPolicy};
18
19use types::DispatchKind;
20
21type HandlerFn = fn(&[Token]) -> Verdict;
22
23static CMD_HANDLERS: LazyLock<HashMap<&'static str, HandlerFn>> =
24 LazyLock::new(crate::handlers::custom_cmd_handlers);
25
26static SUB_HANDLERS: LazyLock<HashMap<&'static str, HandlerFn>> =
27 LazyLock::new(crate::handlers::custom_sub_handlers);
28
29static TOML_REGISTRY: LazyLock<HashMap<String, CommandSpec>> = LazyLock::new(||
30 include!(concat!(env!("OUT_DIR"), "/toml_includes.rs"))
31);
32
33static CUSTOM_REGISTRY: LazyLock<HashMap<String, CommandSpec>> = LazyLock::new(|| {
34 let mut map = HashMap::new();
35 custom::apply_custom(&mut map);
36 map
37});
38
39pub fn toml_dispatch(tokens: &[Token]) -> Option<Verdict> {
40 let cmd = tokens[0].command_name();
41 TOML_REGISTRY.get(cmd).map(|spec| dispatch_spec(tokens, spec))
42}
43
44pub fn custom_dispatch(tokens: &[Token]) -> Option<Verdict> {
49 let cmd = tokens[0].command_name();
50 CUSTOM_REGISTRY.get(cmd).map(|spec| dispatch_spec(tokens, spec))
51}
52
53pub fn canonical_name(cmd: &str) -> &str {
59 CUSTOM_REGISTRY
60 .get(cmd)
61 .or_else(|| TOML_REGISTRY.get(cmd))
62 .map_or(cmd, |spec| spec.name.as_str())
63}
64
65pub(crate) fn command_path_gate(cmd: &str) -> Option<&'static crate::pathgate::RoleSpec> {
69 CUSTOM_REGISTRY
70 .get(cmd)
71 .or_else(|| TOML_REGISTRY.get(cmd))
72 .and_then(|spec| spec.path_gate.as_ref())
73}
74
75pub(crate) fn command_behavior(cmd: &str) -> Option<&'static crate::registry::types::BehaviorSpec> {
80 CUSTOM_REGISTRY
81 .get(cmd)
82 .or_else(|| TOML_REGISTRY.get(cmd))
83 .and_then(|spec| spec.behavior.as_ref())
84}
85
86pub(crate) fn sub_archetypes(tokens: &[Token]) -> Option<Vec<&'static str>> {
93 let cmd = canonical_name(tokens.first()?.command_name());
94 let spec = CUSTOM_REGISTRY.get(cmd).or_else(|| TOML_REGISTRY.get(cmd))?;
95 let sub = walk_to_profiled_sub(&tokens[1..], &spec.kind)?;
96 let mut out = vec![sub.profile.as_deref()?];
97 for flag in &sub.flags {
98 if flag_escalates(tokens, flag) {
99 out.push(flag.classifies.as_str());
100 }
101 }
102 Some(out)
103}
104
105pub(crate) fn command_flag_archetypes(tokens: &[Token]) -> Option<Vec<&'static str>> {
111 let cmd = canonical_name(tokens.first()?.command_name());
112 let spec = CUSTOM_REGISTRY.get(cmd).or_else(|| TOML_REGISTRY.get(cmd))?;
113 let present: Vec<&'static str> = spec
114 .archetype_flags
115 .iter()
116 .filter(|f| flag_escalates(tokens, f))
117 .map(|f| f.classifies.as_str())
118 .collect();
119 (!present.is_empty()).then_some(present)
120}
121
122fn flag_escalates(tokens: &[Token], flag: &types::FlagProvenance) -> bool {
127 if flag.when_absent {
128 return !flag_is_affirmatively_set(tokens, &flag.name);
132 }
133 let Some(prefix) = flag.value_prefix.as_deref() else {
134 return flag_present(tokens, &flag.name);
135 };
136 tokens.windows(2).any(|w| w[0].as_str() == flag.name && w[1].as_str().starts_with(prefix))
138 || tokens.iter().any(|t| {
140 t.as_str()
141 .strip_prefix(flag.name.as_str())
142 .and_then(|r| r.strip_prefix('='))
143 .is_some_and(|v| v.starts_with(prefix))
144 })
145}
146
147fn flag_is_affirmatively_set(tokens: &[Token], flag: &str) -> bool {
152 let neg = format!("--no-{}", flag.trim_start_matches('-'));
153 let mut set = false;
154 for t in tokens {
155 let s = t.as_str();
156 if s == flag {
157 set = true;
158 } else if let Some(v) = s.strip_prefix(flag).and_then(|r| r.strip_prefix('=')) {
159 set = !matches!(v.to_ascii_lowercase().as_str(), "false" | "0" | "no" | "off" | "");
160 } else if s == neg {
161 set = false;
162 }
163 }
164 set
165}
166
167fn walk_to_profiled_sub(
168 remaining: &[Token],
169 kind: &'static DispatchKind,
170) -> Option<&'static types::SubSpec> {
171 let subs = match kind {
172 DispatchKind::Branching { subs, .. } | DispatchKind::Custom { subs, .. } => subs,
173 _ => return None,
174 };
175 let arg = remaining.first()?;
176 let sub = subs.iter().find(|s| s.name == arg.as_str())?;
177 walk_to_profiled_sub(&remaining[1..], &sub.kind).or_else(|| sub.profile.is_some().then_some(sub))
179}
180
181fn walk_to_profiled_sub_rest<'a>(
184 remaining: &'a [Token],
185 kind: &'static DispatchKind,
186) -> Option<(&'static types::SubSpec, &'a [Token])> {
187 let subs = match kind {
188 DispatchKind::Branching { subs, .. } | DispatchKind::Custom { subs, .. } => subs,
189 _ => return None,
190 };
191 let arg = remaining.first()?;
192 let sub = subs.iter().find(|s| s.name == arg.as_str())?;
193 let rest = &remaining[1..];
194 walk_to_profiled_sub_rest(rest, &sub.kind).or_else(|| sub.profile.is_some().then_some((sub, rest)))
195}
196
197pub(crate) fn sub_destination_token(tokens: &[Token]) -> Option<Option<&str>> {
207 let cmd = canonical_name(tokens.first()?.command_name());
208 let spec = CUSTOM_REGISTRY.get(cmd).or_else(|| TOML_REGISTRY.get(cmd))?;
209 let (sub, rest) = walk_to_profiled_sub_rest(&tokens[1..], &spec.kind)?;
210 if !sub.network_destination {
211 return None;
212 }
213 if let Some(flag) = sub.destination_flag.as_deref()
217 && let Some(v) = flag_value(tokens, flag)
218 {
219 return Some(Some(v));
220 }
221 Some(rest.iter().map(Token::as_str).find(|t| !t.starts_with('-')))
222}
223
224fn flag_value<'a>(tokens: &'a [Token], flag: &str) -> Option<&'a str> {
226 if let Some(v) = tokens.iter().find_map(|t| t.as_str().strip_prefix(flag).and_then(|r| r.strip_prefix('='))) {
227 return Some(v);
228 }
229 tokens.windows(2).find(|w| w[0].as_str() == flag).map(|w| w[1].as_str())
230}
231
232pub(crate) fn sub_output_path_token(tokens: &[Token]) -> Option<&str> {
242 let cmd = canonical_name(tokens.first()?.command_name());
243 let spec = CUSTOM_REGISTRY.get(cmd).or_else(|| TOML_REGISTRY.get(cmd))?;
244 let (sub, _rest) = walk_to_profiled_sub_rest(&tokens[1..], &spec.kind)?;
245 sub.output_path_flags.iter().find_map(|f| output_flag_value(tokens, f))
246}
247
248fn output_flag_value<'a>(tokens: &'a [Token], flag: &'a str) -> Option<&'a str> {
252 if let Some(v) = flag_value(tokens, flag) {
253 return Some(v);
254 }
255 if flag.len() == 2 && flag.starts_with('-') && !flag.starts_with("--") {
256 return tokens
257 .iter()
258 .find_map(|t| t.as_str().strip_prefix(flag).filter(|r| !r.is_empty()));
259 }
260 None
261}
262
263fn flag_present(tokens: &[Token], flag: &str) -> bool {
270 let short_char = (flag.len() == 2 && flag.as_bytes()[0] == b'-').then(|| flag.as_bytes()[1]);
272 tokens.iter().any(|t| {
273 let s = t.as_str();
274 if s == flag || s.strip_prefix(flag).is_some_and(|rest| rest.starts_with('=')) {
275 return true;
276 }
277 matches!(short_char, Some(c)
280 if s.len() > 1 && s.as_bytes()[0] == b'-' && s.as_bytes()[1] != b'-'
281 && s[1..].split('=').next().is_some_and(|run| run.as_bytes().contains(&c)))
282 })
283}
284
285pub fn toml_command_names() -> Vec<&'static str> {
286 TOML_REGISTRY
287 .keys()
288 .map(|k| k.as_str())
289 .collect()
290}
291
292#[cfg(test)]
295pub(crate) fn corpus_examples()
296-> Vec<(&'static str, &'static [String], &'static [String])> {
297 TOML_REGISTRY
298 .iter()
299 .map(|(name, spec)| {
300 (name.as_str(), spec.examples_safe.as_slice(), spec.examples_denied.as_slice())
301 })
302 .collect()
303}
304
305pub fn try_sub_dispatch(cmd_name: &str, tokens: &[Token]) -> Option<Verdict> {
310 let spec = handler_spec(cmd_name)?;
311 let DispatchKind::Custom { subs, .. } = &spec.kind else {
312 return None;
313 };
314 let arg = tokens.get(1)?.as_str();
315 let sub = subs.iter().find(|s| s.name == arg)?;
316 Some(dispatch::dispatch_sub_kind(&tokens[1..], &sub.kind))
317}
318
319pub fn try_fallback_grammar(cmd_name: &str, tokens: &[Token]) -> Option<Verdict> {
322 let spec = handler_spec(cmd_name)?;
323 let DispatchKind::Custom { fallback, .. } = &spec.kind else {
324 return None;
325 };
326 let f = fallback.as_ref()?;
327 Some(dispatch::dispatch_fallback(tokens, f))
328}
329
330pub fn try_matrix_dispatch(cmd_name: &str, tokens: &[Token]) -> Option<Verdict> {
339 let spec = handler_spec(cmd_name)?;
340 let DispatchKind::Custom { matrices, handler_policies, .. } = &spec.kind else {
341 return None;
342 };
343 let parent = tokens.get(1)?.as_str();
344 let action = tokens.get(2)?.as_str();
345 for matrix in matrices {
346 if !matrix.parents.iter().any(|p| p == parent) {
347 continue;
348 }
349 let Some(action_spec) = matrix.actions.get(action) else { continue; };
350 if let Some(long) = action_spec.guard.as_deref()
351 && !crate::parse::has_flag(&tokens[2..], action_spec.guard_short.as_deref(), Some(long))
352 {
353 return Some(Verdict::Denied);
354 }
355 let Some(policy) = handler_policies.get(&action_spec.policy_key) else {
356 return Some(Verdict::Denied);
357 };
358 return Some(dispatch::dispatch_matrix_action(&tokens[2..], policy, matrix.level));
359 }
360 None
361}
362
363pub fn check_handler_policy(cmd_name: &str, key: &str, tokens: &[Token]) -> bool {
370 let Some(spec) = handler_spec(cmd_name) else { return false; };
371 let DispatchKind::Custom { handler_policies, .. } = &spec.kind else {
372 return false;
373 };
374 let Some(policy) = handler_policies.get(key) else { return false; };
375 dispatch::check_handler_policy_owned(tokens, policy)
376}
377
378fn handler_spec(cmd_name: &str) -> Option<&'static CommandSpec> {
379 CUSTOM_REGISTRY
380 .get(cmd_name)
381 .or_else(|| TOML_REGISTRY.get(cmd_name))
382}
383
384pub fn is_eval_safe_invocation(tokens: &[Token]) -> bool {
401 if tokens.is_empty() {
402 return false;
403 }
404 let cmd = tokens[0].command_name();
405 let Some(spec) = CUSTOM_REGISTRY.get(cmd).or_else(|| TOML_REGISTRY.get(cmd)) else {
406 return false;
407 };
408 is_eval_safe_for_spec(spec, tokens)
409}
410
411pub(crate) fn is_eval_safe_for_spec(spec: &CommandSpec, tokens: &[Token]) -> bool {
415 if tokens.is_empty() {
416 return false;
417 }
418 walk_to_eval_safe_leaf(
419 &tokens[1..],
420 &spec.kind,
421 spec.eval_safe,
422 &spec.eval_safe_flags,
423 &spec.eval_safe_flag_values,
424 &spec.eval_safe_required_flags,
425 )
426}
427
428fn walk_to_eval_safe_leaf(
429 remaining: &[Token],
430 kind: &DispatchKind,
431 eval_safe: bool,
432 eval_safe_flags: &[String],
433 eval_safe_flag_values: &std::collections::HashMap<String, Vec<String>>,
434 eval_safe_required_flags: &[String],
435) -> bool {
436 let subs_opt = match kind {
437 DispatchKind::Branching { subs, .. } | DispatchKind::Custom { subs, .. } => Some(subs),
438 _ => None,
439 };
440 if let Some(subs) = subs_opt
441 && let Some(arg) = remaining.first()
442 && let Some(sub) = subs.iter().find(|s| s.name == arg.as_str())
443 {
444 return walk_to_eval_safe_leaf(
445 &remaining[1..],
446 &sub.kind,
447 sub.eval_safe,
448 &sub.eval_safe_flags,
449 &sub.eval_safe_flag_values,
450 &sub.eval_safe_required_flags,
451 );
452 }
453 if !eval_safe {
454 return false;
455 }
456 let mut i = 0;
457 let mut seen_required = false;
458 while i < remaining.len() {
459 let s = remaining[i].as_str();
460 if !s.starts_with('-') {
461 i += 1;
462 continue;
463 }
464 let (bare, eq_value) = match s.split_once('=') {
465 Some((k, v)) => (k, Some(v)),
466 None => (s, None),
467 };
468 if !eval_safe_flags.iter().any(|f| f == bare) {
469 return false;
470 }
471 if eval_safe_required_flags.iter().any(|f| f == bare) {
472 seen_required = true;
473 }
474 if let Some(allowed) = eval_safe_flag_values.get(bare) {
475 let value: &str = if let Some(v) = eq_value {
485 v
486 } else if let Some(next) = remaining.get(i + 1) {
487 let v = next.as_str();
488 i += 1;
489 v
490 } else {
491 return false;
492 };
493 if value.is_empty() {
500 return false;
501 }
502 if !allowed.is_empty() && !allowed.iter().any(|av| av == value) {
503 return false;
504 }
505 }
506 i += 1;
507 }
508 if !eval_safe_required_flags.is_empty() && !seen_required {
509 return false;
510 }
511 true
512}
513
514pub fn toml_command_docs() -> Vec<crate::docs::CommandDoc> {
515 TOML_REGISTRY
516 .iter()
517 .filter(|(key, spec)| *key == &spec.name)
518 .map(|(_, spec)| spec.to_command_doc())
519 .collect()
520}
521
522#[cfg(test)]
523mod tests;