diff --git a/prost-types/src/lib.rs b/prost-types/src/lib.rs index ab95dbc5a..2bf6c2cfa 100644 --- a/prost-types/src/lib.rs +++ b/prost-types/src/lib.rs @@ -24,6 +24,11 @@ use core::i64; use core::str::FromStr; use core::time; +use prost::alloc::format; +use prost::alloc::string::String; +use prost::alloc::vec::Vec; +use prost::{DecodeError, EncodeError, Message, Name}; + pub use protobuf::*; // The Protobuf `Duration` and `Timestamp` types can't delegate to the standard library equivalents @@ -33,6 +38,58 @@ pub use protobuf::*; const NANOS_PER_SECOND: i32 = 1_000_000_000; 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 }) + } + + /// 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)?); + } + } + _ => (), + } + + 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::() + } +} + impl Duration { /// Normalizes the duration to a canonical format. /// @@ -85,6 +142,15 @@ impl Duration { } } +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; @@ -298,6 +364,15 @@ impl Timestamp { } } +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")] @@ -421,6 +496,49 @@ impl fmt::Display for Timestamp { } } +/// 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('/')?; + + // 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. +fn type_url_for() -> String { + format!("type.googleapis.com/{}.{}", T::PACKAGE, T::NAME) +} + #[cfg(test)] mod tests { use super::*; @@ -744,4 +862,41 @@ mod tests { ); } } + + #[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/src/lib.rs b/src/lib.rs index 4a97bb819..6ec7838d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ pub use bytes; mod error; mod message; +mod name; mod types; #[doc(hidden)] @@ -18,6 +19,7 @@ pub mod encoding; pub use crate::error::{DecodeError, EncodeError}; pub use crate::message::Message; +pub use crate::name::Name; use bytes::{Buf, BufMut}; diff --git a/src/name.rs b/src/name.rs new file mode 100644 index 000000000..1f94de6a7 --- /dev/null +++ b/src/name.rs @@ -0,0 +1,28 @@ +//! Support for associating type name information with a [`Message`]. + +use crate::Message; +use alloc::{format, string::String}; + +/// Associate a type name with a [`Message`] type. +pub trait Name: Message { + /// Type name for this [`Message`]. This is the camel case name, + /// e.g. `TypeName`. + const NAME: &'static str; + + /// Package name this message type is contained in. They are domain-like + /// and delimited by `.`, e.g. `google.protobuf`. + const PACKAGE: &'static str; + + /// Full name of this message type containing both the package name and + /// type name, e.g. `google.protobuf.TypeName`. + fn full_name() -> String { + format!("{}.{}", Self::NAME, Self::PACKAGE) + } + + /// Type URL for this message, which by default is the full name with a + /// leading slash, but may also include a leading domain name, e.g. + /// `type.googleapis.com/google.profile.Person`. + fn type_url() -> String { + format!("/{}", Self::full_name()) + } +}