diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c68f4fc68..6b5d0f2d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -185,7 +185,7 @@ jobs: uses: model-checking/kani-github-action@v1.1 with: args: | - -p prost-types + -p prost-types --features chrono no-std: runs-on: ubuntu-latest steps: diff --git a/prost-types/Cargo.toml b/prost-types/Cargo.toml index ce764c133..22f3e9270 100644 --- a/prost-types/Cargo.toml +++ b/prost-types/Cargo.toml @@ -20,6 +20,7 @@ arbitrary = ["dep:arbitrary"] [dependencies] prost = { version = "0.13.5", path = "../prost", default-features = false, features = ["prost-derive"] } arbitrary = { version = "1.4", features = ["derive"], optional = true } +chrono = { version = "0.4.34", default-features = false, optional = true } [dev-dependencies] proptest = "1" diff --git a/prost-types/src/duration.rs b/prost-types/src/duration.rs index 3ce993ee5..0ef97c6cc 100644 --- a/prost-types/src/duration.rs +++ b/prost-types/src/duration.rs @@ -183,13 +183,42 @@ impl FromStr for Duration { } } +#[cfg(feature = "chrono")] +mod chrono { + use ::chrono::TimeDelta; + + use super::*; + + impl From<::chrono::TimeDelta> for Duration { + fn from(value: ::chrono::TimeDelta) -> Self { + let mut result = Self { + seconds: value.num_seconds(), + nanos: value.subsec_nanos(), + }; + result.normalize(); + result + } + } + + impl TryFrom for ::chrono::TimeDelta { + type Error = DurationError; + + fn try_from(mut value: Duration) -> Result { + value.normalize(); + let seconds = TimeDelta::try_seconds(value.seconds).ok_or(DurationError::OutOfRange)?; + let nanos = TimeDelta::nanoseconds(value.nanos.into()); + seconds.checked_add(&nanos).ok_or(DurationError::OutOfRange) + } + } +} + #[cfg(kani)] mod proofs { use super::*; #[cfg(feature = "std")] #[kani::proof] - fn check_duration_roundtrip() { + fn check_duration_std_roundtrip() { let seconds = kani::any(); let nanos = kani::any(); kani::assume(nanos < 1_000_000_000); @@ -218,7 +247,7 @@ mod proofs { #[cfg(feature = "std")] #[kani::proof] - fn check_duration_roundtrip_nanos() { + fn check_duration_std_roundtrip_nanos() { let seconds = 0; let nanos = kani::any(); let std_duration = std::time::Duration::new(seconds, nanos); @@ -243,6 +272,31 @@ mod proofs { )) } } + + #[cfg(feature = "chrono")] + #[kani::proof] + fn check_duration_chrono_roundtrip() { + let seconds = kani::any(); + let nanos = kani::any(); + let prost_duration = Duration { seconds, nanos }; + match ::chrono::TimeDelta::try_from(prost_duration) { + Err(DurationError::OutOfRange) => { + // Test case not valid: duration out of range + return; + } + Err(err) => { + panic!("Unexpected error: {err}") + } + Ok(chrono_duration) => { + let mut normalized_prost_duration = prost_duration; + normalized_prost_duration.normalize(); + assert_eq!( + Duration::try_from(chrono_duration).unwrap(), + normalized_prost_duration + ); + } + } + } } #[cfg(test)] diff --git a/prost-types/src/lib.rs b/prost-types/src/lib.rs index b531c1b01..3dcaefbd2 100644 --- a/prost-types/src/lib.rs +++ b/prost-types/src/lib.rs @@ -36,6 +36,7 @@ //! ## Feature Flags //! - `std`: Enable integration with standard library. Disable this feature for `no_std` support. This feature is enabled by default. //! - `arbitrary`: Enable integration with crate `arbitrary`. All types on this crate will implement `trait Arbitrary`. +//! - `chrono`: Enable integration with crate `chrono`. Time related types implement conversions to/from their `chrono` equivalent. //! //! [1]: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf