1use std::sync::{LazyLock, RwLock};
2
3static SECRETS: LazyLock<RwLock<Vec<String>>> = LazyLock::new(|| RwLock::new(Vec::new()));
4
5pub fn register(secret: &str) {
7 if secret.len() < 8 {
8 return;
9 }
10 let mut secrets = SECRETS
11 .write()
12 .unwrap_or_else(|poisoned| poisoned.into_inner());
13 if !secrets.iter().any(|known| known == secret) {
14 secrets.push(secret.to_string());
15 }
16}
17
18pub fn redact(text: &str) -> String {
19 let secrets = SECRETS
20 .read()
21 .unwrap_or_else(|poisoned| poisoned.into_inner());
22 secrets.iter().fold(text.to_string(), |text, secret| {
23 text.replace(secret.as_str(), "[REDACTED]")
24 })
25}
26
27pub fn mask(token: &str) -> String {
29 let chars: Vec<char> = token.chars().collect();
30 if chars.len() <= 12 {
31 return "****".to_string();
32 }
33 let head: String = chars[..4].iter().collect();
34 let tail: String = chars[chars.len() - 4..].iter().collect();
35 format!("{head}…{tail}")
36}
37
38#[cfg(test)]
39mod tests {
40 use super::*;
41
42 #[test]
43 fn registered_secrets_are_redacted() {
44 register("perm-c2VjcmV0.dGVzdA==.abcdef123456");
45 let text = redact("Bearer perm-c2VjcmV0.dGVzdA==.abcdef123456 failed");
46 assert_eq!(text, "Bearer [REDACTED] failed");
47 }
48
49 #[test]
50 fn short_strings_are_not_registered() {
51 register("abc");
52 assert_eq!(redact("abc"), "abc");
53 }
54
55 #[test]
56 fn mask_keeps_four_characters_at_each_end() {
57 assert_eq!(mask("perm-c2VjcmV0.abcdef123456"), "perm…3456");
58 assert_eq!(mask("short"), "****");
59 }
60}