use regex::Regex;
use super::patterns::{BUILTIN_PATTERNS, SecretPattern};
const MASK: &str = "***MASKED***";
pub struct MaskingEngine {
rules: Vec<(Regex, Option<usize>)>,
}
impl MaskingEngine {
pub fn builtin() -> Self {
Self::from_patterns(BUILTIN_PATTERNS, &[])
}
pub fn with_custom(custom: &[String]) -> Self {
Self::from_patterns(BUILTIN_PATTERNS, custom)
}
fn from_patterns(builtin: &[SecretPattern], custom: &[String]) -> Self {
let mut rules = Vec::new();
for sp in builtin {
match Regex::new(sp.pattern) {
Ok(re) => rules.push((re, sp.value_group)),
Err(e) => log::warn!("Built-in mask pattern {:?} failed to compile: {e}", sp.name),
}
}
for pat in custom {
match Regex::new(pat) {
Ok(re) => rules.push((re, None)),
Err(e) => log::warn!("Custom mask pattern {:?} failed to compile: {e}", pat),
}
}
Self { rules }
}
pub fn mask(&self, text: &str) -> String {
let mut result = text.to_string();
for (re, group) in &self.rules {
result = mask_with_regex(&result, re, *group);
}
result
}
pub fn mask_if_needed(&self, text: &str) -> Option<String> {
let masked = self.mask(text);
if masked == text { None } else { Some(masked) }
}
}
fn mask_with_regex(text: &str, re: &Regex, group: Option<usize>) -> String {
match group {
None => re.replace_all(text, MASK).to_string(),
Some(g) => {
let mut result = text.to_string();
let captures: Vec<_> = re.captures_iter(text).collect();
for cap in captures.iter().rev() {
if let Some(m) = cap.get(g) {
result.replace_range(m.start()..m.end(), MASK);
}
}
result
}
}
}
#[cfg(test)]
mod tests;