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:
parent
a3a03be589
commit
c0a2b890f6
2 changed files with 56 additions and 1 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue