cultcache-rs: the decode door names a missing or duplicated field and never quotes a value

describe_decode_error keeps serde's own value-free texts for missing and
duplicate fields (static names from the type), maps unknown-variant and
invalid-type to kind descriptions, and leaves everything else opaque.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-16 04:22:22 +02:00
commit c0a2b890f6
2 changed files with 56 additions and 1 deletions

View file

@ -732,6 +732,7 @@ fn opaque_decode_error(error: rmp_serde::decode::Error) -> anyhow::Error {
anyhow!(describe_decode_error(&error))
}
/// The text of a decode failure. Field names in it come from the type, never from the bytes.
fn describe_decode_error(error: &rmp_serde::decode::Error) -> String {
use rmp_serde::decode::Error;
match error {
@ -744,11 +745,35 @@ fn describe_decode_error(error: &rmp_serde::decode::Error) -> String {
Error::Syntax(message) | Error::Uncategorized(message) if message.starts_with(RECORD_REFUSAL) => {
return message.clone();
}
Error::Syntax(_) | Error::Uncategorized(_) => "a value does not fit the store format",
Error::Syntax(message) | Error::Uncategorized(message) => {
return describe_serde_message(message);
}
}
.to_string()
}
/// serde's own texts, kept only where they cannot carry a received value: a missing or duplicated
/// field is named by the type's static field name (checked to look like one), an unknown tag and a
/// mistyped value are described by kind. Everything else, `unknown field` included, is opaque.
fn describe_serde_message(message: &str) -> String {
for lead in ["missing field `", "duplicate field `"] {
if let Some(name) = message.strip_prefix(lead).and_then(|rest| rest.strip_suffix('`')) {
let is_name = (1..=64).contains(&name.len())
&& name.bytes().all(|byte| byte.is_ascii_alphanumeric() || byte == b'_');
if is_name {
return message.to_string();
}
}
}
if message.starts_with("unknown variant ") {
"a tag names no variant this type knows".to_string()
} else if message.starts_with("invalid type: ") {
"a value has a MessagePack type the store format does not allow".to_string()
} else {
"a value does not fit the store format".to_string()
}
}
/// The store header string, or `None` when the bytes do not open with an array whose
/// first slot is a string (the legacy envelope array starts with a map). Only the
/// leading bytes are read, so a store cut short after its header still names its format.

View file

@ -293,6 +293,36 @@ fn a_union_that_is_not_a_key_and_slots_pair_is_refused_naming_the_enum() {
}
}
// A plain derive struct: the door names a missing or duplicated field (a static name from the type)
// and describes a mistyped value by kind, never quoting it.
#[derive(Debug, serde::Deserialize)]
#[allow(dead_code)]
struct Probe {
name: String,
flag: bool,
}
#[test]
fn a_missing_or_duplicate_field_is_named_and_a_mistyped_value_is_not() {
let name = |value: &str| [&[0xa4][..], b"name", &[0xa0 | value.len() as u8][..], value.as_bytes()].concat();
let flag_true = [&[0xa4][..], b"flag", &[0xc3][..]].concat();
let missing = [&[0x81][..], &name("a")].concat();
let message = cultcache_rs::decode_payload::<Probe>(&missing).unwrap_err().to_string();
assert!(message.contains("missing field `flag`"), "{message}");
let duplicated = [&[0x83][..], &name("a"), &name("b"), &flag_true].concat();
let message = cultcache_rs::decode_payload::<Probe>(&duplicated).unwrap_err().to_string();
assert!(message.contains("duplicate field `name`"), "{message}");
let canary = "CANARY-3d9e";
let flag_text = [&[0xa4][..], b"flag", &[0xa0 | canary.len() as u8][..], canary.as_bytes()].concat();
let mistyped = [&[0x82][..], &name("a"), &flag_text].concat();
let message = cultcache_rs::decode_payload::<Probe>(&mistyped).unwrap_err().to_string();
assert!(!message.contains("CANARY"), "{message}");
assert!(message.contains("MessagePack type"), "{message}");
}
#[test]
fn the_key_decides_the_arm() {
// Tool and Part carry different slots under different keys in the two unions.