Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions rust/bench/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions rust/candid/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand All @@ -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"
Expand Down
21 changes: 17 additions & 4 deletions rust/candid/src/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -1426,6 +1431,7 @@ enum Style {
len: usize,
expect: (Type, Type),
wire: (Type, Type),
key_text_fast: bool,
},
}

Expand Down Expand Up @@ -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();
}
Expand Down
39 changes: 39 additions & 0 deletions rust/candid/tests/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,45 @@ fn test_vector() {
);
}

/// Regression test: decoding a `BTreeMap<String, V>` where `V` is an enum containing
/// a `BTreeMap<K, V2>` 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`.
Comment thread
lwshang marked this conversation as resolved.
#[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<MapKey, u32>),
Lit(String),
}

let mut inner: BTreeMap<MapKey, u32> = BTreeMap::new();
inner.insert(MapKey::A, 1);
inner.insert(MapKey::B, 2);

let mut outer: BTreeMap<String, Value> = 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::<BTreeMap<String, Value>>(&bytes, &config).unwrap();
assert_eq!(outer, decoded);
}

#[test]
fn test_collection() {
use std::collections::{BTreeMap, BTreeSet, HashMap};
Expand Down
2 changes: 1 addition & 1 deletion rust/candid_derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
Loading