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}