use serde::de::Error as _;
use serde_json::Value;
use super::limits::{trace_limit_violation, JsonParseLimits};
use super::sealed;
fn estimated_json_value_bytes(value: &Value) -> usize {
match value {
Value::Null => 4, Value::Bool(b) => {
if *b {
4
} else {
5
}
} Value::Number(_) => 8, Value::String(s) => s.len(),
Value::Array(items) => items.iter().map(estimated_json_value_bytes).sum(),
Value::Object(map) => map
.iter()
.map(|(k, v)| k.len() + estimated_json_value_bytes(v))
.sum(),
}
}
pub(super) fn check_extension_budget<T>(
value: &T,
limits: JsonParseLimits,
) -> Result<(), serde_json::Error>
where
T: Bo4eExtensionData,
{
let data = value.extension_data();
if let Some(max) = limits.max_extension_field_count {
let count = data.len();
if count > max {
trace_limit_violation("extension_field_count", count, max);
return Err(serde_json::Error::custom(format!(
"extension field count {count} exceeds per-call limit {max}"
)));
}
}
if let Some(max) = limits.max_extension_value_bytes {
let used: usize = data
.iter()
.map(|(k, v)| k.len() + estimated_json_value_bytes(v))
.sum();
if used > max {
trace_limit_violation("extension_value_bytes", used, max);
return Err(serde_json::Error::custom(format!(
"extension value budget exceeded: estimated {used} bytes exceeds limit {max}"
)));
}
}
Ok(())
}
#[cfg(feature = "json")]
pub const MAX_EXTENSION_FIELDS: usize = 128;
#[cfg(feature = "json")]
pub const MAX_EXTENSION_KEY_LEN: usize = 256;
#[cfg(feature = "json")]
#[derive(Debug, Clone, Default, PartialEq)]
pub struct LimitedExtensionMap(
)`, `try_insert()`, `is_empty()`, and `Serialize/Deserialize` impls.
Option<Box<indexmap::IndexMap<String, serde_json::Value>>>,
);
#[cfg(feature = "json")]
impl LimitedExtensionMap {
#[inline]
pub(crate) fn as_map(&self) -> Option<&indexmap::IndexMap<String, serde_json::Value>> {
self.0.as_deref()
}
#[inline]
pub fn try_insert(&mut self, key: String, value: serde_json::Value) -> bool {
if key.len() > MAX_EXTENSION_KEY_LEN {
return false;
}
let map = self
.0
.get_or_insert_with(|| Box::new(indexmap::IndexMap::new()));
if map.len() >= MAX_EXTENSION_FIELDS {
return false;
}
map.insert(key, value);
true
}
#[inline]
pub(crate) fn is_empty(&self) -> bool {
self.0.as_ref().is_none_or(|m| m.is_empty())
}
}
#[cfg(feature = "json")]
impl serde::Serialize for LimitedExtensionMap {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap as _;
match &self.0 {
None => serializer.serialize_map(Some(0))?.end(),
Some(map) => map.serialize(serializer),
}
}
}
#[cfg(feature = "json")]
impl<'de> serde::Deserialize<'de> for LimitedExtensionMap {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct LimitedVisitor;
impl<'de> serde::de::Visitor<'de> for LimitedVisitor {
type Value = LimitedExtensionMap;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"a map with at most {MAX_EXTENSION_FIELDS} extension entries"
)
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut access: A,
) -> Result<Self::Value, A::Error> {
let hint = access.size_hint().unwrap_or(0).min(MAX_EXTENSION_FIELDS);
let mut map = indexmap::IndexMap::with_capacity(hint);
while let Some(key) = access.next_key::<String>()? {
if key.len() > MAX_EXTENSION_KEY_LEN {
trace_limit_violation(
"extension_key_len",
key.len(),
MAX_EXTENSION_KEY_LEN,
);
return Err(serde::de::Error::custom(format!(
"extension field key too long: {} bytes exceeds limit {MAX_EXTENSION_KEY_LEN}",
key.len()
)));
}
if map.len() >= MAX_EXTENSION_FIELDS {
trace_limit_violation(
"extension_field_count",
map.len() + 1,
MAX_EXTENSION_FIELDS,
);
return Err(serde::de::Error::custom(format!(
"extension field count exceeds the limit of {MAX_EXTENSION_FIELDS} \
— rejecting payload to prevent unbounded memory growth"
)));
}
let value = access.next_value::<serde_json::Value>()?;
map.insert(key, value);
}
Ok(LimitedExtensionMap(if map.is_empty() {
None
} else {
Some(Box::new(map))
}))
}
}
deserializer.deserialize_map(LimitedVisitor)
}
}
#[cfg(feature = "json")]
pub trait Bo4eExtensionData: sealed::Sealed {
fn extension_data(&self) -> &indexmap::IndexMap<String, serde_json::Value>;
fn has_extension_data(&self) -> bool;
}
#[cfg(feature = "json")]
pub(crate) static EMPTY_EXTENSION_MAP: std::sync::LazyLock<
indexmap::IndexMap<String, serde_json::Value>,
> = std::sync::LazyLock::new(indexmap::IndexMap::new);
#[cfg(all(feature = "json", feature = "schemars"))]
impl schemars::JsonSchema for LimitedExtensionMap {
fn inline_schema() -> bool {
true
}
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("LimitedExtensionMap")
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
<indexmap::IndexMap<String, serde_json::Value>>::json_schema(generator)
}
}
#[cfg(all(feature = "json", feature = "utoipa"))]
impl utoipa::ToSchema for LimitedExtensionMap {
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("LimitedExtensionMap")
}
}
#[cfg(all(feature = "json", feature = "utoipa"))]
impl utoipa::PartialSchema for LimitedExtensionMap {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
utoipa::openapi::ObjectBuilder::new()
.additional_properties(Some(
utoipa::openapi::schema::AdditionalProperties::FreeForm(true),
))
.into()
}
}
#[cfg(feature = "json")]
#[doc(hidden)]
#[inline]
pub fn ext_map_is_empty(m: &LimitedExtensionMap) -> bool {
m.is_empty()
}