Skip to main content

stix_pattern/
parser.rs

1//! Iterative parser using an explicit stack: tokens -> AST. Nesting is capped at [`MAX_NESTING`].
2
3use crate::ast::{
4    Comparison, ComparisonExpression, ComparisonOperand, ComparisonOperator, Literal, ObjectPath,
5    ObservationExpression, PathStep, Pattern, Qualifier,
6};
7use crate::error::{ParseError, Result, Span};
8use crate::lexer::{Token, TokenKind};
9
10/// The deepest nesting [`parse`] accepts; deeper patterns are rejected with the
11/// error "pattern nests too deeply".
12///
13/// Nesting is counted along each path from the root of the pattern to a leaf
14/// as the number of parenthesized groups (at the observation level and inside
15/// `[...]` alike) plus the number of qualifiers (`WITHIN`, `REPEATS`,
16/// `START..STOP`), since each qualifier wraps its expression in another node.
17/// Chains such as `a OR b OR c` add no nesting however long they are: they
18/// parse into a single n-ary node.
19///
20/// The parser itself does not recurse per group. Lowering, the matcher, and
21/// the AST's derived `Clone`, `PartialEq`, `Debug`, `Serialize` and drop glue
22/// do recurse over nesting depth, so this bound is what keeps them within the
23/// stack.
24///
25/// # How the value was chosen
26///
27/// By measurement, in a debug build on a thread with a 256 KiB stack (well
28/// below wasm's 1 MiB default). The pipeline is parse, lower, validate,
29/// render, `serde_json::to_string` of the AST, matching, `without_spans` and
30/// drop. It was run on the worst shapes: observation-level groups with a
31/// `FOLLOWEDBY`, an `OR` and an `AND` node between each pair of groups;
32/// comparison-level groups; the two mixed; stacked qualifiers; and qualified
33/// groups. The deepest shape survived about 56 levels, limited by
34/// `serde_json` serialization of the AST. Every other stage and shape went
35/// deeper. 40 keeps roughly 40% headroom for other platforms and toolchains.
36/// `stix-matcher`'s `tests/deep_nesting.rs` reruns these shapes at this limit.
37///
38/// Real patterns nest a handful of levels at most.
39pub const MAX_NESTING: usize = 40;
40
41/// A parsed expression with its nesting height: the most groups and
42/// qualifiers on any path from it down to a leaf.
43type Parsed<T> = (T, usize);
44
45/// An expression type parsed by [`Parser::parse_infix`]: its binary
46/// operators, by precedence level (level 0 binds loosest), and its chain nodes.
47trait Infix: Sized {
48    /// Number of precedence levels.
49    const LEVELS: usize;
50    /// What `expect` reports when a group is not closed.
51    const CLOSE_GROUP: &'static str;
52    /// The precedence level of `token`, if it is one of this type's operators.
53    fn level_of(token: &TokenKind) -> Option<usize>;
54    /// The chain node at `level` with these operands (at least two).
55    fn chain(level: usize, operands: Vec<Self>) -> Self;
56    /// The operands of `self` if it is a chain node at `level`, else `self`.
57    fn into_operands(self, level: usize) -> std::result::Result<Vec<Self>, Self>;
58}
59
60impl Infix for ObservationExpression {
61    const LEVELS: usize = 3;
62    const CLOSE_GROUP: &'static str = "')' to close grouped observation";
63
64    fn level_of(token: &TokenKind) -> Option<usize> {
65        match token {
66            TokenKind::FollowedBy => Some(0),
67            TokenKind::Or => Some(1),
68            TokenKind::And => Some(2),
69            _ => None,
70        }
71    }
72
73    fn chain(level: usize, operands: Vec<Self>) -> Self {
74        match level {
75            0 => ObservationExpression::FollowedBy(operands),
76            1 => ObservationExpression::Or(operands),
77            _ => ObservationExpression::And(operands),
78        }
79    }
80
81    fn into_operands(self, level: usize) -> std::result::Result<Vec<Self>, Self> {
82        match (level, self) {
83            (0, ObservationExpression::FollowedBy(xs))
84            | (1, ObservationExpression::Or(xs))
85            | (2, ObservationExpression::And(xs)) => Ok(xs),
86            (_, e) => Err(e),
87        }
88    }
89}
90
91impl Infix for ComparisonExpression {
92    const LEVELS: usize = 2;
93    const CLOSE_GROUP: &'static str = "')' to close comparison group";
94
95    fn level_of(token: &TokenKind) -> Option<usize> {
96        match token {
97            TokenKind::Or => Some(0),
98            TokenKind::And => Some(1),
99            _ => None,
100        }
101    }
102
103    fn chain(level: usize, operands: Vec<Self>) -> Self {
104        match level {
105            0 => ComparisonExpression::Or(operands),
106            _ => ComparisonExpression::And(operands),
107        }
108    }
109
110    fn into_operands(self, level: usize) -> std::result::Result<Vec<Self>, Self> {
111        match (level, self) {
112            (0, ComparisonExpression::Or(xs)) | (1, ComparisonExpression::And(xs)) => Ok(xs),
113            (_, e) => Err(e),
114        }
115    }
116}
117
118/// The top level or one open group in [`Parser::parse_infix`].
119struct Frame<T> {
120    /// Per precedence level, the operands so far of the chain being built at
121    /// that level. A list is non-empty only while its operator is pending.
122    chains: Vec<Vec<T>>,
123    /// The greatest nesting height among the operands seen so far.
124    height: usize,
125}
126
127impl<T: Infix> Frame<T> {
128    fn new() -> Self {
129        Frame {
130            chains: (0..T::LEVELS).map(|_| Vec::new()).collect(),
131            height: 0,
132        }
133    }
134
135    /// Add `operand`, which an operator at `level` follows.
136    fn push(&mut self, level: usize, operand: T) {
137        let operand = self.close_above(level, operand);
138        let chain = &mut self.chains[level];
139        if chain.is_empty() {
140            // The chain's first operand: splice in a left-nested chain of
141            // the same operator, as in `(a OR b) OR c`.
142            match operand.into_operands(level) {
143                Ok(operands) => *chain = operands,
144                Err(operand) => chain.push(operand),
145            }
146        } else {
147            chain.push(operand);
148        }
149    }
150
151    /// The whole expression, of which `operand` is the last operand.
152    fn finish(&mut self, operand: T) -> T {
153        let operand = self.close_above(0, operand);
154        Self::close(0, std::mem::take(&mut self.chains[0]), operand)
155    }
156
157    /// Complete the chains at levels above `level`, of which `operand` is the
158    /// last operand, giving the operand they form at `level`.
159    fn close_above(&mut self, level: usize, mut operand: T) -> T {
160        for l in (level + 1..T::LEVELS).rev() {
161            operand = Self::close(l, std::mem::take(&mut self.chains[l]), operand);
162        }
163        operand
164    }
165
166    /// The chain at `level` of `operands` then `last`, or just `last` when no
167    /// operator at that level was seen.
168    fn close(level: usize, mut operands: Vec<T>, last: T) -> T {
169        if operands.is_empty() {
170            return last;
171        }
172        operands.push(last);
173        T::chain(level, operands)
174    }
175}
176
177pub(crate) struct Parser<'a> {
178    tokens: &'a [Token],
179    pos: usize,
180    /// Length of the source string, used to build EOF spans.
181    src_len: usize,
182    /// Number of parenthesized groups currently open around the cursor.
183    open_groups: usize,
184}
185
186impl<'a> Parser<'a> {
187    pub(crate) fn new(tokens: &'a [Token], src: &'a str) -> Self {
188        Parser {
189            tokens,
190            pos: 0,
191            src_len: src.len(),
192            open_groups: 0,
193        }
194    }
195
196    // --- cursor helpers ---
197
198    fn peek(&self) -> Option<&TokenKind> {
199        self.tokens.get(self.pos).map(|t| &t.kind)
200    }
201
202    fn current_span(&self) -> Span {
203        match self.tokens.get(self.pos) {
204            Some(t) => t.span,
205            None => Span::new(self.src_len, self.src_len),
206        }
207    }
208
209    fn advance(&mut self) -> Option<&Token> {
210        let tok = self.tokens.get(self.pos);
211        if tok.is_some() {
212            self.pos += 1;
213        }
214        tok
215    }
216
217    fn at_end(&self) -> bool {
218        self.pos >= self.tokens.len()
219    }
220
221    /// Consume the current token if its kind equals `want`.
222    fn eat(&mut self, want: &TokenKind) -> bool {
223        if self.peek() == Some(want) {
224            self.pos += 1;
225            true
226        } else {
227            false
228        }
229    }
230
231    fn expect(&mut self, want: &TokenKind, what: &str) -> Result<()> {
232        if self.eat(want) {
233            Ok(())
234        } else {
235            Err(ParseError::new(
236                format!("expected {what}"),
237                self.current_span(),
238            ))
239        }
240    }
241
242    fn error_here(&self, msg: impl Into<String>) -> ParseError {
243        ParseError::new(msg, self.current_span())
244    }
245
246    /// Span of the most recently consumed token, or a zero span at the start.
247    fn prev_span(&self) -> Span {
248        match self.pos.checked_sub(1).and_then(|i| self.tokens.get(i)) {
249            Some(t) => t.span,
250            None => Span::new(0, 0),
251        }
252    }
253
254    /// A span from `start` to the end of the most recently consumed token.
255    fn span_from(&self, start: usize) -> Span {
256        Span::new(start, self.prev_span().end)
257    }
258
259    // --- object path ---
260
261    pub(crate) fn parse_object_path(&mut self) -> Result<ObjectPath> {
262        let start = self.current_span().start;
263        // object-type
264        let object_type = match self.advance().map(|t| &t.kind) {
265            Some(TokenKind::Identifier(s)) => s.clone(),
266            _ => return Err(self.error_here("expected object type identifier")),
267        };
268        self.expect(&TokenKind::Colon, "':' after object type")?;
269
270        // first path component (identifier or quoted string)
271        let mut steps = Vec::new();
272        steps.push(PathStep::Key(self.parse_key_component()?));
273
274        // subsequent steps
275        loop {
276            match self.peek() {
277                Some(TokenKind::Dot) => {
278                    self.advance();
279                    steps.push(PathStep::Key(self.parse_key_component()?));
280                }
281                Some(TokenKind::LBracket) => {
282                    self.advance();
283                    let step = match self.peek() {
284                        Some(TokenKind::Star) => {
285                            self.advance();
286                            PathStep::AnyIndex
287                        }
288                        Some(TokenKind::Integer(n)) => {
289                            let n = *n;
290                            self.advance();
291                            if n < 0 {
292                                return Err(self.error_here("list index must be non-negative"));
293                            }
294                            PathStep::Index(n as u64)
295                        }
296                        _ => return Err(self.error_here("expected index or '*' in '[...]'")),
297                    };
298                    self.expect(&TokenKind::RBracket, "']' to close index")?;
299                    steps.push(step);
300                }
301                _ => break,
302            }
303        }
304        Ok(ObjectPath {
305            object_type,
306            steps,
307            span: self.span_from(start),
308        })
309    }
310
311    fn parse_key_component(&mut self) -> Result<String> {
312        match self.advance().map(|t| &t.kind) {
313            Some(TokenKind::Identifier(s)) => Ok(s.clone()),
314            Some(TokenKind::String(s)) => Ok(s.clone()),
315            _ => Err(self.error_here("expected property name")),
316        }
317    }
318
319    // --- infix expressions ---
320
321    /// Error unless one more level of nesting fits around an expression of
322    /// nesting `height`. Reported at the current token.
323    fn check_nesting(&self, height: usize) -> Result<()> {
324        if self.open_groups + height + 1 > MAX_NESTING {
325            Err(self.error_here("pattern nests too deeply"))
326        } else {
327            Ok(())
328        }
329    }
330
331    /// Parse an infix expression of `T` operands: `operand (op operand)*`
332    /// with the operators and precedences `T` defines, and parenthesized
333    /// groups. `leaf` parses an operand that is not a group; `postfix` applies
334    /// any postfix clauses (qualifiers) to each operand, group or leaf.
335    ///
336    /// This does not recurse per group: open groups are frames on an explicit
337    /// stack, so the parser's own stack use does not grow with nesting.
338    ///
339    /// Each chain becomes one n-ary node. If a chain's first operand is
340    /// already a node of the same operator (a group, as in `(a OR b) OR c`),
341    /// its operands are spliced in. Later operands are never flattened, so
342    /// `a OR (b OR c)` keeps its inner node.
343    fn parse_infix<T: Infix>(
344        &mut self,
345        mut leaf: impl FnMut(&mut Self) -> Result<Parsed<T>>,
346        mut postfix: impl FnMut(&mut Self, Parsed<T>) -> Result<Parsed<T>>,
347    ) -> Result<Parsed<T>> {
348        let mut frames: Vec<Frame<T>> = vec![Frame::new()];
349        loop {
350            // Expecting an operand: open any groups, then parse a leaf.
351            while self.peek() == Some(&TokenKind::LParen) {
352                self.check_nesting(0)?;
353                self.advance();
354                self.open_groups += 1;
355                frames.push(Frame::new());
356            }
357            let parsed = leaf(self)?;
358            let (mut operand, mut height) = postfix(self, parsed)?;
359            // After an operand: an operator continues the current group;
360            // anything else ends it.
361            loop {
362                let frame = frames
363                    .last_mut()
364                    .expect("the top-level frame is never popped");
365                frame.height = frame.height.max(height);
366                if let Some(level) = self.peek().and_then(T::level_of) {
367                    self.advance();
368                    frame.push(level, operand);
369                    break;
370                }
371                let expr = frame.finish(operand);
372                let inner_height = frame.height;
373                if frames.len() == 1 {
374                    return Ok((expr, inner_height));
375                }
376                self.expect(&TokenKind::RParen, T::CLOSE_GROUP)?;
377                frames.pop();
378                self.open_groups -= 1;
379                (operand, height) = postfix(self, (expr, inner_height + 1))?;
380            }
381        }
382    }
383
384    // --- comparison expressions ---
385
386    pub(crate) fn parse_comparison_expression(&mut self) -> Result<Parsed<ComparisonExpression>> {
387        self.parse_infix(
388            |p: &mut Self| Ok((p.parse_prop_test()?, 0)),
389            |_, parsed| Ok(parsed),
390        )
391    }
392
393    fn parse_prop_test(&mut self) -> Result<ComparisonExpression> {
394        let start = self.current_span().start;
395
396        // EXISTS objectPath
397        if self.eat(&TokenKind::Exists) {
398            let path = self.parse_object_path()?;
399            let test = ComparisonExpression::Test(Comparison {
400                path,
401                operator: ComparisonOperator::Exists,
402                negated: false,
403                // operand unused for EXISTS; use a benign placeholder.
404                value: ComparisonOperand::Literal(Literal::Boolean(true)),
405                span: self.span_from(start),
406            });
407            return Ok(test);
408        }
409
410        // objectPath NOT? operator operand
411        let path = self.parse_object_path()?;
412        let negated = self.eat(&TokenKind::Not);
413        let operator = self.parse_comparison_operator()?;
414        let value = if operator == ComparisonOperator::In {
415            self.parse_set_literal()?
416        } else {
417            ComparisonOperand::Literal(self.parse_literal()?)
418        };
419        let test = ComparisonExpression::Test(Comparison {
420            path,
421            operator,
422            negated,
423            value,
424            span: self.span_from(start),
425        });
426        Ok(test)
427    }
428
429    fn parse_comparison_operator(&mut self) -> Result<ComparisonOperator> {
430        let op = match self.peek() {
431            Some(TokenKind::Equal) => ComparisonOperator::Equal,
432            Some(TokenKind::NotEqual) => ComparisonOperator::NotEqual,
433            Some(TokenKind::GreaterThan) => ComparisonOperator::GreaterThan,
434            Some(TokenKind::GreaterThanOrEqual) => ComparisonOperator::GreaterThanOrEqual,
435            Some(TokenKind::LessThan) => ComparisonOperator::LessThan,
436            Some(TokenKind::LessThanOrEqual) => ComparisonOperator::LessThanOrEqual,
437            Some(TokenKind::In) => ComparisonOperator::In,
438            Some(TokenKind::Like) => ComparisonOperator::Like,
439            Some(TokenKind::Matches) => ComparisonOperator::Matches,
440            Some(TokenKind::IsSubset) => ComparisonOperator::IsSubset,
441            Some(TokenKind::IsSuperset) => ComparisonOperator::IsSuperset,
442            _ => return Err(self.error_here("expected a comparison operator")),
443        };
444        self.advance();
445        Ok(op)
446    }
447
448    fn parse_set_literal(&mut self) -> Result<ComparisonOperand> {
449        self.expect(&TokenKind::LParen, "'(' to open set literal")?;
450        let mut items = vec![self.parse_literal()?];
451        while self.eat(&TokenKind::Comma) {
452            items.push(self.parse_literal()?);
453        }
454        self.expect(&TokenKind::RParen, "')' to close set literal")?;
455        Ok(ComparisonOperand::Set(items))
456    }
457
458    fn parse_literal(&mut self) -> Result<Literal> {
459        let lit = match self.peek() {
460            Some(TokenKind::String(s)) => Literal::String(s.clone()),
461            Some(TokenKind::Integer(n)) => Literal::Integer(*n),
462            Some(TokenKind::Float(f)) => Literal::Float(*f),
463            Some(TokenKind::Boolean(b)) => Literal::Boolean(*b),
464            Some(TokenKind::Timestamp(s)) => Literal::Timestamp(s.clone()),
465            Some(TokenKind::Binary(s)) => Literal::Binary(s.clone()),
466            Some(TokenKind::Hex(s)) => Literal::Hex(s.clone()),
467            _ => return Err(self.error_here("expected a literal value")),
468        };
469        self.advance();
470        Ok(lit)
471    }
472
473    // --- observation expressions ---
474
475    pub(crate) fn parse_observation_expression(&mut self) -> Result<Parsed<ObservationExpression>> {
476        self.parse_infix(Self::parse_observation, Self::parse_qualifiers)
477    }
478
479    /// Apply any qualifiers that follow an operand. Each one wraps the
480    /// expression in another node, so each counts toward the nesting limit.
481    fn parse_qualifiers(
482        &mut self,
483        (mut expr, mut height): Parsed<ObservationExpression>,
484    ) -> Result<Parsed<ObservationExpression>> {
485        loop {
486            let q_start = self.current_span().start;
487            if matches!(
488                self.peek(),
489                Some(TokenKind::Within | TokenKind::Repeats | TokenKind::Start)
490            ) {
491                // Each qualifier wraps the expression in one more node.
492                self.check_nesting(height)?;
493            }
494            let qualifier = match self.peek() {
495                Some(TokenKind::Within) => {
496                    self.advance();
497                    let seconds = self.parse_number_as_f64()?;
498                    self.expect(&TokenKind::Seconds, "SECONDS after WITHIN value")?;
499                    Qualifier::Within { seconds }
500                }
501                Some(TokenKind::Repeats) => {
502                    self.advance();
503                    let count = self.parse_non_negative_int()?;
504                    self.expect(&TokenKind::Times, "TIMES after REPEATS value")?;
505                    Qualifier::Repeats { count }
506                }
507                Some(TokenKind::Start) => {
508                    self.advance();
509                    let start = self.parse_timestamp_string()?;
510                    self.expect(&TokenKind::Stop, "STOP after START timestamp")?;
511                    let stop = self.parse_timestamp_string()?;
512                    Qualifier::StartStop { start, stop }
513                }
514                _ => break,
515            };
516            expr = ObservationExpression::Qualified {
517                expression: Box::new(expr),
518                qualifier,
519                span: self.span_from(q_start),
520            };
521            height += 1;
522        }
523        Ok((expr, height))
524    }
525
526    /// Parse one `[ comparisonExpr ]` observation. (Groups are handled by
527    /// [`parse_infix`](Self::parse_infix), so only `[` can start one here.)
528    fn parse_observation(&mut self) -> Result<Parsed<ObservationExpression>> {
529        let start = self.current_span().start;
530        if !self.eat(&TokenKind::LBracket) {
531            return Err(self.error_here("expected '[' or '(' to start an observation"));
532        }
533        let (comp, height) = self.parse_comparison_expression()?;
534        self.expect(&TokenKind::RBracket, "']' to close observation")?;
535        let observation = ObservationExpression::Observation {
536            expression: Box::new(comp),
537            span: self.span_from(start),
538        };
539        Ok((observation, height))
540    }
541
542    fn parse_number_as_f64(&mut self) -> Result<f64> {
543        match self.peek() {
544            Some(TokenKind::Integer(n)) => {
545                let v = *n as f64;
546                self.advance();
547                Ok(v)
548            }
549            Some(TokenKind::Float(f)) => {
550                let v = *f;
551                self.advance();
552                Ok(v)
553            }
554            _ => Err(self.error_here("expected a numeric value")),
555        }
556    }
557
558    fn parse_non_negative_int(&mut self) -> Result<u64> {
559        match self.peek() {
560            Some(TokenKind::Integer(n)) if *n >= 0 => {
561                let v = *n as u64;
562                self.advance();
563                Ok(v)
564            }
565            _ => Err(self.error_here("expected a non-negative integer")),
566        }
567    }
568
569    fn parse_timestamp_string(&mut self) -> Result<String> {
570        match self.peek() {
571            Some(TokenKind::Timestamp(s)) => {
572                let v = s.clone();
573                self.advance();
574                Ok(v)
575            }
576            _ => Err(self.error_here("expected a t'...' timestamp literal")),
577        }
578    }
579}
580
581/// Parse a complete pattern string into an AST.
582pub fn parse(src: &str) -> Result<Pattern> {
583    let tokens = crate::lexer::tokenize(src)?;
584    let mut parser = Parser::new(&tokens, src);
585    let (expression, _height) = parser.parse_observation_expression()?;
586    if !parser.at_end() {
587        return Err(parser.error_here("unexpected trailing tokens after pattern"));
588    }
589    Ok(Pattern { expression })
590}
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595    use crate::ast::PathStep;
596
597    fn parse_path(src: &str) -> crate::ast::ObjectPath {
598        let toks = crate::lexer::tokenize(src).unwrap();
599        let mut p = Parser::new(&toks, src);
600        p.parse_object_path().unwrap()
601    }
602
603    #[test]
604    fn parses_simple_path() {
605        let path = parse_path("ipv4-addr:value");
606        assert_eq!(path.object_type, "ipv4-addr");
607        assert_eq!(path.steps, vec![PathStep::Key("value".to_string())]);
608    }
609
610    #[test]
611    fn parses_nested_keys() {
612        let path = parse_path("file:hashes.MD5");
613        assert_eq!(path.object_type, "file");
614        assert_eq!(
615            path.steps,
616            vec![
617                PathStep::Key("hashes".to_string()),
618                PathStep::Key("MD5".to_string())
619            ]
620        );
621    }
622
623    #[test]
624    fn parses_index_and_any_index() {
625        let path = parse_path("network-traffic:protocols[0]");
626        assert_eq!(
627            path.steps,
628            vec![PathStep::Key("protocols".to_string()), PathStep::Index(0)]
629        );
630
631        let path = parse_path("x:list[*]");
632        assert_eq!(
633            path.steps,
634            vec![PathStep::Key("list".to_string()), PathStep::AnyIndex]
635        );
636    }
637
638    #[test]
639    fn parses_quoted_key() {
640        let path = parse_path("file:hashes.'SHA-256'");
641        assert_eq!(
642            path.steps,
643            vec![
644                PathStep::Key("hashes".to_string()),
645                PathStep::Key("SHA-256".to_string())
646            ]
647        );
648    }
649
650    use crate::ast::{
651        Comparison, ComparisonExpression, ComparisonOperand, ComparisonOperator, Literal,
652    };
653
654    fn parse_comp(src: &str) -> ComparisonExpression {
655        let toks = crate::lexer::tokenize(src).unwrap();
656        let mut p = Parser::new(&toks, src);
657        p.parse_comparison_expression().unwrap().0
658    }
659
660    #[test]
661    fn parses_single_comparison() {
662        let c = parse_comp("ipv4-addr:value = '1.2.3.4'");
663        match c {
664            ComparisonExpression::Test(Comparison {
665                operator,
666                negated,
667                value,
668                ..
669            }) => {
670                assert_eq!(operator, ComparisonOperator::Equal);
671                assert!(!negated);
672                assert_eq!(
673                    value,
674                    ComparisonOperand::Literal(Literal::String("1.2.3.4".into()))
675                );
676            }
677            _ => panic!("expected a single test"),
678        }
679    }
680
681    #[test]
682    fn parses_not_operator() {
683        let c = parse_comp("file:size != 0");
684        match c {
685            ComparisonExpression::Test(Comparison {
686                operator, value, ..
687            }) => {
688                assert_eq!(operator, ComparisonOperator::NotEqual);
689                assert_eq!(value, ComparisonOperand::Literal(Literal::Integer(0)));
690            }
691            _ => panic!("expected test"),
692        }
693    }
694
695    #[test]
696    fn parses_not_keyword_prefix() {
697        // `objectPath NOT op value` sets negated = true
698        let c = parse_comp("file:name NOT = 'x'");
699        match c {
700            ComparisonExpression::Test(Comparison { negated, .. }) => assert!(negated),
701            _ => panic!("expected test"),
702        }
703    }
704
705    #[test]
706    fn parses_in_set() {
707        let c = parse_comp("ipv4-addr:value IN ('1.1.1.1', '8.8.8.8')");
708        match c {
709            ComparisonExpression::Test(Comparison {
710                operator, value, ..
711            }) => {
712                assert_eq!(operator, ComparisonOperator::In);
713                assert_eq!(
714                    value,
715                    ComparisonOperand::Set(vec![
716                        Literal::String("1.1.1.1".into()),
717                        Literal::String("8.8.8.8".into()),
718                    ])
719                );
720            }
721            _ => panic!("expected test"),
722        }
723    }
724
725    #[test]
726    fn parses_exists() {
727        let c = parse_comp("EXISTS file:name");
728        match c {
729            ComparisonExpression::Test(Comparison { operator, path, .. }) => {
730                assert_eq!(operator, ComparisonOperator::Exists);
731                assert_eq!(path.object_type, "file");
732            }
733            _ => panic!("expected test"),
734        }
735    }
736
737    #[test]
738    fn comparison_and_binds_tighter_than_or() {
739        // a OR b AND c  =>  a OR (b AND c)
740        let c = parse_comp("file:name = 'a' OR file:name = 'b' AND file:size = 1");
741        match c {
742            ComparisonExpression::Or(xs) => {
743                assert_eq!(xs.len(), 2);
744                assert!(matches!(xs[0], ComparisonExpression::Test(_)));
745                assert!(
746                    matches!(&xs[1], ComparisonExpression::And(ys) if ys.len() == 2),
747                    "right side of OR should be an AND"
748                );
749            }
750            _ => panic!("top should be OR"),
751        }
752    }
753
754    #[test]
755    fn parses_parenthesized_comparison() {
756        let c = parse_comp("(file:name = 'a' OR file:name = 'b') AND file:size = 1");
757        match c {
758            ComparisonExpression::And(xs) => {
759                assert_eq!(xs.len(), 2);
760                assert!(
761                    matches!(&xs[0], ComparisonExpression::Or(ys) if ys.len() == 2),
762                    "left of AND should be OR"
763                );
764                assert!(matches!(xs[1], ComparisonExpression::Test(_)));
765            }
766            _ => panic!("top should be AND"),
767        }
768    }
769
770    fn strip_comp(src: &str) -> ComparisonExpression {
771        // Normalize spans by wrapping the comparison in an observation.
772        let p = parse(&format!("[{src}]")).unwrap().without_spans();
773        match p.expression {
774            ObservationExpression::Observation { expression, .. } => *expression,
775            _ => unreachable!(),
776        }
777    }
778
779    #[test]
780    fn comparison_chain_is_one_node() {
781        let c = parse_comp("x:v = 1 OR x:v = 2 OR x:v = 3 OR x:v = 4");
782        assert!(matches!(&c, ComparisonExpression::Or(xs) if xs.len() == 4));
783        let c = parse_comp("x:v = 1 AND x:v = 2 AND x:v = 3");
784        assert!(matches!(&c, ComparisonExpression::And(xs) if xs.len() == 3));
785    }
786
787    #[test]
788    fn comparison_left_nested_group_is_flattened() {
789        let flat = strip_comp("x:v = 1 OR x:v = 2 OR x:v = 3");
790        assert_eq!(strip_comp("(x:v = 1 OR x:v = 2) OR x:v = 3"), flat);
791        assert_eq!(strip_comp("((x:v = 1 OR x:v = 2)) OR x:v = 3"), flat);
792        assert_eq!(
793            strip_comp("(x:v = 1 AND x:v = 2) AND x:v = 3"),
794            strip_comp("x:v = 1 AND x:v = 2 AND x:v = 3")
795        );
796    }
797
798    #[test]
799    fn comparison_right_nested_group_is_kept() {
800        let c = strip_comp("x:v = 1 OR (x:v = 2 OR x:v = 3)");
801        match c {
802            ComparisonExpression::Or(xs) => {
803                assert_eq!(xs.len(), 2);
804                assert!(matches!(&xs[1], ComparisonExpression::Or(ys) if ys.len() == 2));
805            }
806            _ => panic!("top should be OR"),
807        }
808    }
809
810    use crate::ast::{ObservationExpression, Qualifier};
811    use crate::parser::parse;
812
813    #[test]
814    fn parses_single_observation() {
815        let p = parse("[ipv4-addr:value = '1.2.3.4']").unwrap();
816        match p.expression {
817            ObservationExpression::Observation { .. } => {}
818            _ => panic!("expected single observation"),
819        }
820    }
821
822    #[test]
823    fn observation_and_binds_tighter_than_or() {
824        // [a] OR [b] AND [c] => [a] OR ([b] AND [c])
825        let p = parse("[file:name='a'] OR [file:name='b'] AND [file:size=1]").unwrap();
826        match p.expression {
827            ObservationExpression::Or(xs) => {
828                assert_eq!(xs.len(), 2);
829                assert!(matches!(xs[0], ObservationExpression::Observation { .. }));
830                assert!(
831                    matches!(&xs[1], ObservationExpression::And(ys) if ys.len() == 2),
832                    "right of OR should be AND"
833                );
834            }
835            _ => panic!("top should be OR"),
836        }
837    }
838
839    #[test]
840    fn followedby_is_loosest() {
841        // [a] FOLLOWEDBY [b] OR [c] => [a] FOLLOWEDBY ([b] OR [c])
842        let p = parse("[file:name='a'] FOLLOWEDBY [file:name='b'] OR [file:name='c']").unwrap();
843        match p.expression {
844            ObservationExpression::FollowedBy(xs) => {
845                assert_eq!(xs.len(), 2);
846                assert!(matches!(xs[0], ObservationExpression::Observation { .. }));
847                assert!(
848                    matches!(&xs[1], ObservationExpression::Or(ys) if ys.len() == 2),
849                    "right of FOLLOWEDBY should be OR"
850                );
851            }
852            _ => panic!("top should be FOLLOWEDBY"),
853        }
854    }
855
856    #[test]
857    fn parses_parenthesized_observation() {
858        let p = parse("([file:name='a'] OR [file:name='b']) FOLLOWEDBY [file:size=1]").unwrap();
859        match p.expression {
860            ObservationExpression::FollowedBy(xs) => {
861                assert_eq!(xs.len(), 2);
862                assert!(
863                    matches!(&xs[0], ObservationExpression::Or(ys) if ys.len() == 2),
864                    "left should be OR"
865                );
866                assert!(matches!(xs[1], ObservationExpression::Observation { .. }));
867            }
868            _ => panic!("top should be FOLLOWEDBY"),
869        }
870    }
871
872    fn stripped(src: &str) -> ObservationExpression {
873        parse(src).unwrap().without_spans().expression
874    }
875
876    #[test]
877    fn observation_chains_are_one_node() {
878        let p = stripped("[x:v=1] OR [x:v=2] OR [x:v=3]");
879        assert!(matches!(&p, ObservationExpression::Or(xs) if xs.len() == 3));
880        let p = stripped("[x:v=1] AND [x:v=2] AND [x:v=3] AND [x:v=4]");
881        assert!(matches!(&p, ObservationExpression::And(xs) if xs.len() == 4));
882        let p = stripped("[x:v=1] FOLLOWEDBY [x:v=2] FOLLOWEDBY [x:v=3]");
883        assert!(matches!(&p, ObservationExpression::FollowedBy(xs) if xs.len() == 3));
884    }
885
886    #[test]
887    fn observation_left_nested_group_is_flattened() {
888        for op in ["OR", "AND", "FOLLOWEDBY"] {
889            let flat = stripped(&format!("[x:v=1] {op} [x:v=2] {op} [x:v=3]"));
890            assert_eq!(
891                stripped(&format!("([x:v=1] {op} [x:v=2]) {op} [x:v=3]")),
892                flat,
893                "{op}"
894            );
895            assert_eq!(
896                stripped(&format!("(([x:v=1] {op} [x:v=2])) {op} [x:v=3]")),
897                flat,
898                "{op}"
899            );
900        }
901    }
902
903    #[test]
904    fn observation_right_nested_group_is_kept() {
905        let p = stripped("[x:v=1] FOLLOWEDBY ([x:v=2] FOLLOWEDBY [x:v=3])");
906        match p {
907            ObservationExpression::FollowedBy(xs) => {
908                assert_eq!(xs.len(), 2);
909                assert!(matches!(&xs[1], ObservationExpression::FollowedBy(ys) if ys.len() == 2));
910            }
911            _ => panic!("top should be FOLLOWEDBY"),
912        }
913    }
914
915    #[test]
916    fn qualified_group_is_not_flattened() {
917        let p = stripped("([x:v=1] OR [x:v=2]) WITHIN 5 SECONDS OR [x:v=3]");
918        match p {
919            ObservationExpression::Or(xs) => {
920                assert_eq!(xs.len(), 2);
921                assert!(matches!(xs[0], ObservationExpression::Qualified { .. }));
922            }
923            _ => panic!("top should be OR"),
924        }
925    }
926
927    /// Run `f` on a thread with a small (256 KiB) stack, as a stand-in for
928    /// constrained hosts such as wasm.
929    fn on_small_stack<F: FnOnce() + Send + 'static>(f: F) {
930        std::thread::Builder::new()
931            .stack_size(256 * 1024)
932            .spawn(f)
933            .unwrap()
934            .join()
935            .unwrap();
936    }
937
938    const LEAF: &str = "[x:v = 1]";
939
940    fn obs_parens(n: usize) -> String {
941        format!("{}{LEAF}{}", "(".repeat(n), ")".repeat(n))
942    }
943
944    fn cmp_parens(n: usize) -> String {
945        format!("[{}x:v = 1{}]", "(".repeat(n), ")".repeat(n))
946    }
947
948    fn qualifiers(n: usize) -> String {
949        format!("{LEAF}{}", " REPEATS 2 TIMES".repeat(n))
950    }
951
952    fn assert_too_deep(src: &str, at: usize, token: &str) {
953        let err = parse(src).unwrap_err();
954        assert_eq!(err.message, "pattern nests too deeply");
955        assert_eq!(err.span.start, at, "error span: {:?}", err.span);
956        assert_eq!(&src[err.span.start..err.span.end], token);
957    }
958
959    #[test]
960    fn nesting_at_the_limit_parses() {
961        parse(&obs_parens(MAX_NESTING)).unwrap();
962        parse(&cmp_parens(MAX_NESTING)).unwrap();
963        parse(&qualifiers(MAX_NESTING)).unwrap();
964        // Observation-level and comparison-level groups share one budget.
965        let h = MAX_NESTING / 2;
966        let mixed = format!(
967            "{}[{}x:v = 1{}]{}",
968            "(".repeat(h),
969            "(".repeat(MAX_NESTING - h),
970            ")".repeat(MAX_NESTING - h),
971            ")".repeat(h)
972        );
973        parse(&mixed).unwrap();
974        let mixed_over = format!("({mixed})");
975        assert_eq!(
976            parse(&mixed_over).unwrap_err().message,
977            "pattern nests too deeply"
978        );
979    }
980
981    #[test]
982    fn nesting_past_the_limit_is_an_error_at_the_crossing_token() {
983        // The (MAX_NESTING + 1)-th '(' is where the limit is crossed.
984        assert_too_deep(&obs_parens(MAX_NESTING + 1), MAX_NESTING, "(");
985        assert_too_deep(&cmp_parens(MAX_NESTING + 1), MAX_NESTING + 1, "(");
986        let src = qualifiers(MAX_NESTING + 1);
987        let last = src.rfind("REPEATS").unwrap();
988        assert_too_deep(&src, last, "REPEATS");
989    }
990
991    #[test]
992    fn qualifiers_count_together_with_enclosing_groups() {
993        // A group plus its qualifier is two levels.
994        let level = |inner: &str| format!("({inner}) WITHIN 1 SECONDS");
995        let mut ok = LEAF.to_string();
996        for _ in 0..MAX_NESTING / 2 {
997            ok = level(&ok);
998        }
999        parse(&ok).unwrap();
1000        let err = parse(&level(&ok)).unwrap_err();
1001        assert_eq!(err.message, "pattern nests too deeply");
1002        // Qualifiers stacked on successive groups cannot sneak past the limit,
1003        // even though only a few are ever stacked on one expression.
1004        let mut src = LEAF.to_string();
1005        for _ in 0..MAX_NESTING {
1006            src = format!("({src}) WITHIN 1 SECONDS WITHIN 1 SECONDS");
1007        }
1008        assert_eq!(parse(&src).unwrap_err().message, "pattern nests too deeply");
1009    }
1010
1011    #[test]
1012    fn huge_nesting_is_an_error_not_a_crash() {
1013        on_small_stack(|| {
1014            for n in [10_000, 100_000] {
1015                for src in [obs_parens(n), cmp_parens(n), qualifiers(n)] {
1016                    let err = parse(&src).unwrap_err();
1017                    assert_eq!(err.message, "pattern nests too deeply");
1018                }
1019            }
1020        });
1021    }
1022
1023    #[test]
1024    fn parses_within_qualifier() {
1025        let p = parse("[file:name='a'] REPEATS 2 TIMES WITHIN 60 SECONDS").unwrap();
1026        // Outermost qualifier is the last one parsed (WITHIN), wrapping REPEATS.
1027        match p.expression {
1028            ObservationExpression::Qualified {
1029                qualifier: Qualifier::Within { seconds },
1030                expression,
1031                ..
1032            } => {
1033                assert_eq!(seconds, 60.0);
1034                match *expression {
1035                    ObservationExpression::Qualified {
1036                        qualifier: Qualifier::Repeats { count },
1037                        ..
1038                    } => {
1039                        assert_eq!(count, 2);
1040                    }
1041                    _ => panic!("inner should be REPEATS"),
1042                }
1043            }
1044            _ => panic!("outer should be WITHIN"),
1045        }
1046    }
1047
1048    #[test]
1049    fn parses_start_stop_qualifier() {
1050        let p = parse("[file:name='a'] START t'2020-01-01T00:00:00Z' STOP t'2020-01-02T00:00:00Z'")
1051            .unwrap();
1052        match p.expression {
1053            ObservationExpression::Qualified {
1054                qualifier: Qualifier::StartStop { start, stop },
1055                ..
1056            } => {
1057                assert_eq!(start, "2020-01-01T00:00:00Z");
1058                assert_eq!(stop, "2020-01-02T00:00:00Z");
1059            }
1060            _ => panic!("expected START..STOP"),
1061        }
1062    }
1063
1064    #[test]
1065    fn trailing_tokens_error() {
1066        assert!(parse("[file:name='a'] [file:name='b']").is_err());
1067    }
1068
1069    #[test]
1070    fn records_spans_on_comparison_and_path() {
1071        let src = "[file:size > 1024]";
1072        let p = parse(src).unwrap();
1073        let ObservationExpression::Observation { expression, span } = &p.expression else {
1074            panic!("expected an observation");
1075        };
1076        // The observation span covers the whole `[...]` including brackets.
1077        assert_eq!(&src[span.start..span.end], "[file:size > 1024]");
1078        let ComparisonExpression::Test(c) = expression.as_ref() else {
1079            panic!("expected a test");
1080        };
1081        // The comparison span covers the property test, without the brackets.
1082        assert_eq!(&src[c.span.start..c.span.end], "file:size > 1024");
1083        // The path span covers only the object path.
1084        assert_eq!(&src[c.path.span.start..c.path.span.end], "file:size");
1085    }
1086
1087    #[test]
1088    fn records_span_on_qualifier_clause() {
1089        let src = "[file:name='a'] WITHIN 60 SECONDS";
1090        let p = parse(src).unwrap();
1091        let ObservationExpression::Qualified { span, .. } = &p.expression else {
1092            panic!("expected a qualified expression");
1093        };
1094        assert_eq!(&src[span.start..span.end], "WITHIN 60 SECONDS");
1095    }
1096
1097    #[test]
1098    fn without_spans_zeroes_every_span() {
1099        let a = parse("[file:size > 1024] WITHIN 60 SECONDS").unwrap();
1100        let b = parse("   [file:size > 1024]   WITHIN 60 SECONDS   ").unwrap();
1101        assert_ne!(a, b, "differing offsets should make the raw ASTs unequal");
1102        assert_eq!(
1103            a.without_spans(),
1104            b.without_spans(),
1105            "structurally identical patterns should compare equal without spans"
1106        );
1107    }
1108}