1#![deny(missing_docs)]
27
28pub const REPLACEMENT: &str = "[REDACTED]";
30
31pub fn mask(s: &str) -> String {
33 let mut out = String::with_capacity(s.len());
34 let bytes = s.as_bytes();
35 let mut i = 0;
36 while i < bytes.len() {
37 if let Some(end) = match_secret(s, i) {
38 out.push_str(REPLACEMENT);
39 i = end;
40 } else {
41 let c = s[i..].chars().next().unwrap();
42 out.push(c);
43 i += c.len_utf8();
44 }
45 }
46 out
47}
48
49pub fn has_secret(s: &str) -> bool {
51 let bytes = s.as_bytes();
52 let mut i = 0;
53 while i < bytes.len() {
54 if match_secret(s, i).is_some() {
55 return true;
56 }
57 i += 1;
58 }
59 false
60}
61
62fn match_secret(s: &str, i: usize) -> Option<usize> {
63 let bytes = s.as_bytes();
64 let rest = &s[i..];
65
66 let prefixes: &[&str] = &[
68 "sk-", "sk_live_", "sk_test_", "rk_live_", "ghp_", "github_pat_", "xoxb-", "xoxp-",
69 ];
70 for p in prefixes {
71 if rest.starts_with(p) {
72 let mut end = i + p.len();
73 while end < bytes.len()
74 && (bytes[end].is_ascii_alphanumeric() || matches!(bytes[end], b'_' | b'-'))
75 {
76 end += 1;
77 }
78 if end - (i + p.len()) >= 16 {
79 return Some(end);
80 }
81 }
82 }
83
84 if rest.starts_with("AKIA") {
86 let after = i + 4;
87 if after + 16 <= bytes.len() {
88 let tail = &bytes[after..after + 16];
89 if tail.iter().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit()) {
90 return Some(after + 16);
91 }
92 }
93 }
94
95 if rest.starts_with("eyJ") {
97 let mut end = i;
98 let mut dots = 0;
99 while end < bytes.len() {
100 let c = bytes[end];
101 if c.is_ascii_alphanumeric() || matches!(c, b'.' | b'_' | b'-') {
102 if c == b'.' {
103 dots += 1;
104 }
105 end += 1;
106 } else {
107 break;
108 }
109 }
110 if dots >= 2 && end - i >= 20 {
111 return Some(end);
112 }
113 }
114
115 None
116}