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 61df69d4c fix(rust): add error handling logic for std duration; add 
support for chrono duration (#3490)
61df69d4c is described below

commit 61df69d4cfb5f2fa97e6ceadf55c16e91b446073
Author: Peiyang He <[email protected]>
AuthorDate: Wed Mar 18 00:36:26 2026 -0400

    fix(rust): add error handling logic for std duration; add support for 
chrono duration (#3490)
    
    ## Why?
    
    The default duration type in Rust is `std::time::Duration`, which is
    unsigned, causing potential problems mentioned in
    https://github.com/apache/fory/issues/3484.
    
    ## What does this PR do?
    
    For compatibility purposes, code related to `std::time::Duration` is not
    removed, instead I add some error handling logic around it.
    
    I add write/read logic for `chrono::Duration` similar to
    `std::time::Duration`. It is worth noting that the maximum value of
    `seconds` in `chrono::Duration` is actually `i64::MAX/100`, smaller than
    what the spec allows, but I think this is fine due to the following
    reasons:
    1. `i64::MAX/100` is already large enough for real use cases
    2. duration types in C++, C# module in Fory also have maximum limits
    less than `i64::MAX`
    
    ## Related issues
    Close https://github.com/apache/fory/issues/3484
    
    
    ## AI Contribution Checklist
    
    No
    
    ## Does this PR introduce any user-facing change?
    
    No. Code related to `std::time::Duration` is not removed
    
    ## Benchmark
    
    Existing benchmark code doesn't cover `std::time::Duration`.
---
 rust/fory-core/src/serializer/datetime.rs | 164 ++++++++++++++++++++++++++++--
 rust/fory-core/src/serializer/skip.rs     |   3 +-
 2 files changed, 158 insertions(+), 9 deletions(-)

diff --git a/rust/fory-core/src/serializer/datetime.rs 
b/rust/fory-core/src/serializer/datetime.rs
index 860a83890..bd740110d 100644
--- a/rust/fory-core/src/serializer/datetime.rs
+++ b/rust/fory-core/src/serializer/datetime.rs
@@ -24,7 +24,7 @@ use crate::serializer::ForyDefault;
 use crate::serializer::Serializer;
 use crate::types::TypeId;
 use crate::util::EPOCH;
-use chrono::{NaiveDate, NaiveDateTime};
+use chrono::{Duration as ChronoDuration, NaiveDate, NaiveDateTime};
 use std::mem;
 use std::time::Duration;
 
@@ -156,7 +156,14 @@ impl ForyDefault for NaiveDate {
 impl Serializer for Duration {
     #[inline(always)]
     fn fory_write_data(&self, context: &mut WriteContext) -> Result<(), Error> 
{
-        let secs = self.as_secs() as i64;
+        let raw = self.as_secs();
+        if raw > i64::MAX as u64 {
+            return Err(Error::invalid_data(format!(
+                "std::time::Duration seconds {} exceeds i64::MAX and cannot be 
encoded as varint64",
+                raw
+            )));
+        }
+        let secs = raw as i64;
         let nanos = self.subsec_nanos() as i32;
         context.writer.write_varint64(secs);
         context.writer.write_i32(nanos);
@@ -165,9 +172,24 @@ impl Serializer for Duration {
 
     #[inline(always)]
     fn fory_read_data(context: &mut ReadContext) -> Result<Self, Error> {
-        let secs = context.reader.read_varint64()? as u64;
-        let nanos = context.reader.read_i32()? as u32;
-        Ok(Duration::new(secs, nanos))
+        let secs = context.reader.read_varint64()?;
+        if secs < 0 {
+            return Err(Error::invalid_data(format!(
+                "negative duration seconds {} cannot be represented as 
std::time::Duration; use chrono::Duration instead",
+                secs
+            )));
+        }
+        let nanos = context.reader.read_i32()?;
+        if !(0..=999_999_999).contains(&nanos) {
+            // negative nanos will also be rejected, even though the xlang 
spec actually allows it.
+            // RFC 1040 
(https://rust-lang.github.io/rfcs/1040-duration-reform.html#detailed-design) 
explicitly forbids negative nanoseconds.
+            // If supporting for negative nanoseconds is really needed, we can 
implement **normalization** similar to chrono and Java.
+            return Err(Error::invalid_data(format!(
+                "duration nanoseconds {} out of valid range [0, 999_999_999] 
for std::time::Duration",
+                nanos
+            )));
+        }
+        Ok(Duration::new(secs as u64, nanos as u32))
     }
 
     #[inline(always)]
@@ -214,13 +236,88 @@ impl ForyDefault for Duration {
     }
 }
 
+impl Serializer for ChronoDuration {
+    #[inline(always)]
+    fn fory_write_data(&self, context: &mut WriteContext) -> Result<(), Error> 
{
+        let secs = self.num_seconds();
+        let nanos = self.subsec_nanos();
+        context.writer.write_varint64(secs);
+        context.writer.write_i32(nanos);
+        Ok(())
+    }
+
+    #[inline(always)]
+    fn fory_read_data(context: &mut ReadContext) -> Result<Self, Error> {
+        let secs = context.reader.read_varint64()?;
+        let nanos = context.reader.read_i32()?;
+        if !(-999_999_999..=999_999_999).contains(&nanos) {
+            // chrono supports negative nanoseconds by applying normalization 
internally.
+            return Err(Error::invalid_data(format!(
+                "duration nanoseconds {} out of valid range [-999_999_999, 
999_999_999]",
+                nanos
+            )));
+        }
+        ChronoDuration::try_seconds(secs) // the maximum seconds chrono 
supports is i64::MAX / 1_000, which is smaller than what the spec 
allows(i64::MAX)
+            .and_then(|d| d.checked_add(&ChronoDuration::nanoseconds(nanos as 
i64)))
+            .ok_or_else(|| {
+                Error::invalid_data(format!(
+                    "duration seconds {} out of chrono::Duration valid range",
+                    secs
+                ))
+            })
+    }
+
+    #[inline(always)]
+    fn fory_reserved_space() -> usize {
+        9 + mem::size_of::<i32>() // max varint64 is 9 bytes + 4 bytes for i32
+    }
+
+    #[inline(always)]
+    fn fory_get_type_id(_: &TypeResolver) -> Result<TypeId, Error> {
+        Ok(TypeId::DURATION)
+    }
+
+    #[inline(always)]
+    fn fory_type_id_dyn(&self, _: &TypeResolver) -> Result<TypeId, Error> {
+        Ok(TypeId::DURATION)
+    }
+
+    #[inline(always)]
+    fn fory_static_type_id() -> TypeId {
+        TypeId::DURATION
+    }
+
+    #[inline(always)]
+    fn as_any(&self) -> &dyn std::any::Any {
+        self
+    }
+
+    #[inline(always)]
+    fn fory_write_type_info(context: &mut WriteContext) -> Result<(), Error> {
+        context.writer.write_u8(TypeId::DURATION as u8);
+        Ok(())
+    }
+
+    #[inline(always)]
+    fn fory_read_type_info(context: &mut ReadContext) -> Result<(), Error> {
+        read_basic_type_info::<Self>(context)
+    }
+}
+
+impl ForyDefault for ChronoDuration {
+    #[inline(always)]
+    fn fory_default() -> Self {
+        ChronoDuration::zero()
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
     use crate::fory::Fory;
 
     #[test]
-    fn test_duration_serialization() {
+    fn test_std_duration_serialization() {
         let fory = Fory::default();
 
         // Test various durations
@@ -230,7 +327,7 @@ mod tests {
             Duration::new(1, 0),
             Duration::new(0, 1),
             Duration::new(123, 456789),
-            Duration::new(u64::MAX, 999_999_999),
+            Duration::new(i64::MAX as u64, 999_999_999),
         ];
 
         for duration in test_cases {
@@ -243,4 +340,57 @@ mod tests {
             );
         }
     }
+
+    #[test]
+    fn test_chrono_duration_serialization() {
+        let fory = Fory::default();
+
+        // Test various durations
+        let test_cases = vec![
+            ChronoDuration::zero(),
+            ChronoDuration::new(0, 0).unwrap(),
+            ChronoDuration::new(1, 0).unwrap(),
+            ChronoDuration::new(0, 1).unwrap(),
+            ChronoDuration::new(123, 456789).unwrap(),
+            ChronoDuration::seconds(-1),
+            ChronoDuration::nanoseconds(-1),
+            ChronoDuration::microseconds(-456789),
+            ChronoDuration::MAX,
+            ChronoDuration::MIN,
+        ];
+
+        for duration in test_cases {
+            let bytes = fory.serialize(&duration).unwrap();
+            let deserialized: ChronoDuration = 
fory.deserialize(&bytes).unwrap();
+            assert_eq!(
+                duration, deserialized,
+                "Failed for duration: {:?}",
+                duration
+            );
+        }
+    }
+
+    #[test]
+    fn test_chrono_duration_out_of_range_is_error() {
+        let fory = Fory::default();
+        let too_large = Duration::new(i64::MAX as u64, 0);
+        let bytes = fory.serialize(&too_large).unwrap();
+        let result: Result<ChronoDuration, _> = fory.deserialize(&bytes);
+        assert!(
+            result.is_err(),
+            "out-of-range seconds should not be deserialized into 
chrono::Duration!"
+        );
+    }
+
+    #[test]
+    fn test_negative_std_duration_read_is_error() {
+        let fory = Fory::default();
+        let negative_duration = ChronoDuration::seconds(-1);
+        let bytes = fory.serialize(&negative_duration).unwrap();
+        let result: Result<Duration, _> = fory.deserialize(&bytes);
+        assert!(
+            result.is_err(),
+            "negative duration should not be deserialized into 
std::time::Duration!"
+        );
+    }
 }
diff --git a/rust/fory-core/src/serializer/skip.rs 
b/rust/fory-core/src/serializer/skip.rs
index b96d26e82..697158a35 100644
--- a/rust/fory-core/src/serializer/skip.rs
+++ b/rust/fory-core/src/serializer/skip.rs
@@ -25,9 +25,8 @@ use crate::serializer::Serializer;
 use crate::types;
 use crate::types::RefFlag;
 use crate::util::ENABLE_FORY_DEBUG_OUTPUT;
-use chrono::{NaiveDate, NaiveDateTime};
+use chrono::{Duration, NaiveDate, NaiveDateTime};
 use std::rc::Rc;
-use std::time::Duration;
 
 #[allow(unreachable_code)]
 pub fn skip_field_value(


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

Reply via email to