cultcache-rs: payload nesting bound through one decode, and the derive findings deferred to this cut

check_payload_depth walks MessagePack headers iteratively and refuses the first container past
MAX_PAYLOAD_DEPTH (256); decode_payload is the only payload decode (get, get_all,
get_all_with_keys, finish_prepared_entry, envelope validation), so write is refused through
finish_prepared_entry. Derive: nil arm slots refused naming union and key, ext arm keys classed
non-integer, generated generics and local items prefixed (types named D, S, A, E, ArmKey now
derive), slot keys capped at 65535 at definition, SlotRecord requires a hidden method only the
derives emit, DatabaseEntry/CultSlots expansions are functions the type/schema/generics refusals
are unit-tested through. Tests follow in the next commit; this commit builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-11 10:13:50 +02:00
commit ca6671aefb
2 changed files with 311 additions and 94 deletions

View file

@ -12,6 +12,13 @@ use syn::parse_macro_input;
#[proc_macro_derive(DatabaseEntry, attributes(cultcache))]
pub fn derive_database_entry(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match database_entry_codec(input) {
Ok(tokens) => tokens.into(),
Err(error) => error.to_compile_error().into(),
}
}
fn database_entry_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let ident = input.ident.clone();
let mut entry_type: Option<LitStr> = None;
let mut schema_name: Option<LitStr> = None;
@ -34,36 +41,28 @@ pub fn derive_database_entry(input: TokenStream) -> TokenStream {
Err(meta.error("unsupported cultcache attribute"))
}
});
if let Err(error) = parse_result {
return error.to_compile_error().into();
}
parse_result?;
}
let Some(entry_type) = entry_type else {
return syn::Error::new_spanned(
return Err(syn::Error::new_spanned(
ident,
"DatabaseEntry derive requires #[cultcache(type = \"...\")]",
)
.to_compile_error()
.into();
));
};
let schema_name = schema_name.unwrap_or_else(|| LitStr::new(&ident.to_string(), ident.span()));
let slots = match named_slots(input.data, &ident, "DatabaseEntry") {
Ok(slots) => slots,
Err(error) => return error.to_compile_error().into(),
};
let slots = named_slots(input.data, &ident, "DatabaseEntry")?;
let codec = slot_codec(&ident, &slots);
quote! {
Ok(quote! {
impl ::cultcache_rs::DatabaseEntry for #ident {
const TYPE: &'static str = #entry_type;
const SCHEMA_NAME: &'static str = #schema_name;
}
#codec
}
.into()
})
}
/// A named struct whose fields carry `#[cultcache(key = N)]`: the same positional
@ -72,18 +71,21 @@ pub fn derive_database_entry(input: TokenStream) -> TokenStream {
#[proc_macro_derive(CultSlots, attributes(cultcache))]
pub fn derive_cult_slots(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let ident = input.ident.clone();
if !input.generics.params.is_empty() {
return syn::Error::new_spanned(ident, "CultSlots derive does not support generics")
.to_compile_error()
.into();
}
match named_slots(input.data, &ident, "CultSlots") {
Ok(slots) => slot_codec(&ident, &slots).into(),
match cult_slots_codec(input) {
Ok(tokens) => tokens.into(),
Err(error) => error.to_compile_error().into(),
}
}
fn cult_slots_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let ident = input.ident.clone();
if !input.generics.params.is_empty() {
return Err(syn::Error::new_spanned(ident, "CultSlots derive does not support generics"));
}
let slots = named_slots(input.data, &ident, "CultSlots")?;
Ok(slot_codec(&ident, &slots))
}
/// An enum written as `[key, armSlots]`. Every variant carries `#[cultcache(key = N)]`
/// and is a unit (`[N, []]`) or a newtype over a type that serializes as an array.
/// A key the enum does not declare is a decode error; there is no catch-all arm.
@ -96,6 +98,11 @@ pub fn derive_cult_union(input: TokenStream) -> TokenStream {
}
}
/// The largest array16 length: a slot record is written as a dense array, so a key above it would
/// make every encode allocate and write an unbounded run of nils. Every runtime and the C# catalog
/// refuse the same domain.
const MAX_SLOT_KEY: usize = 65535;
fn named_slots(data: Data, ident: &syn::Ident, owner: &str) -> syn::Result<Vec<CultCacheSlot>> {
let fields = match data {
Data::Struct(data) => match data.fields {
@ -135,7 +142,14 @@ fn named_slots(data: Data, ident: &syn::Ident, owner: &str) -> syn::Result<Vec<C
if meta.path.is_ident("key") {
once(&key, &meta, "key")?;
let value: LitInt = meta.value()?.parse()?;
key = Some(value.base10_parse()?);
let slot: usize = value.base10_parse()?;
if slot > MAX_SLOT_KEY {
return Err(syn::Error::new_spanned(
&value,
format!("{owner} slot key {slot} is outside 0..={MAX_SLOT_KEY}"),
));
}
key = Some(slot);
Ok(())
} else if meta.path.is_ident("default") {
default = true;
@ -261,12 +275,14 @@ fn slot_codec(ident: &syn::Ident, slots: &[CultCacheSlot]) -> proc_macro2::Token
});
quote! {
impl ::cultcache_rs::SlotRecord for #ident {}
impl ::cultcache_rs::SlotRecord for #ident {
fn __slots_array() {}
}
impl ::serde::Serialize for #ident {
fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
fn serialize<__S>(&self, serializer: __S) -> ::std::result::Result<__S::Ok, __S::Error>
where
S: ::serde::Serializer,
__S: ::serde::Serializer,
{
use ::serde::ser::SerializeTuple;
let mut tuple = serializer.serialize_tuple(#max_slot + 1)?;
@ -276,22 +292,22 @@ fn slot_codec(ident: &syn::Ident, slots: &[CultCacheSlot]) -> proc_macro2::Token
}
impl<'de> ::serde::Deserialize<'de> for #ident {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
fn deserialize<__D>(deserializer: __D) -> ::std::result::Result<Self, __D::Error>
where
D: ::serde::Deserializer<'de>,
__D: ::serde::Deserializer<'de>,
{
struct CultCacheVisitor;
struct __CultCacheVisitor;
impl<'de> ::serde::de::Visitor<'de> for CultCacheVisitor {
impl<'de> ::serde::de::Visitor<'de> for __CultCacheVisitor {
type Value = #ident;
fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
formatter.write_str(concat!("a CultCache array for ", stringify!(#ident)))
}
fn visit_seq<A>(self, mut seq: A) -> ::std::result::Result<Self::Value, A::Error>
fn visit_seq<__A>(self, mut seq: __A) -> ::std::result::Result<Self::Value, __A::Error>
where
A: ::serde::de::SeqAccess<'de>,
__A: ::serde::de::SeqAccess<'de>,
{
#(#option_declarations)*
let mut index = 0usize;
@ -309,7 +325,7 @@ fn slot_codec(ident: &syn::Ident, slots: &[CultCacheSlot]) -> proc_macro2::Token
}
}
deserializer.deserialize_tuple(#max_slot + 1, CultCacheVisitor)
deserializer.deserialize_tuple(#max_slot + 1, __CultCacheVisitor)
}
}
}
@ -424,6 +440,7 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let unknown_message = format!("unknown arm key {{}} for union {enum_name}; declared keys: {declared}");
let arity_message = format!("union {enum_name} is not a [key, slots] pair");
let non_integer_message = format!("union {enum_name} has a non-integer arm key; declared keys: {declared}");
let nil_slots_message = format!("union {enum_name} arm {{}} has nil slots; declared keys: {declared}");
let out_of_range_message = format!("union {enum_name} has an arm key outside Int32; declared keys: {declared}");
let payload_checks = arms.iter().filter_map(|arm| arm.payload.as_ref()).map(|ty| {
quote! { assert_slot_record::<#ty>(); }
@ -455,19 +472,37 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
if let Some(ty) = &arm.payload {
quote! {
#key => {
let arm = seq
.next_element::<#ty>()?
.ok_or_else(|| ::serde::de::Error::custom(#arity_message))?;
#ident::#variant(arm)
// A nil in the slots position is not an empty record.
match seq.next_element::<::std::option::Option<#ty>>()? {
::std::option::Option::Some(::std::option::Option::Some(arm)) => #ident::#variant(arm),
::std::option::Option::Some(::std::option::Option::None) => {
return ::std::result::Result::Err(::serde::de::Error::custom(
format!(#nil_slots_message, #key),
));
}
::std::option::Option::None => {
return ::std::result::Result::Err(::serde::de::Error::custom(#arity_message));
}
}
}
}
} else {
quote! {
#key => {
seq.next_element::<[(); 0]>()
match seq
.next_element::<::std::option::Option<[(); 0]>>()
.map_err(|_| ::serde::de::Error::custom(#arity_message))?
.ok_or_else(|| ::serde::de::Error::custom(#arity_message))?;
#ident::#variant
{
::std::option::Option::Some(::std::option::Option::Some(_)) => #ident::#variant,
::std::option::Option::Some(::std::option::Option::None) => {
return ::std::result::Result::Err(::serde::de::Error::custom(
format!(#nil_slots_message, #key),
));
}
::std::option::Option::None => {
return ::std::result::Result::Err(::serde::de::Error::custom(#arity_message));
}
}
}
}
}
@ -483,9 +518,9 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
};
impl ::serde::Serialize for #ident {
fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
fn serialize<__S>(&self, serializer: __S) -> ::std::result::Result<__S::Ok, __S::Error>
where
S: ::serde::Serializer,
__S: ::serde::Serializer,
{
use ::serde::ser::SerializeTuple;
let mut tuple = serializer.serialize_tuple(2)?;
@ -497,128 +532,137 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
}
impl<'de> ::serde::Deserialize<'de> for #ident {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
fn deserialize<__D>(deserializer: __D) -> ::std::result::Result<Self, __D::Error>
where
D: ::serde::Deserializer<'de>,
__D: ::serde::Deserializer<'de>,
{
// The reference classifies the key before the arm: an integer in Int32, an
// integer outside it, or not an integer. No input value is kept or echoed.
enum ArmKey {
enum __ArmKey {
Int(i64),
OutOfRange,
NonInteger,
}
struct ArmKeyProbe(ArmKey);
struct ArmKeyVisitor;
struct __ArmKeyProbe(__ArmKey);
struct __ArmKeyVisitor;
impl<'de> ::serde::de::Visitor<'de> for ArmKeyVisitor {
type Value = ArmKeyProbe;
impl<'de> ::serde::de::Visitor<'de> for __ArmKeyVisitor {
type Value = __ArmKeyProbe;
fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
formatter.write_str("an integer arm key")
}
fn visit_i64<E>(self, value: i64) -> ::std::result::Result<Self::Value, E> {
Ok(ArmKeyProbe(ArmKey::Int(value)))
fn visit_i64<__E>(self, value: i64) -> ::std::result::Result<Self::Value, __E> {
Ok(__ArmKeyProbe(__ArmKey::Int(value)))
}
fn visit_u64<E>(self, value: u64) -> ::std::result::Result<Self::Value, E> {
Ok(ArmKeyProbe(match i64::try_from(value) {
Ok(value) => ArmKey::Int(value),
Err(_) => ArmKey::OutOfRange,
fn visit_u64<__E>(self, value: u64) -> ::std::result::Result<Self::Value, __E> {
Ok(__ArmKeyProbe(match i64::try_from(value) {
Ok(value) => __ArmKey::Int(value),
Err(_) => __ArmKey::OutOfRange,
}))
}
fn visit_bool<E>(self, _: bool) -> ::std::result::Result<Self::Value, E> {
Ok(ArmKeyProbe(ArmKey::NonInteger))
fn visit_bool<__E>(self, _: bool) -> ::std::result::Result<Self::Value, __E> {
Ok(__ArmKeyProbe(__ArmKey::NonInteger))
}
fn visit_f64<E>(self, _: f64) -> ::std::result::Result<Self::Value, E> {
Ok(ArmKeyProbe(ArmKey::NonInteger))
fn visit_f64<__E>(self, _: f64) -> ::std::result::Result<Self::Value, __E> {
Ok(__ArmKeyProbe(__ArmKey::NonInteger))
}
fn visit_str<E>(self, _: &str) -> ::std::result::Result<Self::Value, E> {
Ok(ArmKeyProbe(ArmKey::NonInteger))
fn visit_str<__E>(self, _: &str) -> ::std::result::Result<Self::Value, __E> {
Ok(__ArmKeyProbe(__ArmKey::NonInteger))
}
fn visit_bytes<E>(self, _: &[u8]) -> ::std::result::Result<Self::Value, E> {
Ok(ArmKeyProbe(ArmKey::NonInteger))
fn visit_bytes<__E>(self, _: &[u8]) -> ::std::result::Result<Self::Value, __E> {
Ok(__ArmKeyProbe(__ArmKey::NonInteger))
}
fn visit_unit<E>(self) -> ::std::result::Result<Self::Value, E> {
Ok(ArmKeyProbe(ArmKey::NonInteger))
fn visit_unit<__E>(self) -> ::std::result::Result<Self::Value, __E> {
Ok(__ArmKeyProbe(__ArmKey::NonInteger))
}
fn visit_none<E>(self) -> ::std::result::Result<Self::Value, E> {
Ok(ArmKeyProbe(ArmKey::NonInteger))
fn visit_none<__E>(self) -> ::std::result::Result<Self::Value, __E> {
Ok(__ArmKeyProbe(__ArmKey::NonInteger))
}
fn visit_some<D>(self, deserializer: D) -> ::std::result::Result<Self::Value, D::Error>
fn visit_some<__D>(self, deserializer: __D) -> ::std::result::Result<Self::Value, __D::Error>
where
D: ::serde::Deserializer<'de>,
__D: ::serde::Deserializer<'de>,
{
<::serde::de::IgnoredAny as ::serde::Deserialize>::deserialize(deserializer)?;
Ok(ArmKeyProbe(ArmKey::NonInteger))
Ok(__ArmKeyProbe(__ArmKey::NonInteger))
}
fn visit_seq<A>(self, mut seq: A) -> ::std::result::Result<Self::Value, A::Error>
// A MessagePack ext value (fixext, timestamp, ext8) reaches the visitor here.
fn visit_newtype_struct<__D>(self, deserializer: __D) -> ::std::result::Result<Self::Value, __D::Error>
where
A: ::serde::de::SeqAccess<'de>,
__D: ::serde::Deserializer<'de>,
{
<::serde::de::IgnoredAny as ::serde::Deserialize>::deserialize(deserializer)?;
Ok(__ArmKeyProbe(__ArmKey::NonInteger))
}
fn visit_seq<__A>(self, mut seq: __A) -> ::std::result::Result<Self::Value, __A::Error>
where
__A: ::serde::de::SeqAccess<'de>,
{
while seq.next_element::<::serde::de::IgnoredAny>()?.is_some() {}
Ok(ArmKeyProbe(ArmKey::NonInteger))
Ok(__ArmKeyProbe(__ArmKey::NonInteger))
}
fn visit_map<A>(self, mut map: A) -> ::std::result::Result<Self::Value, A::Error>
fn visit_map<__A>(self, mut map: __A) -> ::std::result::Result<Self::Value, __A::Error>
where
A: ::serde::de::MapAccess<'de>,
__A: ::serde::de::MapAccess<'de>,
{
while map.next_entry::<::serde::de::IgnoredAny, ::serde::de::IgnoredAny>()?.is_some() {}
Ok(ArmKeyProbe(ArmKey::NonInteger))
Ok(__ArmKeyProbe(__ArmKey::NonInteger))
}
}
impl<'de> ::serde::Deserialize<'de> for ArmKeyProbe {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
impl<'de> ::serde::Deserialize<'de> for __ArmKeyProbe {
fn deserialize<__D>(deserializer: __D) -> ::std::result::Result<Self, __D::Error>
where
D: ::serde::Deserializer<'de>,
__D: ::serde::Deserializer<'de>,
{
deserializer.deserialize_any(ArmKeyVisitor)
deserializer.deserialize_any(__ArmKeyVisitor)
}
}
struct CultUnionVisitor;
struct __CultUnionVisitor;
impl<'de> ::serde::de::Visitor<'de> for CultUnionVisitor {
impl<'de> ::serde::de::Visitor<'de> for __CultUnionVisitor {
type Value = #ident;
fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
formatter.write_str(concat!("a [key, slots] pair for union ", stringify!(#ident)))
}
fn visit_seq<A>(self, mut seq: A) -> ::std::result::Result<Self::Value, A::Error>
fn visit_seq<__A>(self, mut seq: __A) -> ::std::result::Result<Self::Value, __A::Error>
where
A: ::serde::de::SeqAccess<'de>,
__A: ::serde::de::SeqAccess<'de>,
{
if seq.size_hint().is_some_and(|len| len != 2) {
return ::std::result::Result::Err(::serde::de::Error::custom(#arity_message));
}
let key = match seq
.next_element::<ArmKeyProbe>()
.next_element::<__ArmKeyProbe>()
.map_err(|_| ::serde::de::Error::custom(#arity_message))?
.ok_or_else(|| ::serde::de::Error::custom(#arity_message))?
.0
{
ArmKey::Int(wide) => match i32::try_from(wide) {
__ArmKey::Int(wide) => match i32::try_from(wide) {
Ok(key) => key,
Err(_) => {
return ::std::result::Result::Err(::serde::de::Error::custom(#out_of_range_message));
}
},
ArmKey::OutOfRange => {
__ArmKey::OutOfRange => {
return ::std::result::Result::Err(::serde::de::Error::custom(#out_of_range_message));
}
ArmKey::NonInteger => {
__ArmKey::NonInteger => {
return ::std::result::Result::Err(::serde::de::Error::custom(#non_integer_message));
}
};
@ -637,7 +681,7 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
}
}
deserializer.deserialize_tuple(2, CultUnionVisitor)
deserializer.deserialize_tuple(2, __CultUnionVisitor)
}
}
})
@ -738,4 +782,54 @@ mod tests {
let message = union_error("enum E { #[cultcache(key = \"1\")] A(Arm) }");
assert!(message.contains("integer literal"), "{message}");
}
fn entry_result(source: &str) -> syn::Result<proc_macro2::TokenStream> {
database_entry_codec(syn::parse_str(source).expect("the fixture parses"))
}
fn slots_result(source: &str) -> syn::Result<proc_macro2::TokenStream> {
cult_slots_codec(syn::parse_str(source).expect("the fixture parses"))
}
// Each attribute names a thing once; the second would silently win otherwise.
#[test]
fn a_repeated_type_or_schema_attribute_is_refused_for_that_reason() {
for source in [
"#[cultcache(type = \"a.one\")] #[cultcache(type = \"a.two\")] struct S { #[cultcache(key = 0)] id: String }",
"#[cultcache(type = \"a.one\", type = \"a.two\")] struct S { #[cultcache(key = 0)] id: String }",
"#[cultcache(type = \"a.one\", schema = \"s.one\")] #[cultcache(schema = \"s.two\")] struct S { #[cultcache(key = 0)] id: String }",
"#[cultcache(type = \"a.one\", schema = \"s.one\", schema = \"s.two\")] struct S { #[cultcache(key = 0)] id: String }",
] {
let message = entry_result(source).expect_err(source).to_string();
assert!(message.contains("repeated `type`") || message.contains("repeated `schema`"), "{source}: {message}");
}
entry_result("#[cultcache(type = \"a.one\", schema = \"s.one\")] struct S { #[cultcache(key = 0)] id: String }")
.expect("one type and one schema are accepted");
}
#[test]
fn a_generic_slots_struct_and_a_missing_type_are_refused_for_their_reasons() {
let message = slots_result("struct S<T> { #[cultcache(key = 0)] id: T }").unwrap_err().to_string();
assert!(message.contains("does not support generics"), "{message}");
let message = entry_result("struct S { #[cultcache(key = 0)] id: String }").unwrap_err().to_string();
assert!(message.contains("requires #[cultcache(type"), "{message}");
}
// A slot record is a dense array: keys stop at the largest array16 length.
#[test]
fn the_slot_key_domain_ends_at_65535() {
slots_error_free("struct S { #[cultcache(key = 0)] a: String, #[cultcache(key = 65535)] b: String }");
for source in [
"struct S { #[cultcache(key = 65536)] a: String }",
"struct S { #[cultcache(key = 4294967296)] a: String }",
"struct S { #[cultcache(key = 0x10000)] a: String }",
] {
let message = slots_error(source);
assert!(message.contains("outside 0..=65535"), "{source}: {message}");
}
}
fn slots_error_free(source: &str) {
let input: DeriveInput = syn::parse_str(source).expect("the fixture parses");
named_slots(input.data, &input.ident, "CultSlots").expect("the fixture is accepted");
}
}

View file

@ -99,6 +99,14 @@
//! enum Bad { #[cultcache(key = 4)] A(Option<Arm>) }
//! ```
//!
//! A hand-written `SlotRecord` impl, which only the derives provide:
//!
//! ```compile_fail,E0046
//! use cultcache_rs::SlotRecord;
//! struct Sneaky(String);
//! impl SlotRecord for Sneaky {}
//! ```
//!
//! A CultSlots field without a key, a repeated slot, a repeated key attribute, and a generic struct:
//!
//! ```compile_fail
@ -163,7 +171,11 @@ pub trait DatabaseEntry: Serialize + DeserializeOwned + Clone + Send + 'static {
label = "a CultUnion arm is a newtype over a #[derive(CultSlots)] struct",
note = "a String, an Option or any other type is not written as an array of slots"
)]
pub trait SlotRecord {}
pub trait SlotRecord {
/// Implemented only by the derives, which write the positional array the contract names.
#[doc(hidden)]
fn __slots_array();
}
pub trait CultCacheRegistry {
fn register_entries(&self, cache: &mut CultCache) -> Result<()>;
@ -484,6 +496,117 @@ fn describe_undecodable(error: &anyhow::Error) -> String {
parts.join(": ")
}
/// The deepest a document payload may nest, on read and on write, in every CultCache runtime: every
/// MessagePack array and map counts, read by a deserializer or skipped past it, and the outermost
/// array is depth 1. A payload of this depth is accepted and one container deeper is refused. It is
/// the C# reference's `CultMessagePackSecurity.MaximumPayloadDepth`.
pub const MAX_PAYLOAD_DEPTH: usize = 256;
/// A payload nests more containers than [`MAX_PAYLOAD_DEPTH`]. It names the depth reached and the
/// limit, never payload bytes, and can be recovered from the `anyhow::Error` a read or write returns.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PayloadTooDeep {
pub depth: usize,
pub limit: usize,
}
impl std::fmt::Display for PayloadTooDeep {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"payload nests {} MessagePack containers deep; the limit is {}",
self.depth, self.limit
)
}
}
impl std::error::Error for PayloadTooDeep {}
/// Walks the MessagePack headers of one payload without recursion, counting every array and map and
/// refusing the first container past [`MAX_PAYLOAD_DEPTH`]. Scalars are skipped by length and nothing
/// is allocated per element. A payload that ends early is the decoder's to refuse.
pub fn check_payload_depth(bytes: &[u8]) -> std::result::Result<(), PayloadTooDeep> {
// The big-endian length after the code byte, when the payload is long enough to hold it.
fn length(bytes: &[u8], position: usize, width: usize) -> Option<usize> {
let field = bytes.get(position + 1..position + 1 + width)?;
Some(field.iter().fold(0usize, |length, byte| (length << 8) | usize::from(*byte)))
}
let mut remaining = [0usize; MAX_PAYLOAD_DEPTH];
let mut depth = 0usize;
let mut position = 0usize;
while position < bytes.len() {
let code = bytes[position];
// (bytes to advance, children to read when the element is a container)
let (advance, children) = match code {
0x00..=0x7f | 0xe0..=0xff | 0xc0..=0xc3 => (1, None),
0x80..=0x8f => (1, Some(2 * usize::from(code & 0x0f))),
0x90..=0x9f => (1, Some(usize::from(code & 0x0f))),
0xa0..=0xbf => (1 + usize::from(code & 0x1f), None),
0xc4 | 0xd9 => (2 + length(bytes, position, 1).unwrap_or(usize::MAX - 2), None),
0xc5 | 0xda => (3 + length(bytes, position, 2).unwrap_or(usize::MAX - 3), None),
0xc6 | 0xdb => (5 + length(bytes, position, 4).unwrap_or(usize::MAX - 5), None),
0xc7 => (3 + length(bytes, position, 1).unwrap_or(usize::MAX - 3), None),
0xc8 => (4 + length(bytes, position, 2).unwrap_or(usize::MAX - 4), None),
0xc9 => (6 + length(bytes, position, 4).unwrap_or(usize::MAX - 6), None),
0xca | 0xce | 0xd2 => (5, None),
0xcb | 0xcf | 0xd3 => (9, None),
0xcc | 0xd0 => (2, None),
0xcd | 0xd1 | 0xd4 => (3, None),
0xd5 => (4, None),
0xd6 => (6, None),
0xd7 => (10, None),
0xd8 => (18, None),
0xdc => match length(bytes, position, 2) {
Some(count) => (3, Some(count)),
None => return Ok(()),
},
0xdd => match length(bytes, position, 4) {
Some(count) => (5, Some(count)),
None => return Ok(()),
},
0xde => match length(bytes, position, 2) {
Some(count) => (3, Some(2 * count)),
None => return Ok(()),
},
0xdf => match length(bytes, position, 4) {
Some(count) => (5, Some(count.saturating_mul(2))),
None => return Ok(()),
},
};
position = position.saturating_add(advance);
if let Some(children) = children {
if depth == MAX_PAYLOAD_DEPTH {
return Err(PayloadTooDeep { depth: MAX_PAYLOAD_DEPTH + 1, limit: MAX_PAYLOAD_DEPTH });
}
if children > 0 {
remaining[depth] = children;
depth += 1;
continue;
}
}
// One element is complete; close every container it was the last element of.
while depth > 0 {
remaining[depth - 1] -= 1;
if remaining[depth - 1] != 0 {
break;
}
depth -= 1;
}
if depth == 0 {
return Ok(());
}
}
Ok(())
}
/// The one decode of a document payload: the nesting bound first, then the deserializer, whose own
/// depth counter (1024) is only a backstop. A failure keeps its kind and never quotes the bytes.
fn decode_payload<T: serde::de::DeserializeOwned>(payload: &[u8]) -> Result<T> {
check_payload_depth(payload)?;
rmp_serde::from_slice(payload).map_err(opaque_decode_error)
}
/// A decoder failure as an error that keeps only its kind. Every decode of stored bytes
/// goes through this or [`describe_undecodable`], so no error text quotes a stored value.
fn opaque_decode_error(error: rmp_serde::decode::Error) -> anyhow::Error {
@ -2420,7 +2543,7 @@ impl CultCache {
let Some(entry) = self.entries.get(&entry_id_parts(T::TYPE, key)) else {
return Ok(None);
};
let payload = rmp_serde::from_slice(&entry.payload).map_err(opaque_decode_error).with_context(|| {
let payload = decode_payload(&entry.payload).with_context(|| {
format!(
"failed to decode CultCache entry {:?} at key {:?} as {}",
T::TYPE,
@ -2453,7 +2576,7 @@ impl CultCache {
if entry.r#type != T::TYPE {
continue;
}
values.push(rmp_serde::from_slice(&entry.payload).map_err(opaque_decode_error).with_context(|| {
values.push(decode_payload(&entry.payload).with_context(|| {
format!(
"failed to decode CultCache entry {:?} at key {:?} as {}",
T::TYPE,
@ -2474,7 +2597,7 @@ impl CultCache {
}
values.push((
entry.key.clone(),
rmp_serde::from_slice(&entry.payload).map_err(opaque_decode_error).with_context(|| {
decode_payload(&entry.payload).with_context(|| {
format!(
"failed to decode CultCache entry {:?} at key {:?} as {}",
T::TYPE,
@ -2556,7 +2679,7 @@ impl CultCache {
key: String,
payload: Vec<u8>,
) -> Result<(CultCacheEnvelope, T)> {
let parsed: T = rmp_serde::from_slice(&payload).map_err(opaque_decode_error).with_context(|| {
let parsed: T = decode_payload(&payload).with_context(|| {
format!(
"failed to validate CultCache entry {:?} at key {:?} as {}",
T::TYPE,
@ -2692,7 +2815,7 @@ impl CultCache {
));
}
rmp_serde::from_slice(&entry.payload).map_err(opaque_decode_error).with_context(|| {
decode_payload(&entry.payload).with_context(|| {
format!(
"failed to validate CultCache envelope {:?} at key {:?} as {}",
T::TYPE,