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(
252        &mut self,
253        value: &Value,
254    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
255        self.transaction(|checker| checker.check_value_at(value, 1))
256    }
257
258    /// Checks and accumulates one homogeneous collection at the payload root.
259    ///
260    /// Returns the first exceeded JSON resource limit. If checking fails, the
261    /// accumulated state is restored to its value before this call.
262    ///
263    /// # Parameters
264    ///
265    /// `values` is the borrowed collection to check as a root array.
266    ///
267    /// # Returns
268    ///
269    /// `Ok(())` when all applicable measurements are admitted.
270    ///
271    /// # Errors
272    ///
273    /// Returns a measured budget error when a per-value or cumulative resource
274    /// limit is exceeded, or when a cumulative counter overflows.
275    #[inline]
276    pub fn check_values(
277        &mut self,
278        values: &MultiValues,
279    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
280        self.transaction(|checker| checker.check_values_at(values, 1))
281    }
282
283    /// Checks and accumulates an explicit scalar-or-collection payload.
284    ///
285    /// Returns the first exceeded JSON resource limit. If checking fails, the
286    /// accumulated state is restored to its value before this call.
287    ///
288    /// # Parameters
289    ///
290    /// `value` selects a borrowed scalar or collection payload.
291    ///
292    /// # Returns
293    ///
294    /// `Ok(())` when all applicable measurements are admitted.
295    ///
296    /// # Errors
297    ///
298    /// Returns a measured budget error when a per-value or cumulative resource
299    /// limit is exceeded, or when a cumulative counter overflows.
300    #[inline]
301    pub fn check_container(
302        &mut self,
303        value: &ValueContainer,
304    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
305        self.transaction(|checker| match value {
306            ValueContainer::Scalar(value) => checker.check_value_at(value, 1),
307            ValueContainer::Collection(values) => checker.check_values_at(values, 1),
308        })
309    }
310
311    /// Executes one public check as a transaction over cumulative counters.
312    ///
313    /// Successful checks retain their measurements. An error restores all
314    /// counters and returns the original budget error unchanged.
315    ///
316    /// # Type Parameters
317    ///
318    /// `F` is the one-shot fallible check applied to the candidate.
319    ///
320    /// # Parameters
321    ///
322    /// `check` runs once against a cloned candidate; only success commits it.
323    ///
324    /// # Returns
325    ///
326    /// `Ok(())` when all applicable measurements are admitted.
327    ///
328    /// # Errors
329    ///
330    /// Returns the first resource-limit or quantity-overflow error encountered.
331    ///
332    /// # Panics
333    ///
334    /// Propagates a panic from `check` without committing candidate counters.
335    fn transaction<F>(&mut self, check: F) -> Result<(), MeasuredBudgetError<JsonResource, usize>>
336    where
337        F: FnOnce(&mut Self) -> Result<(), MeasuredBudgetError<JsonResource, usize>>,
338    {
339        let mut candidate = self.clone();
340        match check(&mut candidate) {
341            Ok(()) => {
342                *self = candidate;
343                Ok(())
344            }
345            Err(error) => Err(error),
346        }
347    }
348
349    /// Checks a value at its supplied nesting depth and records its
350    /// measurements.
351    ///
352    /// # Parameters
353    ///
354    /// `value` is the borrowed node; `depth` is its one-based nesting depth.
355    ///
356    /// # Returns
357    ///
358    /// `Ok(())` when all applicable measurements are admitted.
359    ///
360    /// # Errors
361    ///
362    /// Returns the first resource-limit or quantity-overflow error encountered.
363    #[inline]
364    fn check_value_at(
365        &mut self,
366        value: &Value,
367        depth: usize,
368    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
369        self.check_view_at(value.view(), depth)
370    }
371
372    /// Dispatches a borrowed value to the measurement strategy for its variant.
373    ///
374    /// # Parameters
375    ///
376    /// `value` is the borrowed variant view; `depth` is its one-based nesting
377    /// depth.
378    ///
379    /// # Returns
380    ///
381    /// `Ok(())` when all applicable measurements are admitted.
382    ///
383    /// # Errors
384    ///
385    /// Returns the first resource-limit or quantity-overflow error encountered.
386    fn check_view_at(
387        &mut self,
388        value: ValueRef<'_>,
389        depth: usize,
390    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
391        match value_ref_preflight_strategy(value) {
392            WirePreflightStrategy::Boolean => match value {
393                ValueRef::Bool(_) => self.admit(JsonMeasurement::Boolean { depth }, 1, 0),
394                _ => unreachable!("table strategy and ValueRef variant diverged"),
395            },
396            WirePreflightStrategy::BorrowedText => match value {
397                ValueRef::Char(value) => self.admit_string(depth, value.len_utf8()),
398                ValueRef::String(value) => self.admit_string(depth, value.len()),
399                #[cfg(feature = "url")]
400                ValueRef::Url(value) => self.admit_string(depth, value.as_str().len()),
401                _ => unreachable!("table strategy and ValueRef variant diverged"),
402            },
403            WirePreflightStrategy::JsonInteger => match value {
404                ValueRef::Int8(value) => self.admit_number(depth, decimal_len(value as i128)),
405                ValueRef::Int16(value) => self.admit_number(depth, decimal_len(value as i128)),
406                ValueRef::Int32(value) => self.admit_number(depth, decimal_len(value as i128)),
407                ValueRef::Int64(value) => self.admit_number(depth, decimal_len(value as i128)),
408                ValueRef::UInt8(value) => {
409                    self.admit_number(depth, unsigned_decimal_len(value as u128))
410                }
411                ValueRef::UInt16(value) => {
412                    self.admit_number(depth, unsigned_decimal_len(value as u128))
413                }
414                ValueRef::UInt32(value) => {
415                    self.admit_number(depth, unsigned_decimal_len(value as u128))
416                }
417                ValueRef::UInt64(value) => {
418                    self.admit_number(depth, unsigned_decimal_len(value as u128))
419                }
420                _ => unreachable!("table strategy and ValueRef variant diverged"),
421            },
422            WirePreflightStrategy::DecimalText => match value {
423                ValueRef::Int128(value) => self.admit_string(depth, decimal_len(value)),
424                ValueRef::UInt128(value) => self.admit_string(depth, unsigned_decimal_len(value)),
425                _ => unreachable!("table strategy and ValueRef variant diverged"),
426            },
427            WirePreflightStrategy::UnsetText => match value {
428                ValueRef::Unset(data_type) => {
429                    let tag = WireDataTypeV1::from(data_type).tag();
430                    self.admit_string(depth, tag.len())
431                }
432                _ => unreachable!("table strategy and ValueRef variant diverged"),
433            },
434            WirePreflightStrategy::JsonFloat => match value {
435                ValueRef::Float32(value) => self.admit_number(
436                    depth,
437                    if value.is_finite() {
438                        json_float32_len(value)
439                    } else {
440                        1
441                    },
442                ),
443                ValueRef::Float64(value) => self.admit_number(
444                    depth,
445                    if value.is_finite() {
446                        json_float64_len(value)
447                    } else {
448                        1
449                    },
450                ),
451                _ => unreachable!("table strategy and ValueRef variant diverged"),
452            },
453            WirePreflightStrategy::BigIntegerText => {
454                #[cfg(feature = "big-integer")]
455                {
456                    match value {
457                        ValueRef::BigInteger(value) => {
458                            self.admit_string(depth, bigint_digits(value))
459                        }
460                        _ => unreachable!("table strategy and ValueRef variant diverged"),
461                    }
462                }
463                #[cfg(not(feature = "big-integer"))]
464                {
465                    unreachable!("big-integer strategy is unavailable without its feature")
466                }
467            }
468            WirePreflightStrategy::DecimalObject => {
469                #[cfg(feature = "big-decimal")]
470                {
471                    match value {
472                        ValueRef::BigDecimal(value) => {
473                            let (coefficient, scale) = value.as_bigint_and_scale();
474                            self.admit_object_with_keys(
475                                depth,
476                                [
477                                    ("coefficient", bigint_digits(coefficient.as_ref()), true),
478                                    ("scale", decimal_len(scale as i128), false),
479                                ],
480                            )
481                        }
482                        _ => unreachable!("table strategy and ValueRef variant diverged"),
483                    }
484                }
485                #[cfg(not(feature = "big-decimal"))]
486                {
487                    unreachable!("big-decimal strategy is unavailable without its feature")
488                }
489            }
490            WirePreflightStrategy::TemporalText => match value {
491                #[cfg(feature = "chrono")]
492                ValueRef::Date(_)
493                | ValueRef::Time(_)
494                | ValueRef::DateTime(_)
495                | ValueRef::Instant(_) => self.admit_string(depth, 0),
496                _ => unreachable!("table strategy and ValueRef variant diverged"),
497            },
498            WirePreflightStrategy::DurationObject => match value {
499                ValueRef::Duration(value) => self.admit_duration(depth, *value),
500                _ => unreachable!("table strategy and ValueRef variant diverged"),
501            },
502            WirePreflightStrategy::StringMap => match value {
503                ValueRef::StringMap(value) => self.check_string_map(value, depth),
504                _ => unreachable!("table strategy and ValueRef variant diverged"),
505            },
506            #[cfg(feature = "json")]
507            WirePreflightStrategy::JsonTree => match value {
508                ValueRef::Json(value) => self.check_json(value, depth),
509                _ => unreachable!("table strategy and ValueRef variant diverged"),
510            },
511        }
512    }
513
514    /// Accounts for a homogeneous collection and checks each item one level
515    /// deeper.
516    ///
517    /// # Parameters
518    ///
519    /// `values` supplies the array items; `depth` is the array nesting depth.
520    ///
521    /// # Returns
522    ///
523    /// `Ok(())` when all applicable measurements are admitted.
524    ///
525    /// # Errors
526    ///
527    /// Returns the first resource-limit or quantity-overflow error encountered.
528    fn check_values_at(
529        &mut self,
530        values: &MultiValues,
531        depth: usize,
532    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
533        self.admit_array(depth, values.len())?;
534        let view = values.view();
535        for index in 0..view.len() {
536            if let Some(value) = view.get(index) {
537                self.check_view_at(value, depth + 1)?;
538            }
539        }
540        Ok(())
541    }
542
543    /// Accounts for a string map and checks each key and value against its
544    /// limits.
545    ///
546    /// # Parameters
547    ///
548    /// `map` supplies borrowed keys and values; `depth` is the object nesting
549    /// depth.
550    ///
551    /// # Returns
552    ///
553    /// `Ok(())` when all applicable measurements are admitted.
554    ///
555    /// # Errors
556    ///
557    /// Returns the first resource-limit or quantity-overflow error encountered.
558    fn check_string_map(
559        &mut self,
560        map: &HashMap<String, String>,
561        depth: usize,
562    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
563        self.admit_object(depth, map.len())?;
564        for (key, value) in map {
565            self.admit_key(key)?;
566            self.admit_string(depth + 1, value.len())?;
567        }
568        Ok(())
569    }
570
571    /// Iteratively accounts for a JSON tree without recursively walking nested
572    /// values.
573    ///
574    /// # Parameters
575    ///
576    /// `value` is the borrowed tree root; `depth` is its one-based nesting
577    /// depth.
578    ///
579    /// # Returns
580    ///
581    /// `Ok(())` when all applicable measurements are admitted.
582    ///
583    /// # Errors
584    ///
585    /// Returns the first resource-limit or quantity-overflow error encountered.
586    #[cfg(feature = "json")]
587    fn check_json(
588        &mut self,
589        value: &JsonValue,
590        depth: usize,
591    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
592        let mut frames = vec![Frame::Visit(value, depth)];
593        while let Some(frame) = frames.pop() {
594            match frame {
595                Frame::Visit(value, depth) => match value {
596                    JsonValue::Null => self.admit(JsonMeasurement::Null { depth }, 1, 0)?,
597                    JsonValue::Bool(_) => self.admit(JsonMeasurement::Boolean { depth }, 1, 0)?,
598                    JsonValue::Number(value) => {
599                        let mut writer = JsonLengthWriter::default();
600                        to_writer(&mut writer, value)
601                            .expect("JSON Number serialization cannot fail");
602                        self.admit_number(depth, writer.len)?;
603                    }
604                    JsonValue::String(value) => self.admit_string(depth, value.len())?,
605                    JsonValue::Array(values) => {
606                        self.admit_array(depth, values.len())?;
607                        frames.push(Frame::Array(values.iter(), depth + 1));
608                    }
609                    JsonValue::Object(values) => {
610                        self.admit_object(depth, values.len())?;
611                        frames.push(Frame::Object(values.iter(), depth + 1));
612                    }
613                },
614                Frame::Array(mut values, depth) => {
615                    if let Some(value) = values.next() {
616                        frames.push(Frame::Array(values, depth));
617                        frames.push(Frame::Visit(value, depth));
618                    }
619                }
620                Frame::Object(mut values, depth) => {
621                    if let Some((key, value)) = values.next() {
622                        self.admit_key(key)?;
623                        frames.push(Frame::Object(values, depth));
624                        frames.push(Frame::Visit(value, depth));
625                    }
626                }
627            }
628        }
629        Ok(())
630    }
631
632    /// Accounts for a JSON string, including its quotes in the output estimate.
633    ///
634    /// # Parameters
635    ///
636    /// `depth` is the node depth; `bytes` is its unescaped UTF-8 payload
637    /// length.
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_string(
647        &mut self,
648        depth: usize,
649        bytes: usize,
650    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
651        let output = bytes
652            .checked_add(2)
653            .ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
654        self.admit(JsonMeasurement::String { depth, bytes }, output, bytes)
655    }
656
657    /// Accounts for a JSON number using its conservative encoded byte length.
658    ///
659    /// # Parameters
660    ///
661    /// `depth` is the node depth; `bytes` is its encoded number length lower
662    /// bound.
663    ///
664    /// # Returns
665    ///
666    /// `Ok(())` when all applicable measurements are admitted.
667    ///
668    /// # Errors
669    ///
670    /// Returns the first resource-limit or quantity-overflow error encountered.
671    #[inline]
672    fn admit_number(
673        &mut self,
674        depth: usize,
675        bytes: usize,
676    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
677        self.admit(JsonMeasurement::Number { depth, bytes }, bytes, bytes)
678    }
679
680    /// Accounts for array punctuation without charging it to payload bytes.
681    ///
682    /// # Parameters
683    ///
684    /// `depth` is the array depth; `items` is its element count.
685    ///
686    /// # Returns
687    ///
688    /// `Ok(())` when all applicable measurements are admitted.
689    ///
690    /// # Errors
691    ///
692    /// Returns the first resource-limit or quantity-overflow error encountered.
693    fn admit_array(
694        &mut self,
695        depth: usize,
696        items: usize,
697    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
698        let output = items
699            .saturating_sub(1)
700            .checked_add(2)
701            .ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
702        self.admit(JsonMeasurement::Array { depth, items }, output, 0)
703    }
704
705    /// Accounts for object punctuation without charging it to payload bytes.
706    ///
707    /// # Parameters
708    ///
709    /// `depth` is the object depth; `entries` is its member count.
710    ///
711    /// # Returns
712    ///
713    /// `Ok(())` when all applicable measurements are admitted.
714    ///
715    /// # Errors
716    ///
717    /// Returns the first resource-limit or quantity-overflow error encountered.
718    fn admit_object(
719        &mut self,
720        depth: usize,
721        entries: usize,
722    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
723        let output = entries
724            .checked_add(entries.saturating_sub(1))
725            .and_then(|value| value.checked_add(2))
726            .ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
727        self.admit(JsonMeasurement::Object { depth, entries }, output, 0)
728    }
729
730    /// Accounts for the object representation of a duration.
731    ///
732    /// # Parameters
733    ///
734    /// `depth` is the object depth; `value` supplies seconds and nanoseconds.
735    ///
736    /// # Returns
737    ///
738    /// `Ok(())` when all applicable measurements are admitted.
739    ///
740    /// # Errors
741    ///
742    /// Returns the first resource-limit or quantity-overflow error encountered.
743    #[inline]
744    fn admit_duration(
745        &mut self,
746        depth: usize,
747        value: Duration,
748    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
749        self.admit_object_with_keys(
750            depth,
751            [
752                ("secs", unsigned_decimal_len(value.as_secs() as u128), false),
753                (
754                    "nanos",
755                    unsigned_decimal_len(value.subsec_nanos() as u128),
756                    false,
757                ),
758            ],
759        )
760    }
761
762    /// Accounts for a fixed-key object and each field's scalar value.
763    ///
764    /// # Type Parameters
765    ///
766    /// `N` is the fixed number of object fields.
767    ///
768    /// # Parameters
769    ///
770    /// `depth` is the object depth. Each `fields` tuple contains a key,
771    /// its value length, and whether the value is a JSON string.
772    ///
773    /// # Returns
774    ///
775    /// `Ok(())` when all applicable measurements are admitted.
776    ///
777    /// # Errors
778    ///
779    /// Returns the first resource-limit or quantity-overflow error encountered.
780    fn admit_object_with_keys<const N: usize>(
781        &mut self,
782        depth: usize,
783        fields: [(&str, usize, bool); N],
784    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
785        self.admit_object(depth, N)?;
786        for (key, value, is_string) in fields {
787            self.admit_key(key)?;
788            if is_string {
789                self.admit_string(depth + 1, value)?;
790            } else {
791                self.admit_number(depth + 1, value)?;
792            }
793        }
794        Ok(())
795    }
796
797    /// Accounts for one object key without adding a JSON value node.
798    ///
799    /// # Parameters
800    ///
801    /// `key` is borrowed UTF-8 object-key text, excluding JSON quotes.
802    ///
803    /// # Returns
804    ///
805    /// `Ok(())` when all applicable measurements are admitted.
806    ///
807    /// # Errors
808    ///
809    /// Returns a measured error for a key limit, cumulative limit, or overflow.
810    fn admit_key(&mut self, key: &str) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
811        self.check_point(JsonMeasurement::Key { bytes: key.len() })?;
812        let output = key
813            .len()
814            .checked_add(2)
815            .ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
816        self.payload_bytes = self
817            .payload_bytes
818            .checked_add(key.len())
819            .ok_or_else(|| self.quantity_error(JsonResource::PayloadBytes))?;
820        self.output_bytes = self
821            .output_bytes
822            .checked_add(output)
823            .ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
824        self.check_cumulative(
825            JsonResource::PayloadBytes,
826            self.payload_bytes,
827            self.limits.value_limits().max_payload_bytes(),
828        )?;
829        self.check_cumulative(
830            JsonResource::OutputBytes,
831            self.output_bytes,
832            self.limits.max_output_bytes(),
833        )
834    }
835
836    /// Checks one measurement and adds its node, payload, and output lower
837    /// bounds.
838    ///
839    /// # Parameters
840    ///
841    /// `measurement` describes the node; `output` and `payload` are byte lower
842    /// bounds added to their respective cumulative counters.
843    ///
844    /// # Returns
845    ///
846    /// `Ok(())` when all applicable measurements are admitted.
847    ///
848    /// # Errors
849    ///
850    /// Returns the first resource-limit or quantity-overflow error encountered.
851    fn admit(
852        &mut self,
853        measurement: JsonMeasurement,
854        output: usize,
855        payload: usize,
856    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
857        self.check_point(measurement)?;
858        self.nodes = self
859            .nodes
860            .checked_add(1)
861            .ok_or_else(|| self.quantity_error(JsonResource::Nodes))?;
862        self.payload_bytes = self
863            .payload_bytes
864            .checked_add(payload)
865            .ok_or_else(|| self.quantity_error(JsonResource::PayloadBytes))?;
866        self.output_bytes = self
867            .output_bytes
868            .checked_add(output)
869            .ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
870        self.check_cumulative(
871            JsonResource::Nodes,
872            self.nodes,
873            self.limits.value_limits().max_nodes(),
874        )?;
875        self.check_cumulative(
876            JsonResource::PayloadBytes,
877            self.payload_bytes,
878            self.limits.value_limits().max_payload_bytes(),
879        )?;
880        self.check_cumulative(
881            JsonResource::OutputBytes,
882            self.output_bytes,
883            self.limits.max_output_bytes(),
884        )
885    }
886
887    /// Applies the configured per-value limits to one measurement.
888    ///
889    /// # Parameters
890    ///
891    /// `measurement` describes the individual resource observation to validate.
892    ///
893    /// # Returns
894    ///
895    /// `Ok(())` when all applicable measurements are admitted.
896    ///
897    /// # Errors
898    ///
899    /// Returns the first resource-limit or quantity-overflow error encountered.
900    #[inline]
901    fn check_point(
902        &self,
903        measurement: JsonMeasurement,
904    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
905        self.limits.value_limits().check_point(measurement)
906    }
907
908    /// Rejects a cumulative counter that exceeds its optional configured
909    /// maximum.
910    ///
911    /// # Parameters
912    ///
913    /// `resource` identifies the counter; `observed` is its lower bound.
914    /// `maximum` enables a ceiling with `Some`, or leaves it unlimited with
915    /// `None`.
916    ///
917    /// # Returns
918    ///
919    /// `Ok(())` when all applicable measurements are admitted.
920    ///
921    /// # Errors
922    ///
923    /// Returns the first resource-limit or quantity-overflow error encountered.
924    fn check_cumulative(
925        &self,
926        resource: JsonResource,
927        observed: usize,
928        maximum: Option<usize>,
929    ) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
930        match maximum {
931            Some(maximum) if observed > maximum => {
932                Err(self.lower_bound_error(resource, observed, maximum))
933            }
934            _ => Ok(()),
935        }
936    }
937
938    /// Builds a budget error that reports the observed value as a lower bound.
939    ///
940    /// # Parameters
941    ///
942    /// `resource` identifies the exceeded budget; `observed` is the lower
943    /// bound and `maximum` is the configured ceiling.
944    ///
945    /// # Returns
946    ///
947    /// A measured budget error describing the failed resource.
948    #[inline]
949    fn lower_bound_error(
950        &self,
951        resource: JsonResource,
952        observed: usize,
953        maximum: usize,
954    ) -> MeasuredBudgetError<JsonResource, usize> {
955        MeasuredBudgetError::Budget(BudgetError::LimitExceeded {
956            resource,
957            observed: Observation::AtLeast(observed),
958            maximum,
959        })
960    }
961
962    /// Builds a quantity error for arithmetic that cannot fit in `usize`.
963    ///
964    /// # Parameters
965    ///
966    /// `resource` identifies the counter whose arithmetic overflowed.
967    ///
968    /// # Returns
969    ///
970    /// A measured budget error describing the failed resource.
971    #[inline]
972    fn quantity_error(&self, resource: JsonResource) -> MeasuredBudgetError<JsonResource, usize> {
973        MeasuredBudgetError::quantity(
974            resource,
975            QuantityConversionError::new(QuantityMeasurement::Usize(usize::MAX), "usize"),
976        )
977    }
978}
979
980/// Estimates the decimal digit count, including a sign when the value is
981/// negative.
982///
983/// # Parameters
984///
985/// `value` is the borrowed integer whose decimal text is estimated.
986///
987/// # Returns
988///
989/// A conservative decimal text length including any minus sign.
990#[cfg(any(feature = "big-integer", feature = "big-decimal"))]
991#[must_use]
992#[inline]
993fn bigint_digits(value: &BigInt) -> usize {
994    if value.sign() == Sign::NoSign {
995        1
996    } else {
997        ((value.bits().saturating_sub(1)) / 4 + 1) as usize
998            + usize::from(value.sign() == Sign::Minus)
999    }
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004    use std::collections::HashMap;
1005    use std::panic::AssertUnwindSafe;
1006    use std::panic::catch_unwind;
1007
1008    use qubit_budget::MeasuredBudgetError;
1009    use qubit_budget::json::JsonEncodeLimits;
1010    use qubit_budget::json::JsonMeasurement;
1011    use qubit_budget::json::JsonResource;
1012
1013    use super::ValueWireEncodePreflight;
1014    use crate::Value;
1015
1016    /// Verifies a counter overflow is reported as a quantity error.
1017    fn assert_overflow(error: MeasuredBudgetError<JsonResource, usize>) {
1018        assert!(
1019            matches!(error, MeasuredBudgetError::Quantity { .. }),
1020            "expected a quantity overflow, got {error:?}",
1021        );
1022    }
1023
1024    #[test]
1025    fn test_check_value_reports_node_counter_overflow_and_rolls_back() {
1026        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
1027        checker.nodes = usize::MAX;
1028
1029        let error = checker
1030            .check_value(&Value::Bool(true))
1031            .expect_err("the node counter cannot exceed usize::MAX");
1032
1033        assert_overflow(error);
1034        assert_eq!(checker.nodes, usize::MAX);
1035        assert_eq!(checker.payload_bytes, 0);
1036        assert_eq!(checker.output_bytes, 0);
1037    }
1038
1039    #[test]
1040    fn test_check_value_reports_payload_counter_overflow_and_rolls_back() {
1041        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
1042        checker.payload_bytes = usize::MAX;
1043
1044        let error = checker
1045            .check_value(&Value::String("x".to_owned()))
1046            .expect_err("the payload counter cannot exceed usize::MAX");
1047
1048        assert_overflow(error);
1049        assert_eq!(checker.nodes, 0);
1050        assert_eq!(checker.payload_bytes, usize::MAX);
1051        assert_eq!(checker.output_bytes, 0);
1052    }
1053
1054    #[test]
1055    fn test_check_value_reports_key_payload_counter_overflow_and_rolls_back() {
1056        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
1057        checker.payload_bytes = usize::MAX;
1058
1059        let error = checker
1060            .check_value(&Value::StringMap(HashMap::from([(
1061                "x".to_owned(),
1062                String::new(),
1063            )])))
1064            .expect_err("the key payload counter cannot exceed usize::MAX");
1065
1066        assert_overflow(error);
1067        assert_eq!(checker.nodes, 0);
1068        assert_eq!(checker.payload_bytes, usize::MAX);
1069        assert_eq!(checker.output_bytes, 0);
1070    }
1071
1072    #[test]
1073    fn test_check_value_reports_key_output_counter_overflow_and_rolls_back() {
1074        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
1075        checker.output_bytes = usize::MAX - 3;
1076
1077        let error = checker
1078            .check_value(&Value::StringMap(HashMap::from([(
1079                "x".to_owned(),
1080                String::new(),
1081            )])))
1082            .expect_err("the key output counter cannot exceed usize::MAX");
1083
1084        assert_overflow(error);
1085        assert_eq!(checker.nodes, 0);
1086        assert_eq!(checker.payload_bytes, 0);
1087        assert_eq!(checker.output_bytes, usize::MAX - 3);
1088    }
1089
1090    #[test]
1091    fn test_check_value_reports_output_counter_overflow_and_rolls_back() {
1092        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
1093        checker.output_bytes = usize::MAX;
1094
1095        let error = checker
1096            .check_value(&Value::Bool(true))
1097            .expect_err("the output counter cannot exceed usize::MAX");
1098
1099        assert_overflow(error);
1100        assert_eq!(checker.nodes, 0);
1101        assert_eq!(checker.payload_bytes, 0);
1102        assert_eq!(checker.output_bytes, usize::MAX);
1103    }
1104
1105    #[test]
1106    fn test_admit_helpers_report_arithmetic_overflow_without_large_values() {
1107        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
1108        assert_overflow(
1109            checker
1110                .admit_string(0, usize::MAX)
1111                .expect_err("string output length must not overflow"),
1112        );
1113
1114        assert_overflow(
1115            checker
1116                .admit_array(0, usize::MAX)
1117                .expect_err("array output punctuation must not overflow"),
1118        );
1119
1120        assert_overflow(
1121            checker
1122                .admit_object(0, usize::MAX)
1123                .expect_err("object output punctuation must not overflow"),
1124        );
1125
1126        checker.payload_bytes = usize::MAX;
1127        assert_overflow(
1128            checker
1129                .admit_key("x")
1130                .expect_err("key payload accumulation must not overflow"),
1131        );
1132
1133        checker.payload_bytes = 0;
1134        checker.output_bytes = usize::MAX - 2;
1135        assert_overflow(
1136            checker
1137                .admit_key("x")
1138                .expect_err("key output accumulation must not overflow"),
1139        );
1140    }
1141
1142    #[test]
1143    fn test_admit_reports_each_counter_arithmetic_overflow() {
1144        let measurement = JsonMeasurement::Boolean { depth: 0 };
1145
1146        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
1147        checker.nodes = usize::MAX;
1148        assert_overflow(
1149            checker
1150                .admit(measurement, 1, 0)
1151                .expect_err("node accumulation must not overflow"),
1152        );
1153
1154        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
1155        checker.payload_bytes = usize::MAX;
1156        assert_overflow(
1157            checker
1158                .admit(measurement, 1, 1)
1159                .expect_err("payload accumulation must not overflow"),
1160        );
1161
1162        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
1163        checker.output_bytes = usize::MAX;
1164        assert_overflow(
1165            checker
1166                .admit(measurement, 1, 0)
1167                .expect_err("output accumulation must not overflow"),
1168        );
1169    }
1170
1171    #[test]
1172    fn test_transaction_does_not_commit_counters_when_check_panics() {
1173        let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
1174        let result = catch_unwind(AssertUnwindSafe(|| {
1175            checker.transaction(|candidate| {
1176                candidate.nodes = 17;
1177                candidate.payload_bytes = 23;
1178                panic!("simulated preflight panic");
1179            })
1180        }));
1181
1182        assert!(result.is_err());
1183        assert_eq!(checker.nodes, 0);
1184        assert_eq!(checker.payload_bytes, 0);
1185        assert_eq!(checker.output_bytes, 0);
1186    }
1187}