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:
parent
4ea296ae10
commit
ca6671aefb
2 changed files with 311 additions and 94 deletions
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue