Skip to main content

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}