Skip to main content

buffa_types/
any_ext.rs

1//! Ergonomic helpers for [`google::protobuf::Any`](crate::google::protobuf::Any).
2
3use alloc::string::String;
4
5use crate::google::protobuf::Any;
6
7impl Any {
8    /// Pack a message into an [`Any`] with the given type URL.
9    ///
10    /// The type URL is conventionally of the form
11    /// `type.googleapis.com/fully.qualified.TypeName`, but this method does
12    /// not enforce that convention — any string is accepted.
13    ///
14    /// # Panics
15    ///
16    /// Panics if `msg`'s encoded size exceeds the 2 GiB protobuf limit
17    /// ([`buffa::MAX_MESSAGE_BYTES`]) — see [`try_pack`](Self::try_pack)
18    /// for the error-returning variant.
19    pub fn pack(msg: &impl buffa::Message, type_url: impl Into<String>) -> Self {
20        Self {
21            type_url: type_url.into(),
22            value: msg.encode_to_bytes(),
23            ..Default::default()
24        }
25    }
26
27    /// Pack a message into an [`Any`], returning an error instead of
28    /// panicking if the message's encoded size exceeds the 2 GiB protobuf
29    /// limit ([`buffa::MAX_MESSAGE_BYTES`]).
30    ///
31    /// # Errors
32    ///
33    /// Returns [`buffa::EncodeError::MessageTooLarge`] if the encoded size
34    /// exceeds the limit.
35    pub fn try_pack(
36        msg: &impl buffa::Message,
37        type_url: impl Into<String>,
38    ) -> Result<Self, buffa::EncodeError> {
39        Ok(Self {
40            type_url: type_url.into(),
41            value: msg.try_encode_to_bytes()?,
42            ..Default::default()
43        })
44    }
45
46    /// Unpack the contained message, decoding its bytes as `T`, **without
47    /// checking the `type_url`**.
48    ///
49    /// This method always attempts to decode the payload as `T` regardless
50    /// of whether `type_url` actually identifies `T`. Use [`Any::unpack_if`]
51    /// when you need to verify the stored type before decoding.
52    ///
53    /// # Errors
54    ///
55    /// Returns a [`buffa::DecodeError`] if the bytes cannot be decoded as `T`.
56    pub fn unpack_unchecked<T: buffa::Message>(&self) -> Result<T, buffa::DecodeError> {
57        T::decode(&mut self.value.as_ref())
58    }
59
60    /// Unpack the contained message as `T`, but only if the `type_url`
61    /// matches `expected_type_url`.
62    ///
63    /// Returns `Ok(None)` when the type URL does not match.
64    ///
65    /// # Errors
66    ///
67    /// Returns a [`buffa::DecodeError`] if the type URL matches but the bytes
68    /// cannot be decoded as `T`.
69    pub fn unpack_if<T: buffa::Message>(
70        &self,
71        expected_type_url: &str,
72    ) -> Result<Option<T>, buffa::DecodeError> {
73        if self.type_url != expected_type_url {
74            return Ok(None);
75        }
76        T::decode(&mut self.value.as_ref()).map(Some)
77    }
78
79    /// Returns `true` if this [`Any`]'s `type_url` matches the given string.
80    pub fn is_type(&self, type_url: &str) -> bool {
81        self.type_url == type_url
82    }
83
84    /// Returns the type URL stored in this [`Any`].
85    pub fn type_url(&self) -> &str {
86        &self.type_url
87    }
88}
89
90// ── WKT type registry ───────────────────────────────────────────────────────
91
92/// Registers all well-known types with the given [`TypeRegistry`].
93///
94/// This registers Duration, Timestamp, FieldMask, Value, Struct, ListValue,
95/// Empty, all wrapper types, Any itself, and the remaining official WKTs
96/// (`Api`, `Method`, `Mixin`, `Type`, `Field`, `Enum`, `EnumValue`, `Option`,
97/// `SourceContext`), enabling both proto3-compliant
98/// JSON serialization (under the `json` feature) and textproto
99/// `[type_url] { fields }` Any-expansion when these types appear inside
100/// `google.protobuf.Any` fields.
101///
102/// Text entries are always registered (buffa-types unconditionally enables
103/// `buffa/text`). JSON entries are registered under the `json` feature.
104///
105/// # Example
106///
107/// ```rust,no_run
108/// use buffa::type_registry::{TypeRegistry, set_type_registry};
109///
110/// let mut reg = TypeRegistry::new();
111/// buffa_types::register_wkt_types(&mut reg);
112/// set_type_registry(reg);
113/// ```
114///
115/// [`TypeRegistry`]: buffa::type_registry::TypeRegistry
116pub fn register_wkt_types(reg: &mut buffa::type_registry::TypeRegistry) {
117    use crate::google::protobuf::*;
118    use buffa::type_registry::{any_encode_text, any_merge_text, TextAnyEntry};
119
120    macro_rules! register_type {
121        ($type:ty, $wkt:expr) => {
122            #[cfg(feature = "json")]
123            {
124                use alloc::string::ToString;
125                reg.register_json_any(buffa::type_registry::JsonAnyEntry {
126                    type_url: <$type>::TYPE_URL,
127                    to_json: |bytes| {
128                        let msg = <$type as buffa::Message>::decode(&mut &*bytes)
129                            .map_err(|e| e.to_string())?;
130                        serde_json::to_value(&msg).map_err(|e| e.to_string())
131                    },
132                    from_json: |value| {
133                        let msg: $type =
134                            serde_json::from_value(value).map_err(|e| e.to_string())?;
135                        buffa::Message::try_encode_to_vec(&msg).map_err(|e| e.to_string())
136                    },
137                    is_wkt: $wkt,
138                });
139            }
140            // WKTs all implement TextFormat (generate_text is on for
141            // buffa-types). Non-Option fn-ptrs — presence in the text map
142            // means text-capable. `$wkt` is irrelevant here: textproto has
143            // no `"value"` wrapping distinction.
144            reg.register_text_any(TextAnyEntry {
145                type_url: <$type>::TYPE_URL,
146                text_encode: any_encode_text::<$type>,
147                text_merge: any_merge_text::<$type>,
148            });
149        };
150    }
151    macro_rules! register_text_only {
152        ($type:ty) => {
153            reg.register_text_any(TextAnyEntry {
154                type_url: <$type>::TYPE_URL,
155                text_encode: any_encode_text::<$type>,
156                text_merge: any_merge_text::<$type>,
157            });
158        };
159    }
160
161    // WKTs with special JSON mappings (use "value" wrapping in Any JSON).
162    register_type!(Duration, true);
163    register_type!(Timestamp, true);
164    register_type!(FieldMask, true);
165    register_type!(Value, true);
166    register_type!(Struct, true);
167    register_type!(ListValue, true);
168    register_type!(BoolValue, true);
169    register_type!(Int32Value, true);
170    register_type!(UInt32Value, true);
171    register_type!(Int64Value, true);
172    register_type!(UInt64Value, true);
173    register_type!(FloatValue, true);
174    register_type!(DoubleValue, true);
175    register_type!(StringValue, true);
176    register_type!(BytesValue, true);
177    register_type!(Any, true);
178
179    // Regular messages (fields inlined in Any JSON). Empty has a
180    // hand-written JSON impl; Api/Type/SourceContext and their parts have
181    // generated text impls and no serde impls, so they register for
182    // textproto Any-expansion without a JSON entry.
183    register_type!(Empty, false);
184
185    register_text_only!(Api);
186    register_text_only!(Method);
187    register_text_only!(Mixin);
188    register_text_only!(Type);
189    register_text_only!(Field);
190    register_text_only!(Enum);
191    register_text_only!(EnumValue);
192    register_text_only!(crate::google::protobuf::Option);
193    register_text_only!(SourceContext);
194}
195
196// ── TextFormat impl ─────────────────────────────────────────────────────────
197//
198// Hand-written because textproto packs `Any` as `[type_url] { fields }` when
199// the type is registered — a shape the generated field-by-field impl can't
200// produce. Codegen's `impl_text.rs` skips `google.protobuf.Any` to avoid a
201// conflicting impl.
202//
203// `try_write_any_expanded` and `read_any_expansion` consult the text-format
204// Any map (installed via `set_type_registry`). When no registry is installed,
205// this degrades to the vanilla `type_url: "..." value: "..."` form — still
206// valid textproto, just not the expanded form.
207
208impl buffa::text::TextFormat for Any {
209    fn encode_text(&self, enc: &mut buffa::text::TextEncoder<'_>) -> core::fmt::Result {
210        if !self.type_url.is_empty() && enc.try_write_any_expanded(&self.type_url, &self.value)? {
211            return Ok(());
212        }
213        // Vanilla fallback: unregistered type, or no registry installed.
214        if !self.type_url.is_empty() {
215            enc.write_field_name("type_url")?;
216            enc.write_string(&self.type_url)?;
217        }
218        if !self.value.is_empty() {
219            enc.write_field_name("value")?;
220            enc.write_bytes(&self.value)?;
221        }
222        Ok(())
223    }
224
225    fn merge_text(
226        &mut self,
227        dec: &mut buffa::text::TextDecoder<'_>,
228    ) -> Result<(), buffa::text::ParseError> {
229        while let Some(name) = dec.read_field_name()? {
230            match name {
231                "type_url" => self.type_url = dec.read_string()?.into_owned(),
232                "value" => self.value = dec.read_bytes()?.into(),
233                _ if name.starts_with('[') => {
234                    let (url, bytes) = dec.read_any_expansion(name)?;
235                    self.type_url = url.into();
236                    self.value = bytes.into();
237                }
238                _ => dec.skip_value()?,
239            }
240        }
241        Ok(())
242    }
243}
244
245#[cfg(test)]
246mod text_tests {
247    use super::Any;
248    use buffa::text::{decode_from_str, encode_to_string};
249
250    #[test]
251    fn vanilla_roundtrip_no_registry() {
252        // Without a registry installed, Any uses the plain
253        // `type_url: "..." value: "..."` form — exactly what the old
254        // generated impl did.
255        let orig = Any {
256            type_url: "type.example.com/Foo".into(),
257            value: alloc::vec![0x08, 0x2A].into(), // field 1 = varint 42
258            ..Default::default()
259        };
260        let text = encode_to_string(&orig);
261        assert_eq!(text, r#"type_url: "type.example.com/Foo" value: "\010*""#);
262        let back: Any = decode_from_str(&text).unwrap();
263        assert_eq!(back.type_url, orig.type_url);
264        assert_eq!(back.value, orig.value);
265    }
266
267    // Registry-manipulating tests live in `serde_tests` below — they share
268    // the same global `AtomicPtr` as the JSON tests and must use the same
269    // `REGISTRY_LOCK` to serialize.
270}
271
272// ── serde impls ──────────────────────────────────────────────────────────────
273//
274// Proto3 JSON for `Any` uses the global `AnyRegistry` to serialize the
275// embedded message with its fields inline (regular messages) or wrapped in a
276// `"value"` key (WKTs). Falls back to base64-encoded `value` when the
277// registry is absent or the type URL is not registered.
278
279#[cfg(feature = "json")]
280struct Base64Bytes<'a>(&'a [u8]);
281
282#[cfg(feature = "json")]
283impl serde::Serialize for Base64Bytes<'_> {
284    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
285        buffa::json_helpers::bytes::serialize(self.0, s)
286    }
287}
288
289#[cfg(feature = "json")]
290impl serde::Serialize for Any {
291    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
292        use serde::ser::SerializeMap;
293
294        if self.type_url.is_empty() {
295            return s.serialize_map(Some(0))?.end();
296        }
297
298        let lookup = buffa::any_registry::with_any_registry(|reg| {
299            reg.and_then(|r| r.lookup(&self.type_url))
300                .map(|e| (e.to_json, e.is_wkt))
301        });
302
303        match lookup {
304            Some((to_json, is_wkt)) => {
305                // `to_json` decodes the payload and serializes it, so an `Any`
306                // holding an `Any` re-enters this impl. See
307                // `MAX_ANY_EXPANSION_DEPTH` for why the decoder's recursion
308                // limit does not bound that.
309                let Some(_depth_guard) = buffa::type_registry::enter_any_expansion() else {
310                    return Err(serde::ser::Error::custom(alloc::format!(
311                        "Any expansion nested deeper than {} levels",
312                        buffa::type_registry::MAX_ANY_EXPANSION_DEPTH
313                    )));
314                };
315                let json_val = to_json(&self.value).map_err(serde::ser::Error::custom)?;
316                if is_wkt {
317                    let mut map = s.serialize_map(Some(2))?;
318                    map.serialize_entry("@type", &self.type_url)?;
319                    map.serialize_entry("value", &json_val)?;
320                    map.end()
321                } else {
322                    let fields = match &json_val {
323                        serde_json::Value::Object(m) => m,
324                        _ => {
325                            return Err(serde::ser::Error::custom(
326                                "Any: to_json for non-WKT must return a JSON object",
327                            ))
328                        }
329                    };
330                    let mut map = s.serialize_map(Some(1 + fields.len()))?;
331                    map.serialize_entry("@type", &self.type_url)?;
332                    for (k, v) in fields {
333                        map.serialize_entry(k, v)?;
334                    }
335                    map.end()
336                }
337            }
338            None => {
339                let mut map = s.serialize_map(Some(2))?;
340                map.serialize_entry("@type", &self.type_url)?;
341                map.serialize_entry("value", &Base64Bytes(&self.value))?;
342                map.end()
343            }
344        }
345    }
346}
347
348#[cfg(feature = "json")]
349impl<'de> serde::Deserialize<'de> for Any {
350    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
351        // Buffer the entire object so @type can appear at any position.
352        let mut obj: serde_json::Map<String, serde_json::Value> =
353            serde::Deserialize::deserialize(d)?;
354
355        let type_url = match obj.remove("@type") {
356            Some(serde_json::Value::String(s)) => s,
357            Some(_) => {
358                return Err(serde::de::Error::custom("@type must be a string"));
359            }
360            None if obj.is_empty() => return Ok(Self::default()),
361            None => {
362                return Err(serde::de::Error::custom(
363                    "Any object missing string \"@type\"",
364                ));
365            }
366        };
367
368        // The type URL must be non-empty, contain a '/', and have a non-empty
369        // fully-qualified type name after the final slash (e.g.
370        // "type.googleapis.com/google.protobuf.Duration").
371        let type_name = type_url.rsplit('/').next().unwrap_or("");
372        if type_url.is_empty() || !type_url.contains('/') || type_name.is_empty() {
373            return Err(serde::de::Error::custom(
374                "@type must be a valid type URL containing a '/' and a non-empty type name (e.g. type.googleapis.com/pkg.Type)",
375            ));
376        }
377
378        let lookup = buffa::any_registry::with_any_registry(|reg| {
379            reg.and_then(|r| r.lookup(&type_url))
380                .map(|e| (e.from_json, e.is_wkt))
381        });
382
383        let value = match lookup {
384            Some((from_json, true)) => {
385                let json_val = obj.remove("value").unwrap_or(serde_json::Value::Null);
386                from_json(json_val).map_err(serde::de::Error::custom)?
387            }
388            Some((from_json, false)) => {
389                let json_obj = serde_json::Value::Object(obj);
390                from_json(json_obj).map_err(serde::de::Error::custom)?
391            }
392            None => {
393                // Fallback: base64 decode the "value" field.
394                match obj.remove("value") {
395                    Some(serde_json::Value::String(s)) => buffa::json_helpers::bytes::deserialize(
396                        serde::de::value::StringDeserializer::<D::Error>::new(s),
397                    )?,
398                    _ => alloc::vec::Vec::new(),
399                }
400            }
401        };
402
403        Ok(Self {
404            type_url,
405            value: value.into(),
406            ..Default::default()
407        })
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414    use crate::google::protobuf::Timestamp;
415    use buffa::Message as _;
416
417    #[test]
418    fn any_view_to_owned_from_source_is_zero_copy() {
419        use crate::google::protobuf::__buffa::view::AnyView;
420        use buffa::view::{MessageView as _, OwnedView};
421
422        let src = Any {
423            type_url: "type.googleapis.com/x".into(),
424            value: bytes::Bytes::from_static(&[1u8; 256]),
425            ..Default::default()
426        };
427        let buf = bytes::Bytes::from(src.encode_to_vec());
428
429        // Direct trait path: to_owned_from_source(Some(&buf)) → slice_ref.
430        let view = AnyView::decode_view(&buf).unwrap();
431        let owned = view.to_owned_from_source(Some(&buf)).unwrap();
432        assert_eq!(owned.value, src.value);
433        let value_ptr = owned.value.as_ptr() as usize;
434        let buf_range = (buf.as_ptr() as usize)..(buf.as_ptr() as usize + buf.len());
435        assert!(
436            buf_range.contains(&value_ptr),
437            "owned.value should point into buf (slice_ref), got {value_ptr:#x} outside {buf_range:#x?}"
438        );
439
440        // OwnedView path: the inherent OwnedView::to_owned_message routes
441        // through to_owned_from_source(Some(&self.bytes)), so the bytes field
442        // is a zero-copy slice_ref into the retained buffer.
443        let ov = OwnedView::<AnyView<'static>>::decode(buf.clone()).unwrap();
444        let owned2 = ov.to_owned_message();
445        assert_eq!(owned2.value, src.value);
446        assert!(buf_range.contains(&(owned2.value.as_ptr() as usize)));
447
448        // No-source path still copies (correct, distinct allocation).
449        let copied = view.to_owned_message().unwrap();
450        assert_eq!(copied.value, src.value);
451        assert!(!buf_range.contains(&(copied.value.as_ptr() as usize)));
452    }
453
454    #[cfg(feature = "arbitrary")]
455    #[test]
456    fn any_arbitrary_with_bytes_value() {
457        use arbitrary::{Arbitrary, Unstructured};
458        // Regression pin for https://github.com/anthropics/buffa/issues/88:
459        // Any.value is bytes::Bytes (not Vec<u8>), so derive(Arbitrary) on Any
460        // requires the ::buffa::__private::arbitrary_bytes shim.
461        let raw = [0u8; 64];
462        let mut u = Unstructured::new(&raw);
463        let any = Any::arbitrary(&mut u).unwrap();
464        let _ = any.value.slice(..);
465    }
466
467    /// Test double whose `compute_size` reports over the 2 GiB limit and
468    /// whose `write_to` writes nothing — exercises `pack`'s guard without
469    /// materializing gigabytes. Mirrors buffa's crate-internal
470    /// `test_doubles::SizedMsg` (`#[cfg(test)]` items don't cross the crate
471    /// boundary).
472    #[derive(Clone, Default, PartialEq, Debug)]
473    struct HugeMsg;
474
475    impl buffa::DefaultInstance for HugeMsg {
476        fn default_instance() -> &'static Self {
477            static INST: buffa::__private::OnceBox<HugeMsg> = buffa::__private::OnceBox::new();
478            INST.get_or_init(|| alloc::boxed::Box::new(HugeMsg))
479        }
480    }
481
482    impl buffa::Message for HugeMsg {
483        fn compute_size(&self, _cache: &mut buffa::SizeCache) -> u32 {
484            buffa::MAX_MESSAGE_BYTES + 1
485        }
486        fn write_to(&self, _cache: &mut buffa::SizeCache, _buf: &mut impl buffa::EncodeSink) {}
487        fn merge_field(
488            &mut self,
489            tag: buffa::encoding::Tag,
490            buf: &mut impl bytes::Buf,
491            _ctx: buffa::DecodeContext<'_>,
492        ) -> Result<(), buffa::DecodeError> {
493            buffa::encoding::skip_field(tag, buf)?;
494            Ok(())
495        }
496        fn clear(&mut self) {}
497    }
498
499    #[test]
500    fn try_pack_over_limit_errs() {
501        assert_eq!(
502            Any::try_pack(&HugeMsg, "type.googleapis.com/x"),
503            Err(buffa::EncodeError::MessageTooLarge)
504        );
505    }
506
507    #[test]
508    #[should_panic(expected = "2 GiB protobuf limit")]
509    fn pack_over_limit_panics() {
510        let _ = Any::pack(&HugeMsg, "type.googleapis.com/x");
511    }
512
513    #[test]
514    fn try_pack_matches_pack_for_normal_messages() {
515        let ts = Timestamp {
516            seconds: 42,
517            ..Default::default()
518        };
519        let url = "type.googleapis.com/google.protobuf.Timestamp";
520        assert_eq!(Any::try_pack(&ts, url).unwrap(), Any::pack(&ts, url));
521    }
522
523    #[test]
524    fn pack_and_unpack() {
525        let ts = Timestamp {
526            seconds: 1_000_000_000,
527            nanos: 0,
528            ..Default::default()
529        };
530        let any = Any::pack(&ts, "type.googleapis.com/google.protobuf.Timestamp");
531        assert_eq!(
532            any.type_url(),
533            "type.googleapis.com/google.protobuf.Timestamp"
534        );
535
536        let decoded: Timestamp = any.unpack_unchecked().unwrap();
537        assert_eq!(decoded, ts);
538    }
539
540    #[test]
541    fn unpack_if_matching() {
542        let ts = Timestamp {
543            seconds: 42,
544            ..Default::default()
545        };
546        let any = Any::pack(&ts, "type.googleapis.com/google.protobuf.Timestamp");
547
548        let result: Option<Timestamp> = any
549            .unpack_if("type.googleapis.com/google.protobuf.Timestamp")
550            .unwrap();
551        assert_eq!(result, Some(ts));
552    }
553
554    #[test]
555    fn unpack_if_wrong_type_returns_none() {
556        let ts = Timestamp {
557            seconds: 42,
558            ..Default::default()
559        };
560        let any = Any::pack(&ts, "type.googleapis.com/google.protobuf.Timestamp");
561
562        let result: Option<Timestamp> = any
563            .unpack_if("type.googleapis.com/google.protobuf.Duration")
564            .unwrap();
565        assert!(result.is_none());
566    }
567
568    #[test]
569    fn clone_shares_payload_buffer() {
570        let orig = Any {
571            type_url: "type.googleapis.com/example.Msg".into(),
572            value: alloc::vec![0xAB; 1024].into(),
573            ..Default::default()
574        };
575        let dup = orig.clone();
576        assert_eq!(orig.value.as_ptr(), dup.value.as_ptr());
577        assert_eq!(orig.value.len(), dup.value.len());
578    }
579
580    #[test]
581    fn is_type() {
582        let ts = Timestamp::default();
583        let any = Any::pack(&ts, "type.googleapis.com/google.protobuf.Timestamp");
584        assert!(any.is_type("type.googleapis.com/google.protobuf.Timestamp"));
585        assert!(!any.is_type("type.googleapis.com/google.protobuf.Duration"));
586    }
587
588    #[test]
589    fn round_trip_encoding() {
590        let ts = Timestamp {
591            seconds: 99,
592            nanos: 1,
593            ..Default::default()
594        };
595        let any = Any::pack(&ts, "test");
596
597        let bytes = any.encode_to_vec();
598        let decoded_any = Any::decode(&mut bytes.as_slice()).unwrap();
599        let decoded_ts: Timestamp = decoded_any.unpack_unchecked().unwrap();
600        assert_eq!(decoded_ts, ts);
601    }
602
603    #[cfg(feature = "json")]
604    mod serde_tests {
605        use super::*;
606        use crate::google::protobuf::Duration;
607        use buffa::any_registry::clear_any_registry;
608        use buffa::type_registry::{
609            clear_text_registry, set_type_registry, TypeRegistry, MAX_ANY_EXPANSION_DEPTH,
610        };
611
612        /// Mutex to serialize tests that manipulate the global registries.
613        /// Each test binary needs its own lock since #[cfg(test)] modules
614        /// cannot be shared across crates.
615        static REGISTRY_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
616
617        fn with_registry<R>(f: impl FnOnce() -> R) -> R {
618            let _guard = REGISTRY_LOCK.lock().unwrap();
619            let mut reg = TypeRegistry::new();
620            register_wkt_types(&mut reg);
621            set_type_registry(reg);
622            let result = f();
623            clear_any_registry();
624            clear_text_registry();
625            result
626        }
627
628        fn without_registry<R>(f: impl FnOnce() -> R) -> R {
629            let _guard = REGISTRY_LOCK.lock().unwrap();
630            clear_any_registry();
631            clear_text_registry();
632            f()
633        }
634
635        // ── TextFormat impl (Any expansion) ─────────────────────────────────
636        //
637        // Here rather than in `text_tests` because these manipulate the
638        // same global `AtomicPtr` as the JSON tests above — both must
639        // serialize on `REGISTRY_LOCK`.
640
641        #[test]
642        fn text_registry_roundtrip_wkt() {
643            use crate::google::protobuf::Empty;
644            use buffa::text::{decode_from_str, encode_to_string};
645            with_registry(|| {
646                // register_wkt_types installs Empty with text fn-ptrs.
647                let any = Any::pack(&Empty::default(), Empty::TYPE_URL);
648                let text = encode_to_string(&any);
649                // Empty has no fields → `{}`.
650                assert_eq!(text, "[type.googleapis.com/google.protobuf.Empty] {}");
651
652                let back: Any = decode_from_str(&text).unwrap();
653                assert_eq!(back.type_url, Empty::TYPE_URL);
654                assert_eq!(back.value, alloc::vec::Vec::<u8>::new());
655            });
656        }
657
658        #[test]
659        fn text_registry_roundtrip_source_context() {
660            use crate::google::protobuf::SourceContext;
661            use buffa::text::{decode_from_str, encode_to_string};
662            with_registry(|| {
663                let sc = SourceContext {
664                    file_name: "google/protobuf/api.proto".into(),
665                    ..Default::default()
666                };
667                let any = Any::pack(&sc, SourceContext::TYPE_URL);
668                let text = encode_to_string(&any);
669                assert_eq!(
670                    text,
671                    r#"[type.googleapis.com/google.protobuf.SourceContext] {file_name: "google/protobuf/api.proto"}"#
672                );
673
674                let back: Any = decode_from_str(&text).unwrap();
675                assert_eq!(back.type_url, SourceContext::TYPE_URL);
676                let unpacked: SourceContext = back.unpack_unchecked().unwrap();
677                assert_eq!(unpacked.file_name, sc.file_name);
678            });
679        }
680
681        #[test]
682        fn text_registry_roundtrip_type_and_option() {
683            // `Type` carries a repeated message and an enum; `Option` holds
684            // an `Any`, so its expansion nests a second registered type.
685            use crate::google::protobuf::{Field, SourceContext, Syntax, Type};
686            use buffa::text::{decode_from_str, encode_to_string};
687            with_registry(|| {
688                let ty = Type {
689                    name: "google.example.v1.Msg".into(),
690                    fields: alloc::vec![Field {
691                        name: "id".into(),
692                        number: 1,
693                        json_name: "id".into(),
694                        ..Default::default()
695                    }],
696                    syntax: Syntax::SYNTAX_PROTO3.into(),
697                    ..Default::default()
698                };
699                let any = Any::pack(&ty, Type::TYPE_URL);
700                let text = encode_to_string(&any);
701                assert!(
702                    text.starts_with("[type.googleapis.com/google.protobuf.Type] {"),
703                    "{text}"
704                );
705                let back: Any = decode_from_str(&text).unwrap();
706                let unpacked: Type = back.unpack_unchecked().unwrap();
707                assert_eq!(unpacked, ty);
708
709                let sc = SourceContext {
710                    file_name: "a/b.proto".into(),
711                    ..Default::default()
712                };
713                let opt = crate::google::protobuf::Option {
714                    name: "source".into(),
715                    value: buffa::MessageField::some(Any::pack(&sc, SourceContext::TYPE_URL)),
716                    ..Default::default()
717                };
718                let any = Any::pack(&opt, crate::google::protobuf::Option::TYPE_URL);
719                let text = encode_to_string(&any);
720                assert!(
721                    text.contains("[type.googleapis.com/google.protobuf.SourceContext]"),
722                    "nested Any must expand through the registry: {text}"
723                );
724                let back: Any = decode_from_str(&text).unwrap();
725                let unpacked: crate::google::protobuf::Option = back.unpack_unchecked().unwrap();
726                assert_eq!(unpacked, opt);
727            });
728        }
729
730        #[test]
731        fn text_unregistered_url_errors_on_decode() {
732            use buffa::text::decode_from_str;
733            // Registry installed but URL not in it — the
734            // `AnyFieldWithInvalidType` conformance shape.
735            with_registry(|| {
736                let result: Result<Any, _> =
737                    decode_from_str("[type.googleapis.com/unknown.Type] { x: 1 }");
738                assert!(result.is_err(), "unknown URL should error, not skip");
739            });
740        }
741
742        #[test]
743        fn text_bracket_without_registry_errors() {
744            use buffa::text::decode_from_str;
745            // No registry at all → bracket name is a registry miss → error.
746            without_registry(|| {
747                let result: Result<Any, _> = decode_from_str("[type.example.com/Unknown] { x: 1 }");
748                assert!(result.is_err());
749            });
750        }
751
752        #[test]
753        fn serialize_wkt_uses_value_wrapping() {
754            with_registry(|| {
755                let ts = Timestamp {
756                    seconds: 1_000_000_000,
757                    nanos: 0,
758                    ..Default::default()
759                };
760                let any = Any::pack(&ts, Timestamp::TYPE_URL);
761                let json = serde_json::to_value(&any).unwrap();
762                assert_eq!(json["@type"], Timestamp::TYPE_URL);
763                assert_eq!(json["value"], "2001-09-09T01:46:40Z");
764            });
765        }
766
767        #[test]
768        fn serialize_duration_wkt() {
769            with_registry(|| {
770                let dur = Duration::from_secs_nanos(1, 500_000_000);
771                let any = Any::pack(&dur, Duration::TYPE_URL);
772                let json = serde_json::to_value(&any).unwrap();
773                assert_eq!(json["@type"], Duration::TYPE_URL);
774                assert_eq!(json["value"], "1.500s");
775            });
776        }
777
778        #[test]
779        fn serialize_empty_any_is_empty_object() {
780            with_registry(|| {
781                let any = Any::default();
782                let json = serde_json::to_string(&any).unwrap();
783                assert_eq!(json, "{}");
784            });
785        }
786
787        #[test]
788        fn deserialize_wkt_from_json() {
789            with_registry(|| {
790                let json = r#"{
791                    "@type": "type.googleapis.com/google.protobuf.Duration",
792                    "value": "1.5s"
793                }"#;
794                let any: Any = serde_json::from_str(json).unwrap();
795                assert_eq!(any.type_url, Duration::TYPE_URL);
796
797                let dur: Duration = any.unpack_unchecked().unwrap();
798                assert_eq!(dur.seconds, 1);
799                assert_eq!(dur.nanos, 500_000_000);
800            });
801        }
802
803        #[test]
804        fn deserialize_unordered_type_tag() {
805            with_registry(|| {
806                // @type appears after the value field.
807                let json = r#"{
808                    "value": "1.5s",
809                    "@type": "type.googleapis.com/google.protobuf.Duration"
810                }"#;
811                let any: Any = serde_json::from_str(json).unwrap();
812                assert_eq!(any.type_url, Duration::TYPE_URL);
813
814                let dur: Duration = any.unpack_unchecked().unwrap();
815                assert_eq!(dur.seconds, 1);
816                assert_eq!(dur.nanos, 500_000_000);
817            });
818        }
819
820        #[test]
821        fn roundtrip_wkt_json() {
822            with_registry(|| {
823                let ts = Timestamp {
824                    seconds: 1_000_000_000,
825                    nanos: 0,
826                    ..Default::default()
827                };
828                let any = Any::pack(&ts, Timestamp::TYPE_URL);
829                let json = serde_json::to_string(&any).unwrap();
830                let decoded: Any = serde_json::from_str(&json).unwrap();
831                let decoded_ts: Timestamp = decoded.unpack_unchecked().unwrap();
832                assert_eq!(decoded_ts, ts);
833            });
834        }
835
836        #[test]
837        fn nested_any_roundtrip() {
838            with_registry(|| {
839                let dur = Duration::from_secs(42);
840                let inner_any = Any::pack(&dur, Duration::TYPE_URL);
841                let outer_any = Any::pack(&inner_any, Any::TYPE_URL);
842
843                let json = serde_json::to_string(&outer_any).unwrap();
844                let decoded_outer: Any = serde_json::from_str(&json).unwrap();
845                let decoded_inner: Any = decoded_outer.unpack_unchecked().unwrap();
846                let decoded_dur: Duration = decoded_inner.unpack_unchecked().unwrap();
847                assert_eq!(decoded_dur.seconds, 42);
848            });
849        }
850
851        #[test]
852        fn fallback_base64_without_registry() {
853            without_registry(|| {
854                let any = Any {
855                    type_url: "type.googleapis.com/unknown.Type".into(),
856                    value: vec![0x08, 0x96, 0x01].into(),
857                    ..Default::default()
858                };
859                let json = serde_json::to_string(&any).unwrap();
860                assert!(json.contains("@type"));
861                assert!(json.contains("value"));
862
863                let decoded: Any = serde_json::from_str(&json).unwrap();
864                assert_eq!(decoded.type_url, any.type_url);
865                assert_eq!(decoded.value, any.value);
866            });
867        }
868
869        #[test]
870        fn deserialize_missing_type_returns_default() {
871            let json = r#"{}"#;
872            let any: Any = serde_json::from_str(json).unwrap();
873            assert_eq!(any, Any::default());
874        }
875
876        #[test]
877        fn deserialize_rejects_nonempty_object_without_type() {
878            for json in [r#"{"value":""}"#, r#"{"unknown":1}"#] {
879                let err = serde_json::from_str::<Any>(json).unwrap_err();
880                assert!(
881                    err.to_string()
882                        .contains("Any object missing string \"@type\""),
883                    "{json}: {err}"
884                );
885            }
886        }
887
888        #[test]
889        fn fallback_base64_with_registry_but_unknown_type() {
890            with_registry(|| {
891                let any = Any {
892                    type_url: "type.googleapis.com/unknown.Type".into(),
893                    value: vec![0x08, 0x96, 0x01].into(),
894                    ..Default::default()
895                };
896                let json = serde_json::to_string(&any).unwrap();
897                let decoded: Any = serde_json::from_str(&json).unwrap();
898                assert_eq!(decoded.type_url, any.type_url);
899                assert_eq!(decoded.value, any.value);
900            });
901        }
902
903        #[test]
904        fn deserialize_rejects_empty_type_url() {
905            let json = r#"{"@type": "", "value": ""}"#;
906            let err = serde_json::from_str::<Any>(json).unwrap_err();
907            assert!(err.to_string().contains("valid type URL"), "{err}");
908        }
909
910        #[test]
911        fn deserialize_rejects_type_url_without_slash() {
912            let json = r#"{"@type": "not_a_url", "value": ""}"#;
913            let err = serde_json::from_str::<Any>(json).unwrap_err();
914            assert!(err.to_string().contains("valid type URL"), "{err}");
915        }
916
917        #[test]
918        fn deserialize_accepts_arbitrary_type_url_prefix() {
919            without_registry(|| {
920                let json = r#"{"@type": "example.com/custom.Type", "value": "CAI="}"#;
921                let any: Any = serde_json::from_str(json).unwrap();
922                assert_eq!(any.type_url, "example.com/custom.Type");
923                assert_eq!(any.value, vec![0x08, 0x02]);
924            });
925        }
926
927        #[test]
928        fn deserialize_rejects_type_url_with_empty_type_name() {
929            without_registry(|| {
930                let json = r#"{"@type": "type.googleapis.com/", "value": ""}"#;
931                let err = serde_json::from_str::<Any>(json).unwrap_err();
932                assert!(err.to_string().contains("valid type URL"), "{err}");
933            });
934        }
935
936        // ── Non-WKT registered type (fields inlined at top level) ─────
937        // WKTs use {"@type": ..., "value": <json>} wrapping.
938        // Regular messages use {"@type": ..., "field1": ..., "field2": ...}.
939        // Previously only the WKT path was tested.
940
941        /// Hand-written to_json: decode the Any bytes as a single varint
942        /// field (number=1), return it as a JSON object {"id": N}.
943        fn user_type_to_json(bytes: &[u8]) -> Result<serde_json::Value, String> {
944            use buffa::encoding::Tag;
945            let mut cur = bytes;
946            let mut id = 0i64;
947            while !cur.is_empty() {
948                let tag = Tag::decode(&mut cur).map_err(|e| e.to_string())?;
949                if tag.field_number() == 1 {
950                    id =
951                        buffa::encoding::decode_varint(&mut cur).map_err(|e| e.to_string())? as i64;
952                } else {
953                    buffa::encoding::skip_field(tag, &mut cur).map_err(|e| e.to_string())?;
954                }
955            }
956            Ok(serde_json::json!({ "id": id }))
957        }
958
959        /// Hand-written from_json: extract {"id": N}, encode as varint field 1.
960        fn user_type_from_json(value: serde_json::Value) -> Result<alloc::vec::Vec<u8>, String> {
961            use buffa::encoding::{encode_varint, Tag, WireType};
962            let id = value
963                .get("id")
964                .and_then(|v| v.as_i64())
965                .ok_or_else(|| "missing or invalid 'id' field".to_string())?;
966            let mut buf = alloc::vec::Vec::new();
967            Tag::new(1, WireType::Varint).encode(&mut buf);
968            encode_varint(id as u64, &mut buf);
969            Ok(buf)
970        }
971
972        fn with_user_type_registry<R>(f: impl FnOnce() -> R) -> R {
973            use buffa::type_registry::JsonAnyEntry;
974            let _guard = REGISTRY_LOCK.lock().unwrap();
975            let mut reg = TypeRegistry::new();
976            // Register as NON-WKT (is_wkt=false) — fields inline at top level.
977            reg.register_json_any(JsonAnyEntry {
978                type_url: "type.example.com/user.Thing",
979                to_json: user_type_to_json,
980                from_json: user_type_from_json,
981                is_wkt: false,
982            });
983            set_type_registry(reg);
984            let result = f();
985            clear_any_registry();
986            clear_text_registry();
987            result
988        }
989
990        #[test]
991        fn serialize_non_wkt_inlines_fields() {
992            with_user_type_registry(|| {
993                // Encode {id: 42} as proto wire bytes.
994                let any = Any {
995                    type_url: "type.example.com/user.Thing".into(),
996                    // field 1, varint 42: tag=0x08, value=0x2A
997                    value: vec![0x08, 0x2A].into(),
998                    ..Default::default()
999                };
1000
1001                let json = serde_json::to_value(&any).unwrap();
1002                // Non-WKT format: fields at top level alongside @type.
1003                assert_eq!(json["@type"], "type.example.com/user.Thing");
1004                assert_eq!(json["id"], 42);
1005                // Should NOT have a "value" wrapper key.
1006                assert!(
1007                    json.get("value").is_none(),
1008                    "non-WKT should not use 'value' wrapping: {json}"
1009                );
1010            });
1011        }
1012
1013        #[test]
1014        fn deserialize_non_wkt_from_inlined_fields() {
1015            with_user_type_registry(|| {
1016                let json = r#"{
1017                    "@type": "type.example.com/user.Thing",
1018                    "id": 99
1019                }"#;
1020                let any: Any = serde_json::from_str(json).unwrap();
1021                assert_eq!(any.type_url, "type.example.com/user.Thing");
1022                // Verify the from_json encoded it back to wire bytes.
1023                assert_eq!(any.value, vec![0x08, 99]);
1024            });
1025        }
1026
1027        #[test]
1028        fn non_wkt_round_trip() {
1029            with_user_type_registry(|| {
1030                let original = Any {
1031                    type_url: "type.example.com/user.Thing".into(),
1032                    value: vec![0x08, 0x07].into(), // id=7
1033                    ..Default::default()
1034                };
1035                let json = serde_json::to_string(&original).unwrap();
1036                let decoded: Any = serde_json::from_str(&json).unwrap();
1037                assert_eq!(decoded.type_url, original.type_url);
1038                assert_eq!(decoded.value, original.value);
1039            });
1040        }
1041
1042        #[test]
1043        fn serialize_non_wkt_rejects_non_object_json() {
1044            // If to_json for a non-WKT type returns something other than a
1045            // JSON object, serialization must fail (can't inline non-object
1046            // fields alongside @type).
1047            use buffa::type_registry::JsonAnyEntry;
1048            let _guard = REGISTRY_LOCK.lock().unwrap();
1049            let mut reg = TypeRegistry::new();
1050            reg.register_json_any(JsonAnyEntry {
1051                type_url: "type.example.com/user.BadType",
1052                to_json: |_bytes| Ok(serde_json::Value::Number(42.into())),
1053                from_json: |_v| Ok(alloc::vec::Vec::new()),
1054                is_wkt: false,
1055            });
1056            set_type_registry(reg);
1057
1058            let any = Any {
1059                type_url: "type.example.com/user.BadType".into(),
1060                value: vec![].into(),
1061                ..Default::default()
1062            };
1063            let result = serde_json::to_string(&any);
1064            clear_any_registry();
1065            clear_text_registry();
1066            assert!(result.is_err(), "expected error for non-object to_json");
1067            assert!(
1068                result
1069                    .unwrap_err()
1070                    .to_string()
1071                    .contains("must return a JSON object"),
1072                "wrong error message"
1073            );
1074        }
1075
1076        /// Corrupt `Any.value` bytes are ordinary untrusted input, not an
1077        /// invariant violation, so encoding them must not panic.
1078        ///
1079        /// `Any.value` is a raw `bytes` field that decode never validates.
1080        /// The release path already falls back to an empty body; a
1081        /// `debug_assert!` on the same condition makes a single malformed
1082        /// byte a panic wherever debug assertions are on — which this test
1083        /// suite has, so it fails here.
1084        #[test]
1085        fn corrupt_any_value_bytes_encode_to_text_without_panicking() {
1086            with_registry(|| {
1087                let a = Any {
1088                    type_url: Duration::TYPE_URL.to_string(),
1089                    // 0xFF is a varint continuation byte with nothing after
1090                    // it: a truncated field, not a valid Duration.
1091                    value: bytes::Bytes::from_static(&[0xFF]),
1092                    ..Default::default()
1093                };
1094                assert_eq!(
1095                    buffa::text::encode_to_string(&a),
1096                    "[type.googleapis.com/google.protobuf.Duration] {}",
1097                    "the expansion is still emitted, with an empty body"
1098                );
1099            });
1100        }
1101
1102        #[test]
1103        fn deserialize_rejects_non_string_type() {
1104            // @type as a non-string value → error.
1105            let json = r#"{"@type": 123}"#;
1106            let err = serde_json::from_str::<Any>(json).unwrap_err();
1107            assert!(err.to_string().contains("@type must be a string"), "{err}");
1108        }
1109
1110        /// An `Any` chain `depth` levels deep.
1111        fn any_chain(depth: usize) -> Any {
1112            let mut cur = Any::default();
1113            for _ in 0..depth {
1114                cur = Any {
1115                    type_url: Any::TYPE_URL.to_string(),
1116                    value: buffa::Message::encode_to_vec(&cur).into(),
1117                    ..Default::default()
1118                };
1119            }
1120            cur
1121        }
1122
1123        #[test]
1124        fn a_shallow_any_chain_still_expands() {
1125            with_registry(|| {
1126                let json = serde_json::to_string(&any_chain(8)).expect("well within the cap");
1127                // Eight expansions, so eight nested "@type" keys.
1128                assert_eq!(json.matches("\"@type\"").count(), 8, "{json}");
1129            });
1130        }
1131
1132        #[test]
1133        fn a_deep_any_chain_is_refused_rather_than_recursed() {
1134            with_registry(|| {
1135                // Deep enough to overflow a worker-sized stack if unbounded:
1136                // the decode below is cheap and shallow either way.
1137                let deep = any_chain(usize::try_from(MAX_ANY_EXPANSION_DEPTH).unwrap() + 5);
1138                let wire = buffa::Message::encode_to_vec(&deep);
1139                let decoded = <Any as buffa::Message>::decode_from_slice(&wire)
1140                    .expect("decoding the chain is one level deep and always succeeds");
1141
1142                let err = serde_json::to_string(&decoded)
1143                    .expect_err("expansion past the cap must be an error, not a deeper stack");
1144                assert!(
1145                    err.to_string()
1146                        .contains(&alloc::format!("{MAX_ANY_EXPANSION_DEPTH} levels")),
1147                    "the error should state the limit it hit, as a number the \
1148                     reader can act on rather than a constant to go look up: {err}"
1149                );
1150            });
1151        }
1152
1153        #[test]
1154        fn the_expansion_depth_is_restored_after_a_refusal() {
1155            with_registry(|| {
1156                let deep = any_chain(usize::try_from(MAX_ANY_EXPANSION_DEPTH).unwrap() + 5);
1157                assert!(serde_json::to_string(&deep).is_err());
1158
1159                // The counter is ambient, so a refusal that failed to unwind
1160                // it would leave this thread unable to serialize any `Any`
1161                // again — a far worse outcome than the rejection itself.
1162                let json = serde_json::to_string(&any_chain(4))
1163                    .expect("a rejected serialization must not poison the thread");
1164                assert_eq!(json.matches("\"@type\"").count(), 4, "{json}");
1165            });
1166        }
1167
1168        #[test]
1169        fn a_deep_any_chain_falls_back_to_the_vanilla_text_form() {
1170            with_registry(|| {
1171                let deep = any_chain(usize::try_from(MAX_ANY_EXPANSION_DEPTH).unwrap() + 5);
1172                // Textproto has no error channel here beyond a writer failure,
1173                // so past the cap the encoder emits the unexpanded
1174                // `type_url`/`value` form: still valid textproto, and finite.
1175                let text = buffa::text::encode_to_string(&deep);
1176                assert!(
1177                    text.contains("type_url:"),
1178                    "the innermost levels must fall back to the vanilla form: {text}"
1179                );
1180                // Count the expansion bracket itself, not bare `[` — a length
1181                // prefix of 0x5B inside the escaped `value` bytes renders as a
1182                // literal `[` and would inflate a looser count. Exact, so a
1183                // regression that expands one level too many also fails.
1184                assert_eq!(
1185                    text.matches("[type.googleapis.com/google.protobuf.Any]")
1186                        .count(),
1187                    usize::try_from(MAX_ANY_EXPANSION_DEPTH).unwrap(),
1188                    "expansion should stop at exactly the cap"
1189                );
1190            });
1191        }
1192    }
1193}