use crate::RedactionStrategy;
use std::fmt;
#[derive(Default, Clone)]
pub struct CardRedactor;
const MIN_CARD_DIGITS: usize = 13;
impl<T: AsRef<str>> RedactionStrategy<T> for CardRedactor {
fn display(&self, value: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = value.as_ref();
let total_digits = s.chars().filter(|c| c.is_ascii_digit()).count();
if total_digits < MIN_CARD_DIGITS {
return write!(f, "[REDACTED: invalid card number]");
}
let redact_count = total_digits - 4;
let mut digits_seen = 0;
for c in s.chars() {
if c.is_ascii_digit() {
if digits_seen < redact_count {
write!(f, "*")?;
} else {
write!(f, "{c}")?;
}
digits_seen += 1;
} else {
write!(f, "{c}")?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Redacted;
fn card(input: &str) -> String {
Redacted::<_, CardRedactor>::new(input).to_string()
}
#[test]
fn test_plain_card() {
assert_eq!(card("4242424242424242"), "************4242");
}
#[test]
fn test_amex() {
assert_eq!(card("378282246310005"), "***********0005");
}
#[test]
fn test_19_digit_card() {
assert_eq!(card("6011000000000000000"), "***************0000");
}
#[test]
fn test_invalid_card() {
let expected = "[REDACTED: invalid card number]";
assert_eq!(card("123"), expected);
assert_eq!(card("1234"), expected);
assert_eq!(card("123456789012"), expected); assert_eq!(card("1234-5678-9012"), expected); assert_eq!(card("no digits here"), expected);
}
#[test]
fn test_13_digit_card() {
assert_eq!(card("4000000000000"), "*********0000");
}
#[test]
fn test_with_dashes() {
assert_eq!(card("4242-4242-4242-4242"), "****-****-****-4242");
}
#[test]
fn test_with_spaces() {
assert_eq!(card("4242 4242 4242 4242"), "**** **** **** 4242");
}
#[test]
fn test_leading_trailing_spaces() {
assert_eq!(card(" 4242424242424242 "), " ************4242 ");
}
#[test]
fn test_mixed_formatting() {
assert_eq!(card(" 4242-4242 4242-4242 "), " ****-**** ****-4242 ");
}
}