1use std::borrow::Cow;
4use std::fmt;
5
6use serde::{Deserialize, Deserializer};
7
8#[derive(Clone, PartialEq, Eq)]
13pub struct Secret<T>(T);
14
15impl<T> Secret<T> {
16 pub fn new(value: T) -> Self {
18 Self(value)
19 }
20
21 pub fn expose(&self) -> &T {
23 &self.0
24 }
25}
26
27impl<T> fmt::Debug for Secret<T> {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 f.write_str("Secret([REDACTED])")
30 }
31}
32
33impl<T> From<T> for Secret<T> {
34 fn from(value: T) -> Self {
35 Self(value)
36 }
37}
38
39impl<'de, T: Deserialize<'de>> Deserialize<'de> for Secret<T> {
40 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
41 T::deserialize(deserializer).map(Secret)
42 }
43}
44
45#[derive(Debug, Clone, Default)]
49pub struct Redactor {
50 secrets: Vec<String>,
51}
52
53impl Redactor {
54 pub const MIN_LEN: usize = 6;
56
57 pub fn new() -> Self {
59 Self::default()
60 }
61
62 pub fn add(&mut self, secret: &Secret<String>) {
64 let value = secret.expose();
65 if value.len() >= Self::MIN_LEN && !self.secrets.contains(value) {
66 self.secrets.push(value.clone());
67 self.secrets.sort_by_key(|s| std::cmp::Reverse(s.len()));
69 }
70 }
71
72 pub fn redact<'a>(&self, text: &'a str) -> Cow<'a, str> {
74 let mut out = Cow::Borrowed(text);
75 for secret in &self.secrets {
76 if out.contains(secret.as_str()) {
77 out = Cow::Owned(out.replace(secret.as_str(), "[REDACTED]"));
78 }
79 }
80 out
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn debug_hides_value() {
90 let s = Secret::new("hunter22".to_string());
91 assert_eq!(format!("{s:?}"), "Secret([REDACTED])");
92 assert_eq!(s.expose(), "hunter22");
93 }
94
95 #[test]
96 fn redactor_replaces_known_secrets() {
97 let mut r = Redactor::new();
98 r.add(&Secret::new("123456:abc-xyz".to_string()));
99 r.add(&Secret::new("short".to_string()));
100 assert_eq!(
101 r.redact("GET https://bot-api/bot123456:abc-xyz/getMe short"),
102 "GET https://bot-api/bot[REDACTED]/getMe short"
103 );
104 assert!(matches!(r.redact("nothing here"), Cow::Borrowed(_)));
105 }
106}