use std::borrow::Cow;
#[inline]
fn is_log_injection_char(c: char, keep_tab: bool) -> bool {
if c == '\t' {
return !keep_tab;
}
matches!(c,
'\u{0000}'..='\u{001F}' | '\u{007F}' | '\u{0080}'..='\u{009F}' | '\u{2028}' | '\u{2029}' )
}
pub(crate) fn strip_log_injection_str<'a>(
text: &'a str,
replacement: &str,
keep_tab: bool,
) -> Cow<'a, str> {
if !text.chars().any(|c| is_log_injection_char(c, keep_tab)) {
return Cow::Borrowed(text);
}
let mut out = String::with_capacity(text.len());
for c in text.chars() {
if is_log_injection_char(c, keep_tab) {
out.push_str(replacement);
} else {
out.push(c);
}
}
Cow::Owned(out)
}
pub(crate) fn validate_log_replacement(
replacement: &str,
keep_tab: bool,
) -> Result<(), crate::ErrorRepr> {
if let Some(c) = replacement
.chars()
.find(|&c| is_log_injection_char(c, keep_tab))
{
return Err(crate::ErrorRepr::InvalidLogReplacement {
codepoint: c as u32,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn strip(s: &str) -> String {
strip_log_injection_str(s, "\u{FFFD}", false).into_owned()
}
#[test]
fn neutralizes_crlf_and_nul() {
assert_eq!(strip("a\r\nb\0c"), "a\u{FFFD}\u{FFFD}b\u{FFFD}c");
}
#[test]
fn neutralizes_ansi_introducer_leaving_residue() {
assert_eq!(strip("\u{1B}[31mred"), "\u{FFFD}[31mred");
}
#[test]
fn neutralizes_nel_ls_ps_and_c1() {
assert_eq!(
strip("a\u{0085}b\u{2028}c\u{2029}d\u{009B}e"),
"a\u{FFFD}b\u{FFFD}c\u{FFFD}d\u{FFFD}e"
);
}
#[test]
fn neutralizes_del() {
assert_eq!(strip("a\u{7F}b"), "a\u{FFFD}b");
}
#[test]
fn tab_neutralized_by_default_kept_when_opted_in() {
assert_eq!(strip("a\tb"), "a\u{FFFD}b");
assert_eq!(
strip_log_injection_str("a\tb", "\u{FFFD}", true).into_owned(),
"a\tb"
);
}
#[test]
fn clean_line_borrows() {
assert!(matches!(
strip_log_injection_str("plain ascii line", "\u{FFFD}", false),
Cow::Borrowed(_)
));
assert!(matches!(
strip_log_injection_str("café <b>&</b> ☕", "\u{FFFD}", false),
Cow::Borrowed(_)
));
}
#[test]
fn preserves_html_metacharacters() {
let s = "<script>alert(1)</script> & \"x\"";
assert_eq!(strip(s), s);
}
#[test]
fn idempotent() {
let s = "a\r\nb\u{1B}\tc";
let once = strip(s);
assert_eq!(strip(&once), once);
}
#[test]
fn output_has_no_raw_cr_lf_esc() {
let s = "x\r\n\u{1B}\u{0085}\u{2028}y";
let out = strip(s);
assert!(!out.contains(['\r', '\n', '\u{1B}']));
}
}