use std::collections::HashMap;
use std::time::Duration;
#[cfg(any(feature = "big-integer", feature = "big-decimal"))]
use num_bigint::BigInt;
#[cfg(any(feature = "big-integer", feature = "big-decimal"))]
use num_bigint::Sign;
use qubit_budget::BudgetError;
use qubit_budget::MeasuredBudgetError;
use qubit_budget::Observation;
use qubit_budget::QuantityConversionError;
use qubit_budget::QuantityMeasurement;
use qubit_budget::json::JsonEncodeLimits;
use qubit_budget::json::JsonEncodeLimits as JsonEncodeLimitsU64;
use qubit_budget::json::JsonMeasurement;
use qubit_budget::json::JsonResource;
use qubit_budget::json::JsonValueLimits;
use serde_json::Value as JsonValue;
use serde_json::to_writer;
use self::internal::frame::Frame;
use self::internal::json_length_writer::JsonLengthWriter;
use self::internal::wire_scalar_measurement::decimal_len;
use self::internal::wire_scalar_measurement::json_float32_len;
use self::internal::wire_scalar_measurement::json_float64_len;
use self::internal::wire_scalar_measurement::unsigned_decimal_len;
use super::internal::WireDataTypeV1;
use super::internal::WirePreflightStrategy;
use crate::MultiValues;
use crate::Value;
use crate::ValueContainer;
use crate::ValueRef;
mod internal;
macro_rules! define_value_ref_preflight_strategy {
(
$($arg:expr),*;
$(
(
[$($cfg:meta),*],
$variant:ident,
$type:ty,
$_data_type:expr,
$_materialization:ident,
$_json_class:ident,
$_number_projection:ident,
$_value_doc:literal,
$_multi_doc:literal,
[$($scalar_attr:meta),*],
[$($collection_attr:meta),*],
$_tag:literal,
$_wire_preflight:ident
)
),+ $(,)?
) => {
#[must_use]
fn value_ref_preflight_strategy(value: ValueRef<'_>) -> WirePreflightStrategy {
match value {
ValueRef::Unset(_) => WirePreflightStrategy::UnsetText,
$(
$(#[$cfg])*
ValueRef::$variant(_) => WireDataTypeV1::$variant.preflight_strategy(),
)+
}
}
};
}
for_each_value_type!(define_value_ref_preflight_strategy);
#[derive(Debug, Clone)]
pub struct ValueWireEncodePreflight {
limits: JsonEncodeLimits,
nodes: usize,
payload_bytes: usize,
output_bytes: usize,
}
#[inline]
#[must_use]
fn saturating_u64_to_usize(value: u64) -> usize {
usize::try_from(value).unwrap_or(usize::MAX)
}
impl ValueWireEncodePreflight {
#[must_use]
#[inline]
pub fn new(limits: JsonEncodeLimits) -> Self {
Self {
limits,
nodes: 0,
payload_bytes: 0,
output_bytes: 0,
}
}
#[must_use]
#[inline]
pub fn new_value_limits(limits: JsonValueLimits) -> Self {
let mut checker = Self::new(JsonEncodeLimits::new());
checker.limits = JsonEncodeLimits::builder().value_limits(limits).build();
checker
}
#[must_use]
pub fn new_u64_limits(limits: JsonEncodeLimitsU64<JsonResource, u64>) -> Self {
let value = limits.value_limits();
let mut builder = JsonEncodeLimits::builder();
if let Some(limit) = limits.max_output_bytes() {
builder = builder.max_output_bytes(saturating_u64_to_usize(limit));
}
if let Some(limit) = value.max_depth() {
builder = builder.max_depth(saturating_u64_to_usize(limit));
}
if let Some(limit) = value.max_nodes() {
builder = builder.max_nodes(saturating_u64_to_usize(limit));
}
if let Some(limit) = value.max_sequence_items() {
builder = builder.max_sequence_items(saturating_u64_to_usize(limit));
}
if let Some(limit) = value.max_map_entries() {
builder = builder.max_map_entries(saturating_u64_to_usize(limit));
}
if let Some(limit) = value.max_key_bytes() {
builder = builder.max_key_bytes(saturating_u64_to_usize(limit));
}
if let Some(limit) = value.max_string_bytes() {
builder = builder.max_string_bytes(saturating_u64_to_usize(limit));
}
if let Some(limit) = value.max_number_bytes() {
builder = builder.max_number_bytes(saturating_u64_to_usize(limit));
}
if let Some(limit) = value.max_payload_bytes() {
builder = builder.max_payload_bytes(saturating_u64_to_usize(limit));
}
Self::new(builder.build())
}
#[inline]
pub fn check_value(
&mut self,
value: &Value,
) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
self.transaction(|checker| checker.check_value_at(value, 1))
}
#[inline]
pub fn check_values(
&mut self,
values: &MultiValues,
) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
self.transaction(|checker| checker.check_values_at(values, 1))
}
#[inline]
pub fn check_container(
&mut self,
value: &ValueContainer,
) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
self.transaction(|checker| match value {
ValueContainer::Scalar(value) => checker.check_value_at(value, 1),
ValueContainer::Collection(values) => checker.check_values_at(values, 1),
})
}
fn transaction<F>(&mut self, check: F) -> Result<(), MeasuredBudgetError<JsonResource, usize>>
where
F: FnOnce(&mut Self) -> Result<(), MeasuredBudgetError<JsonResource, usize>>,
{
let mut candidate = self.clone();
match check(&mut candidate) {
Ok(()) => {
*self = candidate;
Ok(())
}
Err(error) => Err(error),
}
}
#[inline]
fn check_value_at(
&mut self,
value: &Value,
depth: usize,
) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
self.check_view_at(value.view(), depth)
}
fn check_view_at(
&mut self,
value: ValueRef<'_>,
depth: usize,
) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
match value_ref_preflight_strategy(value) {
WirePreflightStrategy::Boolean => match value {
ValueRef::Bool(_) => self.admit(JsonMeasurement::Boolean { depth }, 1, 0),
_ => unreachable!("table strategy and ValueRef variant diverged"),
},
WirePreflightStrategy::BorrowedText => match value {
ValueRef::Char(value) => self.admit_string(depth, value.len_utf8()),
ValueRef::String(value) => self.admit_string(depth, value.len()),
#[cfg(feature = "url")]
ValueRef::Url(value) => self.admit_string(depth, value.as_str().len()),
_ => unreachable!("table strategy and ValueRef variant diverged"),
},
WirePreflightStrategy::JsonInteger => match value {
ValueRef::Int8(value) => self.admit_number(depth, decimal_len(value as i128)),
ValueRef::Int16(value) => self.admit_number(depth, decimal_len(value as i128)),
ValueRef::Int32(value) => self.admit_number(depth, decimal_len(value as i128)),
ValueRef::Int64(value) => self.admit_number(depth, decimal_len(value as i128)),
ValueRef::UInt8(value) => {
self.admit_number(depth, unsigned_decimal_len(value as u128))
}
ValueRef::UInt16(value) => {
self.admit_number(depth, unsigned_decimal_len(value as u128))
}
ValueRef::UInt32(value) => {
self.admit_number(depth, unsigned_decimal_len(value as u128))
}
ValueRef::UInt64(value) => {
self.admit_number(depth, unsigned_decimal_len(value as u128))
}
_ => unreachable!("table strategy and ValueRef variant diverged"),
},
WirePreflightStrategy::DecimalText => match value {
ValueRef::Int128(value) => self.admit_string(depth, decimal_len(value)),
ValueRef::UInt128(value) => self.admit_string(depth, unsigned_decimal_len(value)),
_ => unreachable!("table strategy and ValueRef variant diverged"),
},
WirePreflightStrategy::UnsetText => match value {
ValueRef::Unset(data_type) => {
let tag = WireDataTypeV1::from(data_type).tag();
self.admit_string(depth, tag.len())
}
_ => unreachable!("table strategy and ValueRef variant diverged"),
},
WirePreflightStrategy::JsonFloat => match value {
ValueRef::Float32(value) => self.admit_number(
depth,
if value.is_finite() {
json_float32_len(value)
} else {
1
},
),
ValueRef::Float64(value) => self.admit_number(
depth,
if value.is_finite() {
json_float64_len(value)
} else {
1
},
),
_ => unreachable!("table strategy and ValueRef variant diverged"),
},
WirePreflightStrategy::BigIntegerText => {
#[cfg(feature = "big-integer")]
{
match value {
ValueRef::BigInteger(value) => {
self.admit_string(depth, bigint_digits(value))
}
_ => unreachable!("table strategy and ValueRef variant diverged"),
}
}
#[cfg(not(feature = "big-integer"))]
{
unreachable!("big-integer strategy is unavailable without its feature")
}
}
WirePreflightStrategy::DecimalObject => {
#[cfg(feature = "big-decimal")]
{
match value {
ValueRef::BigDecimal(value) => {
let (coefficient, scale) = value.as_bigint_and_scale();
self.admit_object_with_keys(
depth,
[
("coefficient", bigint_digits(coefficient.as_ref()), true),
("scale", decimal_len(scale as i128), false),
],
)
}
_ => unreachable!("table strategy and ValueRef variant diverged"),
}
}
#[cfg(not(feature = "big-decimal"))]
{
unreachable!("big-decimal strategy is unavailable without its feature")
}
}
WirePreflightStrategy::TemporalText => match value {
#[cfg(feature = "chrono")]
ValueRef::Date(_)
| ValueRef::Time(_)
| ValueRef::DateTime(_)
| ValueRef::Instant(_) => self.admit_string(depth, 0),
_ => unreachable!("table strategy and ValueRef variant diverged"),
},
WirePreflightStrategy::DurationObject => match value {
ValueRef::Duration(value) => self.admit_duration(depth, *value),
_ => unreachable!("table strategy and ValueRef variant diverged"),
},
WirePreflightStrategy::StringMap => match value {
ValueRef::StringMap(value) => self.check_string_map(value, depth),
_ => unreachable!("table strategy and ValueRef variant diverged"),
},
#[cfg(feature = "json")]
WirePreflightStrategy::JsonTree => match value {
ValueRef::Json(value) => self.check_json(value, depth),
_ => unreachable!("table strategy and ValueRef variant diverged"),
},
}
}
fn check_values_at(
&mut self,
values: &MultiValues,
depth: usize,
) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
self.admit_array(depth, values.len())?;
let view = values.view();
for index in 0..view.len() {
if let Some(value) = view.get(index) {
self.check_view_at(value, depth + 1)?;
}
}
Ok(())
}
fn check_string_map(
&mut self,
map: &HashMap<String, String>,
depth: usize,
) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
self.admit_object(depth, map.len())?;
for (key, value) in map {
self.admit_key(key)?;
self.admit_string(depth + 1, value.len())?;
}
Ok(())
}
#[cfg(feature = "json")]
fn check_json(
&mut self,
value: &JsonValue,
depth: usize,
) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
let mut frames = vec![Frame::Visit(value, depth)];
while let Some(frame) = frames.pop() {
match frame {
Frame::Visit(value, depth) => match value {
JsonValue::Null => self.admit(JsonMeasurement::Null { depth }, 1, 0)?,
JsonValue::Bool(_) => self.admit(JsonMeasurement::Boolean { depth }, 1, 0)?,
JsonValue::Number(value) => {
let mut writer = JsonLengthWriter::default();
to_writer(&mut writer, value)
.expect("JSON Number serialization cannot fail");
self.admit_number(depth, writer.len)?;
}
JsonValue::String(value) => self.admit_string(depth, value.len())?,
JsonValue::Array(values) => {
self.admit_array(depth, values.len())?;
frames.push(Frame::Array(values.iter(), depth + 1));
}
JsonValue::Object(values) => {
self.admit_object(depth, values.len())?;
frames.push(Frame::Object(values.iter(), depth + 1));
}
},
Frame::Array(mut values, depth) => {
if let Some(value) = values.next() {
frames.push(Frame::Array(values, depth));
frames.push(Frame::Visit(value, depth));
}
}
Frame::Object(mut values, depth) => {
if let Some((key, value)) = values.next() {
self.admit_key(key)?;
frames.push(Frame::Object(values, depth));
frames.push(Frame::Visit(value, depth));
}
}
}
}
Ok(())
}
fn admit_string(
&mut self,
depth: usize,
bytes: usize,
) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
let output = bytes
.checked_add(2)
.ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
self.admit(JsonMeasurement::String { depth, bytes }, output, bytes)
}
#[inline]
fn admit_number(
&mut self,
depth: usize,
bytes: usize,
) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
self.admit(JsonMeasurement::Number { depth, bytes }, bytes, bytes)
}
fn admit_array(
&mut self,
depth: usize,
items: usize,
) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
let output = items
.saturating_sub(1)
.checked_add(2)
.ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
self.admit(JsonMeasurement::Array { depth, items }, output, 0)
}
fn admit_object(
&mut self,
depth: usize,
entries: usize,
) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
let output = entries
.checked_add(entries.saturating_sub(1))
.and_then(|value| value.checked_add(2))
.ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
self.admit(JsonMeasurement::Object { depth, entries }, output, 0)
}
#[inline]
fn admit_duration(
&mut self,
depth: usize,
value: Duration,
) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
self.admit_object_with_keys(
depth,
[
("secs", unsigned_decimal_len(value.as_secs() as u128), false),
(
"nanos",
unsigned_decimal_len(value.subsec_nanos() as u128),
false,
),
],
)
}
fn admit_object_with_keys<const N: usize>(
&mut self,
depth: usize,
fields: [(&str, usize, bool); N],
) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
self.admit_object(depth, N)?;
for (key, value, is_string) in fields {
self.admit_key(key)?;
if is_string {
self.admit_string(depth + 1, value)?;
} else {
self.admit_number(depth + 1, value)?;
}
}
Ok(())
}
fn admit_key(&mut self, key: &str) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
self.check_point(JsonMeasurement::Key { bytes: key.len() })?;
let output = key
.len()
.checked_add(2)
.ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
self.payload_bytes = self
.payload_bytes
.checked_add(key.len())
.ok_or_else(|| self.quantity_error(JsonResource::PayloadBytes))?;
self.output_bytes = self
.output_bytes
.checked_add(output)
.ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
self.check_cumulative(
JsonResource::PayloadBytes,
self.payload_bytes,
self.limits.value_limits().max_payload_bytes(),
)?;
self.check_cumulative(
JsonResource::OutputBytes,
self.output_bytes,
self.limits.max_output_bytes(),
)
}
fn admit(
&mut self,
measurement: JsonMeasurement,
output: usize,
payload: usize,
) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
self.check_point(measurement)?;
self.nodes = self
.nodes
.checked_add(1)
.ok_or_else(|| self.quantity_error(JsonResource::Nodes))?;
self.payload_bytes = self
.payload_bytes
.checked_add(payload)
.ok_or_else(|| self.quantity_error(JsonResource::PayloadBytes))?;
self.output_bytes = self
.output_bytes
.checked_add(output)
.ok_or_else(|| self.quantity_error(JsonResource::OutputBytes))?;
self.check_cumulative(
JsonResource::Nodes,
self.nodes,
self.limits.value_limits().max_nodes(),
)?;
self.check_cumulative(
JsonResource::PayloadBytes,
self.payload_bytes,
self.limits.value_limits().max_payload_bytes(),
)?;
self.check_cumulative(
JsonResource::OutputBytes,
self.output_bytes,
self.limits.max_output_bytes(),
)
}
#[inline]
fn check_point(
&self,
measurement: JsonMeasurement,
) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
self.limits.value_limits().check_point(measurement)
}
fn check_cumulative(
&self,
resource: JsonResource,
observed: usize,
maximum: Option<usize>,
) -> Result<(), MeasuredBudgetError<JsonResource, usize>> {
match maximum {
Some(maximum) if observed > maximum => {
Err(self.lower_bound_error(resource, observed, maximum))
}
_ => Ok(()),
}
}
#[inline]
fn lower_bound_error(
&self,
resource: JsonResource,
observed: usize,
maximum: usize,
) -> MeasuredBudgetError<JsonResource, usize> {
MeasuredBudgetError::Budget(BudgetError::LimitExceeded {
resource,
observed: Observation::AtLeast(observed),
maximum,
})
}
#[inline]
fn quantity_error(&self, resource: JsonResource) -> MeasuredBudgetError<JsonResource, usize> {
MeasuredBudgetError::quantity(
resource,
QuantityConversionError::new(QuantityMeasurement::Usize(usize::MAX), "usize"),
)
}
}
#[cfg(any(feature = "big-integer", feature = "big-decimal"))]
#[must_use]
#[inline]
fn bigint_digits(value: &BigInt) -> usize {
if value.sign() == Sign::NoSign {
1
} else {
((value.bits().saturating_sub(1)) / 4 + 1) as usize
+ usize::from(value.sign() == Sign::Minus)
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::panic::AssertUnwindSafe;
use std::panic::catch_unwind;
use qubit_budget::MeasuredBudgetError;
use qubit_budget::json::JsonEncodeLimits;
use qubit_budget::json::JsonMeasurement;
use qubit_budget::json::JsonResource;
use super::ValueWireEncodePreflight;
use crate::Value;
fn assert_overflow(error: MeasuredBudgetError<JsonResource, usize>) {
assert!(
matches!(error, MeasuredBudgetError::Quantity { .. }),
"expected a quantity overflow, got {error:?}",
);
}
#[test]
fn test_check_value_reports_node_counter_overflow_and_rolls_back() {
let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
checker.nodes = usize::MAX;
let error = checker
.check_value(&Value::Bool(true))
.expect_err("the node counter cannot exceed usize::MAX");
assert_overflow(error);
assert_eq!(checker.nodes, usize::MAX);
assert_eq!(checker.payload_bytes, 0);
assert_eq!(checker.output_bytes, 0);
}
#[test]
fn test_check_value_reports_payload_counter_overflow_and_rolls_back() {
let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
checker.payload_bytes = usize::MAX;
let error = checker
.check_value(&Value::String("x".to_owned()))
.expect_err("the payload counter cannot exceed usize::MAX");
assert_overflow(error);
assert_eq!(checker.nodes, 0);
assert_eq!(checker.payload_bytes, usize::MAX);
assert_eq!(checker.output_bytes, 0);
}
#[test]
fn test_check_value_reports_key_payload_counter_overflow_and_rolls_back() {
let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
checker.payload_bytes = usize::MAX;
let error = checker
.check_value(&Value::StringMap(HashMap::from([(
"x".to_owned(),
String::new(),
)])))
.expect_err("the key payload counter cannot exceed usize::MAX");
assert_overflow(error);
assert_eq!(checker.nodes, 0);
assert_eq!(checker.payload_bytes, usize::MAX);
assert_eq!(checker.output_bytes, 0);
}
#[test]
fn test_check_value_reports_key_output_counter_overflow_and_rolls_back() {
let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
checker.output_bytes = usize::MAX - 3;
let error = checker
.check_value(&Value::StringMap(HashMap::from([(
"x".to_owned(),
String::new(),
)])))
.expect_err("the key output counter cannot exceed usize::MAX");
assert_overflow(error);
assert_eq!(checker.nodes, 0);
assert_eq!(checker.payload_bytes, 0);
assert_eq!(checker.output_bytes, usize::MAX - 3);
}
#[test]
fn test_check_value_reports_output_counter_overflow_and_rolls_back() {
let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
checker.output_bytes = usize::MAX;
let error = checker
.check_value(&Value::Bool(true))
.expect_err("the output counter cannot exceed usize::MAX");
assert_overflow(error);
assert_eq!(checker.nodes, 0);
assert_eq!(checker.payload_bytes, 0);
assert_eq!(checker.output_bytes, usize::MAX);
}
#[test]
fn test_admit_helpers_report_arithmetic_overflow_without_large_values() {
let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
assert_overflow(
checker
.admit_string(0, usize::MAX)
.expect_err("string output length must not overflow"),
);
assert_overflow(
checker
.admit_array(0, usize::MAX)
.expect_err("array output punctuation must not overflow"),
);
assert_overflow(
checker
.admit_object(0, usize::MAX)
.expect_err("object output punctuation must not overflow"),
);
checker.payload_bytes = usize::MAX;
assert_overflow(
checker
.admit_key("x")
.expect_err("key payload accumulation must not overflow"),
);
checker.payload_bytes = 0;
checker.output_bytes = usize::MAX - 2;
assert_overflow(
checker
.admit_key("x")
.expect_err("key output accumulation must not overflow"),
);
}
#[test]
fn test_admit_reports_each_counter_arithmetic_overflow() {
let measurement = JsonMeasurement::Boolean { depth: 0 };
let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
checker.nodes = usize::MAX;
assert_overflow(
checker
.admit(measurement, 1, 0)
.expect_err("node accumulation must not overflow"),
);
let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
checker.payload_bytes = usize::MAX;
assert_overflow(
checker
.admit(measurement, 1, 1)
.expect_err("payload accumulation must not overflow"),
);
let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
checker.output_bytes = usize::MAX;
assert_overflow(
checker
.admit(measurement, 1, 0)
.expect_err("output accumulation must not overflow"),
);
}
#[test]
fn test_transaction_does_not_commit_counters_when_check_panics() {
let mut checker = ValueWireEncodePreflight::new(JsonEncodeLimits::new());
let result = catch_unwind(AssertUnwindSafe(|| {
checker.transaction(|candidate| {
candidate.nodes = 17;
candidate.payload_bytes = 23;
panic!("simulated preflight panic");
})
}));
assert!(result.is_err());
assert_eq!(checker.nodes, 0);
assert_eq!(checker.payload_bytes, 0);
assert_eq!(checker.output_bytes, 0);
}
}