qubit_redact/policy/masking/masking_policy.rs
1// =============================================================================
2// Copyright (c) 2025 - 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Four-level immutable masking configuration.
9// qubit-style: allow multiple-public-types
10
11use std::borrow::Cow;
12
13use super::MaskPolicy;
14use crate::policy::PolicyError;
15use crate::policy::PolicyLocation;
16use crate::policy::Sensitivity;
17
18/// Mutable construction state for a [`MaskingPolicy`].
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct MaskingPolicyBuilder {
21 /// Draft mask for low-sensitivity values.
22 low: MaskPolicy,
23 /// Draft mask for medium-sensitivity values.
24 medium: MaskPolicy,
25 /// Draft mask for high-sensitivity values.
26 high: MaskPolicy,
27 /// Draft mask for secret values.
28 secret: MaskPolicy,
29}
30
31/// Mask policies assigned to all supported sensitivity levels.
32///
33/// # Examples
34///
35/// ```
36/// use qubit_redact::MaskingPolicy;
37/// use qubit_redact::Sensitivity;
38///
39/// let masking = MaskingPolicy::builder().build();
40/// assert_eq!(masking.mask(Sensitivity::Secret, "raw"), "<redacted>");
41/// ```
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct MaskingPolicy {
44 /// Policy for low-sensitivity values.
45 low: MaskPolicy,
46 /// Policy for medium-sensitivity values.
47 medium: MaskPolicy,
48 /// Policy for high-sensitivity values.
49 high: MaskPolicy,
50 /// Policy for secret values.
51 secret: MaskPolicy,
52}
53
54impl MaskingPolicy {
55 /// Creates a builder initialized with the standard masking policies.
56 #[must_use]
57 #[inline]
58 pub fn builder() -> MaskingPolicyBuilder {
59 MaskingPolicyBuilder::default()
60 }
61
62 /// Creates a builder by copying an existing masking configuration.
63 #[must_use]
64 #[inline]
65 pub(crate) fn builder_from(base: &Self) -> MaskingPolicyBuilder {
66 MaskingPolicyBuilder {
67 low: base.low.clone(),
68 medium: base.medium.clone(),
69 high: base.high.clone(),
70 secret: base.secret.clone(),
71 }
72 }
73
74 /// Masks `value` with the policy configured for `level`.
75 ///
76 /// Empty values remain empty; otherwise the selected policy determines
77 /// whether the result borrows or owns its contents.
78 ///
79 /// # Type Parameters
80 ///
81 /// * `'a` - Lifetime of the input and any borrowed result.
82 ///
83 /// # Parameters
84 ///
85 /// * `level` - Sensitivity level selecting the mask policy.
86 /// * `value` - Value to mask.
87 ///
88 /// # Returns
89 ///
90 /// The borrowed empty input or an owned masked value.
91 #[must_use]
92 #[inline(always)]
93 pub fn mask<'a>(&self, level: Sensitivity, value: &'a str) -> Cow<'a, str> {
94 self.for_level(level).mask(value)
95 }
96
97 /// Returns the configured complete replacement for an opaque value.
98 ///
99 /// This never reads the original value. Edge-preserving policies return
100 /// only their replacement text because no prefix or suffix is safe to
101 /// retain when the value is opaque.
102 ///
103 /// # Parameters
104 ///
105 /// * `level` - Sensitivity level selecting the mask policy.
106 ///
107 /// # Returns
108 ///
109 /// The complete replacement configured for `level`.
110 #[must_use]
111 #[inline(always)]
112 pub fn mask_opaque(&self, level: Sensitivity) -> &str {
113 self.for_level(level).opaque_mask()
114 }
115
116 /// Masks a value without allocating beyond a byte limit.
117 ///
118 /// # Type Parameters
119 ///
120 /// * `'a` - Lifetime of the input and any borrowed result.
121 ///
122 /// # Parameters
123 ///
124 /// * `level` - Sensitivity level selecting the mask policy.
125 /// * `value` - Value to mask.
126 /// * `max_bytes` - Maximum bytes allocated for the masked result.
127 ///
128 /// # Returns
129 ///
130 /// The borrowed empty input or an owned mask bounded by `max_bytes`.
131 #[must_use]
132 #[inline(always)]
133 #[cfg(feature = "http")]
134 pub(crate) fn mask_bounded<'a>(&self, level: Sensitivity, value: &'a str, max_bytes: usize) -> Cow<'a, str> {
135 self.for_level(level).mask_bounded(value, max_bytes)
136 }
137
138 /// Masks a value and reports byte-limit truncation.
139 #[inline(always)]
140 pub(crate) fn mask_bounded_with_truncation<'a>(
141 &self,
142 level: Sensitivity,
143 value: &'a str,
144 max_bytes: usize,
145 ) -> (Cow<'a, str>, bool) {
146 self.for_level(level).mask_bounded_with_truncation(value, max_bytes)
147 }
148
149 /// Returns an opaque replacement constrained to `max_bytes`.
150 ///
151 /// # Parameters
152 ///
153 /// * `level` - Sensitivity level selecting the mask policy.
154 /// * `max_bytes` - Maximum bytes retained from the replacement.
155 ///
156 /// # Returns
157 ///
158 /// An owned bounded prefix of the configured opaque replacement.
159 #[must_use]
160 #[inline(always)]
161 pub(crate) fn mask_opaque_bounded(&self, level: Sensitivity, max_bytes: usize) -> String {
162 self.for_level(level).opaque_mask_bounded(max_bytes)
163 }
164
165 /// Returns the mask policy configured for `level`.
166 ///
167 /// # Parameters
168 ///
169 /// * `level` - Sensitivity level to resolve.
170 ///
171 /// # Returns
172 ///
173 /// The mask policy assigned to `level`.
174 #[must_use]
175 #[inline(always)]
176 pub const fn for_level(&self, level: Sensitivity) -> &MaskPolicy {
177 match level {
178 Sensitivity::Low => &self.low,
179 Sensitivity::Medium => &self.medium,
180 Sensitivity::High => &self.high,
181 Sensitivity::Secret => &self.secret,
182 }
183 }
184
185 /// Validates fixed replacements for one policy construction location.
186 pub(crate) fn validate(&self, location: PolicyLocation) -> Result<(), PolicyError> {
187 for level in [
188 Sensitivity::Low,
189 Sensitivity::Medium,
190 Sensitivity::High,
191 Sensitivity::Secret,
192 ] {
193 if matches!(
194 self.for_level(level),
195 MaskPolicy::Fixed { replacement } if replacement.is_empty()
196 ) {
197 return Err(PolicyError::EmptyFixedReplacement { location, level });
198 }
199 }
200 Ok(())
201 }
202}
203
204impl MaskingPolicyBuilder {
205 /// Sets the policy for low-sensitivity values.
206 #[inline]
207 pub fn low(&mut self, policy: MaskPolicy) -> &mut Self {
208 self.low = policy;
209 self
210 }
211
212 /// Sets the policy for medium-sensitivity values.
213 #[inline]
214 pub fn medium(&mut self, policy: MaskPolicy) -> &mut Self {
215 self.medium = policy;
216 self
217 }
218
219 /// Sets the policy for high-sensitivity values.
220 #[inline]
221 pub fn high(&mut self, policy: MaskPolicy) -> &mut Self {
222 self.high = policy;
223 self
224 }
225
226 /// Sets the policy for secret values.
227 #[inline]
228 pub fn secret(&mut self, policy: MaskPolicy) -> &mut Self {
229 self.secret = policy;
230 self
231 }
232
233 /// Replaces one sensitivity policy while rebuilding an existing policy.
234 #[inline]
235 pub(crate) fn policy(&mut self, level: Sensitivity, policy: MaskPolicy) {
236 match level {
237 Sensitivity::Low => self.low(policy),
238 Sensitivity::Medium => self.medium(policy),
239 Sensitivity::High => self.high(policy),
240 Sensitivity::Secret => self.secret(policy),
241 };
242 }
243
244 /// Builds the immutable masking configuration.
245 #[must_use]
246 #[inline]
247 pub fn build(self) -> MaskingPolicy {
248 MaskingPolicy {
249 low: self.low,
250 medium: self.medium,
251 high: self.high,
252 secret: self.secret,
253 }
254 }
255}
256
257impl Default for MaskingPolicyBuilder {
258 /// Creates a builder with the standard masking policies.
259 fn default() -> Self {
260 Self {
261 low: MaskPolicy::preserve_edges(2, 2, "****", 4),
262 medium: MaskPolicy::preserve_suffix(1, "*******", 1),
263 high: MaskPolicy::fixed("****"),
264 secret: MaskPolicy::fixed("<redacted>"),
265 }
266 }
267}
268
269impl Default for MaskingPolicy {
270 /// Creates the built-in conservative four-level masking configuration.
271 ///
272 /// # Returns
273 ///
274 /// The built-in masking configuration.
275 fn default() -> Self {
276 Self::builder().build()
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::MaskingPolicy;
283 use crate::MaskPolicy;
284 use crate::Sensitivity;
285
286 #[test]
287 fn builder_low_and_medium_replace_their_respective_policies() {
288 let mut builder = MaskingPolicy::builder();
289 builder.low(MaskPolicy::fixed("low"));
290 builder.medium(MaskPolicy::fixed("medium"));
291 let policy = builder.build();
292
293 assert_eq!(policy.for_level(Sensitivity::Low).mask("value"), "low");
294 assert_eq!(policy.for_level(Sensitivity::Medium).mask("value"), "medium");
295 }
296}