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, ExtensionInsertError, LimitedExtensionMap,
MAX_EXTENSION_FIELDS, MAX_EXTENSION_KEY_LEN,
};
#[cfg(feature = "versioned")]
pub use extension::{Bo4eExtensions, UnknownFieldError};
pub use limits::{
json_limit_hit_counters, JsonLimitHitCounters, JsonParseLimits, DEFAULT_MAX_NESTING_DEPTH,
};
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,
KeyTransform,
};
#[cfg(feature = "tracing")]
use limits::trace_json_outcome;
use limits::{
check_payload_limit, deserialize_german_from_slice, deserialize_german_from_str,
deserialize_german_from_value, install_extension_budget, resolved_max_depth, 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, KeyTransform::new(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,
KeyTransform::new(snake_to_camel),
DEFAULT_MAX_NESTING_DEPTH,
)
},
)
}
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,
KeyTransform::new(snake_to_camel),
DEFAULT_MAX_NESTING_DEPTH,
)
},
)
}
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, DEFAULT_MAX_NESTING_DEPTH),
)
}
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, DEFAULT_MAX_NESTING_DEPTH),
)
}
fn from_json_value(value: serde_json::Value) -> Result<Self, serde_json::Error> {
traced_deserialize::<Self, _>(
"deserialize",
"german_value",
0,
"BO4E deserialization failed in from_json_value",
|| deserialize_german_from_value::<Self>(value, DEFAULT_MAX_NESTING_DEPTH),
)
}
fn from_json_value_hardened(
value: serde_json::Value,
limits: JsonParseLimits,
) -> Result<Self, serde_json::Error> {
traced_deserialize::<Self, _>(
"deserialize_hardened",
"german_value",
0,
"BO4E hardened deserialization failed in from_json_value_hardened",
|| {
let _budget = install_extension_budget(limits);
deserialize_german_from_value::<Self>(value, resolved_max_depth(limits))
},
)
}
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);
deserialize_german_from_str::<Self>(s, resolved_max_depth(limits))
},
)
}
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);
deserialize_with_key_transform_from_str::<Self>(
s,
KeyTransform::new(snake_to_camel),
resolved_max_depth(limits),
)
},
)
}
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);
deserialize_german_from_slice::<Self>(bytes, resolved_max_depth(limits))
},
)
}
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);
deserialize_with_key_transform_from_slice::<Self>(
bytes,
KeyTransform::new(snake_to_camel),
resolved_max_depth(limits),
)
},
)
}
}
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> SortedSeq<S> {
fn push<T, E>(&mut self, value: &T) -> Result<(), E>
where
T: ?Sized + Serialize,
E: serde::ser::Error,
{
let mut vbuf = Vec::new();
{
let mut vser = serde_json::Serializer::new(&mut vbuf);
value
.serialize(SortedSerializer { inner: &mut vser })
.map_err(|e| E::custom(e.to_string()))?;
}
self.buf.push(vbuf);
Ok(())
}
}
impl<S> SortedSeq<S> {
fn take_raw<E: serde::ser::Error>(
&mut self,
) -> Result<Vec<Box<serde_json::value::RawValue>>, E> {
self.buf
.drain(..)
.map(|vbuf| {
serde_json::value::RawValue::from_string(
String::from_utf8(vbuf).expect("serde_json always produces valid UTF-8"),
)
.map_err(E::custom)
})
.collect()
}
}
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> {
self.push(value)
}
fn end(mut self) -> Result<Self::Ok, Self::Error> {
let items = self.take_raw()?;
let mut seq = self.inner.serialize_seq(Some(items.len()))?;
for raw in &items {
seq.serialize_element(raw)?;
}
seq.end()
}
}
impl<S: serde::ser::Serializer> serde::ser::SerializeTuple for SortedSeq<S> {
type Ok = S::Ok;
type Error = S::Error;
fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
self.push(value)
}
fn end(mut self) -> Result<Self::Ok, Self::Error> {
let items = self.take_raw()?;
let mut tuple = self.inner.serialize_tuple(items.len())?;
for raw in &items {
tuple.serialize_element(raw)?;
}
tuple.end()
}
}
impl<S: serde::ser::Serializer> serde::ser::SerializeTupleStruct for SortedSeq<S> {
type Ok = S::Ok;
type Error = S::Error;
fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
self.push(value)
}
fn end(mut self) -> Result<Self::Ok, Self::Error> {
use serde::ser::SerializeTuple as _;
let items = self.take_raw()?;
let mut tuple = self.inner.serialize_tuple(items.len())?;
for raw in &items {
tuple.serialize_element(raw)?;
}
tuple.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 = SortedSeq<S>;
type SerializeTupleStruct = SortedSeq<S>;
type SerializeTupleVariant = SortedVariant<S>;
type SerializeMap = SortedMap<S>;
type SerializeStruct = SortedMap<S>;
type SerializeStructVariant = SortedVariant<S>;
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> {
Ok(SortedSeq {
inner: self.inner,
buf: Vec::new(),
})
}
fn serialize_tuple_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleStruct, S::Error> {
Ok(SortedSeq {
inner: self.inner,
buf: Vec::new(),
})
}
fn serialize_tuple_variant(
self,
name: &'static str,
vi: u32,
v: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleVariant, S::Error> {
Ok(SortedVariant::new(self.inner, name, vi, v, Shape::Seq))
}
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> {
Ok(SortedVariant::new(self.inner, name, vi, v, Shape::Map))
}
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)
}
}
#[derive(Clone, Copy)]
enum Shape {
Seq,
Map,
}
struct SortedVariant<S> {
inner: S,
name: &'static str,
index: u32,
variant: &'static str,
shape: Shape,
items: Vec<Vec<u8>>,
entries: Vec<(String, Vec<u8>)>,
}
impl<S> SortedVariant<S> {
fn new(inner: S, name: &'static str, index: u32, variant: &'static str, shape: Shape) -> Self {
Self {
inner,
name,
index,
variant,
shape,
items: Vec::new(),
entries: Vec::new(),
}
}
fn buffer<T, E>(value: &T) -> Result<Vec<u8>, E>
where
T: ?Sized + Serialize,
E: serde::ser::Error,
{
let mut buf = Vec::new();
{
let mut ser = serde_json::Serializer::new(&mut buf);
value
.serialize(SortedSerializer { inner: &mut ser })
.map_err(|e| E::custom(e.to_string()))?;
}
Ok(buf)
}
}
impl<S: serde::ser::Serializer> SortedVariant<S> {
fn finish(mut self) -> Result<S::Ok, S::Error> {
let mut payload = Vec::new();
match self.shape {
Shape::Seq => {
payload.push(b'[');
for (i, item) in self.items.drain(..).enumerate() {
if i > 0 {
payload.push(b',');
}
payload.extend_from_slice(&item);
}
payload.push(b']');
}
Shape::Map => {
self.entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
payload.push(b'{');
for (i, (key, value)) in self.entries.drain(..).enumerate() {
if i > 0 {
payload.push(b',');
}
serde_json::to_writer(&mut payload, &key).map_err(serde::ser::Error::custom)?;
payload.push(b':');
payload.extend_from_slice(&value);
}
payload.push(b'}');
}
}
let raw = serde_json::value::RawValue::from_string(
String::from_utf8(payload).expect("serde_json always produces valid UTF-8"),
)
.map_err(serde::ser::Error::custom)?;
self.inner
.serialize_newtype_variant(self.name, self.index, self.variant, &raw)
}
}
impl<S: serde::ser::Serializer> serde::ser::SerializeTupleVariant for SortedVariant<S> {
type Ok = S::Ok;
type Error = S::Error;
fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
self.items.push(Self::buffer(value)?);
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
self.finish()
}
}
impl<S: serde::ser::Serializer> serde::ser::SerializeStructVariant for SortedVariant<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> {
let buf = Self::buffer(value)?;
self.entries.push((key.to_owned(), buf));
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
self.finish()
}
}
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_sorts_inside_every_serde_shape() {
#[derive(Serialize, Deserialize)]
struct Inner {
#[serde(rename = "zInner")]
z: u32,
#[serde(rename = "aInner")]
a: u32,
}
#[derive(Serialize, Deserialize)]
struct Tup(Inner, Inner);
#[derive(Serialize, Deserialize)]
enum Shaped {
Tuple(Inner, u32),
Struct {
#[serde(rename = "zField")]
z: Inner,
#[serde(rename = "aField")]
a: u32,
},
}
#[derive(Serialize, Deserialize)]
struct Wrapper {
plain_tuple: (Inner, u32),
tuple_struct: Tup,
tuple_variant: Shaped,
struct_variant: Shaped,
seq: Vec<Inner>,
}
impl sealed::Sealed for Wrapper {}
impl Bo4eJsonExt for Wrapper {}
let inner = || Inner { z: 1, a: 2 };
let v = Wrapper {
plain_tuple: (inner(), 7),
tuple_struct: Tup(inner(), inner()),
tuple_variant: Shaped::Tuple(inner(), 7),
struct_variant: Shaped::Struct { z: inner(), a: 9 },
seq: vec![inner()],
};
let canonical = v.to_json_canonical().unwrap();
assert!(
!canonical.contains(r#""zInner":1,"aInner""#),
"an inner object came out unsorted: {canonical}"
);
assert_eq!(
canonical.matches(r#""aInner":2,"zInner":1"#).count(),
6,
"expected all six nested objects sorted: {canonical}"
);
assert!(
canonical.contains(r#""Struct":{"aField":9,"zField""#),
"struct variant lost its tag or its ordering: {canonical}"
);
assert!(
canonical.contains(r#""Tuple":["#),
"tuple variant lost its tag: {canonical}"
);
assert!(
canonical.contains(r#""Tuple":[{"aInner":2,"zInner":1},7]"#),
"tuple variant element order changed: {canonical}"
);
let _: serde_json::Value = serde_json::from_str(&canonical).expect("valid JSON");
}
#[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::unlimited().with_max_payload_bytes(Some(5)),
)
.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::unlimited().with_max_nesting_depth(Some(2)),
)
.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::unlimited().with_max_extension_value_bytes(Some(4)),
)
.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::unlimited()
.with_max_payload_bytes(Some(1024))
.with_max_nesting_depth(Some(8))
.with_max_extension_value_bytes(Some(64))
.with_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::unlimited().with_max_payload_bytes(Some(1)),
);
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}"
);
}
}