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};
#[cfg(feature = "simd-json")]
const SIMD_JSON_STR_MIN_BYTES: usize = 2048;
#[cfg(feature = "simd-json")]
const SIMD_JSON_BYTES_MIN_BYTES: usize = 1536;
#[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"
);
}
fn increment_limit_counter(kind: &'static str) {
match kind {
"payload_bytes" => {
JSON_LIMIT_HIT_PAYLOAD_BYTES.fetch_add(1, Ordering::Relaxed);
}
"nesting_depth" => {
JSON_LIMIT_HIT_NESTING_DEPTH.fetch_add(1, Ordering::Relaxed);
}
"extension_value_bytes" => {
JSON_LIMIT_HIT_EXTENSION_VALUE_BYTES.fetch_add(1, Ordering::Relaxed);
}
"extension_field_count" => {
JSON_LIMIT_HIT_EXTENSION_FIELD_COUNT.fetch_add(1, Ordering::Relaxed);
}
"extension_key_len" => {
JSON_LIMIT_HIT_EXTENSION_KEY_LEN.fetch_add(1, Ordering::Relaxed);
}
_ => {
debug_assert!(false, "unknown limit kind: {kind}");
}
}
#[cfg(feature = "metrics")]
metrics::counter!("bo4e_json_limit_hit_total", "kind" => kind).increment(1);
}
pub(super) fn trace_limit_violation(kind: &'static str, actual: usize, limit: usize) {
increment_limit_counter(kind);
#[cfg(feature = "tracing")]
tracing::warn!(kind, actual, limit, "bo4e json parse limit exceeded");
#[cfg(not(feature = "tracing"))]
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)]
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),
}
}
}
pub(super) const DEFAULT_MAX_NESTING_DEPTH: usize = 128;
pub(super) fn scan_max_nesting_depth(bytes: &[u8]) -> usize {
let mut depth: usize = 0;
let mut max: usize = 0;
let mut in_string = false;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if in_string {
if b == b'\\' {
i += 1; } else if b == b'"' {
in_string = false;
}
} else {
match b {
b'"' => in_string = true,
b'{' | b'[' => {
depth += 1;
if depth > max {
max = depth;
}
}
b'}' | b']' => {
depth = depth.saturating_sub(1);
}
_ => {}
}
}
i += 1;
}
max
}
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("payload_bytes", payload_len, max);
return Err(serde_json::Error::custom(format!(
"payload too large: {payload_len} bytes exceeds limit {max}"
)));
}
}
Ok(())
}
pub(super) fn check_default_depth(bytes: &[u8]) -> Result<(), serde_json::Error> {
let actual = scan_max_nesting_depth(bytes);
if actual > DEFAULT_MAX_NESTING_DEPTH {
trace_limit_violation("nesting_depth", actual, DEFAULT_MAX_NESTING_DEPTH);
Err(serde_json::Error::custom(format!(
"JSON nesting depth {actual} exceeds default limit {DEFAULT_MAX_NESTING_DEPTH}; \
use from_json_german_hardened with a JsonParseLimits to adjust"
)))
} else {
Ok(())
}
}
pub(super) fn deserialize_german_from_str<T: DeserializeOwned>(
s: &str,
) -> Result<T, serde_json::Error> {
#[cfg(feature = "simd-json")]
{
if s.len() < SIMD_JSON_STR_MIN_BYTES {
} else {
check_default_depth(s.as_bytes())?;
let mut buf = s.as_bytes().to_vec();
return simd_json::from_slice::<T>(&mut buf).map_err(serde_json::Error::custom);
}
}
let state = DepthState::new(DEFAULT_MAX_NESTING_DEPTH);
let mut de = serde_json::Deserializer::from_str(s);
T::deserialize(DepthLimitedDeserializer::new(&mut de, &state))
}
pub(super) fn deserialize_german_from_slice<T: DeserializeOwned>(
bytes: &[u8],
) -> Result<T, serde_json::Error> {
#[cfg(feature = "simd-json")]
{
if bytes.len() < SIMD_JSON_BYTES_MIN_BYTES {
} else {
check_default_depth(bytes)?;
let mut buf = bytes.to_vec();
return simd_json::from_slice::<T>(&mut buf).map_err(serde_json::Error::custom);
}
}
let state = DepthState::new(DEFAULT_MAX_NESTING_DEPTH);
let mut de = serde_json::Deserializer::from_slice(bytes);
T::deserialize(DepthLimitedDeserializer::new(&mut de, &state))
}