1use crate::ast::{Argument, Command, Test};
4use crate::lex::{Token, TokenizeError, tokenize};
5
6#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
8pub enum ParseError {
9 #[error("tokenize: {0}")]
11 Tokenize(#[from] TokenizeError),
12 #[error("unexpected end of input")]
14 UnexpectedEof,
15 #[error("expected {expected} at token {at}, got {got:?}")]
18 Expected {
19 expected: String,
21 at: usize,
23 got: Option<Token>,
25 },
26 #[error("expected test expression at token {at}")]
29 ExpectedTest {
30 at: usize,
32 },
33}
34
35struct Parser {
36 tokens: Vec<Token>,
37 pos: usize,
38}
39
40impl Parser {
41 fn new(tokens: Vec<Token>) -> Self {
42 Self { tokens, pos: 0 }
43 }
44
45 fn peek(&self) -> Option<&Token> {
46 self.tokens.get(self.pos)
47 }
48
49 fn bump(&mut self) -> Option<Token> {
50 let t = self.tokens.get(self.pos).cloned();
51 if t.is_some() {
52 self.pos += 1;
53 }
54 t
55 }
56
57 fn expect(&mut self, want: &Token) -> Result<(), ParseError> {
58 match self.peek() {
59 Some(t) if t == want => {
60 self.pos += 1;
61 Ok(())
62 }
63 other => Err(ParseError::Expected {
64 expected: format!("{want}"),
65 at: self.pos,
66 got: other.cloned(),
67 }),
68 }
69 }
70
71 fn parse_commands(&mut self) -> Result<Vec<Command>, ParseError> {
72 let mut out = Vec::new();
73 while let Some(t) = self.peek() {
74 if matches!(t, Token::RBrace) {
75 break;
76 }
77 out.push(self.parse_command()?);
78 }
79 Ok(out)
80 }
81
82 fn parse_command(&mut self) -> Result<Command, ParseError> {
83 let name = match self.bump() {
84 Some(Token::Identifier(n)) => n,
85 other => {
86 return Err(ParseError::Expected {
87 expected: "command identifier".into(),
88 at: self.pos.saturating_sub(1),
89 got: other,
90 });
91 }
92 };
93
94 let mut args = Vec::new();
95 while let Some(t) = self.peek() {
96 match t {
97 Token::Semicolon | Token::LBrace => break,
98 _ => args.push(self.parse_argument_or_test(&name)?),
99 }
100 }
101
102 let block = if matches!(self.peek(), Some(Token::LBrace)) {
103 self.bump();
104 let inner = self.parse_commands()?;
105 self.expect(&Token::RBrace)?;
106 inner
107 } else {
108 self.expect(&Token::Semicolon)?;
109 Vec::new()
110 };
111
112 Ok(Command { name, args, block })
113 }
114
115 fn parse_argument_or_test(&mut self, parent: &str) -> Result<Argument, ParseError> {
120 let starts_test = matches!(
122 parent,
123 "if" | "elsif" | "while"
124 );
125 if starts_test {
126 return Ok(Argument::Test(self.parse_test()?));
127 }
128 self.parse_argument()
129 }
130
131 fn parse_argument(&mut self) -> Result<Argument, ParseError> {
132 match self.peek().cloned() {
133 Some(Token::Tag(t)) => {
134 self.bump();
135 Ok(Argument::Tag(t))
136 }
137 Some(Token::Number(n)) => {
138 self.bump();
139 Ok(Argument::Number(n))
140 }
141 Some(Token::String(s)) => {
142 self.bump();
143 Ok(Argument::String(s))
144 }
145 Some(Token::LBracket) => {
146 self.bump();
147 let mut items = Vec::new();
148 loop {
149 match self.peek().cloned() {
150 Some(Token::String(s)) => {
151 self.bump();
152 items.push(s);
153 }
154 other => {
155 return Err(ParseError::Expected {
156 expected: "string inside list".into(),
157 at: self.pos,
158 got: other,
159 });
160 }
161 }
162 match self.peek() {
163 Some(Token::Comma) => {
164 self.bump();
165 continue;
166 }
167 Some(Token::RBracket) => {
168 self.bump();
169 break;
170 }
171 other => {
172 return Err(ParseError::Expected {
173 expected: ", or ]".into(),
174 at: self.pos,
175 got: other.cloned(),
176 });
177 }
178 }
179 }
180 Ok(Argument::StringList(items))
181 }
182 Some(Token::Identifier(_)) => {
183 Ok(Argument::Test(self.parse_test()?))
186 }
187 other => Err(ParseError::Expected {
188 expected: "argument".into(),
189 at: self.pos,
190 got: other,
191 }),
192 }
193 }
194
195 fn parse_test(&mut self) -> Result<Test, ParseError> {
196 let name = match self.bump() {
197 Some(Token::Identifier(n)) => n,
198 other => {
199 return Err(ParseError::Expected {
200 expected: "test identifier".into(),
201 at: self.pos.saturating_sub(1),
202 got: other,
203 });
204 }
205 };
206
207 let mut children = Vec::new();
209 match name.as_str() {
210 "not" => {
211 children.push(self.parse_test()?);
212 return Ok(Test {
213 name,
214 tags: Vec::new(),
215 args: Vec::new(),
216 children,
217 });
218 }
219 "allof" | "anyof" => {
220 self.expect(&Token::LParen)?;
221 loop {
222 children.push(self.parse_test()?);
223 match self.peek() {
224 Some(Token::Comma) => {
225 self.bump();
226 continue;
227 }
228 Some(Token::RParen) => {
229 self.bump();
230 break;
231 }
232 other => {
233 return Err(ParseError::Expected {
234 expected: ", or )".into(),
235 at: self.pos,
236 got: other.cloned(),
237 });
238 }
239 }
240 }
241 return Ok(Test {
242 name,
243 tags: Vec::new(),
244 args: Vec::new(),
245 children,
246 });
247 }
248 _ => {}
249 }
250
251 let mut tags = Vec::new();
254 let mut args = Vec::new();
255 while let Some(t) = self.peek().cloned() {
256 match t {
257 Token::Tag(s) => {
258 self.bump();
259 tags.push(s);
260 }
261 Token::Number(n) => {
262 self.bump();
263 args.push(Argument::Number(n));
264 }
265 Token::String(s) => {
266 self.bump();
267 args.push(Argument::String(s));
268 }
269 Token::LBracket => {
270 let list = self.parse_argument()?;
271 args.push(list);
272 }
273 Token::RParen | Token::Comma | Token::Semicolon | Token::LBrace => break,
274 Token::Identifier(_) => break, other => {
276 return Err(ParseError::Expected {
277 expected: "test arg or delimiter".into(),
278 at: self.pos,
279 got: Some(other),
280 });
281 }
282 }
283 }
284
285 Ok(Test {
286 name,
287 tags,
288 args,
289 children,
290 })
291 }
292}
293
294pub fn parse_script(src: &str) -> Result<Vec<Command>, ParseError> {
296 let tokens = tokenize(src)?;
297 let mut p = Parser::new(tokens);
298 p.parse_commands()
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use crate::ast::Argument;
305
306 #[test]
307 fn empty_script() {
308 let cmds = parse_script("").unwrap();
309 assert!(cmds.is_empty());
310 }
311
312 #[test]
313 fn just_keep() {
314 let cmds = parse_script("keep;").unwrap();
315 assert_eq!(cmds.len(), 1);
316 assert_eq!(cmds[0].name, "keep");
317 assert!(cmds[0].block.is_empty());
318 assert!(cmds[0].args.is_empty());
319 }
320
321 #[test]
322 fn require_string_list() {
323 let cmds = parse_script(r#"require ["fileinto", "envelope"];"#).unwrap();
324 assert_eq!(cmds.len(), 1);
325 assert_eq!(cmds[0].name, "require");
326 assert_eq!(
327 cmds[0].args,
328 vec![Argument::StringList(vec![
329 "fileinto".into(),
330 "envelope".into()
331 ])]
332 );
333 }
334
335 #[test]
336 fn fileinto_with_string_arg() {
337 let cmds = parse_script(r#"fileinto "Junk";"#).unwrap();
338 assert_eq!(cmds[0].name, "fileinto");
339 assert_eq!(cmds[0].args, vec![Argument::String("Junk".into())]);
340 }
341
342 #[test]
343 fn if_header_is_then_block() {
344 let src = r#"if header :is "Subject" "spam" { discard; }"#;
345 let cmds = parse_script(src).unwrap();
346 assert_eq!(cmds.len(), 1);
347 assert_eq!(cmds[0].name, "if");
348 assert_eq!(cmds[0].block.len(), 1);
349 assert_eq!(cmds[0].block[0].name, "discard");
350 match &cmds[0].args[0] {
352 Argument::Test(t) => {
353 assert_eq!(t.name, "header");
354 assert_eq!(t.tags, vec!["is".to_string()]);
355 assert_eq!(t.args.len(), 2);
356 }
357 other => panic!("expected Test, got {other:?}"),
358 }
359 }
360
361 #[test]
362 fn if_else_chain() {
363 let src = r#"
364 if header :is "Subject" "spam" { discard; }
365 elsif header :contains "Subject" "ad" { fileinto "Ads"; }
366 else { keep; }
367 "#;
368 let cmds = parse_script(src).unwrap();
369 assert_eq!(cmds.len(), 3);
370 assert_eq!(cmds[0].name, "if");
371 assert_eq!(cmds[1].name, "elsif");
372 assert_eq!(cmds[2].name, "else");
373 }
374
375 #[test]
376 fn allof_anyof() {
377 let src = r#"if allof(header :is "X" "1", header :is "Y" "2") { keep; }"#;
378 let cmds = parse_script(src).unwrap();
379 match &cmds[0].args[0] {
380 Argument::Test(t) => {
381 assert_eq!(t.name, "allof");
382 assert_eq!(t.children.len(), 2);
383 }
384 other => panic!("expected Test, got {other:?}"),
385 }
386 }
387
388 #[test]
389 fn not_wrap() {
390 let src = r#"if not header :is "Subject" "spam" { keep; }"#;
391 let cmds = parse_script(src).unwrap();
392 match &cmds[0].args[0] {
393 Argument::Test(t) => {
394 assert_eq!(t.name, "not");
395 assert_eq!(t.children.len(), 1);
396 assert_eq!(t.children[0].name, "header");
397 }
398 other => panic!("expected Test, got {other:?}"),
399 }
400 }
401
402 #[test]
403 fn size_test() {
404 let src = "if size :over 1M { discard; }";
405 let cmds = parse_script(src).unwrap();
406 match &cmds[0].args[0] {
407 Argument::Test(t) => {
408 assert_eq!(t.name, "size");
409 assert_eq!(t.tags, vec!["over".to_string()]);
410 assert_eq!(t.args, vec![Argument::Number(1024 * 1024)]);
411 }
412 other => panic!("expected Test, got {other:?}"),
413 }
414 }
415
416 #[test]
417 fn redirect() {
418 let cmds = parse_script(r#"redirect "alice@example.com";"#).unwrap();
419 assert_eq!(cmds[0].name, "redirect");
420 assert_eq!(
421 cmds[0].args,
422 vec![Argument::String("alice@example.com".into())]
423 );
424 }
425}