Skip to main content

qubit_value/value_wire/
value_wire_encode_preflight.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
9//! Conservative preparation checks for bounded V1 JSON encoding.
10
11use std::collections::HashMap;
12use std::time::Duration;
13
14#[cfg(any(feature = "big-integer", feature = "big-decimal"))]
15use num_bigint::BigInt;
16#[cfg(any(feature = "big-integer", feature = "big-decimal"))]
17use num_bigint::Sign;
18use qubit_budget::BudgetError;
19use qubit_budget::MeasuredBudgetError;
20use qubit_budget::Observation;
21use qubit_budget::QuantityConversionError;
22use qubit_budget::QuantityMeasurement;
23use qubit_budget::json::JsonEncodeLimits;
24use qubit_budget::json::JsonEncodeLimits as JsonEncodeLimitsU64;
25use qubit_budget::json::JsonMeasurement;
26use qubit_budget::json::JsonResource;
27use qubit_budget::json::JsonValueLimits;
28use serde_json::Value as JsonValue;
29use serde_json::to_writer;
30
31use self::internal::frame::Frame;
32use self::internal::json_length_writer::JsonLengthWriter;
33use self::internal::wire_scalar_measurement::decimal_len;
34use self::internal::wire_scalar_measurement::json_float32_len;
35use self::internal::wire_scalar_measurement::json_float64_len;
36use self::internal::wire_scalar_measurement::unsigned_decimal_len;
37use super::internal::WireDataTypeV1;
38use super::internal::WirePreflightStrategy;
39use crate::MultiValues;
40use crate::Value;
41use crate::ValueContainer;
42use crate::ValueRef;
43
44mod internal;
45
46macro_rules! define_value_ref_preflight_strategy {
47    (
48        $($arg:expr),*;
49        $(
50            (
51                [$($cfg:meta),*],
52                $variant:ident,
53                $type:ty,
54                $_data_type:expr,
55                $_materialization:ident,
56                $_json_class:ident,
57                $_number_projection:ident,
58                $_value_doc:literal,
59                $_multi_doc:literal,
60                [$($scalar_attr:meta),*],
61                [$($collection_attr:meta),*],
62                $_tag:literal,
63                $_wire_preflight:ident
64            )
65        ),+ $(,)?
66    ) => {
67        /// Selects the conservative measurement strategy for a borrowed value.
68        ///
69        /// # Parameters
70        ///
71        /// `value` is the borrowed variant whose wire representation selects the strategy.
72        ///
73        /// # Returns
74        ///
75        /// The selected conservative wire strategy.
76        #[must_use]
77        fn value_ref_preflight_strategy(value: ValueRef<'_>) -> WirePreflightStrategy {
78            match value {
79                ValueRef::Unset(_) => WirePreflightStrategy::UnsetText,
80                $(
81                    $(#[$cfg])*
82                    ValueRef::$variant(_) => WireDataTypeV1::$variant.preflight_strategy(),
83                )+
84            }
85        }
86    };
87}
88
89for_each_value_type!(define_value_ref_preflight_strategy);
90
91/// Performs conservative resource checks before Wire V1 sorting and formatting.
92///
93/// The checker accumulates conservative lower bounds across successful calls.
94/// Each public check is atomic: if it returns an error, all counters are
95/// restored to their values before that call. The checker does not serialize,
96/// retain a sorting index, or replace the authoritative final
97/// `JsonEncodeSession` checks.
98///
99/// # Examples
100///
101/// ```
102/// use qubit_budget::MeasuredBudgetError;
103/// use qubit_budget::json::JsonEncodeLimits;
104/// use qubit_budget::json::JsonResource;
105/// use qubit_value::Value;
106/// use qubit_value::ValueWireEncodePreflight;
107///
108/// # fn main() -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
109/// let limits = JsonEncodeLimits::builder().max_nodes(2_usize).build();
110/// let mut preflight = ValueWireEncodePreflight::new(limits);
111/// preflight.check_value(&Value::from(1_i32))?;
112/// preflight.check_value(&Value::from(2_i32))?;
113/// # Ok(())
114/// # }
115/// ```
116#[derive(Debug, Clone)]
117pub struct ValueWireEncodePreflight {
118    /// Configured per-value and cumulative ceilings for subsequent checks.
119    limits: JsonEncodeLimits,
120    /// Number of admitted JSON value nodes across successful checks.
121    nodes: usize,
122    /// Accumulated lower bound for textual payload bytes, including keys.
123    payload_bytes: usize,
124    /// Accumulated lower bound for encoded bytes, including punctuation.
125    output_bytes: usize,
126}
127
128/// Converts a `u64` limit to the current platform's native budget quantity.
129///
130/// Values outside the `usize` range saturate at [`usize::MAX`].
131///
132/// # Parameters
133///
134/// `value` is the configured unsigned limit to convert.
135///
136/// # Returns
137///
138/// The converted quantity, saturated at `usize::MAX` if necessary.
139#[inline]
140#[must_use]
141fn saturating_u64_to_usize(value: u64) -> usize {
142    usize::try_from(value).unwrap_or(usize::MAX)
143}
144
145impl ValueWireEncodePreflight {
146    /// Creates a checker with zero accumulated usage from one limit profile.
147    ///
148    /// # Parameters
149    ///
150    /// `limits` supplies the per-value and cumulative resource ceilings.
151    ///
152    /// # Returns
153    ///
154    /// A checker with zero accumulated usage.
155    #[must_use]
156    #[inline]
157    pub fn new(limits: JsonEncodeLimits) -> Self {
158        Self {
159            limits,
160            nodes: 0,
161            payload_bytes: 0,
162            output_bytes: 0,
163        }
164    }
165
166    /// Creates a checker with zero accumulated usage for value-only limits.
167    ///
168    /// The outer output budget remains unconfigured because another protocol
169    /// envelope is expected to account for it.
170    ///
171    /// # Parameters
172    ///
173    /// `limits` supplies value ceilings without an outer output limit.
174    ///
175    /// # Returns
176    ///
177    /// A checker with zero accumulated usage.
178    #[must_use]
179    #[inline]
180    pub fn new_value_limits(limits: JsonValueLimits) -> Self {
181        let mut checker = Self::new(JsonEncodeLimits::new());
182        checker.limits = JsonEncodeLimits::builder().value_limits(limits).build();
183        checker
184    }
185
186    /// Creates a checker from the `u64` profile used by configuration wire
187    /// limits.
188    ///
189    /// Values greater than [`usize::MAX`] on the current platform saturate at
190    /// [`usize::MAX`] instead of wrapping or truncating.
191    ///
192    /// # Parameters
193    ///
194    /// `limits` supplies the unsigned 64-bit configuration profile.
195    ///
196    /// # Returns
197    ///
198    /// A checker with zero accumulated usage.
199    #[must_use]
200    pub fn new_u64_limits(limits: JsonEncodeLimitsU64<JsonResource, u64>) -> Self {
201        let value = limits.value_limits();
202        let mut builder = JsonEncodeLimits::builder();
203        if let Some(limit) = limits.max_output_bytes() {
204            builder = builder.max_output_bytes(saturating_u64_to_usize(limit));
205        }
206        if let Some(limit) = value.max_depth() {
207            builder = builder.max_depth(saturating_u64_to_usize(limit));
208        }
209        if let Some(limit) = value.max_nodes() {
210            builder = builder.max_nodes(saturating_u64_to_usize(limit));
211        }
212        if let Some(limit) = value.max_sequence_items() {
213            builder = builder.max_sequence_items(saturating_u64_to_usize(limit));
214        }
215        if let Some(limit) = value.max_map_entries() {
216            builder = builder.max_map_entries(saturating_u64_to_usize(limit));
217        }
218        if let Some(limit) = value.max_key_bytes() {
219            builder = builder.max_key_bytes(saturating_u64_to_usize(limit));
220        }
221        if let Some(limit) = value.max_string_bytes() {
222            builder = builder.max_string_bytes(saturating_u64_to_usize(limit));
223        }
224        if let Some(limit) = value.max_number_bytes() {
225            builder = builder.max_number_bytes(saturating_u64_to_usize(limit));
226        }
227        if let Some(limit) = value.max_payload_bytes() {
228            builder = builder.max_payload_bytes(saturating_u64_to_usize(limit));
229        }
230        Self::new(builder.build())
231    }
232
233    /// Checks and accumulates one scalar value at the root of a payload.
234    ///
235    /// Returns the first exceeded JSON resource limit. If checking fails, the
236    /// accumulated state is restored to its value before this call.
237    ///
238    /// # Parameters
239    ///
240    /// `value` is borrowed for this check and is not retained.
241    ///
242    /// # Returns
243    ///
244    /// `Ok(())` when all applicable measurements are admitted.
245    ///
246    /// # Errors
247    ///
248    /// Returns a measured budget error when a per-value or cumulative resource
249    /// limit is exceeded, or when a cumulative counter overflows.
250    #[inline]
251    pub fn check_value(&mut self, value: &Value) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
252        self.transaction(|checker| checker.check_value_at(value, 1))
253    }
254
255    /// Checks and accumulates one homogeneous collection at the payload root.
256    ///
257    /// Returns the first exceeded JSON resource limit. If checking fails, the
258    /// accumulated state is restored to its value before this call.
259    ///
260    /// # Parameters
261    ///
262    /// `values` is the borrowed collection to check as a root array.
263    ///
264    /// # Returns
265    ///
266    /// `Ok(())` when all applicable measurements are admitted.
267    ///
268    /// # Errors
269    ///
270    /// Returns a measured budget error when a per-value or cumulative resource
271    /// limit is exceeded, or when a cumulative counter overflows.
272    #[inline]
273    pub fn check_values(&mut self, values: &MultiValues) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
274        self.transaction(|checker| checker.check_values_at(values, 1))
275    }
276
277    /// Checks and accumulates an explicit scalar-or-collection payload.
278    ///
279    /// Returns the first exceeded JSON resource limit. If checking fails, the
280    /// accumulated state is restored to its value before this call.
281    ///
282    /// # Parameters
283    ///
284    /// `value` selects a borrowed scalar or collection payload.
285    ///
286    /// # Returns
287    ///
288    /// `Ok(())` when all applicable measurements are admitted.
289    ///
290    /// # Errors
291    ///
292    /// Returns a measured budget error when a per-value or cumulative resource
293    /// limit is exceeded, or when a cumulative counter overflows.
294    #[inline]
295    pub fn check_container(&mut self, value: &ValueContainer) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
296        self.transaction(|checker| match value {
297            ValueContainer::Scalar(value) => checker.check_value_at(value, 1),
298            ValueContainer::Collection(values) => checker.check_values_at(values, 1),
299        })
300    }
301
302    /// Executes one public check as a transaction over cumulative counters.
303    ///
304    /// Successful checks retain their measurements. An error restores all
305    /// counters and returns the original budget error unchanged.
306    ///
307    /// # Type Parameters
308    ///
309    /// `F` is the one-shot fallible check applied to the candidate.
310    ///
311    /// # Parameters
312    ///
313    /// `check` runs once against a cloned candidate; only success commits it.
314    ///
315    /// # Returns
316    ///
317    /// `Ok(())` when all applicable measurements are admitted.
318    ///
319    /// # Errors
320    ///
321    /// Returns the first resource-limit or quantity-overflow error encountered.
322    ///
323    /// # Panics
324    ///
325    /// Propagates a panic from `check` without committing candidate counters.
326    fn transaction<F>(&mut self, check: F) -> Result<(), MeasuredBudgetError<JsonResource, usize>>
327    where
328        F: FnOnce(&mut Self) -> Result<(), MeasuredBudgetError<JsonResource, usize>>,
329    {
330        let mut candidate = self.clone();
331        match check(&mut candidate) {
332            Ok(()) => {
333                *self = candidate;
334                Ok(())
335            }
336            Err(error) => Err(error),
337        }
338    }
339
340    /// Checks a value at its supplied nesting depth and records its
341    /// measurements.
342    ///
343    /// # Parameters
344    ///
345    /// `value` is the borrowed node; `depth` is its one-based nesting depth.
346    ///
347    /// # Returns
348    ///
349    /// `Ok(())` when all applicable measurements are admitted.
350    ///
351    /// # Errors
352    ///
353    /// Returns the first resource-limit or quantity-overflow error encountered.
354    #[inline]
355    fn check_value_at(&mut self, value: &Value, depth: usize) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
356        self.check_view_at(value.view(), depth)
357    }
358
359    /// Dispatches a borrowed value to the measurement strategy for its variant.
360    ///
361    /// # Parameters
362    ///
363    /// `value` is the borrowed variant view; `depth` is its one-based nesting
364    /// depth.
365    ///
366    /// # Returns
367    ///
368    /// `Ok(())` when all applicable measurements are admitted.
369    ///
370    /// # Errors
371    ///
372    /// Returns the first resource-limit or quantity-overflow error encountered.
373    fn check_view_at(
374        &mut self,
375        value: ValueRef<'_>,
376        depth: usize,
377    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
378        match value_ref_preflight_strategy(value) {
379            WirePreflightStrategy::Boolean => match value {
380                ValueRef::Bool(_) => self.admit(JsonMeasurement::Boolean { depth }, 1, 0),
381                _ => unreachable!("table strategy and ValueRef variant diverged"),
382            },
383            WirePreflightStrategy::BorrowedText => match value {
384                ValueRef::Char(value) => self.admit_string(depth, value.len_utf8()),
385                ValueRef::String(value) => self.admit_string(depth, value.len()),
386                #[cfg(feature = "url")]
387                ValueRef::Url(value) => self.admit_string(depth, value.as_str().len()),
388                _ => unreachable!("table strategy and ValueRef variant diverged"),
389            },
390            WirePreflightStrategy::JsonInteger => match value {
391                ValueRef::Int8(value) => self.admit_number(depth, decimal_len(value as i128)),
392                ValueRef::Int16(value) => self.admit_number(depth, decimal_len(value as i128)),
393                ValueRef::Int32(value) => self.admit_number(depth, decimal_len(value as i128)),
394                ValueRef::Int64(value) => self.admit_number(depth, decimal_len(value as i128)),
395                ValueRef::UInt8(value) => self.admit_number(depth, unsigned_decimal_len(value as u128)),
396                ValueRef::UInt16(value) => self.admit_number(depth, unsigned_decimal_len(value as u128)),
397                ValueRef::UInt32(value) => self.admit_number(depth, unsigned_decimal_len(value as u128)),
398                ValueRef::UInt64(value) => self.admit_number(depth, unsigned_decimal_len(value as u128)),
399                _ => unreachable!("table strategy and ValueRef variant diverged"),
400            },
401            WirePreflightStrategy::DecimalText => match value {
402                ValueRef::Int128(value) => self.admit_string(depth, decimal_len(value)),
403                ValueRef::UInt128(value) => self.admit_string(depth, unsigned_decimal_len(value)),
404                _ => unreachable!("table strategy and ValueRef variant diverged"),
405            },
406            WirePreflightStrategy::UnsetText => match value {
407                ValueRef::Unset(data_type) => {
408                    let tag = WireDataTypeV1::from(data_type).tag();
409                    self.admit_string(depth, tag.len())
410                }
411                _ => unreachable!("table strategy and ValueRef variant diverged"),
412            },
413            WirePreflightStrategy::JsonFloat => match value {
414                ValueRef::Float32(value) => {
415                    self.admit_number(depth, if value.is_finite() { json_float32_len(value) } else { 1 })
416                }
417                ValueRef::Float64(value) => {
418                    self.admit_number(depth, if value.is_finite() { json_float64_len(value) } else { 1 })
419                }
420                _ => unreachable!("table strategy and ValueRef variant diverged"),
421            },
422            WirePreflightStrategy::BigIntegerText => {
423                #[cfg(feature = "big-integer")]
424                {
425                    match value {
426                        ValueRef::BigInteger(value) => self.admit_string(depth, bigint_digits(value)),
427                        _ => unreachable!("table strategy and ValueRef variant diverged"),
428                    }
429                }
430                #[cfg(not(feature = "big-integer"))]
431                {
432                    unreachable!("big-integer strategy is unavailable without its feature")
433                }
434            }
435            WirePreflightStrategy::DecimalObject => {
436                #[cfg(feature = "big-decimal")]
437                {
438                    match value {
439                        ValueRef::BigDecimal(value) => {
440                            let (coefficient, scale) = value.as_bigint_and_scale();
441                            self.admit_object_with_keys(
442                                depth,
443                                [
444                                    ("coefficient", bigint_digits(coefficient.as_ref()), true),
445                                    ("scale", decimal_len(scale as i128), false),
446                                ],
447                            )
448                        }
449                        _ => unreachable!("table strategy and ValueRef variant diverged"),
450                    }
451                }
452                #[cfg(not(feature = "big-decimal"))]
453                {
454                    unreachable!("big-decimal strategy is unavailable without its feature")
455                }
456            }
457            WirePreflightStrategy::TemporalText => match value {
458                #[cfg(feature = "chrono")]
459                ValueRef::Date(_) | ValueRef::Time(_) | ValueRef::DateTime(_) | ValueRef::Instant(_) => {
460                    self.admit_string(depth, 0)
461                }
462                _ => unreachable!("table strategy and ValueRef variant diverged"),
463            },
464            WirePreflightStrategy::DurationObject => match value {
465                ValueRef::Duration(value) => self.admit_duration(depth, *value),
466                _ => unreachable!("table strategy and ValueRef variant diverged"),
467            },
468            WirePreflightStrategy::StringMap => match value {
469                ValueRef::StringMap(value) => self.check_string_map(value, depth),
470                _ => unreachable!("table strategy and ValueRef variant diverged"),
471            },
472            #[cfg(feature = "json")]
473            WirePreflightStrategy::JsonTree => match value {
474                ValueRef::Json(value) => self.check_json(value, depth),
475                _ => unreachable!("table strategy and ValueRef variant diverged"),
476            },
477        }
478    }
479
480    /// Accounts for a homogeneous collection and checks each item one level
481    /// deeper.
482    ///
483    /// # Parameters
484    ///
485    /// `values` supplies the array items; `depth` is the array nesting depth.
486    ///
487    /// # Returns
488    ///
489    /// `Ok(())` when all applicable measurements are admitted.
490    ///
491    /// # Errors
492    ///
493    /// Returns the first resource-limit or quantity-overflow error encountered.
494    fn check_values_at(
495        &mut self,
496        values: &MultiValues,
497        depth: usize,
498    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
499        self.admit_array(depth, values.len())?;
500        let view = values.view();
501        for index in 0..view.len() {
502            if let Some(value) = view.get(index) {
503                self.check_view_at(value, depth + 1)?;
504            }
505        }
506        Ok(())
507    }
508
509    /// Accounts for a string map and checks each key and value against its
510    /// limits.
511    ///
512    /// # Parameters
513    ///
514    /// `map` supplies borrowed keys and values; `depth` is the object nesting
515    /// depth.
516    ///
517    /// # Returns
518    ///
519    /// `Ok(())` when all applicable measurements are admitted.
520    ///
521    /// # Errors
522    ///
523    /// Returns the first resource-limit or quantity-overflow error encountered.
524    fn check_string_map(
525        &mut self,
526        map: &HashMap<String, String>,
527        depth: usize,
528    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
529        self.admit_object(depth, map.len())?;
530        for (key, value) in map {
531            self.admit_key(key)?;
532            self.admit_string(depth + 1, value.len())?;
533        }
534        Ok(())
535    }
536
537    /// Iteratively accounts for a JSON tree without recursively walking nested
538    /// values.
539    ///
540    /// # Parameters
541    ///
542    /// `value` is the borrowed tree root; `depth` is its one-based nesting
543    /// depth.
544    ///
545    /// # Returns
546    ///
547    /// `Ok(())` when all applicable measurements are admitted.
548    ///
549    /// # Errors
550    ///
551    /// Returns the first resource-limit or quantity-overflow error encountered.
552    #[cfg(feature = "json")]
553    fn check_json(&mut self, value: &JsonValue, depth: usize) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
554        let mut frames = vec![Frame::Visit(value, depth)];
555        while let Some(frame) = frames.pop() {
556            match frame {
557                Frame::Visit(value, depth) => match value {
558                    JsonValue::Null => self.admit(JsonMeasurement::Null { depth }, 1, 0)?,
559                    JsonValue::Bool(_) => self.admit(JsonMeasurement::Boolean { depth }, 1, 0)?,
560                    JsonValue::Number(value) => {
561                        let mut writer = JsonLengthWriter::default();
562                        to_writer(&mut writer, value).expect("JSON Number serialization cannot fail");
563                        self.admit_number(depth, writer.len)?;
564                    }
565                    JsonValue::String(value) => self.admit_string(depth, value.len())?,
566                    JsonValue::Array(values) => {
567                        self.admit_array(depth, values.len())?;
568                        frames.push(Frame::Array(values.iter(), depth + 1));
569                    }
570                    JsonValue::Object(values) => {
571                        self.admit_object(depth, values.len())?;
572                        frames.push(Frame::Object(values.iter(), depth + 1));
573                    }
574                },
575                Frame::Array(mut values, depth) => {
576                    if let Some(value) = values.next() {
577                        frames.push(Frame::Array(values, depth));
578                        frames.push(Frame::Visit(value, depth));
579                    }
580                }
581                Frame::Object(mut values, depth) => {
582                    if let Some((key, value)) = values.next() {
583                        self.admit_key(key)?;
584                        frames.push(Frame::Object(values, depth));
585                        frames.push(Frame::Visit(value, depth));
586                    }
587                }
588            }
589        }
590        Ok(())
591    }
592
593    /// Accounts for a JSON string, including its quotes in the output estimate.
594    ///
595    /// # Parameters
596    ///
597    /// `depth` is the node depth; `bytes` is its unescaped UTF-8 payload
598    /// length.
599    ///
600    /// # Returns
601    ///
602    /// `Ok(())` when all applicable measurements are admitted.
603    ///
604    /// # Errors
605    ///
606    /// Returns the first resource-limit or quantity-overflow error encountered.
607    fn admit_string(&mut self, depth: usize, bytes: usize) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
608        let output = bytes
609            .checked_add(2)
610            .ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
611        self.admit(JsonMeasurement::String { depth, bytes }, output, bytes)
612    }
613
614    /// Accounts for a JSON number using its conservative encoded byte length.
615    ///
616    /// # Parameters
617    ///
618    /// `depth` is the node depth; `bytes` is its encoded number length lower
619    /// bound.
620    ///
621    /// # Returns
622    ///
623    /// `Ok(())` when all applicable measurements are admitted.
624    ///
625    /// # Errors
626    ///
627    /// Returns the first resource-limit or quantity-overflow error encountered.
628    #[inline]
629    fn admit_number(&mut self, depth: usize, bytes: usize) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
630        self.admit(JsonMeasurement::Number { depth, bytes }, bytes, bytes)
631    }
632
633    /// Accounts for array punctuation without charging it to payload bytes.
634    ///
635    /// # Parameters
636    ///
637    /// `depth` is the array depth; `items` is its element count.
638    ///
639    /// # Returns
640    ///
641    /// `Ok(())` when all applicable measurements are admitted.
642    ///
643    /// # Errors
644    ///
645    /// Returns the first resource-limit or quantity-overflow error encountered.
646    fn admit_array(&mut self, depth: usize, items: usize) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
647        let output = items
648            .saturating_sub(1)
649            .checked_add(2)
650            .ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
651        self.admit(JsonMeasurement::Array { depth, items }, output, 0)
652    }
653
654    /// Accounts for object punctuation without charging it to payload bytes.
655    ///
656    /// # Parameters
657    ///
658    /// `depth` is the object depth; `entries` is its member count.
659    ///
660    /// # Returns
661    ///
662    /// `Ok(())` when all applicable measurements are admitted.
663    ///
664    /// # Errors
665    ///
666    /// Returns the first resource-limit or quantity-overflow error encountered.
667    fn admit_object(&mut self, depth: usize, entries: usize) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
668        let output = entries
669            .checked_add(entries.saturating_sub(1))
670            .and_then(|value| value.checked_add(2))
671            .ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
672        self.admit(JsonMeasurement::Object { depth, entries }, output, 0)
673    }
674
675    /// Accounts for the object representation of a duration.
676    ///
677    /// # Parameters
678    ///
679    /// `depth` is the object depth; `value` supplies seconds and nanoseconds.
680    ///
681    /// # Returns
682    ///
683    /// `Ok(())` when all applicable measurements are admitted.
684    ///
685    /// # Errors
686    ///
687    /// Returns the first resource-limit or quantity-overflow error encountered.
688    #[inline]
689    fn admit_duration(
690        &mut self,
691        depth: usize,
692        value: Duration,
693    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
694        self.admit_object_with_keys(
695            depth,
696            [
697                ("secs", unsigned_decimal_len(value.as_secs() as u128), false),
698                ("nanos", unsigned_decimal_len(value.subsec_nanos() as u128), false),
699            ],
700        )
701    }
702
703    /// Accounts for a fixed-key object and each field's scalar value.
704    ///
705    /// # Type Parameters
706    ///
707    /// `N` is the fixed number of object fields.
708    ///
709    /// # Parameters
710    ///
711    /// `depth` is the object depth. Each `fields` tuple contains a key,
712    /// its value length, and whether the value is a JSON string.
713    ///
714    /// # Returns
715    ///
716    /// `Ok(())` when all applicable measurements are admitted.
717    ///
718    /// # Errors
719    ///
720    /// Returns the first resource-limit or quantity-overflow error encountered.
721    fn admit_object_with_keys<const N: usize>(
722        &mut self,
723        depth: usize,
724        fields: [(&str, usize, bool); N],
725    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
726        self.admit_object(depth, N)?;
727        for (key, value, is_string) in fields {
728            self.admit_key(key)?;
729            if is_string {
730                self.admit_string(depth + 1, value)?;
731            } else {
732                self.admit_number(depth + 1, value)?;
733            }
734        }
735        Ok(())
736    }
737
738    /// Accounts for one object key without adding a JSON value node.
739    ///
740    /// # Parameters
741    ///
742    /// `key` is borrowed UTF-8 object-key text, excluding JSON quotes.
743    ///
744    /// # Returns
745    ///
746    /// `Ok(())` when all applicable measurements are admitted.
747    ///
748    /// # Errors
749    ///
750    /// Returns a measured error for a key limit, cumulative limit, or overflow.
751    fn admit_key(&mut self, key: &str) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
752        self.check_point(JsonMeasurement::Key { bytes: key.len() })?;
753        let output = key
754            .len()
755            .checked_add(2)
756            .ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
757        self.payload_bytes = self
758            .payload_bytes
759            .checked_add(key.len())
760            .ok_or_else(|| self.quantity_error(JsonResource::PayloadBytes))?;
761        self.output_bytes = self
762            .output_bytes
763            .checked_add(output)
764            .ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
765        self.check_cumulative(
766            JsonResource::PayloadBytes,
767            self.payload_bytes,
768            self.limits.value_limits().max_payload_bytes(),
769        )?;
770        self.check_cumulative(
771            JsonResource::OutputBytes,
772            self.output_bytes,
773            self.limits.max_output_bytes(),
774        )
775    }
776
777    /// Checks one measurement and adds its node, payload, and output lower
778    /// bounds.
779    ///
780    /// # Parameters
781    ///
782    /// `measurement` describes the node; `output` and `payload` are byte lower
783    /// bounds added to their respective cumulative counters.
784    ///
785    /// # Returns
786    ///
787    /// `Ok(())` when all applicable measurements are admitted.
788    ///
789    /// # Errors
790    ///
791    /// Returns the first resource-limit or quantity-overflow error encountered.
792    fn admit(
793        &mut self,
794        measurement: JsonMeasurement,
795        output: usize,
796        payload: usize,
797    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
798        self.check_point(measurement)?;
799        self.nodes = self
800            .nodes
801            .checked_add(1)
802            .ok_or_else(|| self.quantity_error(JsonResource::Nodes))?;
803        self.payload_bytes = self
804            .payload_bytes
805            .checked_add(payload)
806            .ok_or_else(|| self.quantity_error(JsonResource::PayloadBytes))?;
807        self.output_bytes = self
808            .output_bytes
809            .checked_add(output)
810            .ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
811        self.check_cumulative(JsonResource::Nodes, self.nodes, self.limits.value_limits().max_nodes())?;
812        self.check_cumulative(
813            JsonResource::PayloadBytes,
814            self.payload_bytes,
815            self.limits.value_limits().max_payload_bytes(),
816        )?;
817        self.check_cumulative(
818            JsonResource::OutputBytes,
819            self.output_bytes,
820            self.limits.max_output_bytes(),
821        )
822    }
823
824    /// Applies the configured per-value limits to one measurement.
825    ///
826    /// # Parameters
827    ///
828    /// `measurement` describes the individual resource observation to validate.
829    ///
830    /// # Returns
831    ///
832    /// `Ok(())` when all applicable measurements are admitted.
833    ///
834    /// # Errors
835    ///
836    /// Returns the first resource-limit or quantity-overflow error encountered.
837    #[inline]
838    fn check_point(&self, measurement: JsonMeasurement) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
839        self.limits.value_limits().check_point(measurement)
840    }
841
842    /// Rejects a cumulative counter that exceeds its optional configured
843    /// maximum.
844    ///
845    /// # Parameters
846    ///
847    /// `resource` identifies the counter; `observed` is its lower bound.
848    /// `maximum` enables a ceiling with `Some`, or leaves it unlimited with
849    /// `None`.
850    ///
851    /// # Returns
852    ///
853    /// `Ok(())` when all applicable measurements are admitted.
854    ///
855    /// # Errors
856    ///
857    /// Returns the first resource-limit or quantity-overflow error encountered.
858    fn check_cumulative(
859        &self,
860        resource: JsonResource,
861        observed: usize,
862        maximum: Option<usize>,
863    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
864        match maximum {
865            Some(maximum) if observed > maximum => Err(self.lower_bound_error(resource, observed, maximum)),
866            _ => Ok(()),
867        }
868    }
869
870    /// Builds a budget error that reports the observed value as a lower bound.
871    ///
872    /// # Parameters
873    ///
874    /// `resource` identifies the exceeded budget; `observed` is the lower
875    /// bound and `maximum` is the configured ceiling.
876    ///
877    /// # Returns
878    ///
879    /// A measured budget error describing the failed resource.
880    #[inline]
881    fn lower_bound_error(
882        &self,
883        resource: JsonResource,
884        observed: usize,
885        maximum: usize,
886    ) -> MeasuredBudgetError<JsonResource, usize> {
887        MeasuredBudgetError::Budget(BudgetError::LimitExceeded {
888            resource,
889            observed: Observation::AtLeast(observed),
890            maximum,
891        })
892    }
893
894    /// Builds a quantity error for arithmetic that cannot fit in `usize`.
895    ///
896    /// # Parameters
897    ///
898    /// `resource` identifies the counter whose arithmetic overflowed.
899    ///
900    /// # Returns
901    ///
902    /// A measured budget error describing the failed resource.
903    #[inline]
904    fn quantity_error(&self, resource: JsonResource) -> MeasuredBudgetError<JsonResource, usize> {
905        MeasuredBudgetError::quantity(
906            resource,
907            QuantityConversionError::new(QuantityMeasurement::Usize(usize::MAX), "usize"),
908        )
909    }
910}
911
912/// Estimates the decimal digit count, including a sign when the value is
913/// negative.
914///
915/// # Parameters
916///
917/// `value` is the borrowed integer whose decimal text is estimated.
918///
919/// # Returns
920///
921/// A conservative decimal text length including any minus sign.
922#[cfg(any(feature = "big-integer", feature = "big-decimal"))]
923#[must_use]
924#[inline]
925fn bigint_digits(value: &BigInt) -> usize {
926    if value.sign() == Sign::NoSign {
927        1
928    } else {
929        ((value.bits().saturating_sub(1)) / 4 + 1) as usize + usize::from(value.sign() == Sign::Minus)
930    }
931}
932
933#[cfg(test)]
934mod tests {
935    use std::collections::HashMap;
936    use std::panic::AssertUnwindSafe;
937    use std::panic::catch_unwind;
938
939    use qubit_budget::MeasuredBudgetError;
940    use qubit_budget::json::JsonEncodeLimits;
941    use qubit_budget::json::JsonMeasurement;
942    use qubit_budget::json::JsonResource;
943
944    use super::ValueWireEncodePreflight;
945    use crate::Value;
946
947    /// Verifies a counter overflow is reported as a quantity error.
948    fn assert_overflow(error: MeasuredBudgetError<JsonResource, usize>) {
949        assert!(
950            matches!(error, MeasuredBudgetError::Quantity { .. }),
951            "expected a quantity overflow, got {error:?}",
952        );
953    }
954
955    #[test]
956    fn test_check_value_reports_node_counter_overflow_and_rolls_back() {
957        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
958        checker.nodes = usize::MAX;
959
960        let error = checker
961            .check_value(&Value::Bool(true))
962            .expect_err("the node counter cannot exceed usize::MAX");
963
964        assert_overflow(error);
965        assert_eq!(checker.nodes, usize::MAX);
966        assert_eq!(checker.payload_bytes, 0);
967        assert_eq!(checker.output_bytes, 0);
968    }
969
970    #[test]
971    fn test_check_value_reports_payload_counter_overflow_and_rolls_back() {
972        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
973        checker.payload_bytes = usize::MAX;
974
975        let error = checker
976            .check_value(&Value::String("x".to_owned()))
977            .expect_err("the payload counter cannot exceed usize::MAX");
978
979        assert_overflow(error);
980        assert_eq!(checker.nodes, 0);
981        assert_eq!(checker.payload_bytes, usize::MAX);
982        assert_eq!(checker.output_bytes, 0);
983    }
984
985    #[test]
986    fn test_check_value_reports_key_payload_counter_overflow_and_rolls_back() {
987        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
988        checker.payload_bytes = usize::MAX;
989
990        let error = checker
991            .check_value(&Value::StringMap(HashMap::from([("x".to_owned(), String::new())])))
992            .expect_err("the key payload counter cannot exceed usize::MAX");
993
994        assert_overflow(error);
995        assert_eq!(checker.nodes, 0);
996        assert_eq!(checker.payload_bytes, usize::MAX);
997        assert_eq!(checker.output_bytes, 0);
998    }
999
1000    #[test]
1001    fn test_check_value_reports_key_output_counter_overflow_and_rolls_back() {
1002        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
1003        checker.output_bytes = usize::MAX - 3;
1004
1005        let error = checker
1006            .check_value(&Value::StringMap(HashMap::from([("x".to_owned(), String::new())])))
1007            .expect_err("the key output counter cannot exceed usize::MAX");
1008
1009        assert_overflow(error);
1010        assert_eq!(checker.nodes, 0);
1011        assert_eq!(checker.payload_bytes, 0);
1012        assert_eq!(checker.output_bytes, usize::MAX - 3);
1013    }
1014
1015    #[test]
1016    fn test_check_value_reports_output_counter_overflow_and_rolls_back() {
1017        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
1018        checker.output_bytes = usize::MAX;
1019
1020        let error = checker
1021            .check_value(&Value::Bool(true))
1022            .expect_err("the output counter cannot exceed usize::MAX");
1023
1024        assert_overflow(error);
1025        assert_eq!(checker.nodes, 0);
1026        assert_eq!(checker.payload_bytes, 0);
1027        assert_eq!(checker.output_bytes, usize::MAX);
1028    }
1029
1030    #[test]
1031    fn test_admit_helpers_report_arithmetic_overflow_without_large_values() {
1032        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
1033        assert_overflow(
1034            checker
1035                .admit_string(0, usize::MAX)
1036                .expect_err("string output length must not overflow"),
1037        );
1038
1039        assert_overflow(
1040            checker
1041                .admit_array(0, usize::MAX)
1042                .expect_err("array output punctuation must not overflow"),
1043        );
1044
1045        assert_overflow(
1046            checker
1047                .admit_object(0, usize::MAX)
1048                .expect_err("object output punctuation must not overflow"),
1049        );
1050
1051        checker.payload_bytes = usize::MAX;
1052        assert_overflow(
1053            checker
1054                .admit_key("x")
1055                .expect_err("key payload accumulation must not overflow"),
1056        );
1057
1058        checker.payload_bytes = 0;
1059        checker.output_bytes = usize::MAX - 2;
1060        assert_overflow(
1061            checker
1062                .admit_key("x")
1063                .expect_err("key output accumulation must not overflow"),
1064        );
1065    }
1066
1067    #[test]
1068    fn test_admit_reports_each_counter_arithmetic_overflow() {
1069        let measurement = JsonMeasurement::Boolean { depth: 0 };
1070
1071        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
1072        checker.nodes = usize::MAX;
1073        assert_overflow(
1074            checker
1075                .admit(measurement, 1, 0)
1076                .expect_err("node accumulation must not overflow"),
1077        );
1078
1079        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
1080        checker.payload_bytes = usize::MAX;
1081        assert_overflow(
1082            checker
1083                .admit(measurement, 1, 1)
1084                .expect_err("payload accumulation must not overflow"),
1085        );
1086
1087        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
1088        checker.output_bytes = usize::MAX;
1089        assert_overflow(
1090            checker
1091                .admit(measurement, 1, 0)
1092                .expect_err("output accumulation must not overflow"),
1093        );
1094    }
1095
1096    #[test]
1097    fn test_transaction_does_not_commit_counters_when_check_panics() {
1098        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
1099        let result = catch_unwind(AssertUnwindSafe(|| {
1100            checker.transaction(|candidate| {
1101                candidate.nodes = 17;
1102                candidate.payload_bytes = 23;
1103                panic!("simulated preflight panic");
1104            })
1105        }));
1106
1107        assert!(result.is_err());
1108        assert_eq!(checker.nodes, 0);
1109        assert_eq!(checker.payload_bytes, 0);
1110        assert_eq!(checker.output_bytes, 0);
1111    }
1112}