Skip to main content

safe_chains/registry/
dispatch.rs

1use 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}