redact_engine/
redaction.rs

1//! redaction function user interface
2use 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
16/// Define redact settings
17pub struct Redaction {
18    /// Define an option to redact text in JSON schema. enable by `redact-json`
19    /// feature flag enabled.
20    #[cfg(feature = "redact-json")]
21    json: json::Redact,
22
23    /// Define the default redact option by patterns logic.
24    pattern: pattern::Redact,
25}
26
27impl Default for Redaction {
28    /// Create a [`Redaction`] Methods
29    ///
30    /// # Example
31    ///
32    /// ```rust
33    /// use redact_engine::Redaction;
34    /// Redaction::default()
35    /// # ;
36    /// ```
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl Redaction {
43    #[must_use]
44    /// Create a [`Redaction`] Methods
45    ///
46    /// # Example
47    ///
48    /// ```rust
49    /// use redact_engine::Redaction;
50    /// Redaction::custom("CUSTOM_HIDDEN_TEXT")
51    /// # ;
52    /// ```
53    pub fn new() -> Self {
54        Self::custom(REDACT_PLACEHOLDER)
55    }
56
57    #[must_use]
58    /// Create a [`Redaction`] with redact placeholder text.
59    ///
60    /// # Arguments
61    /// * `redact_placeholder` - placeholder redaction
62    ///
63    /// # Example
64    ///
65    /// ```rust
66    /// use redact_engine::Redaction;
67    /// Redaction::custom("[HIDDEN_VALUE]")
68    /// # ;
69    /// ```
70    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    /// redact exact string match
80    ///
81    /// # Arguments
82    /// * `value` - The redaction value
83    ///
84    /// # Example
85    ///
86    /// ```rust
87    /// use redact_engine::Redaction;
88    /// let text = "foo,bar";
89    /// Redaction::new().add_value("foo");
90    /// # ;
91    /// ```
92    /// # Errors
93    /// when the value could not converted to a regex
94    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    /// redact exact string match from list of strings
104    ///
105    /// # Arguments
106    /// * `values` - List of redaction value
107    ///
108    /// # Example
109    ///
110    /// ```rust
111    /// use redact_engine::Redaction;
112    /// let text = "foo,bar,baz";
113    /// Redaction::new().add_values(vec!["foo", "baz"]);
114    /// # ;
115    /// ```
116    /// # Errors
117    /// when the value could not converted to a regex
118    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    /// Add a [`Pattern`] to the redaction list
141    ///
142    /// # Arguments
143    /// * `pattern` - redact [Pattern]
144    ///
145    /// # Example
146    ///
147    /// ```rust
148    /// use redact_engine::{Redaction, Pattern};
149    /// use regex::Regex;
150    /// let text = "foo,bar";
151    /// let pattern = Pattern {
152    ///    test: Regex::new("(bar)").unwrap(),
153    ///    group: 1,
154    /// };
155    ///
156    /// Redaction::new().add_pattern(pattern);
157    /// # ;
158    /// ```
159    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    /// Add list if [`Pattern`] to the redaction list
166    ///
167    /// # Arguments
168    /// * `patterns` - List of redact [Pattern]
169    ///
170    /// # Example
171    ///
172    /// ```rust
173    /// use redact_engine::{Redaction, Pattern};
174    /// use regex::Regex;
175    /// let text = "foo,bar";
176    /// let pattern = Pattern {
177    ///    test: Regex::new("(bar)").unwrap(),
178    ///    group: 1,
179    /// };
180    ///
181    /// Redaction::new().add_patterns(vec![pattern]);
182    /// # ;
183    /// ```
184    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    /// Redact the JSON value of the given keys. enable by `redact-json`
192    ///
193    /// # Optional
194    /// When `redact-json` feature flag is enabled
195    ///
196    /// # Arguments
197    /// * `key` -  The JSON key
198    ///
199    /// # Example
200    ///
201    /// ```rust
202    /// use redact_engine::Redaction;
203    /// Redaction::new().add_keys(vec!["bar", "array"]);
204    /// # ;
205    /// ```
206    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    /// Redact the JSON by JSON paths. enable by `redact-json`.
214    ///
215    /// # Optional
216    /// When `redact-json` feature flag is enabled
217    ///
218    /// # Example
219    ///
220    /// ```rust
221    /// use redact_engine::Redaction;
222    /// Redaction::new().add_paths(vec!["bar", "array.*"]);
223    /// # ;
224    /// ```
225    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    /// Redact from string
232    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    /// Redact from string with extra information of the matches
239    ///
240    /// # Optional
241    /// When `redact-info` feature flag is enabled
242    pub fn redact_str_with_info(&self, str: &str) -> Info {
243        self.pattern.redact_patterns(str, true)
244    }
245
246    /// Redact text from reader
247    ///
248    /// # Errors
249    /// - When file not exists.
250    /// - Could not open reader.
251    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    /// Redact text from reader with extra information of the matches
262    ///
263    /// # Optional
264    /// When `redact-info` feature flag is enabled
265    ///
266    /// # Errors
267    /// - When file not exists.
268    /// - Could not open reader.
269    #[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    /// Redact from string.
282    ///
283    /// # Optional
284    /// When `redact-json` feature flag is enabled
285    ///
286    /// # Errors
287    /// return an error when the given str is not a JSON string
288    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    /// Redact from serde Value.
294    ///
295    /// # Optional
296    /// When `redact-json` feature flag is enabled
297    ///
298    /// # Errors
299    /// return an error when the given str is not a JSON string
300    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}