Skip to main content

spaces_printer/
secrets.rs

1use std::sync::Arc;
2
3pub const DEFAULT_MAX_REDACTIONS: usize = 16;
4pub const DEFAULT_MIN_SECRET_LENGTH: usize = 4;
5
6#[derive(Debug, Clone)]
7pub struct Secrets {
8    pub secrets: Vec<Arc<str>>,
9    pub redacted: Arc<str>,
10    pub max_redactions: usize,
11    pub min_secret_length: usize,
12}
13
14impl Secrets {
15    pub fn redact(&self, text: Arc<str>) -> Arc<str> {
16        if self.secrets.is_empty() {
17            text
18        } else {
19            let mut result = text.to_string();
20            for secret in &self.secrets {
21                if !secret.is_empty() && secret.len() >= self.min_secret_length {
22                    result = result.replacen(
23                        secret.as_ref(),
24                        self.redacted.as_ref(),
25                        self.max_redactions,
26                    );
27                    if let Some(pos) = result.find(secret.as_ref()) {
28                        result.truncate(pos);
29                        result.push_str("...");
30                    }
31                }
32            }
33            result.into()
34        }
35    }
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    #[test]
43    fn redact_skips_empty_secrets() {
44        let secrets = Secrets {
45            secrets: vec!["".into()],
46            redacted: "REDACTED".into(),
47            max_redactions: usize::MAX,
48            min_secret_length: 0,
49        };
50        let input: Arc<str> = "hello world".into();
51        let result = secrets.redact(input.clone());
52        assert_eq!(result, input, "Empty secret should not alter the text");
53    }
54
55    #[test]
56    fn redact_skips_empty_secrets_among_valid_ones() {
57        let secrets = Secrets {
58            secrets: vec!["".into(), "world".into()],
59            redacted: "REDACTED".into(),
60            max_redactions: usize::MAX,
61            min_secret_length: 0,
62        };
63        let input: Arc<str> = "hello world".into();
64        let result = secrets.redact(input);
65        assert_eq!(
66            result.as_ref(),
67            "hello REDACTED",
68            "Only the non-empty secret should be redacted"
69        );
70    }
71
72    #[test]
73    fn redact_with_no_secrets() {
74        let secrets = Secrets {
75            secrets: vec![],
76            redacted: "REDACTED".into(),
77            max_redactions: usize::MAX,
78            min_secret_length: 0,
79        };
80        let input: Arc<str> = "hello world".into();
81        let result = secrets.redact(input.clone());
82        assert_eq!(result, input, "No secrets means text is returned unchanged");
83    }
84
85    #[test]
86    fn redact_replaces_valid_secret() {
87        let secrets = Secrets {
88            secrets: vec!["secret_token".into()],
89            redacted: "REDACTED".into(),
90            max_redactions: usize::MAX,
91            min_secret_length: 0,
92        };
93        let input: Arc<str> = "my secret_token is here".into();
94        let result = secrets.redact(input);
95        assert_eq!(result.as_ref(), "my REDACTED is here");
96    }
97
98    #[test]
99    fn redact_all_empty_secrets_leaves_text_unchanged() {
100        let secrets = Secrets {
101            secrets: vec!["".into(), "".into(), "".into()],
102            redacted: "REDACTED".into(),
103            max_redactions: usize::MAX,
104            min_secret_length: 0,
105        };
106        let input: Arc<str> = "nothing should change".into();
107        let result = secrets.redact(input.clone());
108        assert_eq!(
109            result, input,
110            "All-empty secrets list should not alter the text"
111        );
112    }
113
114    #[test]
115    fn redact_max_redactions_limits_replacements() {
116        let secrets = Secrets {
117            secrets: vec!["secret".into()],
118            redacted: "REDACTED".into(),
119            max_redactions: 2,
120            min_secret_length: 0,
121        };
122        let input: Arc<str> = "secret secret secret secret".into();
123        let result = secrets.redact(input);
124        assert_eq!(
125            result.as_ref(),
126            "REDACTED REDACTED ...",
127            "Only the first max_redactions occurrences should be replaced, remainder truncated"
128        );
129    }
130
131    #[test]
132    fn redact_max_redactions_zero_truncates_at_first_occurrence() {
133        let secrets = Secrets {
134            secrets: vec!["secret".into()],
135            redacted: "REDACTED".into(),
136            max_redactions: 0,
137            min_secret_length: 0,
138        };
139        let input: Arc<str> = "secret secret".into();
140        let result = secrets.redact(input);
141        assert_eq!(
142            result.as_ref(),
143            "...",
144            "max_redactions=0 replaces nothing, so secret is immediately truncated"
145        );
146    }
147
148    #[test]
149    fn redact_truncates_after_max_redactions() {
150        let secrets = Secrets {
151            secrets: vec!["secret".into()],
152            redacted: "REDACTED".into(),
153            max_redactions: 1,
154            min_secret_length: 0,
155        };
156        let input: Arc<str> = "before secret after secret trailing".into();
157        let result = secrets.redact(input);
158        assert_eq!(
159            result.as_ref(),
160            "before REDACTED after ...",
161            "Text before the unredacted occurrence should be preserved, then truncated with ..."
162        );
163    }
164
165    #[test]
166    fn redact_min_secret_length_skips_short_secrets() {
167        let secrets = Secrets {
168            secrets: vec!["ab".into(), "longersecret".into()],
169            redacted: "REDACTED".into(),
170            max_redactions: usize::MAX,
171            min_secret_length: 5,
172        };
173        let input: Arc<str> = "ab longersecret".into();
174        let result = secrets.redact(input);
175        assert_eq!(
176            result.as_ref(),
177            "ab REDACTED",
178            "Secrets shorter than min_secret_length should not be redacted"
179        );
180    }
181
182    #[test]
183    fn redact_min_secret_length_exact_boundary() {
184        let secrets = Secrets {
185            secrets: vec!["abc".into(), "abcd".into()],
186            redacted: "REDACTED".into(),
187            max_redactions: usize::MAX,
188            min_secret_length: 4,
189        };
190        let input: Arc<str> = "abc abcd".into();
191        let result = secrets.redact(input);
192        assert_eq!(
193            result.as_ref(),
194            "abc REDACTED",
195            "Secret with length == min_secret_length should be redacted, shorter should not"
196        );
197    }
198}