Skip to main content

mailrs_sieve_core/
parse.rs

1//! RFC 5228 §3-4 recursive-descent parser.
2
3use crate::ast::{Argument, Command, Test};
4use crate::lex::{Token, TokenizeError, tokenize};
5
6/// Parse failure modes.
7#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
8pub enum ParseError {
9    /// Tokenizer rejected the source before any parsing started.
10    #[error("tokenize: {0}")]
11    Tokenize(#[from] TokenizeError),
12    /// Reached end-of-input while expecting more tokens.
13    #[error("unexpected end of input")]
14    UnexpectedEof,
15    /// A specific token kind was required but a different one (or
16    /// none) was found at this position.
17    #[error("expected {expected} at token {at}, got {got:?}")]
18    Expected {
19        /// What the parser was looking for, as a human-readable label.
20        expected: String,
21        /// 0-based token index in the lexed stream.
22        at: usize,
23        /// What was actually found, or `None` for end-of-input.
24        got: Option<Token>,
25    },
26    /// A test was expected (after `if`, inside `allof(…)`, etc.)
27    /// but none could be parsed.
28    #[error("expected test expression at token {at}")]
29    ExpectedTest {
30        /// 0-based token index.
31        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    /// Argument *or* test — used at the top level inside a command's
116    /// argument list. `if`, `elsif`, `not`, `allof`, `anyof` always
117    /// start a test; otherwise the parser checks whether the next
118    /// identifier is a known test name.
119    fn parse_argument_or_test(&mut self, parent: &str) -> Result<Argument, ParseError> {
120        // For control-flow commands the next token MUST be a test.
121        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                // identifier inside an arg slot must be a test
184                // (this handles e.g. `not header :is …`)
185                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        // `allof(t1, t2)` / `anyof(t1, t2)` / `not test`
208        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        // Regular test: tags + args until we hit a delimiter that
252        // unambiguously ends a test (`)`, `,`, `;`, `{`)
253        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, // start of next command — test is done
275                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
294/// Tokenize + parse a full Sieve script.
295pub 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        // first arg must be the parsed Test
351        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}