mask_text/
mask.rs

1//! Mask implementation
2//!
3//!
4//! # Example:
5//! ```
6#![doc = include_str!("../examples/percentage.rs")]
7//! ```
8use regex::Regex;
9
10#[derive(Debug)]
11pub enum Kind {
12    Percentage(String, u8, usize, String),
13    Regex(String, Regex, usize, String),
14    Prefix(String, usize, String),
15    All(String, String),
16}
17
18impl Kind {
19    #[must_use]
20    pub fn mask(self) -> String {
21        match self {
22            Self::Percentage(mask_str, percentage, min_chars, mask_char) => {
23                with_percentage(mask_str, percentage, min_chars, mask_char)
24            }
25            Self::Regex(mask_str, re, group, mask_char) => {
26                with_regex(mask_str, re, group, mask_char)
27            }
28            Self::Prefix(mask_str, until, mask_char) => with_prefix(mask_str, until, mask_char),
29            Self::All(mask_str, mask_char) => all(mask_str, mask_char),
30        }
31    }
32}
33
34/// Mask percentage text.
35///
36/// # Example
37///
38/// ```rust
39/// let masked_text = mask_text::Kind::Percentage("text to mask".to_string(), 80, 3, "*".to_string()).mask();
40/// ```
41///
42/// Arguments:
43/// * `text` - Text to mask.
44/// * `percentage` - Percentage number. 100% masking all the text.
45/// * `min_chars` - The minimum number of the text to apply percentage logic. if
46///   the text length is lower from the given text all the text will mask.
47/// * `mask_char` - Mask char.
48#[must_use]
49fn with_percentage(text: String, percentage: u8, min_chars: usize, mask_char: String) -> String {
50    #[allow(clippy::cast_sign_loss)]
51    #[allow(clippy::cast_possible_truncation)]
52    #[allow(clippy::cast_precision_loss)]
53    let mask_from = if text.len() > min_chars {
54        text.len() - ((f32::from(percentage) * text.len() as f32) / 100.0).floor() as usize
55    } else {
56        // mask all text
57        0
58    };
59    mask(text, mask_from, mask_char)
60}
61
62/// Mask string by regex. if the mask group is not found, we go to the safe side
63/// and mask all the text
64///
65/// # Example
66///
67/// ```rust
68/// use regex::Regex;
69/// let re = Regex::new("([a-z].*) (mask) ([a-z].*)").unwrap();
70/// let masked_text = mask_text::Kind::Regex("text to mask on group".to_string(), re, 2, "*".to_string()).mask();
71/// ```
72///
73/// Arguments:
74/// * `text` - Text to mask.
75/// * `re` - Regex to capture.
76/// * `group` - Mask regex group.
77/// * `mask_char` - Mask char.
78#[must_use]
79fn with_regex(text: String, re: Regex, group: usize, mask_char: String) -> String {
80    let cap_text = re
81        .captures(&text)
82        .and_then(|f| f.get(group))
83        .map_or(text.as_str(), |m| m.as_str());
84
85    text.replace(cap_text, &mask(cap_text.to_string(), 0, mask_char))
86}
87
88/// Mask string from prefix.
89///
90/// # Example
91///
92/// ```rust
93/// let masked_text = mask_text::Kind::Prefix("text to mask".to_string(), 3, "*".to_string()).mask();
94/// ```
95///
96/// Arguments:
97/// * `text` - Text to mask.
98/// * `until` - Mask chats from the start until the given number.
99/// * `mask_char` - Mask char.
100#[must_use]
101fn with_prefix(text: String, until: usize, mask_char: String) -> String {
102    let until = if until >= text.len() { 0 } else { until };
103    mask(text, until, mask_char)
104}
105
106/// Mask all string
107///
108/// # Example
109///
110/// ```rust
111/// let masked_text = mask_text::Kind::All("text to mask".to_string(), "*".to_string()).mask();
112/// ```
113///
114/// Arguments:
115/// * `text` - Text to mask.
116/// * `mask_char` - Mask char.
117#[must_use]
118fn all(text: String, mask_char: String) -> String {
119    mask(text, 0, mask_char)
120}
121
122fn mask(str: String, from: usize, mask_char: String) -> String {
123    str.chars()
124        .enumerate()
125        .map(|(i, c)| {
126            if c as u8 == 0x0d {
127                "\r".to_string()
128            } else if c as u8 == 0x0a {
129                "\n".to_string()
130            } else if i >= from {
131                mask_char.to_string()
132            } else {
133                c.to_string()
134            }
135        })
136        .collect::<String>()
137}
138
139#[cfg(test)]
140mod test_mask {
141
142    use insta::assert_debug_snapshot;
143    use rstest::rstest;
144
145    use super::*;
146
147    macro_rules! set_snapshot_suffix {
148        ($($expr:expr),*) => {
149            let mut settings = insta::Settings::clone_current();
150            settings.set_prepend_module_to_snapshot(false);
151            settings.set_snapshot_suffix(format!($($expr,)*));
152            let _guard = settings.bind_to_scope();
153        }
154    }
155
156    #[rstest]
157    #[case(20, 3, "*")]
158    #[case(80, 3, "*")]
159    #[case(100, 3, "*")]
160    #[case(80, 20, "*")]
161    #[case(80, 3, ".")]
162    fn cat_mask_with_percentage(
163        #[case] percentage: u8,
164        #[case] min_chars: usize,
165        #[case] mask_char: String,
166    ) {
167        let text = "text to mask".to_string();
168
169        set_snapshot_suffix!(
170            "[{}]-[{}]-[{}]",
171            percentage,
172            min_chars,
173            mask_char.replace('*', "asterisk")
174        );
175
176        assert_debug_snapshot!(Kind::Percentage(text, percentage, min_chars, mask_char,).mask());
177    }
178
179    #[rstest]
180    #[case("([a-z].*) (mask) ([a-z].*)", 2, "*")]
181    #[case("([a-z].*) (mask) ([a-z].*)", 3, "*")]
182    #[case("([a-z].*) (mask) ([a-z].*)", 1, ".")]
183    fn cat_mask_with_regex(#[case] re: &str, #[case] group: usize, #[case] mask_char: String) {
184        let text = "text to mask on group".to_string();
185
186        set_snapshot_suffix!(
187            "[{}]-[{}]-[{}]",
188            re.replace('*', "asterisk"),
189            group,
190            mask_char.replace('*', "asterisk")
191        );
192
193        let re = Regex::new(re).unwrap();
194        assert_debug_snapshot!(Kind::Regex(text, re, group, mask_char).mask());
195    }
196
197    #[rstest]
198    #[case(3, "*")]
199    #[case(200, "*")]
200    fn cat_mask_with_prefix(#[case] until: usize, #[case] mask_char: String) {
201        let text = "text to mask".to_string();
202
203        set_snapshot_suffix!("[{}]-[{}]", until, mask_char.replace('*', "asterisk"));
204
205        assert_debug_snapshot!(Kind::Prefix(text, until, mask_char).mask());
206    }
207}