Skip to main content

qubit_redact/policy/
redaction_limits.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//! Immutable structural limits used by redaction.
9
10use qubit_budget::StructureLimits;
11#[cfg(feature = "json")]
12use qubit_budget::json::JsonValueLimits;
13
14use super::RedactionLimitsBuilder;
15
16/// Structural and JSON limits for one redaction operation.
17///
18/// # Examples
19///
20/// ```
21/// use qubit_redact::RedactionLimits;
22///
23/// let mut builder = RedactionLimits::builder();
24/// builder.max_output_bytes(128);
25/// let limits = builder.build();
26/// assert_eq!(limits.max_output_bytes(), 128);
27/// ```
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct RedactionLimits {
30    /// Maximum source bytes admitted for inspection.
31    max_input_bytes: usize,
32    /// Maximum safe bytes retained in output.
33    max_output_bytes: usize,
34    /// Maximum logical scalar bytes passed to a Serde serializer.
35    #[cfg(feature = "serde")]
36    max_serde_payload_bytes: usize,
37    /// Structural limits shared by domain and format traversal.
38    domain: StructureLimits,
39    /// JSON-specific structural and payload limits.
40    #[cfg(feature = "json")]
41    json: JsonValueLimits,
42}
43
44impl RedactionLimits {
45    /// Creates a builder initialized with the standard redaction limits.
46    ///
47    /// # Returns
48    ///
49    /// A mutable builder initialized with finite standard resource ceilings.
50    #[must_use]
51    #[inline(always)]
52    pub fn builder() -> RedactionLimitsBuilder {
53        RedactionLimitsBuilder::default()
54    }
55
56    /// Creates a builder from an immutable limit snapshot.
57    ///
58    /// # Parameters
59    ///
60    /// - `base`: Snapshot whose configured ceilings are copied.
61    ///
62    /// # Returns
63    ///
64    /// A mutable builder preserving every ceiling of the supplied snapshot.
65    #[must_use]
66    #[inline(always)]
67    pub(crate) fn builder_from(base: &Self) -> RedactionLimitsBuilder {
68        RedactionLimitsBuilder::from_limits(base)
69    }
70
71    /// Creates a snapshot from the builder's complete component state.
72    ///
73    /// # Parameters
74    ///
75    /// - `max_input_bytes`: Cumulative source-byte ceiling.
76    /// - `max_output_bytes`: Retained output-byte ceiling.
77    /// - `max_serde_payload_bytes`: Logical scalar-byte ceiling, with Serde
78    ///   enabled.
79    /// - `domain`: Structural admission ceilings shared by the transaction.
80    /// - `json`: JSON-specific ceilings, with JSON enabled.
81    ///
82    /// # Returns
83    ///
84    /// A snapshot retaining all supplied ceilings; policy construction
85    /// validates it.
86    #[must_use]
87    #[inline(always)]
88    pub(super) fn from_parts(
89        max_input_bytes: usize,
90        max_output_bytes: usize,
91        #[cfg(feature = "serde")] max_serde_payload_bytes: usize,
92        domain: StructureLimits,
93        #[cfg(feature = "json")] json: JsonValueLimits,
94    ) -> Self {
95        Self {
96            max_input_bytes,
97            max_output_bytes,
98            #[cfg(feature = "serde")]
99            max_serde_payload_bytes,
100            domain,
101            #[cfg(feature = "json")]
102            json,
103        }
104    }
105
106    /// Returns the maximum source bytes one transaction may inspect.
107    ///
108    /// # Returns
109    ///
110    /// Cumulative source bytes admitted by one transaction.
111    #[must_use]
112    #[inline(always)]
113    pub const fn max_input_bytes(&self) -> usize {
114        self.max_input_bytes
115    }
116
117    /// Returns the maximum safe output bytes one transaction may retain.
118    ///
119    /// # Returns
120    ///
121    /// Maximum final output bytes retained by one transaction.
122    #[must_use]
123    #[inline(always)]
124    pub const fn max_output_bytes(&self) -> usize {
125        self.max_output_bytes
126    }
127
128    /// Returns the logical scalar payload limit for a structured Serde scope.
129    ///
130    /// Counts UTF-8 strings/chars, byte slices, and scalar representations.
131    /// Excludes static field names, container framing, and serializer escaping.
132    /// A caller-owned serializer controls its final encoded byte length;
133    /// `Redactor::to_json` additionally enforces `max_output_bytes`
134    /// when the `json` feature is enabled. The default is 16 KiB; zero is
135    /// valid.
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// use qubit_redact::RedactionLimits;
141    /// let mut builder = RedactionLimits::builder();
142    /// builder.max_serde_payload_bytes(4).max_output_bytes(32);
143    /// assert_eq!(builder.build().max_serde_payload_bytes(), 4);
144    /// ```
145    ///
146    /// # Returns
147    ///
148    /// Logical scalar bytes admitted by one structured Serde scope.
149    #[cfg(feature = "serde")]
150    #[must_use]
151    #[inline(always)]
152    pub const fn max_serde_payload_bytes(&self) -> usize {
153        self.max_serde_payload_bytes
154    }
155
156    /// Returns the maximum nested structural depth.
157    ///
158    /// # Returns
159    ///
160    /// `Some(maximum)` is the configured ceiling; `None` disables this limit.
161    #[must_use]
162    #[inline(always)]
163    pub const fn max_depth(&self) -> Option<usize> {
164        self.domain.max_depth()
165    }
166
167    /// Returns the maximum number of structural nodes.
168    ///
169    /// # Returns
170    ///
171    /// `Some(maximum)` is the configured ceiling; `None` disables this limit.
172    #[must_use]
173    #[inline(always)]
174    pub const fn max_nodes(&self) -> Option<usize> {
175        self.domain.max_nodes()
176    }
177
178    /// Returns the cumulative item allowance shared by transaction collections.
179    ///
180    /// # Returns
181    ///
182    /// `Some(maximum)` is the configured ceiling; `None` disables this limit.
183    #[must_use]
184    #[inline(always)]
185    pub const fn max_collection_items(&self) -> Option<usize> {
186        self.domain.max_sequence_items()
187    }
188
189    /// Returns the maximum raw UTF-8 length of a domain field or classification
190    /// key, checked before normalization and value access. JSON keys also have
191    /// their independent JSON admission limits.
192    ///
193    /// # Returns
194    ///
195    /// `Some(maximum)` bounds raw key bytes; `None` disables this limit.
196    #[must_use]
197    #[inline(always)]
198    pub const fn max_key_bytes(&self) -> Option<usize> {
199        self.domain.max_key_bytes()
200    }
201
202    /// Returns the maximum JSON nesting depth.
203    ///
204    /// # Returns
205    ///
206    /// `Some(maximum)` is the configured ceiling; `None` disables this limit.
207    #[cfg(feature = "json")]
208    #[must_use]
209    #[inline(always)]
210    pub const fn max_json_depth(&self) -> Option<usize> {
211        self.json.max_depth()
212    }
213
214    /// Returns the maximum number of JSON nodes.
215    ///
216    /// # Returns
217    ///
218    /// `Some(maximum)` is the configured ceiling; `None` disables this limit.
219    #[cfg(feature = "json")]
220    #[must_use]
221    #[inline(always)]
222    pub const fn max_json_nodes(&self) -> Option<usize> {
223        self.json.max_nodes()
224    }
225
226    /// Returns the maximum number of items in one JSON collection.
227    ///
228    /// # Returns
229    ///
230    /// `Some(maximum)` is the configured ceiling; `None` disables this limit.
231    #[cfg(feature = "json")]
232    #[must_use]
233    #[inline(always)]
234    pub const fn max_json_collection_items(&self) -> Option<usize> {
235        self.json.max_sequence_items()
236    }
237
238    /// Returns the maximum JSON object-key length.
239    ///
240    /// # Returns
241    ///
242    /// `Some(maximum)` is the configured ceiling; `None` disables this limit.
243    #[cfg(feature = "json")]
244    #[must_use]
245    #[inline(always)]
246    pub const fn max_json_key_bytes(&self) -> Option<usize> {
247        self.json.max_key_bytes()
248    }
249
250    /// Returns the maximum JSON string length.
251    ///
252    /// # Returns
253    ///
254    /// `Some(maximum)` is the configured ceiling; `None` disables this limit.
255    #[cfg(feature = "json")]
256    #[must_use]
257    #[inline(always)]
258    pub const fn max_json_string_bytes(&self) -> Option<usize> {
259        self.json.max_string_bytes()
260    }
261
262    /// Returns the maximum JSON number representation length.
263    ///
264    /// # Returns
265    ///
266    /// `Some(maximum)` is the configured ceiling; `None` disables this limit.
267    #[cfg(feature = "json")]
268    #[must_use]
269    #[inline(always)]
270    pub const fn max_json_number_bytes(&self) -> Option<usize> {
271        self.json.max_number_bytes()
272    }
273
274    /// Returns the cumulative JSON payload-byte maximum.
275    ///
276    /// # Returns
277    ///
278    /// `Some(maximum)` is the configured ceiling; `None` disables this limit.
279    #[cfg(feature = "json")]
280    #[must_use]
281    #[inline(always)]
282    pub const fn max_json_payload_bytes(&self) -> Option<usize> {
283        self.json.max_payload_bytes()
284    }
285
286    /// Returns the internal structural limits for transaction construction.
287    ///
288    /// # Returns
289    ///
290    /// The shared immutable structural admission ceilings.
291    #[must_use]
292    #[inline(always)]
293    pub(crate) const fn structural_limits(&self) -> StructureLimits {
294        self.domain
295    }
296
297    /// Returns the internal JSON limits for transaction construction.
298    ///
299    /// # Returns
300    ///
301    /// The immutable JSON-specific admission ceilings.
302    #[cfg(feature = "json")]
303    #[must_use]
304    #[inline(always)]
305    pub(crate) const fn json_limits(&self) -> JsonValueLimits {
306        self.json
307    }
308
309    /// Validates limits whose values would otherwise reach collection
310    /// allocation code during transaction rendering.
311    ///
312    /// # Errors
313    ///
314    /// Returns [`super::PolicyError::OutputLimitTooLarge`] when the output
315    /// ceiling exceeds the maximum addressable Rust collection capacity.
316    /// With `serde`, an oversized logical payload ceiling returns
317    /// `PolicyError::SerdePayloadLimitTooLarge`.
318    ///
319    /// # Returns
320    ///
321    /// Success when output and enabled Serde payload ceilings fit Rust
322    /// allocation limits.
323    pub(crate) fn validate(&self) -> Result<(), super::PolicyError> {
324        if self.max_output_bytes > isize::MAX as usize {
325            return Err(super::PolicyError::OutputLimitTooLarge {
326                maximum: self.max_output_bytes,
327            });
328        }
329        #[cfg(feature = "serde")]
330        if self.max_serde_payload_bytes > isize::MAX as usize {
331            return Err(super::PolicyError::SerdePayloadLimitTooLarge {
332                maximum: self.max_serde_payload_bytes,
333            });
334        }
335        Ok(())
336    }
337}
338
339impl Default for RedactionLimits {
340    /// Builds the immutable standard limit snapshot.
341    ///
342    /// # Returns
343    ///
344    /// The standard immutable resource limits produced by the default builder.
345    #[inline(always)]
346    fn default() -> Self {
347        Self::builder().build()
348    }
349}