const MASK_SENTINEL: &str = "\u{0}TS\u{0}";
pub(crate) fn mask_volatile_prefix(line: &str) -> Option<String> {
let mut rest = strip_iso_timestamp(line).or_else(|| strip_syslog_timestamp(line))?;
while let Some(after) = strip_bracketed_counter(rest) {
rest = after;
}
Some(format!("{MASK_SENTINEL}{rest}"))
}
fn strip_digits(s: &str, n: usize) -> Option<&str> {
let head = s.get(..n)?;
head.bytes()
.all(|byte| byte.is_ascii_digit())
.then(|| &s[n..])
}
fn strip_syslog_day(s: &str) -> Option<&str> {
strip_digits(s, 2).or_else(|| strip_digits(s.strip_prefix(' ')?, 1))
}
fn strip_iso_timestamp(s: &str) -> Option<&str> {
let s = strip_digits(s, 4)?;
let s = s.strip_prefix('-')?;
let s = strip_digits(s, 2)?;
let s = s.strip_prefix('-')?;
let s = strip_digits(s, 2)?;
let s = s.strip_prefix('T').or_else(|| s.strip_prefix(' '))?;
let s = strip_digits(s, 2)?;
let s = s.strip_prefix(':')?;
let s = strip_digits(s, 2)?;
let s = s.strip_prefix(':')?;
let s = strip_digits(s, 2)?;
Some(strip_offset(strip_fraction(s)))
}
fn strip_fraction(s: &str) -> &str {
let Some(rest) = s.strip_prefix('.').or_else(|| s.strip_prefix(',')) else {
return s;
};
let digits = rest.bytes().take_while(u8::is_ascii_digit).count();
if digits == 0 {
s
} else {
&rest[digits..]
}
}
fn strip_offset(s: &str) -> &str {
if let Some(rest) = s.strip_prefix('Z') {
return rest;
}
let Some(rest) = s.strip_prefix('+').or_else(|| s.strip_prefix('-')) else {
return s;
};
strip_digits(rest, 2)
.and_then(|r| r.strip_prefix(':'))
.and_then(|r| strip_digits(r, 2))
.unwrap_or(s)
}
fn strip_syslog_timestamp(s: &str) -> Option<&str> {
const MONTHS: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
let month = MONTHS.iter().find(|m| s.starts_with(*m))?;
let s = &s[month.len()..];
let s = s.strip_prefix(' ')?;
let s = strip_syslog_day(s)?;
let s = s.strip_prefix(' ')?;
let s = strip_digits(s, 2)?;
let s = s.strip_prefix(':')?;
let s = strip_digits(s, 2)?;
let s = s.strip_prefix(':')?;
strip_digits(s, 2)
}
fn strip_bracketed_counter(s: &str) -> Option<&str> {
let s = s.strip_prefix(' ').unwrap_or(s);
let rest = s.strip_prefix('[')?;
let end = rest.find(']')?;
let token = &rest[..end];
(!token.is_empty() && token.bytes().all(|b| b.is_ascii_hexdigit())).then(|| &rest[end + 1..])
}
#[cfg(test)]
#[path = "log_normalize_tests.rs"]
mod tests;