From e82afc96ea8702976756e6841e5e53904550c57f Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 17 Mar 2024 18:15:36 +0100 Subject: [PATCH 1/3] prost-types: Place type implementations in modules. --- prost-types/src/lib.rs | 862 +++++++++++++++++++++-------------------- 1 file changed, 442 insertions(+), 420 deletions(-) diff --git a/prost-types/src/lib.rs b/prost-types/src/lib.rs index 81fcd9670..dcfac8610 100644 --- a/prost-types/src/lib.rs +++ b/prost-types/src/lib.rs @@ -40,511 +40,533 @@ const NANOS_MAX: i32 = NANOS_PER_SECOND - 1; const PACKAGE: &str = "google.protobuf"; -impl Any { - /// Serialize the given message type `M` as [`Any`]. - pub fn from_msg(msg: &M) -> Result - where - M: Name, - { - let type_url = M::type_url(); - let mut value = Vec::new(); - Message::encode(msg, &mut value)?; - Ok(Any { type_url, value }) - } +mod any { + use super::*; - /// Decode the given message type `M` from [`Any`], validating that it has - /// the expected type URL. - pub fn to_msg(&self) -> Result - where - M: Default + Name + Sized, - { - let expected_type_url = M::type_url(); - - match ( - TypeUrl::new(&expected_type_url), - TypeUrl::new(&self.type_url), - ) { - (Some(expected), Some(actual)) => { - if expected == actual { - return Ok(M::decode(self.value.as_slice())?); + impl Any { + /// Serialize the given message type `M` as [`Any`]. + pub fn from_msg(msg: &M) -> Result + where + M: Name, + { + let type_url = M::type_url(); + let mut value = Vec::new(); + Message::encode(msg, &mut value)?; + Ok(Any { type_url, value }) + } + + /// Decode the given message type `M` from [`Any`], validating that it has + /// the expected type URL. + pub fn to_msg(&self) -> Result + where + M: Default + Name + Sized, + { + let expected_type_url = M::type_url(); + + match ( + TypeUrl::new(&expected_type_url), + TypeUrl::new(&self.type_url), + ) { + (Some(expected), Some(actual)) => { + if expected == actual { + return Ok(M::decode(self.value.as_slice())?); + } } + _ => (), } - _ => (), - } - let mut err = DecodeError::new(format!( - "expected type URL: \"{}\" (got: \"{}\")", - expected_type_url, &self.type_url - )); - err.push("unexpected type URL", "type_url"); - Err(err) + let mut err = DecodeError::new(format!( + "expected type URL: \"{}\" (got: \"{}\")", + expected_type_url, &self.type_url + )); + err.push("unexpected type URL", "type_url"); + Err(err) + } } -} -impl Name for Any { - const PACKAGE: &'static str = PACKAGE; - const NAME: &'static str = "Any"; + impl Name for Any { + const PACKAGE: &'static str = PACKAGE; + const NAME: &'static str = "Any"; - fn type_url() -> String { - type_url_for::() + fn type_url() -> String { + type_url_for::() + } } } -#[cfg(feature = "std")] -impl std::hash::Hash for Duration { - fn hash(&self, state: &mut H) { - self.seconds.hash(state); - self.nanos.hash(state); - } -} +pub use duration::DurationError; +mod duration { + use super::*; -impl Duration { - /// Normalizes the duration to a canonical format. - /// - /// Based on [`google::protobuf::util::CreateNormalized`][1]. - /// - /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L79-L100 - pub fn normalize(&mut self) { - // Make sure nanos is in the range. - if self.nanos <= -NANOS_PER_SECOND || self.nanos >= NANOS_PER_SECOND { - if let Some(seconds) = self - .seconds - .checked_add((self.nanos / NANOS_PER_SECOND) as i64) - { - self.seconds = seconds; - self.nanos %= NANOS_PER_SECOND; - } else if self.nanos < 0 { - // Negative overflow! Set to the least normal value. - self.seconds = i64::MIN; - self.nanos = -NANOS_MAX; - } else { - // Positive overflow! Set to the greatest normal value. - self.seconds = i64::MAX; - self.nanos = NANOS_MAX; - } + #[cfg(feature = "std")] + impl std::hash::Hash for Duration { + fn hash(&self, state: &mut H) { + self.seconds.hash(state); + self.nanos.hash(state); } + } - // nanos should have the same sign as seconds. - if self.seconds < 0 && self.nanos > 0 { - if let Some(seconds) = self.seconds.checked_add(1) { - self.seconds = seconds; - self.nanos -= NANOS_PER_SECOND; - } else { - // Positive overflow! Set to the greatest normal value. - debug_assert_eq!(self.seconds, i64::MAX); - self.nanos = NANOS_MAX; + impl Duration { + /// Normalizes the duration to a canonical format. + /// + /// Based on [`google::protobuf::util::CreateNormalized`][1]. + /// + /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L79-L100 + pub fn normalize(&mut self) { + // Make sure nanos is in the range. + if self.nanos <= -NANOS_PER_SECOND || self.nanos >= NANOS_PER_SECOND { + if let Some(seconds) = self + .seconds + .checked_add((self.nanos / NANOS_PER_SECOND) as i64) + { + self.seconds = seconds; + self.nanos %= NANOS_PER_SECOND; + } else if self.nanos < 0 { + // Negative overflow! Set to the least normal value. + self.seconds = i64::MIN; + self.nanos = -NANOS_MAX; + } else { + // Positive overflow! Set to the greatest normal value. + self.seconds = i64::MAX; + self.nanos = NANOS_MAX; + } } - } else if self.seconds > 0 && self.nanos < 0 { - if let Some(seconds) = self.seconds.checked_sub(1) { - self.seconds = seconds; - self.nanos += NANOS_PER_SECOND; - } else { - // Negative overflow! Set to the least normal value. - debug_assert_eq!(self.seconds, i64::MIN); - self.nanos = -NANOS_MAX; + + // nanos should have the same sign as seconds. + if self.seconds < 0 && self.nanos > 0 { + if let Some(seconds) = self.seconds.checked_add(1) { + self.seconds = seconds; + self.nanos -= NANOS_PER_SECOND; + } else { + // Positive overflow! Set to the greatest normal value. + debug_assert_eq!(self.seconds, i64::MAX); + self.nanos = NANOS_MAX; + } + } else if self.seconds > 0 && self.nanos < 0 { + if let Some(seconds) = self.seconds.checked_sub(1) { + self.seconds = seconds; + self.nanos += NANOS_PER_SECOND; + } else { + // Negative overflow! Set to the least normal value. + debug_assert_eq!(self.seconds, i64::MIN); + self.nanos = -NANOS_MAX; + } } + // TODO: should this be checked? + // debug_assert!(self.seconds >= -315_576_000_000 && self.seconds <= 315_576_000_000, + // "invalid duration: {:?}", self); } - // TODO: should this be checked? - // debug_assert!(self.seconds >= -315_576_000_000 && self.seconds <= 315_576_000_000, - // "invalid duration: {:?}", self); } -} -impl Name for Duration { - const PACKAGE: &'static str = PACKAGE; - const NAME: &'static str = "Duration"; + impl Name for Duration { + const PACKAGE: &'static str = PACKAGE; + const NAME: &'static str = "Duration"; - fn type_url() -> String { - type_url_for::() + fn type_url() -> String { + type_url_for::() + } } -} -impl TryFrom for Duration { - type Error = DurationError; + impl TryFrom for Duration { + type Error = DurationError; - /// Converts a `std::time::Duration` to a `Duration`, failing if the duration is too large. - fn try_from(duration: time::Duration) -> Result { - let seconds = i64::try_from(duration.as_secs()).map_err(|_| DurationError::OutOfRange)?; - let nanos = duration.subsec_nanos() as i32; + /// Converts a `std::time::Duration` to a `Duration`, failing if the duration is too large. + fn try_from(duration: time::Duration) -> Result { + let seconds = + i64::try_from(duration.as_secs()).map_err(|_| DurationError::OutOfRange)?; + let nanos = duration.subsec_nanos() as i32; - let mut duration = Duration { seconds, nanos }; - duration.normalize(); - Ok(duration) + let mut duration = Duration { seconds, nanos }; + duration.normalize(); + Ok(duration) + } } -} -impl TryFrom for time::Duration { - type Error = DurationError; - - /// Converts a `Duration` to a `std::time::Duration`, failing if the duration is negative. - fn try_from(mut duration: Duration) -> Result { - duration.normalize(); - if duration.seconds >= 0 && duration.nanos >= 0 { - Ok(time::Duration::new( - duration.seconds as u64, - duration.nanos as u32, - )) - } else { - Err(DurationError::NegativeDuration(time::Duration::new( - (-duration.seconds) as u64, - (-duration.nanos) as u32, - ))) + impl TryFrom for time::Duration { + type Error = DurationError; + + /// Converts a `Duration` to a `std::time::Duration`, failing if the duration is negative. + fn try_from(mut duration: Duration) -> Result { + duration.normalize(); + if duration.seconds >= 0 && duration.nanos >= 0 { + Ok(time::Duration::new( + duration.seconds as u64, + duration.nanos as u32, + )) + } else { + Err(DurationError::NegativeDuration(time::Duration::new( + (-duration.seconds) as u64, + (-duration.nanos) as u32, + ))) + } } } -} -impl fmt::Display for Duration { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut d = self.clone(); - d.normalize(); - if self.seconds < 0 && self.nanos < 0 { - write!(f, "-")?; - } - write!(f, "{}", d.seconds.abs())?; - - // Format subseconds to either nothing, millis, micros, or nanos. - let nanos = d.nanos.abs(); - if nanos == 0 { - write!(f, "s") - } else if nanos % 1_000_000 == 0 { - write!(f, ".{:03}s", nanos / 1_000_000) - } else if nanos % 1_000 == 0 { - write!(f, ".{:06}s", nanos / 1_000) - } else { - write!(f, ".{:09}s", nanos) + impl fmt::Display for Duration { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = self.clone(); + d.normalize(); + if self.seconds < 0 && self.nanos < 0 { + write!(f, "-")?; + } + write!(f, "{}", d.seconds.abs())?; + + // Format subseconds to either nothing, millis, micros, or nanos. + let nanos = d.nanos.abs(); + if nanos == 0 { + write!(f, "s") + } else if nanos % 1_000_000 == 0 { + write!(f, ".{:03}s", nanos / 1_000_000) + } else if nanos % 1_000 == 0 { + write!(f, ".{:06}s", nanos / 1_000) + } else { + write!(f, ".{:09}s", nanos) + } } } -} -/// A duration handling error. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Debug, PartialEq)] -#[non_exhaustive] -pub enum DurationError { - /// Indicates failure to parse a [`Duration`] from a string. - /// - /// The [`Duration`] string format is specified in the [Protobuf JSON mapping specification][1]. - /// - /// [1]: https://developers.google.com/protocol-buffers/docs/proto3#json - ParseFailure, - - /// Indicates failure to convert a `prost_types::Duration` to a `std::time::Duration` because - /// the duration is negative. The included `std::time::Duration` matches the magnitude of the - /// original negative `prost_types::Duration`. - NegativeDuration(time::Duration), - - /// Indicates failure to convert a `std::time::Duration` to a `prost_types::Duration`. - /// - /// Converting a `std::time::Duration` to a `prost_types::Duration` fails if the magnitude - /// exceeds that representable by `prost_types::Duration`. - OutOfRange, -} - -impl fmt::Display for DurationError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - DurationError::ParseFailure => write!(f, "failed to parse duration"), - DurationError::NegativeDuration(duration) => { - write!(f, "failed to convert negative duration: {:?}", duration) - } - DurationError::OutOfRange => { - write!(f, "failed to convert duration out of range") + /// A duration handling error. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Debug, PartialEq)] + #[non_exhaustive] + pub enum DurationError { + /// Indicates failure to parse a [`Duration`] from a string. + /// + /// The [`Duration`] string format is specified in the [Protobuf JSON mapping specification][1]. + /// + /// [1]: https://developers.google.com/protocol-buffers/docs/proto3#json + ParseFailure, + + /// Indicates failure to convert a `prost_types::Duration` to a `std::time::Duration` because + /// the duration is negative. The included `std::time::Duration` matches the magnitude of the + /// original negative `prost_types::Duration`. + NegativeDuration(time::Duration), + + /// Indicates failure to convert a `std::time::Duration` to a `prost_types::Duration`. + /// + /// Converting a `std::time::Duration` to a `prost_types::Duration` fails if the magnitude + /// exceeds that representable by `prost_types::Duration`. + OutOfRange, + } + + impl fmt::Display for DurationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DurationError::ParseFailure => write!(f, "failed to parse duration"), + DurationError::NegativeDuration(duration) => { + write!(f, "failed to convert negative duration: {:?}", duration) + } + DurationError::OutOfRange => { + write!(f, "failed to convert duration out of range") + } } } } -} -#[cfg(feature = "std")] -impl std::error::Error for DurationError {} + #[cfg(feature = "std")] + impl std::error::Error for DurationError {} -impl FromStr for Duration { - type Err = DurationError; + impl FromStr for Duration { + type Err = DurationError; - fn from_str(s: &str) -> Result { - datetime::parse_duration(s).ok_or(DurationError::ParseFailure) + fn from_str(s: &str) -> Result { + datetime::parse_duration(s).ok_or(DurationError::ParseFailure) + } } } -impl Timestamp { - /// Normalizes the timestamp to a canonical format. - /// - /// Based on [`google::protobuf::util::CreateNormalized`][1]. - /// - /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L59-L77 - pub fn normalize(&mut self) { - // Make sure nanos is in the range. - if self.nanos <= -NANOS_PER_SECOND || self.nanos >= NANOS_PER_SECOND { - if let Some(seconds) = self - .seconds - .checked_add((self.nanos / NANOS_PER_SECOND) as i64) - { - self.seconds = seconds; - self.nanos %= NANOS_PER_SECOND; - } else if self.nanos < 0 { - // Negative overflow! Set to the earliest normal value. - self.seconds = i64::MIN; - self.nanos = 0; - } else { - // Positive overflow! Set to the latest normal value. - self.seconds = i64::MAX; - self.nanos = 999_999_999; +pub use timestamp::TimestampError; +mod timestamp { + use super::*; + + impl Timestamp { + /// Normalizes the timestamp to a canonical format. + /// + /// Based on [`google::protobuf::util::CreateNormalized`][1]. + /// + /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L59-L77 + pub fn normalize(&mut self) { + // Make sure nanos is in the range. + if self.nanos <= -NANOS_PER_SECOND || self.nanos >= NANOS_PER_SECOND { + if let Some(seconds) = self + .seconds + .checked_add((self.nanos / NANOS_PER_SECOND) as i64) + { + self.seconds = seconds; + self.nanos %= NANOS_PER_SECOND; + } else if self.nanos < 0 { + // Negative overflow! Set to the earliest normal value. + self.seconds = i64::MIN; + self.nanos = 0; + } else { + // Positive overflow! Set to the latest normal value. + self.seconds = i64::MAX; + self.nanos = 999_999_999; + } + } + + // For Timestamp nanos should be in the range [0, 999999999]. + if self.nanos < 0 { + if let Some(seconds) = self.seconds.checked_sub(1) { + self.seconds = seconds; + self.nanos += NANOS_PER_SECOND; + } else { + // Negative overflow! Set to the earliest normal value. + debug_assert_eq!(self.seconds, i64::MIN); + self.nanos = 0; + } } + + // TODO: should this be checked? + // debug_assert!(self.seconds >= -62_135_596_800 && self.seconds <= 253_402_300_799, + // "invalid timestamp: {:?}", self); } - // For Timestamp nanos should be in the range [0, 999999999]. - if self.nanos < 0 { - if let Some(seconds) = self.seconds.checked_sub(1) { - self.seconds = seconds; - self.nanos += NANOS_PER_SECOND; + /// Normalizes the timestamp to a canonical format, returning the original value if it cannot be + /// normalized. + /// + /// Normalization is based on [`google::protobuf::util::CreateNormalized`][1]. + /// + /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L59-L77 + pub fn try_normalize(mut self) -> Result { + let before = self.clone(); + self.normalize(); + // If the seconds value has changed, and is either i64::MIN or i64::MAX, then the timestamp + // normalization overflowed. + if (self.seconds == i64::MAX || self.seconds == i64::MIN) + && self.seconds != before.seconds + { + Err(before) } else { - // Negative overflow! Set to the earliest normal value. - debug_assert_eq!(self.seconds, i64::MIN); - self.nanos = 0; + Ok(self) } } - // TODO: should this be checked? - // debug_assert!(self.seconds >= -62_135_596_800 && self.seconds <= 253_402_300_799, - // "invalid timestamp: {:?}", self); - } + /// Creates a new `Timestamp` at the start of the provided UTC date. + pub fn date(year: i64, month: u8, day: u8) -> Result { + Timestamp::date_time_nanos(year, month, day, 0, 0, 0, 0) + } - /// Normalizes the timestamp to a canonical format, returning the original value if it cannot be - /// normalized. - /// - /// Normalization is based on [`google::protobuf::util::CreateNormalized`][1]. - /// - /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L59-L77 - pub fn try_normalize(mut self) -> Result { - let before = self.clone(); - self.normalize(); - // If the seconds value has changed, and is either i64::MIN or i64::MAX, then the timestamp - // normalization overflowed. - if (self.seconds == i64::MAX || self.seconds == i64::MIN) && self.seconds != before.seconds - { - Err(before) - } else { - Ok(self) + /// Creates a new `Timestamp` instance with the provided UTC date and time. + pub fn date_time( + year: i64, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + ) -> Result { + Timestamp::date_time_nanos(year, month, day, hour, minute, second, 0) } - } - /// Creates a new `Timestamp` at the start of the provided UTC date. - pub fn date(year: i64, month: u8, day: u8) -> Result { - Timestamp::date_time_nanos(year, month, day, 0, 0, 0, 0) - } + /// Creates a new `Timestamp` instance with the provided UTC date and time. + pub fn date_time_nanos( + year: i64, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + nanos: u32, + ) -> Result { + let date_time = datetime::DateTime { + year, + month, + day, + hour, + minute, + second, + nanos, + }; - /// Creates a new `Timestamp` instance with the provided UTC date and time. - pub fn date_time( - year: i64, - month: u8, - day: u8, - hour: u8, - minute: u8, - second: u8, - ) -> Result { - Timestamp::date_time_nanos(year, month, day, hour, minute, second, 0) + if date_time.is_valid() { + Ok(Timestamp::from(date_time)) + } else { + Err(TimestampError::InvalidDateTime) + } + } } - /// Creates a new `Timestamp` instance with the provided UTC date and time. - pub fn date_time_nanos( - year: i64, - month: u8, - day: u8, - hour: u8, - minute: u8, - second: u8, - nanos: u32, - ) -> Result { - let date_time = datetime::DateTime { - year, - month, - day, - hour, - minute, - second, - nanos, - }; + impl Name for Timestamp { + const PACKAGE: &'static str = PACKAGE; + const NAME: &'static str = "Timestamp"; - if date_time.is_valid() { - Ok(Timestamp::from(date_time)) - } else { - Err(TimestampError::InvalidDateTime) + fn type_url() -> String { + type_url_for::() } } -} - -impl Name for Timestamp { - const PACKAGE: &'static str = PACKAGE; - const NAME: &'static str = "Timestamp"; - fn type_url() -> String { - type_url_for::() - } -} + /// Implements the unstable/naive version of `Eq`: a basic equality check on the internal fields of the `Timestamp`. + /// This implies that `normalized_ts != non_normalized_ts` even if `normalized_ts == non_normalized_ts.normalized()`. + #[cfg(feature = "std")] + impl Eq for Timestamp {} -/// Implements the unstable/naive version of `Eq`: a basic equality check on the internal fields of the `Timestamp`. -/// This implies that `normalized_ts != non_normalized_ts` even if `normalized_ts == non_normalized_ts.normalized()`. -#[cfg(feature = "std")] -impl Eq for Timestamp {} - -#[cfg(feature = "std")] -#[allow(clippy::derive_hash_xor_eq)] // Derived logic is correct: comparing the 2 fields for equality -impl std::hash::Hash for Timestamp { - fn hash(&self, state: &mut H) { - self.seconds.hash(state); - self.nanos.hash(state); + #[cfg(feature = "std")] + #[allow(clippy::derive_hash_xor_eq)] // Derived logic is correct: comparing the 2 fields for equality + impl std::hash::Hash for Timestamp { + fn hash(&self, state: &mut H) { + self.seconds.hash(state); + self.nanos.hash(state); + } } -} -#[cfg(feature = "std")] -impl From for Timestamp { - fn from(system_time: std::time::SystemTime) -> Timestamp { - let (seconds, nanos) = match system_time.duration_since(std::time::UNIX_EPOCH) { - Ok(duration) => { - let seconds = i64::try_from(duration.as_secs()).unwrap(); - (seconds, duration.subsec_nanos() as i32) - } - Err(error) => { - let duration = error.duration(); - let seconds = i64::try_from(duration.as_secs()).unwrap(); - let nanos = duration.subsec_nanos() as i32; - if nanos == 0 { - (-seconds, 0) - } else { - (-seconds - 1, 1_000_000_000 - nanos) + #[cfg(feature = "std")] + impl From for Timestamp { + fn from(system_time: std::time::SystemTime) -> Timestamp { + let (seconds, nanos) = match system_time.duration_since(std::time::UNIX_EPOCH) { + Ok(duration) => { + let seconds = i64::try_from(duration.as_secs()).unwrap(); + (seconds, duration.subsec_nanos() as i32) } - } - }; - Timestamp { seconds, nanos } + Err(error) => { + let duration = error.duration(); + let seconds = i64::try_from(duration.as_secs()).unwrap(); + let nanos = duration.subsec_nanos() as i32; + if nanos == 0 { + (-seconds, 0) + } else { + (-seconds - 1, 1_000_000_000 - nanos) + } + } + }; + Timestamp { seconds, nanos } + } } -} - -/// A timestamp handling error. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Debug, PartialEq)] -#[non_exhaustive] -pub enum TimestampError { - /// Indicates that a [`Timestamp`] could not be converted to - /// [`SystemTime`][std::time::SystemTime] because it is out of range. - /// - /// The range of times that can be represented by `SystemTime` depends on the platform. All - /// `Timestamp`s are likely representable on 64-bit Unix-like platforms, but other platforms, - /// such as Windows and 32-bit Linux, may not be able to represent the full range of - /// `Timestamp`s. - OutOfSystemRange(Timestamp), - - /// An error indicating failure to parse a timestamp in RFC-3339 format. - ParseFailure, - - /// Indicates an error when constructing a timestamp due to invalid date or time data. - InvalidDateTime, -} -impl fmt::Display for TimestampError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - TimestampError::OutOfSystemRange(timestamp) => { - write!( - f, - "{} is not representable as a `SystemTime` because it is out of range", - timestamp - ) - } - TimestampError::ParseFailure => { - write!(f, "failed to parse RFC-3339 formatted timestamp") - } - TimestampError::InvalidDateTime => { - write!(f, "invalid date or time") + /// A timestamp handling error. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Debug, PartialEq)] + #[non_exhaustive] + pub enum TimestampError { + /// Indicates that a [`Timestamp`] could not be converted to + /// [`SystemTime`][std::time::SystemTime] because it is out of range. + /// + /// The range of times that can be represented by `SystemTime` depends on the platform. All + /// `Timestamp`s are likely representable on 64-bit Unix-like platforms, but other platforms, + /// such as Windows and 32-bit Linux, may not be able to represent the full range of + /// `Timestamp`s. + OutOfSystemRange(Timestamp), + + /// An error indicating failure to parse a timestamp in RFC-3339 format. + ParseFailure, + + /// Indicates an error when constructing a timestamp due to invalid date or time data. + InvalidDateTime, + } + + impl fmt::Display for TimestampError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TimestampError::OutOfSystemRange(timestamp) => { + write!( + f, + "{} is not representable as a `SystemTime` because it is out of range", + timestamp + ) + } + TimestampError::ParseFailure => { + write!(f, "failed to parse RFC-3339 formatted timestamp") + } + TimestampError::InvalidDateTime => { + write!(f, "invalid date or time") + } } } } -} -#[cfg(feature = "std")] -impl std::error::Error for TimestampError {} + #[cfg(feature = "std")] + impl std::error::Error for TimestampError {} -#[cfg(feature = "std")] -impl TryFrom for std::time::SystemTime { - type Error = TimestampError; + #[cfg(feature = "std")] + impl TryFrom for std::time::SystemTime { + type Error = TimestampError; - fn try_from(mut timestamp: Timestamp) -> Result { - let orig_timestamp = timestamp.clone(); - timestamp.normalize(); + fn try_from(mut timestamp: Timestamp) -> Result { + let orig_timestamp = timestamp.clone(); + timestamp.normalize(); - let system_time = if timestamp.seconds >= 0 { - std::time::UNIX_EPOCH.checked_add(time::Duration::from_secs(timestamp.seconds as u64)) - } else { - std::time::UNIX_EPOCH.checked_sub(time::Duration::from_secs( - timestamp - .seconds - .checked_neg() - .ok_or_else(|| TimestampError::OutOfSystemRange(timestamp.clone()))? - as u64, - )) - }; + let system_time = if timestamp.seconds >= 0 { + std::time::UNIX_EPOCH + .checked_add(time::Duration::from_secs(timestamp.seconds as u64)) + } else { + std::time::UNIX_EPOCH.checked_sub(time::Duration::from_secs( + timestamp + .seconds + .checked_neg() + .ok_or_else(|| TimestampError::OutOfSystemRange(timestamp.clone()))? + as u64, + )) + }; - let system_time = system_time.and_then(|system_time| { - system_time.checked_add(time::Duration::from_nanos(timestamp.nanos as u64)) - }); + let system_time = system_time.and_then(|system_time| { + system_time.checked_add(time::Duration::from_nanos(timestamp.nanos as u64)) + }); - system_time.ok_or(TimestampError::OutOfSystemRange(orig_timestamp)) + system_time.ok_or(TimestampError::OutOfSystemRange(orig_timestamp)) + } } -} -impl FromStr for Timestamp { - type Err = TimestampError; + impl FromStr for Timestamp { + type Err = TimestampError; - fn from_str(s: &str) -> Result { - datetime::parse_timestamp(s).ok_or(TimestampError::ParseFailure) + fn from_str(s: &str) -> Result { + datetime::parse_timestamp(s).ok_or(TimestampError::ParseFailure) + } } -} -impl fmt::Display for Timestamp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - datetime::DateTime::from(self.clone()).fmt(f) + impl fmt::Display for Timestamp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + datetime::DateTime::from(self.clone()).fmt(f) + } } } -/// URL/resource name that uniquely identifies the type of the serialized protocol buffer message, -/// e.g. `type.googleapis.com/google.protobuf.Duration`. -/// -/// This string must contain at least one "/" character. -/// -/// The last segment of the URL's path must represent the fully qualified name of the type (as in -/// `path/google.protobuf.Duration`). The name should be in a canonical form (e.g., leading "." is -/// not accepted). -/// -/// If no scheme is provided, `https` is assumed. -/// -/// Schemes other than `http`, `https` (or the empty scheme) might be used with implementation -/// specific semantics. -#[derive(Debug, Eq, PartialEq)] -struct TypeUrl<'a> { - /// Fully qualified name of the type, e.g. `google.protobuf.Duration` - full_name: &'a str, -} - -impl<'a> TypeUrl<'a> { - fn new(s: &'a str) -> core::option::Option { - // Must contain at least one "/" character. - let slash_pos = s.rfind('/')?; +pub(crate) use type_url::{type_url_for, TypeUrl}; +mod type_url { + use super::*; - // The last segment of the URL's path must represent the fully qualified name - // of the type (as in `path/google.protobuf.Duration`) - let full_name = s.get((slash_pos + 1)..)?; + /// URL/resource name that uniquely identifies the type of the serialized protocol buffer message, + /// e.g. `type.googleapis.com/google.protobuf.Duration`. + /// + /// This string must contain at least one "/" character. + /// + /// The last segment of the URL's path must represent the fully qualified name of the type (as in + /// `path/google.protobuf.Duration`). The name should be in a canonical form (e.g., leading "." is + /// not accepted). + /// + /// If no scheme is provided, `https` is assumed. + /// + /// Schemes other than `http`, `https` (or the empty scheme) might be used with implementation + /// specific semantics. + #[derive(Debug, Eq, PartialEq)] + pub(crate) struct TypeUrl<'a> { + /// Fully qualified name of the type, e.g. `google.protobuf.Duration` + pub(crate) full_name: &'a str, + } + + impl<'a> TypeUrl<'a> { + pub(crate) fn new(s: &'a str) -> core::option::Option { + // Must contain at least one "/" character. + let slash_pos = s.rfind('/')?; + + // The last segment of the URL's path must represent the fully qualified name + // of the type (as in `path/google.protobuf.Duration`) + let full_name = s.get((slash_pos + 1)..)?; + + // The name should be in a canonical form (e.g., leading "." is not accepted). + if full_name.starts_with('.') { + return None; + } - // The name should be in a canonical form (e.g., leading "." is not accepted). - if full_name.starts_with('.') { - return None; + Some(Self { full_name }) } - - Some(Self { full_name }) } -} -/// Compute the type URL for the given `google.protobuf` type, using `type.googleapis.com` as the -/// authority for the URL. -fn type_url_for() -> String { - format!("type.googleapis.com/{}.{}", T::PACKAGE, T::NAME) + /// Compute the type URL for the given `google.protobuf` type, using `type.googleapis.com` as the + /// authority for the URL. + pub(crate) fn type_url_for() -> String { + format!("type.googleapis.com/{}.{}", T::PACKAGE, T::NAME) + } } #[cfg(test)] From 17d57938a7069f144740e1405c31496e77d4655c Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 17 Mar 2024 18:17:15 +0100 Subject: [PATCH 2/3] prost-types: Extract modules to separate files. --- prost-types/src/any.rs | 51 ++++ prost-types/src/duration.rs | 176 ++++++++++++ prost-types/src/lib.rs | 526 +---------------------------------- prost-types/src/timestamp.rs | 240 ++++++++++++++++ prost-types/src/type_url.rs | 44 +++ 5 files changed, 515 insertions(+), 522 deletions(-) create mode 100644 prost-types/src/any.rs create mode 100644 prost-types/src/duration.rs create mode 100644 prost-types/src/timestamp.rs create mode 100644 prost-types/src/type_url.rs diff --git a/prost-types/src/any.rs b/prost-types/src/any.rs new file mode 100644 index 000000000..91a954ade --- /dev/null +++ b/prost-types/src/any.rs @@ -0,0 +1,51 @@ +use super::*; + +impl Any { + /// Serialize the given message type `M` as [`Any`]. + pub fn from_msg(msg: &M) -> Result + where + M: Name, + { + let type_url = M::type_url(); + let mut value = Vec::new(); + Message::encode(msg, &mut value)?; + Ok(Any { type_url, value }) + } + + /// Decode the given message type `M` from [`Any`], validating that it has + /// the expected type URL. + pub fn to_msg(&self) -> Result + where + M: Default + Name + Sized, + { + let expected_type_url = M::type_url(); + + match ( + TypeUrl::new(&expected_type_url), + TypeUrl::new(&self.type_url), + ) { + (Some(expected), Some(actual)) => { + if expected == actual { + return Ok(M::decode(self.value.as_slice())?); + } + } + _ => (), + } + + let mut err = DecodeError::new(format!( + "expected type URL: \"{}\" (got: \"{}\")", + expected_type_url, &self.type_url + )); + err.push("unexpected type URL", "type_url"); + Err(err) + } +} + +impl Name for Any { + const PACKAGE: &'static str = PACKAGE; + const NAME: &'static str = "Any"; + + fn type_url() -> String { + type_url_for::() + } +} diff --git a/prost-types/src/duration.rs b/prost-types/src/duration.rs new file mode 100644 index 000000000..9129746d7 --- /dev/null +++ b/prost-types/src/duration.rs @@ -0,0 +1,176 @@ +use super::*; + +#[cfg(feature = "std")] +impl std::hash::Hash for Duration { + fn hash(&self, state: &mut H) { + self.seconds.hash(state); + self.nanos.hash(state); + } +} + +impl Duration { + /// Normalizes the duration to a canonical format. + /// + /// Based on [`google::protobuf::util::CreateNormalized`][1]. + /// + /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L79-L100 + pub fn normalize(&mut self) { + // Make sure nanos is in the range. + if self.nanos <= -NANOS_PER_SECOND || self.nanos >= NANOS_PER_SECOND { + if let Some(seconds) = self + .seconds + .checked_add((self.nanos / NANOS_PER_SECOND) as i64) + { + self.seconds = seconds; + self.nanos %= NANOS_PER_SECOND; + } else if self.nanos < 0 { + // Negative overflow! Set to the least normal value. + self.seconds = i64::MIN; + self.nanos = -NANOS_MAX; + } else { + // Positive overflow! Set to the greatest normal value. + self.seconds = i64::MAX; + self.nanos = NANOS_MAX; + } + } + + // nanos should have the same sign as seconds. + if self.seconds < 0 && self.nanos > 0 { + if let Some(seconds) = self.seconds.checked_add(1) { + self.seconds = seconds; + self.nanos -= NANOS_PER_SECOND; + } else { + // Positive overflow! Set to the greatest normal value. + debug_assert_eq!(self.seconds, i64::MAX); + self.nanos = NANOS_MAX; + } + } else if self.seconds > 0 && self.nanos < 0 { + if let Some(seconds) = self.seconds.checked_sub(1) { + self.seconds = seconds; + self.nanos += NANOS_PER_SECOND; + } else { + // Negative overflow! Set to the least normal value. + debug_assert_eq!(self.seconds, i64::MIN); + self.nanos = -NANOS_MAX; + } + } + // TODO: should this be checked? + // debug_assert!(self.seconds >= -315_576_000_000 && self.seconds <= 315_576_000_000, + // "invalid duration: {:?}", self); + } +} + +impl Name for Duration { + const PACKAGE: &'static str = PACKAGE; + const NAME: &'static str = "Duration"; + + fn type_url() -> String { + type_url_for::() + } +} + +impl TryFrom for Duration { + type Error = DurationError; + + /// Converts a `std::time::Duration` to a `Duration`, failing if the duration is too large. + fn try_from(duration: time::Duration) -> Result { + let seconds = i64::try_from(duration.as_secs()).map_err(|_| DurationError::OutOfRange)?; + let nanos = duration.subsec_nanos() as i32; + + let mut duration = Duration { seconds, nanos }; + duration.normalize(); + Ok(duration) + } +} + +impl TryFrom for time::Duration { + type Error = DurationError; + + /// Converts a `Duration` to a `std::time::Duration`, failing if the duration is negative. + fn try_from(mut duration: Duration) -> Result { + duration.normalize(); + if duration.seconds >= 0 && duration.nanos >= 0 { + Ok(time::Duration::new( + duration.seconds as u64, + duration.nanos as u32, + )) + } else { + Err(DurationError::NegativeDuration(time::Duration::new( + (-duration.seconds) as u64, + (-duration.nanos) as u32, + ))) + } + } +} + +impl fmt::Display for Duration { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = self.clone(); + d.normalize(); + if self.seconds < 0 && self.nanos < 0 { + write!(f, "-")?; + } + write!(f, "{}", d.seconds.abs())?; + + // Format subseconds to either nothing, millis, micros, or nanos. + let nanos = d.nanos.abs(); + if nanos == 0 { + write!(f, "s") + } else if nanos % 1_000_000 == 0 { + write!(f, ".{:03}s", nanos / 1_000_000) + } else if nanos % 1_000 == 0 { + write!(f, ".{:06}s", nanos / 1_000) + } else { + write!(f, ".{:09}s", nanos) + } + } +} + +/// A duration handling error. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, PartialEq)] +#[non_exhaustive] +pub enum DurationError { + /// Indicates failure to parse a [`Duration`] from a string. + /// + /// The [`Duration`] string format is specified in the [Protobuf JSON mapping specification][1]. + /// + /// [1]: https://developers.google.com/protocol-buffers/docs/proto3#json + ParseFailure, + + /// Indicates failure to convert a `prost_types::Duration` to a `std::time::Duration` because + /// the duration is negative. The included `std::time::Duration` matches the magnitude of the + /// original negative `prost_types::Duration`. + NegativeDuration(time::Duration), + + /// Indicates failure to convert a `std::time::Duration` to a `prost_types::Duration`. + /// + /// Converting a `std::time::Duration` to a `prost_types::Duration` fails if the magnitude + /// exceeds that representable by `prost_types::Duration`. + OutOfRange, +} + +impl fmt::Display for DurationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DurationError::ParseFailure => write!(f, "failed to parse duration"), + DurationError::NegativeDuration(duration) => { + write!(f, "failed to convert negative duration: {:?}", duration) + } + DurationError::OutOfRange => { + write!(f, "failed to convert duration out of range") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for DurationError {} + +impl FromStr for Duration { + type Err = DurationError; + + fn from_str(s: &str) -> Result { + datetime::parse_duration(s).ok_or(DurationError::ParseFailure) + } +} diff --git a/prost-types/src/lib.rs b/prost-types/src/lib.rs index dcfac8610..381e16e73 100644 --- a/prost-types/src/lib.rs +++ b/prost-types/src/lib.rs @@ -40,534 +40,16 @@ const NANOS_MAX: i32 = NANOS_PER_SECOND - 1; const PACKAGE: &str = "google.protobuf"; -mod any { - use super::*; - - impl Any { - /// Serialize the given message type `M` as [`Any`]. - pub fn from_msg(msg: &M) -> Result - where - M: Name, - { - let type_url = M::type_url(); - let mut value = Vec::new(); - Message::encode(msg, &mut value)?; - Ok(Any { type_url, value }) - } - - /// Decode the given message type `M` from [`Any`], validating that it has - /// the expected type URL. - pub fn to_msg(&self) -> Result - where - M: Default + Name + Sized, - { - let expected_type_url = M::type_url(); - - match ( - TypeUrl::new(&expected_type_url), - TypeUrl::new(&self.type_url), - ) { - (Some(expected), Some(actual)) => { - if expected == actual { - return Ok(M::decode(self.value.as_slice())?); - } - } - _ => (), - } - - let mut err = DecodeError::new(format!( - "expected type URL: \"{}\" (got: \"{}\")", - expected_type_url, &self.type_url - )); - err.push("unexpected type URL", "type_url"); - Err(err) - } - } - - impl Name for Any { - const PACKAGE: &'static str = PACKAGE; - const NAME: &'static str = "Any"; - - fn type_url() -> String { - type_url_for::() - } - } -} +mod any; +mod duration; pub use duration::DurationError; -mod duration { - use super::*; - - #[cfg(feature = "std")] - impl std::hash::Hash for Duration { - fn hash(&self, state: &mut H) { - self.seconds.hash(state); - self.nanos.hash(state); - } - } - - impl Duration { - /// Normalizes the duration to a canonical format. - /// - /// Based on [`google::protobuf::util::CreateNormalized`][1]. - /// - /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L79-L100 - pub fn normalize(&mut self) { - // Make sure nanos is in the range. - if self.nanos <= -NANOS_PER_SECOND || self.nanos >= NANOS_PER_SECOND { - if let Some(seconds) = self - .seconds - .checked_add((self.nanos / NANOS_PER_SECOND) as i64) - { - self.seconds = seconds; - self.nanos %= NANOS_PER_SECOND; - } else if self.nanos < 0 { - // Negative overflow! Set to the least normal value. - self.seconds = i64::MIN; - self.nanos = -NANOS_MAX; - } else { - // Positive overflow! Set to the greatest normal value. - self.seconds = i64::MAX; - self.nanos = NANOS_MAX; - } - } - - // nanos should have the same sign as seconds. - if self.seconds < 0 && self.nanos > 0 { - if let Some(seconds) = self.seconds.checked_add(1) { - self.seconds = seconds; - self.nanos -= NANOS_PER_SECOND; - } else { - // Positive overflow! Set to the greatest normal value. - debug_assert_eq!(self.seconds, i64::MAX); - self.nanos = NANOS_MAX; - } - } else if self.seconds > 0 && self.nanos < 0 { - if let Some(seconds) = self.seconds.checked_sub(1) { - self.seconds = seconds; - self.nanos += NANOS_PER_SECOND; - } else { - // Negative overflow! Set to the least normal value. - debug_assert_eq!(self.seconds, i64::MIN); - self.nanos = -NANOS_MAX; - } - } - // TODO: should this be checked? - // debug_assert!(self.seconds >= -315_576_000_000 && self.seconds <= 315_576_000_000, - // "invalid duration: {:?}", self); - } - } - - impl Name for Duration { - const PACKAGE: &'static str = PACKAGE; - const NAME: &'static str = "Duration"; - - fn type_url() -> String { - type_url_for::() - } - } - - impl TryFrom for Duration { - type Error = DurationError; - - /// Converts a `std::time::Duration` to a `Duration`, failing if the duration is too large. - fn try_from(duration: time::Duration) -> Result { - let seconds = - i64::try_from(duration.as_secs()).map_err(|_| DurationError::OutOfRange)?; - let nanos = duration.subsec_nanos() as i32; - - let mut duration = Duration { seconds, nanos }; - duration.normalize(); - Ok(duration) - } - } - - impl TryFrom for time::Duration { - type Error = DurationError; - - /// Converts a `Duration` to a `std::time::Duration`, failing if the duration is negative. - fn try_from(mut duration: Duration) -> Result { - duration.normalize(); - if duration.seconds >= 0 && duration.nanos >= 0 { - Ok(time::Duration::new( - duration.seconds as u64, - duration.nanos as u32, - )) - } else { - Err(DurationError::NegativeDuration(time::Duration::new( - (-duration.seconds) as u64, - (-duration.nanos) as u32, - ))) - } - } - } - - impl fmt::Display for Duration { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut d = self.clone(); - d.normalize(); - if self.seconds < 0 && self.nanos < 0 { - write!(f, "-")?; - } - write!(f, "{}", d.seconds.abs())?; - - // Format subseconds to either nothing, millis, micros, or nanos. - let nanos = d.nanos.abs(); - if nanos == 0 { - write!(f, "s") - } else if nanos % 1_000_000 == 0 { - write!(f, ".{:03}s", nanos / 1_000_000) - } else if nanos % 1_000 == 0 { - write!(f, ".{:06}s", nanos / 1_000) - } else { - write!(f, ".{:09}s", nanos) - } - } - } - - /// A duration handling error. - #[allow(clippy::derive_partial_eq_without_eq)] - #[derive(Debug, PartialEq)] - #[non_exhaustive] - pub enum DurationError { - /// Indicates failure to parse a [`Duration`] from a string. - /// - /// The [`Duration`] string format is specified in the [Protobuf JSON mapping specification][1]. - /// - /// [1]: https://developers.google.com/protocol-buffers/docs/proto3#json - ParseFailure, - - /// Indicates failure to convert a `prost_types::Duration` to a `std::time::Duration` because - /// the duration is negative. The included `std::time::Duration` matches the magnitude of the - /// original negative `prost_types::Duration`. - NegativeDuration(time::Duration), - - /// Indicates failure to convert a `std::time::Duration` to a `prost_types::Duration`. - /// - /// Converting a `std::time::Duration` to a `prost_types::Duration` fails if the magnitude - /// exceeds that representable by `prost_types::Duration`. - OutOfRange, - } - - impl fmt::Display for DurationError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - DurationError::ParseFailure => write!(f, "failed to parse duration"), - DurationError::NegativeDuration(duration) => { - write!(f, "failed to convert negative duration: {:?}", duration) - } - DurationError::OutOfRange => { - write!(f, "failed to convert duration out of range") - } - } - } - } - - #[cfg(feature = "std")] - impl std::error::Error for DurationError {} - - impl FromStr for Duration { - type Err = DurationError; - - fn from_str(s: &str) -> Result { - datetime::parse_duration(s).ok_or(DurationError::ParseFailure) - } - } -} +mod timestamp; pub use timestamp::TimestampError; -mod timestamp { - use super::*; - - impl Timestamp { - /// Normalizes the timestamp to a canonical format. - /// - /// Based on [`google::protobuf::util::CreateNormalized`][1]. - /// - /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L59-L77 - pub fn normalize(&mut self) { - // Make sure nanos is in the range. - if self.nanos <= -NANOS_PER_SECOND || self.nanos >= NANOS_PER_SECOND { - if let Some(seconds) = self - .seconds - .checked_add((self.nanos / NANOS_PER_SECOND) as i64) - { - self.seconds = seconds; - self.nanos %= NANOS_PER_SECOND; - } else if self.nanos < 0 { - // Negative overflow! Set to the earliest normal value. - self.seconds = i64::MIN; - self.nanos = 0; - } else { - // Positive overflow! Set to the latest normal value. - self.seconds = i64::MAX; - self.nanos = 999_999_999; - } - } - - // For Timestamp nanos should be in the range [0, 999999999]. - if self.nanos < 0 { - if let Some(seconds) = self.seconds.checked_sub(1) { - self.seconds = seconds; - self.nanos += NANOS_PER_SECOND; - } else { - // Negative overflow! Set to the earliest normal value. - debug_assert_eq!(self.seconds, i64::MIN); - self.nanos = 0; - } - } - - // TODO: should this be checked? - // debug_assert!(self.seconds >= -62_135_596_800 && self.seconds <= 253_402_300_799, - // "invalid timestamp: {:?}", self); - } - - /// Normalizes the timestamp to a canonical format, returning the original value if it cannot be - /// normalized. - /// - /// Normalization is based on [`google::protobuf::util::CreateNormalized`][1]. - /// - /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L59-L77 - pub fn try_normalize(mut self) -> Result { - let before = self.clone(); - self.normalize(); - // If the seconds value has changed, and is either i64::MIN or i64::MAX, then the timestamp - // normalization overflowed. - if (self.seconds == i64::MAX || self.seconds == i64::MIN) - && self.seconds != before.seconds - { - Err(before) - } else { - Ok(self) - } - } - - /// Creates a new `Timestamp` at the start of the provided UTC date. - pub fn date(year: i64, month: u8, day: u8) -> Result { - Timestamp::date_time_nanos(year, month, day, 0, 0, 0, 0) - } - - /// Creates a new `Timestamp` instance with the provided UTC date and time. - pub fn date_time( - year: i64, - month: u8, - day: u8, - hour: u8, - minute: u8, - second: u8, - ) -> Result { - Timestamp::date_time_nanos(year, month, day, hour, minute, second, 0) - } - - /// Creates a new `Timestamp` instance with the provided UTC date and time. - pub fn date_time_nanos( - year: i64, - month: u8, - day: u8, - hour: u8, - minute: u8, - second: u8, - nanos: u32, - ) -> Result { - let date_time = datetime::DateTime { - year, - month, - day, - hour, - minute, - second, - nanos, - }; - - if date_time.is_valid() { - Ok(Timestamp::from(date_time)) - } else { - Err(TimestampError::InvalidDateTime) - } - } - } - - impl Name for Timestamp { - const PACKAGE: &'static str = PACKAGE; - const NAME: &'static str = "Timestamp"; - - fn type_url() -> String { - type_url_for::() - } - } - - /// Implements the unstable/naive version of `Eq`: a basic equality check on the internal fields of the `Timestamp`. - /// This implies that `normalized_ts != non_normalized_ts` even if `normalized_ts == non_normalized_ts.normalized()`. - #[cfg(feature = "std")] - impl Eq for Timestamp {} - - #[cfg(feature = "std")] - #[allow(clippy::derive_hash_xor_eq)] // Derived logic is correct: comparing the 2 fields for equality - impl std::hash::Hash for Timestamp { - fn hash(&self, state: &mut H) { - self.seconds.hash(state); - self.nanos.hash(state); - } - } - - #[cfg(feature = "std")] - impl From for Timestamp { - fn from(system_time: std::time::SystemTime) -> Timestamp { - let (seconds, nanos) = match system_time.duration_since(std::time::UNIX_EPOCH) { - Ok(duration) => { - let seconds = i64::try_from(duration.as_secs()).unwrap(); - (seconds, duration.subsec_nanos() as i32) - } - Err(error) => { - let duration = error.duration(); - let seconds = i64::try_from(duration.as_secs()).unwrap(); - let nanos = duration.subsec_nanos() as i32; - if nanos == 0 { - (-seconds, 0) - } else { - (-seconds - 1, 1_000_000_000 - nanos) - } - } - }; - Timestamp { seconds, nanos } - } - } - - /// A timestamp handling error. - #[allow(clippy::derive_partial_eq_without_eq)] - #[derive(Debug, PartialEq)] - #[non_exhaustive] - pub enum TimestampError { - /// Indicates that a [`Timestamp`] could not be converted to - /// [`SystemTime`][std::time::SystemTime] because it is out of range. - /// - /// The range of times that can be represented by `SystemTime` depends on the platform. All - /// `Timestamp`s are likely representable on 64-bit Unix-like platforms, but other platforms, - /// such as Windows and 32-bit Linux, may not be able to represent the full range of - /// `Timestamp`s. - OutOfSystemRange(Timestamp), - - /// An error indicating failure to parse a timestamp in RFC-3339 format. - ParseFailure, - - /// Indicates an error when constructing a timestamp due to invalid date or time data. - InvalidDateTime, - } - - impl fmt::Display for TimestampError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - TimestampError::OutOfSystemRange(timestamp) => { - write!( - f, - "{} is not representable as a `SystemTime` because it is out of range", - timestamp - ) - } - TimestampError::ParseFailure => { - write!(f, "failed to parse RFC-3339 formatted timestamp") - } - TimestampError::InvalidDateTime => { - write!(f, "invalid date or time") - } - } - } - } - - #[cfg(feature = "std")] - impl std::error::Error for TimestampError {} - - #[cfg(feature = "std")] - impl TryFrom for std::time::SystemTime { - type Error = TimestampError; - - fn try_from(mut timestamp: Timestamp) -> Result { - let orig_timestamp = timestamp.clone(); - timestamp.normalize(); - - let system_time = if timestamp.seconds >= 0 { - std::time::UNIX_EPOCH - .checked_add(time::Duration::from_secs(timestamp.seconds as u64)) - } else { - std::time::UNIX_EPOCH.checked_sub(time::Duration::from_secs( - timestamp - .seconds - .checked_neg() - .ok_or_else(|| TimestampError::OutOfSystemRange(timestamp.clone()))? - as u64, - )) - }; - - let system_time = system_time.and_then(|system_time| { - system_time.checked_add(time::Duration::from_nanos(timestamp.nanos as u64)) - }); - - system_time.ok_or(TimestampError::OutOfSystemRange(orig_timestamp)) - } - } - - impl FromStr for Timestamp { - type Err = TimestampError; - - fn from_str(s: &str) -> Result { - datetime::parse_timestamp(s).ok_or(TimestampError::ParseFailure) - } - } - - impl fmt::Display for Timestamp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - datetime::DateTime::from(self.clone()).fmt(f) - } - } -} +mod type_url; pub(crate) use type_url::{type_url_for, TypeUrl}; -mod type_url { - use super::*; - - /// URL/resource name that uniquely identifies the type of the serialized protocol buffer message, - /// e.g. `type.googleapis.com/google.protobuf.Duration`. - /// - /// This string must contain at least one "/" character. - /// - /// The last segment of the URL's path must represent the fully qualified name of the type (as in - /// `path/google.protobuf.Duration`). The name should be in a canonical form (e.g., leading "." is - /// not accepted). - /// - /// If no scheme is provided, `https` is assumed. - /// - /// Schemes other than `http`, `https` (or the empty scheme) might be used with implementation - /// specific semantics. - #[derive(Debug, Eq, PartialEq)] - pub(crate) struct TypeUrl<'a> { - /// Fully qualified name of the type, e.g. `google.protobuf.Duration` - pub(crate) full_name: &'a str, - } - - impl<'a> TypeUrl<'a> { - pub(crate) fn new(s: &'a str) -> core::option::Option { - // Must contain at least one "/" character. - let slash_pos = s.rfind('/')?; - - // The last segment of the URL's path must represent the fully qualified name - // of the type (as in `path/google.protobuf.Duration`) - let full_name = s.get((slash_pos + 1)..)?; - - // The name should be in a canonical form (e.g., leading "." is not accepted). - if full_name.starts_with('.') { - return None; - } - - Some(Self { full_name }) - } - } - - /// Compute the type URL for the given `google.protobuf` type, using `type.googleapis.com` as the - /// authority for the URL. - pub(crate) fn type_url_for() -> String { - format!("type.googleapis.com/{}.{}", T::PACKAGE, T::NAME) - } -} #[cfg(test)] mod tests { diff --git a/prost-types/src/timestamp.rs b/prost-types/src/timestamp.rs new file mode 100644 index 000000000..774f54e66 --- /dev/null +++ b/prost-types/src/timestamp.rs @@ -0,0 +1,240 @@ +use super::*; + +impl Timestamp { + /// Normalizes the timestamp to a canonical format. + /// + /// Based on [`google::protobuf::util::CreateNormalized`][1]. + /// + /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L59-L77 + pub fn normalize(&mut self) { + // Make sure nanos is in the range. + if self.nanos <= -NANOS_PER_SECOND || self.nanos >= NANOS_PER_SECOND { + if let Some(seconds) = self + .seconds + .checked_add((self.nanos / NANOS_PER_SECOND) as i64) + { + self.seconds = seconds; + self.nanos %= NANOS_PER_SECOND; + } else if self.nanos < 0 { + // Negative overflow! Set to the earliest normal value. + self.seconds = i64::MIN; + self.nanos = 0; + } else { + // Positive overflow! Set to the latest normal value. + self.seconds = i64::MAX; + self.nanos = 999_999_999; + } + } + + // For Timestamp nanos should be in the range [0, 999999999]. + if self.nanos < 0 { + if let Some(seconds) = self.seconds.checked_sub(1) { + self.seconds = seconds; + self.nanos += NANOS_PER_SECOND; + } else { + // Negative overflow! Set to the earliest normal value. + debug_assert_eq!(self.seconds, i64::MIN); + self.nanos = 0; + } + } + + // TODO: should this be checked? + // debug_assert!(self.seconds >= -62_135_596_800 && self.seconds <= 253_402_300_799, + // "invalid timestamp: {:?}", self); + } + + /// Normalizes the timestamp to a canonical format, returning the original value if it cannot be + /// normalized. + /// + /// Normalization is based on [`google::protobuf::util::CreateNormalized`][1]. + /// + /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L59-L77 + pub fn try_normalize(mut self) -> Result { + let before = self.clone(); + self.normalize(); + // If the seconds value has changed, and is either i64::MIN or i64::MAX, then the timestamp + // normalization overflowed. + if (self.seconds == i64::MAX || self.seconds == i64::MIN) && self.seconds != before.seconds + { + Err(before) + } else { + Ok(self) + } + } + + /// Creates a new `Timestamp` at the start of the provided UTC date. + pub fn date(year: i64, month: u8, day: u8) -> Result { + Timestamp::date_time_nanos(year, month, day, 0, 0, 0, 0) + } + + /// Creates a new `Timestamp` instance with the provided UTC date and time. + pub fn date_time( + year: i64, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + ) -> Result { + Timestamp::date_time_nanos(year, month, day, hour, minute, second, 0) + } + + /// Creates a new `Timestamp` instance with the provided UTC date and time. + pub fn date_time_nanos( + year: i64, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + nanos: u32, + ) -> Result { + let date_time = datetime::DateTime { + year, + month, + day, + hour, + minute, + second, + nanos, + }; + + if date_time.is_valid() { + Ok(Timestamp::from(date_time)) + } else { + Err(TimestampError::InvalidDateTime) + } + } +} + +impl Name for Timestamp { + const PACKAGE: &'static str = PACKAGE; + const NAME: &'static str = "Timestamp"; + + fn type_url() -> String { + type_url_for::() + } +} + +/// Implements the unstable/naive version of `Eq`: a basic equality check on the internal fields of the `Timestamp`. +/// This implies that `normalized_ts != non_normalized_ts` even if `normalized_ts == non_normalized_ts.normalized()`. +#[cfg(feature = "std")] +impl Eq for Timestamp {} + +#[cfg(feature = "std")] +#[allow(clippy::derive_hash_xor_eq)] // Derived logic is correct: comparing the 2 fields for equality +impl std::hash::Hash for Timestamp { + fn hash(&self, state: &mut H) { + self.seconds.hash(state); + self.nanos.hash(state); + } +} + +#[cfg(feature = "std")] +impl From for Timestamp { + fn from(system_time: std::time::SystemTime) -> Timestamp { + let (seconds, nanos) = match system_time.duration_since(std::time::UNIX_EPOCH) { + Ok(duration) => { + let seconds = i64::try_from(duration.as_secs()).unwrap(); + (seconds, duration.subsec_nanos() as i32) + } + Err(error) => { + let duration = error.duration(); + let seconds = i64::try_from(duration.as_secs()).unwrap(); + let nanos = duration.subsec_nanos() as i32; + if nanos == 0 { + (-seconds, 0) + } else { + (-seconds - 1, 1_000_000_000 - nanos) + } + } + }; + Timestamp { seconds, nanos } + } +} + +/// A timestamp handling error. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, PartialEq)] +#[non_exhaustive] +pub enum TimestampError { + /// Indicates that a [`Timestamp`] could not be converted to + /// [`SystemTime`][std::time::SystemTime] because it is out of range. + /// + /// The range of times that can be represented by `SystemTime` depends on the platform. All + /// `Timestamp`s are likely representable on 64-bit Unix-like platforms, but other platforms, + /// such as Windows and 32-bit Linux, may not be able to represent the full range of + /// `Timestamp`s. + OutOfSystemRange(Timestamp), + + /// An error indicating failure to parse a timestamp in RFC-3339 format. + ParseFailure, + + /// Indicates an error when constructing a timestamp due to invalid date or time data. + InvalidDateTime, +} + +impl fmt::Display for TimestampError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TimestampError::OutOfSystemRange(timestamp) => { + write!( + f, + "{} is not representable as a `SystemTime` because it is out of range", + timestamp + ) + } + TimestampError::ParseFailure => { + write!(f, "failed to parse RFC-3339 formatted timestamp") + } + TimestampError::InvalidDateTime => { + write!(f, "invalid date or time") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for TimestampError {} + +#[cfg(feature = "std")] +impl TryFrom for std::time::SystemTime { + type Error = TimestampError; + + fn try_from(mut timestamp: Timestamp) -> Result { + let orig_timestamp = timestamp.clone(); + timestamp.normalize(); + + let system_time = if timestamp.seconds >= 0 { + std::time::UNIX_EPOCH.checked_add(time::Duration::from_secs(timestamp.seconds as u64)) + } else { + std::time::UNIX_EPOCH.checked_sub(time::Duration::from_secs( + timestamp + .seconds + .checked_neg() + .ok_or_else(|| TimestampError::OutOfSystemRange(timestamp.clone()))? + as u64, + )) + }; + + let system_time = system_time.and_then(|system_time| { + system_time.checked_add(time::Duration::from_nanos(timestamp.nanos as u64)) + }); + + system_time.ok_or(TimestampError::OutOfSystemRange(orig_timestamp)) + } +} + +impl FromStr for Timestamp { + type Err = TimestampError; + + fn from_str(s: &str) -> Result { + datetime::parse_timestamp(s).ok_or(TimestampError::ParseFailure) + } +} + +impl fmt::Display for Timestamp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + datetime::DateTime::from(self.clone()).fmt(f) + } +} diff --git a/prost-types/src/type_url.rs b/prost-types/src/type_url.rs new file mode 100644 index 000000000..56ba1ce75 --- /dev/null +++ b/prost-types/src/type_url.rs @@ -0,0 +1,44 @@ +use super::*; + +/// URL/resource name that uniquely identifies the type of the serialized protocol buffer message, +/// e.g. `type.googleapis.com/google.protobuf.Duration`. +/// +/// This string must contain at least one "/" character. +/// +/// The last segment of the URL's path must represent the fully qualified name of the type (as in +/// `path/google.protobuf.Duration`). The name should be in a canonical form (e.g., leading "." is +/// not accepted). +/// +/// If no scheme is provided, `https` is assumed. +/// +/// Schemes other than `http`, `https` (or the empty scheme) might be used with implementation +/// specific semantics. +#[derive(Debug, Eq, PartialEq)] +pub(crate) struct TypeUrl<'a> { + /// Fully qualified name of the type, e.g. `google.protobuf.Duration` + pub(crate) full_name: &'a str, +} + +impl<'a> TypeUrl<'a> { + pub(crate) fn new(s: &'a str) -> core::option::Option { + // Must contain at least one "/" character. + let slash_pos = s.rfind('/')?; + + // The last segment of the URL's path must represent the fully qualified name + // of the type (as in `path/google.protobuf.Duration`) + let full_name = s.get((slash_pos + 1)..)?; + + // The name should be in a canonical form (e.g., leading "." is not accepted). + if full_name.starts_with('.') { + return None; + } + + Some(Self { full_name }) + } +} + +/// Compute the type URL for the given `google.protobuf` type, using `type.googleapis.com` as the +/// authority for the URL. +pub(crate) fn type_url_for() -> String { + format!("type.googleapis.com/{}.{}", T::PACKAGE, T::NAME) +} From 9000830708f6972da5915d815041d76862efd739 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 17 Mar 2024 18:25:47 +0100 Subject: [PATCH 3/3] prost-types: Move tests to the respective modules. --- prost-types/src/any.rs | 21 ++ prost-types/src/duration.rs | 157 +++++++++++++++ prost-types/src/lib.rs | 362 ----------------------------------- prost-types/src/timestamp.rs | 177 +++++++++++++++++ prost-types/src/type_url.rs | 26 +++ 5 files changed, 381 insertions(+), 362 deletions(-) diff --git a/prost-types/src/any.rs b/prost-types/src/any.rs index 91a954ade..8c2bda479 100644 --- a/prost-types/src/any.rs +++ b/prost-types/src/any.rs @@ -49,3 +49,24 @@ impl Name for Any { type_url_for::() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_any_serialization() { + let message = Timestamp::date(2000, 1, 1).unwrap(); + let any = Any::from_msg(&message).unwrap(); + assert_eq!( + &any.type_url, + "type.googleapis.com/google.protobuf.Timestamp" + ); + + let message2 = any.to_msg::().unwrap(); + assert_eq!(message, message2); + + // Wrong type URL + assert!(any.to_msg::().is_err()); + } +} diff --git a/prost-types/src/duration.rs b/prost-types/src/duration.rs index 9129746d7..600716933 100644 --- a/prost-types/src/duration.rs +++ b/prost-types/src/duration.rs @@ -174,3 +174,160 @@ impl FromStr for Duration { datetime::parse_duration(s).ok_or(DurationError::ParseFailure) } } +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(feature = "std")] + use proptest::prelude::*; + + #[cfg(feature = "std")] + proptest! { + #[test] + fn check_duration_roundtrip( + seconds in u64::arbitrary(), + nanos in 0u32..1_000_000_000u32, + ) { + let std_duration = time::Duration::new(seconds, nanos); + let prost_duration = match Duration::try_from(std_duration) { + Ok(duration) => duration, + Err(_) => return Err(TestCaseError::reject("duration out of range")), + }; + prop_assert_eq!(time::Duration::try_from(prost_duration.clone()).unwrap(), std_duration); + + if std_duration != time::Duration::default() { + let neg_prost_duration = Duration { + seconds: -prost_duration.seconds, + nanos: -prost_duration.nanos, + }; + + prop_assert!( + matches!( + time::Duration::try_from(neg_prost_duration), + Err(DurationError::NegativeDuration(d)) if d == std_duration, + ) + ) + } + } + + #[test] + fn check_duration_roundtrip_nanos( + nanos in u32::arbitrary(), + ) { + let seconds = 0; + let std_duration = std::time::Duration::new(seconds, nanos); + let prost_duration = match Duration::try_from(std_duration) { + Ok(duration) => duration, + Err(_) => return Err(TestCaseError::reject("duration out of range")), + }; + prop_assert_eq!(time::Duration::try_from(prost_duration.clone()).unwrap(), std_duration); + + if std_duration != time::Duration::default() { + let neg_prost_duration = Duration { + seconds: -prost_duration.seconds, + nanos: -prost_duration.nanos, + }; + + prop_assert!( + matches!( + time::Duration::try_from(neg_prost_duration), + Err(DurationError::NegativeDuration(d)) if d == std_duration, + ) + ) + } + } + } + + #[cfg(feature = "std")] + #[test] + fn check_duration_try_from_negative_nanos() { + let seconds: u64 = 0; + let nanos: u32 = 1; + let std_duration = std::time::Duration::new(seconds, nanos); + + let neg_prost_duration = Duration { + seconds: 0, + nanos: -1, + }; + + assert!(matches!( + time::Duration::try_from(neg_prost_duration), + Err(DurationError::NegativeDuration(d)) if d == std_duration, + )) + } + + #[test] + fn check_duration_normalize() { + #[rustfmt::skip] // Don't mangle the table formatting. + let cases = [ + // --- Table of test cases --- + // test seconds test nanos expected seconds expected nanos + (line!(), 0, 0, 0, 0), + (line!(), 1, 1, 1, 1), + (line!(), -1, -1, -1, -1), + (line!(), 0, 999_999_999, 0, 999_999_999), + (line!(), 0, -999_999_999, 0, -999_999_999), + (line!(), 0, 1_000_000_000, 1, 0), + (line!(), 0, -1_000_000_000, -1, 0), + (line!(), 0, 1_000_000_001, 1, 1), + (line!(), 0, -1_000_000_001, -1, -1), + (line!(), -1, 1, 0, -999_999_999), + (line!(), 1, -1, 0, 999_999_999), + (line!(), -1, 1_000_000_000, 0, 0), + (line!(), 1, -1_000_000_000, 0, 0), + (line!(), i64::MIN , 0, i64::MIN , 0), + (line!(), i64::MIN + 1, 0, i64::MIN + 1, 0), + (line!(), i64::MIN , 1, i64::MIN + 1, -999_999_999), + (line!(), i64::MIN , 1_000_000_000, i64::MIN + 1, 0), + (line!(), i64::MIN , -1_000_000_000, i64::MIN , -999_999_999), + (line!(), i64::MIN + 1, -1_000_000_000, i64::MIN , 0), + (line!(), i64::MIN + 2, -1_000_000_000, i64::MIN + 1, 0), + (line!(), i64::MIN , -1_999_999_998, i64::MIN , -999_999_999), + (line!(), i64::MIN + 1, -1_999_999_998, i64::MIN , -999_999_998), + (line!(), i64::MIN + 2, -1_999_999_998, i64::MIN + 1, -999_999_998), + (line!(), i64::MIN , -1_999_999_999, i64::MIN , -999_999_999), + (line!(), i64::MIN + 1, -1_999_999_999, i64::MIN , -999_999_999), + (line!(), i64::MIN + 2, -1_999_999_999, i64::MIN + 1, -999_999_999), + (line!(), i64::MIN , -2_000_000_000, i64::MIN , -999_999_999), + (line!(), i64::MIN + 1, -2_000_000_000, i64::MIN , -999_999_999), + (line!(), i64::MIN + 2, -2_000_000_000, i64::MIN , 0), + (line!(), i64::MIN , -999_999_998, i64::MIN , -999_999_998), + (line!(), i64::MIN + 1, -999_999_998, i64::MIN + 1, -999_999_998), + (line!(), i64::MAX , 0, i64::MAX , 0), + (line!(), i64::MAX - 1, 0, i64::MAX - 1, 0), + (line!(), i64::MAX , -1, i64::MAX - 1, 999_999_999), + (line!(), i64::MAX , 1_000_000_000, i64::MAX , 999_999_999), + (line!(), i64::MAX - 1, 1_000_000_000, i64::MAX , 0), + (line!(), i64::MAX - 2, 1_000_000_000, i64::MAX - 1, 0), + (line!(), i64::MAX , 1_999_999_998, i64::MAX , 999_999_999), + (line!(), i64::MAX - 1, 1_999_999_998, i64::MAX , 999_999_998), + (line!(), i64::MAX - 2, 1_999_999_998, i64::MAX - 1, 999_999_998), + (line!(), i64::MAX , 1_999_999_999, i64::MAX , 999_999_999), + (line!(), i64::MAX - 1, 1_999_999_999, i64::MAX , 999_999_999), + (line!(), i64::MAX - 2, 1_999_999_999, i64::MAX - 1, 999_999_999), + (line!(), i64::MAX , 2_000_000_000, i64::MAX , 999_999_999), + (line!(), i64::MAX - 1, 2_000_000_000, i64::MAX , 999_999_999), + (line!(), i64::MAX - 2, 2_000_000_000, i64::MAX , 0), + (line!(), i64::MAX , 999_999_998, i64::MAX , 999_999_998), + (line!(), i64::MAX - 1, 999_999_998, i64::MAX - 1, 999_999_998), + ]; + + for case in cases.iter() { + let mut test_duration = Duration { + seconds: case.1, + nanos: case.2, + }; + test_duration.normalize(); + + assert_eq!( + test_duration, + Duration { + seconds: case.3, + nanos: case.4, + }, + "test case on line {} doesn't match", + case.0, + ); + } + } +} diff --git a/prost-types/src/lib.rs b/prost-types/src/lib.rs index 381e16e73..33458c78c 100644 --- a/prost-types/src/lib.rs +++ b/prost-types/src/lib.rs @@ -50,365 +50,3 @@ pub use timestamp::TimestampError; mod type_url; pub(crate) use type_url::{type_url_for, TypeUrl}; - -#[cfg(test)] -mod tests { - use super::*; - - #[cfg(feature = "std")] - use proptest::prelude::*; - #[cfg(feature = "std")] - use std::time::{self, SystemTime, UNIX_EPOCH}; - - #[cfg(feature = "std")] - proptest! { - #[test] - fn check_system_time_roundtrip( - system_time in SystemTime::arbitrary(), - ) { - prop_assert_eq!(SystemTime::try_from(Timestamp::from(system_time)).unwrap(), system_time); - } - - #[test] - fn check_timestamp_roundtrip_via_system_time( - seconds in i64::arbitrary(), - nanos in i32::arbitrary(), - ) { - let mut timestamp = Timestamp { seconds, nanos }; - timestamp.normalize(); - if let Ok(system_time) = SystemTime::try_from(timestamp.clone()) { - prop_assert_eq!(Timestamp::from(system_time), timestamp); - } - } - - #[test] - fn check_duration_roundtrip( - seconds in u64::arbitrary(), - nanos in 0u32..1_000_000_000u32, - ) { - let std_duration = time::Duration::new(seconds, nanos); - let prost_duration = match Duration::try_from(std_duration) { - Ok(duration) => duration, - Err(_) => return Err(TestCaseError::reject("duration out of range")), - }; - prop_assert_eq!(time::Duration::try_from(prost_duration.clone()).unwrap(), std_duration); - - if std_duration != time::Duration::default() { - let neg_prost_duration = Duration { - seconds: -prost_duration.seconds, - nanos: -prost_duration.nanos, - }; - - prop_assert!( - matches!( - time::Duration::try_from(neg_prost_duration), - Err(DurationError::NegativeDuration(d)) if d == std_duration, - ) - ) - } - } - - #[test] - fn check_duration_roundtrip_nanos( - nanos in u32::arbitrary(), - ) { - let seconds = 0; - let std_duration = std::time::Duration::new(seconds, nanos); - let prost_duration = match Duration::try_from(std_duration) { - Ok(duration) => duration, - Err(_) => return Err(TestCaseError::reject("duration out of range")), - }; - prop_assert_eq!(time::Duration::try_from(prost_duration.clone()).unwrap(), std_duration); - - if std_duration != time::Duration::default() { - let neg_prost_duration = Duration { - seconds: -prost_duration.seconds, - nanos: -prost_duration.nanos, - }; - - prop_assert!( - matches!( - time::Duration::try_from(neg_prost_duration), - Err(DurationError::NegativeDuration(d)) if d == std_duration, - ) - ) - } - } - } - - #[cfg(feature = "std")] - #[test] - fn check_duration_try_from_negative_nanos() { - let seconds: u64 = 0; - let nanos: u32 = 1; - let std_duration = std::time::Duration::new(seconds, nanos); - - let neg_prost_duration = Duration { - seconds: 0, - nanos: -1, - }; - - assert!(matches!( - time::Duration::try_from(neg_prost_duration), - Err(DurationError::NegativeDuration(d)) if d == std_duration, - )) - } - - #[cfg(feature = "std")] - #[test] - fn check_timestamp_negative_seconds() { - // Representative tests for the case of timestamps before the UTC Epoch time: - // validate the expected behaviour that "negative second values with fractions - // must still have non-negative nanos values that count forward in time" - // https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Timestamp - // - // To ensure cross-platform compatibility, all nanosecond values in these - // tests are in minimum 100 ns increments. This does not affect the general - // character of the behaviour being tested, but ensures that the tests are - // valid for both POSIX (1 ns precision) and Windows (100 ns precision). - assert_eq!( - Timestamp::from(UNIX_EPOCH - time::Duration::new(1_001, 0)), - Timestamp { - seconds: -1_001, - nanos: 0 - } - ); - assert_eq!( - Timestamp::from(UNIX_EPOCH - time::Duration::new(0, 999_999_900)), - Timestamp { - seconds: -1, - nanos: 100 - } - ); - assert_eq!( - Timestamp::from(UNIX_EPOCH - time::Duration::new(2_001_234, 12_300)), - Timestamp { - seconds: -2_001_235, - nanos: 999_987_700 - } - ); - assert_eq!( - Timestamp::from(UNIX_EPOCH - time::Duration::new(768, 65_432_100)), - Timestamp { - seconds: -769, - nanos: 934_567_900 - } - ); - } - - #[cfg(all(unix, feature = "std"))] - #[test] - fn check_timestamp_negative_seconds_1ns() { - // UNIX-only test cases with 1 ns precision - assert_eq!( - Timestamp::from(UNIX_EPOCH - time::Duration::new(0, 999_999_999)), - Timestamp { - seconds: -1, - nanos: 1 - } - ); - assert_eq!( - Timestamp::from(UNIX_EPOCH - time::Duration::new(1_234_567, 123)), - Timestamp { - seconds: -1_234_568, - nanos: 999_999_877 - } - ); - assert_eq!( - Timestamp::from(UNIX_EPOCH - time::Duration::new(890, 987_654_321)), - Timestamp { - seconds: -891, - nanos: 12_345_679 - } - ); - } - - #[test] - fn check_duration_normalize() { - #[rustfmt::skip] // Don't mangle the table formatting. - let cases = [ - // --- Table of test cases --- - // test seconds test nanos expected seconds expected nanos - (line!(), 0, 0, 0, 0), - (line!(), 1, 1, 1, 1), - (line!(), -1, -1, -1, -1), - (line!(), 0, 999_999_999, 0, 999_999_999), - (line!(), 0, -999_999_999, 0, -999_999_999), - (line!(), 0, 1_000_000_000, 1, 0), - (line!(), 0, -1_000_000_000, -1, 0), - (line!(), 0, 1_000_000_001, 1, 1), - (line!(), 0, -1_000_000_001, -1, -1), - (line!(), -1, 1, 0, -999_999_999), - (line!(), 1, -1, 0, 999_999_999), - (line!(), -1, 1_000_000_000, 0, 0), - (line!(), 1, -1_000_000_000, 0, 0), - (line!(), i64::MIN , 0, i64::MIN , 0), - (line!(), i64::MIN + 1, 0, i64::MIN + 1, 0), - (line!(), i64::MIN , 1, i64::MIN + 1, -999_999_999), - (line!(), i64::MIN , 1_000_000_000, i64::MIN + 1, 0), - (line!(), i64::MIN , -1_000_000_000, i64::MIN , -999_999_999), - (line!(), i64::MIN + 1, -1_000_000_000, i64::MIN , 0), - (line!(), i64::MIN + 2, -1_000_000_000, i64::MIN + 1, 0), - (line!(), i64::MIN , -1_999_999_998, i64::MIN , -999_999_999), - (line!(), i64::MIN + 1, -1_999_999_998, i64::MIN , -999_999_998), - (line!(), i64::MIN + 2, -1_999_999_998, i64::MIN + 1, -999_999_998), - (line!(), i64::MIN , -1_999_999_999, i64::MIN , -999_999_999), - (line!(), i64::MIN + 1, -1_999_999_999, i64::MIN , -999_999_999), - (line!(), i64::MIN + 2, -1_999_999_999, i64::MIN + 1, -999_999_999), - (line!(), i64::MIN , -2_000_000_000, i64::MIN , -999_999_999), - (line!(), i64::MIN + 1, -2_000_000_000, i64::MIN , -999_999_999), - (line!(), i64::MIN + 2, -2_000_000_000, i64::MIN , 0), - (line!(), i64::MIN , -999_999_998, i64::MIN , -999_999_998), - (line!(), i64::MIN + 1, -999_999_998, i64::MIN + 1, -999_999_998), - (line!(), i64::MAX , 0, i64::MAX , 0), - (line!(), i64::MAX - 1, 0, i64::MAX - 1, 0), - (line!(), i64::MAX , -1, i64::MAX - 1, 999_999_999), - (line!(), i64::MAX , 1_000_000_000, i64::MAX , 999_999_999), - (line!(), i64::MAX - 1, 1_000_000_000, i64::MAX , 0), - (line!(), i64::MAX - 2, 1_000_000_000, i64::MAX - 1, 0), - (line!(), i64::MAX , 1_999_999_998, i64::MAX , 999_999_999), - (line!(), i64::MAX - 1, 1_999_999_998, i64::MAX , 999_999_998), - (line!(), i64::MAX - 2, 1_999_999_998, i64::MAX - 1, 999_999_998), - (line!(), i64::MAX , 1_999_999_999, i64::MAX , 999_999_999), - (line!(), i64::MAX - 1, 1_999_999_999, i64::MAX , 999_999_999), - (line!(), i64::MAX - 2, 1_999_999_999, i64::MAX - 1, 999_999_999), - (line!(), i64::MAX , 2_000_000_000, i64::MAX , 999_999_999), - (line!(), i64::MAX - 1, 2_000_000_000, i64::MAX , 999_999_999), - (line!(), i64::MAX - 2, 2_000_000_000, i64::MAX , 0), - (line!(), i64::MAX , 999_999_998, i64::MAX , 999_999_998), - (line!(), i64::MAX - 1, 999_999_998, i64::MAX - 1, 999_999_998), - ]; - - for case in cases.iter() { - let mut test_duration = Duration { - seconds: case.1, - nanos: case.2, - }; - test_duration.normalize(); - - assert_eq!( - test_duration, - Duration { - seconds: case.3, - nanos: case.4, - }, - "test case on line {} doesn't match", - case.0, - ); - } - } - - #[cfg(feature = "std")] - #[test] - fn check_timestamp_normalize() { - // Make sure that `Timestamp::normalize` behaves correctly on and near overflow. - #[rustfmt::skip] // Don't mangle the table formatting. - let cases = [ - // --- Table of test cases --- - // test seconds test nanos expected seconds expected nanos - (line!(), 0, 0, 0, 0), - (line!(), 1, 1, 1, 1), - (line!(), -1, -1, -2, 999_999_999), - (line!(), 0, 999_999_999, 0, 999_999_999), - (line!(), 0, -999_999_999, -1, 1), - (line!(), 0, 1_000_000_000, 1, 0), - (line!(), 0, -1_000_000_000, -1, 0), - (line!(), 0, 1_000_000_001, 1, 1), - (line!(), 0, -1_000_000_001, -2, 999_999_999), - (line!(), -1, 1, -1, 1), - (line!(), 1, -1, 0, 999_999_999), - (line!(), -1, 1_000_000_000, 0, 0), - (line!(), 1, -1_000_000_000, 0, 0), - (line!(), i64::MIN , 0, i64::MIN , 0), - (line!(), i64::MIN + 1, 0, i64::MIN + 1, 0), - (line!(), i64::MIN , 1, i64::MIN , 1), - (line!(), i64::MIN , 1_000_000_000, i64::MIN + 1, 0), - (line!(), i64::MIN , -1_000_000_000, i64::MIN , 0), - (line!(), i64::MIN + 1, -1_000_000_000, i64::MIN , 0), - (line!(), i64::MIN + 2, -1_000_000_000, i64::MIN + 1, 0), - (line!(), i64::MIN , -1_999_999_998, i64::MIN , 0), - (line!(), i64::MIN + 1, -1_999_999_998, i64::MIN , 0), - (line!(), i64::MIN + 2, -1_999_999_998, i64::MIN , 2), - (line!(), i64::MIN , -1_999_999_999, i64::MIN , 0), - (line!(), i64::MIN + 1, -1_999_999_999, i64::MIN , 0), - (line!(), i64::MIN + 2, -1_999_999_999, i64::MIN , 1), - (line!(), i64::MIN , -2_000_000_000, i64::MIN , 0), - (line!(), i64::MIN + 1, -2_000_000_000, i64::MIN , 0), - (line!(), i64::MIN + 2, -2_000_000_000, i64::MIN , 0), - (line!(), i64::MIN , -999_999_998, i64::MIN , 0), - (line!(), i64::MIN + 1, -999_999_998, i64::MIN , 2), - (line!(), i64::MAX , 0, i64::MAX , 0), - (line!(), i64::MAX - 1, 0, i64::MAX - 1, 0), - (line!(), i64::MAX , -1, i64::MAX - 1, 999_999_999), - (line!(), i64::MAX , 1_000_000_000, i64::MAX , 999_999_999), - (line!(), i64::MAX - 1, 1_000_000_000, i64::MAX , 0), - (line!(), i64::MAX - 2, 1_000_000_000, i64::MAX - 1, 0), - (line!(), i64::MAX , 1_999_999_998, i64::MAX , 999_999_999), - (line!(), i64::MAX - 1, 1_999_999_998, i64::MAX , 999_999_998), - (line!(), i64::MAX - 2, 1_999_999_998, i64::MAX - 1, 999_999_998), - (line!(), i64::MAX , 1_999_999_999, i64::MAX , 999_999_999), - (line!(), i64::MAX - 1, 1_999_999_999, i64::MAX , 999_999_999), - (line!(), i64::MAX - 2, 1_999_999_999, i64::MAX - 1, 999_999_999), - (line!(), i64::MAX , 2_000_000_000, i64::MAX , 999_999_999), - (line!(), i64::MAX - 1, 2_000_000_000, i64::MAX , 999_999_999), - (line!(), i64::MAX - 2, 2_000_000_000, i64::MAX , 0), - (line!(), i64::MAX , 999_999_998, i64::MAX , 999_999_998), - (line!(), i64::MAX - 1, 999_999_998, i64::MAX - 1, 999_999_998), - ]; - - for case in cases.iter() { - let mut test_timestamp = crate::Timestamp { - seconds: case.1, - nanos: case.2, - }; - test_timestamp.normalize(); - - assert_eq!( - test_timestamp, - crate::Timestamp { - seconds: case.3, - nanos: case.4, - }, - "test case on line {} doesn't match", - case.0, - ); - } - } - - #[test] - fn check_any_serialization() { - let message = Timestamp::date(2000, 01, 01).unwrap(); - let any = Any::from_msg(&message).unwrap(); - assert_eq!( - &any.type_url, - "type.googleapis.com/google.protobuf.Timestamp" - ); - - let message2 = any.to_msg::().unwrap(); - assert_eq!(message, message2); - - // Wrong type URL - assert!(any.to_msg::().is_err()); - } - - #[test] - fn check_type_url_parsing() { - let example_type_name = "google.protobuf.Duration"; - - let url = TypeUrl::new("type.googleapis.com/google.protobuf.Duration").unwrap(); - assert_eq!(url.full_name, example_type_name); - - let full_url = - TypeUrl::new("https://type.googleapis.com/google.protobuf.Duration").unwrap(); - assert_eq!(full_url.full_name, example_type_name); - - let relative_url = TypeUrl::new("/google.protobuf.Duration").unwrap(); - assert_eq!(relative_url.full_name, example_type_name); - - // The name should be in a canonical form (e.g., leading "." is not accepted). - assert_eq!(TypeUrl::new("/.google.protobuf.Duration"), None); - - // Must contain at least one "/" character. - assert_eq!(TypeUrl::new("google.protobuf.Duration"), None); - } -} diff --git a/prost-types/src/timestamp.rs b/prost-types/src/timestamp.rs index 774f54e66..d8e69a9f8 100644 --- a/prost-types/src/timestamp.rs +++ b/prost-types/src/timestamp.rs @@ -238,3 +238,180 @@ impl fmt::Display for Timestamp { datetime::DateTime::from(self.clone()).fmt(f) } } +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(feature = "std")] + use proptest::prelude::*; + #[cfg(feature = "std")] + use std::time::{self, SystemTime, UNIX_EPOCH}; + + #[cfg(feature = "std")] + proptest! { + #[test] + fn check_system_time_roundtrip( + system_time in SystemTime::arbitrary(), + ) { + prop_assert_eq!(SystemTime::try_from(Timestamp::from(system_time)).unwrap(), system_time); + } + + #[test] + fn check_timestamp_roundtrip_via_system_time( + seconds in i64::arbitrary(), + nanos in i32::arbitrary(), + ) { + let mut timestamp = Timestamp { seconds, nanos }; + timestamp.normalize(); + if let Ok(system_time) = SystemTime::try_from(timestamp.clone()) { + prop_assert_eq!(Timestamp::from(system_time), timestamp); + } + } + } + + #[cfg(feature = "std")] + #[test] + fn check_timestamp_negative_seconds() { + // Representative tests for the case of timestamps before the UTC Epoch time: + // validate the expected behaviour that "negative second values with fractions + // must still have non-negative nanos values that count forward in time" + // https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Timestamp + // + // To ensure cross-platform compatibility, all nanosecond values in these + // tests are in minimum 100 ns increments. This does not affect the general + // character of the behaviour being tested, but ensures that the tests are + // valid for both POSIX (1 ns precision) and Windows (100 ns precision). + assert_eq!( + Timestamp::from(UNIX_EPOCH - time::Duration::new(1_001, 0)), + Timestamp { + seconds: -1_001, + nanos: 0 + } + ); + assert_eq!( + Timestamp::from(UNIX_EPOCH - time::Duration::new(0, 999_999_900)), + Timestamp { + seconds: -1, + nanos: 100 + } + ); + assert_eq!( + Timestamp::from(UNIX_EPOCH - time::Duration::new(2_001_234, 12_300)), + Timestamp { + seconds: -2_001_235, + nanos: 999_987_700 + } + ); + assert_eq!( + Timestamp::from(UNIX_EPOCH - time::Duration::new(768, 65_432_100)), + Timestamp { + seconds: -769, + nanos: 934_567_900 + } + ); + } + + #[cfg(all(unix, feature = "std"))] + #[test] + fn check_timestamp_negative_seconds_1ns() { + // UNIX-only test cases with 1 ns precision + assert_eq!( + Timestamp::from(UNIX_EPOCH - time::Duration::new(0, 999_999_999)), + Timestamp { + seconds: -1, + nanos: 1 + } + ); + assert_eq!( + Timestamp::from(UNIX_EPOCH - time::Duration::new(1_234_567, 123)), + Timestamp { + seconds: -1_234_568, + nanos: 999_999_877 + } + ); + assert_eq!( + Timestamp::from(UNIX_EPOCH - time::Duration::new(890, 987_654_321)), + Timestamp { + seconds: -891, + nanos: 12_345_679 + } + ); + } + + #[cfg(feature = "std")] + #[test] + fn check_timestamp_normalize() { + // Make sure that `Timestamp::normalize` behaves correctly on and near overflow. + #[rustfmt::skip] // Don't mangle the table formatting. + let cases = [ + // --- Table of test cases --- + // test seconds test nanos expected seconds expected nanos + (line!(), 0, 0, 0, 0), + (line!(), 1, 1, 1, 1), + (line!(), -1, -1, -2, 999_999_999), + (line!(), 0, 999_999_999, 0, 999_999_999), + (line!(), 0, -999_999_999, -1, 1), + (line!(), 0, 1_000_000_000, 1, 0), + (line!(), 0, -1_000_000_000, -1, 0), + (line!(), 0, 1_000_000_001, 1, 1), + (line!(), 0, -1_000_000_001, -2, 999_999_999), + (line!(), -1, 1, -1, 1), + (line!(), 1, -1, 0, 999_999_999), + (line!(), -1, 1_000_000_000, 0, 0), + (line!(), 1, -1_000_000_000, 0, 0), + (line!(), i64::MIN , 0, i64::MIN , 0), + (line!(), i64::MIN + 1, 0, i64::MIN + 1, 0), + (line!(), i64::MIN , 1, i64::MIN , 1), + (line!(), i64::MIN , 1_000_000_000, i64::MIN + 1, 0), + (line!(), i64::MIN , -1_000_000_000, i64::MIN , 0), + (line!(), i64::MIN + 1, -1_000_000_000, i64::MIN , 0), + (line!(), i64::MIN + 2, -1_000_000_000, i64::MIN + 1, 0), + (line!(), i64::MIN , -1_999_999_998, i64::MIN , 0), + (line!(), i64::MIN + 1, -1_999_999_998, i64::MIN , 0), + (line!(), i64::MIN + 2, -1_999_999_998, i64::MIN , 2), + (line!(), i64::MIN , -1_999_999_999, i64::MIN , 0), + (line!(), i64::MIN + 1, -1_999_999_999, i64::MIN , 0), + (line!(), i64::MIN + 2, -1_999_999_999, i64::MIN , 1), + (line!(), i64::MIN , -2_000_000_000, i64::MIN , 0), + (line!(), i64::MIN + 1, -2_000_000_000, i64::MIN , 0), + (line!(), i64::MIN + 2, -2_000_000_000, i64::MIN , 0), + (line!(), i64::MIN , -999_999_998, i64::MIN , 0), + (line!(), i64::MIN + 1, -999_999_998, i64::MIN , 2), + (line!(), i64::MAX , 0, i64::MAX , 0), + (line!(), i64::MAX - 1, 0, i64::MAX - 1, 0), + (line!(), i64::MAX , -1, i64::MAX - 1, 999_999_999), + (line!(), i64::MAX , 1_000_000_000, i64::MAX , 999_999_999), + (line!(), i64::MAX - 1, 1_000_000_000, i64::MAX , 0), + (line!(), i64::MAX - 2, 1_000_000_000, i64::MAX - 1, 0), + (line!(), i64::MAX , 1_999_999_998, i64::MAX , 999_999_999), + (line!(), i64::MAX - 1, 1_999_999_998, i64::MAX , 999_999_998), + (line!(), i64::MAX - 2, 1_999_999_998, i64::MAX - 1, 999_999_998), + (line!(), i64::MAX , 1_999_999_999, i64::MAX , 999_999_999), + (line!(), i64::MAX - 1, 1_999_999_999, i64::MAX , 999_999_999), + (line!(), i64::MAX - 2, 1_999_999_999, i64::MAX - 1, 999_999_999), + (line!(), i64::MAX , 2_000_000_000, i64::MAX , 999_999_999), + (line!(), i64::MAX - 1, 2_000_000_000, i64::MAX , 999_999_999), + (line!(), i64::MAX - 2, 2_000_000_000, i64::MAX , 0), + (line!(), i64::MAX , 999_999_998, i64::MAX , 999_999_998), + (line!(), i64::MAX - 1, 999_999_998, i64::MAX - 1, 999_999_998), + ]; + + for case in cases.iter() { + let mut test_timestamp = crate::Timestamp { + seconds: case.1, + nanos: case.2, + }; + test_timestamp.normalize(); + + assert_eq!( + test_timestamp, + crate::Timestamp { + seconds: case.3, + nanos: case.4, + }, + "test case on line {} doesn't match", + case.0, + ); + } + } +} diff --git a/prost-types/src/type_url.rs b/prost-types/src/type_url.rs index 56ba1ce75..01597554f 100644 --- a/prost-types/src/type_url.rs +++ b/prost-types/src/type_url.rs @@ -42,3 +42,29 @@ impl<'a> TypeUrl<'a> { pub(crate) fn type_url_for() -> String { format!("type.googleapis.com/{}.{}", T::PACKAGE, T::NAME) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_type_url_parsing() { + let example_type_name = "google.protobuf.Duration"; + + let url = TypeUrl::new("type.googleapis.com/google.protobuf.Duration").unwrap(); + assert_eq!(url.full_name, example_type_name); + + let full_url = + TypeUrl::new("https://type.googleapis.com/google.protobuf.Duration").unwrap(); + assert_eq!(full_url.full_name, example_type_name); + + let relative_url = TypeUrl::new("/google.protobuf.Duration").unwrap(); + assert_eq!(relative_url.full_name, example_type_name); + + // The name should be in a canonical form (e.g., leading "." is not accepted). + assert_eq!(TypeUrl::new("/.google.protobuf.Duration"), None); + + // Must contain at least one "/" character. + assert_eq!(TypeUrl::new("google.protobuf.Duration"), None); + } +}