cultcache-rs-derive: arm keys are Int32 and given once, an arm is a slots record, refusals classified like the reference

Key attributes (variant, field, entry type/schema) are refused when repeated. A CultUnion key
parses as the reference's Int32, negatives included; a key outside it is a compile error. Decode
classifies the key as the reference does: non-integer, outside Int32, undeclared (named), and a
pair of the wrong length is refused before its key is read; no value is echoed beyond the
undeclared key the reference names. The SlotRecord trait, implemented by the shared slot
generator, bounds an arm payload, so a String or Option arm is a compile error. compile_fail
fixtures use keys other than 0; the derive crate's unit tests assert each refusal's message.

Unverified: committed before the first build; the verification job runs next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-11 04:37:47 +02:00
commit c586640e81
4 changed files with 460 additions and 29 deletions

View file

@ -189,7 +189,7 @@ without pretending Rust has runtime reflection hiding under the floorboards.
A union that crosses runtimes is written `[armKey, [armSlots...]]`, the shape the C# reference writes for
`[Union(key, type)]`. Derive `CultUnion` on an enum: every variant carries `#[cultcache(key = N)]` and is a unit
(`[N, []]`) or a newtype over a `#[derive(CultSlots)]` struct, which is a `DatabaseEntry`'s slot array without the
entry consts. A key the enum does not declare is a decode error naming the enum and the declared keys, and never
entry consts (a `String` or an `Option` is not an arm, and is a compile error). An arm key is the reference's Int32, negatives included, and is given once. A key the enum does not declare is a decode error naming the enum and the declared keys, and never
quoting the payload. A null union is `Option<Enum>`. Plain serde enums are untouched and keep their bytes.
```rust

View file

@ -23,9 +23,11 @@ pub fn derive_database_entry(input: TokenStream) -> TokenStream {
{
let parse_result = attribute.parse_nested_meta(|meta| {
if meta.path.is_ident("type") {
once(&entry_type, &meta, "type")?;
entry_type = Some(meta.value()?.parse()?);
Ok(())
} else if meta.path.is_ident("schema") {
once(&schema_name, &meta, "schema")?;
schema_name = Some(meta.value()?.parse()?);
Ok(())
} else {
@ -131,6 +133,7 @@ fn named_slots(data: Data, ident: &syn::Ident, owner: &str) -> syn::Result<Vec<C
{
attribute.parse_nested_meta(|meta| {
if meta.path.is_ident("key") {
once(&key, &meta, "key")?;
let value: LitInt = meta.value()?.parse()?;
key = Some(value.base10_parse()?);
Ok(())
@ -258,6 +261,8 @@ fn slot_codec(ident: &syn::Ident, slots: &[CultCacheSlot]) -> proc_macro2::Token
});
quote! {
impl ::cultcache_rs::SlotRecord for #ident {}
impl ::serde::Serialize for #ident {
fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
where
@ -310,6 +315,31 @@ fn slot_codec(ident: &syn::Ident, slots: &[CultCacheSlot]) -> proc_macro2::Token
}
}
/// An attribute names each thing once; a second `key = N` would silently win otherwise.
fn once<T>(seen: &Option<T>, meta: &syn::meta::ParseNestedMeta, name: &str) -> syn::Result<()> {
if seen.is_some() {
return Err(meta.error(format!("repeated `{name}`: #[cultcache({name} = ...)] is given once")));
}
Ok(())
}
/// An arm key is the reference's Int32, negatives included: `key = 5` or `key = -1`.
fn arm_key(meta: &syn::meta::ParseNestedMeta) -> syn::Result<i32> {
let expr: syn::Expr = meta.value()?.parse()?;
let (negative, literal) = match expr {
syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(literal), .. }) => (false, literal),
syn::Expr::Unary(syn::ExprUnary { op: syn::UnOp::Neg(_), expr, .. }) => match *expr {
syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(literal), .. }) => (true, literal),
other => return Err(syn::Error::new_spanned(other, "a CultUnion arm key is an integer literal")),
},
other => return Err(syn::Error::new_spanned(other, "a CultUnion arm key is an integer literal")),
};
let magnitude: i128 = literal.base10_parse()?;
let value = if negative { -magnitude } else { magnitude };
i32::try_from(value)
.map_err(|_| syn::Error::new_spanned(&literal, format!("CultUnion arm key {value} is outside Int32")))
}
struct CultCacheSlot {
key: usize,
ident: syn::Ident,
@ -334,13 +364,13 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
}
struct Arm {
key: u64,
key: i32,
ident: syn::Ident,
payload: Option<Type>,
}
let mut arms: Vec<Arm> = Vec::new();
for variant in data.variants {
let mut key: Option<u64> = None;
let mut key: Option<i32> = None;
for attribute in variant
.attrs
.iter()
@ -348,8 +378,8 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
{
attribute.parse_nested_meta(|meta| {
if meta.path.is_ident("key") {
let value: LitInt = meta.value()?.parse()?;
key = Some(value.base10_parse()?);
once(&key, &meta, "key")?;
key = Some(arm_key(&meta)?);
Ok(())
} else {
Err(meta.error("unsupported variant cultcache attribute"))
@ -388,13 +418,16 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
}
let enum_name = ident.to_string();
let declared = arms
.iter()
.map(|arm| arm.key.to_string())
.collect::<Vec<_>>()
.join(", ");
let mut declared_keys: Vec<i32> = arms.iter().map(|arm| arm.key).collect();
declared_keys.sort_unstable();
let declared = declared_keys.iter().map(i32::to_string).collect::<Vec<_>>().join(", ");
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 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>(); }
});
let serialize_arms = arms.iter().map(|arm| {
let variant = &arm.ident;
@ -441,6 +474,14 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
});
Ok(quote! {
// An arm is a slots array: a newtype over anything else is refused here, by the
// trait the CultSlots and DatabaseEntry derives implement.
const _: fn() = || {
#[allow(dead_code)]
fn assert_slot_record<T: ::cultcache_rs::SlotRecord>() {}
#(#payload_checks)*
};
impl ::serde::Serialize for #ident {
fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
where
@ -460,6 +501,92 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
where
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 {
Int(i64),
OutOfRange,
NonInteger,
}
struct ArmKeyProbe(ArmKey);
struct ArmKeyVisitor;
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_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_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_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_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>
where
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))
}
fn visit_map<A>(self, mut map: A) -> ::std::result::Result<Self::Value, A::Error>
where
A: ::serde::de::MapAccess<'de>,
{
while map.next_entry::<::serde::de::IgnoredAny, ::serde::de::IgnoredAny>()?.is_some() {}
Ok(ArmKeyProbe(ArmKey::NonInteger))
}
}
impl<'de> ::serde::Deserialize<'de> for ArmKeyProbe {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
deserializer.deserialize_any(ArmKeyVisitor)
}
}
struct CultUnionVisitor;
impl<'de> ::serde::de::Visitor<'de> for CultUnionVisitor {
@ -473,10 +600,28 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
where
A: ::serde::de::SeqAccess<'de>,
{
let key = seq
.next_element::<u64>()
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>()
.map_err(|_| ::serde::de::Error::custom(#arity_message))?
.ok_or_else(|| ::serde::de::Error::custom(#arity_message))?;
.ok_or_else(|| ::serde::de::Error::custom(#arity_message))?
.0
{
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 => {
return ::std::result::Result::Err(::serde::de::Error::custom(#out_of_range_message));
}
ArmKey::NonInteger => {
return ::std::result::Result::Err(::serde::de::Error::custom(#non_integer_message));
}
};
let value = match key {
#(#deserialize_arms)*
other => {
@ -497,3 +642,100 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
}
})
}
#[cfg(test)]
mod tests {
use super::*;
fn union_error(source: &str) -> String {
let input: DeriveInput = syn::parse_str(source).expect("the fixture parses");
union_codec(input).expect_err("the fixture is refused").to_string()
}
fn slots_error(source: &str) -> String {
let input: DeriveInput = syn::parse_str(source).expect("the fixture parses");
let Err(error) = named_slots(input.data, &input.ident, "CultSlots") else {
panic!("the fixture is refused");
};
error.to_string()
}
fn union_ok(source: &str) {
let input: DeriveInput = syn::parse_str(source).expect("the fixture parses");
union_codec(input).expect("the fixture is accepted");
}
// Keys other than 0 throughout, so a default of 0 cannot stand in for a missing key.
#[test]
fn a_variant_without_a_key_is_refused_for_that_reason() {
let message = union_error("enum E { #[cultcache(key = 4)] A(Arm), B }");
assert!(message.contains("require #[cultcache(key = N)]"), "{message}");
let message = union_error("enum E { B, #[cultcache(key = 4)] A(Arm) }");
assert!(message.contains("require #[cultcache(key = N)]"), "{message}");
}
#[test]
fn two_variants_with_one_key_are_refused_for_that_reason() {
let message = union_error("enum E { #[cultcache(key = 4)] A(Arm), #[cultcache(key = 4)] B }");
assert!(message.contains("duplicate CultUnion key 4"), "{message}");
let message = union_error("enum E { #[cultcache(key = 0x4)] A(Arm), #[cultcache(key = 4)] B }");
assert!(message.contains("duplicate CultUnion key 4"), "{message}");
}
#[test]
fn a_repeated_key_attribute_is_refused_on_a_variant_and_on_a_field() {
for source in [
"enum E { #[cultcache(key = 7)] #[cultcache(key = 0)] A(Arm) }",
"enum E { #[cultcache(key = 7, key = 0)] A(Arm) }",
] {
let message = union_error(source);
assert!(message.contains("repeated `key`"), "{source}: {message}");
}
for source in [
"struct S { #[cultcache(key = 7)] #[cultcache(key = 0)] a: String }",
"struct S { #[cultcache(key = 7, key = 0)] a: String }",
] {
let message = slots_error(source);
assert!(message.contains("repeated `key`"), "{source}: {message}");
}
}
#[test]
fn a_duplicate_slot_is_refused_for_that_reason() {
let message = slots_error("struct S { #[cultcache(key = 3)] a: String, #[cultcache(key = 3)] b: String }");
assert!(message.contains("duplicate CultSlots slot 3"), "{message}");
}
#[test]
fn a_field_without_a_key_is_refused_for_that_reason() {
let message = slots_error("struct S { #[cultcache(key = 3)] a: String, b: String }");
assert!(message.contains("fields require #[cultcache(key = N)]"), "{message}");
}
#[test]
fn struct_multi_field_and_generic_variants_are_refused_for_their_reasons() {
let message = union_error("enum E { #[cultcache(key = 4)] A { id: String } }");
assert!(message.contains("use a newtype over a #[derive(CultSlots)] struct"), "{message}");
let message = union_error("enum E { #[cultcache(key = 4)] A(Arm, Arm) }");
assert!(message.contains("use a newtype over a #[derive(CultSlots)] struct"), "{message}");
let message = union_error("enum E<T> { #[cultcache(key = 4)] A(Arm), #[cultcache(key = 5)] B(T) }");
assert!(message.contains("does not support generics"), "{message}");
}
// The arm key domain is the reference's Int32: negatives declare, Int32::MAX declares,
// one past it is a compile error.
#[test]
fn the_arm_key_domain_is_int32() {
union_ok("enum E { #[cultcache(key = -1)] A(Arm), #[cultcache(key = 2147483647)] B, #[cultcache(key = -2147483648)] C }");
for source in [
"enum E { #[cultcache(key = 2147483648)] A(Arm) }",
"enum E { #[cultcache(key = -2147483649)] A(Arm) }",
"enum E { #[cultcache(key = 4294967296)] A(Arm) }",
] {
let message = union_error(source);
assert!(message.contains("outside Int32"), "{source}: {message}");
}
let message = union_error("enum E { #[cultcache(key = \"1\")] A(Arm) }");
assert!(message.contains("integer literal"), "{message}");
}
}

View file

@ -1,22 +1,32 @@
//! The derives refuse a malformed union at compile time. Each block below is a valid
//! union with one property changed.
//! union with one property changed; keys are never 0, so a refusal cannot be a default
//! key colliding with a declared 0. The derive crate's unit tests assert the message of
//! every refusal the derive itself makes; a `compile_fail` block shows only that it fails.
//!
//! ```
//! use cultcache_rs::{CultSlots, CultUnion};
//! #[derive(CultSlots)]
//! struct Arm { #[cultcache(key = 0)] id: String }
//! struct Arm { #[cultcache(key = 3)] id: String }
//! #[derive(CultUnion)]
//! enum Ok_ { #[cultcache(key = 0)] A(Arm), #[cultcache(key = 1)] B }
//! enum Fine { #[cultcache(key = -1)] A(Arm), #[cultcache(key = 2147483647)] B, #[cultcache(key = 4)] C }
//! ```
//!
//! A variant without a key:
//! A variant without a key, first and last:
//!
//! ```compile_fail
//! use cultcache_rs::{CultSlots, CultUnion};
//! #[derive(CultSlots)]
//! struct Arm { #[cultcache(key = 0)] id: String }
//! struct Arm { #[cultcache(key = 3)] id: String }
//! #[derive(CultUnion)]
//! enum Bad { #[cultcache(key = 0)] A(Arm), B }
//! enum Bad { #[cultcache(key = 4)] A(Arm), B }
//! ```
//!
//! ```compile_fail
//! use cultcache_rs::{CultSlots, CultUnion};
//! #[derive(CultSlots)]
//! struct Arm { #[cultcache(key = 3)] id: String }
//! #[derive(CultUnion)]
//! enum Bad { B, #[cultcache(key = 4)] A(Arm) }
//! ```
//!
//! Two variants with one key:
@ -24,9 +34,25 @@
//! ```compile_fail
//! use cultcache_rs::{CultSlots, CultUnion};
//! #[derive(CultSlots)]
//! struct Arm { #[cultcache(key = 0)] id: String }
//! struct Arm { #[cultcache(key = 3)] id: String }
//! #[derive(CultUnion)]
//! enum Bad { #[cultcache(key = 0)] A(Arm), #[cultcache(key = 0)] B }
//! enum Bad { #[cultcache(key = 4)] A(Arm), #[cultcache(key = 4)] B }
//! ```
//!
//! A key attribute given twice:
//!
//! ```compile_fail
//! use cultcache_rs::CultUnion;
//! #[derive(CultUnion)]
//! enum Bad { #[cultcache(key = 4)] #[cultcache(key = 5)] A }
//! ```
//!
//! A key outside Int32:
//!
//! ```compile_fail
//! use cultcache_rs::CultUnion;
//! #[derive(CultUnion)]
//! enum Bad { #[cultcache(key = 2147483648)] A }
//! ```
//!
//! A struct variant:
@ -34,7 +60,7 @@
//! ```compile_fail
//! use cultcache_rs::CultUnion;
//! #[derive(CultUnion)]
//! enum Bad { #[cultcache(key = 0)] A { id: String }, #[cultcache(key = 1)] B }
//! enum Bad { #[cultcache(key = 4)] A { id: String }, #[cultcache(key = 5)] B }
//! ```
//!
//! A multi-field tuple variant:
@ -42,9 +68,9 @@
//! ```compile_fail
//! use cultcache_rs::{CultSlots, CultUnion};
//! #[derive(CultSlots)]
//! struct Arm { #[cultcache(key = 0)] id: String }
//! struct Arm { #[cultcache(key = 3)] id: String }
//! #[derive(CultUnion)]
//! enum Bad { #[cultcache(key = 0)] A(Arm, Arm), #[cultcache(key = 1)] B }
//! enum Bad { #[cultcache(key = 4)] A(Arm, Arm), #[cultcache(key = 5)] B }
//! ```
//!
//! A generic enum:
@ -52,23 +78,51 @@
//! ```compile_fail
//! use cultcache_rs::{CultSlots, CultUnion};
//! #[derive(CultSlots)]
//! struct Arm { #[cultcache(key = 0)] id: String }
//! struct Arm { #[cultcache(key = 3)] id: String }
//! #[derive(CultUnion)]
//! enum Bad<T> { #[cultcache(key = 0)] A(Arm), #[cultcache(key = 1)] B(std::marker::PhantomData<T>) }
//! enum Bad<T> { #[cultcache(key = 4)] A(Arm), #[cultcache(key = 5)] B(std::marker::PhantomData<T>) }
//! ```
//!
//! A CultSlots field without a key, and a generic CultSlots struct:
//! An arm that is not a slots record (rustc's `SlotRecord` bound, not the derive, refuses it):
//!
//! ```compile_fail
//! use cultcache_rs::CultUnion;
//! #[derive(CultUnion)]
//! enum Bad { #[cultcache(key = 4)] A(String) }
//! ```
//!
//! ```compile_fail
//! use cultcache_rs::{CultSlots, CultUnion};
//! #[derive(CultSlots)]
//! struct Arm { #[cultcache(key = 3)] id: String }
//! #[derive(CultUnion)]
//! enum Bad { #[cultcache(key = 4)] A(Option<Arm>) }
//! ```
//!
//! A CultSlots field without a key, a repeated slot, a repeated key attribute, and a generic struct:
//!
//! ```compile_fail
//! use cultcache_rs::CultSlots;
//! #[derive(CultSlots)]
//! struct Arm { #[cultcache(key = 0)] id: String, name: String }
//! struct Arm { #[cultcache(key = 3)] id: String, name: String }
//! ```
//!
//! ```compile_fail
//! use cultcache_rs::CultSlots;
//! #[derive(CultSlots)]
//! struct Arm<T> { #[cultcache(key = 0)] id: T }
//! struct Arm { #[cultcache(key = 3)] id: String, #[cultcache(key = 3)] name: String }
//! ```
//!
//! ```compile_fail
//! use cultcache_rs::CultSlots;
//! #[derive(CultSlots)]
//! struct Arm { #[cultcache(key = 3)] #[cultcache(key = 4)] id: String }
//! ```
//!
//! ```compile_fail
//! use cultcache_rs::CultSlots;
//! #[derive(CultSlots)]
//! struct Arm<T> { #[cultcache(key = 3)] id: T }
//! ```
use anyhow::Context;
@ -102,6 +156,15 @@ pub trait DatabaseEntry: Serialize + DeserializeOwned + Clone + Send + 'static {
const SCHEMA_NAME: &'static str = "DatabaseEntry";
}
/// A type written as a positional slots array: what the `CultSlots` and `DatabaseEntry`
/// derives implement, and what a `CultUnion` arm must carry.
#[diagnostic::on_unimplemented(
message = "`{Self}` is not a slots record",
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 CultCacheRegistry {
fn register_entries(&self, cache: &mut CultCache) -> Result<()>;
}

