use fancy_regex::Regex;
use std::collections::HashSet;
use crate::error::TemplateError;
use crate::types::{ValueOption, ValueOptions};
#[derive(Debug, Clone)]
pub struct ValueDef {
pub name: String,
pub pattern: String,
pub options: ValueOptions,
pub(crate) template_pattern: String,
pub(crate) compiled_regex: Option<Regex>,
}
impl ValueDef {
pub const MAX_NAME_LEN: usize = 48;
pub fn parse(line: &str, line_num: usize) -> Result<Self, TemplateError> {
let trimmed = line.trim();
if !trimmed.starts_with("Value ") {
return Err(TemplateError::InvalidValue {
line: line_num,
message: "line must start with 'Value '".into(),
});
}
let rest = &trimmed[6..];
let regex_start = rest.find('(').ok_or_else(|| TemplateError::InvalidValue {
line: line_num,
message: "regex pattern must be wrapped in parentheses".into(),
})?;
let before_regex = rest[..regex_start].trim();
let pattern = rest[regex_start..].trim();
let mut parts = before_regex.split_whitespace();
let first = parts.next();
let second = parts.next();
let third = parts.next();
let (options, name) = match (first, second, third) {
(None, _, _) => {
return Err(TemplateError::InvalidValue {
line: line_num,
message: "missing value name".into(),
});
}
(Some(name), None, _) => (HashSet::new(), name.to_string()),
(Some(opts), Some(name), None) => {
if opts.contains(',') || ValueOption::parse(opts).is_some() {
let options = Self::parse_options(opts, line_num)?;
(options, name.to_string())
} else {
return Err(TemplateError::InvalidValue {
line: line_num,
message: format!(
"invalid format - expected 'Value [Options] Name (regex)', got unknown token '{}'",
opts
),
});
}
}
(Some(_), Some(_), Some(_)) => {
return Err(TemplateError::InvalidValue {
line: line_num,
message: "too many tokens before regex pattern".into(),
});
}
};
if name.len() > Self::MAX_NAME_LEN {
return Err(TemplateError::InvalidValue {
line: line_num,
message: format!(
"name '{}' exceeds maximum length of {}",
name,
Self::MAX_NAME_LEN
),
});
}
if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err(TemplateError::InvalidValue {
line: line_num,
message: format!("name '{}' contains invalid characters", name),
});
}
if !pattern.starts_with('(') || !pattern.ends_with(')') {
return Err(TemplateError::InvalidValue {
line: line_num,
message: "regex must be wrapped in parentheses".into(),
});
}
if pattern.ends_with("\\)") {
return Err(TemplateError::InvalidValue {
line: line_num,
message: "regex cannot end with escaped parenthesis".into(),
});
}
let pattern = normalize_pattern(pattern);
Regex::new(&pattern).map_err(|e| TemplateError::InvalidRegex {
pattern: pattern.to_string(),
message: e.to_string(),
})?;
let inner_pattern = &pattern[1..pattern.len() - 1];
let template_pattern = format!("(?P<{}>{})", name, inner_pattern);
let compiled_regex = if options.contains(&ValueOption::List) {
let re = Regex::new(&pattern).ok();
re.filter(|r| r.captures_len() > 1)
} else {
None
};
Ok(Self {
name,
pattern,
options,
template_pattern,
compiled_regex,
})
}
fn parse_options(opts_str: &str, _line_num: usize) -> Result<ValueOptions, TemplateError> {
let mut options = HashSet::new();
for opt_name in opts_str.split(',') {
let opt_name = opt_name.trim();
let opt = ValueOption::parse(opt_name)
.ok_or_else(|| TemplateError::UnknownOption(opt_name.into()))?;
if !options.insert(opt) {
return Err(TemplateError::DuplicateOption(opt_name.into()));
}
}
Ok(options)
}
pub fn has_option(&self, opt: ValueOption) -> bool {
self.options.contains(&opt)
}
}
pub(crate) fn normalize_pattern(pattern: &str) -> String {
let mut result = String::with_capacity(pattern.len());
let chars: Vec<char> = pattern.chars().collect();
let len = chars.len();
let mut i = 0;
let mut group_stack: Vec<bool> = Vec::new();
while i < len {
if chars[i] == '\\' && i + 1 < len {
if chars[i + 1] == '<' || chars[i + 1] == '>' {
result.push(chars[i + 1]);
i += 2;
continue;
}
result.push(chars[i]);
result.push(chars[i + 1]);
i += 2;
continue;
}
if chars[i] == '[' {
result.push(chars[i]);
i += 1;
if i < len && chars[i] == '^' {
result.push(chars[i]);
i += 1;
}
if i < len && chars[i] == ']' {
result.push(chars[i]);
i += 1;
}
while i < len && chars[i] != ']' {
if chars[i] == '\\' && i + 1 < len {
result.push(chars[i]);
result.push(chars[i + 1]);
i += 2;
} else {
result.push(chars[i]);
i += 1;
}
}
if i < len {
result.push(chars[i]); i += 1;
}
continue;
}
if chars[i] == '(' {
let is_lookaround = if i + 2 < len && chars[i + 1] == '?' {
chars[i + 2] == '=' || chars[i + 2] == '!'
|| (i + 3 < len
&& chars[i + 2] == '<'
&& (chars[i + 3] == '=' || chars[i + 3] == '!'))
} else {
false
};
group_stack.push(is_lookaround);
result.push(chars[i]);
i += 1;
continue;
}
if chars[i] == ')' {
let is_lookaround = group_stack.pop().unwrap_or(false);
result.push(chars[i]);
i += 1;
if is_lookaround && i < len {
i = skip_quantifier(&chars, i);
}
continue;
}
result.push(chars[i]);
i += 1;
}
result
}
fn skip_quantifier(chars: &[char], mut i: usize) -> usize {
let len = chars.len();
if i >= len {
return i;
}
match chars[i] {
'+' | '*' | '?' => {
i += 1;
if i < len && chars[i] == '?' {
i += 1;
}
}
'{' => {
let start = i;
i += 1;
if i >= len || !chars[i].is_ascii_digit() {
return start; }
while i < len && chars[i].is_ascii_digit() {
i += 1;
}
if i < len && chars[i] == ',' {
i += 1;
while i < len && chars[i].is_ascii_digit() {
i += 1;
}
}
if i < len && chars[i] == '}' {
i += 1;
if i < len && chars[i] == '?' {
i += 1;
}
} else {
return start; }
}
_ => {} }
i
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_value() {
let v = ValueDef::parse("Value Interface (\\S+)", 1).unwrap();
assert_eq!(v.name, "Interface");
assert_eq!(v.pattern, "(\\S+)");
assert!(v.options.is_empty());
assert_eq!(v.template_pattern, "(?P<Interface>\\S+)");
}
#[test]
fn test_parse_value_with_options() {
let v = ValueDef::parse("Value Required,Filldown Hostname (\\S+)", 1).unwrap();
assert_eq!(v.name, "Hostname");
assert!(v.has_option(ValueOption::Required));
assert!(v.has_option(ValueOption::Filldown));
assert!(!v.has_option(ValueOption::List));
}
#[test]
fn test_parse_value_with_spaces_in_regex() {
let v = ValueDef::parse("Value Status (up|down|administratively down)", 1).unwrap();
assert_eq!(v.name, "Status");
assert_eq!(v.pattern, "(up|down|administratively down)");
}
#[test]
fn test_invalid_regex() {
let result = ValueDef::parse("Value Bad ([invalid)", 1);
assert!(matches!(result, Err(TemplateError::InvalidRegex { .. })));
}
#[test]
fn test_missing_parens() {
let result = ValueDef::parse("Value Name \\S+", 1);
assert!(matches!(result, Err(TemplateError::InvalidValue { .. })));
}
#[test]
fn test_normalize_angle_brackets() {
let v = ValueDef::parse(r"Value DateTime (\S+\s+\d+\s+\d+|\<no date\>)", 1).unwrap();
assert!(v.pattern.contains("<no date>"));
assert!(!v.pattern.contains(r"\<"));
}
#[test]
fn test_normalize_pattern_angle_brackets() {
assert_eq!(normalize_pattern(r"^\s*\<\S+"), r"^\s*<\S+");
assert_eq!(normalize_pattern(r"\<omited\>"), "<omited>");
assert_eq!(normalize_pattern(r"\s+\d+"), r"\s+\d+");
assert_eq!(normalize_pattern("<already>"), "<already>");
}
#[test]
fn test_normalize_pattern_lookaround_quantifiers() {
assert_eq!(
normalize_pattern(r"(?<=[^()\s])+"),
r"(?<=[^()\s])"
);
assert_eq!(normalize_pattern(r"(?=foo)*"), r"(?=foo)");
assert_eq!(normalize_pattern(r"(?<!bar)?"), r"(?<!bar)");
assert_eq!(normalize_pattern(r"(?!baz){2,3}"), r"(?!baz)");
assert_eq!(normalize_pattern(r"(?<=x)+?"), r"(?<=x)");
}
#[test]
fn test_normalize_pattern_preserves_normal_groups() {
assert_eq!(normalize_pattern(r"(foo)+"), r"(foo)+");
assert_eq!(normalize_pattern(r"(?:bar)*"), r"(?:bar)*");
assert_eq!(normalize_pattern(r"(?P<name>baz){2}"), r"(?P<name>baz){2}");
}
#[test]
fn test_normalize_pattern_combined() {
let input = r"^\s+\<omited\s+output\>(?<=[^()\s])+";
let expected = r"^\s+<omited\s+output>(?<=[^()\s])";
assert_eq!(normalize_pattern(input), expected);
}
#[test]
fn test_normalize_pattern_char_class_with_parens() {
assert_eq!(
normalize_pattern(r"(?<=[^()\s])+(\s+foo)"),
r"(?<=[^()\s])(\s+foo)"
);
}
}