diff --git a/CHANGELOG.md b/CHANGELOG.md index 90e1e3a1..3efcfa0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 2026-05-27 + +### Candid 0.10.29 + +* Bug fixes: + + Fix `text_fast_path` leakage between nested maps: an inner map with non-text keys would fail with a "Type mismatch" error when enclosed in an outer map with text keys + ## 2026-05-20 ### Candid 0.10.28 diff --git a/Cargo.lock b/Cargo.lock index 0b6118c3..2529594e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,13 +209,13 @@ dependencies = [ [[package]] name = "candid" -version = "0.10.28" +version = "0.10.29" dependencies = [ "anyhow", "bincode", "binread", "byteorder", - "candid_derive 0.10.28", + "candid_derive 0.10.29", "candid_parser 0.3.2", "hex", "ic_principal 0.1.3", @@ -247,7 +247,7 @@ dependencies = [ [[package]] name = "candid_derive" -version = "0.10.28" +version = "0.10.29" dependencies = [ "lazy_static", "proc-macro2 1.0.86", @@ -280,7 +280,7 @@ version = "0.3.2" dependencies = [ "anyhow", "arbitrary", - "candid 0.10.28", + "candid 0.10.29", "codespan-reporting", "console", "convert_case", diff --git a/rust/bench/Cargo.lock b/rust/bench/Cargo.lock index 99c0795f..cd34cf04 100644 --- a/rust/bench/Cargo.lock +++ b/rust/bench/Cargo.lock @@ -156,7 +156,7 @@ dependencies = [ [[package]] name = "candid" -version = "0.10.28" +version = "0.10.29" dependencies = [ "anyhow", "binread", @@ -177,7 +177,7 @@ dependencies = [ [[package]] name = "candid_derive" -version = "0.10.28" +version = "0.10.29" dependencies = [ "lazy_static", "proc-macro2", diff --git a/rust/candid/Cargo.toml b/rust/candid/Cargo.toml index f5125e92..f46667c7 100644 --- a/rust/candid/Cargo.toml +++ b/rust/candid/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "candid" # sync with the version in `candid_derive/Cargo.toml` -version = "0.10.28" +version = "0.10.29" edition = "2021" rust-version.workspace = true authors = ["DFINITY Team"] @@ -16,7 +16,7 @@ keywords = ["internet-computer", "idl", "candid", "dfinity"] include = ["src", "Cargo.toml", "LICENSE", "README.md"] [dependencies] -candid_derive = { path = "../candid_derive", version = "=0.10.28" } +candid_derive = { path = "../candid_derive", version = "=0.10.29" } ic_principal = { path = "../ic_principal", version = "0.1.0" } binread = { version = "2.2", features = ["debug_template"] } byteorder = "1.5.0" diff --git a/rust/candid/src/de.rs b/rust/candid/src/de.rs index d457d945..f18a334b 100644 --- a/rust/candid/src/de.rs +++ b/rust/candid/src/de.rs @@ -1280,7 +1280,12 @@ impl<'de> de::Deserializer<'de> for &mut Deserializer<'de> { let result = visitor.visit_map(Compound::new( self, - Style::Map { len, expect, wire }, + Style::Map { + len, + expect, + wire, + key_text_fast, + }, )); self.text_fast_path = false; #[cfg(feature = "bignum")] @@ -1426,6 +1431,7 @@ enum Style { len: usize, expect: (Type, Type), wire: (Type, Type), + key_text_fast: bool, }, } @@ -1701,19 +1707,26 @@ impl<'de> de::MapAccess<'de> for Compound<'_, 'de> { ref mut len, ref expect, ref wire, + key_text_fast, } => { if *len == 0 { return Ok(None); } *len -= 1; #[cfg(feature = "bignum")] - let any_fast = self.de.text_fast_path || self.de.bignum_vec_fast_path.is_some(); + let any_fast = key_text_fast || self.de.bignum_vec_fast_path.is_some(); #[cfg(not(feature = "bignum"))] - let any_fast = self.de.text_fast_path; + let any_fast = key_text_fast; if !any_fast { self.de.add_cost(4)?; } - if !self.de.text_fast_path { + // Always set text_fast_path based on THIS map's key type. The global + // text_fast_path may be true from an enclosing map with text keys; using + // it directly would skip setting expect_type/wire_type for non-text keys + // of this (inner) map, leading to a "Type mismatch" when deserializing + // those keys. + self.de.text_fast_path = key_text_fast; + if !key_text_fast { self.de.expect_type = expect.0.clone(); self.de.wire_type = wire.0.clone(); } diff --git a/rust/candid/tests/serde.rs b/rust/candid/tests/serde.rs index 39b6ef03..c521fb0a 100644 --- a/rust/candid/tests/serde.rs +++ b/rust/candid/tests/serde.rs @@ -796,6 +796,45 @@ fn test_vector() { ); } +/// Regression test: decoding a `BTreeMap` where `V` is an enum containing +/// a `BTreeMap` with a non-text enum key used to fail with "Type mismatch". +/// +/// The bug was that `text_fast_path`, set to `true` by the outer string-keyed map's +/// `deserialize_map`, leaked into the inner map's `next_key_seed`. There it +/// suppressed the update of `expect_type`/`wire_type` for the inner key, so that +/// `deserialize_enum` on the inner key saw a stale (non-variant) `expect_type` and +/// raised a subtyping error at `de.rs:1380`. +#[test] +fn test_nested_map_non_text_key() { + use std::collections::BTreeMap; + + #[derive(CandidType, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] + enum MapKey { + A, + B, + } + + #[derive(CandidType, Deserialize, Debug, Clone, PartialEq)] + enum Value { + Fun(BTreeMap), + Lit(String), + } + + let mut inner: BTreeMap = BTreeMap::new(); + inner.insert(MapKey::A, 1); + inner.insert(MapKey::B, 2); + + let mut outer: BTreeMap = BTreeMap::new(); + outer.insert("fun".to_string(), Value::Fun(inner)); + outer.insert("lit".to_string(), Value::Lit("hello".to_string())); + + // encode → decode must round-trip without "Type mismatch" panic + let bytes = encode_one(&outer).unwrap(); + let config = get_config(); + let decoded = decode_one_with_config::>(&bytes, &config).unwrap(); + assert_eq!(outer, decoded); +} + #[test] fn test_collection() { use std::collections::{BTreeMap, BTreeSet, HashMap}; diff --git a/rust/candid_derive/Cargo.toml b/rust/candid_derive/Cargo.toml index 71b04038..6cc23929 100644 --- a/rust/candid_derive/Cargo.toml +++ b/rust/candid_derive/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "candid_derive" # sync with the version in `candid/Cargo.toml` -version = "0.10.28" +version = "0.10.29" edition = "2021" rust-version.workspace = true authors = ["DFINITY Team"]