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
10use std::borrow::Cow;
11
12use super::MaskPolicy;
13use super::MaskingPolicyBuilder;
14use crate::policy::PolicyError;
15use crate::policy::PolicyLocation;
16use crate::policy::Sensitivity;
17
18/// Mask policies assigned to all supported sensitivity levels.
19///
20/// # Examples
21///
22/// ```
23/// use qubit_redact::MaskingPolicy;
24/// use qubit_redact::Sensitivity;
25///
26/// let masking = MaskingPolicy::builder().build();
27/// assert_eq!(masking.mask(Sensitivity::Secret, "raw"), "<redacted>");
28/// ```
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct MaskingPolicy {
31    /// Policy for low-sensitivity values.
32    low: MaskPolicy,
33    /// Policy for medium-sensitivity values.
34    medium: MaskPolicy,
35    /// Policy for high-sensitivity values.
36    high: MaskPolicy,
37    /// Policy for secret values.
38    secret: MaskPolicy,
39}
40
41impl MaskingPolicy {
42    /// Creates a builder initialized with the standard masking policies.
43    ///
44    /// # Returns
45    ///
46    /// A builder with the standard mask for each sensitivity level.
47    #[must_use]
48    #[inline(always)]
49    pub fn builder() -> MaskingPolicyBuilder {
50        MaskingPolicyBuilder::default()
51    }
52
53    /// Creates a builder by copying an existing masking configuration.
54    ///
55    /// # Parameters
56    ///
57    /// - `base`: Immutable mask table whose four choices are copied.
58    ///
59    /// # Returns
60    ///
61    /// An independent builder initialized from the supplied table.
62    #[must_use]
63    #[inline(always)]
64    pub(crate) fn builder_from(base: &Self) -> MaskingPolicyBuilder {
65        MaskingPolicyBuilder::from_policy(base)
66    }
67
68    /// Creates an immutable mask table from the completed builder fields.
69    ///
70    /// # Parameters
71    ///
72    /// - `low`: Mask for low sensitivity.
73    /// - `medium`: Mask for medium sensitivity.
74    /// - `high`: Mask for high sensitivity.
75    /// - `secret`: Mask for secret sensitivity.
76    ///
77    /// # Returns
78    ///
79    /// The immutable table. Enclosing policy construction validates its masks.
80    #[must_use]
81    #[inline(always)]
82    pub(super) fn from_parts(low: MaskPolicy, medium: MaskPolicy, high: MaskPolicy, secret: MaskPolicy) -> Self {
83        Self {
84            low,
85            medium,
86            high,
87            secret,
88        }
89    }
90
91    /// Returns the mask policy configured for `level`.
92    ///
93    /// # Parameters
94    ///
95    /// * `level` - Sensitivity level to resolve.
96    ///
97    /// # Returns
98    ///
99    /// The mask policy assigned to `level`.
100    #[must_use]
101    #[inline(always)]
102    pub const fn for_level(&self, level: Sensitivity) -> &MaskPolicy {
103        match level {
104            Sensitivity::Low => &self.low,
105            Sensitivity::Medium => &self.medium,
106            Sensitivity::High => &self.high,
107            Sensitivity::Secret => &self.secret,
108        }
109    }
110
111    /// Masks `value` with the policy configured for `level`.
112    ///
113    /// Empty values remain empty; otherwise the selected policy determines
114    /// whether the result borrows or owns its contents.
115    ///
116    /// # Type Parameters
117    ///
118    /// * `'a` - Lifetime of the input and any borrowed result.
119    ///
120    /// # Parameters
121    ///
122    /// * `level` - Sensitivity level selecting the mask policy.
123    /// * `value` - Value to mask.
124    ///
125    /// # Returns
126    ///
127    /// The borrowed empty input or an owned masked value.
128    #[must_use]
129    #[inline(always)]
130    pub fn mask<'a>(&self, level: Sensitivity, value: &'a str) -> Cow<'a, str> {
131        self.for_level(level).mask(value)
132    }
133
134    /// Returns the configured complete replacement for an opaque value.
135    ///
136    /// This never reads the original value. Edge-preserving policies return
137    /// only their replacement text because no prefix or suffix is safe to
138    /// retain when the value is opaque.
139    ///
140    /// # Parameters
141    ///
142    /// * `level` - Sensitivity level selecting the mask policy.
143    ///
144    /// # Returns
145    ///
146    /// The complete replacement configured for `level`.
147    #[must_use]
148    #[inline(always)]
149    pub fn mask_opaque(&self, level: Sensitivity) -> &str {
150        self.for_level(level).opaque_mask()
151    }
152
153    /// Masks a value without allocating beyond a byte limit.
154    ///
155    /// # Type Parameters
156    ///
157    /// * `'a` - Lifetime of the input and any borrowed result.
158    ///
159    /// # Parameters
160    ///
161    /// * `level` - Sensitivity level selecting the mask policy.
162    /// * `value` - Value to mask.
163    /// * `max_bytes` - Maximum bytes allocated for the masked result.
164    ///
165    /// # Returns
166    ///
167    /// The borrowed empty input or an owned mask bounded by `max_bytes`.
168    #[must_use]
169    #[inline(always)]
170    #[cfg(feature = "http")]
171    pub(crate) fn mask_bounded<'a>(&self, level: Sensitivity, value: &'a str, max_bytes: usize) -> Cow<'a, str> {
172        self.for_level(level).mask_bounded(value, max_bytes)
173    }
174
175    /// Masks a value and reports byte-limit truncation.
176    ///
177    /// # Type Parameters
178    ///
179    /// - `'a`: Borrow of the input retained when no allocation is required.
180    ///
181    /// # Parameters
182    ///
183    /// - `level`: Sensitivity selecting a mask policy.
184    /// - `value`: Source text needed by edge-preserving masks.
185    /// - `max_bytes`: Maximum allocated bytes for the resulting mask.
186    ///
187    /// # Returns
188    ///
189    /// The borrowed empty input or bounded owned mask, followed by whether
190    /// the byte allowance omitted any part of the selected mask.
191    #[must_use]
192    #[inline(always)]
193    pub(crate) fn mask_bounded_with_truncation<'a>(
194        &self,
195        level: Sensitivity,
196        value: &'a str,
197        max_bytes: usize,
198    ) -> (Cow<'a, str>, bool) {
199        self.for_level(level).mask_bounded_with_truncation(value, max_bytes)
200    }
201
202    /// Returns an opaque replacement constrained to `max_bytes`.
203    ///
204    /// # Parameters
205    ///
206    /// * `level` - Sensitivity level selecting the mask policy.
207    /// * `max_bytes` - Maximum bytes retained from the replacement.
208    ///
209    /// # Returns
210    ///
211    /// An owned bounded prefix of the configured opaque replacement.
212    #[must_use]
213    #[inline(always)]
214    pub(crate) fn mask_opaque_bounded(&self, level: Sensitivity, max_bytes: usize) -> String {
215        self.for_level(level).opaque_mask_bounded(max_bytes)
216    }
217
218    /// Validates fixed replacements for one policy construction location.
219    ///
220    /// # Errors
221    ///
222    /// Returns [`PolicyError::EmptyFixedReplacement`] when a fixed mask is
223    /// empty, reporting its sensitivity and construction location.
224    ///
225    /// # Parameters
226    ///
227    /// - `location`: Construction context attached to a rejected fixed mask.
228    ///
229    /// # Returns
230    ///
231    /// Success when every fixed replacement is nonempty.
232    pub(crate) fn validate(&self, location: PolicyLocation) -> Result<(), PolicyError> {
233        for level in [
234            Sensitivity::Low,
235            Sensitivity::Medium,
236            Sensitivity::High,
237            Sensitivity::Secret,
238        ] {
239            if matches!(
240                self.for_level(level),
241                MaskPolicy::Fixed { replacement } if replacement.is_empty()
242            ) {
243                return Err(PolicyError::EmptyFixedReplacement { location, level });
244            }
245        }
246        Ok(())
247    }
248}
249
250impl Default for MaskingPolicy {
251    /// Creates the built-in conservative four-level masking configuration.
252    ///
253    /// # Returns
254    ///
255    /// The built-in masking configuration.
256    #[inline(always)]
257    fn default() -> Self {
258        Self::builder().build()
259    }
260}