spaces_printer/
secrets.rs1use 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}