From 338481fe44295f796c961fa6f97d5f6277b94857 Mon Sep 17 00:00:00 2001 From: Michael Weigelt Date: Fri, 13 Feb 2026 14:41:11 +0000 Subject: [PATCH 1/3] wip --- rust/candid/src/types/bounded_vec.rs | 463 +++++++++++++++++++++++++++ rust/candid/src/types/mod.rs | 1 + 2 files changed, 464 insertions(+) create mode 100644 rust/candid/src/types/bounded_vec.rs diff --git a/rust/candid/src/types/bounded_vec.rs b/rust/candid/src/types/bounded_vec.rs new file mode 100644 index 00000000..63068cd1 --- /dev/null +++ b/rust/candid/src/types/bounded_vec.rs @@ -0,0 +1,463 @@ +use data_size::DataSize; +use serde::{Deserialize, Deserializer}; +use std::{fmt, rc::Rc}; + +use crate::{ + types::{Field, Label, TypeInner}, + CandidType, +}; + +/// Indicates that `BoundedVec<...>` template parameter (eg. length, total data size, etc) is unbounded. +pub const UNBOUNDED: usize = usize::MAX; + +/// Struct for bounding vector by different parameters: +/// - number of elements +/// - total data size in bytes +/// - single element data size in bytes +#[derive(Clone, Eq, PartialEq, Debug, Default)] +pub struct BoundedVec< + const MAX_ALLOWED_LEN: usize, + const MAX_ALLOWED_TOTAL_DATA_SIZE: usize, + const MAX_ALLOWED_ELEMENT_DATA_SIZE: usize, + T, +>(Vec); + +impl< + const MAX_ALLOWED_LEN: usize, + const MAX_ALLOWED_TOTAL_DATA_SIZE: usize, + const MAX_ALLOWED_ELEMENT_DATA_SIZE: usize, + T: CandidType, + > CandidType + for BoundedVec +{ + fn _ty() -> super::Type { + TypeInner::Vec(T::_ty()).into() + // TypeInner::Record(vec![Field { + // id: Rc::new(Label::Unnamed(0)), + // ty: TypeInner::Vec(T::_ty()).into(), + // }]) + // .into() + } + + fn idl_serialize(&self, serializer: S) -> Result<(), S::Error> + where + S: super::Serializer, + { + self.0.idl_serialize(serializer) + } +} + +impl< + const MAX_ALLOWED_LEN: usize, + const MAX_ALLOWED_TOTAL_DATA_SIZE: usize, + const MAX_ALLOWED_ELEMENT_DATA_SIZE: usize, + T, + > BoundedVec +{ + pub fn new(data: Vec) -> Self { + assert!( + MAX_ALLOWED_LEN != UNBOUNDED + || MAX_ALLOWED_TOTAL_DATA_SIZE != UNBOUNDED + || MAX_ALLOWED_ELEMENT_DATA_SIZE != UNBOUNDED, + "BoundedVec must be bounded by at least one parameter." + ); + + Self(data) + } + + pub fn get(&self) -> &Vec { + &self.0 + } +} + +impl< + 'de, + const MAX_ALLOWED_LEN: usize, + const MAX_ALLOWED_TOTAL_DATA_SIZE: usize, + const MAX_ALLOWED_ELEMENT_DATA_SIZE: usize, + T: Deserialize<'de> + DataSize, + > Deserialize<'de> + for BoundedVec +{ + fn deserialize>(deserializer: D) -> Result { + struct SeqVisitor< + const MAX_ALLOWED_LEN: usize, + const MAX_ALLOWED_TOTAL_DATA_SIZE: usize, + const MAX_ALLOWED_ELEMENT_DATA_SIZE: usize, + T, + > { + _marker: std::marker::PhantomData, + } + + use serde::de::{SeqAccess, Visitor}; + + impl< + 'de, + const MAX_ALLOWED_LEN: usize, + const MAX_ALLOWED_TOTAL_DATA_SIZE: usize, + const MAX_ALLOWED_ELEMENT_DATA_SIZE: usize, + T: Deserialize<'de> + DataSize, + > Visitor<'de> + for SeqVisitor< + MAX_ALLOWED_LEN, + MAX_ALLOWED_TOTAL_DATA_SIZE, + MAX_ALLOWED_ELEMENT_DATA_SIZE, + T, + > + { + type Value = BoundedVec< + MAX_ALLOWED_LEN, + MAX_ALLOWED_TOTAL_DATA_SIZE, + MAX_ALLOWED_ELEMENT_DATA_SIZE, + T, + >; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!( + formatter, + "{}", + describe_sequence( + MAX_ALLOWED_LEN, + MAX_ALLOWED_TOTAL_DATA_SIZE, + MAX_ALLOWED_ELEMENT_DATA_SIZE, + ) + ) + } + + fn visit_seq(self, mut seq: S) -> Result + where + S: SeqAccess<'de>, + { + let mut total_data_size = 0; + let mut elements = if MAX_ALLOWED_LEN == UNBOUNDED { + Vec::new() + } else { + Vec::with_capacity(MAX_ALLOWED_LEN) + }; + while let Some(element) = seq.next_element::()? { + if elements.len() >= MAX_ALLOWED_LEN { + return Err(serde::de::Error::custom(format!( + "The number of elements exceeds maximum allowed {MAX_ALLOWED_LEN}" + ))); + } + // Check that the new element data size is below the maximum allowed limit. + let new_element_data_size = element.data_size(); + if new_element_data_size > MAX_ALLOWED_ELEMENT_DATA_SIZE { + return Err(serde::de::Error::custom(format!( + "The single element data size exceeds maximum allowed {MAX_ALLOWED_ELEMENT_DATA_SIZE}" + ))); + } + // Check that the new total data size (including new element data size) + // is below the maximum allowed limit. + let new_total_data_size = total_data_size + new_element_data_size; + if new_total_data_size > MAX_ALLOWED_TOTAL_DATA_SIZE { + return Err(serde::de::Error::custom(format!( + "The total data size exceeds maximum allowed {MAX_ALLOWED_TOTAL_DATA_SIZE}" + ))); + } + total_data_size = new_total_data_size; + elements.push(element); + } + Ok(BoundedVec::new(elements)) + } + } + + deserializer.deserialize_seq(SeqVisitor::< + MAX_ALLOWED_LEN, + MAX_ALLOWED_TOTAL_DATA_SIZE, + MAX_ALLOWED_ELEMENT_DATA_SIZE, + T, + > { + _marker: std::marker::PhantomData, + }) + } +} + +fn describe_sequence( + max_allowed_len: usize, + max_allowed_total_data_size: usize, + max_allowed_element_data_size: usize, +) -> String { + let mut msg = String::new(); + if max_allowed_len != UNBOUNDED { + msg.push_str(&format!("max {max_allowed_len} elements")); + }; + if max_allowed_total_data_size != UNBOUNDED { + if !msg.is_empty() { + msg.push_str(", "); + } + msg.push_str(&format!("max {max_allowed_total_data_size} bytes total")); + }; + if max_allowed_element_data_size != UNBOUNDED { + if !msg.is_empty() { + msg.push_str(", "); + } + msg.push_str(&format!( + "max {max_allowed_element_data_size} bytes per element" + )); + }; + format!("a sequence with {msg}") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Decode, Encode}; + + #[test] + fn test_describe_sequence() { + assert_eq!( + describe_sequence(42, UNBOUNDED, UNBOUNDED), + "a sequence with max 42 elements".to_string() + ); + assert_eq!( + describe_sequence(UNBOUNDED, 256, UNBOUNDED), + "a sequence with max 256 bytes total".to_string(), + ); + assert_eq!( + describe_sequence(UNBOUNDED, UNBOUNDED, 64), + "a sequence with max 64 bytes per element".to_string(), + ); + assert_eq!( + describe_sequence(42, 256, UNBOUNDED), + "a sequence with max 42 elements, max 256 bytes total".to_string(), + ); + assert_eq!( + describe_sequence(42, UNBOUNDED, 64), + "a sequence with max 42 elements, max 64 bytes per element".to_string(), + ); + assert_eq!( + describe_sequence(UNBOUNDED, 256, 64), + "a sequence with max 256 bytes total, max 64 bytes per element".to_string(), + ); + assert_eq!( + describe_sequence(42, 256, 64), + "a sequence with max 42 elements, max 256 bytes total, max 64 bytes per element" + .to_string(), + ); + } + + #[test] + #[should_panic] + fn test_not_bounded_vector_fails() { + type NotBoundedVec = BoundedVec; + + let _ = NotBoundedVec::new(vec![1, 2, 3]); + } + + #[test] + fn test_bounded_vector_lengths() { + // This test verifies that the structures containing BoundedVec correctly + // throw an error when the number of elements exceeds the maximum allowed. + type BoundedLen = BoundedVec; + + const MAX_ALLOWED_LEN: usize = 30; + const TEST_START: usize = 20; + const TEST_END: usize = 40; + for i in TEST_START..=TEST_END { + // Arrange. + let data = BoundedLen::new(vec![42; i]); + + // Act. + let result = Decode!(&Encode!(&data).unwrap(), BoundedLen); + println!("{:?}", result); + + // Assert. + if i <= MAX_ALLOWED_LEN { + // Verify decoding without errors for allowed sizes. + assert!(result.is_ok()); + assert_eq!(result.unwrap(), data); + } else { + // Verify decoding with errors for disallowed sizes. + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!( + error.to_string().contains(&format!( + "Deserialize error: The number of elements exceeds maximum allowed {MAX_ALLOWED_LEN}" + )), + "Actual: {}", + error.to_string() + ); + } + } + } + + #[test] + fn test_bounded_vector_total_data_sizes() { + // This test verifies that the structures containing BoundedVec correctly + // throw an error when the total data size exceeds the maximum allowed. + const MAX_ALLOWED_TOTAL_DATA_SIZE: usize = 100; + const ELEMENT_SIZE: usize = 37; + // Assert element size is not a multiple of total size. + assert_ne!(MAX_ALLOWED_TOTAL_DATA_SIZE % ELEMENT_SIZE, 0); + for aimed_total_size in 64..=256 { + // Arrange. + type BoundedSize = + BoundedVec>; + let element = vec![b'a'; ELEMENT_SIZE - std::mem::size_of::>()]; + let elements_count = aimed_total_size / element.data_size(); + let data = BoundedSize::new(vec![element; elements_count]); + let actual_total_size = data.get().data_size(); + + // Act. + let result = Decode!(&Encode!(&data).unwrap(), BoundedSize); + + // Assert. + if actual_total_size <= MAX_ALLOWED_TOTAL_DATA_SIZE { + // Verify decoding without errors for allowed sizes. + assert!(result.is_ok()); + assert_eq!(result.unwrap(), data); + } else { + // Verify decoding with errors for disallowed sizes. + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!( + error.to_string().contains(&format!( + "Deserialize error: The total data size exceeds maximum allowed {MAX_ALLOWED_TOTAL_DATA_SIZE}" + )), + "Actual: {}", + error.to_string() + ); + } + } + } + + #[test] + fn test_bounded_vector_element_data_sizes() { + // This test verifies that the structures containing BoundedVec correctly + // throw an error when the element data size exceeds the maximum allowed. + const MAX_ALLOWED_ELEMENT_DATA_SIZE: usize = 100; + for element_size in 64..=256 { + // Arrange. + type BoundedSize = + BoundedVec>; + let element = vec![b'a'; element_size - std::mem::size_of::>()]; + let data = BoundedSize::new(vec![element; 42]); + + // Act. + let result = Decode!(&Encode!(&data).unwrap(), BoundedSize); + + // Assert. + if element_size <= MAX_ALLOWED_ELEMENT_DATA_SIZE { + // Verify decoding without errors for allowed sizes. + assert!(result.is_ok()); + assert_eq!(result.unwrap(), data); + } else { + // Verify decoding with errors for disallowed sizes. + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!( + error.to_string().contains(&format!( + "Deserialize error: The single element data size exceeds maximum allowed {MAX_ALLOWED_ELEMENT_DATA_SIZE}" + )), + "Actual: {}", + error.to_string() + ); + } + } + } +} + +mod data_size { + // We implement a standalone trait `DataSize` here and use it + // instead of `CountBytes`. + + /// Trait to reasonably estimate the memory usage of a value in bytes. + /// + /// Default implementation returns zero. + pub trait DataSize { + /// Default implementation returns zero. + fn data_size(&self) -> usize { + 0 + } + } + + impl DataSize for u8 { + fn data_size(&self) -> usize { + std::mem::size_of::() + } + } + + impl DataSize for [u8] { + fn data_size(&self) -> usize { + std::mem::size_of_val(self) + } + } + + impl DataSize for u64 { + fn data_size(&self) -> usize { + std::mem::size_of::() + } + } + + impl DataSize for &str { + fn data_size(&self) -> usize { + self.as_bytes().data_size() + } + } + + impl DataSize for String { + fn data_size(&self) -> usize { + self.as_bytes().data_size() + } + } + + impl DataSize for Vec { + fn data_size(&self) -> usize { + std::mem::size_of::() + self.iter().map(|x| x.data_size()).sum::() + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn test_data_size_u8() { + assert_eq!(0_u8.data_size(), 1); + assert_eq!(42_u8.data_size(), 1); + } + + #[test] + fn test_data_size_u8_slice() { + let a: [u8; 0] = []; + assert_eq!(a.data_size(), 0); + assert_eq!([1_u8].data_size(), 1); + assert_eq!([1_u8, 2_u8].data_size(), 2); + } + + #[test] + fn test_data_size_u64() { + assert_eq!(0_u64.data_size(), 8); + assert_eq!(42_u64.data_size(), 8); + } + + #[test] + fn test_data_size_u8_vec() { + let base = 24; + assert_eq!(Vec::::from([]).data_size(), base); + assert_eq!(Vec::::from([1]).data_size(), base + 1); + assert_eq!(Vec::::from([1, 2]).data_size(), base + 2); + } + + #[test] + fn test_data_size_str() { + assert_eq!("a".data_size(), 1); + assert_eq!("ab".data_size(), 2); + } + + #[test] + fn test_data_size_string() { + assert_eq!(String::from("a").data_size(), 1); + assert_eq!(String::from("ab").data_size(), 2); + for size_bytes in 0..1_024 { + assert_eq!( + String::from_utf8(vec![b'x'; size_bytes]) + .unwrap() + .data_size(), + size_bytes + ); + } + } + } +} diff --git a/rust/candid/src/types/mod.rs b/rust/candid/src/types/mod.rs index 49af606d..367780e0 100644 --- a/rust/candid/src/types/mod.rs +++ b/rust/candid/src/types/mod.rs @@ -19,6 +19,7 @@ pub use self::internal::{ }; pub use type_env::TypeEnv; +pub mod bounded_vec; pub mod leb128; #[cfg(feature = "bignum")] pub mod number; From 9068eab9e8b5dafd68ed1279d60cb19ea220a130 Mon Sep 17 00:00:00 2001 From: Michael Weigelt Date: Fri, 13 Feb 2026 15:24:59 +0000 Subject: [PATCH 2/3] fix --- rust/candid/src/types/bounded_vec.rs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/rust/candid/src/types/bounded_vec.rs b/rust/candid/src/types/bounded_vec.rs index 63068cd1..c7245de9 100644 --- a/rust/candid/src/types/bounded_vec.rs +++ b/rust/candid/src/types/bounded_vec.rs @@ -1,11 +1,8 @@ use data_size::DataSize; use serde::{Deserialize, Deserializer}; -use std::{fmt, rc::Rc}; +use std::fmt; -use crate::{ - types::{Field, Label, TypeInner}, - CandidType, -}; +use crate::{types::TypeInner, CandidType}; /// Indicates that `BoundedVec<...>` template parameter (eg. length, total data size, etc) is unbounded. pub const UNBOUNDED: usize = usize::MAX; @@ -32,11 +29,6 @@ impl< { fn _ty() -> super::Type { TypeInner::Vec(T::_ty()).into() - // TypeInner::Record(vec![Field { - // id: Rc::new(Label::Unnamed(0)), - // ty: TypeInner::Vec(T::_ty()).into(), - // }]) - // .into() } fn idl_serialize(&self, serializer: S) -> Result<(), S::Error> @@ -260,7 +252,6 @@ mod tests { // Act. let result = Decode!(&Encode!(&data).unwrap(), BoundedLen); - println!("{:?}", result); // Assert. if i <= MAX_ALLOWED_LEN { @@ -272,7 +263,7 @@ mod tests { assert!(result.is_err()); let error = result.unwrap_err(); assert!( - error.to_string().contains(&format!( + format!("{error:?}").contains(&format!( "Deserialize error: The number of elements exceeds maximum allowed {MAX_ALLOWED_LEN}" )), "Actual: {}", @@ -312,7 +303,7 @@ mod tests { assert!(result.is_err()); let error = result.unwrap_err(); assert!( - error.to_string().contains(&format!( + format!("{error:?}").contains(&format!( "Deserialize error: The total data size exceeds maximum allowed {MAX_ALLOWED_TOTAL_DATA_SIZE}" )), "Actual: {}", @@ -347,7 +338,7 @@ mod tests { assert!(result.is_err()); let error = result.unwrap_err(); assert!( - error.to_string().contains(&format!( + format!("{error:?}").contains(&format!( "Deserialize error: The single element data size exceeds maximum allowed {MAX_ALLOWED_ELEMENT_DATA_SIZE}" )), "Actual: {}", From c02f134e054788c0c1952e26905a15139f0b0a0b Mon Sep 17 00:00:00 2001 From: Michael Weigelt Date: Fri, 13 Feb 2026 15:31:55 +0000 Subject: [PATCH 3/3] fix --- rust/candid/src/types/bounded_vec.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/rust/candid/src/types/bounded_vec.rs b/rust/candid/src/types/bounded_vec.rs index c7245de9..8eb57930 100644 --- a/rust/candid/src/types/bounded_vec.rs +++ b/rust/candid/src/types/bounded_vec.rs @@ -251,7 +251,8 @@ mod tests { let data = BoundedLen::new(vec![42; i]); // Act. - let result = Decode!(&Encode!(&data).unwrap(), BoundedLen); + let bytes = Encode!(&data).unwrap(); + let result = Decode!(&bytes, BoundedLen); // Assert. if i <= MAX_ALLOWED_LEN { @@ -267,7 +268,7 @@ mod tests { "Deserialize error: The number of elements exceeds maximum allowed {MAX_ALLOWED_LEN}" )), "Actual: {}", - error.to_string() + error ); } } @@ -291,7 +292,8 @@ mod tests { let actual_total_size = data.get().data_size(); // Act. - let result = Decode!(&Encode!(&data).unwrap(), BoundedSize); + let bytes = Encode!(&data).unwrap(); + let result = Decode!(&bytes, BoundedSize); // Assert. if actual_total_size <= MAX_ALLOWED_TOTAL_DATA_SIZE { @@ -307,7 +309,7 @@ mod tests { "Deserialize error: The total data size exceeds maximum allowed {MAX_ALLOWED_TOTAL_DATA_SIZE}" )), "Actual: {}", - error.to_string() + error ); } } @@ -326,7 +328,8 @@ mod tests { let data = BoundedSize::new(vec![element; 42]); // Act. - let result = Decode!(&Encode!(&data).unwrap(), BoundedSize); + let bytes = Encode!(&data).unwrap(); + let result = Decode!(&bytes, BoundedSize); // Assert. if element_size <= MAX_ALLOWED_ELEMENT_DATA_SIZE { @@ -342,7 +345,7 @@ mod tests { "Deserialize error: The single element data size exceeds maximum allowed {MAX_ALLOWED_ELEMENT_DATA_SIZE}" )), "Actual: {}", - error.to_string() + error ); } }