Skip to main content

qubit_redact/policy/
redaction_session.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//! Operation-scoped mutable accounting for bounded redaction.
9
10use std::cell::RefCell;
11
12use super::{
13    InputOutputLimit,
14    RedactionPolicy,
15};
16
17mod budget {
18    use super::InputOutputLimit;
19
20    /// Mutable input/output accounting for one redaction event.
21    ///
22    /// A budget is intentionally not cloneable. Callers must pass the same
23    /// instance through every component that contributes to one diagnostic
24    /// rendering so that a child cannot reset the parent's allowance.
25    #[must_use]
26    #[derive(Debug)]
27    pub(crate) struct DiagnosticBudget {
28        remaining_input_bytes: usize,
29        remaining_output_bytes: usize,
30        input_exhausted: bool,
31        output_exhausted: bool,
32    }
33
34    /// Result of charging an eagerly returned diagnostic fragment.
35    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
36    pub(crate) enum OutputCharge {
37        /// The complete fragment was charged and may be returned.
38        Complete,
39        /// The complete fragment did not fit, but one fallback marker was
40        /// charged.
41        Fallback,
42        /// Neither the fragment nor its fallback can be emitted within the
43        /// budget.
44        Exhausted,
45    }
46
47    impl DiagnosticBudget {
48        /// Creates runtime accounting from an immutable input/output limit.
49        #[must_use = "retain the runtime budget for accounting"]
50        #[inline]
51        pub(crate) const fn new(limit: InputOutputLimit) -> Self {
52            Self {
53                remaining_input_bytes: limit.max_input_bytes(),
54                remaining_output_bytes: limit.max_output_bytes(),
55                input_exhausted: false,
56                output_exhausted: false,
57            }
58        }
59
60        /// Reserves input bytes before inspecting source data.
61        #[inline]
62        pub(crate) fn consume_input(&mut self, bytes: usize) -> bool {
63            if self.input_exhausted || bytes > self.remaining_input_bytes {
64                self.input_exhausted = true;
65                self.remaining_input_bytes = 0;
66                return false;
67            }
68            self.remaining_input_bytes -= bytes;
69            if self.remaining_input_bytes == 0 {
70                self.input_exhausted = true;
71            }
72            true
73        }
74
75        /// Atomically charges either a complete fragment or its terminal
76        /// fallback.
77        pub(crate) fn charge_output_or_fallback(
78            &mut self,
79            bytes: usize,
80            fallback_bytes: usize,
81        ) -> OutputCharge {
82            if !self.output_exhausted && bytes <= self.remaining_output_bytes {
83                self.remaining_output_bytes -= bytes;
84                self.output_exhausted = self.remaining_output_bytes == 0;
85                return OutputCharge::Complete;
86            }
87            if !self.output_exhausted
88                && fallback_bytes <= self.remaining_output_bytes
89            {
90                self.remaining_output_bytes = 0;
91                self.output_exhausted = true;
92                return OutputCharge::Fallback;
93            }
94            self.remaining_output_bytes = 0;
95            self.output_exhausted = true;
96            OutputCharge::Exhausted
97        }
98
99        /// Returns the input bytes still available for inspection.
100        #[must_use]
101        #[inline]
102        pub(crate) const fn remaining_input_bytes(&self) -> usize {
103            self.remaining_input_bytes
104        }
105
106        /// Returns the output bytes still available for rendering.
107        #[must_use]
108        #[inline]
109        pub(crate) const fn remaining_output_bytes(&self) -> usize {
110            self.remaining_output_bytes
111        }
112
113        /// Returns whether this event can no longer accept input or output.
114        #[must_use]
115        #[inline]
116        pub(crate) const fn is_exhausted(&self) -> bool {
117            self.input_exhausted || self.output_exhausted
118        }
119    }
120}
121
122pub(crate) use budget::{
123    DiagnosticBudget,
124    OutputCharge,
125};
126
127mod session_kind {
128    /// Identifies whether a session is an ordinary operation or a diagnostic
129    /// event.
130    #[must_use]
131    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
132    pub enum RedactionSessionKind {
133        /// An independent ordinary redaction operation.
134        Operation,
135        /// A diagnostic representation with bounded output.
136        Diagnostic,
137    }
138}
139
140pub use session_kind::RedactionSessionKind;
141
142/// Carries one immutable policy and one mutable budget through a redaction
143/// operation.
144#[must_use]
145#[derive(Debug)]
146pub struct RedactionSession<'policy> {
147    policy: &'policy RedactionPolicy,
148    budget: RefCell<DiagnosticBudget>,
149    kind: RedactionSessionKind,
150}
151
152impl<'policy> RedactionSession<'policy> {
153    /// Creates an ordinary operation session from `policy`.
154    #[must_use = "retain the operation session for redaction"]
155    #[inline]
156    pub fn operation(policy: &'policy RedactionPolicy) -> Self {
157        Self {
158            policy,
159            budget: RefCell::new(DiagnosticBudget::new(
160                policy.limits().ordinary_operation(),
161            )),
162            kind: RedactionSessionKind::Operation,
163        }
164    }
165
166    /// Creates a diagnostic session from `policy`.
167    #[must_use = "retain the diagnostic session for redaction"]
168    #[inline]
169    pub fn diagnostic(policy: &'policy RedactionPolicy) -> Self {
170        Self {
171            policy,
172            budget: RefCell::new(DiagnosticBudget::new(
173                policy.limits().diagnostic_event(),
174            )),
175            kind: RedactionSessionKind::Diagnostic,
176        }
177    }
178
179    /// Returns the immutable policy snapshot used by this session.
180    #[must_use = "use the policy snapshot for redaction"]
181    #[inline]
182    pub const fn policy(&self) -> &'policy RedactionPolicy {
183        self.policy
184    }
185
186    /// Returns the kind of operation represented by this session.
187    #[must_use = "use the session kind when selecting operation behavior"]
188    #[inline]
189    pub const fn kind(&self) -> RedactionSessionKind {
190        self.kind
191    }
192
193    /// Reserves input bytes in the shared event budget.
194    #[inline]
195    pub fn consume_input(&self, bytes: usize) -> bool {
196        self.budget.borrow_mut().consume_input(bytes)
197    }
198
199    /// Charges an eager fragment or, if it does not fit, one terminal marker.
200    pub(crate) fn charge_output_or_fallback(
201        &self,
202        bytes: usize,
203        fallback_bytes: usize,
204    ) -> OutputCharge {
205        self.budget
206            .borrow_mut()
207            .charge_output_or_fallback(bytes, fallback_bytes)
208    }
209
210    /// Returns the remaining input allowance.
211    #[must_use]
212    #[inline]
213    pub fn remaining_input_bytes(&self) -> usize {
214        self.budget.borrow().remaining_input_bytes()
215    }
216
217    /// Returns the remaining output allowance.
218    #[must_use]
219    #[inline]
220    pub fn remaining_output_bytes(&self) -> usize {
221        self.budget.borrow().remaining_output_bytes()
222    }
223
224    /// Returns whether this session is exhausted.
225    #[must_use]
226    #[inline]
227    pub fn is_exhausted(&self) -> bool {
228        self.budget.borrow().is_exhausted()
229    }
230}