use secir::{Error, Result};
use super::ConditionExpr;
pub(crate) struct ConditionParser {
pub(crate) tokens: Vec<String>,
pub(crate) index: usize,
pub(crate) depth: usize,
}
const MAX_CONDITION_DEPTH: usize = 64;
pub(crate) fn tokenize_condition(input: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut current = String::new();
let flush = |current: &mut String, tokens: &mut Vec<String>| {
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
tokens.push(trimmed);
}
current.clear();
};
let chars: Vec<char> = input.chars().collect();
let mut i = 0;
while i < chars.len() {
let ch = chars[i];
match ch {
'(' | ')' | '|' | ',' => {
flush(&mut current, &mut tokens);
tokens.push(ch.to_string());
}
'>' | '<' | '=' => {
flush(&mut current, &mut tokens);
if i + 1 < chars.len() && chars[i + 1] == '=' {
tokens.push(format!("{}{}", ch, chars[i + 1]));
i += 1;
} else {
tokens.push(ch.to_string());
}
}
c if c.is_whitespace() => {
flush(&mut current, &mut tokens);
}
_ => current.push(ch),
}
i += 1;
}
flush(&mut current, &mut tokens);
tokens
}
impl ConditionParser {
pub(crate) fn parse_expr(&mut self) -> Result<ConditionExpr> {
if self.depth >= MAX_CONDITION_DEPTH {
return Err(Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: format!(
"condition nesting exceeds limit of {MAX_CONDITION_DEPTH}. \
Fix: simplify the condition expression."
),
});
}
self.depth += 1;
let result = self.parse_expr_inner();
self.depth -= 1;
result
}
fn parse_expr_inner(&mut self) -> Result<ConditionExpr> {
let mut expr = self.parse_and()?;
while self.peek_keyword("|") {
self.index += 1;
if self.peek_keyword("near") {
self.index += 1;
let right = self.parse_and()?;
expr = ConditionExpr::Near(Box::new(expr), Box::new(right));
} else if self.peek_keyword("count") {
return Err(Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: "Aggregation functions like count() are not supported yet. Fix: remove aggregation from the rule.".to_string(),
});
} else {
break;
}
}
Ok(expr)
}
fn parse_and(&mut self) -> Result<ConditionExpr> {
let mut expr = self.parse_or()?;
while self.peek_keyword("and") {
self.index += 1;
let right = self.parse_or()?;
expr = ConditionExpr::And(Box::new(expr), Box::new(right));
}
Ok(expr)
}
fn parse_or(&mut self) -> Result<ConditionExpr> {
let mut expr = self.parse_not()?;
while self.peek_keyword("or") {
self.index += 1;
let right = self.parse_not()?;
expr = ConditionExpr::Or(Box::new(expr), Box::new(right));
}
Ok(expr)
}
fn parse_not(&mut self) -> Result<ConditionExpr> {
if self.peek_keyword("not") {
self.index += 1;
let inner = self.parse_primary()?;
return Ok(ConditionExpr::Not(Box::new(inner)));
}
self.parse_primary()
}
fn parse_primary(&mut self) -> Result<ConditionExpr> {
if let Some(of_expr) = self.parse_of_expression()? {
return Ok(of_expr);
}
let token = self
.tokens
.get(self.index)
.ok_or_else(|| Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: "unexpected end of condition".to_string(),
})?;
if token == "(" {
self.index += 1;
let expr = self.parse_expr()?;
let closing = self
.tokens
.get(self.index)
.ok_or_else(|| Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: "missing ')' in condition".to_string(),
})?;
if closing != ")" {
return Err(Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: "missing ')' in condition".to_string(),
});
}
self.index += 1;
return Ok(expr);
}
if matches!(
token.to_ascii_lowercase().as_str(),
"and" | "or" | "not" | ")"
) {
return Err(Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: format!("unexpected token '{}'", token),
});
}
self.index += 1;
Ok(ConditionExpr::Selection(token.clone()))
}
fn parse_of_expression(&mut self) -> Result<Option<ConditionExpr>> {
if self.index >= self.tokens.len() {
return Ok(None);
}
let (count_token, offset) = if self.peek_keyword("at")
&& self
.tokens
.get(self.index + 1)
.is_some_and(|t| t.eq_ignore_ascii_case("least"))
{
let token = self.tokens.get(self.index + 2).map(|s| s.to_ascii_lowercase());
(token, 3)
} else {
(Some(self.tokens[self.index].to_ascii_lowercase()), 1)
};
let count = match count_token.as_deref() {
Some("all") => None,
Some("any") => Some(1),
Some(s) => {
if let Ok(value) = s.parse::<usize>() {
Some(value)
} else {
return Ok(None);
}
}
None => return Ok(None),
};
if !self
.tokens
.get(self.index + offset)
.is_some_and(|token| token.eq_ignore_ascii_case("of"))
{
return Ok(None);
}
let pattern_start = self.index + offset + 1;
let (pattern, consumed) = if self
.tokens
.get(pattern_start)
.is_some_and(|t| t == "(")
{
let mut parts = Vec::new();
let mut j = pattern_start + 1;
while j < self.tokens.len() && self.tokens[j] != ")" {
if self.tokens[j] != "," {
parts.push(self.tokens[j].as_str());
}
j += 1;
}
if j >= self.tokens.len() {
return Err(Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: "missing ')' in 'of' expression pattern".to_string(),
});
}
if parts.is_empty() {
return Err(Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: "empty pattern inside 'of' expression".to_string(),
});
}
(parts.join(","), j - pattern_start + 1)
} else if let Some(p) = self.tokens.get(pattern_start) {
(p.clone(), 1)
} else {
return Err(Error::TemplateValidation {
id: "sigma-condition".to_string(),
reason: "unexpected end of condition in 'of' expression".to_string(),
});
};
self.index = pattern_start + consumed;
Ok(Some(ConditionExpr::Of { count, pattern }))
}
fn peek_keyword(&self, keyword: &str) -> bool {
self.tokens
.get(self.index)
.map(|token| token.eq_ignore_ascii_case(keyword))
.unwrap_or(false)
}
}