use regex::Regex;
use regex::RegexBuilder;
use serde::Deserialize;
use std::path::Path;
use super::{FailurePattern, FailureStrategy, Pattern, SuccessPattern, SuccessStrategy};
use crate::error::Error;
const MAX_REGEX_LENGTH: usize = 500;
const REGEX_SIZE_LIMIT: usize = 100 * 1024;
fn validate_and_compile_regex(pattern: &str) -> Result<Regex, Error> {
if pattern.len() > MAX_REGEX_LENGTH {
return Err(Error::Pattern(format!(
"regex too long ({} > {} chars)",
pattern.len(),
MAX_REGEX_LENGTH
)));
}
RegexBuilder::new(pattern)
.size_limit(REGEX_SIZE_LIMIT)
.build()
.map_err(|e| Error::Pattern(format!("regex compilation failed: {e}")))
}
#[derive(Deserialize)]
pub struct PatternFile {
pub command_match: String,
pub success: Option<SuccessSection>,
pub failure: Option<FailureSection>,
}
#[derive(Deserialize)]
pub struct SuccessSection {
#[serde(default)]
pub(crate) strategy: Option<String>,
#[serde(rename = "pattern")]
pub(crate) success_pattern: Option<String>,
pub(crate) summary: Option<String>,
pub(crate) lines: Option<usize>,
#[serde(rename = "grep")]
pub(crate) grep_pattern: Option<String>,
}
#[derive(Deserialize)]
pub struct FailureSection {
pub(crate) strategy: Option<String>,
pub(crate) lines: Option<usize>,
#[serde(rename = "grep")]
pub(crate) grep_pattern: Option<String>,
pub(crate) start: Option<String>,
pub(crate) end: Option<String>,
}
pub fn load_user_patterns(dir: &Path) -> Vec<Pattern> {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
let mut patterns = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "toml") {
if let Ok(p) = load_pattern_file(&path) {
patterns.push(p);
}
}
}
patterns
}
fn load_pattern_file(path: &Path) -> Result<Pattern, Error> {
let content =
std::fs::read_to_string(path).map_err(|e| Error::Pattern(format!("{path:?}: {e}")))?;
parse_pattern_str(&content).map_err(|e| {
if let Error::Pattern(msg) = e {
Error::Pattern(format!("{path:?}: {msg}"))
} else {
e
}
})
}
pub fn parse_pattern_str(content: &str) -> Result<Pattern, Error> {
let pf: PatternFile =
toml::from_str(content).map_err(|e| Error::Pattern(format!("TOML parse: {e}")))?;
let command_match = validate_and_compile_regex(&pf.command_match)?;
let success = pf
.success
.map(|s| -> Result<SuccessPattern, Error> {
let strategy = match s.strategy.as_deref().unwrap_or("regex") {
"tail" => SuccessStrategy::Tail {
lines: s.lines.unwrap_or(30),
},
"head" => SuccessStrategy::Head {
lines: s.lines.unwrap_or(20),
},
"grep" => {
let pat = s.grep_pattern.ok_or_else(|| {
Error::Pattern("grep strategy requires 'grep' field".into())
})?;
let pattern = validate_and_compile_regex(&pat)?;
SuccessStrategy::Grep { pattern }
}
"regex" => {
let pattern = s.success_pattern.ok_or_else(|| {
Error::Pattern("regex strategy requires 'pattern' field".into())
})?;
let summary = s.summary.ok_or_else(|| {
Error::Pattern("regex strategy requires 'summary' field".into())
})?;
let regex = validate_and_compile_regex(&pattern)?;
SuccessStrategy::Regex {
pattern: regex,
summary,
}
}
other => {
return Err(Error::Pattern(format!("unknown success strategy: {other}")));
}
};
Ok(SuccessPattern { strategy })
})
.transpose()?;
let failure = pf
.failure
.map(|f| -> Result<FailurePattern, Error> {
let strategy = match f.strategy.as_deref().unwrap_or("tail") {
"tail" => FailureStrategy::Tail {
lines: f.lines.unwrap_or(30),
},
"head" => FailureStrategy::Head {
lines: f.lines.unwrap_or(20),
},
"grep" => {
let pat = f.grep_pattern.ok_or_else(|| {
Error::Pattern("grep strategy requires 'grep' field".into())
})?;
let pattern = validate_and_compile_regex(&pat)?;
FailureStrategy::Grep { pattern }
}
"between" => {
let start = f.start.ok_or_else(|| {
Error::Pattern("between strategy requires 'start'".into())
})?;
let end = f
.end
.ok_or_else(|| Error::Pattern("between strategy requires 'end'".into()))?;
FailureStrategy::Between { start, end }
}
other => {
return Err(Error::Pattern(format!("unknown strategy: {other}")));
}
};
Ok(FailurePattern { strategy })
})
.transpose()?;
Ok(Pattern {
command_match,
success,
failure,
})
}
pub fn validate_pattern_regexes(toml_str: &str) -> Result<(), Error> {
#[derive(Deserialize)]
struct Check {
command_match: String,
#[serde(default)]
success: Option<SuccessSection>,
#[serde(default)]
failure: Option<FailureSection>,
}
let check: Check =
toml::from_str(toml_str).map_err(|e| Error::Pattern(format!("TOML parse: {e}")))?;
validate_and_compile_regex(&check.command_match)?;
if let Some(ref s) = check.success {
match s.strategy.as_deref().unwrap_or("regex") {
"tail" | "head" => {} "grep" => {
let pat = s
.grep_pattern
.as_ref()
.ok_or_else(|| Error::Pattern("grep strategy requires 'grep' field".into()))?;
if pat.is_empty() {
return Err(Error::Pattern("grep regex must not be empty".into()));
}
validate_and_compile_regex(pat)?;
}
"regex" => {
let pattern = s.success_pattern.as_ref().ok_or_else(|| {
Error::Pattern("regex strategy requires 'pattern' field".into())
})?;
validate_and_compile_regex(pattern)?;
}
other => return Err(Error::Pattern(format!("unknown success strategy: {other}"))),
}
}
if let Some(ref f) = check.failure {
match f.strategy.as_deref().unwrap_or("tail") {
"tail" | "head" => {} "grep" => {
let pat = f
.grep_pattern
.as_ref()
.ok_or_else(|| Error::Pattern("grep strategy requires 'grep' field".into()))?;
if pat.is_empty() {
return Err(Error::Pattern("grep regex must not be empty".into()));
}
validate_and_compile_regex(pat)?;
}
"between" => {
let start = f.start.as_ref().ok_or_else(|| {
Error::Pattern("between strategy requires 'start' field".into())
})?;
let end = f.end.as_ref().ok_or_else(|| {
Error::Pattern("between strategy requires 'end' field".into())
})?;
if start.is_empty() {
return Err(Error::Pattern("between 'start' must not be empty".into()));
}
if end.is_empty() {
return Err(Error::Pattern("between 'end' must not be empty".into()));
}
validate_and_compile_regex(start)?;
validate_and_compile_regex(end)?;
}
other => return Err(Error::Pattern(format!("unknown failure strategy: {other}"))),
}
}
Ok(())
}