use alloc::string::String;
use crate::google::protobuf::Any;
impl Any {
pub fn pack(msg: &impl buffa::Message, type_url: impl Into<String>) -> Self {
Self {
type_url: type_url.into(),
value: msg.encode_to_bytes(),
..Default::default()
}
}
pub fn try_pack(
msg: &impl buffa::Message,
type_url: impl Into<String>,
) -> Result<Self, buffa::EncodeError> {
Ok(Self {
type_url: type_url.into(),
value: msg.try_encode_to_bytes()?,
..Default::default()
})
}
pub fn unpack_unchecked<T: buffa::Message>(&self) -> Result<T, buffa::DecodeError> {
T::decode(&mut self.value.as_ref())
}
pub fn unpack_if<T: buffa::Message>(
&self,
expected_type_url: &str,
) -> Result<Option<T>, buffa::DecodeError> {
if self.type_url != expected_type_url {
return Ok(None);
}
T::decode(&mut self.value.as_ref()).map(Some)
}
pub fn is_type(&self, type_url: &str) -> bool {
self.type_url == type_url
}
pub fn type_url(&self) -> &str {
&self.type_url
}
}
pub fn register_wkt_types(reg: &mut buffa::type_registry::TypeRegistry) {
use crate::google::protobuf::*;
use buffa::type_registry::{any_encode_text, any_merge_text, TextAnyEntry};
macro_rules! register_type {
($type:ty, $wkt:expr) => {
#[cfg(feature = "json")]
{
use alloc::string::ToString;
reg.register_json_any(buffa::type_registry::JsonAnyEntry {
type_url: <$type>::TYPE_URL,
to_json: |bytes| {
let msg = <$type as buffa::Message>::decode(&mut &*bytes)
.map_err(|e| e.to_string())?;
serde_json::to_value(&msg).map_err(|e| e.to_string())
},
from_json: |value| {
let msg: $type =
serde_json::from_value(value).map_err(|e| e.to_string())?;
buffa::Message::try_encode_to_vec(&msg).map_err(|e| e.to_string())
},
is_wkt: $wkt,
});
}
reg.register_text_any(TextAnyEntry {
type_url: <$type>::TYPE_URL,
text_encode: any_encode_text::<$type>,
text_merge: any_merge_text::<$type>,
});
};
}
macro_rules! register_text_only {
($type:ty) => {
reg.register_text_any(TextAnyEntry {
type_url: <$type>::TYPE_URL,
text_encode: any_encode_text::<$type>,
text_merge: any_merge_text::<$type>,
});
};
}
register_type!(Duration, true);
register_type!(Timestamp, true);
register_type!(FieldMask, true);
register_type!(Value, true);
register_type!(Struct, true);
register_type!(ListValue, true);
register_type!(BoolValue, true);
register_type!(Int32Value, true);
register_type!(UInt32Value, true);
register_type!(Int64Value, true);
register_type!(UInt64Value, true);
register_type!(FloatValue, true);
register_type!(DoubleValue, true);
register_type!(StringValue, true);
register_type!(BytesValue, true);
register_type!(Any, true);
register_type!(Empty, false);
register_text_only!(Api);
register_text_only!(Method);
register_text_only!(Mixin);
register_text_only!(Type);
register_text_only!(Field);
register_text_only!(Enum);
register_text_only!(EnumValue);
register_text_only!(crate::google::protobuf::Option);
register_text_only!(SourceContext);
}
impl buffa::text::TextFormat for Any {
fn encode_text(&self, enc: &mut buffa::text::TextEncoder<'_>) -> core::fmt::Result {
if !self.type_url.is_empty() && enc.try_write_any_expanded(&self.type_url, &self.value)? {
return Ok(());
}
if !self.type_url.is_empty() {
enc.write_field_name("type_url")?;
enc.write_string(&self.type_url)?;
}
if !self.value.is_empty() {
enc.write_field_name("value")?;
enc.write_bytes(&self.value)?;
}
Ok(())
}
fn merge_text(
&mut self,
dec: &mut buffa::text::TextDecoder<'_>,
) -> Result<(), buffa::text::ParseError> {
while let Some(name) = dec.read_field_name()? {
match name {
"type_url" => self.type_url = dec.read_string()?.into_owned(),
"value" => self.value = dec.read_bytes()?.into(),
_ if name.starts_with('[') => {
let (url, bytes) = dec.read_any_expansion(name)?;
self.type_url = url.into();
self.value = bytes.into();
}
_ => dec.skip_value()?,
}
}
Ok(())
}
}
#[cfg(test)]
mod text_tests {
use super::Any;
use buffa::text::{decode_from_str, encode_to_string};
#[test]
fn vanilla_roundtrip_no_registry() {
let orig = Any {
type_url: "type.example.com/Foo".into(),
value: alloc::vec![0x08, 0x2A].into(), ..Default::default()
};
let text = encode_to_string(&orig);
assert_eq!(text, r#"type_url: "type.example.com/Foo" value: "\010*""#);
let back: Any = decode_from_str(&text).unwrap();
assert_eq!(back.type_url, orig.type_url);
assert_eq!(back.value, orig.value);
}
}
#[cfg(feature = "json")]
struct Base64Bytes<'a>(&'a [u8]);
#[cfg(feature = "json")]
impl serde::Serialize for Base64Bytes<'_> {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
buffa::json_helpers::bytes::serialize(self.0, s)
}
}
#[cfg(feature = "json")]
impl serde::Serialize for Any {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
if self.type_url.is_empty() {
return s.serialize_map(Some(0))?.end();
}
let lookup = buffa::any_registry::with_any_registry(|reg| {
reg.and_then(|r| r.lookup(&self.type_url))
.map(|e| (e.to_json, e.is_wkt))
});
match lookup {
Some((to_json, is_wkt)) => {
let Some(_depth_guard) = buffa::type_registry::enter_any_expansion() else {
return Err(serde::ser::Error::custom(alloc::format!(
"Any expansion nested deeper than {} levels",
buffa::type_registry::MAX_ANY_EXPANSION_DEPTH
)));
};
let json_val = to_json(&self.value).map_err(serde::ser::Error::custom)?;
if is_wkt {
let mut map = s.serialize_map(Some(2))?;
map.serialize_entry("@type", &self.type_url)?;
map.serialize_entry("value", &json_val)?;
map.end()
} else {
let fields = match &json_val {
serde_json::Value::Object(m) => m,
_ => {
return Err(serde::ser::Error::custom(
"Any: to_json for non-WKT must return a JSON object",
))
}
};
let mut map = s.serialize_map(Some(1 + fields.len()))?;
map.serialize_entry("@type", &self.type_url)?;
for (k, v) in fields {
map.serialize_entry(k, v)?;
}
map.end()
}
}
None => {
let mut map = s.serialize_map(Some(2))?;
map.serialize_entry("@type", &self.type_url)?;
map.serialize_entry("value", &Base64Bytes(&self.value))?;
map.end()
}
}
}
}
#[cfg(feature = "json")]
impl<'de> serde::Deserialize<'de> for Any {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let mut obj: serde_json::Map<String, serde_json::Value> =
serde::Deserialize::deserialize(d)?;
let type_url = match obj.remove("@type") {
Some(serde_json::Value::String(s)) => s,
Some(_) => {
return Err(serde::de::Error::custom("@type must be a string"));
}
None if obj.is_empty() => return Ok(Self::default()),
None => {
return Err(serde::de::Error::custom(
"Any object missing string \"@type\"",
));
}
};
let type_name = type_url.rsplit('/').next().unwrap_or("");
if type_url.is_empty() || !type_url.contains('/') || type_name.is_empty() {
return Err(serde::de::Error::custom(
"@type must be a valid type URL containing a '/' and a non-empty type name (e.g. type.googleapis.com/pkg.Type)",
));
}
let lookup = buffa::any_registry::with_any_registry(|reg| {
reg.and_then(|r| r.lookup(&type_url))
.map(|e| (e.from_json, e.is_wkt))
});
let value = match lookup {
Some((from_json, true)) => {
let json_val = obj.remove("value").unwrap_or(serde_json::Value::Null);
from_json(json_val).map_err(serde::de::Error::custom)?
}
Some((from_json, false)) => {
let json_obj = serde_json::Value::Object(obj);
from_json(json_obj).map_err(serde::de::Error::custom)?
}
None => {
match obj.remove("value") {
Some(serde_json::Value::String(s)) => buffa::json_helpers::bytes::deserialize(
serde::de::value::StringDeserializer::<D::Error>::new(s),
)?,
_ => alloc::vec::Vec::new(),
}
}
};
Ok(Self {
type_url,
value: value.into(),
..Default::default()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::google::protobuf::Timestamp;
use buffa::Message as _;
#[test]
fn any_view_to_owned_from_source_is_zero_copy() {
use crate::google::protobuf::__buffa::view::AnyView;
use buffa::view::{MessageView as _, OwnedView};
let src = Any {
type_url: "type.googleapis.com/x".into(),
value: bytes::Bytes::from_static(&[1u8; 256]),
..Default::default()
};
let buf = bytes::Bytes::from(src.encode_to_vec());
let view = AnyView::decode_view(&buf).unwrap();
let owned = view.to_owned_from_source(Some(&buf)).unwrap();
assert_eq!(owned.value, src.value);
let value_ptr = owned.value.as_ptr() as usize;
let buf_range = (buf.as_ptr() as usize)..(buf.as_ptr() as usize + buf.len());
assert!(
buf_range.contains(&value_ptr),
"owned.value should point into buf (slice_ref), got {value_ptr:#x} outside {buf_range:#x?}"
);
let ov = OwnedView::<AnyView<'static>>::decode(buf.clone()).unwrap();
let owned2 = ov.to_owned_message();
assert_eq!(owned2.value, src.value);
assert!(buf_range.contains(&(owned2.value.as_ptr() as usize)));
let copied = view.to_owned_message().unwrap();
assert_eq!(copied.value, src.value);
assert!(!buf_range.contains(&(copied.value.as_ptr() as usize)));
}
#[cfg(feature = "arbitrary")]
#[test]
fn any_arbitrary_with_bytes_value() {
use arbitrary::{Arbitrary, Unstructured};
let raw = [0u8; 64];
let mut u = Unstructured::new(&raw);
let any = Any::arbitrary(&mut u).unwrap();
let _ = any.value.slice(..);
}
#[derive(Clone, Default, PartialEq, Debug)]
struct HugeMsg;
impl buffa::DefaultInstance for HugeMsg {
fn default_instance() -> &'static Self {
static INST: buffa::__private::OnceBox<HugeMsg> = buffa::__private::OnceBox::new();
INST.get_or_init(|| alloc::boxed::Box::new(HugeMsg))
}
}
impl buffa::Message for HugeMsg {
fn compute_size(&self, _cache: &mut buffa::SizeCache) -> u32 {
buffa::MAX_MESSAGE_BYTES + 1
}
fn write_to(&self, _cache: &mut buffa::SizeCache, _buf: &mut impl buffa::EncodeSink) {}
fn merge_field(
&mut self,
tag: buffa::encoding::Tag,
buf: &mut impl bytes::Buf,
_ctx: buffa::DecodeContext<'_>,
) -> Result<(), buffa::DecodeError> {
buffa::encoding::skip_field(tag, buf)?;
Ok(())
}
fn clear(&mut self) {}
}
#[test]
fn try_pack_over_limit_errs() {
assert_eq!(
Any::try_pack(&HugeMsg, "type.googleapis.com/x"),
Err(buffa::EncodeError::MessageTooLarge)
);
}
#[test]
#[should_panic(expected = "2 GiB protobuf limit")]
fn pack_over_limit_panics() {
let _ = Any::pack(&HugeMsg, "type.googleapis.com/x");
}
#[test]
fn try_pack_matches_pack_for_normal_messages() {
let ts = Timestamp {
seconds: 42,
..Default::default()
};
let url = "type.googleapis.com/google.protobuf.Timestamp";
assert_eq!(Any::try_pack(&ts, url).unwrap(), Any::pack(&ts, url));
}
#[test]
fn pack_and_unpack() {
let ts = Timestamp {
seconds: 1_000_000_000,
nanos: 0,
..Default::default()
};
let any = Any::pack(&ts, "type.googleapis.com/google.protobuf.Timestamp");
assert_eq!(
any.type_url(),
"type.googleapis.com/google.protobuf.Timestamp"
);
let decoded: Timestamp = any.unpack_unchecked().unwrap();
assert_eq!(decoded, ts);
}
#[test]
fn unpack_if_matching() {
let ts = Timestamp {
seconds: 42,
..Default::default()
};
let any = Any::pack(&ts, "type.googleapis.com/google.protobuf.Timestamp");
let result: Option<Timestamp> = any
.unpack_if("type.googleapis.com/google.protobuf.Timestamp")
.unwrap();
assert_eq!(result, Some(ts));
}
#[test]
fn unpack_if_wrong_type_returns_none() {
let ts = Timestamp {
seconds: 42,
..Default::default()
};
let any = Any::pack(&ts, "type.googleapis.com/google.protobuf.Timestamp");
let result: Option<Timestamp> = any
.unpack_if("type.googleapis.com/google.protobuf.Duration")
.unwrap();
assert!(result.is_none());
}
#[test]
fn clone_shares_payload_buffer() {
let orig = Any {
type_url: "type.googleapis.com/example.Msg".into(),
value: alloc::vec![0xAB; 1024].into(),
..Default::default()
};
let dup = orig.clone();
assert_eq!(orig.value.as_ptr(), dup.value.as_ptr());
assert_eq!(orig.value.len(), dup.value.len());
}
#[test]
fn is_type() {
let ts = Timestamp::default();
let any = Any::pack(&ts, "type.googleapis.com/google.protobuf.Timestamp");
assert!(any.is_type("type.googleapis.com/google.protobuf.Timestamp"));
assert!(!any.is_type("type.googleapis.com/google.protobuf.Duration"));
}
#[test]
fn round_trip_encoding() {
let ts = Timestamp {
seconds: 99,
nanos: 1,
..Default::default()
};
let any = Any::pack(&ts, "test");
let bytes = any.encode_to_vec();
let decoded_any = Any::decode(&mut bytes.as_slice()).unwrap();
let decoded_ts: Timestamp = decoded_any.unpack_unchecked().unwrap();
assert_eq!(decoded_ts, ts);
}
#[cfg(feature = "json")]
mod serde_tests {
use super::*;
use crate::google::protobuf::Duration;
use buffa::any_registry::clear_any_registry;
use buffa::type_registry::{
clear_text_registry, set_type_registry, TypeRegistry, MAX_ANY_EXPANSION_DEPTH,
};
static REGISTRY_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn with_registry<R>(f: impl FnOnce() -> R) -> R {
let _guard = REGISTRY_LOCK.lock().unwrap();
let mut reg = TypeRegistry::new();
register_wkt_types(&mut reg);
set_type_registry(reg);
let result = f();
clear_any_registry();
clear_text_registry();
result
}
fn without_registry<R>(f: impl FnOnce() -> R) -> R {
let _guard = REGISTRY_LOCK.lock().unwrap();
clear_any_registry();
clear_text_registry();
f()
}
#[test]
fn text_registry_roundtrip_wkt() {
use crate::google::protobuf::Empty;
use buffa::text::{decode_from_str, encode_to_string};
with_registry(|| {
let any = Any::pack(&Empty::default(), Empty::TYPE_URL);
let text = encode_to_string(&any);
assert_eq!(text, "[type.googleapis.com/google.protobuf.Empty] {}");
let back: Any = decode_from_str(&text).unwrap();
assert_eq!(back.type_url, Empty::TYPE_URL);
assert_eq!(back.value, alloc::vec::Vec::<u8>::new());
});
}
#[test]
fn text_registry_roundtrip_source_context() {
use crate::google::protobuf::SourceContext;
use buffa::text::{decode_from_str, encode_to_string};
with_registry(|| {
let sc = SourceContext {
file_name: "google/protobuf/api.proto".into(),
..Default::default()
};
let any = Any::pack(&sc, SourceContext::TYPE_URL);
let text = encode_to_string(&any);
assert_eq!(
text,
r#"[type.googleapis.com/google.protobuf.SourceContext] {file_name: "google/protobuf/api.proto"}"#
);
let back: Any = decode_from_str(&text).unwrap();
assert_eq!(back.type_url, SourceContext::TYPE_URL);
let unpacked: SourceContext = back.unpack_unchecked().unwrap();
assert_eq!(unpacked.file_name, sc.file_name);
});
}
#[test]
fn text_registry_roundtrip_type_and_option() {
use crate::google::protobuf::{Field, SourceContext, Syntax, Type};
use buffa::text::{decode_from_str, encode_to_string};
with_registry(|| {
let ty = Type {
name: "google.example.v1.Msg".into(),
fields: alloc::vec![Field {
name: "id".into(),
number: 1,
json_name: "id".into(),
..Default::default()
}],
syntax: Syntax::SYNTAX_PROTO3.into(),
..Default::default()
};
let any = Any::pack(&ty, Type::TYPE_URL);
let text = encode_to_string(&any);
assert!(
text.starts_with("[type.googleapis.com/google.protobuf.Type] {"),
"{text}"
);
let back: Any = decode_from_str(&text).unwrap();
let unpacked: Type = back.unpack_unchecked().unwrap();
assert_eq!(unpacked, ty);
let sc = SourceContext {
file_name: "a/b.proto".into(),
..Default::default()
};
let opt = crate::google::protobuf::Option {
name: "source".into(),
value: buffa::MessageField::some(Any::pack(&sc, SourceContext::TYPE_URL)),
..Default::default()
};
let any = Any::pack(&opt, crate::google::protobuf::Option::TYPE_URL);
let text = encode_to_string(&any);
assert!(
text.contains("[type.googleapis.com/google.protobuf.SourceContext]"),
"nested Any must expand through the registry: {text}"
);
let back: Any = decode_from_str(&text).unwrap();
let unpacked: crate::google::protobuf::Option = back.unpack_unchecked().unwrap();
assert_eq!(unpacked, opt);
});
}
#[test]
fn text_unregistered_url_errors_on_decode() {
use buffa::text::decode_from_str;
with_registry(|| {
let result: Result<Any, _> =
decode_from_str("[type.googleapis.com/unknown.Type] { x: 1 }");
assert!(result.is_err(), "unknown URL should error, not skip");
});
}
#[test]
fn text_bracket_without_registry_errors() {
use buffa::text::decode_from_str;
without_registry(|| {
let result: Result<Any, _> = decode_from_str("[type.example.com/Unknown] { x: 1 }");
assert!(result.is_err());
});
}
#[test]
fn serialize_wkt_uses_value_wrapping() {
with_registry(|| {
let ts = Timestamp {
seconds: 1_000_000_000,
nanos: 0,
..Default::default()
};
let any = Any::pack(&ts, Timestamp::TYPE_URL);
let json = serde_json::to_value(&any).unwrap();
assert_eq!(json["@type"], Timestamp::TYPE_URL);
assert_eq!(json["value"], "2001-09-09T01:46:40Z");
});
}
#[test]
fn serialize_duration_wkt() {
with_registry(|| {
let dur = Duration::from_secs_nanos(1, 500_000_000);
let any = Any::pack(&dur, Duration::TYPE_URL);
let json = serde_json::to_value(&any).unwrap();
assert_eq!(json["@type"], Duration::TYPE_URL);
assert_eq!(json["value"], "1.500s");
});
}
#[test]
fn serialize_empty_any_is_empty_object() {
with_registry(|| {
let any = Any::default();
let json = serde_json::to_string(&any).unwrap();
assert_eq!(json, "{}");
});
}
#[test]
fn deserialize_wkt_from_json() {
with_registry(|| {
let json = r#"{
"@type": "type.googleapis.com/google.protobuf.Duration",
"value": "1.5s"
}"#;
let any: Any = serde_json::from_str(json).unwrap();
assert_eq!(any.type_url, Duration::TYPE_URL);
let dur: Duration = any.unpack_unchecked().unwrap();
assert_eq!(dur.seconds, 1);
assert_eq!(dur.nanos, 500_000_000);
});
}
#[test]
fn deserialize_unordered_type_tag() {
with_registry(|| {
let json = r#"{
"value": "1.5s",
"@type": "type.googleapis.com/google.protobuf.Duration"
}"#;
let any: Any = serde_json::from_str(json).unwrap();
assert_eq!(any.type_url, Duration::TYPE_URL);
let dur: Duration = any.unpack_unchecked().unwrap();
assert_eq!(dur.seconds, 1);
assert_eq!(dur.nanos, 500_000_000);
});
}
#[test]
fn roundtrip_wkt_json() {
with_registry(|| {
let ts = Timestamp {
seconds: 1_000_000_000,
nanos: 0,
..Default::default()
};
let any = Any::pack(&ts, Timestamp::TYPE_URL);
let json = serde_json::to_string(&any).unwrap();
let decoded: Any = serde_json::from_str(&json).unwrap();
let decoded_ts: Timestamp = decoded.unpack_unchecked().unwrap();
assert_eq!(decoded_ts, ts);
});
}
#[test]
fn nested_any_roundtrip() {
with_registry(|| {
let dur = Duration::from_secs(42);
let inner_any = Any::pack(&dur, Duration::TYPE_URL);
let outer_any = Any::pack(&inner_any, Any::TYPE_URL);
let json = serde_json::to_string(&outer_any).unwrap();
let decoded_outer: Any = serde_json::from_str(&json).unwrap();
let decoded_inner: Any = decoded_outer.unpack_unchecked().unwrap();
let decoded_dur: Duration = decoded_inner.unpack_unchecked().unwrap();
assert_eq!(decoded_dur.seconds, 42);
});
}
#[test]
fn fallback_base64_without_registry() {
without_registry(|| {
let any = Any {
type_url: "type.googleapis.com/unknown.Type".into(),
value: vec![0x08, 0x96, 0x01].into(),
..Default::default()
};
let json = serde_json::to_string(&any).unwrap();
assert!(json.contains("@type"));
assert!(json.contains("value"));
let decoded: Any = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.type_url, any.type_url);
assert_eq!(decoded.value, any.value);
});
}
#[test]
fn deserialize_missing_type_returns_default() {
let json = r#"{}"#;
let any: Any = serde_json::from_str(json).unwrap();
assert_eq!(any, Any::default());
}
#[test]
fn deserialize_rejects_nonempty_object_without_type() {
for json in [r#"{"value":""}"#, r#"{"unknown":1}"#] {
let err = serde_json::from_str::<Any>(json).unwrap_err();
assert!(
err.to_string()
.contains("Any object missing string \"@type\""),
"{json}: {err}"
);
}
}
#[test]
fn fallback_base64_with_registry_but_unknown_type() {
with_registry(|| {
let any = Any {
type_url: "type.googleapis.com/unknown.Type".into(),
value: vec![0x08, 0x96, 0x01].into(),
..Default::default()
};
let json = serde_json::to_string(&any).unwrap();
let decoded: Any = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.type_url, any.type_url);
assert_eq!(decoded.value, any.value);
});
}
#[test]
fn deserialize_rejects_empty_type_url() {
let json = r#"{"@type": "", "value": ""}"#;
let err = serde_json::from_str::<Any>(json).unwrap_err();
assert!(err.to_string().contains("valid type URL"), "{err}");
}
#[test]
fn deserialize_rejects_type_url_without_slash() {
let json = r#"{"@type": "not_a_url", "value": ""}"#;
let err = serde_json::from_str::<Any>(json).unwrap_err();
assert!(err.to_string().contains("valid type URL"), "{err}");
}
#[test]
fn deserialize_accepts_arbitrary_type_url_prefix() {
without_registry(|| {
let json = r#"{"@type": "example.com/custom.Type", "value": "CAI="}"#;
let any: Any = serde_json::from_str(json).unwrap();
assert_eq!(any.type_url, "example.com/custom.Type");
assert_eq!(any.value, vec![0x08, 0x02]);
});
}
#[test]
fn deserialize_rejects_type_url_with_empty_type_name() {
without_registry(|| {
let json = r#"{"@type": "type.googleapis.com/", "value": ""}"#;
let err = serde_json::from_str::<Any>(json).unwrap_err();
assert!(err.to_string().contains("valid type URL"), "{err}");
});
}
fn user_type_to_json(bytes: &[u8]) -> Result<serde_json::Value, String> {
use buffa::encoding::Tag;
let mut cur = bytes;
let mut id = 0i64;
while !cur.is_empty() {
let tag = Tag::decode(&mut cur).map_err(|e| e.to_string())?;
if tag.field_number() == 1 {
id =
buffa::encoding::decode_varint(&mut cur).map_err(|e| e.to_string())? as i64;
} else {
buffa::encoding::skip_field(tag, &mut cur).map_err(|e| e.to_string())?;
}
}
Ok(serde_json::json!({ "id": id }))
}
fn user_type_from_json(value: serde_json::Value) -> Result<alloc::vec::Vec<u8>, String> {
use buffa::encoding::{encode_varint, Tag, WireType};
let id = value
.get("id")
.and_then(|v| v.as_i64())
.ok_or_else(|| "missing or invalid 'id' field".to_string())?;
let mut buf = alloc::vec::Vec::new();
Tag::new(1, WireType::Varint).encode(&mut buf);
encode_varint(id as u64, &mut buf);
Ok(buf)
}
fn with_user_type_registry<R>(f: impl FnOnce() -> R) -> R {
use buffa::type_registry::JsonAnyEntry;
let _guard = REGISTRY_LOCK.lock().unwrap();
let mut reg = TypeRegistry::new();
reg.register_json_any(JsonAnyEntry {
type_url: "type.example.com/user.Thing",
to_json: user_type_to_json,
from_json: user_type_from_json,
is_wkt: false,
});
set_type_registry(reg);
let result = f();
clear_any_registry();
clear_text_registry();
result
}
#[test]
fn serialize_non_wkt_inlines_fields() {
with_user_type_registry(|| {
let any = Any {
type_url: "type.example.com/user.Thing".into(),
value: vec![0x08, 0x2A].into(),
..Default::default()
};
let json = serde_json::to_value(&any).unwrap();
assert_eq!(json["@type"], "type.example.com/user.Thing");
assert_eq!(json["id"], 42);
assert!(
json.get("value").is_none(),
"non-WKT should not use 'value' wrapping: {json}"
);
});
}
#[test]
fn deserialize_non_wkt_from_inlined_fields() {
with_user_type_registry(|| {
let json = r#"{
"@type": "type.example.com/user.Thing",
"id": 99
}"#;
let any: Any = serde_json::from_str(json).unwrap();
assert_eq!(any.type_url, "type.example.com/user.Thing");
assert_eq!(any.value, vec![0x08, 99]);
});
}
#[test]
fn non_wkt_round_trip() {
with_user_type_registry(|| {
let original = Any {
type_url: "type.example.com/user.Thing".into(),
value: vec![0x08, 0x07].into(), ..Default::default()
};
let json = serde_json::to_string(&original).unwrap();
let decoded: Any = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.type_url, original.type_url);
assert_eq!(decoded.value, original.value);
});
}
#[test]
fn serialize_non_wkt_rejects_non_object_json() {
use buffa::type_registry::JsonAnyEntry;
let _guard = REGISTRY_LOCK.lock().unwrap();
let mut reg = TypeRegistry::new();
reg.register_json_any(JsonAnyEntry {
type_url: "type.example.com/user.BadType",
to_json: |_bytes| Ok(serde_json::Value::Number(42.into())),
from_json: |_v| Ok(alloc::vec::Vec::new()),
is_wkt: false,
});
set_type_registry(reg);
let any = Any {
type_url: "type.example.com/user.BadType".into(),
value: vec![].into(),
..Default::default()
};
let result = serde_json::to_string(&any);
clear_any_registry();
clear_text_registry();
assert!(result.is_err(), "expected error for non-object to_json");
assert!(
result
.unwrap_err()
.to_string()
.contains("must return a JSON object"),
"wrong error message"
);
}
#[test]
fn corrupt_any_value_bytes_encode_to_text_without_panicking() {
with_registry(|| {
let a = Any {
type_url: Duration::TYPE_URL.to_string(),
value: bytes::Bytes::from_static(&[0xFF]),
..Default::default()
};
assert_eq!(
buffa::text::encode_to_string(&a),
"[type.googleapis.com/google.protobuf.Duration] {}",
"the expansion is still emitted, with an empty body"
);
});
}
#[test]
fn deserialize_rejects_non_string_type() {
let json = r#"{"@type": 123}"#;
let err = serde_json::from_str::<Any>(json).unwrap_err();
assert!(err.to_string().contains("@type must be a string"), "{err}");
}
fn any_chain(depth: usize) -> Any {
let mut cur = Any::default();
for _ in 0..depth {
cur = Any {
type_url: Any::TYPE_URL.to_string(),
value: buffa::Message::encode_to_vec(&cur).into(),
..Default::default()
};
}
cur
}
#[test]
fn a_shallow_any_chain_still_expands() {
with_registry(|| {
let json = serde_json::to_string(&any_chain(8)).expect("well within the cap");
assert_eq!(json.matches("\"@type\"").count(), 8, "{json}");
});
}
#[test]
fn a_deep_any_chain_is_refused_rather_than_recursed() {
with_registry(|| {
let deep = any_chain(usize::try_from(MAX_ANY_EXPANSION_DEPTH).unwrap() + 5);
let wire = buffa::Message::encode_to_vec(&deep);
let decoded = <Any as buffa::Message>::decode_from_slice(&wire)
.expect("decoding the chain is one level deep and always succeeds");
let err = serde_json::to_string(&decoded)
.expect_err("expansion past the cap must be an error, not a deeper stack");
assert!(
err.to_string()
.contains(&alloc::format!("{MAX_ANY_EXPANSION_DEPTH} levels")),
"the error should state the limit it hit, as a number the \
reader can act on rather than a constant to go look up: {err}"
);
});
}
#[test]
fn the_expansion_depth_is_restored_after_a_refusal() {
with_registry(|| {
let deep = any_chain(usize::try_from(MAX_ANY_EXPANSION_DEPTH).unwrap() + 5);
assert!(serde_json::to_string(&deep).is_err());
let json = serde_json::to_string(&any_chain(4))
.expect("a rejected serialization must not poison the thread");
assert_eq!(json.matches("\"@type\"").count(), 4, "{json}");
});
}
#[test]
fn a_deep_any_chain_falls_back_to_the_vanilla_text_form() {
with_registry(|| {
let deep = any_chain(usize::try_from(MAX_ANY_EXPANSION_DEPTH).unwrap() + 5);
let text = buffa::text::encode_to_string(&deep);
assert!(
text.contains("type_url:"),
"the innermost levels must fall back to the vanilla form: {text}"
);
assert_eq!(
text.matches("[type.googleapis.com/google.protobuf.Any]")
.count(),
usize::try_from(MAX_ANY_EXPANSION_DEPTH).unwrap(),
"expansion should stop at exactly the cap"
);
});
}
}
}