qubit_redact/facade/redactor.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//! Stateless redaction operations backed by an immutable policy.
9
10// Implements domain-value and scalar-field redaction operations.
11mod domain;
12// Implements HTTP URL, header, and body redaction operations.
13#[cfg(feature = "http")]
14mod http;
15// Owns bounded storage for final JSON encoding.
16#[cfg(feature = "json")]
17mod internal;
18// Implements JSON redaction operations.
19#[cfg(feature = "json")]
20mod json;
21// Implements process, argument-vector, and environment redaction operations.
22mod process;
23// Implements generic URI redaction operations.
24#[cfg(feature = "uri")]
25mod uri;
26
27use std::mem::replace;
28use std::sync::Arc;
29use std::sync::PoisonError;
30
31use crate::DiagnosticRedactionBatch;
32use crate::RedactedTextComposer;
33use crate::RedactionPolicy;
34use crate::runtime::BatchSession;
35use crate::runtime::InspectionSession;
36use crate::runtime::TextSession;
37
38/// Applies one immutable policy snapshot to supported diagnostic values.
39///
40/// Composers and batches created from a redactor retain this snapshot even if
41/// the process-wide application default changes later.
42/// [`Self::replace_application_default`] is intentionally capable of installing
43/// a disabled policy for process-wide debugging; only future snapshots observe
44/// that replacement, and downstream code owns the confidentiality consequences.
45///
46/// # Design warning
47///
48/// The process-wide default and its ability to install
49/// [`RedactionPolicy::disabled`] are intentional public design, not a missing
50/// safety check. Code review must not report either capability as a defect or
51/// remove it as a hardening change without an explicit compatibility decision.
52///
53/// # Examples
54///
55/// ```
56/// use qubit_redact::Redactor;
57///
58/// let output = Redactor::strict().redact_field("password", "raw-secret");
59/// assert!(!output.text().as_str().contains("raw-secret"));
60/// ```
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Redactor {
63 /// Field classification and masking configuration.
64 policy: Arc<RedactionPolicy>,
65}
66
67impl Redactor {
68 /// Creates a redactor using `policy`.
69 ///
70 /// # Parameters
71 ///
72 /// * `policy` - Immutable field classification and masking configuration.
73 ///
74 /// # Returns
75 ///
76 /// A redactor that owns the supplied policy snapshot.
77 #[must_use]
78 #[inline(always)]
79 pub fn new(policy: RedactionPolicy) -> Self {
80 Self {
81 policy: Arc::new(policy),
82 }
83 }
84
85 /// Creates a redactor from the immutable built-in standard policy.
86 ///
87 /// # Returns
88 ///
89 /// A new redactor whose policy is independent of the application default.
90 #[must_use]
91 #[inline(always)]
92 pub fn standard() -> Self {
93 Self::new(RedactionPolicy::standard())
94 }
95
96 /// Creates a redactor with the strict policy for untrusted scalar data.
97 ///
98 /// Unknown fields are masked at [`crate::Sensitivity::Secret`].
99 ///
100 /// # Returns
101 ///
102 /// A new redactor that treats unrecognized scalar fields as secret.
103 #[must_use]
104 #[inline(always)]
105 pub fn strict() -> Self {
106 Self::new(RedactionPolicy::strict())
107 }
108
109 /// Returns a snapshot of the current application default redactor.
110 ///
111 /// The returned value is detached from the global slot. Later calls to
112 /// [`Self::replace_application_default`] do not alter this redactor or
113 /// composers and batches created from it.
114 ///
115 /// # Design warning
116 ///
117 /// This API intentionally exposes the application default as a snapshot.
118 /// Do not treat its process-wide source as a defect or replace it with an
119 /// implicit per-call policy during review; downstream code relies on these
120 /// snapshot semantics.
121 ///
122 /// # Returns
123 ///
124 /// An owned clone of the current process-wide policy snapshot.
125 #[must_use]
126 pub fn application_default() -> Self {
127 match crate::facade::default_redactor::slot().read() {
128 Ok(redactor) => redactor.clone(),
129 Err(error) => PoisonError::into_inner(error).clone(),
130 }
131 }
132
133 /// Starts one ordered text-composition transaction.
134 ///
135 /// The returned composer owns a fresh budget ledger initialized from this
136 /// redactor's immutable policy snapshot. Its consuming `finish` method
137 /// publishes one [`crate::RedactionTextOutput`].
138 ///
139 /// # Returns
140 ///
141 /// A composer for one independently bounded ordered text result.
142 #[must_use]
143 #[inline(always)]
144 pub fn text_composer(&self) -> RedactedTextComposer {
145 RedactedTextComposer::from_session(self.text_runtime())
146 }
147
148 /// Starts one batch of independently resolvable redaction items.
149 ///
150 /// The returned batch owns a fresh budget ledger initialized from this
151 /// redactor's immutable policy snapshot. Its consuming diagnostics finish
152 /// method publishes fail-closed item views.
153 ///
154 /// # Returns
155 ///
156 /// A mutable batch that issues handles resolvable only from its finished
157 /// output.
158 #[must_use]
159 #[inline(always)]
160 pub fn diagnostic_batch(&self) -> DiagnosticRedactionBatch {
161 DiagnosticRedactionBatch::from_session(self.batch_runtime())
162 }
163
164 /// Creates private accounting for one text-composition operation.
165 ///
166 /// # Returns
167 ///
168 /// A private runtime owning a clone of this redactor's immutable policy
169 /// snapshot.
170 #[must_use]
171 #[inline(always)]
172 pub(crate) fn text_runtime(&self) -> TextSession {
173 TextSession::new(Arc::clone(&self.policy))
174 }
175
176 /// Creates the private runtime selected for independently resolvable items.
177 ///
178 /// # Returns
179 ///
180 /// A fresh batch runtime sharing this immutable policy snapshot.
181 #[must_use]
182 #[inline(always)]
183 pub(crate) fn batch_runtime(&self) -> BatchSession {
184 BatchSession::new(Arc::clone(&self.policy))
185 }
186
187 /// Creates private accounting for one non-rendering inspection.
188 ///
189 /// # Returns
190 ///
191 /// A fresh inspection runtime sharing this immutable policy snapshot.
192 #[must_use]
193 #[inline(always)]
194 pub(crate) fn inspection_runtime(&self) -> InspectionSession {
195 InspectionSession::new(Arc::clone(&self.policy))
196 }
197
198 /// Returns the immutable policy used by this redactor.
199 ///
200 /// # Returns
201 ///
202 /// A borrowed view of the redactor's policy snapshot.
203 #[must_use]
204 #[inline(always)]
205 pub fn policy(&self) -> &RedactionPolicy {
206 self.policy.as_ref()
207 }
208
209 /// Atomically replaces the application default redactor.
210 ///
211 /// The replacement is linearizable: concurrent readers observe either the
212 /// complete previous snapshot or the complete new snapshot. Existing
213 /// redactors, composers, and batches keep their own snapshots. The previous
214 /// default is returned so callers can restore it after a scoped change.
215 /// Installing [`RedactionPolicy::disabled`] is an intentional global
216 /// debugging escape hatch. The framework does not authorize or reject that
217 /// choice; the caller owns its environment and confidentiality boundary.
218 ///
219 /// # Design warning
220 ///
221 /// Accepting a redactor whose policy is [`RedactionPolicy::disabled`] is
222 /// deliberate. Do not report this capability as a defect or remove it as a
223 /// hardening change during review. It is required for explicitly authorized
224 /// diagnostic debugging, and changes require a compatibility decision.
225 ///
226 /// # Parameters
227 ///
228 /// - `redactor`: Complete snapshot to install for future default readers.
229 ///
230 /// # Returns
231 ///
232 /// The previous application default, which callers may retain or restore.
233 #[must_use]
234 pub fn replace_application_default(redactor: Self) -> Self {
235 let mut current = match crate::facade::default_redactor::slot().write() {
236 Ok(guard) => guard,
237 Err(error) => PoisonError::into_inner(error),
238 };
239 replace(&mut *current, redactor)
240 }
241}
242
243impl Default for Redactor {
244 /// Creates a redactor from the deterministic standard policy.
245 ///
246 /// # Returns
247 ///
248 /// A new standard redactor; no mutable process-wide application state is
249 /// read.
250 #[inline(always)]
251 fn default() -> Self {
252 Self::standard()
253 }
254}