1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
use std::borrow::Cow;
use regex::Regex;
/// An enum representing a redaction rule.
///
/// A `Redactor` can be a simple string replacement or a more complex
/// regex-based replacement.
pub enum Redactor {
/// A simple string-for-string replacement.
/// The first `String` is the pattern to find, and the second is the
/// replacement.
Simple(String, String),
/// A regex-based replacement.
/// The `Regex` is the pattern to find, and the `String` is the
/// replacement.
Re(Regex, String),
/// A regex-based replacement that uses capture groups.
/// The `Regex` is the pattern, and the `String` is the replacement
/// which can include capture group references like `$1`, `$2`.
ReWithCapture(Regex, String),
/// A regex that finds candidates, which are then passed to a validator
/// function. Only if the validator returns true is the match redacted.
Validated(Regex, fn(&str) -> bool, String),
}
impl Redactor {
/// Creates a new `Redactor::Simple` variant.
///
/// # Arguments
///
/// * `pattern` - The string pattern to search for.
/// * `beep` - An optional replacement string. If `None`, a default replacer
/// will be used.
pub fn simple(pattern: String, beep: Option<String>) -> Self {
let replacer = beep.clone().unwrap_or(String::from("•••"));
Redactor::Simple(pattern, replacer)
}
/// Creates a new `Redactor::Re` variant.
///
/// # Arguments
///
/// * `pattern` - The regex pattern to search for.
/// * `beep` - An optional replacement string. If `None`, a default replacer
/// will be used.
pub fn regex(pattern: Regex, beep: Option<String>) -> Self {
let replacer = beep.clone().unwrap_or(String::from("•••"));
Redactor::Re(pattern, replacer)
}
/// Creates a new `Redactor::ReWithCapture` variant.
///
/// # Arguments
///
/// * `pattern` - The regex pattern to search for.
/// * `replacer` - The replacement string with capture groups.
pub fn regex_with_capture(pattern: Regex, replacer: String) -> Self {
Redactor::ReWithCapture(pattern, replacer)
}
/// Creates a new `Redactor::Validated` variant.
///
/// # Arguments
///
/// * `pattern` - The regex pattern to search for.
/// * `validator` - A function to validate the redacted text.
/// * `beep` - An optional replacement string. If `None`, a default replacer
/// will be used.
pub fn validated(
pattern: Regex,
validator: fn(&str) -> bool,
beep: Option<String>,
) -> Self {
let replacer = beep.clone().unwrap_or(String::from("•••"));
Redactor::Validated(pattern, validator, replacer)
}
/// Applies the redactor to a given text.
///
/// # Arguments
///
/// * `text` - The text to be redacted.
///
/// # Returns
///
/// A new `String` with the redactions applied.
pub fn redact<'a>(&self, text: &'a str) -> Cow<'a, str> {
match self {
Redactor::Simple(pattern, replacer) => {
if text.contains(pattern) {
Cow::Owned(text.replace(pattern, replacer))
} else {
Cow::Borrowed(text)
}
}
Redactor::Re(pattern, replacer)
| Redactor::ReWithCapture(pattern, replacer) => {
pattern.replace_all(text, replacer.as_str())
}
Redactor::Validated(pattern, validator, replacer) => {
let mut owned: Option<String> = None;
let mut last_end = 0;
for m in pattern.find_iter(text) {
if validator(m.as_str()) {
// First time we find a valid match, we must allocate.
if owned.is_none() {
owned = Some(String::with_capacity(text.len()));
}
let owned_str = owned.as_mut().unwrap();
// Append the text from the end of the last match to the
// start of this one.
owned_str.push_str(&text[last_end..m.start()]);
// Append the replacement string.
owned_str.push_str(replacer);
// Update our position.
last_end = m.end();
}
}
// If `owned` is Some, it means we performed at least one
// redaction. We finish by appending the
// remainder of the original string.
match owned {
Some(mut s) => {
s.push_str(&text[last_end..]);
Cow::Owned(s)
}
// If `owned` is None, no valid matches were found, so we
// can return the original string slice
// without any allocation.
None => Cow::Borrowed(text),
}
}
}
}
}