use anyhow::Result;
use log::{debug, warn};
use regex::{Regex, RegexBuilder};
use lazy_static::lazy_static;
use std::sync::{Arc, RwLock};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::collections::hash_map::DefaultHasher;
use crate::config::{RedactionRule, RedactionConfig, MAX_PATTERN_LENGTH};
use crate::errors::CleanshError;
#[derive(Debug)]
pub struct CompiledRule {
pub regex: Regex,
pub replace_with: String,
pub name: String,
pub programmatic_validation: bool,
}
#[derive(Debug)]
pub struct CompiledRules {
pub rules: Vec<CompiledRule>,
}
lazy_static! {
static ref COMPILED_RULES_CACHE: RwLock<HashMap<u64, Arc<CompiledRules>>> = RwLock::new(HashMap::new());
}
fn hash_config(config: &RedactionConfig) -> u64 {
let mut hasher = DefaultHasher::new();
let mut rules_to_hash = config.rules.clone();
rules_to_hash.sort_by(|a, b| a.name.cmp(&b.name));
rules_to_hash.hash(&mut hasher);
hasher.finish()
}
pub fn compile_rules(rules_to_compile: Vec<RedactionRule>) -> Result<CompiledRules, CleanshError> {
debug!("Starting compilation of {} rules.", rules_to_compile.len());
let mut compiled_rules = Vec::new();
let mut compilation_errors = Vec::new();
for rule in rules_to_compile {
match rule.pattern.as_ref() {
Some(pattern) => {
debug!(
"Attempting to compile rule: '{}' with pattern '{:?}'",
&rule.name, pattern
);
if pattern.len() > MAX_PATTERN_LENGTH {
compilation_errors.push(CleanshError::PatternLengthExceeded(
rule.name,
pattern.len(),
MAX_PATTERN_LENGTH
));
continue;
}
let regex_result = RegexBuilder::new(pattern)
.multi_line(rule.multiline)
.dot_matches_new_line(rule.dot_matches_new_line)
.size_limit(10 * (1 << 20)) .build();
match regex_result {
Ok(regex) => {
log::debug!(
target: "cleansh_core::sanitizer",
"Rule '{}' compiled successfully.",
&rule.name
);
compiled_rules.push(CompiledRule {
regex,
replace_with: rule.replace_with,
name: rule.name,
programmatic_validation: rule.programmatic_validation,
});
}
Err(e) => {
compilation_errors.push(CleanshError::RuleCompilationError(rule.name, e));
}
}
}
None => {
warn!("Skipping rule '{}' because its pattern is missing.", &rule.name);
continue;
}
}
}
if !compilation_errors.is_empty() {
let error_message = compilation_errors.iter()
.map(|e| e.to_string())
.collect::<Vec<String>>()
.join("\n");
Err(CleanshError::Fatal(format!("Failed to compile {} rule(s):\n{}", compilation_errors.len(), error_message)))
} else {
debug!(
"Finished compiling rules. Total compiled: {}.",
compiled_rules.len()
);
Ok(CompiledRules { rules: compiled_rules })
}
}
pub fn get_or_compile_rules(config: &RedactionConfig) -> Result<Arc<CompiledRules>> {
let cache_key = hash_config(config);
{
let cache = COMPILED_RULES_CACHE.read().unwrap();
if let Some(rules) = cache.get(&cache_key) {
debug!("Serving compiled rules from cache for key: {}", &cache_key);
return Ok(Arc::clone(rules));
}
}
debug!("Compiled rules not found in cache. Compiling now.");
let compiled = compile_rules(config.rules.clone())?;
let compiled_arc = Arc::new(compiled);
COMPILED_RULES_CACHE.write().unwrap().insert(cache_key, Arc::clone(&compiled_arc));
debug!("Successfully compiled and cached rules for key: {}", &cache_key);
Ok(compiled_arc)
}