1use 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
10pub const MAX_NESTING: usize = 40;
40
41type Parsed<T> = (T, usize);
44
45trait Infix: Sized {
48 const LEVELS: usize;
50 const CLOSE_GROUP: &'static str;
52 fn level_of(token: &TokenKind) -> Option<usize>;
54 fn chain(level: usize, operands: Vec<Self>) -> Self;
56 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
118struct Frame<T> {
120 chains: Vec<Vec<T>>,
123 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 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 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 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 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 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 src_len: usize,
182 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 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 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 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 fn span_from(&self, start: usize) -> Span {
256 Span::new(start, self.prev_span().end)
257 }
258
259 pub(crate) fn parse_object_path(&mut self) -> Result<ObjectPath> {
262 let start = self.current_span().start;
263 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 let mut steps = Vec::new();
272 steps.push(PathStep::Key(self.parse_key_component()?));
273
274 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 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 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 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 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 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 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 value: ComparisonOperand::Literal(Literal::Boolean(true)),
405 span: self.span_from(start),
406 });
407 return Ok(test);
408 }
409
410 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 pub(crate) fn parse_observation_expression(&mut self) -> Result<Parsed<ObservationExpression>> {
476 self.parse_infix(Self::parse_observation, Self::parse_qualifiers)
477 }
478
479 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 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 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
581pub 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(&src[c.span.start..c.span.end], "file:size > 1024");
1083 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}