Skip to main content

qubit_sanitize/core/
mask_policy.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10use std::borrow::Cow;
11
12/// Strategy used to mask one sensitive field value.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum MaskPolicy {
15    /// Replaces non-empty values with a fixed replacement string.
16    Fixed {
17        /// Replacement used for non-empty values.
18        replacement: String,
19    },
20    /// Preserves a prefix and suffix for diagnosability.
21    PreserveEdges {
22        /// Number of leading Unicode scalar values to retain.
23        prefix_chars: usize,
24        /// Number of trailing Unicode scalar values to retain.
25        suffix_chars: usize,
26        /// Replacement inserted between retained edges.
27        replacement: String,
28        /// Values at or below this character length are fully masked.
29        full_mask_below_or_equal: usize,
30    },
31    /// Preserves only the final part of the value.
32    PreserveSuffix {
33        /// Number of trailing Unicode scalar values to retain.
34        suffix_chars: usize,
35        /// Replacement inserted before the retained suffix.
36        replacement: String,
37        /// Values at or below this character length are fully masked.
38        full_mask_below_or_equal: usize,
39    },
40    /// Removes non-empty values entirely.
41    Empty,
42}
43
44impl MaskPolicy {
45    /// Creates a fixed-replacement mask policy.
46    ///
47    /// # Parameters
48    ///
49    /// * `replacement` - Replacement used for non-empty values.
50    ///
51    /// # Returns
52    ///
53    /// A mask policy that replaces non-empty values with `replacement`.
54    pub fn fixed(replacement: &str) -> Self {
55        Self::Fixed {
56            replacement: replacement.to_string(),
57        }
58    }
59
60    /// Creates an edge-preserving mask policy.
61    ///
62    /// # Parameters
63    ///
64    /// * `prefix_chars` - Number of leading characters to retain.
65    /// * `suffix_chars` - Number of trailing characters to retain.
66    /// * `replacement` - Replacement inserted between retained edges.
67    /// * `full_mask_below_or_equal` - Character length threshold for full masks.
68    ///
69    /// # Returns
70    ///
71    /// A mask policy that keeps selected value edges for long values.
72    pub fn preserve_edges(
73        prefix_chars: usize,
74        suffix_chars: usize,
75        replacement: &str,
76        full_mask_below_or_equal: usize,
77    ) -> Self {
78        Self::PreserveEdges {
79            prefix_chars,
80            suffix_chars,
81            replacement: replacement.to_string(),
82            full_mask_below_or_equal,
83        }
84    }
85
86    /// Creates a suffix-preserving mask policy.
87    ///
88    /// # Parameters
89    ///
90    /// * `suffix_chars` - Number of trailing characters to retain.
91    /// * `replacement` - Replacement inserted before the suffix.
92    /// * `full_mask_below_or_equal` - Character length threshold for full masks.
93    ///
94    /// # Returns
95    ///
96    /// A mask policy that keeps only the selected trailing characters.
97    pub fn preserve_suffix(suffix_chars: usize, replacement: &str, full_mask_below_or_equal: usize) -> Self {
98        Self::PreserveSuffix {
99            suffix_chars,
100            replacement: replacement.to_string(),
101            full_mask_below_or_equal,
102        }
103    }
104
105    /// Creates a policy that removes non-empty values.
106    ///
107    /// # Returns
108    ///
109    /// A mask policy that returns an empty value for every non-empty input.
110    pub const fn empty() -> Self {
111        Self::Empty
112    }
113
114    /// Masks one value according to this policy.
115    ///
116    /// Empty values are preserved as empty because they do not leak sensitive
117    /// material and keeping them empty preserves field semantics.
118    ///
119    /// # Parameters
120    ///
121    /// * `value` - Field value to mask.
122    ///
123    /// # Returns
124    ///
125    /// Borrowed `value` when it is empty, otherwise an owned masked value.
126    pub fn mask<'a>(&self, value: &'a str) -> Cow<'a, str> {
127        if value.is_empty() {
128            return Cow::Borrowed(value);
129        }
130        match self {
131            Self::Fixed { replacement } => Cow::Owned(replacement.clone()),
132            Self::PreserveEdges {
133                prefix_chars,
134                suffix_chars,
135                replacement,
136                full_mask_below_or_equal,
137            } => mask_preserving_edges(
138                value,
139                *prefix_chars,
140                *suffix_chars,
141                replacement,
142                *full_mask_below_or_equal,
143            ),
144            Self::PreserveSuffix {
145                suffix_chars,
146                replacement,
147                full_mask_below_or_equal,
148            } => mask_preserving_suffix(value, *suffix_chars, replacement, *full_mask_below_or_equal),
149            Self::Empty => Cow::Owned(String::new()),
150        }
151    }
152}
153
154/// Masks a value while preserving a prefix and suffix.
155///
156/// # Parameters
157///
158/// * `value` - Field value to mask.
159/// * `prefix_chars` - Number of leading characters to retain.
160/// * `suffix_chars` - Number of trailing characters to retain.
161/// * `replacement` - Replacement inserted between retained edges.
162/// * `full_mask_below_or_equal` - Character length threshold for full masks.
163///
164/// # Returns
165///
166/// Owned masked value.
167fn mask_preserving_edges<'a>(
168    value: &str,
169    prefix_chars: usize,
170    suffix_chars: usize,
171    replacement: &str,
172    full_mask_below_or_equal: usize,
173) -> Cow<'a, str> {
174    let chars = value.chars().collect::<Vec<_>>();
175    if chars.len() <= full_mask_below_or_equal || chars.len() <= prefix_chars + suffix_chars {
176        return Cow::Owned(replacement.to_string());
177    }
178    let prefix = chars.iter().take(prefix_chars).collect::<String>();
179    let suffix = chars.iter().skip(chars.len() - suffix_chars).collect::<String>();
180    Cow::Owned(format!("{prefix}{replacement}{suffix}"))
181}
182
183/// Masks a value while preserving only a suffix.
184///
185/// # Parameters
186///
187/// * `value` - Field value to mask.
188/// * `suffix_chars` - Number of trailing characters to retain.
189/// * `replacement` - Replacement inserted before the retained suffix.
190/// * `full_mask_below_or_equal` - Character length threshold for full masks.
191///
192/// # Returns
193///
194/// Owned masked value.
195fn mask_preserving_suffix<'a>(
196    value: &str,
197    suffix_chars: usize,
198    replacement: &str,
199    full_mask_below_or_equal: usize,
200) -> Cow<'a, str> {
201    let chars = value.chars().collect::<Vec<_>>();
202    if chars.len() <= full_mask_below_or_equal || chars.len() <= suffix_chars {
203        return Cow::Owned(replacement.to_string());
204    }
205    let suffix = chars.iter().skip(chars.len() - suffix_chars).collect::<String>();
206    Cow::Owned(format!("{replacement}{suffix}"))
207}