Skip to main content

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}