1use std::{io, str};
3
4use anyhow::{bail, Result};
5use regex::{escape, Regex};
6
7#[cfg(feature = "redact-info")]
8use crate::data::Info;
9#[cfg(feature = "redact-json")]
10use crate::json;
11use crate::{
12 data::{Pattern, REDACT_PLACEHOLDER},
13 pattern,
14};
15
16pub struct Redaction {
18 #[cfg(feature = "redact-json")]
21 json: json::Redact,
22
23 pattern: pattern::Redact,
25}
26
27impl Default for Redaction {
28 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl Redaction {
43 #[must_use]
44 pub fn new() -> Self {
54 Self::custom(REDACT_PLACEHOLDER)
55 }
56
57 #[must_use]
58 pub fn custom(redact_placeholder: &str) -> Self {
71 Self {
72 #[cfg(feature = "redact-json")]
73 json: json::Redact::with_redact_placeholder(redact_placeholder),
74
75 pattern: pattern::Redact::with_redact_placeholder(redact_placeholder),
76 }
77 }
78
79 pub fn add_value(self, value: &str) -> Result<Self> {
95 let pattern = Pattern {
96 test: Regex::new(&format!("({})", escape(value)))?,
97 group: 1,
98 };
99
100 Ok(self.add_pattern(pattern))
101 }
102
103 pub fn add_values(self, values: Vec<&str>) -> Result<Self> {
119 let mut errors = vec![];
120
121 let patterns = values
122 .iter()
123 .filter_map(|val| match Regex::new(&format!("({})", escape(val))) {
124 Ok(test) => Some(Pattern { test, group: 1 }),
125 Err(_e) => {
126 errors.push((*val).to_string());
127 None
128 }
129 })
130 .collect::<Vec<_>>();
131
132 if !errors.is_empty() {
133 bail!("could not parse {} to regex", errors.join(","))
134 }
135
136 Ok(self.add_patterns(patterns))
137 }
138
139 #[must_use]
140 pub fn add_pattern(mut self, pattern: Pattern) -> Self {
160 self.pattern = self.pattern.add_pattern(pattern);
161 self
162 }
163
164 #[must_use]
165 pub fn add_patterns(mut self, patterns: Vec<Pattern>) -> Self {
185 self.pattern = self.pattern.add_patterns(patterns);
186 self
187 }
188
189 #[cfg(feature = "redact-json")]
190 #[must_use]
191 pub fn add_keys(mut self, keys: Vec<&str>) -> Self {
207 self.json = self.json.add_keys(keys);
208 self
209 }
210
211 #[cfg(feature = "redact-json")]
212 #[must_use]
213 pub fn add_paths(mut self, key: Vec<&str>) -> Self {
226 self.json = self.json.add_paths(key);
227 self
228 }
229
230 #[must_use]
231 pub fn redact_str(&self, str: &str) -> String {
233 self.pattern.redact_patterns(str, false).string
234 }
235
236 #[cfg(feature = "redact-info")]
237 #[must_use]
238 pub fn redact_str_with_info(&self, str: &str) -> Info {
243 self.pattern.redact_patterns(str, true)
244 }
245
246 pub fn redact_reader<R>(&self, rdr: R) -> Result<String>
252 where
253 R: io::Read,
254 {
255 let mut rdr_box = Box::new(rdr);
256 let mut buffer = Vec::new();
257 rdr_box.read_to_end(&mut buffer)?;
258 Ok(self.redact_str(str::from_utf8(&buffer)?))
259 }
260
261 #[cfg(feature = "redact-info")]
270 pub fn redact_reader_with_info<R>(&self, rdr: R) -> Result<Info>
271 where
272 R: io::Read,
273 {
274 let mut rdr_box = Box::new(rdr);
275 let mut buffer = Vec::new();
276 rdr_box.read_to_end(&mut buffer)?;
277 Ok(self.redact_str_with_info(str::from_utf8(&buffer)?))
278 }
279
280 #[cfg(feature = "redact-json")]
281 pub fn redact_json(&self, str: &str) -> Result<String> {
289 self.json.redact_str(&self.redact_str(str))
290 }
291
292 #[cfg(feature = "redact-json")]
293 pub fn redact_json_value(&self, value: &serde_json::Value) -> Result<serde_json::Value> {
301 let redact_str = self.redact_str(&value.to_string());
302 let mut value: serde_json::Value = serde_json::from_str(&redact_str)?;
303 Ok(self.json.redact_from_value(&mut value))
304 }
305}
306
307#[cfg(test)]
308mod test_redaction {
309
310 use std::{env, fs::File, io::Write};
311
312 use insta::assert_debug_snapshot;
313
314 use super::*;
315
316 const TEXT: &str = "foo,bar,baz,extra";
317
318 #[cfg(feature = "redact-json")]
319 use serde_json::json;
320
321 #[test]
322 fn test_by_pattern() {
323 let pattern = Pattern {
324 test: Regex::new("(foo)").unwrap(),
325 group: 1,
326 };
327 let patterns = vec![
328 Pattern {
329 test: Regex::new("(bar)").unwrap(),
330 group: 1,
331 },
332 Pattern {
333 test: Regex::new("(baz)").unwrap(),
334 group: 1,
335 },
336 ];
337 assert_debug_snapshot!(Redaction::new()
338 .add_pattern(pattern)
339 .add_patterns(patterns)
340 .redact_str(TEXT));
341 }
342
343 #[test]
344 fn test_bt_value() {
345 assert_debug_snapshot!(Redaction::new()
346 .add_value("foo")
347 .unwrap()
348 .add_values(vec!["bar", "baz"])
349 .unwrap()
350 .redact_str(TEXT));
351 }
352
353 #[test]
354 fn can_redact_str() {
355 let pattern = Pattern {
356 test: Regex::new("(bar)").unwrap(),
357 group: 1,
358 };
359 let redaction = Redaction::new().add_pattern(pattern);
360 assert_debug_snapshot!(redaction.redact_str(TEXT));
361 }
362
363 #[test]
364 #[cfg(feature = "redact-info")]
365 fn can_redact_str_with_info() {
366 let pattern = Pattern {
367 test: Regex::new("(bar)").unwrap(),
368 group: 1,
369 };
370 let redaction = Redaction::new().add_pattern(pattern);
371 assert_debug_snapshot!(redaction.redact_str_with_info(TEXT));
372 }
373
374 #[test]
375 fn can_redact_reader() {
376 let file_path = env::temp_dir().join("foo.txt");
377
378 let mut f = File::create(&file_path).unwrap();
379 #[allow(clippy::unused_io_amount)]
380 f.write(TEXT.as_bytes()).unwrap();
381
382 let pattern = Pattern {
383 test: Regex::new("(bar)").unwrap(),
384 group: 1,
385 };
386
387 let redaction = Redaction::new().add_pattern(pattern);
388 assert_debug_snapshot!(redaction.redact_reader(File::open(file_path).unwrap()));
389 }
390
391 #[test]
392 #[cfg(feature = "redact-info")]
393 fn can_redact_reader_with_info() {
394 let file_path = env::temp_dir().join("foo.txt");
395
396 let mut f = File::create(&file_path).unwrap();
397 #[allow(clippy::unused_io_amount)]
398 f.write(TEXT.as_bytes()).unwrap();
399
400 let pattern = Pattern {
401 test: Regex::new("(bar)").unwrap(),
402 group: 1,
403 };
404
405 let redaction = Redaction::new().add_pattern(pattern);
406 assert_debug_snapshot!(redaction.redact_reader_with_info(File::open(file_path).unwrap()));
407 }
408
409 #[test]
410 fn can_redact_with_multiple_patterns() {
411 let patterns = vec![
412 Pattern {
413 test: Regex::new("(bar)").unwrap(),
414 group: 1,
415 },
416 Pattern {
417 test: Regex::new("(foo),(bar),(baz)").unwrap(),
418 group: 3,
419 },
420 ];
421
422 let redaction = Redaction::new().add_patterns(patterns);
423 assert_debug_snapshot!(redaction.redact_str(TEXT));
424 }
425
426 #[test]
427 fn can_redact_with_placeholder_text() {
428 let pattern = Pattern {
429 test: Regex::new("(bar)").unwrap(),
430 group: 1,
431 };
432 let redaction = Redaction::custom("[HIDDEN_TEXT]").add_pattern(pattern);
433 assert_debug_snapshot!(redaction.redact_str(TEXT));
434 }
435
436 #[test]
437 #[cfg(feature = "redact-json")]
438 fn can_redact_json() {
439 let pattern = Pattern {
440 test: Regex::new("(redact-by-pattern)").unwrap(),
441 group: 1,
442 };
443
444 let json = json!({
445 "all-path": {
446 "b": {
447 "key": "redact_me",
448 },
449 "foo": "redact_me",
450 "key": "redact_me",
451 },
452 "specific-key": {
453 "b": {
454 "key": "skip-redaction",
455 },
456 "foo": "skip-redaction",
457 "key": "redact_me"
458 },
459 "key": "redact_me",
460 "skip": "skip-redaction",
461 "by-value": "bar",
462 "by-pattern": "redact-by-pattern",
463 })
464 .to_string();
465
466 let redaction = Redaction::default()
467 .add_pattern(pattern)
468 .add_paths(vec!["all-path.*", "specific-key.key"])
469 .add_keys(vec!["key"])
470 .add_value("bar")
471 .unwrap();
472 assert_debug_snapshot!(redaction.redact_json(&json));
473 }
474
475 #[test]
476 #[cfg(feature = "redact-json")]
477 fn can_redact_json_value() {
478 let pattern = Pattern {
479 test: Regex::new("(redact-by-pattern)").unwrap(),
480 group: 1,
481 };
482
483 let json = json!({
484 "all-path": {
485 "b": {
486 "key": "redact_me",
487 },
488 "foo": "redact_me",
489 "key": "redact_me",
490 },
491 "specific-key": {
492 "b": {
493 "key": "skip-redaction",
494 },
495 "foo": "skip-redaction",
496 "key": "redact_me"
497 },
498 "key": "redact_me",
499 "skip": "skip-redaction",
500 "by-value": "bar",
501 "by-pattern": "redact-by-pattern",
502 });
503
504 let redaction = Redaction::default()
505 .add_pattern(pattern)
506 .add_paths(vec!["all-path.*", "specific-key.key"])
507 .add_keys(vec!["key"])
508 .add_value("bar")
509 .unwrap();
510 assert_debug_snapshot!(redaction.redact_json_value(&json));
511 }
512}