1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
// =============================================================================
// Copyright (c) 2025 - 2026 Haixing Hu.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0.
// =============================================================================
//! Stateless redaction operations backed by an immutable policy.
use std::borrow::Cow;
use crate::{
FieldClassification,
FieldRedaction,
PassThroughReason,
RedactMapValueMut,
RedactedKeyedValue,
RedactedText,
RedactionPolicy,
RedactionSession,
Sensitivity,
policy::{
OutputCharge,
ResolvedField,
},
};
/// Applies one immutable policy to scalar values and string maps.
#[must_use]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Redactor {
/// Field classification and masking configuration.
policy: RedactionPolicy,
}
impl Redactor {
/// Creates a redactor using `policy`.
///
/// # Parameters
///
/// * `policy` - Immutable field classification and masking configuration.
///
/// # Returns
///
/// A redactor that owns the supplied policy snapshot.
#[inline(always)]
pub const fn new(policy: RedactionPolicy) -> Self {
Self { policy }
}
/// Creates a redactor with the strict policy for untrusted scalar data.
///
/// Unknown fields are masked at [`Sensitivity::Secret`].
#[inline]
pub fn strict() -> Self {
Self::new(RedactionPolicy::strict())
}
/// Returns the immutable policy used by this redactor.
///
/// # Returns
///
/// A borrowed view of the redactor's policy snapshot.
#[must_use = "use the policy snapshot backing this redactor"]
#[inline(always)]
pub const fn policy(&self) -> &RedactionPolicy {
&self.policy
}
/// Redacts one value according to its field name.
///
/// Unknown and explicitly allowed fields retain a borrow of `value`.
/// Sensitive fields return the value produced by the configured mask.
/// This method classifies only `field`; it never scans `value` for secret
/// syntax. Do not pass an arbitrary error message or complete diagnostic
/// under a generic field name and expect embedded credentials to be found.
/// Use structured fields, [`Self::redact_at`] for an opaque value whose
/// sensitivity is already known, or a fixed safe public summary with the
/// original error retained only as an error source.
///
/// # Type Parameters
///
/// * `'a` - Lifetime of the input and any borrowed redacted result.
///
/// # Parameters
///
/// * `field` - Raw field name to classify.
/// * `value` - Field value to redact when classified as sensitive.
///
/// # Returns
///
/// A typed result that distinguishes masked values from pass-through
/// values while borrowing safe input where possible.
#[must_use = "use the returned redacted value"]
#[inline]
pub fn redact_field<'a>(
&self,
field: &str,
value: &'a str,
) -> FieldRedaction<'a> {
let session = RedactionSession::operation(&self.policy);
self.redact_field_with_session(&session, field, value)
}
/// Redacts one field while consuming the supplied operation session.
///
/// This is the composition entry point for diagnostics that contain more
/// than one field-producing adapter. Input accounting happens before the
/// value is inspected, and generated masks are charged to the same
/// session.
#[must_use = "use the returned redacted value"]
pub fn redact_field_with_session<'a>(
&self,
session: &RedactionSession<'_>,
field: &str,
value: &'a str,
) -> FieldRedaction<'a> {
if !session.consume_input(field.len().saturating_add(value.len())) {
return self.fallback_field(session);
}
let resolved = self.policy.resolve_field(field);
match resolved {
ResolvedField::Sensitive { sensitivity } => {
let max_bytes = session.remaining_output_bytes();
let masked = self.policy.masking().mask_bounded(
sensitivity,
value,
max_bytes,
);
let mask_len = masked.len();
let fallback = self.opaque_mask();
match session
.charge_output_or_fallback(mask_len, fallback.len())
{
OutputCharge::Complete => FieldRedaction::Masked {
value: RedactedText::new(masked),
sensitivity,
},
OutputCharge::Fallback => FieldRedaction::Masked {
value: RedactedText::new(Cow::Owned(
fallback.to_owned(),
)),
sensitivity: Sensitivity::Secret,
},
OutputCharge::Exhausted => FieldRedaction::Masked {
value: RedactedText::new(Cow::Owned(String::new())),
sensitivity: Sensitivity::Secret,
},
}
}
ResolvedField::PassThrough => {
let reason = match self.policy.classify_field(field) {
FieldClassification::Allowed { .. } => {
PassThroughReason::Allowed
}
FieldClassification::Sensitive { .. }
| FieldClassification::Unknown => {
PassThroughReason::Unknown
}
};
match session.charge_output_or_fallback(
value.len(),
self.opaque_mask().len(),
) {
OutputCharge::Complete => {
FieldRedaction::PassedThrough { value, reason }
}
OutputCharge::Fallback => FieldRedaction::Masked {
value: RedactedText::new(Cow::Owned(
self.opaque_mask().to_owned(),
)),
sensitivity: Sensitivity::Secret,
},
OutputCharge::Exhausted => FieldRedaction::Masked {
value: RedactedText::new(Cow::Owned(String::new())),
sensitivity: Sensitivity::Secret,
},
}
}
}
}
/// Redacts one value at an explicit sensitivity level.
///
/// This ignores field classification and allow rules. Use it at a boundary
/// where the value is known to be sensitive regardless of its field name.
///
/// # Type Parameters
///
/// * `'a` - Lifetime of the input and any borrowed redacted result.
///
/// # Parameters
///
/// * `level` - Sensitivity required by the calling boundary.
/// * `value` - Value to mask.
///
/// # Returns
///
/// Typed redacted text produced by the configured mask for `level`.
#[must_use = "use the returned redacted value"]
#[inline]
pub fn redact_at<'a>(
&self,
level: Sensitivity,
value: &'a str,
) -> RedactedText<'a> {
let session = RedactionSession::operation(&self.policy);
self.redact_at_with_session(&session, level, value)
}
/// Redacts an explicitly sensitive value through an existing session.
#[must_use = "use the returned redacted value"]
pub fn redact_at_with_session<'a>(
&self,
session: &RedactionSession<'_>,
level: Sensitivity,
value: &'a str,
) -> RedactedText<'a> {
if !session.consume_input(value.len()) {
return self.fallback_text(session);
}
let masked = self.policy.masking().mask_bounded(
level,
value,
session.remaining_output_bytes(),
);
let length = masked.len();
let fallback = self.opaque_mask();
match session.charge_output_or_fallback(length, fallback.len()) {
OutputCharge::Complete => RedactedText::new(masked),
OutputCharge::Fallback => {
RedactedText::new(Cow::Owned(fallback.to_owned()))
}
OutputCharge::Exhausted => {
RedactedText::new(Cow::Owned(String::new()))
}
}
}
/// Returns the policy's opaque Secret mask.
#[inline(always)]
fn opaque_mask(&self) -> &str {
self.policy.masking().mask_opaque(Sensitivity::Secret)
}
/// Charges one fail-closed scalar fallback through the shared session.
fn fallback_text<'a>(
&self,
session: &RedactionSession<'_>,
) -> RedactedText<'a> {
let fallback = self.opaque_mask();
match session.charge_output_or_fallback(fallback.len(), fallback.len())
{
OutputCharge::Complete => {
RedactedText::new(Cow::Owned(fallback.to_owned()))
}
OutputCharge::Fallback | OutputCharge::Exhausted => {
RedactedText::new(Cow::Owned(String::new()))
}
}
}
/// Wraps a charged fail-closed scalar fallback as a field result.
fn fallback_field<'a>(
&self,
session: &RedactionSession<'_>,
) -> FieldRedaction<'a> {
FieldRedaction::Masked {
value: self.fallback_text(session),
sensitivity: Sensitivity::Secret,
}
}
/// Creates a lazy redacted view selected by an external key.
///
/// The returned view borrows this redactor's policy snapshot. When its key
/// is sensitive, it masks the complete value through
/// [`RedactValue`](crate::RedactValue). Otherwise it delegates to the
/// value's recursive redaction contracts.
///
/// # Type Parameters
///
/// * `'value` - Lifetime of the borrowed key and value.
/// * `T` - Value type rendered or serialized through redaction.
///
/// # Parameters
///
/// * `key` - Field name used only for policy classification.
/// * `value` - Value to render or serialize through the selected policy.
///
/// # Returns
///
/// A lazy keyed redaction view borrowing `key` and `value`.
#[must_use = "format or serialize the returned keyed redaction view"]
#[inline(always)]
pub fn redact_keyed<'value, T: ?Sized>(
&self,
key: &'value str,
value: &'value T,
) -> RedactedKeyedValue<'value, '_, T> {
RedactedKeyedValue::new(key, value, &self.policy)
}
/// Creates a redacted copy of a text-keyed, mutable text-valued map.
///
/// The source map is never modified. Its concrete collection type is
/// preserved by cloning the collection before applying in-place redaction.
///
/// # Type Parameters
///
/// * `M` - Cloneable map-like collection returned after redaction.
/// * `K` - Runtime key type used for field classification.
/// * `V` - Mutable map-value type redacted in the cloned collection.
///
/// # Parameters
///
/// * `map` - Map whose values are classified by their corresponding keys.
///
/// # Returns
///
/// A map of the same type containing redacted values.
#[must_use = "use the returned redacted map"]
pub fn redact_map<M, K: ?Sized, V: ?Sized>(&self, map: &M) -> M
where
M: Clone + RedactMapValueMut<K, V>,
{
let mut redacted = map.clone();
RedactMapValueMut::redact_map_in_place(&mut redacted, &self.policy);
redacted
}
/// Redacts sensitive values of a text-keyed map in place.
///
/// # Type Parameters
///
/// * `M` - Mutable map-like collection type.
/// * `K` - Runtime key type used for field classification.
/// * `V` - Mutable map-value type redacted in place.
///
/// # Parameters
///
/// * `map` - Mutable map whose values are classified by their keys.
#[inline(always)]
pub fn redact_map_in_place<M, K: ?Sized, V: ?Sized>(&self, map: &mut M)
where
M: RedactMapValueMut<K, V> + ?Sized,
{
RedactMapValueMut::redact_map_in_place(map, &self.policy);
}
}
impl Default for Redactor {
/// Creates a redactor from the current global redaction configuration.
///
/// # Returns
///
/// A redactor that is unaffected by later policy configuration attempts.
#[inline(always)]
fn default() -> Self {
Self::new(RedactionPolicy::default())
}
}