View file

@ -293,6 +293,132 @@ fn union_store_reads() {
assert_eq!(cache.get::<UnionSurface>("surface").unwrap(), Some(constructed()));
}
// The arm key domain is the reference's Int32. Expected bytes are what MessagePack-CSharp writes
// for the same keys (WriteInt32: fixneg, uint32, int32); the refusal classes are CultUnionGuard's.
mod arm_keys {
use cultcache_rs::CultSlots;
use cultcache_rs::CultUnion;
#[derive(Clone, Debug, PartialEq, CultSlots)]
struct Count {
#[cultcache(key = 0)]
name: String,
#[cultcache(key = 1)]
count: i32,
}
#[derive(Clone, Debug, PartialEq, CultUnion)]
enum Keyed {
#[cultcache(key = -1)]
Negative(Count),
#[cultcache(key = 2147483647)]
Max(Count),
#[cultcache(key = -2147483648)]
Min,
#[cultcache(key = 7)]
Plain(Count),
}
fn count() -> Count {
Count { name: "n".into(), count: 1 }
}
fn message(bytes: &[u8]) -> String {
rmp_serde::from_slice::<Keyed>(bytes).unwrap_err().to_string()
}
#[test]
fn negative_and_int32_max_keys_are_declared_and_round_trip() {
let cases: [(Keyed, &[u8]); 4] = [
(Keyed::Negative(count()), &[0x92, 0xff, 0x92, 0xa1, b'n', 0x01]),
(Keyed::Max(count()), &[0x92, 0xce, 0x7f, 0xff, 0xff, 0xff, 0x92, 0xa1, b'n', 0x01]),
(Keyed::Min, &[0x92, 0xd2, 0x80, 0x00, 0x00, 0x00, 0x90]),
(Keyed::Plain(count()), &[0x92, 0x07, 0x92, 0xa1, b'n', 0x01]),
];
for (value, bytes) in cases {
assert_eq!(rmp_serde::to_vec(&value).unwrap(), bytes, "{value:?}");
assert_eq!(rmp_serde::from_slice::<Keyed>(bytes).unwrap(), value);
}
}
// Every integer width of a negative key reads as the same arm.
#[test]
fn a_negative_key_reads_in_every_integer_width() {
for key in [&[0xff][..], &[0xd0, 0xff], &[0xd1, 0xff, 0xff], &[0xd2, 0xff, 0xff, 0xff, 0xff], &[0xd3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]] {
let mut bytes = vec![0x92];
bytes.extend_from_slice(key);
bytes.extend_from_slice(&[0x92, 0xa1, b'n', 0x01]);
assert_eq!(rmp_serde::from_slice::<Keyed>(&bytes).unwrap(), Keyed::Negative(count()), "{key:02x?}");
}
}
fn pair(key: &[u8]) -> Vec<u8> {
let mut bytes = vec![0x92];
bytes.extend_from_slice(key);
bytes.extend_from_slice(&[0x92, 0xa1, b'C', 0xa1, b'A']);
bytes
}
// A negative key the union does not declare is an unknown arm naming the key, as in the reference.
#[test]
fn a_negative_undeclared_key_is_an_unknown_arm_naming_the_key() {
for (key, shown) in [(&[0xe0][..], "-32"), (&[0xd0, 0xdf], "-33"), (&[0xd1, 0x80, 0x00], "-32768"), (&[0xd2, 0x80, 0x00, 0x00, 0x01], "-2147483647")] {
let text = message(&pair(key));
assert!(text.contains(&format!("unknown arm key {shown};")), "{text}");
assert!(text.contains("declared keys: -2147483648, -1, 7, 2147483647"), "{text}");
assert!(!text.contains("CA"), "{text}");
}
}
// Outside Int32 is its own class, naming neither the value nor the payload.
#[test]
fn keys_outside_int32_are_their_own_class_and_never_echoed() {
let wide: [&[u8]; 6] = [
&[0xce, 0x80, 0x00, 0x00, 0x00],
&[0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00],
&[0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
&[0xd3, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xff, 0xff],
&[0xd3, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
&[0xcf, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
];
for key in wide {
let text = message(&pair(key));
assert!(text.contains("an arm key outside Int32"), "{key:02x?}: {text}");
assert!(text.contains("Keyed") && !text.contains("CA"), "{text}");
for value in ["4294967296", "9223372036854775807", "9223372036854775808", "18446744073709551615"] {
assert!(!text.contains(value), "{text}");
}
}
}
#[test]
fn a_key_that_is_not_an_integer_is_its_own_class_and_never_echoed() {
let keys: [&[u8]; 7] = [
&[0xc0],
&[0xc3],
&[0xcb, 0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
&[0xa6, b'C', b'A', b'N', b'A', b'R', b'Y'],
&[0xc4, 0x02, b'C', b'A'],
&[0x91, 0xa6, b'C', b'A', b'N', b'A', b'R', b'Y'],
&[0x81, 0xa6, b'C', b'A', b'N', b'A', b'R', b'Y', 0x01],
];
for key in keys {
let text = message(&pair(key));
assert!(text.contains("a non-integer arm key") && text.contains("Keyed"), "{key:02x?}: {text}");
assert!(!text.contains("CANARY") && !text.contains("CA"), "{text}");
}
}
// Anything that is not a two-element array is the arity class, whatever its first element is.
#[test]
fn a_pair_of_the_wrong_length_is_refused_before_its_key_is_read() {
for bytes in [&[0x91, 0xa1, b'x'][..], &[0x93, 0x07, 0x90, 0x00], &[0x90]] {
let text = message(bytes);
assert!(text.contains("is not a [key, slots] pair"), "{bytes:02x?}: {text}");
}
}
}
mod serde_enum_bytes {
use cultcache_rs::DatabaseEntry;