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]