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