From f861448dddc5c94402499bfd4df4cbb254d554e7 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 30 Jul 2024 09:29:26 +0200 Subject: [PATCH 1/9] proto: Expose well-known types `Any`, `Duration` and `Timestamp` --- proto/src/google/mod.rs | 1 + proto/src/google/protobuf/any.rs | 147 +++++++++++++++++++++++++ proto/src/google/protobuf/duration.rs | 38 +++++++ proto/src/google/protobuf/mod.rs | 12 ++ proto/src/google/protobuf/timestamp.rs | 47 ++++++++ proto/src/google/protobuf/type_url.rs | 72 ++++++++++++ proto/src/lib.rs | 9 +- proto/src/protobuf.rs | 59 ---------- tools/proto-compiler/src/main.rs | 6 +- 9 files changed, 322 insertions(+), 69 deletions(-) create mode 100644 proto/src/google/mod.rs create mode 100644 proto/src/google/protobuf/any.rs create mode 100644 proto/src/google/protobuf/duration.rs create mode 100644 proto/src/google/protobuf/mod.rs create mode 100644 proto/src/google/protobuf/timestamp.rs create mode 100644 proto/src/google/protobuf/type_url.rs delete mode 100644 proto/src/protobuf.rs diff --git a/proto/src/google/mod.rs b/proto/src/google/mod.rs new file mode 100644 index 000000000..91e41667b --- /dev/null +++ b/proto/src/google/mod.rs @@ -0,0 +1 @@ +pub mod protobuf; diff --git a/proto/src/google/protobuf/any.rs b/proto/src/google/protobuf/any.rs new file mode 100644 index 000000000..de6e59cf7 --- /dev/null +++ b/proto/src/google/protobuf/any.rs @@ -0,0 +1,147 @@ +use prost::{DecodeError, EncodeError, Message, Name}; + +use crate::prelude::*; + +use super::type_url::{type_url_for, TypeUrl}; +use super::PACKAGE; + +/// `Any` contains an arbitrary serialized protocol buffer message along with a +/// URL that describes the type of the serialized message. +/// +/// Protobuf library provides support to pack/unpack Any values in the form +/// of utility functions or additional generated methods of the Any type. +/// +/// # Example +/// +/// Pack and unpack a message in Rust: +/// +/// ```rust,ignore +/// let foo1 = Foo { ... }; +/// let any = Any::from_msg(&foo1)?; +/// let foo2 = any.to_msg::()?; +/// assert_eq!(foo1, foo2); +/// ``` +/// +/// The pack methods provided by protobuf library will by default use +/// 'type.googleapis.com/full.type.name' as the type URL and the unpack +/// methods only use the fully qualified type name after the last '/' +/// in the type URL, for example "foo.bar.com/x/y.z" will yield type +/// name "y.z". +/// +/// # JSON +/// +/// > JSON serialization of Any cannot be made compatible with the specification, +/// > and is therefore left unimplemented at the moment. +/// > See for more information. +/// +/// The JSON representation of an `Any` value uses the regular +/// representation of the deserialized, embedded message, with an +/// additional field `@type` which contains the type URL. Example: +/// +/// ```text +/// package google.profile; +/// message Person { +/// string first_name = 1; +/// string last_name = 2; +/// } +/// +/// { +/// "@type": "type.googleapis.com/google.profile.Person", +/// "firstName": , +/// "lastName": +/// } +/// ``` +/// +/// If the embedded message type is well-known and has a custom JSON +/// representation, that representation will be embedded adding a field +/// `value` which holds the custom JSON in addition to the `@type` +/// field. Example (for message \[google.protobuf.Duration\]\[\]): +/// +/// ```text +/// { +/// "@type": "type.googleapis.com/google.protobuf.Duration", +/// "value": "1.212s" +/// } +/// ``` +#[derive(Clone, PartialEq, Eq, ::prost::Message)] +pub struct Any { + /// A URL/resource name that uniquely identifies the type of the serialized + /// protocol buffer message. 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). + /// + /// In practice, teams usually precompile into the binary all types that they + /// expect it to use in the context of Any. However, for URLs which use the + /// scheme `http`, `https`, or no scheme, one can optionally set up a type + /// server that maps type URLs to message definitions as follows: + /// + /// * If no scheme is provided, `https` is assumed. + /// * An HTTP GET on the URL must yield a \[google.protobuf.Type\]\[\] + /// value in binary format, or produce an error. + /// * Applications are allowed to cache lookup results based on the + /// URL, or have them precompiled into a binary to avoid any + /// lookup. Therefore, binary compatibility needs to be preserved + /// on changes to types. (Use versioned type names to manage + /// breaking changes.) + /// + /// Note: this functionality is not currently available in the official + /// protobuf release, and it is not used for type URLs beginning with + /// type.googleapis.com. + /// + /// Schemes other than `http`, `https` (or the empty scheme) might be + /// used with implementation specific semantics. + #[prost(string, tag = "1")] + pub type_url: ::prost::alloc::string::String, + /// Must be a valid serialized protocol buffer of the above specified type. + #[prost(bytes = "vec", tag = "2")] + pub value: ::prost::alloc::vec::Vec, +} + +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(); + + if let (Some(expected), Some(actual)) = ( + TypeUrl::new(&expected_type_url), + TypeUrl::new(&self.type_url), + ) { + if expected == actual { + return 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/proto/src/google/protobuf/duration.rs b/proto/src/google/protobuf/duration.rs new file mode 100644 index 000000000..e9c682fd9 --- /dev/null +++ b/proto/src/google/protobuf/duration.rs @@ -0,0 +1,38 @@ +use prost::Name; + +use crate::prelude::*; + +use super::type_url::type_url_for; +use super::PACKAGE; + +/// A Duration represents a signed, fixed-length span of time represented +/// as a count of seconds and fractions of seconds at nanosecond +/// resolution. It is independent of any calendar and concepts like "day" +/// or "month". It is related to Timestamp in that the difference between +/// two Timestamp values is a Duration and it can be added or subtracted +/// from a Timestamp. Range is approximately +-10,000 years. +#[derive(Copy, Clone, PartialEq, ::prost::Message, ::serde::Deserialize, ::serde::Serialize)] +pub struct Duration { + /// Signed seconds of the span of time. Must be from -315,576,000,000 + /// to +315,576,000,000 inclusive. Note: these bounds are computed from: + /// 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years + #[prost(int64, tag = "1")] + pub seconds: i64, + /// Signed fractions of a second at nanosecond resolution of the span + /// of time. Durations less than one second are represented with a 0 + /// `seconds` field and a positive or negative `nanos` field. For durations + /// of one second or more, a non-zero value for the `nanos` field must be + /// of the same sign as the `seconds` field. Must be from -999,999,999 + /// to +999,999,999 inclusive. + #[prost(int32, tag = "2")] + pub nanos: i32, +} + +impl Name for Duration { + const PACKAGE: &'static str = PACKAGE; + const NAME: &'static str = "Duration"; + + fn type_url() -> String { + type_url_for::() + } +} diff --git a/proto/src/google/protobuf/mod.rs b/proto/src/google/protobuf/mod.rs new file mode 100644 index 000000000..92710470a --- /dev/null +++ b/proto/src/google/protobuf/mod.rs @@ -0,0 +1,12 @@ +pub const PACKAGE: &str = "google.protobuf"; + +mod any; +pub use any::Any; + +mod duration; +pub use duration::Duration; + +mod timestamp; +pub use timestamp::Timestamp; + +mod type_url; diff --git a/proto/src/google/protobuf/timestamp.rs b/proto/src/google/protobuf/timestamp.rs new file mode 100644 index 000000000..9215d7b76 --- /dev/null +++ b/proto/src/google/protobuf/timestamp.rs @@ -0,0 +1,47 @@ +use prost::Name; + +use crate::prelude::*; + +use super::type_url::type_url_for; +use super::PACKAGE; + +/// A Timestamp represents a point in time independent of any time zone or local +/// calendar, encoded as a count of seconds and fractions of seconds at +/// nanosecond resolution. The count is relative to an epoch at UTC midnight on +/// January 1, 1970, in the proleptic Gregorian calendar which extends the +/// Gregorian calendar backwards to year one. +/// +/// All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap +/// second table is needed for interpretation, using a +/// [24-hour linear smear](https://developers.google.com/time/smear). +/// +/// The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By +/// restricting to that range, we ensure that we can convert to and from +/// [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. +#[derive(Copy, Clone, PartialEq, ::prost::Message, ::serde::Deserialize, ::serde::Serialize)] +#[serde( + from = "crate::serializers::timestamp::Rfc3339", + into = "crate::serializers::timestamp::Rfc3339" +)] +pub struct Timestamp { + /// Represents seconds of UTC time since Unix epoch + /// 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + /// 9999-12-31T23:59:59Z inclusive. + #[prost(int64, tag = "1")] + pub seconds: i64, + /// Non-negative fractions of a second at nanosecond resolution. Negative + /// second values with fractions must still have non-negative nanos values + /// that count forward in time. Must be from 0 to 999,999,999 + /// inclusive. + #[prost(int32, tag = "2")] + pub nanos: i32, +} + +impl Name for Timestamp { + const PACKAGE: &'static str = PACKAGE; + const NAME: &'static str = "Timestamp"; + + fn type_url() -> String { + type_url_for::() + } +} diff --git a/proto/src/google/protobuf/type_url.rs b/proto/src/google/protobuf/type_url.rs new file mode 100644 index 000000000..1b79bca26 --- /dev/null +++ b/proto/src/google/protobuf/type_url.rs @@ -0,0 +1,72 @@ +use prost::Name; + +use crate::prelude::*; + +/// 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 { + 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); + } +} diff --git a/proto/src/lib.rs b/proto/src/lib.rs index 54944fff9..86cb6750a 100644 --- a/proto/src/lib.rs +++ b/proto/src/lib.rs @@ -22,13 +22,8 @@ pub mod serializers; use prelude::*; -/// Built-in prost_types with slight customization to enable JSON-encoding -pub mod google { - pub mod protobuf { - // custom Timeout and Duration types that have valid doctest documentation texts - include!("protobuf.rs"); - } -} +/// Built-in `prost_types` with slight customization to enable JSON-encoding. +pub mod google; /// Allows for easy Google Protocol Buffers encoding and decoding of domain /// types with validation. diff --git a/proto/src/protobuf.rs b/proto/src/protobuf.rs deleted file mode 100644 index 7a587c209..000000000 --- a/proto/src/protobuf.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Google protobuf Timestamp and Duration types reimplemented because their comments are turned -// into invalid documentation texts and doctest chokes on them. See https://github.com/danburkert/prost/issues/374 -// Prost does not seem to have a way yet to remove documentations defined in protobuf files. -// These structs are defined in gogoproto v1.3.1 at https://github.com/gogo/protobuf/tree/v1.3.1/protobuf/google/protobuf - -/// A Timestamp represents a point in time independent of any time zone or local -/// calendar, encoded as a count of seconds and fractions of seconds at -/// nanosecond resolution. The count is relative to an epoch at UTC midnight on -/// January 1, 1970, in the proleptic Gregorian calendar which extends the -/// Gregorian calendar backwards to year one. -/// -/// All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap -/// second table is needed for interpretation, using a [24-hour linear -/// smear](https://developers.google.com/time/smear). -/// -/// The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By -/// restricting to that range, we ensure that we can convert to and from [RFC -/// 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. -#[derive(Copy, Clone, PartialEq, ::prost::Message, ::serde::Deserialize, ::serde::Serialize)] -#[serde( - from = "crate::serializers::timestamp::Rfc3339", - into = "crate::serializers::timestamp::Rfc3339" -)] -pub struct Timestamp { - /// Represents seconds of UTC time since Unix epoch - /// 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to - /// 9999-12-31T23:59:59Z inclusive. - #[prost(int64, tag = "1")] - pub seconds: i64, - /// Non-negative fractions of a second at nanosecond resolution. Negative - /// second values with fractions must still have non-negative nanos values - /// that count forward in time. Must be from 0 to 999,999,999 - /// inclusive. - #[prost(int32, tag = "2")] - pub nanos: i32, -} - -/// A Duration represents a signed, fixed-length span of time represented -/// as a count of seconds and fractions of seconds at nanosecond -/// resolution. It is independent of any calendar and concepts like "day" -/// or "month". It is related to Timestamp in that the difference between -/// two Timestamp values is a Duration and it can be added or subtracted -/// from a Timestamp. Range is approximately +-10,000 years. -#[derive(Copy, Clone, PartialEq, ::prost::Message, ::serde::Deserialize, ::serde::Serialize)] -pub struct Duration { - /// Signed seconds of the span of time. Must be from -315,576,000,000 - /// to +315,576,000,000 inclusive. Note: these bounds are computed from: - /// 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years - #[prost(int64, tag = "1")] - pub seconds: i64, - /// Signed fractions of a second at nanosecond resolution of the span - /// of time. Durations less than one second are represented with a 0 - /// `seconds` field and a positive or negative `nanos` field. For durations - /// of one second or more, a non-zero value for the `nanos` field must be - /// of the same sign as the `seconds` field. Must be from -999,999,999 - /// to +999,999,999 inclusive. - #[prost(int32, tag = "2")] - pub nanos: i32, -} diff --git a/tools/proto-compiler/src/main.rs b/tools/proto-compiler/src/main.rs index 29c7b4a5a..4d1439a14 100644 --- a/tools/proto-compiler/src/main.rs +++ b/tools/proto-compiler/src/main.rs @@ -96,9 +96,8 @@ fn main() { for field_attribute in CUSTOM_FIELD_ATTRIBUTES { pb.field_attribute(field_attribute.0, field_attribute.1); } - // The below in-place path redirection replaces references to the Duration - // and Timestamp WKTs with our own versions that have valid doctest comments. - // See also https://github.com/danburkert/prost/issues/374 . + // The below in-place path redirection replaces references to the + // Duration, Timestamp, and Any "well-know types" with our own versions. pb.extern_path( ".google.protobuf.Duration", "crate::google::protobuf::Duration", @@ -107,6 +106,7 @@ fn main() { ".google.protobuf.Timestamp", "crate::google::protobuf::Timestamp", ); + pb.extern_path(".google.protobuf.Any", "crate::google::protobuf::Any"); println!("[info] => Creating structs and interfaces."); let builder = tonic_build::configure() From 965b25554417f35127caae0246001ae969c47859 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 30 Jul 2024 10:59:19 +0200 Subject: [PATCH 2/9] Add proper JSON serialization for `Duration` --- proto/src/google/protobuf/any.rs | 3 + proto/src/google/protobuf/duration.rs | 205 ++++++++++++++++++++++++- proto/src/google/protobuf/timestamp.rs | 3 + 3 files changed, 210 insertions(+), 1 deletion(-) diff --git a/proto/src/google/protobuf/any.rs b/proto/src/google/protobuf/any.rs index de6e59cf7..67ab87cce 100644 --- a/proto/src/google/protobuf/any.rs +++ b/proto/src/google/protobuf/any.rs @@ -1,3 +1,6 @@ +// Original code from +// Copyright 2022 Dan Burkert & Tokio Contributors + use prost::{DecodeError, EncodeError, Message, Name}; use crate::prelude::*; diff --git a/proto/src/google/protobuf/duration.rs b/proto/src/google/protobuf/duration.rs index e9c682fd9..fab60bcca 100644 --- a/proto/src/google/protobuf/duration.rs +++ b/proto/src/google/protobuf/duration.rs @@ -1,3 +1,9 @@ +// Original code from +// Copyright 2022 Dan Burkert & Tokio Contributors +// +// Original serialization code from +// Copyright (c) 2020 InfluxData + use prost::Name; use crate::prelude::*; @@ -11,7 +17,7 @@ use super::PACKAGE; /// or "month". It is related to Timestamp in that the difference between /// two Timestamp values is a Duration and it can be added or subtracted /// from a Timestamp. Range is approximately +-10,000 years. -#[derive(Copy, Clone, PartialEq, ::prost::Message, ::serde::Deserialize, ::serde::Serialize)] +#[derive(Copy, Clone, PartialEq, ::prost::Message)] pub struct Duration { /// Signed seconds of the span of time. Must be from -315,576,000,000 /// to +315,576,000,000 inclusive. Note: these bounds are computed from: @@ -36,3 +42,200 @@ impl Name for Duration { type_url_for::() } } + +impl TryFrom for core::time::Duration { + type Error = core::num::TryFromIntError; + + fn try_from(value: Duration) -> Result { + Ok(Self::new( + value.seconds.try_into()?, + value.nanos.try_into()?, + )) + } +} + +impl From for Duration { + fn from(value: core::time::Duration) -> Self { + Self { + seconds: value.as_secs() as _, + nanos: value.subsec_nanos() as _, + } + } +} + +impl serde::Serialize for Duration { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if self.seconds != 0 && self.nanos != 0 && (self.nanos < 0) != (self.seconds < 0) { + return Err(serde::ser::Error::custom("Duration has inconsistent signs")); + } + + let mut s = if self.seconds == 0 { + if self.nanos < 0 { + "-0".to_string() + } else { + "0".to_string() + } + } else { + self.seconds.to_string() + }; + + if self.nanos != 0 { + s.push('.'); + let f = match split_nanos(self.nanos.unsigned_abs()) { + (millis, 0, 0) => format!("{:03}", millis), + (millis, micros, 0) => format!("{:03}{:03}", millis, micros), + (millis, micros, nanos) => format!("{:03}{:03}{:03}", millis, micros, nanos), + }; + s.push_str(&f); + } + + s.push('s'); + serializer.serialize_str(&s) + } +} + +struct DurationVisitor; + +impl<'de> serde::de::Visitor<'de> for DurationVisitor { + type Value = Duration; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("a duration string") + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + let s = s + .strip_suffix('s') + .ok_or_else(|| serde::de::Error::custom("missing 's' suffix"))?; + + let (negative, s) = match s.strip_prefix('-') { + Some(s) => (true, s), + None => (false, s), + }; + + let duration = match s.split_once('.') { + Some((seconds_str, decimal_str)) => { + let exp = 9_u32 + .checked_sub(decimal_str.len() as u32) + .ok_or_else(|| serde::de::Error::custom("too many decimal places"))?; + + let pow = 10_u32.pow(exp); + let seconds = seconds_str.parse().map_err(serde::de::Error::custom)?; + let decimal: u32 = decimal_str.parse().map_err(serde::de::Error::custom)?; + + Duration { + seconds, + nanos: (decimal * pow) as i32, + } + }, + None => Duration { + seconds: s.parse().map_err(serde::de::Error::custom)?, + nanos: 0, + }, + }; + + Ok(match negative { + true => Duration { + seconds: -duration.seconds, + nanos: -duration.nanos, + }, + false => duration, + }) + } +} + +impl<'de> serde::Deserialize<'de> for Duration { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(DurationVisitor) + } +} + +/// Splits nanoseconds into whole milliseconds, microseconds, and nanoseconds +fn split_nanos(mut nanos: u32) -> (u32, u32, u32) { + let millis = nanos / 1_000_000; + nanos -= millis * 1_000_000; + let micros = nanos / 1_000; + nanos -= micros * 1_000; + (millis, micros, nanos) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_duration() { + let verify = |duration: &Duration, expected: &str| { + assert_eq!(serde_json::to_string(duration).unwrap().as_str(), expected); + assert_eq!( + &serde_json::from_str::(expected).unwrap(), + duration + ) + }; + + let duration = Duration { + seconds: 0, + nanos: 0, + }; + verify(&duration, "\"0s\""); + + let duration = Duration { + seconds: 0, + nanos: 123, + }; + verify(&duration, "\"0.000000123s\""); + + let duration = Duration { + seconds: 0, + nanos: 123456, + }; + verify(&duration, "\"0.000123456s\""); + + let duration = Duration { + seconds: 0, + nanos: 123456789, + }; + verify(&duration, "\"0.123456789s\""); + + let duration = Duration { + seconds: 0, + nanos: -67088, + }; + verify(&duration, "\"-0.000067088s\""); + + let duration = Duration { + seconds: 121, + nanos: 3454, + }; + verify(&duration, "\"121.000003454s\""); + + let duration = Duration { + seconds: -90, + nanos: -2456301, + }; + verify(&duration, "\"-90.002456301s\""); + + let duration = Duration { + seconds: -90, + nanos: 234, + }; + serde_json::to_string(&duration).unwrap_err(); + + let duration = Duration { + seconds: 90, + nanos: -234, + }; + serde_json::to_string(&duration).unwrap_err(); + + serde_json::from_str::("90.1234567891s").unwrap_err(); + } +} diff --git a/proto/src/google/protobuf/timestamp.rs b/proto/src/google/protobuf/timestamp.rs index 9215d7b76..dc6fdcb70 100644 --- a/proto/src/google/protobuf/timestamp.rs +++ b/proto/src/google/protobuf/timestamp.rs @@ -1,3 +1,6 @@ +// Original code from +// Copyright 2022 Dan Burkert & Tokio Contributors + use prost::Name; use crate::prelude::*; From cd3b9f0a336c92f83cc5966fc503214db954d1f7 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 30 Jul 2024 11:19:45 +0200 Subject: [PATCH 3/9] Add non-compliant JSON serialization for `Any` --- proto/src/google/protobuf/any.rs | 153 +++++++++++++++++++++++++------ 1 file changed, 125 insertions(+), 28 deletions(-) diff --git a/proto/src/google/protobuf/any.rs b/proto/src/google/protobuf/any.rs index 67ab87cce..e82390b61 100644 --- a/proto/src/google/protobuf/any.rs +++ b/proto/src/google/protobuf/any.rs @@ -2,6 +2,7 @@ // Copyright 2022 Dan Burkert & Tokio Contributors use prost::{DecodeError, EncodeError, Message, Name}; +use subtle_encoding::base64; use crate::prelude::*; @@ -33,37 +34,18 @@ use super::PACKAGE; /// /// # JSON /// -/// > JSON serialization of Any cannot be made compatible with the specification, -/// > and is therefore left unimplemented at the moment. -/// > See for more information. +/// JSON serialization of Any cannot be made compatible with the specification. +/// See for more information. /// -/// The JSON representation of an `Any` value uses the regular -/// representation of the deserialized, embedded message, with an -/// additional field `@type` which contains the type URL. Example: +/// At the moment, an `Any` struct will be serialized as a JSON object with two fields: +/// - `typeUrl` (string): the type URL of the message +/// - `value` (string): the base64-encoded serialized message /// -/// ```text -/// package google.profile; -/// message Person { -/// string first_name = 1; -/// string last_name = 2; -/// } -/// -/// { -/// "@type": "type.googleapis.com/google.profile.Person", -/// "firstName": , -/// "lastName": -/// } -/// ``` -/// -/// If the embedded message type is well-known and has a custom JSON -/// representation, that representation will be embedded adding a field -/// `value` which holds the custom JSON in addition to the `@type` -/// field. Example (for message \[google.protobuf.Duration\]\[\]): -/// -/// ```text +/// For example: +/// ```json /// { -/// "@type": "type.googleapis.com/google.protobuf.Duration", -/// "value": "1.212s" +/// "typeUrl": "type.googleapis.com/google.protobuf.Duration", +/// "value": "Cg0KB2NvcnA=" /// } /// ``` #[derive(Clone, PartialEq, Eq, ::prost::Message)] @@ -148,3 +130,118 @@ impl Name for Any { type_url_for::() } } + +impl serde::Serialize for Any { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.type_url.is_empty() { + len += 1; + } + if !self.value.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("google.protobuf.Any", len)?; + if !self.type_url.is_empty() { + struct_ser.serialize_field("typeUrl", &self.type_url)?; + } + if !self.value.is_empty() { + // NOTE: A base64 string is always valid UTF-8. + struct_ser.serialize_field( + "value", + &String::from_utf8_lossy(&base64::encode(&self.value)), + )?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for Any { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &["type_url", "typeUrl", "value"]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + TypeUrl, + Value, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "typeUrl" | "type_url" => Ok(GeneratedField::TypeUrl), + "value" => Ok(GeneratedField::Value), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Any; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct google.protobuf.Any") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut type_url__ = None; + let mut value__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::TypeUrl => { + if type_url__.is_some() { + return Err(serde::de::Error::duplicate_field("typeUrl")); + } + type_url__ = Some(map_.next_value()?); + }, + GeneratedField::Value => { + if value__.is_some() { + return Err(serde::de::Error::duplicate_field("value")); + } + let b64_str = map_.next_value::()?; + let value = base64::decode(b64_str.as_bytes()).map_err(|e| { + serde::de::Error::custom(format!("base64 decode error: {e}")) + })?; + value__ = Some(value); + }, + } + } + Ok(Any { + type_url: type_url__.unwrap_or_default(), + value: value__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("google.protobuf.Any", FIELDS, GeneratedVisitor) + } +} From c18fc5fba6067ec9cac5a67ac5abe2da277e7996 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 30 Jul 2024 11:26:46 +0200 Subject: [PATCH 4/9] Fix no_std compatibility --- proto/src/google/protobuf/any.rs | 16 ++++++++-------- proto/src/google/protobuf/duration.rs | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/proto/src/google/protobuf/any.rs b/proto/src/google/protobuf/any.rs index e82390b61..7615be71f 100644 --- a/proto/src/google/protobuf/any.rs +++ b/proto/src/google/protobuf/any.rs @@ -132,7 +132,7 @@ impl Name for Any { } impl serde::Serialize for Any { - fn serialize(&self, serializer: S) -> std::result::Result + fn serialize(&self, serializer: S) -> core::result::Result where S: serde::Serializer, { @@ -159,7 +159,7 @@ impl serde::Serialize for Any { } } impl<'de> serde::Deserialize<'de> for Any { - fn deserialize(deserializer: D) -> std::result::Result + fn deserialize(deserializer: D) -> core::result::Result where D: serde::Deserializer<'de>, { @@ -171,7 +171,7 @@ impl<'de> serde::Deserialize<'de> for Any { Value, } impl<'de> serde::Deserialize<'de> for GeneratedField { - fn deserialize(deserializer: D) -> std::result::Result + fn deserialize(deserializer: D) -> core::result::Result where D: serde::Deserializer<'de>, { @@ -182,13 +182,13 @@ impl<'de> serde::Deserialize<'de> for Any { fn expecting( &self, - formatter: &mut std::fmt::Formatter<'_>, - ) -> std::fmt::Result { + formatter: &mut core::fmt::Formatter<'_>, + ) -> core::fmt::Result { write!(formatter, "expected one of: {:?}", &FIELDS) } #[allow(unused_variables)] - fn visit_str(self, value: &str) -> std::result::Result + fn visit_str(self, value: &str) -> core::result::Result where E: serde::de::Error, { @@ -206,11 +206,11 @@ impl<'de> serde::Deserialize<'de> for Any { impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { type Value = Any; - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { formatter.write_str("struct google.protobuf.Any") } - fn visit_map(self, mut map_: V) -> std::result::Result + fn visit_map(self, mut map_: V) -> core::result::Result where V: serde::de::MapAccess<'de>, { diff --git a/proto/src/google/protobuf/duration.rs b/proto/src/google/protobuf/duration.rs index fab60bcca..c343e61fe 100644 --- a/proto/src/google/protobuf/duration.rs +++ b/proto/src/google/protobuf/duration.rs @@ -102,7 +102,7 @@ struct DurationVisitor; impl<'de> serde::de::Visitor<'de> for DurationVisitor { type Value = Duration; - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { formatter.write_str("a duration string") } From ade3519879636d84ad531e09a1b4473fb32d87d2 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 31 Jul 2024 10:54:02 +0200 Subject: [PATCH 5/9] Add changelog entries --- .changelog/unreleased/features/1445-any-proto.md | 1 + .changelog/unreleased/improvements/1452-proto-name.md | 2 ++ .changelog/unreleased/improvements/1452-proto-serialization.md | 1 + 3 files changed, 4 insertions(+) create mode 100644 .changelog/unreleased/features/1445-any-proto.md create mode 100644 .changelog/unreleased/improvements/1452-proto-name.md create mode 100644 .changelog/unreleased/improvements/1452-proto-serialization.md diff --git a/.changelog/unreleased/features/1445-any-proto.md b/.changelog/unreleased/features/1445-any-proto.md new file mode 100644 index 000000000..6b3619e32 --- /dev/null +++ b/.changelog/unreleased/features/1445-any-proto.md @@ -0,0 +1 @@ +- `[tendermint-proto]` Add `Any` type under `tendermint_proto::google::protobuf::Any` ([#1445](https://github.com/informalsystems/tendermint-rs/issues/1445)) diff --git a/.changelog/unreleased/improvements/1452-proto-name.md b/.changelog/unreleased/improvements/1452-proto-name.md new file mode 100644 index 000000000..e0424cc3e --- /dev/null +++ b/.changelog/unreleased/improvements/1452-proto-name.md @@ -0,0 +1,2 @@ +- `[tendermint-proto]` Implement `prost::Name` for `tendermint_proto::google::protobuf::{Duration, Timestamp}` ([#1452](https://github.com/informalsystems/tendermint-rs/pull/1452/)) + diff --git a/.changelog/unreleased/improvements/1452-proto-serialization.md b/.changelog/unreleased/improvements/1452-proto-serialization.md new file mode 100644 index 000000000..5edf40ac9 --- /dev/null +++ b/.changelog/unreleased/improvements/1452-proto-serialization.md @@ -0,0 +1 @@ +- `[tendermint-proto]` Improve ProtoJSON serialization of `tendermint_proto::google::protobuf::{Duration, Timestamp}` ([#1452](https://github.com/informalsystems/tendermint-rs/pull/1452/)) From a63416cc6432c86ce9c6b125453d1d2da24a9fb1 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 31 Jul 2024 17:40:23 +0200 Subject: [PATCH 6/9] Add `json-schema` feature flag to enable derivation of `schemars::JsonSchema` on well-known types --- proto/Cargo.toml | 4 +++- proto/src/google/protobuf/any.rs | 1 + proto/src/google/protobuf/duration.rs | 3 ++- proto/src/google/protobuf/timestamp.rs | 5 ++++- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/proto/Cargo.toml b/proto/Cargo.toml index 94a11f7e7..b393415f1 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -16,7 +16,8 @@ description = """ [features] default = [] grpc = ["grpc-server"] -grpc-server = ["tonic"] +grpc-server = ["dep:tonic"] +json-schema = ["dep:schemars"] [package.metadata.docs.rs] all-features = true @@ -31,6 +32,7 @@ subtle-encoding = { version = "0.5", default-features = false, features = ["hex" time = { version = "0.3", default-features = false, features = ["macros", "parsing"] } flex-error = { version = "0.4.4", default-features = false } tonic = { version = "0.12", optional = true } +schemars = { version = "0.8", optional = true } [dev-dependencies] serde_json = { version = "1.0", default-features = false, features = ["alloc"] } diff --git a/proto/src/google/protobuf/any.rs b/proto/src/google/protobuf/any.rs index 7615be71f..5c8842d74 100644 --- a/proto/src/google/protobuf/any.rs +++ b/proto/src/google/protobuf/any.rs @@ -49,6 +49,7 @@ use super::PACKAGE; /// } /// ``` #[derive(Clone, PartialEq, Eq, ::prost::Message)] +#[cfg_attr(feature = "json-schema", derive(::schemars::JsonSchema))] pub struct Any { /// A URL/resource name that uniquely identifies the type of the serialized /// protocol buffer message. This string must contain at least diff --git a/proto/src/google/protobuf/duration.rs b/proto/src/google/protobuf/duration.rs index c343e61fe..825220dc4 100644 --- a/proto/src/google/protobuf/duration.rs +++ b/proto/src/google/protobuf/duration.rs @@ -17,7 +17,8 @@ use super::PACKAGE; /// or "month". It is related to Timestamp in that the difference between /// two Timestamp values is a Duration and it can be added or subtracted /// from a Timestamp. Range is approximately +-10,000 years. -#[derive(Copy, Clone, PartialEq, ::prost::Message)] +#[derive(Copy, Clone, PartialEq, Eq, ::prost::Message)] +#[cfg_attr(feature = "json-schema", derive(::schemars::JsonSchema))] pub struct Duration { /// Signed seconds of the span of time. Must be from -315,576,000,000 /// to +315,576,000,000 inclusive. Note: these bounds are computed from: diff --git a/proto/src/google/protobuf/timestamp.rs b/proto/src/google/protobuf/timestamp.rs index dc6fdcb70..9355c4563 100644 --- a/proto/src/google/protobuf/timestamp.rs +++ b/proto/src/google/protobuf/timestamp.rs @@ -21,11 +21,14 @@ use super::PACKAGE; /// The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By /// restricting to that range, we ensure that we can convert to and from /// [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. -#[derive(Copy, Clone, PartialEq, ::prost::Message, ::serde::Deserialize, ::serde::Serialize)] +#[derive( + Copy, Clone, PartialEq, Eq, ::prost::Message, ::serde::Deserialize, ::serde::Serialize, +)] #[serde( from = "crate::serializers::timestamp::Rfc3339", into = "crate::serializers::timestamp::Rfc3339" )] +#[cfg_attr(feature = "json-schema", derive(::schemars::JsonSchema))] pub struct Timestamp { /// Represents seconds of UTC time since Unix epoch /// 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to From ae69d1807fae3172c53ca0cd6ed35e2c4a646a4c Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 31 Jul 2024 17:54:23 +0200 Subject: [PATCH 7/9] Add conversion from and into `core::time::Duration` for `google::protobuf::Duration` --- proto/src/google/protobuf/duration.rs | 94 +++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 11 deletions(-) diff --git a/proto/src/google/protobuf/duration.rs b/proto/src/google/protobuf/duration.rs index 825220dc4..b684abc32 100644 --- a/proto/src/google/protobuf/duration.rs +++ b/proto/src/google/protobuf/duration.rs @@ -4,6 +4,8 @@ // Original serialization code from // Copyright (c) 2020 InfluxData +use core::convert::TryFrom; + use prost::Name; use crate::prelude::*; @@ -44,22 +46,92 @@ impl Name for Duration { } } -impl TryFrom for core::time::Duration { - type Error = core::num::TryFromIntError; +const NANOS_PER_SECOND: i32 = 1_000_000_000; +const NANOS_MAX: i32 = NANOS_PER_SECOND - 1; + +impl Duration { + /// Normalizes the duration to a canonical format. + 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; + } + } - fn try_from(value: Duration) -> Result { - Ok(Self::new( - value.seconds.try_into()?, - value.nanos.try_into()?, - )) + // 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; + } + } } } +/// Converts a `core::time::Duration` to a `Duration`. impl From for Duration { - fn from(value: core::time::Duration) -> Self { - Self { - seconds: value.as_secs() as _, - nanos: value.subsec_nanos() as _, + fn from(duration: core::time::Duration) -> Duration { + let seconds = duration.as_secs(); + let seconds = if seconds > i64::MAX as u64 { + i64::MAX + } else { + seconds as i64 + }; + let nanos = duration.subsec_nanos(); + let nanos = if nanos > i32::MAX as u32 { + i32::MAX + } else { + nanos as i32 + }; + let mut duration = Duration { seconds, nanos }; + duration.normalize(); + duration + } +} + +impl TryFrom for core::time::Duration { + type Error = core::time::Duration; + + /// Converts a `Duration` to a result containing a positive (`Ok`) or negative (`Err`) + /// `std::time::Duration`. + fn try_from(mut duration: Duration) -> Result { + duration.normalize(); + if duration.seconds >= 0 { + Ok(core::time::Duration::new( + duration.seconds as u64, + duration.nanos as u32, + )) + } else { + Err(core::time::Duration::new( + (-duration.seconds) as u64, + (-duration.nanos) as u32, + )) } } } From fa38ea865d4fc300f54962cdab6cf1ce45b99e55 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 31 Jul 2024 17:55:23 +0200 Subject: [PATCH 8/9] Add conversion from and into `std::time::SystemTime` for `google::protobuf::Timestamp`, feature-guarded by an `std` feature --- proto/src/google/protobuf/timestamp.rs | 132 ++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 3 deletions(-) diff --git a/proto/src/google/protobuf/timestamp.rs b/proto/src/google/protobuf/timestamp.rs index 9355c4563..879cea7d9 100644 --- a/proto/src/google/protobuf/timestamp.rs +++ b/proto/src/google/protobuf/timestamp.rs @@ -21,9 +21,7 @@ use super::PACKAGE; /// The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By /// restricting to that range, we ensure that we can convert to and from /// [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. -#[derive( - Copy, Clone, PartialEq, Eq, ::prost::Message, ::serde::Deserialize, ::serde::Serialize, -)] +#[derive(Copy, Clone, PartialEq, ::prost::Message, ::serde::Deserialize, ::serde::Serialize)] #[serde( from = "crate::serializers::timestamp::Rfc3339", into = "crate::serializers::timestamp::Rfc3339" @@ -51,3 +49,131 @@ impl Name for Timestamp { type_url_for::() } } + +const NANOS_PER_SECOND: i32 = 1_000_000_000; + +impl Timestamp { + /// Normalizes the timestamp to a canonical format. + 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; + } + } + } +} + +/// 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()`. +impl Eq for Timestamp {} + +// Derived logic is correct: comparing the 2 fields for equality +#[allow(clippy::derived_hash_with_manual_eq)] +impl core::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 } + } +} + +/// 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. +#[cfg(feature = "std")] +#[derive(Debug)] +#[non_exhaustive] +pub struct TimestampOutOfSystemRangeError { + pub timestamp: Timestamp, +} + +#[cfg(feature = "std")] +impl core::fmt::Display for TimestampOutOfSystemRangeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "{self:?} is not representable as a `SystemTime` because it is out of range" + ) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for TimestampOutOfSystemRangeError {} + +#[cfg(feature = "std")] +impl TryFrom for std::time::SystemTime { + type Error = TimestampOutOfSystemRangeError; + + fn try_from(mut timestamp: Timestamp) -> Result { + let orig_timestamp = timestamp; + + timestamp.normalize(); + + let system_time = if timestamp.seconds >= 0 { + std::time::UNIX_EPOCH + .checked_add(core::time::Duration::from_secs(timestamp.seconds as u64)) + } else { + std::time::UNIX_EPOCH + .checked_sub(core::time::Duration::from_secs((-timestamp.seconds) as u64)) + }; + + let system_time = system_time.and_then(|system_time| { + system_time.checked_add(core::time::Duration::from_nanos(timestamp.nanos as u64)) + }); + + system_time.ok_or(TimestampOutOfSystemRangeError { + timestamp: orig_timestamp, + }) + } +} From e0b13b191ce7b8425d270a12dde60bf33b9949cf Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 31 Jul 2024 17:55:37 +0200 Subject: [PATCH 9/9] Add `borsh` and `parity-scale-codec` features with corresponding derivations --- proto/Cargo.toml | 13 +++++ proto/src/google/protobuf/any.rs | 88 ++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/proto/Cargo.toml b/proto/Cargo.toml index b393415f1..107e32d54 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -15,9 +15,12 @@ description = """ [features] default = [] +std = [] grpc = ["grpc-server"] grpc-server = ["dep:tonic"] json-schema = ["dep:schemars"] +borsh = ["dep:borsh"] +parity-scale-codec = ["dep:parity-scale-codec", "dep:scale-info"] [package.metadata.docs.rs] all-features = true @@ -32,7 +35,17 @@ subtle-encoding = { version = "0.5", default-features = false, features = ["hex" time = { version = "0.3", default-features = false, features = ["macros", "parsing"] } flex-error = { version = "0.4.4", default-features = false } tonic = { version = "0.12", optional = true } + +## Optional: enabled by the `json-schema` feature schemars = { version = "0.8", optional = true } +## Optional: enabled by the `borsh` feature +## For borsh encode or decode, needs to track `anchor-lang` and `near-sdk-rs` borsh version +borsh = { version = "1", default-features = false, features = ["derive"], optional = true } + +## Optional: enabled by the `parity-scale-codec` feature +parity-scale-codec = { version = "3.0.0", default-features = false, features = ["full"], optional = true } +scale-info = { version = "2.1.2", default-features = false, features = ["derive"], optional = true } + [dev-dependencies] serde_json = { version = "1.0", default-features = false, features = ["alloc"] } diff --git a/proto/src/google/protobuf/any.rs b/proto/src/google/protobuf/any.rs index 5c8842d74..a901edcdd 100644 --- a/proto/src/google/protobuf/any.rs +++ b/proto/src/google/protobuf/any.rs @@ -246,3 +246,91 @@ impl<'de> serde::Deserialize<'de> for Any { deserializer.deserialize_struct("google.protobuf.Any", FIELDS, GeneratedVisitor) } } + +#[cfg(any(feature = "borsh", feature = "parity-scale-codec"))] +mod sealed { + use super::Any; + + use alloc::string::String; + use alloc::vec::Vec; + + #[cfg_attr( + feature = "parity-scale-codec", + derive( + parity_scale_codec::Encode, + parity_scale_codec::Decode, + scale_info::TypeInfo + ) + )] + #[cfg_attr( + feature = "borsh", + derive(borsh::BorshSerialize, borsh::BorshDeserialize) + )] + struct InnerAny { + pub type_url: String, + pub value: Vec, + } + + #[cfg(feature = "borsh")] + impl borsh::BorshSerialize for Any { + fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { + let inner_any = InnerAny { + type_url: self.type_url.clone(), + value: self.value.clone(), + }; + + borsh::BorshSerialize::serialize(&inner_any, writer) + } + } + + #[cfg(feature = "borsh")] + impl borsh::BorshDeserialize for Any { + fn deserialize_reader(reader: &mut R) -> borsh::io::Result { + let inner_any = InnerAny::deserialize_reader(reader)?; + + Ok(Any { + type_url: inner_any.type_url, + value: inner_any.value, + }) + } + } + + #[cfg(feature = "parity-scale-codec")] + impl parity_scale_codec::Encode for Any { + fn encode_to(&self, writer: &mut T) { + let inner_any = InnerAny { + type_url: self.type_url.clone(), + value: self.value.clone(), + }; + inner_any.encode_to(writer); + } + } + #[cfg(feature = "parity-scale-codec")] + impl parity_scale_codec::Decode for Any { + fn decode( + input: &mut I, + ) -> Result { + let inner_any = InnerAny::decode(input)?; + Ok(Any { + type_url: inner_any.type_url.clone(), + value: inner_any.value, + }) + } + } + + #[cfg(feature = "parity-scale-codec")] + impl scale_info::TypeInfo for Any { + type Identity = Self; + + fn type_info() -> scale_info::Type { + scale_info::Type::builder() + .path(scale_info::Path::new("Any", "ibc_proto::google::protobuf")) + // i128 is chosen before we represent the timestamp is nanoseconds, which is represented as a i128 by Time + .composite( + scale_info::build::Fields::named() + .field(|f| f.ty::().name("type_url").type_name("String")) + .field(|f| f.ty::>().name("value").type_name("Vec")), + ) + } + } +}