use regex::Regex;
pub(super) fn apply_regex(s: &str, re: &Regex, replacement: &str) -> (String, usize) {
let count = re.find_iter(s).count();
if count == 0 {
return (s.to_string(), 0);
}
(re.replace_all(s, replacement).into_owned(), count)
}
pub(super) fn redact_env_kv(s: &str) -> (String, usize) {
const SECRET_KEYWORDS: &[&str] = &[
"TOKEN",
"SECRET",
"KEY",
"PASSWORD",
"PASSWD",
"PWD",
"APIKEY",
"API_KEY",
"CREDENTIAL",
"PASS",
];
let mut out = String::with_capacity(s.len());
let mut count = 0usize;
for line in s.split('\n') {
if let Some(eq_pos) = line.find('=') {
let key = &line[..eq_pos];
let upper_key = key.to_ascii_uppercase();
let value = &line[eq_pos + 1..];
if !value.is_empty() && SECRET_KEYWORDS.iter().any(|kw| upper_key.contains(kw)) {
out.push_str(key);
out.push_str("=[REDACTED_VALUE]");
count += 1;
} else {
out.push_str(line);
}
} else {
out.push_str(line);
}
out.push('\n');
}
if !s.ends_with('\n') && out.ends_with('\n') {
out.pop();
}
(out, count)
}
pub(super) fn redact_posix_paths(s: &str) -> (String, usize) {
let mut out = String::with_capacity(s.len());
let mut count = 0usize;
let mut chars = s.char_indices().peekable();
while let Some((i, ch)) = chars.next() {
if ch == '/' {
let prev_is_boundary = i == 0 || {
let prev_char = s[..i].chars().next_back().unwrap_or(' ');
prev_char.is_whitespace() || "\"'(,;:".contains(prev_char)
};
let next_is_alpha = s[i + ch.len_utf8()..]
.chars()
.next()
.map(|c| c.is_alphabetic() || c == '_' || c == '~')
.unwrap_or(false);
if prev_is_boundary && next_is_alpha {
let token_end = s[i..]
.find(|c: char| c.is_whitespace() || "\"'),:;".contains(c))
.map(|n| i + n)
.unwrap_or(s.len());
out.push('~');
count += 1;
while chars.peek().map(|&(j, _)| j < token_end).unwrap_or(false) {
chars.next();
}
continue;
}
}
out.push(ch);
}
(out, count)
}
pub(super) fn redact_windows_paths(s: &str) -> (String, usize) {
let mut out = String::with_capacity(s.len());
let mut count = 0usize;
let mut i = 0;
let bytes = s.as_bytes();
while i < bytes.len() {
if i + 2 < bytes.len()
&& bytes[i].is_ascii_alphabetic()
&& bytes[i + 1] == b':'
&& bytes[i + 2] == b'\\'
{
let is_start = i == 0 || {
let prev = bytes[i - 1];
prev.is_ascii_whitespace() || prev == b'"' || prev == b'\''
};
if is_start {
let end = s[i..]
.find(|c: char| c.is_whitespace() || "\"'".contains(c))
.map(|n| i + n)
.unwrap_or(s.len());
out.push('~');
count += 1;
i = end;
continue;
}
}
let ch = s[i..].chars().next().unwrap_or('\0');
out.push(ch);
i += ch.len_utf8();
}
(out, count)
}