This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new 3e94a4572 feat(rust): add configurable size guardrails (#3579)
3e94a4572 is described below

commit 3e94a45724dff40662b07455084ebdc968603bed
Author: Ayush Kumar <[email protected]>
AuthorDate: Sun Apr 19 20:26:34 2026 +0530

    feat(rust): add configurable size guardrails (#3579)
    
    ## Why?
    To prevent excessive allocation from malicious untrusted payloads in the
    Rust runtime.
    
    ## What does this PR do?
    This brings the Rust implementation into parity with the C++ runtime by
    introducing configurable guardrails for binary sizes and collection
    counts.
    
    ## Related issues
    #3409
    
    ## AI Contribution Checklist
    
    - [x] Substantial AI assistance was used in this PR: `yes` / `no`
    - [x] If `yes`, I included a completed [AI Contribution
    
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
    in this PR description and the required `AI Usage Disclosure`.
    - [x] Substantial AI assistance was used in this PR: `yes` / `no`
    - [x] If `yes`, I included the standardized `AI Usage Disclosure` block
    below.
    - [x] If `yes`, I can explain and defend all important changes without
    AI help.
    - [x] If `yes`, I reviewed AI-assisted code changes line by line before
    submission.
    - [x] If `yes`, I completed line-by-line self-review first and fixed
    issues before requesting AI review.
    - [x] If `yes`, I ran two fresh AI review agents on the current PR diff
    or current HEAD after the latest code changes: one using
    `.claude/skills/fory-code-review/SKILL.md` and one without that skill.
    - [x] If `yes`, I addressed all AI review comments and repeated the
    review loop until both ai reviewers reported no further actionable
    comments.
    - [ ] If `yes`, I attached screenshot evidence of the final clean AI
    review results from both fresh reviewers on the current PR diff or
    current HEAD after the latest code changes in this PR body.
    - [ ] If `yes`, I ran adequate human verification and recorded evidence
    (checks run locally or in CI, pass/fail summary, and confirmation I
    reviewed results).
    - [x] If `yes`, I added/updated tests and specs where required.
    - [x] If `yes`, I validated protocol/performance impacts with evidence
    when applicable.
    - [x] If `yes`, I verified licensing and provenance compliance.
    
    AI Usage Disclosure (only when substantial AI assistance = `yes`):
    
    - [x] If `yes`, my PR description includes the required `ai_review`
    summary and screenshot evidence of the final clean AI review results
    from both fresh reviewers on the current PR diff or current HEAD after
    the latest code changes.
    ```
    The implementation looks solid, matches the C++ reference implementation 
accurately, and is ready for the main branch. I've reviewed your code changes 
across config.rs, context.rs, and the serializer module, verifying edge cases 
and consistency.
    
    Here are the key aspects that verify the correctness of the Rust 
implementation:
    
    Guardrail Logic Parity with C++:
    
    max_collection_size correctly intercepts untrusted payload lengths before 
large iterations or memory allocations occur in Vec, HashMap, BTreeMap, and 
other collection variants. This perfectly encapsulates the C++ 
read_collection_data_slow patterns.
    max_binary_size accurately targets raw-byte slice operations across 
primitive_list.rs without inappropriately restricting types like String, 
mirroring how C++ omits this check inside string_serializer.h.
    Accurate Error Propagation:
    
    The addition of Error::SizeLimitExceeded works brilliantly with the new 
Fory configuration model. You're properly halting deserialization and passing 
clear payload dimensions to Error::size_limit_exceeded, ensuring graceful 
failure without panics or memory exhaustion.
    Validation and Pre-Allocation Consistency:
    
    Checking max_collection_size just prior to enforcing buffer limits inside 
check_collection_len and check_map_len is well-ordered. By prioritizing the 
runtime limit checks, you avoid false OOMs or deep iterations if Fory receives 
heavily corrupted payload headers.
    ```
    
    
    ## Does this PR introduce any user-facing change?
    - [x] Does this PR introduce any public API change? ((Yes, adds Fory
    configuration methods `max_binary_size()` and `max_collection_size()`,
    as well as `Error::SizeLimitExceeded`))
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
---
 rust/fory-core/src/config.rs                    | 20 ++++++
 rust/fory-core/src/error.rs                     | 30 +++++++++
 rust/fory-core/src/fory.rs                      | 81 +++++++++++++++++++++++--
 rust/fory-core/src/resolver/context.rs          | 16 +++++
 rust/fory-core/src/serializer/collection.rs     | 15 +++++
 rust/fory-core/src/serializer/map.rs            | 13 ++++
 rust/fory-core/src/serializer/primitive_list.rs |  9 +++
 rust/tests/tests/test_collection.rs             | 26 ++++++++
 rust/tests/tests/test_fory.rs                   | 34 ++++++++++-
 rust/tests/tests/test_list.rs                   | 22 +++++++
 rust/tests/tests/test_map.rs                    | 50 +++++++++++++++
 rust/tests/tests/test_unsigned.rs               | 42 +++++++++++++
 12 files changed, 353 insertions(+), 5 deletions(-)

diff --git a/rust/fory-core/src/config.rs b/rust/fory-core/src/config.rs
index 991db1d72..13342700d 100644
--- a/rust/fory-core/src/config.rs
+++ b/rust/fory-core/src/config.rs
@@ -38,6 +38,12 @@ pub struct Config {
     /// When enabled, shared references and circular references are tracked
     /// and preserved during serialization/deserialization.
     pub track_ref: bool,
+    /// Maximum allowed size for binary data in bytes.
+    /// Prevents excessive memory allocation from untrusted payloads.
+    pub max_binary_size: u32,
+    /// Maximum allowed number of elements in a collection or entries in a map.
+    /// Prevents excessive memory allocation from untrusted payloads.
+    pub max_collection_size: u32,
 }
 
 impl Default for Config {
@@ -50,6 +56,8 @@ impl Default for Config {
             max_dyn_depth: 5,
             check_struct_version: false,
             track_ref: false,
+            max_binary_size: 64 * 1024 * 1024, // 64MB default
+            max_collection_size: 1024 * 1024,  // 1M elements default
         }
     }
 }
@@ -101,4 +109,16 @@ impl Config {
     pub fn is_track_ref(&self) -> bool {
         self.track_ref
     }
+
+    /// Get maximum allowed binary data size in bytes.
+    #[inline(always)]
+    pub fn max_binary_size(&self) -> u32 {
+        self.max_binary_size
+    }
+
+    /// Get maximum allowed collection/map element count.
+    #[inline(always)]
+    pub fn max_collection_size(&self) -> u32 {
+        self.max_collection_size
+    }
 }
diff --git a/rust/fory-core/src/error.rs b/rust/fory-core/src/error.rs
index 10455ee79..c198a98da 100644
--- a/rust/fory-core/src/error.rs
+++ b/rust/fory-core/src/error.rs
@@ -196,6 +196,15 @@ pub enum Error {
     /// Do not construct this variant directly; use 
[`Error::struct_version_mismatch`] instead.
     #[error("{0}")]
     StructVersionMismatch(Cow<'static, str>),
+
+    /// Deserialization size limit exceeded.
+    ///
+    /// Returned when a payload-driven length exceeds a configured guardrail
+    /// (e.g. `max_binary_size` or `max_collection_size`).
+    ///
+    /// Do not construct this variant directly; use 
[`Error::size_limit_exceeded`] instead.
+    #[error("{0}")]
+    SizeLimitExceeded(Cow<'static, str>),
 }
 
 impl Error {
@@ -495,6 +504,27 @@ impl Error {
         err
     }
 
+    /// Creates a new [`Error::SizeLimitExceeded`] from a string or static 
message.
+    ///
+    /// If `FORY_PANIC_ON_ERROR` environment variable is set, this will panic 
with the error message.
+    ///
+    /// # Example
+    /// ```
+    /// use fory_core::error::Error;
+    ///
+    /// let err = Error::size_limit_exceeded("Collection size 2000000 exceeds 
limit 1048576");
+    /// ```
+    #[inline(always)]
+    #[cold]
+    #[track_caller]
+    pub fn size_limit_exceeded<S: Into<Cow<'static, str>>>(s: S) -> Self {
+        let err = Error::SizeLimitExceeded(s.into());
+        if PANIC_ON_ERROR {
+            panic!("FORY_PANIC_ON_ERROR: {}", err);
+        }
+        err
+    }
+
     /// Enhances a [`Error::TypeError`] with additional type name information.
     ///
     /// If the error is a `TypeError`, appends the type name to the message.
diff --git a/rust/fory-core/src/fory.rs b/rust/fory-core/src/fory.rs
index d4e2359b3..0aeded559 100644
--- a/rust/fory-core/src/fory.rs
+++ b/rust/fory-core/src/fory.rs
@@ -321,6 +321,65 @@ impl Fory {
         self
     }
 
+    /// Sets the maximum allowed size for binary data during deserialization.
+    ///
+    /// # Arguments
+    ///
+    /// * `max_binary_size` - The maximum number of bytes allowed for a single 
binary/primitive-array
+    ///   payload during deserialization. Payloads exceeding this limit will 
cause a
+    ///   `SizeLimitExceeded` error.
+    ///
+    /// # Returns
+    ///
+    /// Returns `self` for method chaining.
+    ///
+    /// # Default
+    ///
+    /// The default value is `64 * 1024 * 1024` (64 MB).
+    ///
+    /// # Examples
+    ///
+    /// ```rust
+    /// use fory_core::Fory;
+    ///
+    /// // Limit binary payloads to 1 MB
+    /// let fory = Fory::default().max_binary_size(1024 * 1024);
+    /// ```
+    pub fn max_binary_size(mut self, max_binary_size: u32) -> Self {
+        self.config.max_binary_size = max_binary_size;
+        self
+    }
+
+    /// Sets the maximum allowed number of elements in a collection or entries 
in a map
+    /// during deserialization.
+    ///
+    /// # Arguments
+    ///
+    /// * `max_collection_size` - The maximum number of elements/entries 
allowed for a single
+    ///   collection or map during deserialization. Payloads exceeding this 
limit will cause a
+    ///   `SizeLimitExceeded` error.
+    ///
+    /// # Returns
+    ///
+    /// Returns `self` for method chaining.
+    ///
+    /// # Default
+    ///
+    /// The default value is `1024 * 1024` (1 million elements).
+    ///
+    /// # Examples
+    ///
+    /// ```rust
+    /// use fory_core::Fory;
+    ///
+    /// // Limit collections to 10000 elements
+    /// let fory = Fory::default().max_collection_size(10000);
+    /// ```
+    pub fn max_collection_size(mut self, max_collection_size: u32) -> Self {
+        self.config.max_collection_size = max_collection_size;
+        self
+    }
+
     /// Returns whether cross-language serialization is enabled.
     pub fn is_xlang(&self) -> bool {
         self.config.xlang
@@ -358,6 +417,16 @@ impl Fory {
         self.config.max_dyn_depth
     }
 
+    /// Returns the maximum allowed binary data size in bytes.
+    pub fn get_max_binary_size(&self) -> u32 {
+        self.config.max_binary_size
+    }
+
+    /// Returns the maximum allowed collection/map element count.
+    pub fn get_max_collection_size(&self) -> u32 {
+        self.config.max_collection_size
+    }
+
     /// Returns whether class version checking is enabled.
     ///
     /// # Returns
@@ -599,12 +668,14 @@ impl Fory {
         WRITE_CONTEXTS.with(|cache| {
             let cache = unsafe { &mut *cache.get() };
             let id = self.id;
-            let config = self.config.clone();
 
             let context = cache.get_or_insert_result(id, || {
                 // Only fetch type resolver when creating a new context
                 let type_resolver = self.get_final_type_resolver()?;
-                Ok(Box::new(WriteContext::new(type_resolver.clone(), config)))
+                Ok(Box::new(WriteContext::new(
+                    type_resolver.clone(),
+                    self.config.clone(),
+                )))
             })?;
             f(context)
         })
@@ -1015,12 +1086,14 @@ impl Fory {
         READ_CONTEXTS.with(|cache| {
             let cache = unsafe { &mut *cache.get() };
             let id = self.id;
-            let config = self.config.clone();
 
             let context = cache.get_or_insert_result(id, || {
                 // Only fetch type resolver when creating a new context
                 let type_resolver = self.get_final_type_resolver()?;
-                Ok(Box::new(ReadContext::new(type_resolver.clone(), config)))
+                Ok(Box::new(ReadContext::new(
+                    type_resolver.clone(),
+                    self.config.clone(),
+                )))
             })?;
             f(context)
         })
diff --git a/rust/fory-core/src/resolver/context.rs 
b/rust/fory-core/src/resolver/context.rs
index 4f7f08c83..42bd19783 100644
--- a/rust/fory-core/src/resolver/context.rs
+++ b/rust/fory-core/src/resolver/context.rs
@@ -315,6 +315,8 @@ pub struct ReadContext<'a> {
     xlang: bool,
     max_dyn_depth: u32,
     check_struct_version: bool,
+    max_binary_size: u32,
+    max_collection_size: u32,
 
     // Context-specific fields
     pub reader: Reader<'a>,
@@ -342,6 +344,8 @@ impl<'a> ReadContext<'a> {
             xlang: config.xlang,
             max_dyn_depth: config.max_dyn_depth,
             check_struct_version: config.check_struct_version,
+            max_binary_size: config.max_binary_size,
+            max_collection_size: config.max_collection_size,
             reader: Reader::default(),
             meta_resolver: MetaReaderResolver::default(),
             meta_string_resolver: MetaStringReaderResolver::default(),
@@ -386,6 +390,18 @@ impl<'a> ReadContext<'a> {
         self.max_dyn_depth
     }
 
+    /// Get maximum allowed binary data size in bytes.
+    #[inline(always)]
+    pub fn max_binary_size(&self) -> u32 {
+        self.max_binary_size
+    }
+
+    /// Get maximum allowed collection/map element count.
+    #[inline(always)]
+    pub fn max_collection_size(&self) -> u32 {
+        self.max_collection_size
+    }
+
     #[inline(always)]
     pub fn attach_reader(&mut self, reader: Reader<'a>) {
         self.reader = reader;
diff --git a/rust/fory-core/src/serializer/collection.rs 
b/rust/fory-core/src/serializer/collection.rs
index ae16620c7..cdd3c1dc6 100644
--- a/rust/fory-core/src/serializer/collection.rs
+++ b/rust/fory-core/src/serializer/collection.rs
@@ -32,6 +32,11 @@ pub const DECL_ELEMENT_TYPE: u8 = 0b100;
 //  Whether collection elements type same.
 pub const IS_SAME_TYPE: u8 = 0b1000;
 
+#[cold]
+fn collection_size_limit_exceeded(len: u32, max: u32) -> Error {
+    Error::size_limit_exceeded(format!("Collection size {} exceeds limit {}", 
len, max))
+}
+
 fn check_collection_len<T: Serializer>(context: &ReadContext, len: u32) -> 
Result<(), Error> {
     if std::mem::size_of::<T>() == 0 {
         return Ok(());
@@ -95,6 +100,8 @@ where
         context.writer.write_u8(header);
         T::fory_write_type_info(context)?;
     }
+    // Pre-reserve buffer space to avoid per-element capacity checks in the 
write loop.
+    context.writer.reserve(len * T::fory_reserved_space());
     if !has_null {
         for item in iter {
             item.fory_write_data_generic(context, has_generics)?;
@@ -243,6 +250,10 @@ where
     if len == 0 {
         return Ok(C::from_iter(std::iter::empty()));
     }
+    let max = context.max_collection_size();
+    if len > max {
+        return Err(collection_size_limit_exceeded(len, max));
+    }
     if T::fory_is_polymorphic() || T::fory_is_shared_ref() {
         return read_collection_data_dyn_ref(context, len);
     }
@@ -285,6 +296,10 @@ where
     if len == 0 {
         return Ok(Vec::new());
     }
+    let max = context.max_collection_size();
+    if len > max {
+        return Err(collection_size_limit_exceeded(len, max));
+    }
     if T::fory_is_polymorphic() || T::fory_is_shared_ref() {
         return read_vec_data_dyn_ref(context, len);
     }
diff --git a/rust/fory-core/src/serializer/map.rs 
b/rust/fory-core/src/serializer/map.rs
index f8c7997c0..6677679ff 100644
--- a/rust/fory-core/src/serializer/map.rs
+++ b/rust/fory-core/src/serializer/map.rs
@@ -34,6 +34,11 @@ const TRACKING_VALUE_REF: u8 = 0b1000;
 pub const VALUE_NULL: u8 = 0b10000;
 pub const DECL_VALUE_TYPE: u8 = 0b100000;
 
+#[cold]
+fn map_size_limit_exceeded(len: u32, max: u32) -> Error {
+    Error::size_limit_exceeded(format!("Map size {} exceeds limit {}", len, 
max))
+}
+
 fn check_map_len(context: &ReadContext, len: u32) -> Result<(), Error> {
     let len = len as usize;
     let remaining = context.reader.slice_after_cursor().len();
@@ -560,6 +565,10 @@ impl<K: Serializer + ForyDefault + Eq + std::hash::Hash, 
V: Serializer + ForyDef
         if len == 0 {
             return Ok(HashMap::new());
         }
+        let max = context.max_collection_size();
+        if len > max {
+            return Err(map_size_limit_exceeded(len, max));
+        }
         check_map_len(context, len)?;
         if K::fory_is_polymorphic()
             || K::fory_is_shared_ref()
@@ -712,6 +721,10 @@ impl<K: Serializer + ForyDefault + Ord + std::hash::Hash, 
V: Serializer + ForyDe
         if len == 0 {
             return Ok(BTreeMap::new());
         }
+        let max = context.max_collection_size();
+        if len > max {
+            return Err(map_size_limit_exceeded(len, max));
+        }
         check_map_len(context, len)?;
         let mut map = BTreeMap::<K, V>::new();
         if K::fory_is_polymorphic()
diff --git a/rust/fory-core/src/serializer/primitive_list.rs 
b/rust/fory-core/src/serializer/primitive_list.rs
index cd381a7db..5110c8d6d 100644
--- a/rust/fory-core/src/serializer/primitive_list.rs
+++ b/rust/fory-core/src/serializer/primitive_list.rs
@@ -22,6 +22,11 @@ use crate::resolver::context::WriteContext;
 use crate::serializer::Serializer;
 use crate::types::TypeId;
 
+#[cold]
+fn binary_size_limit_exceeded(size_bytes: usize, max: usize) -> Error {
+    Error::size_limit_exceeded(format!("Binary size {} exceeds limit {}", 
size_bytes, max))
+}
+
 pub fn fory_write_data<T: Serializer>(this: &[T], context: &mut WriteContext) 
-> Result<(), Error> {
     // U128, USIZE, ISIZE, INT128 are Rust-specific and not supported in xlang 
mode
     if context.is_xlang() {
@@ -83,6 +88,10 @@ pub fn fory_read_data<T: Serializer>(context: &mut 
ReadContext) -> Result<Vec<T>
     if size_bytes % std::mem::size_of::<T>() != 0 {
         return Err(Error::invalid_data("Invalid data length"));
     }
+    let max = context.max_binary_size() as usize;
+    if size_bytes > max {
+        return Err(binary_size_limit_exceeded(size_bytes, max));
+    }
     let remaining = context.reader.slice_after_cursor().len();
     if size_bytes > remaining {
         let cursor = context.reader.get_cursor();
diff --git a/rust/tests/tests/test_collection.rs 
b/rust/tests/tests/test_collection.rs
index e8b3c2f72..0d6264793 100644
--- a/rust/tests/tests/test_collection.rs
+++ b/rust/tests/tests/test_collection.rs
@@ -120,3 +120,29 @@ fn test_heap_container() {
     assert_eq!(deserialized.binary_heap.len(), 3);
     assert_eq!(deserialized.binary_heap.peek(), Some(&3));
 }
+
+#[test]
+fn test_hashset_max_collection_size_guardrail() {
+    let fory = Fory::default();
+    let original = HashSet::from([
+        "apple".to_string(),
+        "banana".to_string(),
+        "cherry".to_string(),
+    ]);
+    let serialized = fory.serialize(&original).unwrap();
+
+    let limited_fory = Fory::default().max_collection_size(2);
+    let err = limited_fory
+        .deserialize::<HashSet<String>>(&serialized)
+        .expect_err("expected collection size guardrail to reject the 
payload");
+
+    assert!(
+        matches!(err, fory_core::Error::SizeLimitExceeded(_)),
+        "expected SizeLimitExceeded, got: {err}"
+    );
+    assert!(
+        err.to_string()
+            .contains("Collection size 3 exceeds limit 2"),
+        "unexpected error message: {err}"
+    );
+}
diff --git a/rust/tests/tests/test_fory.rs b/rust/tests/tests/test_fory.rs
index 1c8be4cb4..332428f9e 100644
--- a/rust/tests/tests/test_fory.rs
+++ b/rust/tests/tests/test_fory.rs
@@ -16,6 +16,7 @@
 // under the License.
 
 use fory_core::buffer::Reader;
+use fory_core::error::Error;
 use fory_core::fory::Fory;
 use fory_derive::ForyObject;
 
@@ -289,7 +290,6 @@ fn test_unregistered_type_error_message() {
 
 #[test]
 fn test_type_mismatch_error_shows_type_name() {
-    use fory_core::error::Error;
     use fory_core::types::{format_type_id, TypeId};
 
     // Test internal type (BOOL = 1), no registered_id
@@ -331,3 +331,35 @@ fn test_type_mismatch_error_shows_type_name() {
         err_str
     );
 }
+
+#[test]
+fn test_size_guardrail_configuration_accessors() {
+    let default_fory = Fory::default();
+    assert_eq!(default_fory.get_max_binary_size(), 64 * 1024 * 1024);
+    assert_eq!(default_fory.get_max_collection_size(), 1024 * 1024);
+
+    let configured_fory = Fory::default()
+        .max_binary_size(4096)
+        .max_collection_size(128);
+    assert_eq!(configured_fory.get_max_binary_size(), 4096);
+    assert_eq!(configured_fory.get_max_collection_size(), 128);
+}
+
+#[test]
+fn test_max_binary_size_does_not_limit_string_reads() {
+    let fory = Fory::default();
+    let original = "this string should not be treated as binary".repeat(4);
+    let serialized = fory.serialize(&original).unwrap();
+
+    let limited_fory = Fory::default().max_binary_size(4);
+    let deserialized: String = limited_fory.deserialize(&serialized).unwrap();
+
+    assert_eq!(deserialized, original);
+}
+
+#[test]
+fn test_size_limit_exceeded_error_display() {
+    let err = Error::size_limit_exceeded("Collection size 3 exceeds limit 2");
+    assert!(matches!(err, Error::SizeLimitExceeded(_)));
+    assert_eq!(err.to_string(), "Collection size 3 exceeds limit 2");
+}
diff --git a/rust/tests/tests/test_list.rs b/rust/tests/tests/test_list.rs
index 627dd6264..0c8f8f5d2 100644
--- a/rust/tests/tests/test_list.rs
+++ b/rust/tests/tests/test_list.rs
@@ -190,3 +190,25 @@ fn test_vec_float16_empty() {
         .expect("deserialize empty float16 vec");
     assert_eq!(obj.len(), 0);
 }
+
+#[test]
+fn test_vec_max_collection_size_guardrail() {
+    let fory = Fory::default();
+    let original = vec!["alpha".to_string(), "beta".to_string(), 
"gamma".to_string()];
+    let serialized = fory.serialize(&original).unwrap();
+
+    let limited_fory = Fory::default().max_collection_size(2);
+    let err = limited_fory
+        .deserialize::<Vec<String>>(&serialized)
+        .expect_err("expected vec deserialization to fail on 
max_collection_size");
+
+    assert!(
+        matches!(err, fory_core::Error::SizeLimitExceeded(_)),
+        "expected SizeLimitExceeded, got: {err}"
+    );
+    assert!(
+        err.to_string()
+            .contains("Collection size 3 exceeds limit 2"),
+        "unexpected error message: {err}"
+    );
+}
diff --git a/rust/tests/tests/test_map.rs b/rust/tests/tests/test_map.rs
index f619800a7..832dca516 100644
--- a/rust/tests/tests/test_map.rs
+++ b/rust/tests/tests/test_map.rs
@@ -67,3 +67,53 @@ fn test_struct_with_maps() {
     let obj: MapContainer = fory.deserialize(&bin).expect("deserialize");
     assert_eq!(container, obj);
 }
+
+#[test]
+fn test_hashmap_max_collection_size_guardrail() {
+    let fory = Fory::default();
+    let map = HashMap::from([
+        ("key1".to_string(), 1_i32),
+        ("key2".to_string(), 2_i32),
+        ("key3".to_string(), 3_i32),
+    ]);
+    let serialized = fory.serialize(&map).unwrap();
+
+    let limited_fory = Fory::default().max_collection_size(2);
+    let err = limited_fory
+        .deserialize::<HashMap<String, i32>>(&serialized)
+        .expect_err("expected hashmap deserialization to fail on 
max_collection_size");
+
+    assert!(
+        matches!(err, fory_core::Error::SizeLimitExceeded(_)),
+        "expected SizeLimitExceeded, got: {err}"
+    );
+    assert!(
+        err.to_string().contains("Map size 3 exceeds limit 2"),
+        "unexpected error message: {err}"
+    );
+}
+
+#[test]
+fn test_btreemap_max_collection_size_guardrail() {
+    let fory = Fory::default();
+    let map = BTreeMap::from([
+        ("key1".to_string(), 1_i32),
+        ("key2".to_string(), 2_i32),
+        ("key3".to_string(), 3_i32),
+    ]);
+    let serialized = fory.serialize(&map).unwrap();
+
+    let limited_fory = Fory::default().max_collection_size(2);
+    let err = limited_fory
+        .deserialize::<BTreeMap<String, i32>>(&serialized)
+        .expect_err("expected btreemap deserialization to fail on 
max_collection_size");
+
+    assert!(
+        matches!(err, fory_core::Error::SizeLimitExceeded(_)),
+        "expected SizeLimitExceeded, got: {err}"
+    );
+    assert!(
+        err.to_string().contains("Map size 3 exceeds limit 2"),
+        "unexpected error message: {err}"
+    );
+}
diff --git a/rust/tests/tests/test_unsigned.rs 
b/rust/tests/tests/test_unsigned.rs
index 66b1b751e..c3a853ed6 100644
--- a/rust/tests/tests/test_unsigned.rs
+++ b/rust/tests/tests/test_unsigned.rs
@@ -72,6 +72,48 @@ fn test_binary_when_xlang() {
     assert_eq!(data, result);
 }
 
+#[test]
+fn test_binary_max_size_guardrail_for_vec_u8() {
+    let fory = Fory::default();
+    let original = vec![1_u8, 2, 3, 4, 5];
+    let serialized = fory.serialize(&original).unwrap();
+
+    let limited_fory = Fory::default().max_binary_size(4);
+    let err = limited_fory
+        .deserialize::<Vec<u8>>(&serialized)
+        .expect_err("expected binary size guardrail to reject the payload");
+
+    assert!(
+        matches!(err, fory_core::Error::SizeLimitExceeded(_)),
+        "expected SizeLimitExceeded, got: {err}"
+    );
+    assert!(
+        err.to_string().contains("Binary size 5 exceeds limit 4"),
+        "unexpected error message: {err}"
+    );
+}
+
+#[test]
+fn test_binary_max_size_guardrail_for_vec_u32() {
+    let fory = Fory::default();
+    let original = vec![10_u32, 20, 30];
+    let serialized = fory.serialize(&original).unwrap();
+
+    let limited_fory = Fory::default().max_binary_size(8);
+    let err = limited_fory
+        .deserialize::<Vec<u32>>(&serialized)
+        .expect_err("expected primitive array size guardrail to reject the 
payload");
+
+    assert!(
+        matches!(err, fory_core::Error::SizeLimitExceeded(_)),
+        "expected SizeLimitExceeded, got: {err}"
+    );
+    assert!(
+        err.to_string().contains("Binary size 12 exceeds limit 8"),
+        "unexpected error message: {err}"
+    );
+}
+
 #[test]
 fn test_unsigned_struct_non_compatible() {
     #[derive(ForyObject, Debug, PartialEq)]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to