返回 DeepSeek-TUI-2026
parser.rs
根目录 / crates / tui / src / execpolicy / parser.rs
1 use multimap::MultiMap;
2 use shlex;
3 use starlark::any::ProvidesStaticType;
4 use starlark::environment::GlobalsBuilder;
5 use starlark::environment::Module;
6 use starlark::eval::Evaluator;
7 use starlark::starlark_module;
8 use starlark::syntax::AstModule;
9 use starlark::syntax::Dialect;
10 use starlark::values::Value;
11 use starlark::values::list::ListRef;
12 use starlark::values::list::UnpackList;
13 use starlark::values::none::NoneType;
14 use std::cell::RefCell;
15 use std::cell::RefMut;
16 use std::sync::Arc;
17
18 use super::decision::Decision;
19 use super::error::Error;
20 use super::error::Result;
21 use super::rule::PatternToken;
22 use super::rule::PrefixPattern;
23 use super::rule::PrefixRule;
24 use super::rule::RuleRef;
25 use super::rule::validate_match_examples;
26 use super::rule::validate_not_match_examples;
27
28 pub struct PolicyParser {
29 builder: RefCell<PolicyBuilder>,
30 }
31
32 impl Default for PolicyParser {
33 fn default() -> Self {
34 Self::new()
35 }
36 }
37
38 impl PolicyParser {
39 pub fn new() -> Self {
40 Self {
41 builder: RefCell::new(PolicyBuilder::new()),
42 }
43 }
44
45 /// Parses a policy, tagging parser errors with `policy_identifier` so failures include the
46 /// identifier alongside line numbers.
47 pub fn parse(&mut self, policy_identifier: &str, policy_file_contents: &str) -> Result<()> {
48 let mut dialect = Dialect::Extended.clone();
49 dialect.enable_f_strings = true;
50 let ast = AstModule::parse(
51 policy_identifier,
52 policy_file_contents.to_string(),
53 &dialect,
54 )
55 .map_err(Error::Starlark)?;
56 let globals = GlobalsBuilder::standard().with(policy_builtins).build();
57 let module = Module::new();
58 {
59 let mut eval = Evaluator::new(&module);
60 eval.extra = Some(&self.builder);
61 eval.eval_module(ast, &globals).map_err(Error::Starlark)?;
62 }
63 Ok(())
64 }
65
66 pub fn build(self) -> super::policy::Policy {
67 self.builder.into_inner().build()
68 }
69 }
70
71 #[derive(Debug, ProvidesStaticType)]
72 struct PolicyBuilder {
73 rules_by_program: MultiMap<String, RuleRef>,
74 }
75
76 impl PolicyBuilder {
77 fn new() -> Self {
78 Self {
79 rules_by_program: MultiMap::new(),
80 }
81 }
82
83 fn add_rule(&mut self, rule: RuleRef) {
84 self.rules_by_program
85 .insert(rule.program().to_string(), rule);
86 }
87
88 fn build(self) -> super::policy::Policy {
89 super::policy::Policy::new(self.rules_by_program)
90 }
91 }
92
93 fn parse_pattern<'v>(pattern: UnpackList<Value<'v>>) -> Result<Vec<PatternToken>> {
94 let tokens: Vec<PatternToken> = pattern
95 .items
96 .into_iter()
97 .map(parse_pattern_token)
98 .collect::<Result<_>>()?;
99 if tokens.is_empty() {
100 Err(Error::InvalidPattern("pattern cannot be empty".to_string()))
101 } else {
102 Ok(tokens)
103 }
104 }
105
106 fn parse_pattern_token<'v>(value: Value<'v>) -> Result<PatternToken> {
107 if let Some(s) = value.unpack_str() {
108 Ok(PatternToken::Single(s.to_string()))
109 } else if let Some(list) = ListRef::from_value(value) {
110 let tokens: Vec<String> = list
111 .content()
112 .iter()
113 .map(|value| {
114 value
115 .unpack_str()
116 .ok_or_else(|| {
117 Error::InvalidPattern(format!(
118 "pattern alternative must be a string (got {})",
119 value.get_type()
120 ))
121 })
122 .map(str::to_string)
123 })
124 .collect::<Result<_>>()?;
125
126 match tokens.as_slice() {
127 [] => Err(Error::InvalidPattern(
128 "pattern alternatives cannot be empty".to_string(),
129 )),
130 [single] => Ok(PatternToken::Single(single.clone())),
131 _ => Ok(PatternToken::Alts(tokens)),
132 }
133 } else {
134 Err(Error::InvalidPattern(format!(
135 "pattern element must be a string or list of strings (got {})",
136 value.get_type()
137 )))
138 }
139 }
140
141 fn parse_examples<'v>(examples: UnpackList<Value<'v>>) -> Result<Vec<Vec<String>>> {
142 examples.items.into_iter().map(parse_example).collect()
143 }
144
145 fn parse_example<'v>(value: Value<'v>) -> Result<Vec<String>> {
146 if let Some(raw) = value.unpack_str() {
147 parse_string_example(raw)
148 } else if let Some(list) = ListRef::from_value(value) {
149 parse_list_example(list)
150 } else {
151 Err(Error::InvalidExample(format!(
152 "example must be a string or list of strings (got {})",
153 value.get_type()
154 )))
155 }
156 }
157
158 fn parse_string_example(raw: &str) -> Result<Vec<String>> {
159 let tokens = shlex::split(raw).ok_or_else(|| {
160 Error::InvalidExample("example string has invalid shell syntax".to_string())
161 })?;
162
163 if tokens.is_empty() {
164 Err(Error::InvalidExample(
165 "example cannot be an empty string".to_string(),
166 ))
167 } else {
168 Ok(tokens)
169 }
170 }
171
172 fn parse_list_example(list: &ListRef) -> Result<Vec<String>> {
173 let tokens: Vec<String> = list
174 .content()
175 .iter()
176 .map(|value| {
177 value
178 .unpack_str()
179 .ok_or_else(|| {
180 Error::InvalidExample(format!(
181 "example tokens must be strings (got {})",
182 value.get_type()
183 ))
184 })
185 .map(str::to_string)
186 })
187 .collect::<Result<_>>()?;
188
189 if tokens.is_empty() {
190 Err(Error::InvalidExample(
191 "example cannot be an empty list".to_string(),
192 ))
193 } else {
194 Ok(tokens)
195 }
196 }
197
198 fn policy_builder<'v, 'a>(eval: &Evaluator<'v, 'a, '_>) -> RefMut<'a, PolicyBuilder> {
199 #[expect(clippy::expect_used)]
200 eval.extra
201 .as_ref()
202 .expect("policy_builder requires Evaluator.extra to be populated")
203 .downcast_ref::<RefCell<PolicyBuilder>>()
204 .expect("Evaluator.extra must contain a PolicyBuilder")
205 .borrow_mut()
206 }
207
208 #[starlark_module]
209 fn policy_builtins(builder: &mut GlobalsBuilder) {
210 fn prefix_rule<'v>(
211 pattern: UnpackList<Value<'v>>,
212 decision: Option<&'v str>,
213 r#match: Option<UnpackList<Value<'v>>>,
214 not_match: Option<UnpackList<Value<'v>>>,
215 justification: Option<&'v str>,
216 eval: &mut Evaluator<'v, '_, '_>,
217 ) -> anyhow::Result<NoneType> {
218 let decision = match decision {
219 Some(raw) => Decision::parse(raw)?,
220 None => Decision::Allow,
221 };
222
223 let justification = match justification {
224 Some(raw) if raw.trim().is_empty() => {
225 return Err(Error::InvalidRule("justification cannot be empty".to_string()).into());
226 }
227 Some(raw) => Some(raw.to_string()),
228 None => None,
229 };
230
231 let pattern_tokens = parse_pattern(pattern)?;
232
233 let matches: Vec<Vec<String>> =
234 r#match.map(parse_examples).transpose()?.unwrap_or_default();
235 let not_matches: Vec<Vec<String>> = not_match
236 .map(parse_examples)
237 .transpose()?
238 .unwrap_or_default();
239
240 let mut builder = policy_builder(eval);
241
242 let (first_token, remaining_tokens) = pattern_tokens
243 .split_first()
244 .ok_or_else(|| Error::InvalidPattern("pattern cannot be empty".to_string()))?;
245
246 let rest: Arc<[PatternToken]> = remaining_tokens.to_vec().into();
247
248 let rules: Vec<RuleRef> = first_token
249 .alternatives()
250 .iter()
251 .map(|head| {
252 Arc::new(PrefixRule {
253 pattern: PrefixPattern {
254 first: Arc::from(head.as_str()),
255 rest: rest.clone(),
256 },
257 decision,
258 justification: justification.clone(),
259 }) as RuleRef
260 })
261 .collect();
262
263 validate_not_match_examples(&rules, &not_matches)?;
264 validate_match_examples(&rules, &matches)?;
265
266 rules.into_iter().for_each(|rule| builder.add_rule(rule));
267 Ok(NoneType)
268 }
269 }
270
270 lines RUST