apollo_redaction/strategy/
card.rs1use crate::RedactionStrategy;
2use std::fmt;
3
4#[derive(Default, Clone)]
25pub struct CardRedactor;
26
27const MIN_CARD_DIGITS: usize = 13;
29
30impl<T: AsRef<str>> RedactionStrategy<T> for CardRedactor {
31 fn display(&self, value: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 let s = value.as_ref();
33
34 let total_digits = s.chars().filter(|c| c.is_ascii_digit()).count();
36
37 if total_digits < MIN_CARD_DIGITS {
39 return write!(f, "[REDACTED: invalid card number]");
40 }
41
42 let redact_count = total_digits - 4;
43
44 let mut digits_seen = 0;
46 for c in s.chars() {
47 if c.is_ascii_digit() {
48 if digits_seen < redact_count {
49 write!(f, "*")?;
50 } else {
51 write!(f, "{c}")?;
52 }
53 digits_seen += 1;
54 } else {
55 write!(f, "{c}")?;
56 }
57 }
58
59 Ok(())
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use crate::Redacted;
67
68 fn card(input: &str) -> String {
69 Redacted::<_, CardRedactor>::new(input).to_string()
70 }
71
72 #[test]
73 fn test_plain_card() {
74 assert_eq!(card("4242424242424242"), "************4242");
75 }
76
77 #[test]
78 fn test_amex() {
79 assert_eq!(card("378282246310005"), "***********0005");
80 }
81
82 #[test]
83 fn test_19_digit_card() {
84 assert_eq!(card("6011000000000000000"), "***************0000");
85 }
86
87 #[test]
88 fn test_invalid_card() {
89 let expected = "[REDACTED: invalid card number]";
90 assert_eq!(card("123"), expected);
91 assert_eq!(card("1234"), expected);
92 assert_eq!(card("123456789012"), expected); assert_eq!(card("1234-5678-9012"), expected); assert_eq!(card("no digits here"), expected);
95 }
96
97 #[test]
98 fn test_13_digit_card() {
99 assert_eq!(card("4000000000000"), "*********0000");
100 }
101
102 #[test]
103 fn test_with_dashes() {
104 assert_eq!(card("4242-4242-4242-4242"), "****-****-****-4242");
105 }
106
107 #[test]
108 fn test_with_spaces() {
109 assert_eq!(card("4242 4242 4242 4242"), "**** **** **** 4242");
110 }
111
112 #[test]
113 fn test_leading_trailing_spaces() {
114 assert_eq!(card(" 4242424242424242 "), " ************4242 ");
115 }
116
117 #[test]
118 fn test_mixed_formatting() {
119 assert_eq!(card(" 4242-4242 4242-4242 "), " ****-**** ****-4242 ");
120 }
121}