pub(crate) mod depth;
pub(crate) mod extension;
pub(crate) mod key_transform;
pub(crate) mod limits;
pub use extension::{
ext_map_is_empty, Bo4eExtensionData, LimitedExtensionMap, MAX_EXTENSION_FIELDS,
MAX_EXTENSION_KEY_LEN,
};
pub use limits::{json_limit_hit_counters, JsonLimitHitCounters, JsonParseLimits};
use depth::{DepthLimitedDeserializer, DepthState};
use key_transform::{
camel_to_snake, deserialize_with_key_transform_from_slice,
deserialize_with_key_transform_from_str, serialize_with_key_transform, snake_to_camel,
KeyTransformDeserializer,
};
#[cfg(feature = "tracing")]
use limits::trace_json_outcome;
use limits::{
check_payload_limit, deserialize_german_from_slice, deserialize_german_from_str,
install_extension_budget, trace_deser_error,
};
#[cfg(feature = "tracing")]
use std::time::Instant;
use serde::de::DeserializeOwned;
use serde::Serialize;
#[inline]
fn traced_serialize<T: ?Sized>(
mode: &'static str,
op: impl FnOnce() -> Result<String, serde_json::Error>,
) -> Result<String, serde_json::Error> {
#[cfg(not(feature = "tracing"))]
{
let _ = mode;
op()
}
#[cfg(feature = "tracing")]
{
let started = Instant::now();
let result = op();
trace_json_outcome(
"serialize",
mode,
std::any::type_name::<T>(),
None,
result.as_ref().ok().map(String::len),
started,
result.is_ok(),
);
result
}
}
#[inline]
fn traced_deserialize<S: ?Sized, T>(
operation: &'static str,
mode: &'static str,
input_len: usize,
context: &'static str,
op: impl FnOnce() -> Result<T, serde_json::Error>,
) -> Result<T, serde_json::Error> {
#[cfg(feature = "tracing")]
let started = Instant::now();
let result = op();
trace_deser_error(&result, context);
#[cfg(feature = "tracing")]
trace_json_outcome(
operation,
mode,
std::any::type_name::<S>(),
Some(input_len),
None,
started,
result.is_ok(),
);
#[cfg(not(feature = "tracing"))]
let _ = (operation, mode, input_len);
result
}
pub(crate) mod sealed {
pub trait Sealed {}
}
pub trait Bo4eJsonExt: sealed::Sealed + Serialize + DeserializeOwned + Sized {
fn to_json_german(&self) -> Result<String, serde_json::Error> {
traced_serialize::<Self>("german", || serde_json::to_string(self))
}
fn to_json_canonical(&self) -> Result<String, serde_json::Error> {
traced_serialize::<Self>("canonical", || {
let mut out = Vec::new();
{
let mut ser = serde_json::Serializer::new(&mut out);
self.serialize(SortedSerializer { inner: &mut ser })?;
}
Ok(String::from_utf8(out).expect("serde_json always produces valid UTF-8"))
})
}
fn to_json_snake_case(&self) -> Result<String, serde_json::Error> {
traced_serialize::<Self>("snake_case", || {
serialize_with_key_transform(self, camel_to_snake)
})
}
fn from_json_snake_case(s: &str) -> Result<Self, serde_json::Error> {
traced_deserialize::<Self, _>(
"deserialize",
"snake_case_str",
s.len(),
"BO4E deserialization failed in from_json_snake_case",
|| deserialize_with_key_transform_from_str::<Self, _>(s, &snake_to_camel),
)
}
fn from_json_snake_case_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
traced_deserialize::<Self, _>(
"deserialize",
"snake_case_bytes",
bytes.len(),
"BO4E deserialization failed in from_json_snake_case_bytes",
|| deserialize_with_key_transform_from_slice::<Self, _>(bytes, &snake_to_camel),
)
}
fn from_json_german(s: &str) -> Result<Self, serde_json::Error> {
traced_deserialize::<Self, _>(
"deserialize",
"german_str",
s.len(),
"BO4E deserialization failed in from_json_german",
|| deserialize_german_from_str::<Self>(s),
)
}
fn from_json_german_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
traced_deserialize::<Self, _>(
"deserialize",
"german_bytes",
bytes.len(),
"BO4E deserialization failed in from_json_german_bytes",
|| deserialize_german_from_slice(bytes),
)
}
fn from_json_german_hardened(
s: &str,
limits: JsonParseLimits,
) -> Result<Self, serde_json::Error> {
check_payload_limit(s.len(), limits)?;
traced_deserialize::<Self, _>(
"deserialize_hardened",
"german_str",
s.len(),
"BO4E hardened deserialization failed in from_json_german_hardened",
|| {
let _budget = install_extension_budget(limits);
match limits.max_nesting_depth {
Some(max_depth) => {
let state = DepthState::new(max_depth);
let mut de = serde_json::Deserializer::from_str(s);
Self::deserialize(DepthLimitedDeserializer::new(&mut de, &state))
}
None => deserialize_german_from_str::<Self>(s),
}
},
)
}
fn from_json_snake_case_hardened(
s: &str,
limits: JsonParseLimits,
) -> Result<Self, serde_json::Error> {
check_payload_limit(s.len(), limits)?;
traced_deserialize::<Self, _>(
"deserialize_hardened",
"snake_case_str",
s.len(),
"BO4E hardened deserialization failed in from_json_snake_case_hardened",
|| {
let _budget = install_extension_budget(limits);
match limits.max_nesting_depth {
Some(max_depth) => {
let state = DepthState::new(max_depth);
let mut de = serde_json::Deserializer::from_str(s);
Self::deserialize(KeyTransformDeserializer::new(
DepthLimitedDeserializer::new(&mut de, &state),
&snake_to_camel,
))
}
None => deserialize_with_key_transform_from_str::<Self, _>(s, &snake_to_camel),
}
},
)
}
fn from_json_german_bytes_hardened(
bytes: &[u8],
limits: JsonParseLimits,
) -> Result<Self, serde_json::Error> {
check_payload_limit(bytes.len(), limits)?;
traced_deserialize::<Self, _>(
"deserialize_hardened",
"german_bytes",
bytes.len(),
"BO4E hardened deserialization failed in from_json_german_bytes_hardened",
|| {
let _budget = install_extension_budget(limits);
match limits.max_nesting_depth {
Some(max_depth) => {
let state = DepthState::new(max_depth);
let mut de = serde_json::Deserializer::from_slice(bytes);
Self::deserialize(DepthLimitedDeserializer::new(&mut de, &state))
}
None => deserialize_german_from_slice::<Self>(bytes),
}
},
)
}
fn from_json_snake_case_bytes_hardened(
bytes: &[u8],
limits: JsonParseLimits,
) -> Result<Self, serde_json::Error> {
check_payload_limit(bytes.len(), limits)?;
traced_deserialize::<Self, _>(
"deserialize_hardened",
"snake_case_bytes",
bytes.len(),
"BO4E hardened deserialization failed in from_json_snake_case_bytes_hardened",
|| {
let _budget = install_extension_budget(limits);
match limits.max_nesting_depth {
Some(max_depth) => {
let state = DepthState::new(max_depth);
let mut de = serde_json::Deserializer::from_slice(bytes);
Self::deserialize(KeyTransformDeserializer::new(
DepthLimitedDeserializer::new(&mut de, &state),
&snake_to_camel,
))
}
None => {
deserialize_with_key_transform_from_slice::<Self, _>(bytes, &snake_to_camel)
}
}
},
)
}
}
struct SortedSerializer<S> {
inner: S,
}
struct SortedMap<S> {
inner: S,
entries: Vec<(String, Vec<u8>)>,
}
impl<S: serde::ser::Serializer> serde::ser::SerializeMap for SortedMap<S> {
type Ok = S::Ok;
type Error = S::Error;
fn serialize_key<T: ?Sized + Serialize>(&mut self, _key: &T) -> Result<(), Self::Error> {
debug_assert!(
false,
"SortedMap: serialize_key called without serialize_value; use serialize_entry"
);
Err(serde::ser::Error::custom(
"internal: SortedMap requires serialize_entry; serialize_key/serialize_value are unsupported",
))
}
fn serialize_value<T: ?Sized + Serialize>(&mut self, _value: &T) -> Result<(), Self::Error> {
debug_assert!(
false,
"SortedMap: serialize_value called without serialize_key; use serialize_entry"
);
Err(serde::ser::Error::custom(
"internal: SortedMap requires serialize_entry; serialize_key/serialize_value are unsupported",
))
}
fn serialize_entry<K: ?Sized + Serialize, V: ?Sized + Serialize>(
&mut self,
key: &K,
value: &V,
) -> Result<(), Self::Error> {
let key_str = {
let mut kbuf = Vec::with_capacity(32);
let mut kser = serde_json::Serializer::new(&mut kbuf);
key.serialize(&mut kser)
.map_err(|e| serde::ser::Error::custom(e.to_string()))?;
let raw = String::from_utf8(kbuf)
.map_err(|e| serde::ser::Error::custom(format!("key utf-8: {e}")))?;
if raw.starts_with('"') && raw.ends_with('"') && raw.len() >= 2 {
raw[1..raw.len() - 1].to_owned()
} else {
raw
}
};
let mut vbuf = Vec::new();
{
let mut vser = serde_json::Serializer::new(&mut vbuf);
value
.serialize(SortedSerializer { inner: &mut vser })
.map_err(|e| serde::ser::Error::custom(e.to_string()))?;
}
self.entries.push((key_str, vbuf));
Ok(())
}
fn end(mut self) -> Result<Self::Ok, Self::Error> {
self.entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
let mut map = self.inner.serialize_map(Some(self.entries.len()))?;
for (key, value_bytes) in self.entries.drain(..) {
let raw_value = serde_json::value::RawValue::from_string(
String::from_utf8(value_bytes).expect("serde_json always produces valid UTF-8"),
)
.map_err(serde::ser::Error::custom)?;
map.serialize_entry(key.as_str(), &raw_value)?;
}
map.end()
}
}
struct SortedSeq<S> {
inner: S,
buf: Vec<Vec<u8>>,
}
impl<S: serde::ser::Serializer> serde::ser::SerializeSeq for SortedSeq<S> {
type Ok = S::Ok;
type Error = S::Error;
fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
let mut vbuf = Vec::new();
{
let mut vser = serde_json::Serializer::new(&mut vbuf);
value
.serialize(SortedSerializer { inner: &mut vser })
.map_err(|e| serde::ser::Error::custom(e.to_string()))?;
}
self.buf.push(vbuf);
Ok(())
}
fn end(mut self) -> Result<Self::Ok, Self::Error> {
let mut seq = self.inner.serialize_seq(Some(self.buf.len()))?;
for vbuf in self.buf.drain(..) {
let raw_value = serde_json::value::RawValue::from_string(
String::from_utf8(vbuf).expect("serde_json always produces valid UTF-8"),
)
.map_err(serde::ser::Error::custom)?;
seq.serialize_element(&raw_value)?;
}
seq.end()
}
}
impl<S: serde::ser::Serializer> serde::ser::Serializer for SortedSerializer<S> {
type Ok = S::Ok;
type Error = S::Error;
type SerializeSeq = SortedSeq<S>;
type SerializeTuple = S::SerializeTuple;
type SerializeTupleStruct = S::SerializeTupleStruct;
type SerializeTupleVariant = S::SerializeTupleVariant;
type SerializeMap = SortedMap<S>;
type SerializeStruct = SortedMap<S>;
type SerializeStructVariant = S::SerializeStructVariant;
fn serialize_bool(self, v: bool) -> Result<S::Ok, S::Error> {
self.inner.serialize_bool(v)
}
fn serialize_i8(self, v: i8) -> Result<S::Ok, S::Error> {
self.inner.serialize_i8(v)
}
fn serialize_i16(self, v: i16) -> Result<S::Ok, S::Error> {
self.inner.serialize_i16(v)
}
fn serialize_i32(self, v: i32) -> Result<S::Ok, S::Error> {
self.inner.serialize_i32(v)
}
fn serialize_i64(self, v: i64) -> Result<S::Ok, S::Error> {
self.inner.serialize_i64(v)
}
fn serialize_u8(self, v: u8) -> Result<S::Ok, S::Error> {
self.inner.serialize_u8(v)
}
fn serialize_u16(self, v: u16) -> Result<S::Ok, S::Error> {
self.inner.serialize_u16(v)
}
fn serialize_u32(self, v: u32) -> Result<S::Ok, S::Error> {
self.inner.serialize_u32(v)
}
fn serialize_u64(self, v: u64) -> Result<S::Ok, S::Error> {
self.inner.serialize_u64(v)
}
fn serialize_f32(self, v: f32) -> Result<S::Ok, S::Error> {
self.inner.serialize_f32(v)
}
fn serialize_f64(self, v: f64) -> Result<S::Ok, S::Error> {
self.inner.serialize_f64(v)
}
fn serialize_char(self, v: char) -> Result<S::Ok, S::Error> {
self.inner.serialize_char(v)
}
fn serialize_str(self, v: &str) -> Result<S::Ok, S::Error> {
self.inner.serialize_str(v)
}
fn serialize_bytes(self, v: &[u8]) -> Result<S::Ok, S::Error> {
self.inner.serialize_bytes(v)
}
fn serialize_none(self) -> Result<S::Ok, S::Error> {
self.inner.serialize_none()
}
fn serialize_unit(self) -> Result<S::Ok, S::Error> {
self.inner.serialize_unit()
}
fn serialize_unit_struct(self, name: &'static str) -> Result<S::Ok, S::Error> {
self.inner.serialize_unit_struct(name)
}
fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<S::Ok, S::Error> {
self.inner.serialize_some(&SortedSerializeValue(value))
}
fn serialize_unit_variant(
self,
name: &'static str,
vi: u32,
v: &'static str,
) -> Result<S::Ok, S::Error> {
self.inner.serialize_unit_variant(name, vi, v)
}
fn serialize_newtype_struct<T: ?Sized + Serialize>(
self,
name: &'static str,
value: &T,
) -> Result<S::Ok, S::Error> {
self.inner
.serialize_newtype_struct(name, &SortedSerializeValue(value))
}
fn serialize_newtype_variant<T: ?Sized + Serialize>(
self,
name: &'static str,
vi: u32,
v: &'static str,
value: &T,
) -> Result<S::Ok, S::Error> {
self.inner
.serialize_newtype_variant(name, vi, v, &SortedSerializeValue(value))
}
fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, S::Error> {
Ok(SortedSeq {
inner: self.inner,
buf: Vec::new(),
})
}
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, S::Error> {
self.inner.serialize_tuple(len)
}
fn serialize_tuple_struct(
self,
name: &'static str,
len: usize,
) -> Result<Self::SerializeTupleStruct, S::Error> {
self.inner.serialize_tuple_struct(name, len)
}
fn serialize_tuple_variant(
self,
name: &'static str,
vi: u32,
v: &'static str,
len: usize,
) -> Result<Self::SerializeTupleVariant, S::Error> {
self.inner.serialize_tuple_variant(name, vi, v, len)
}
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, S::Error> {
Ok(SortedMap {
inner: self.inner,
entries: Vec::new(),
})
}
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, S::Error> {
Ok(SortedMap {
inner: self.inner,
entries: Vec::new(),
})
}
fn serialize_struct_variant(
self,
name: &'static str,
vi: u32,
v: &'static str,
len: usize,
) -> Result<Self::SerializeStructVariant, S::Error> {
self.inner.serialize_struct_variant(name, vi, v, len)
}
fn is_human_readable(&self) -> bool {
self.inner.is_human_readable()
}
fn collect_str<T: ?Sized + std::fmt::Display>(self, value: &T) -> Result<S::Ok, S::Error> {
self.inner.collect_str(value)
}
}
impl<S: serde::ser::Serializer> serde::ser::SerializeStruct for SortedMap<S> {
type Ok = S::Ok;
type Error = S::Error;
fn serialize_field<T: ?Sized + Serialize>(
&mut self,
key: &'static str,
value: &T,
) -> Result<(), Self::Error> {
serde::ser::SerializeMap::serialize_entry(self, key, value)
}
fn end(self) -> Result<Self::Ok, Self::Error> {
serde::ser::SerializeMap::end(self)
}
}
struct SortedSerializeValue<'a, T: ?Sized>(&'a T);
impl<T: ?Sized + Serialize> Serialize for SortedSerializeValue<'_, T> {
fn serialize<S: serde::ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.0.serialize(SortedSerializer { inner: serializer })
}
}
#[cfg(test)]
mod tests {
use super::*;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Sample {
#[serde(rename = "zField")]
z_field: String,
#[serde(rename = "aField")]
a_field: u32,
#[serde(rename = "mField", skip_serializing_if = "Option::is_none")]
m_field: Option<String>,
}
impl sealed::Sealed for Sample {}
impl Bo4eJsonExt for Sample {}
#[derive(Debug, PartialEq, Serialize, Deserialize, Default)]
struct HardenedSample {
#[serde(rename = "name")]
name: String,
#[serde(flatten)]
#[serde(skip_serializing_if = "crate::json::ext_map_is_empty")]
_additional: LimitedExtensionMap,
}
impl sealed::Sealed for HardenedSample {}
impl Bo4eJsonExt for HardenedSample {}
impl Bo4eExtensionData for HardenedSample {
fn extension_data(&self) -> &IndexMap<String, serde_json::Value> {
static EMPTY: std::sync::LazyLock<IndexMap<String, serde_json::Value>> =
std::sync::LazyLock::new(IndexMap::new);
self._additional.as_map().unwrap_or(&EMPTY)
}
fn has_extension_data(&self) -> bool {
!self._additional.is_empty()
}
}
#[test]
fn to_json_german_round_trips() {
let v = Sample {
z_field: "hello".into(),
a_field: 42,
m_field: None,
};
let json = v.to_json_german().unwrap();
let back: Sample = Sample::from_json_german(&json).unwrap();
assert_eq!(v, back);
}
#[test]
fn from_json_german_bytes_round_trips() {
let v = Sample {
z_field: "hello".into(),
a_field: 42,
m_field: None,
};
let json = v.to_json_german().unwrap();
let back: Sample = Sample::from_json_german_bytes(&json.into_bytes()).unwrap();
assert_eq!(v, back);
}
#[test]
fn from_json_snake_case_bytes_round_trips() {
let v = Sample {
z_field: "x".into(),
a_field: 7,
m_field: Some("y".into()),
};
let json = v.to_json_snake_case().unwrap();
let back = Sample::from_json_snake_case_bytes(&json.into_bytes()).unwrap();
assert_eq!(v, back);
}
#[test]
fn bytes_and_str_variants_are_equivalent() {
let v = Sample {
z_field: "abc".into(),
a_field: 99,
m_field: Some("z".into()),
};
let german = v.to_json_german().unwrap();
let snake = v.to_json_snake_case().unwrap();
assert_eq!(
Sample::from_json_german(&german).unwrap(),
Sample::from_json_german_bytes(&german.clone().into_bytes()).unwrap(),
);
assert_eq!(
Sample::from_json_snake_case(&snake).unwrap(),
Sample::from_json_snake_case_bytes(&snake.clone().into_bytes()).unwrap(),
);
}
#[test]
fn to_json_german_uses_serde_rename() {
let v = Sample {
z_field: "hello".into(),
a_field: 42,
m_field: None,
};
let json = v.to_json_german().unwrap();
assert!(
json.contains("\"zField\""),
"expected German camelCase key: {json}"
);
assert!(
json.contains("\"aField\""),
"expected German camelCase key: {json}"
);
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct SnakeSample {
#[serde(rename = "marktlokationsId")]
marktlokations_id: String,
#[serde(rename = "anteilProzent")]
anteil_prozent: u32,
#[serde(rename = "hoechstpreisHT", skip_serializing_if = "Option::is_none")]
hoechstpreis_ht: Option<String>,
}
impl sealed::Sealed for SnakeSample {}
impl Bo4eJsonExt for SnakeSample {}
#[test]
fn to_json_snake_case_uses_snake_case_keys() {
let v = SnakeSample {
marktlokations_id: "51238696781".into(),
anteil_prozent: 42,
hoechstpreis_ht: None,
};
let json = v.to_json_snake_case().unwrap();
assert!(
json.contains("\"marktlokations_id\""),
"expected snake_case key: {json}"
);
assert!(
json.contains("\"anteil_prozent\""),
"expected snake_case key: {json}"
);
assert!(
!json.contains("\"marktlokationsId\""),
"must not contain camelCase: {json}"
);
}
#[test]
fn to_json_snake_case_round_trips() {
let v = SnakeSample {
marktlokations_id: "51238696781".into(),
anteil_prozent: 7,
hoechstpreis_ht: Some("12.50".into()),
};
let json = v.to_json_snake_case().unwrap();
let back = SnakeSample::from_json_snake_case(&json).unwrap();
assert_eq!(v, back);
}
#[test]
fn acronym_keys_survive_snake_case_round_trip() {
let v = SnakeSample {
marktlokations_id: "51238696781".into(),
anteil_prozent: 1,
hoechstpreis_ht: Some("99.00".into()),
};
let json = v.to_json_snake_case().unwrap();
assert!(
json.contains("\"hoechstpreis_ht\""),
"expected snake_case key: {json}"
);
let back = SnakeSample::from_json_snake_case(&json).unwrap();
assert_eq!(back.hoechstpreis_ht.as_deref(), Some("99.00"));
assert_eq!(v, back);
}
#[test]
fn underscore_prefixed_keys_survive_snake_case_round_trip() {
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct WithMeta {
#[serde(rename = "_typ")]
typ: String,
#[serde(rename = "anteilProzent")]
anteil_prozent: u32,
}
impl sealed::Sealed for WithMeta {}
impl Bo4eJsonExt for WithMeta {}
let v = WithMeta {
typ: "Vertrag".into(),
anteil_prozent: 1,
};
let json = v.to_json_snake_case().unwrap();
assert!(json.contains("\"_typ\""), "_typ must survive: {json}");
assert!(
json.contains("\"anteil_prozent\""),
"anteil_prozent must appear: {json}"
);
let back = WithMeta::from_json_snake_case(&json).unwrap();
assert_eq!(v, back);
}
#[test]
fn to_json_canonical_sorts_keys() {
let v = Sample {
z_field: "z".into(),
a_field: 1,
m_field: Some("m".into()),
};
let canonical = v.to_json_canonical().unwrap();
let a = canonical.find("aField").unwrap();
let m = canonical.find("mField").unwrap();
let z = canonical.find("zField").unwrap();
assert!(a < m && m < z, "keys not sorted: {canonical}");
}
#[test]
fn to_json_canonical_nested_objects_sorted() {
#[derive(Serialize, Deserialize)]
struct Outer {
#[serde(rename = "zField")]
z: Inner,
#[serde(rename = "aField")]
a: u32,
}
#[derive(Serialize, Deserialize)]
struct Inner {
#[serde(rename = "zInner")]
z_inner: String,
#[serde(rename = "aInner")]
a_inner: u32,
}
impl sealed::Sealed for Outer {}
impl Bo4eJsonExt for Outer {}
impl sealed::Sealed for Inner {}
impl Bo4eJsonExt for Inner {}
let v = Outer {
z: Inner {
z_inner: "z".into(),
a_inner: 1,
},
a: 99,
};
let canonical = v.to_json_canonical().unwrap();
let outer_a = canonical.find("\"aField\"").unwrap();
let outer_z = canonical.find("\"zField\"").unwrap();
assert!(outer_a < outer_z, "outer keys not sorted: {canonical}");
let inner_a = canonical.find("aInner").unwrap();
let inner_z = canonical.find("zInner").unwrap();
assert!(inner_a < inner_z, "inner keys not sorted: {canonical}");
}
#[test]
fn to_json_canonical_is_deterministic() {
let v = Sample {
z_field: "abc".into(),
a_field: 7,
m_field: None,
};
assert_eq!(
v.to_json_canonical().unwrap(),
v.to_json_canonical().unwrap()
);
}
#[test]
fn hardened_german_rejects_payload_limit() {
let json = r#"{"name":"abc"}"#;
let err = HardenedSample::from_json_german_hardened(
json,
JsonParseLimits {
max_payload_bytes: Some(5),
..JsonParseLimits::default()
},
)
.unwrap_err();
assert!(
err.to_string().contains("payload too large"),
"unexpected error: {err}"
);
}
#[test]
fn hardened_german_rejects_depth_limit() {
let json = r#"{"name":"ok","nested":{"a":{"b":1}}}"#;
let err = HardenedSample::from_json_german_hardened(
json,
JsonParseLimits {
max_nesting_depth: Some(2),
..JsonParseLimits::default()
},
)
.unwrap_err();
assert!(
err.to_string().contains("nesting depth"),
"unexpected error: {err}"
);
}
#[test]
fn hardened_german_rejects_extension_budget() {
let json = r#"{"name":"ok","x":{"very":"large extension payload"}}"#;
let err = HardenedSample::from_json_german_hardened(
json,
JsonParseLimits {
max_extension_value_bytes: Some(4),
..JsonParseLimits::default()
},
)
.unwrap_err();
assert!(
err.to_string().contains("extension value budget exceeded"),
"unexpected error: {err}"
);
}
#[test]
fn hardened_snake_case_accepts_within_limits() {
let json = r#"{"name":"ok","custom_field":"x"}"#;
let parsed = HardenedSample::from_json_snake_case_hardened(
json,
JsonParseLimits {
max_payload_bytes: Some(1024),
max_nesting_depth: Some(8),
max_extension_value_bytes: Some(64),
max_extension_field_count: Some(16),
},
)
.unwrap();
assert_eq!(parsed.name, "ok");
assert!(parsed.has_extension_data());
}
#[test]
fn hardened_bytes_paths_round_trip() {
let json = r#"{"name":"ok","extra":{"x":1}}"#.as_bytes().to_vec();
let parsed_german = HardenedSample::from_json_german_bytes_hardened(
&json.clone(),
JsonParseLimits::untrusted_defaults(),
)
.unwrap();
assert_eq!(parsed_german.name, "ok");
let snake = r#"{"name":"ok","extra_field":1}"#.as_bytes().to_vec();
let parsed_snake = HardenedSample::from_json_snake_case_bytes_hardened(
&snake.clone(),
JsonParseLimits::untrusted_defaults(),
)
.unwrap();
assert_eq!(parsed_snake.name, "ok");
}
#[test]
fn hardened_limit_counters_increment() {
let before = json_limit_hit_counters();
let json = r#"{"name":"abc"}"#;
let _ = HardenedSample::from_json_german_hardened(
json,
JsonParseLimits {
max_payload_bytes: Some(1),
..JsonParseLimits::default()
},
);
let after = json_limit_hit_counters();
assert!(
after.payload_bytes > before.payload_bytes,
"payload limit counter did not increase: before={before:?} after={after:?}"
);
}
#[cfg(feature = "serde")]
#[test]
fn identifier_deser_failure_counter_increments() {
let before = crate::identifiers::identifier_deser_failure_count();
let _ = serde_json::from_str::<crate::identifiers::MaloId>("\"41373559242\"");
let after = crate::identifiers::identifier_deser_failure_count();
assert!(
after > before,
"identifier failure counter did not increase: before={before} after={after}"
);
}
}