#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirectiveLine {
pub line_no: usize,
pub tokens: Vec<String>,
}
pub fn lex(input: &str) -> Vec<DirectiveLine> {
let mut out = Vec::new();
let mut logical = String::new();
let mut logical_start = 0usize;
for (idx, raw_line) in input.lines().enumerate() {
let line_no = idx + 1;
if logical.is_empty() {
logical_start = line_no;
}
let trimmed_end = raw_line.trim_end();
if let Some(prefix) = trimmed_end.strip_suffix('\\') {
logical.push_str(prefix);
continue;
}
logical.push_str(raw_line);
if let Some(dl) = tokenize_logical(&logical, logical_start) {
out.push(dl);
}
logical.clear();
}
if !logical.is_empty() {
if let Some(dl) = tokenize_logical(&logical, logical_start) {
out.push(dl);
}
}
out
}
fn tokenize_logical(line: &str, line_no: usize) -> Option<DirectiveLine> {
let bytes = line.as_bytes();
let n = bytes.len();
let mut i = 0usize;
while i < n && (bytes[i] == b' ' || bytes[i] == b'\t') {
i += 1;
}
if i >= n || bytes[i] == b'#' {
return None;
}
let mut tokens: Vec<String> = Vec::new();
while i < n {
while i < n && (bytes[i] == b' ' || bytes[i] == b'\t') {
i += 1;
}
if i >= n {
break;
}
let mut tok = String::new();
if bytes[i] == b'"' {
i += 1;
while i < n {
let c = bytes[i];
if c == b'\\' && i + 1 < n {
let next = bytes[i + 1];
match next {
b'"' => {
tok.push('"');
i += 2;
}
b'\\' => {
tok.push('\\');
i += 2;
}
_ => {
tok.push('\\');
i += 1;
}
}
} else if c == b'"' {
i += 1; break;
} else {
push_byte(&mut tok, line, &mut i);
}
}
} else {
while i < n && bytes[i] != b' ' && bytes[i] != b'\t' {
push_byte(&mut tok, line, &mut i);
}
}
tokens.push(tok);
}
if tokens.is_empty() {
None
} else {
Some(DirectiveLine { line_no, tokens })
}
}
fn push_byte(tok: &mut String, line: &str, i: &mut usize) {
let rest = &line[*i..];
if let Some(ch) = rest.chars().next() {
tok.push(ch);
*i += ch.len_utf8();
} else {
*i += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn toks(input: &str) -> Vec<Vec<String>> {
lex(input).into_iter().map(|d| d.tokens).collect()
}
#[test]
fn simple_secrule_three_tokens() {
let got = toks(r#"SecRule ARGS "@rx union\s+select" "id:1,phase:2,block""#);
assert_eq!(
got,
vec![vec![
"SecRule".to_string(),
"ARGS".to_string(),
r"@rx union\s+select".to_string(),
"id:1,phase:2,block".to_string(),
]]
);
}
#[test]
fn regex_backslashes_preserved_singly() {
let got = toks(r#"SecRule ARGS "@rx \bunion\b\s\d+\." "id:2""#);
assert_eq!(got[0][2], r"@rx \bunion\b\s\d+\.");
}
#[test]
fn escaped_quote_inside_string() {
let got = toks(r#"SecRule ARGS "@rx say \"hi\"" "id:3""#);
assert_eq!(got[0][2], r#"@rx say "hi""#);
}
#[test]
fn double_backslash_collapses() {
let got = toks(r#"SecRule ARGS "@rx a\\b" "id:4""#);
assert_eq!(got[0][2], r"@rx a\b");
}
#[test]
fn line_continuation_joins() {
let src = "SecRule ARGS \"@rx foo\" \\\n \"id:5,phase:2,\\\n block\"";
let got = toks(src);
assert_eq!(got.len(), 1);
assert_eq!(got[0][0], "SecRule");
assert_eq!(got[0][1], "ARGS");
assert_eq!(got[0][2], "@rx foo");
assert_eq!(got[0][3], "id:5,phase:2, block");
}
#[test]
fn full_line_comment_dropped() {
let src = "# this is a comment\nSecRule ARGS \"@rx x\" \"id:6\"\n # indented comment";
let got = toks(src);
assert_eq!(got.len(), 1);
assert_eq!(got[0][3], "id:6");
}
#[test]
fn hash_inside_regex_is_not_a_comment() {
let got = toks(r##"SecRule ARGS "@rx a#b" "id:7""##);
assert_eq!(got[0][2], "@rx a#b");
}
#[test]
fn blank_lines_ignored() {
let src = "\n\n \nSecRule ARGS \"@rx x\" \"id:8\"\n\n";
assert_eq!(lex(src).len(), 1);
}
#[test]
fn line_numbers_tracked() {
let src = "# c\n\nSecRule ARGS \"@rx x\" \"id:9\"";
let dl = &lex(src)[0];
assert_eq!(dl.line_no, 3);
}
#[test]
fn pipe_separated_variables_stay_one_token() {
let got = toks(r#"SecRule ARGS|REQUEST_HEADERS:User-Agent "@rx x" "id:10""#);
assert_eq!(got[0][1], "ARGS|REQUEST_HEADERS:User-Agent");
}
#[test]
fn secaction_and_unquoted_operator() {
let got = toks(r#"SecAction "id:900,phase:1,pass,t:none""#);
assert_eq!(got[0][0], "SecAction");
assert_eq!(got[0][1], "id:900,phase:1,pass,t:none");
}
}