Skip to main content

qubit_redact/facade/
redaction_summary.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Machine-readable redaction summaries.
9// qubit-style: allow multiple-public-types
10
11use crate::output::RedactionCompletion;
12
13/// Reason why a safe representation is degraded.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum RedactionReason {
17    /// The admitted input prefix reached the configured input-byte limit.
18    InputLimitReached,
19    /// The shared transaction output reached its configured byte limit.
20    OutputLimitReached,
21    /// Structural traversal reached a configured limit.
22    TraversalLimitReached,
23    /// Maximum traversal depth was reached.
24    DepthLimitReached,
25    /// Source data was already truncated at its ingress boundary.
26    SourceTruncated,
27    /// Source data was not valid JSON.
28    InvalidJson,
29    /// Source data was not a valid URI.
30    InvalidUri,
31    /// Source content type was invalid.
32    InvalidContentType,
33    /// Source content type is unsupported.
34    UnsupportedContentType,
35    /// Source data was not a valid URL-encoded form.
36    InvalidForm,
37    /// Source data was not a valid multipart body.
38    InvalidMultipart,
39}
40
41impl RedactionReason {
42    /// Returns the stable bit assigned to this reason in a reason set.
43    const fn bit(self) -> u64 {
44        match self {
45            Self::InputLimitReached => 1 << 0,
46            Self::OutputLimitReached => 1 << 1,
47            Self::TraversalLimitReached => 1 << 2,
48            Self::DepthLimitReached => 1 << 3,
49            Self::SourceTruncated => 1 << 4,
50            Self::InvalidJson => 1 << 5,
51            Self::InvalidUri => 1 << 6,
52            Self::InvalidContentType => 1 << 7,
53            Self::UnsupportedContentType => 1 << 8,
54            Self::InvalidForm => 1 << 9,
55            Self::InvalidMultipart => 1 << 10,
56        }
57    }
58}
59
60/// Measured resource use for one redaction transaction.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub struct RedactionUsage {
63    /// Bytes presented at public input boundaries.
64    presented_input_bytes: usize,
65    /// Presented bytes admitted for inspection.
66    inspected_input_bytes: usize,
67    /// Escaped bytes retained in final output.
68    output_bytes: usize,
69    /// Structural nodes admitted during traversal.
70    visited_nodes: usize,
71    /// Sequence and map items admitted during traversal.
72    visited_collection_items: usize,
73    /// Greatest active structural depth observed.
74    max_depth: usize,
75    /// Known bytes omitted at admission boundaries.
76    omitted_input_bytes: Option<usize>,
77}
78
79impl Default for RedactionUsage {
80    /// Creates the empty resource measurement used by a fresh transaction.
81    fn default() -> Self {
82        Self::empty()
83    }
84}
85
86impl RedactionUsage {
87    /// Creates an empty measurement for a newly started transaction.
88    #[must_use]
89    pub const fn empty() -> Self {
90        Self {
91            presented_input_bytes: 0,
92            inspected_input_bytes: 0,
93            output_bytes: 0,
94            visited_nodes: 0,
95            visited_collection_items: 0,
96            max_depth: 0,
97            omitted_input_bytes: Some(0),
98        }
99    }
100
101    /// Returns the bytes supplied by callers before input admission.
102    #[must_use]
103    #[inline(always)]
104    pub const fn presented_input_bytes(self) -> usize {
105        self.presented_input_bytes
106    }
107
108    /// Returns the bytes the transaction actually inspected.
109    #[must_use]
110    #[inline(always)]
111    pub const fn inspected_input_bytes(self) -> usize {
112        self.inspected_input_bytes
113    }
114
115    /// Returns the final escaped bytes retained by the transaction.
116    #[must_use]
117    #[inline(always)]
118    pub const fn output_bytes(self) -> usize {
119        self.output_bytes
120    }
121
122    /// Returns the admitted domain or format nodes visited by the transaction.
123    #[must_use]
124    #[inline(always)]
125    pub const fn visited_nodes(self) -> usize {
126        self.visited_nodes
127    }
128
129    /// Returns the admitted collection items visited by the transaction.
130    #[must_use]
131    #[inline(always)]
132    pub const fn visited_collection_items(self) -> usize {
133        self.visited_collection_items
134    }
135
136    /// Returns the greatest active structural depth observed by the
137    /// transaction.
138    #[must_use]
139    #[inline(always)]
140    pub const fn max_depth(self) -> usize {
141        self.max_depth
142    }
143
144    /// Returns omitted source bytes when the source length is known.
145    #[must_use]
146    #[inline(always)]
147    pub const fn omitted_input_bytes(self) -> Option<usize> {
148        self.omitted_input_bytes
149    }
150
151    /// Adds bytes written to the final output buffer.
152    #[must_use]
153    pub(crate) const fn with_added_output_bytes(mut self, bytes: usize) -> Self {
154        self.output_bytes = self.output_bytes.saturating_add(bytes);
155        self
156    }
157
158    /// Records input supplied to and, when admitted, inspected by an adapter.
159    #[must_use]
160    pub(crate) const fn with_input(mut self, presented: usize, inspected: usize) -> Self {
161        self.presented_input_bytes = self.presented_input_bytes.saturating_add(presented);
162        self.inspected_input_bytes = self.inspected_input_bytes.saturating_add(inspected);
163        self.omitted_input_bytes = match self.omitted_input_bytes {
164            Some(omitted) => Some(omitted.saturating_add(presented.saturating_sub(inspected))),
165            None => None,
166        };
167        self
168    }
169
170    /// Records input whose omitted-byte count is supplied by the source.
171    #[cfg(feature = "http")]
172    #[must_use]
173    pub(crate) const fn with_source_input(
174        mut self,
175        presented: usize,
176        inspected: usize,
177        omitted: Option<usize>,
178    ) -> Self {
179        self.presented_input_bytes = self.presented_input_bytes.saturating_add(presented);
180        self.inspected_input_bytes = self.inspected_input_bytes.saturating_add(inspected);
181        self.omitted_input_bytes = match (self.omitted_input_bytes, omitted) {
182            (Some(previous), Some(current)) => Some(previous.saturating_add(current)),
183            _ => None,
184        };
185        self
186    }
187
188    /// Records one admitted structural node.
189    #[must_use]
190    pub(crate) const fn with_domain_node(mut self, depth: usize) -> Self {
191        self.visited_nodes = self.visited_nodes.saturating_add(1);
192        self.max_depth = if self.max_depth > depth { self.max_depth } else { depth };
193        self
194    }
195
196    /// Records one admitted collection item.
197    #[must_use]
198    pub(crate) const fn with_collection_item(mut self) -> Self {
199        self.visited_collection_items = self.visited_collection_items.saturating_add(1);
200        self
201    }
202}
203
204/// Compact set of summary reasons.
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
206pub struct RedactionReasons(
207    /// Stable bit flags for the reasons accumulated by one operation.
208    u64,
209);
210
211impl RedactionReasons {
212    /// Creates an empty reason set.
213    #[must_use]
214    pub const fn empty() -> Self {
215        Self(0)
216    }
217
218    /// Adds one reason.
219    #[must_use]
220    pub const fn with(self, reason: RedactionReason) -> Self {
221        Self(self.0 | reason.bit())
222    }
223
224    /// Returns whether a reason is present.
225    #[must_use]
226    pub const fn contains(self, reason: RedactionReason) -> bool {
227        self.0 & reason.bit() != 0
228    }
229
230    /// Combines two reason sets.
231    #[must_use]
232    pub const fn union(self, other: Self) -> Self {
233        Self(self.0 | other.0)
234    }
235}
236
237/// Machine-readable summary of one redaction operation.
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub struct RedactionSummary {
240    /// Whether the operation intentionally bypassed redaction.
241    redaction_disabled: bool,
242    /// Final completion state of the operation.
243    completion: RedactionCompletion,
244    /// Reasons explaining degraded completion.
245    reasons: RedactionReasons,
246    /// Resource accounting captured by the operation.
247    usage: RedactionUsage,
248}
249
250impl RedactionSummary {
251    /// Creates a summary from runtime-owned completion, reasons, and usage.
252    #[must_use]
253    pub(crate) const fn from_parts(
254        redaction_disabled: bool,
255        completion: RedactionCompletion,
256        reasons: RedactionReasons,
257        usage: RedactionUsage,
258    ) -> Self {
259        Self {
260            redaction_disabled,
261            completion,
262            reasons,
263            usage,
264        }
265    }
266
267    /// Creates a complete summary.
268    #[must_use]
269    pub(crate) const fn complete() -> Self {
270        Self {
271            redaction_disabled: false,
272            completion: RedactionCompletion::Complete,
273            reasons: RedactionReasons::empty(),
274            usage: RedactionUsage::empty(),
275        }
276    }
277
278    /// Creates a degraded summary.
279    #[must_use]
280    pub(crate) const fn truncated(reason: RedactionReason) -> Self {
281        Self {
282            redaction_disabled: false,
283            completion: RedactionCompletion::Truncated,
284            reasons: RedactionReasons::empty().with(reason),
285            usage: RedactionUsage::empty(),
286        }
287    }
288
289    /// Returns completion state.
290    #[must_use]
291    #[inline(always)]
292    pub const fn completion(self) -> RedactionCompletion {
293        self.completion
294    }
295
296    /// Returns whether redaction was globally disabled for this operation.
297    #[must_use]
298    #[inline(always)]
299    pub const fn is_redaction_disabled(self) -> bool {
300        self.redaction_disabled
301    }
302
303    /// Returns accumulated reasons.
304    #[must_use]
305    #[inline(always)]
306    pub const fn reasons(self) -> RedactionReasons {
307        self.reasons
308    }
309
310    /// Returns resource use measured by the operation that produced this
311    /// summary.
312    #[must_use]
313    #[inline(always)]
314    pub const fn usage(self) -> RedactionUsage {
315        self.usage
316    }
317
318    /// Creates a summary for a transaction that exhausted safe output capacity.
319    #[must_use]
320    pub(crate) const fn exhausted(reason: RedactionReason) -> Self {
321        Self {
322            redaction_disabled: false,
323            completion: RedactionCompletion::Exhausted,
324            reasons: RedactionReasons::empty().with(reason),
325            usage: RedactionUsage::empty(),
326        }
327    }
328}