use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(feature = "tracing")]
use std::time::Instant;
use serde::de::DeserializeOwned;
use serde::de::Error as _;
use super::depth::{DepthLimitedDeserializer, DepthState};
#[inline]
pub(super) fn trace_deser_error<T>(result: &Result<T, serde_json::Error>, context: &'static str) {
#[cfg(feature = "tracing")]
if let Err(ref e) = result {
tracing::debug!(error = %e, "{context}");
}
#[cfg(not(feature = "tracing"))]
{
let _ = (result, context);
}
}
#[cfg(feature = "tracing")]
pub(super) fn trace_json_outcome(
operation: &'static str,
mode: &'static str,
bo_type: &'static str,
input_len: Option<usize>,
output_len: Option<usize>,
start: Instant,
ok: bool,
) {
let elapsed_us = start.elapsed().as_micros() as u64;
tracing::debug!(
operation,
mode,
bo_type,
input_len,
output_len,
ok,
elapsed_us,
"bo4e json operation completed"
);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum LimitKind {
PayloadBytes,
NestingDepth,
ExtensionValueBytes,
ExtensionFieldCount,
ExtensionKeyLen,
}
impl LimitKind {
#[cfg(any(feature = "tracing", feature = "metrics"))]
const fn as_str(self) -> &'static str {
match self {
Self::PayloadBytes => "payload_bytes",
Self::NestingDepth => "nesting_depth",
Self::ExtensionValueBytes => "extension_value_bytes",
Self::ExtensionFieldCount => "extension_field_count",
Self::ExtensionKeyLen => "extension_key_len",
}
}
fn counter(self) -> &'static AtomicU64 {
match self {
Self::PayloadBytes => &JSON_LIMIT_HIT_PAYLOAD_BYTES,
Self::NestingDepth => &JSON_LIMIT_HIT_NESTING_DEPTH,
Self::ExtensionValueBytes => &JSON_LIMIT_HIT_EXTENSION_VALUE_BYTES,
Self::ExtensionFieldCount => &JSON_LIMIT_HIT_EXTENSION_FIELD_COUNT,
Self::ExtensionKeyLen => &JSON_LIMIT_HIT_EXTENSION_KEY_LEN,
}
}
}
pub(super) fn trace_limit_violation(kind: LimitKind, actual: usize, limit: usize) {
kind.counter().fetch_add(1, Ordering::Relaxed);
#[cfg(feature = "metrics")]
metrics::counter!("bo4e_json_limit_hit_total", "kind" => kind.as_str()).increment(1);
#[cfg(feature = "tracing")]
tracing::warn!(
kind = kind.as_str(),
actual,
limit,
"bo4e json parse limit exceeded"
);
#[cfg(not(any(feature = "tracing", feature = "metrics")))]
let _ = (actual, limit);
}
static JSON_LIMIT_HIT_PAYLOAD_BYTES: AtomicU64 = AtomicU64::new(0);
static JSON_LIMIT_HIT_NESTING_DEPTH: AtomicU64 = AtomicU64::new(0);
static JSON_LIMIT_HIT_EXTENSION_VALUE_BYTES: AtomicU64 = AtomicU64::new(0);
static JSON_LIMIT_HIT_EXTENSION_FIELD_COUNT: AtomicU64 = AtomicU64::new(0);
static JSON_LIMIT_HIT_EXTENSION_KEY_LEN: AtomicU64 = AtomicU64::new(0);
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct JsonLimitHitCounters {
pub payload_bytes: u64,
pub nesting_depth: u64,
pub extension_value_bytes: u64,
pub extension_field_count: u64,
pub extension_key_len: u64,
}
#[must_use]
pub fn json_limit_hit_counters() -> JsonLimitHitCounters {
JsonLimitHitCounters {
payload_bytes: JSON_LIMIT_HIT_PAYLOAD_BYTES.load(Ordering::Relaxed),
nesting_depth: JSON_LIMIT_HIT_NESTING_DEPTH.load(Ordering::Relaxed),
extension_value_bytes: JSON_LIMIT_HIT_EXTENSION_VALUE_BYTES.load(Ordering::Relaxed),
extension_field_count: JSON_LIMIT_HIT_EXTENSION_FIELD_COUNT.load(Ordering::Relaxed),
extension_key_len: JSON_LIMIT_HIT_EXTENSION_KEY_LEN.load(Ordering::Relaxed),
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct JsonParseLimits {
pub max_payload_bytes: Option<usize>,
pub max_nesting_depth: Option<usize>,
pub max_extension_value_bytes: Option<usize>,
pub max_extension_field_count: Option<usize>,
}
impl JsonParseLimits {
#[must_use]
pub const fn unlimited() -> Self {
Self {
max_payload_bytes: None,
max_nesting_depth: None,
max_extension_value_bytes: None,
max_extension_field_count: None,
}
}
#[must_use]
pub const fn untrusted_defaults() -> Self {
Self {
max_payload_bytes: Some(1_000_000),
max_nesting_depth: Some(64),
max_extension_value_bytes: Some(64_000),
max_extension_field_count: Some(32),
}
}
#[must_use]
pub const fn with_max_payload_bytes(mut self, bytes: Option<usize>) -> Self {
self.max_payload_bytes = bytes;
self
}
#[must_use]
pub const fn with_max_nesting_depth(mut self, depth: Option<usize>) -> Self {
self.max_nesting_depth = depth;
self
}
#[must_use]
pub const fn with_max_extension_value_bytes(mut self, bytes: Option<usize>) -> Self {
self.max_extension_value_bytes = bytes;
self
}
#[must_use]
pub const fn with_max_extension_field_count(mut self, count: Option<usize>) -> Self {
self.max_extension_field_count = count;
self
}
}
thread_local! {
static EXTENSION_BUDGET: std::cell::Cell<Option<ExtensionBudget>> =
const { std::cell::Cell::new(None) };
}
#[derive(Debug, Clone, Copy)]
pub(super) struct ExtensionBudget {
remaining_bytes: Option<usize>,
max_fields_per_struct: Option<usize>,
}
pub(super) struct BudgetGuard(Option<ExtensionBudget>);
impl Drop for BudgetGuard {
fn drop(&mut self) {
EXTENSION_BUDGET.with(|b| b.set(self.0));
}
}
pub(super) fn install_extension_budget(limits: JsonParseLimits) -> BudgetGuard {
let budget = ExtensionBudget {
remaining_bytes: limits.max_extension_value_bytes,
max_fields_per_struct: limits.max_extension_field_count,
};
let previous = EXTENSION_BUDGET.with(|b| b.replace(Some(budget)));
BudgetGuard(previous)
}
#[inline]
pub(super) fn budget_max_fields_per_struct() -> Option<usize> {
EXTENSION_BUDGET
.with(|b| b.get())
.and_then(|b| b.max_fields_per_struct)
}
#[inline]
pub(super) fn charge_extension_bytes(bytes: usize) -> Result<(), (usize, usize)> {
EXTENSION_BUDGET.with(|cell| {
let Some(mut budget) = cell.get() else {
return Ok(());
};
let Some(remaining) = budget.remaining_bytes else {
return Ok(());
};
let Some(left) = remaining.checked_sub(bytes) else {
return Err((bytes, remaining));
};
budget.remaining_bytes = Some(left);
cell.set(Some(budget));
Ok(())
})
}
pub const DEFAULT_MAX_NESTING_DEPTH: usize = 128;
pub(super) fn check_payload_limit(
payload_len: usize,
limits: JsonParseLimits,
) -> Result<(), serde_json::Error> {
if let Some(max) = limits.max_payload_bytes {
if payload_len > max {
trace_limit_violation(LimitKind::PayloadBytes, payload_len, max);
return Err(serde_json::Error::custom(format!(
"payload too large: {payload_len} bytes exceeds limit {max}"
)));
}
}
Ok(())
}
#[inline]
pub(super) fn resolved_max_depth(limits: JsonParseLimits) -> usize {
limits
.max_nesting_depth
.unwrap_or(DEFAULT_MAX_NESTING_DEPTH)
}
pub(super) fn deserialize_german_from_str<T: DeserializeOwned>(
s: &str,
max_depth: usize,
) -> Result<T, serde_json::Error> {
let state = DepthState::new(max_depth);
let mut de = serde_json::Deserializer::from_str(s);
let value = T::deserialize(DepthLimitedDeserializer::new(&mut de, &state))?;
de.end()?;
Ok(value)
}
pub(super) fn deserialize_german_from_slice<T: DeserializeOwned>(
bytes: &[u8],
max_depth: usize,
) -> Result<T, serde_json::Error> {
let state = DepthState::new(max_depth);
let mut de = serde_json::Deserializer::from_slice(bytes);
let value = T::deserialize(DepthLimitedDeserializer::new(&mut de, &state))?;
de.end()?;
Ok(value)
}