1use crate::parse::Token;
2use crate::policy::FlagSet;
3use crate::verdict::{SafetyLevel, Verdict};
4
5use super::policy::check_owned;
6use super::types::*;
7use super::{CMD_HANDLERS, SUB_HANDLERS};
8
9type HandlerMap = std::collections::HashMap<&'static str, super::HandlerFn>;
10
11fn short_flag_char(s: &str) -> Option<char> {
12 let bytes = s.as_bytes();
13 if bytes.len() == 2 && bytes[0] == b'-' && bytes[1] != b'-' {
14 s.chars().nth(1)
15 } else {
16 None
17 }
18}
19
20fn is_combined_short(s: &str) -> bool {
21 let bytes = s.as_bytes();
22 bytes.len() > 2 && bytes[0] == b'-' && bytes[1] != b'-'
23}
24
25fn dispatch_first_arg(tokens: &[Token], patterns: &[String], level: SafetyLevel) -> Verdict {
26 if tokens.len() == 2 && (tokens[1] == "--help" || tokens[1] == "-h") {
27 return Verdict::Allowed(SafetyLevel::Inert);
28 }
29 let Some(arg) = tokens.get(1) else {
30 return Verdict::Denied;
31 };
32 let arg_str = arg.as_str();
33 let matches = patterns.iter().any(|p| {
34 if let Some(prefix) = p.strip_suffix('*') {
35 arg_str.starts_with(prefix)
36 } else {
37 arg_str == p
38 }
39 });
40 if matches { Verdict::Allowed(level) } else { Verdict::Denied }
41}
42
43fn dispatch_require_any(
44 tokens: &[Token],
45 require_any: &[String],
46 policy: &OwnedPolicy,
47 level: SafetyLevel,
48 accept_bare_help: bool,
49) -> Verdict {
50 if tokens.len() == 2 {
51 let t = tokens[1].as_str();
52 if t == "--help" || t == "-h" || (accept_bare_help && t == "help") {
53 return Verdict::Allowed(SafetyLevel::Inert);
54 }
55 }
56 let has_required = tokens[1..].iter().any(|t| {
57 require_any.iter().any(|r| {
58 let t_str = t.as_str();
59 if t_str == r.as_str() {
60 return true;
61 }
62 if r.starts_with("--") && t_str.starts_with(&format!("{r}=")) {
63 return true;
64 }
65 if let Some(short_char) = short_flag_char(r)
66 && is_combined_short(t_str)
67 && t_str[1..].contains(short_char)
68 {
69 return true;
70 }
71 false
72 })
73 });
74 if has_required && check_owned(tokens, policy) {
75 Verdict::Allowed(level)
76 } else {
77 Verdict::Denied
78 }
79}
80
81fn skip_pre_flags(
82 tokens: &[Token],
83 pre_standalone: &[String],
84 pre_valued: &[String],
85 start: usize,
86) -> usize {
87 let mut i = start;
88 while i < tokens.len() {
89 let t = &tokens[i];
90 let s = t.as_str();
91 if !s.starts_with('-') {
92 break;
93 }
94 if pre_valued.contains_flag(s) {
95 i += 2;
96 continue;
97 }
98 if let Some((flag, _)) = s.split_once('=')
99 && pre_valued.contains_flag(flag)
100 {
101 i += 1;
102 continue;
103 }
104 if pre_standalone.contains_flag(s) {
105 i += 1;
106 continue;
107 }
108 let bytes = s.as_bytes();
112 if bytes.len() > 2
113 && bytes[1] != b'-'
114 && bytes[1..].iter().all(|&b| pre_standalone.contains_short(b))
115 {
116 i += 1;
117 continue;
118 }
119 break;
120 }
121 i
122}
123
124fn dispatch_branching(
125 tokens: &[Token],
126 subs: &[SubSpec],
127 bare_flags: &[String],
128 bare_ok: bool,
129 pre_flags: (&[String], &[String]),
130 first_arg: &[String],
131 first_arg_level: SafetyLevel,
132 credential_first_arg: &[String],
133) -> Verdict {
134 let (pre_standalone, pre_valued) = pre_flags;
135 let start = skip_pre_flags(tokens, pre_standalone, pre_valued, 1);
136 if start >= tokens.len() {
137 return if bare_ok { Verdict::Allowed(SafetyLevel::Inert) } else { Verdict::Denied };
138 }
139 let arg = tokens[start].as_str();
140 let is_bare_flag = bare_flags.iter().any(|f| f == arg)
141 || (bare_flags.is_empty() && matches!(arg, "--help" | "-h"));
142 if is_bare_flag {
143 let after = skip_pre_flags(tokens, pre_standalone, pre_valued, start + 1);
144 if after >= tokens.len() {
145 return Verdict::Allowed(SafetyLevel::Inert);
146 }
147 if bare_flags.is_empty() {
148 return Verdict::Denied;
149 }
150 }
151 if let Some(sub) = subs.iter().find(|s| s.name == arg) {
152 return dispatch_kind(&tokens[start..], &sub.kind, &SUB_HANDLERS);
153 }
154 let glob_match = |p: &str| match p.strip_suffix('*') {
155 Some(prefix) => arg.starts_with(prefix),
156 None => arg == p,
157 };
158 let glob_match_ci = |p: &str| match p.strip_suffix('*') {
165 Some(prefix) => {
166 arg.len() >= prefix.len()
167 && arg.as_bytes()[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes())
168 }
169 None => arg.eq_ignore_ascii_case(p),
170 };
171 if credential_first_arg.iter().any(|p| glob_match_ci(p)) {
172 return Verdict::Denied;
173 }
174 if !first_arg.is_empty() && first_arg.iter().any(|p| glob_match(p)) {
175 return Verdict::Allowed(first_arg_level);
176 }
177 Verdict::Denied
178}
179
180fn dispatch_wrapper(
181 tokens: &[Token],
182 standalone: &[String],
183 valued: &[String],
184 positional_skip: usize,
185 separator: Option<&str>,
186 bare_ok: bool,
187) -> Verdict {
188 let mut i = 1;
189 while i < tokens.len() {
190 let t = &tokens[i];
191 if let Some(sep) = separator
192 && t == sep
193 {
194 i += 1;
195 break;
196 }
197 if !t.starts_with('-') {
198 break;
199 }
200 if valued.iter().any(|f| t == f.as_str()) {
201 i += 2;
202 continue;
203 }
204 if valued.iter().any(|f| t.as_str().starts_with(&format!("{f}="))) {
205 i += 1;
206 continue;
207 }
208 if standalone.iter().any(|f| t == f.as_str()) {
209 i += 1;
210 continue;
211 }
212 return Verdict::Denied;
213 }
214 for _ in 0..positional_skip {
215 if i >= tokens.len() {
216 return if bare_ok {
217 Verdict::Allowed(SafetyLevel::Inert)
218 } else {
219 Verdict::Denied
220 };
221 }
222 i += 1;
223 }
224 if i >= tokens.len() {
225 return if bare_ok {
226 Verdict::Allowed(SafetyLevel::Inert)
227 } else {
228 Verdict::Denied
229 };
230 }
231 let inner = shell_words::join(tokens[i..].iter().map(|t| t.as_str()));
232 crate::command_verdict(&inner)
233}
234
235fn dispatch_kind(tokens: &[Token], kind: &DispatchKind, handlers: &HandlerMap) -> Verdict {
236 match kind {
237 DispatchKind::Policy { policy, level } => {
238 if check_owned(tokens, policy) {
239 Verdict::Allowed(*level)
240 } else {
241 Verdict::Denied
242 }
243 }
244 DispatchKind::FirstArg { patterns, level } => {
245 dispatch_first_arg(tokens, patterns, *level)
246 }
247 DispatchKind::RequireAny { require_any, policy, level, accept_bare_help } => {
248 dispatch_require_any(tokens, require_any, policy, *level, *accept_bare_help)
249 }
250 DispatchKind::Branching {
251 subs, bare_flags, bare_ok, pre_standalone, pre_valued, first_arg, first_arg_level,
252 credential_first_arg,
253 } => {
254 dispatch_branching(
255 tokens, subs, bare_flags, *bare_ok, (pre_standalone, pre_valued),
256 first_arg, *first_arg_level, credential_first_arg,
257 )
258 }
259 DispatchKind::WriteFlagged { policy, base_level, write_flags } => {
260 if !check_owned(tokens, policy) {
261 return Verdict::Denied;
262 }
263 let has_write = tokens[1..].iter().any(|t| {
264 write_flags.iter().any(|f| t == f.as_str() || t.as_str().starts_with(&format!("{f}=")))
265 });
266 if has_write {
267 Verdict::Allowed(SafetyLevel::SafeWrite)
268 } else {
269 Verdict::Allowed(*base_level)
270 }
271 }
272 DispatchKind::DelegateAfterSeparator { separator } => {
273 let sep_pos = tokens[1..].iter().position(|t| t == separator.as_str());
274 let Some(pos) = sep_pos else {
275 return Verdict::Denied;
276 };
277 let inner_start = pos + 2;
278 if inner_start >= tokens.len() {
279 return Verdict::Denied;
280 }
281 let inner = shell_words::join(tokens[inner_start..].iter().map(|t| t.as_str()));
282 crate::command_verdict(&inner)
283 }
284 DispatchKind::DelegateSkip { skip } => {
285 if tokens.len() <= *skip {
286 return Verdict::Denied;
287 }
288 let inner = shell_words::join(tokens[*skip..].iter().map(|t| t.as_str()));
289 crate::command_verdict(&inner)
290 }
291 DispatchKind::Wrapper {
292 standalone, valued, positional_skip, separator, bare_ok,
293 } => {
294 dispatch_wrapper(tokens, standalone, valued, *positional_skip, separator.as_deref(), *bare_ok)
295 }
296 DispatchKind::VerbChain(spec) => dispatch_verb_chain(tokens, spec),
297 DispatchKind::Executor { policy, level, kind, redirect_flag, shape } => {
298 dispatch_executor(tokens, policy, *kind, *level, redirect_flag.as_deref(), *shape)
299 }
300 DispatchKind::Custom { handler_name, .. } => {
301 handlers
302 .get(handler_name.as_str())
303 .map(|f| f(tokens))
304 .unwrap_or(Verdict::Denied)
305 }
306 }
307}
308
309pub fn dispatch_spec(tokens: &[Token], spec: &CommandSpec) -> Verdict {
310 dispatch_kind(tokens, &spec.kind, &CMD_HANDLERS)
311}
312
313pub(super) fn dispatch_sub_kind(tokens: &[Token], kind: &DispatchKind) -> Verdict {
316 dispatch_kind(tokens, kind, &SUB_HANDLERS)
317}
318
319pub(super) fn check_handler_policy_owned(tokens: &[Token], policy: &OwnedPolicy) -> bool {
320 check_owned(tokens, policy)
321}
322
323pub(super) fn dispatch_matrix_action(
324 tokens: &[Token],
325 policy: &OwnedPolicy,
326 level: SafetyLevel,
327) -> Verdict {
328 if check_owned(tokens, policy) {
329 Verdict::Allowed(level)
330 } else {
331 Verdict::Denied
332 }
333}
334
335pub(super) fn dispatch_verb_chain(tokens: &[Token], spec: &VerbChainSpec) -> Verdict {
342 if tokens.len() == 1 {
343 return Verdict::Denied;
344 }
345 let has = |set: &[String], s: &str| set.iter().any(|f| f == s);
346 let mut i = 1;
347 while i < tokens.len() {
348 let s = tokens[i].as_str();
349 if s == "--" {
351 return verb_chain_tail(&tokens[i + 1..], spec);
352 }
353 if !tokens[i].starts_with('-') {
355 return verb_chain_tail(&tokens[i..], spec);
356 }
357 if has(&spec.main_standalone, s) {
358 i += 1;
359 } else if has(&spec.main_variadic, s) {
360 i += 1;
363 while i < tokens.len() && tokens[i].as_str() != "--" {
364 i += 1;
365 }
366 } else if has(&spec.main_valued, s) {
367 i += if crate::policy::consumes_next_value(tokens.get(i + 1)) { 2 } else { 1 };
368 } else if s.split_once('=').is_some_and(|(f, _)| has(&spec.main_valued, f)) {
369 i += 1;
370 } else {
371 return Verdict::Denied;
372 }
373 }
374 Verdict::Allowed(spec.level)
376}
377
378fn verb_chain_tail(region: &[Token], spec: &VerbChainSpec) -> Verdict {
380 let mut expect_verb = true;
381 for t in region {
382 if t.as_str() == spec.separator {
383 expect_verb = true;
384 continue;
385 }
386 if expect_verb {
387 if !spec.verbs.contains(t.as_str()) {
388 return Verdict::Denied;
389 }
390 expect_verb = false;
391 }
392 }
393 if expect_verb { Verdict::Denied } else { Verdict::Allowed(spec.level) }
395}
396
397pub(super) fn dispatch_executor(
408 tokens: &[Token],
409 policy: &OwnedPolicy,
410 kind: ExecutorKind,
411 level: SafetyLevel,
412 redirect_flag: Option<&str>,
413 shape: Option<crate::policy::PositionalShape>,
414) -> Verdict {
415 match kind {
416 ExecutorKind::File => match super::policy::first_positional(tokens, policy) {
417 Some(first) if shape.is_some_and(|s| !s.matches(first)) => Verdict::Denied,
420 Some(first) => crate::engine::resolve::execute_file_verdict(first),
421 None if check_owned(tokens, policy) => Verdict::Allowed(level),
422 None => Verdict::Denied,
423 },
424 ExecutorKind::Project => {
425 if !check_owned(tokens, policy) {
426 return Verdict::Denied;
427 }
428 match redirect_flag.and_then(|f| flag_value(tokens, f)) {
429 Some(redirected) => crate::engine::resolve::execute_file_verdict(redirected),
430 None => crate::engine::resolve::execute_project_verdict(),
431 }
432 }
433 }
434}
435
436fn flag_value<'a>(tokens: &'a [Token], flag: &str) -> Option<&'a str> {
440 let mut i = 1;
441 while i < tokens.len() {
442 let t = tokens[i].as_str();
443 if t == "--" {
444 return None;
445 }
446 if t == flag {
447 return tokens.get(i + 1).map(Token::as_str);
448 }
449 if let Some(rest) = t.strip_prefix(flag).and_then(|r| r.strip_prefix('=')) {
450 return Some(rest);
451 }
452 i += 1;
453 }
454 None
455}
456
457pub(super) fn dispatch_fallback(tokens: &[Token], spec: &FallbackSpec) -> Verdict {
460 if let Some(kind) = spec.executor {
461 return dispatch_executor(
462 tokens, &spec.policy, kind, spec.level,
463 spec.executor_redirect_flag.as_deref(), spec.positional_shape,
464 );
465 }
466 if let Some(shape) = spec.positional_shape
467 && let Some(first) = super::policy::first_positional(tokens, &spec.policy)
468 && !shape.matches(first)
469 {
470 return Verdict::Denied;
471 }
472 if !check_owned(tokens, &spec.policy) {
473 return Verdict::Denied;
474 }
475 Verdict::Allowed(spec.level)
476}