use serde_json::Value;
use super::limits::{
budget_max_fields_per_struct, charge_extension_bytes, trace_limit_violation, LimitKind,
};
use super::sealed;
pub(super) 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(),
}
}
#[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)]
pub struct LimitedExtensionMap(
Option<Box<indexmap::IndexMap<String, serde_json::Value>>>,
);
#[cfg(feature = "json")]
impl LimitedExtensionMap {
#[cfg(any(feature = "versioned", test))]
#[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,
) -> Result<Option<serde_json::Value>, ExtensionInsertError> {
if key.len() > MAX_EXTENSION_KEY_LEN {
return Err(ExtensionInsertError::KeyTooLong { len: key.len() });
}
if let Some(map) = self.0.as_deref() {
if map.len() >= MAX_EXTENSION_FIELDS && !map.contains_key(&key) {
return Err(ExtensionInsertError::Full);
}
}
let map = self
.0
.get_or_insert_with(|| Box::new(indexmap::IndexMap::new()));
Ok(map.insert(key, value))
}
#[inline]
#[must_use]
pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
self.0.as_deref()?.get(key)
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.0.as_deref().map_or(0, indexmap::IndexMap::len)
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.as_ref().is_none_or(|m| m.is_empty())
}
#[inline]
pub fn iter(&self) -> impl Iterator<Item = (&String, &serde_json::Value)> {
self.0.as_deref().into_iter().flatten()
}
}
#[cfg(feature = "json")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ExtensionInsertError {
#[error(
"extension key is {len} bytes, over the {} byte limit",
MAX_EXTENSION_KEY_LEN
)]
KeyTooLong {
len: usize,
},
#[error(
"extension map already holds the maximum of {} fields",
MAX_EXTENSION_FIELDS
)]
Full,
}
#[cfg(feature = "json")]
impl PartialEq for LimitedExtensionMap {
fn eq(&self, other: &Self) -> bool {
match (self.0.as_deref(), other.0.as_deref()) {
(Some(a), Some(b)) => a == b,
(Some(m), None) | (None, Some(m)) => m.is_empty(),
(None, None) => true,
}
}
}
#[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);
let field_cap = budget_max_fields_per_struct()
.map_or(MAX_EXTENSION_FIELDS, |b| b.min(MAX_EXTENSION_FIELDS));
while let Some(key) = access.next_key::<String>()? {
if key.len() > MAX_EXTENSION_KEY_LEN {
trace_limit_violation(
LimitKind::ExtensionKeyLen,
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() >= field_cap {
trace_limit_violation(
LimitKind::ExtensionFieldCount,
map.len() + 1,
field_cap,
);
return Err(serde::de::Error::custom(format!(
"extension field count exceeds the limit of {field_cap} \
— rejecting payload to prevent unbounded memory growth"
)));
}
let value = access.next_value::<serde_json::Value>()?;
let cost = key.len() + estimated_json_value_bytes(&value);
if let Err((requested, remaining)) = charge_extension_bytes(cost) {
trace_limit_violation(LimitKind::ExtensionValueBytes, requested, remaining);
return Err(serde::de::Error::custom(format!(
"extension value budget exceeded: field {key:?} needs {requested} \
bytes but only {remaining} remain in this call's allowance"
)));
}
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(all(feature = "json", feature = "versioned"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "json", feature = "versioned"))))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownFieldError {
pub paths: Vec<String>,
}
#[cfg(all(feature = "json", feature = "versioned"))]
impl std::fmt::Display for UnknownFieldError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} field(s) are not defined by this BO4E schema version: {}",
self.paths.len(),
self.paths.join(", ")
)
}
}
#[cfg(all(feature = "json", feature = "versioned"))]
impl std::error::Error for UnknownFieldError {}
#[cfg(all(feature = "json", feature = "versioned", feature = "validate"))]
impl From<UnknownFieldError> for garde::Error {
fn from(e: UnknownFieldError) -> Self {
garde::Error::new(e.to_string())
}
}
#[cfg(all(feature = "json", feature = "versioned"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "json", feature = "versioned"))))]
pub trait Bo4eExtensions {
fn collect_extension_paths(&self, path: &str, out: &mut Vec<String>);
fn extension_paths(&self) -> Vec<String> {
let mut out = Vec::new();
self.collect_extension_paths("", &mut out);
out
}
fn ensure_no_extension_data(&self) -> Result<(), UnknownFieldError> {
let paths = self.extension_paths();
if paths.is_empty() {
Ok(())
} else {
Err(UnknownFieldError { paths })
}
}
}
#[cfg(all(feature = "json", feature = "versioned"))]
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()
}
#[cfg(all(test, feature = "json"))]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn a_fresh_map_is_empty_and_allocates_nothing() {
let ext = LimitedExtensionMap::default();
assert!(ext.is_empty());
assert_eq!(ext.len(), 0);
assert_eq!(ext.get("anything"), None);
assert_eq!(ext.iter().count(), 0);
assert!(ext.as_map().is_none(), "an empty map must not allocate");
}
#[test]
fn insert_returns_the_displaced_value_and_keeps_arrival_order() {
let mut ext = LimitedExtensionMap::default();
assert_eq!(ext.try_insert("b".into(), json!(1)), Ok(None));
assert_eq!(ext.try_insert("a".into(), json!(2)), Ok(None));
assert_eq!(ext.try_insert("b".into(), json!(3)), Ok(Some(json!(1))));
assert_eq!(ext.len(), 2, "replacing must not grow the map");
assert_eq!(ext.get("b"), Some(&json!(3)));
assert_eq!(
ext.iter().map(|(k, _)| k.as_str()).collect::<Vec<_>>(),
["b", "a"],
"iteration follows insertion order, not sort order"
);
}
#[test]
fn an_oversized_key_is_refused_without_allocating() {
let mut ext = LimitedExtensionMap::default();
let key = "k".repeat(MAX_EXTENSION_KEY_LEN + 1);
assert_eq!(
ext.try_insert(key.clone(), json!(1)),
Err(ExtensionInsertError::KeyTooLong { len: key.len() })
);
assert!(ext.is_empty());
assert!(
ext.as_map().is_none(),
"a refused insert must not leave an allocated empty map behind"
);
}
#[test]
fn a_key_at_the_limit_is_accepted() {
let mut ext = LimitedExtensionMap::default();
let key = "k".repeat(MAX_EXTENSION_KEY_LEN);
assert_eq!(ext.try_insert(key, json!(1)), Ok(None));
assert_eq!(ext.len(), 1);
}
#[test]
fn a_full_map_refuses_new_keys_but_still_accepts_replacements() {
let mut ext = LimitedExtensionMap::default();
for i in 0..MAX_EXTENSION_FIELDS {
assert_eq!(ext.try_insert(format!("k{i}"), json!(i)), Ok(None));
}
assert_eq!(ext.len(), MAX_EXTENSION_FIELDS);
assert_eq!(
ext.try_insert("one_too_many".into(), json!(0)),
Err(ExtensionInsertError::Full)
);
assert_eq!(ext.len(), MAX_EXTENSION_FIELDS);
assert_eq!(
ext.try_insert("k0".into(), json!("new")),
Ok(Some(json!(0)))
);
assert_eq!(ext.get("k0"), Some(&json!("new")));
}
#[test]
fn emptiness_compares_equal_however_it_arose() {
let unallocated = LimitedExtensionMap::default();
let mut allocated = LimitedExtensionMap::default();
assert_eq!(allocated.try_insert("x".into(), json!(1)), Ok(None));
assert_ne!(allocated, unallocated);
let from_empty: LimitedExtensionMap = serde_json::from_str("{}").expect("valid");
assert_eq!(from_empty, unallocated);
assert!(from_empty.as_map().is_none());
}
#[test]
fn deserialization_rejects_more_fields_than_the_hard_cap() {
let body: String = (0..=MAX_EXTENSION_FIELDS)
.map(|i| format!(r#""k{i}":{i}"#))
.collect::<Vec<_>>()
.join(",");
let err = serde_json::from_str::<LimitedExtensionMap>(&format!("{{{body}}}"))
.expect_err("over the hard cap");
assert!(
err.to_string().contains("extension field count"),
"unexpected error: {err}"
);
}
#[test]
fn deserialization_rejects_an_oversized_key() {
let key = "k".repeat(MAX_EXTENSION_KEY_LEN + 1);
let err = serde_json::from_str::<LimitedExtensionMap>(&format!(r#"{{"{key}":1}}"#))
.expect_err("over the key-length cap");
assert!(
err.to_string().contains("key too long"),
"unexpected error: {err}"
);
}
}