safe_chains/registry/
dispatch.rs1use crate::parse::Token;
2use crate::verdict::{SafetyLevel, Verdict};
3
4use super::policy::check_owned;
5use super::types::*;
6use super::{CMD_HANDLERS, SUB_HANDLERS};
7
8fn has_flag_owned(tokens: &[Token], short: Option<&str>, long: &str) -> bool {
9 tokens[1..].iter().any(|t| {
10 t == long
11 || short.is_some_and(|s| t == s)
12 || t.as_str().starts_with(&format!("{long}="))
13 })
14}
15
16fn dispatch_first_arg(tokens: &[Token], patterns: &[String], level: SafetyLevel) -> Verdict {
17 if tokens.len() == 2 && (tokens[1] == "--help" || tokens[1] == "-h") {
18 return Verdict::Allowed(SafetyLevel::Inert);
19 }
20 let Some(arg) = tokens.get(1) else {
21 return Verdict::Denied;
22 };
23 let arg_str = arg.as_str();
24 let matches = patterns.iter().any(|p| {
25 if let Some(prefix) = p.strip_suffix('*') {
26 arg_str.starts_with(prefix)
27 } else {
28 arg_str == p
29 }
30 });
31 if matches { Verdict::Allowed(level) } else { Verdict::Denied }
32}
33
34fn dispatch_require_any(
35 tokens: &[Token],
36 require_any: &[String],
37 policy: &OwnedPolicy,
38 level: SafetyLevel,
39) -> Verdict {
40 if tokens.len() == 2 && (tokens[1] == "--help" || tokens[1] == "-h") {
41 return Verdict::Allowed(SafetyLevel::Inert);
42 }
43 let has_required = tokens[1..].iter().any(|t| {
44 require_any.iter().any(|r| {
45 t == r.as_str() || t.as_str().starts_with(&format!("{r}="))
46 })
47 });
48 if has_required && check_owned(tokens, policy) {
49 Verdict::Allowed(level)
50 } else {
51 Verdict::Denied
52 }
53}
54
55fn dispatch_sub(tokens: &[Token], sub: &SubSpec) -> Verdict {
56 match &sub.kind {
57 SubKind::Policy { policy, level } => {
58 if check_owned(tokens, policy) {
59 Verdict::Allowed(*level)
60 } else {
61 Verdict::Denied
62 }
63 }
64 SubKind::Guarded {
65 guard_long,
66 guard_short,
67 policy,
68 level,
69 } => {
70 if has_flag_owned(tokens, guard_short.as_deref(), guard_long)
71 && check_owned(tokens, policy)
72 {
73 Verdict::Allowed(*level)
74 } else {
75 Verdict::Denied
76 }
77 }
78 SubKind::Nested { subs, allow_bare } => {
79 if tokens.len() < 2 {
80 if *allow_bare {
81 return Verdict::Allowed(SafetyLevel::Inert);
82 }
83 return Verdict::Denied;
84 }
85 let arg = tokens[1].as_str();
86 if *allow_bare && (arg == "--help" || arg == "-h") {
87 return Verdict::Allowed(SafetyLevel::Inert);
88 }
89 subs.iter()
90 .find(|s| s.name == arg)
91 .map(|s| dispatch_sub(&tokens[1..], s))
92 .unwrap_or(Verdict::Denied)
93 }
94 SubKind::AllowAll { level } => Verdict::Allowed(*level),
95 SubKind::WriteFlagged {
96 policy,
97 base_level,
98 write_flags,
99 } => {
100 if !check_owned(tokens, policy) {
101 return Verdict::Denied;
102 }
103 let has_write = tokens[1..].iter().any(|t| {
104 write_flags.iter().any(|f| t == f.as_str() || t.as_str().starts_with(&format!("{f}=")))
105 });
106 if has_write {
107 Verdict::Allowed(SafetyLevel::SafeWrite)
108 } else {
109 Verdict::Allowed(*base_level)
110 }
111 }
112 SubKind::FirstArgFilter { patterns, level } => {
113 dispatch_first_arg(tokens, patterns, *level)
114 }
115 SubKind::RequireAny {
116 require_any,
117 policy,
118 level,
119 } => dispatch_require_any(tokens, require_any, policy, *level),
120 SubKind::DelegateAfterSeparator { separator } => {
121 let sep_pos = tokens[1..].iter().position(|t| t == separator.as_str());
122 let Some(pos) = sep_pos else {
123 return Verdict::Denied;
124 };
125 let inner_start = pos + 2;
126 if inner_start >= tokens.len() {
127 return Verdict::Denied;
128 }
129 let inner = shell_words::join(tokens[inner_start..].iter().map(|t| t.as_str()));
130 crate::command_verdict(&inner)
131 }
132 SubKind::DelegateSkip { skip, .. } => {
133 if tokens.len() <= *skip {
134 return Verdict::Denied;
135 }
136 let inner = shell_words::join(tokens[*skip..].iter().map(|t| t.as_str()));
137 crate::command_verdict(&inner)
138 }
139 SubKind::Custom { handler_name } => {
140 SUB_HANDLERS
141 .get(handler_name.as_str())
142 .map(|f| f(tokens))
143 .unwrap_or(Verdict::Denied)
144 }
145 }
146}
147
148pub fn dispatch_spec(tokens: &[Token], spec: &CommandSpec) -> Verdict {
149 match &spec.kind {
150 CommandKind::Flat { policy, level } => {
151 if check_owned(tokens, policy) {
152 Verdict::Allowed(*level)
153 } else {
154 Verdict::Denied
155 }
156 }
157 CommandKind::FlatFirstArg { patterns, level } => {
158 dispatch_first_arg(tokens, patterns, *level)
159 }
160 CommandKind::FlatRequireAny {
161 require_any,
162 policy,
163 level,
164 } => dispatch_require_any(tokens, require_any, policy, *level),
165 CommandKind::Structured { bare_flags, subs } => {
166 if tokens.len() < 2 {
167 return Verdict::Denied;
168 }
169 let arg = tokens[1].as_str();
170 if tokens.len() == 2 && bare_flags.iter().any(|f| f == arg) {
171 return Verdict::Allowed(SafetyLevel::Inert);
172 }
173 subs.iter()
174 .find(|s| s.name == arg)
175 .map(|s| dispatch_sub(&tokens[1..], s))
176 .unwrap_or(Verdict::Denied)
177 }
178 CommandKind::Wrapper {
179 standalone,
180 valued,
181 positional_skip,
182 separator,
183 bare_ok,
184 } => {
185 let mut i = 1;
186 while i < tokens.len() {
187 let t = &tokens[i];
188 if let Some(sep) = separator
189 && t == sep.as_str()
190 {
191 i += 1;
192 break;
193 }
194 if !t.starts_with('-') {
195 break;
196 }
197 if valued.iter().any(|f| t == f.as_str()) {
198 i += 2;
199 continue;
200 }
201 if standalone.iter().any(|f| t == f.as_str()) {
202 i += 1;
203 continue;
204 }
205 i += 1;
206 }
207 for _ in 0..*positional_skip {
208 if i >= tokens.len() {
209 return if *bare_ok {
210 Verdict::Allowed(SafetyLevel::Inert)
211 } else {
212 Verdict::Denied
213 };
214 }
215 i += 1;
216 }
217 if i >= tokens.len() {
218 return if *bare_ok {
219 Verdict::Allowed(SafetyLevel::Inert)
220 } else {
221 Verdict::Denied
222 };
223 }
224 let inner = shell_words::join(tokens[i..].iter().map(|t| t.as_str()));
225 crate::command_verdict(&inner)
226 }
227 CommandKind::Custom { handler_name } => {
228 CMD_HANDLERS
229 .get(handler_name.as_str())
230 .map(|f| f(tokens))
231 .unwrap_or(Verdict::Denied)
232 }
233 }
234}