Skip to main content

grpc_codec_prost/
lib.rs

1//! Protocol Buffers serialization for gRPC message payloads using Prost.
2//!
3//! Messages are appended directly to the caller's buffer, without a staging
4//! buffer, a Protobuf length delimiter, or a gRPC frame header. Decoding follows
5//! Protobuf rules, including acceptance of well-formed unknown fields.
6
7use std::marker::PhantomData;
8
9use grpc_core::codec::Codec;
10/// Serialization backend used by this adapter.
11pub use prost as backend;
12use prost::Message;
13
14/// Encodes `E` and decodes `D` as individual Protobuf payloads.
15///
16/// The directions may use different message types, as is usual for RPC requests
17/// and responses. The adapter holds no message data between calls; callers may
18/// reuse both the codec and the outbound buffer across messages.
19#[derive(Debug)]
20pub struct ProstCodec<E, D> {
21    message_types: PhantomData<fn(E) -> D>,
22}
23
24impl<E, D> ProstCodec<E, D> {
25    /// HTTP content type emitted for Protobuf messages.
26    pub const CONTENT_TYPE: &'static str = "application/grpc";
27
28    /// Creates a codec for the specified outbound and inbound message types.
29    #[must_use]
30    pub const fn new() -> Self {
31        Self {
32            message_types: PhantomData,
33        }
34    }
35
36    /// Accepts the default gRPC media type and its explicit Protobuf variant.
37    ///
38    /// Media types are compared case-insensitively, ignoring surrounding HTTP
39    /// whitespace and optional media type parameters.
40    #[must_use]
41    pub fn accepts_content_type(content_type: &str) -> bool {
42        let media_type = content_type
43            .split_once(';')
44            .map_or(content_type, |(media_type, _)| media_type)
45            .trim_matches([' ', '\t']);
46        media_type.eq_ignore_ascii_case(Self::CONTENT_TYPE)
47            || media_type.eq_ignore_ascii_case("application/grpc+proto")
48    }
49}
50
51impl<E, D> Default for ProstCodec<E, D> {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl<E: Message, D: Message + Default> Codec for ProstCodec<E, D> {
58    type EncodeItem = E;
59    type DecodeItem = D;
60    type Error = ProstCodecError;
61
62    const CONTENT_TYPE: &'static str = Self::CONTENT_TYPE;
63
64    fn encode(&mut self, item: &E, destination: &mut Vec<u8>) -> Result<(), Self::Error> {
65        item.encode(destination).map_err(ProstCodecError::Encode)
66    }
67
68    fn decode(&mut self, payload: &[u8]) -> Result<D, Self::Error> {
69        D::decode(payload).map_err(ProstCodecError::Decode)
70    }
71}
72
73/// A Protobuf serialization failure, preserving the backend error as its source.
74#[derive(Debug, thiserror::Error)]
75pub enum ProstCodecError {
76    /// The outbound message could not be encoded into the destination.
77    #[error("Protobuf encoding failed: {0}")]
78    Encode(#[from] prost::EncodeError),
79    /// The inbound payload is not a valid message of the requested type.
80    #[error("Protobuf decoding failed: {0}")]
81    Decode(#[from] prost::DecodeError),
82}
83
84#[cfg(test)]
85mod unit {
86    use std::error::Error;
87
88    use super::*;
89
90    #[test]
91    fn scenario_encode_appends_wire_payload_to_reusable_buffer() {
92        let mut codec = ProstCodec::<String, String>::new();
93        let mut destination = Vec::with_capacity(64);
94        destination.extend_from_slice(&[0xaa, 0xbb]);
95        let pointer = destination.as_ptr();
96        let capacity = destination.capacity();
97
98        codec
99            .encode(&String::from("hello"), &mut destination)
100            .unwrap();
101
102        assert_eq!(destination, b"\xaa\xbb\x0a\x05hello");
103        assert_eq!(destination.as_ptr(), pointer);
104        assert_eq!(destination.capacity(), capacity);
105        assert_eq!(codec.decode(&destination[2..]).unwrap(), "hello");
106
107        destination.clear();
108        codec
109            .encode(&String::from("next"), &mut destination)
110            .unwrap();
111        assert_eq!(destination, b"\x0a\x04next");
112        assert_eq!(destination.as_ptr(), pointer);
113        assert_eq!(codec.decode(&destination).unwrap(), "next");
114    }
115
116    #[test]
117    fn scenario_encode_grows_destination_without_capacity_requirement() {
118        let mut codec = ProstCodec::<Vec<u8>, Vec<u8>>::new();
119        let value = vec![0xff; 512];
120        let mut destination = Vec::new();
121        codec.encode(&value, &mut destination).unwrap();
122        assert_eq!(destination, value.encode_to_vec());
123        assert_eq!(codec.decode(&destination).unwrap(), value);
124    }
125
126    #[test]
127    fn scenario_request_and_response_types_are_independent() {
128        let mut codec = ProstCodec::<String, u32>::default();
129        let mut destination = Vec::new();
130        codec
131            .encode(&String::from("request"), &mut destination)
132            .unwrap();
133        assert_eq!(destination, b"\x0a\x07request");
134        assert_eq!(codec.decode(&[0x08, 0x96, 0x01]).unwrap(), 150);
135    }
136
137    #[test]
138    fn scenario_empty_message_has_empty_payload() {
139        let mut codec = ProstCodec::<(), ()>::new();
140        let mut destination = vec![0xaa];
141        codec.encode(&(), &mut destination).unwrap();
142        assert_eq!(destination, [0xaa]);
143        codec.decode(&[]).unwrap();
144    }
145
146    #[test]
147    fn scenario_decode_preserves_protobuf_unknown_field_semantics() {
148        let mut codec = ProstCodec::<String, String>::new();
149        assert_eq!(codec.decode(b"\x0a\x02ok\x10\x01").unwrap(), "ok");
150        assert_eq!(codec.decode(b"\x10\x01").unwrap(), "");
151    }
152
153    #[test]
154    fn scenario_unknown_wire_types_do_not_hide_later_known_fields() {
155        let mut codec = ProstCodec::<String, String>::new();
156        let payload = b"\x0a\x03old\x11abcdefgh\x1dijkl\x22\x03xyz\x28\x96\x01\x0a\x03new";
157        assert_eq!(codec.decode(payload).unwrap(), "new");
158
159        for truncated_unknown in [
160            &b"\x11abcdefg"[..],
161            &b"\x1dabc"[..],
162            &b"\x22\x04xyz"[..],
163            &b"\x28\x80"[..],
164        ] {
165            let mut payload = b"\x0a\x02ok".to_vec();
166            payload.extend_from_slice(truncated_unknown);
167            assert!(codec.decode(&payload).is_err());
168        }
169    }
170
171    #[test]
172    fn scenario_decode_rejects_malformed_payloads_and_trailing_bytes() {
173        let mut codec = ProstCodec::<String, String>::new();
174        for payload in [
175            &b"\x0a\x05short\x00"[..],
176            &b"\x0a\x05bad"[..],
177            &b"\x0a\x01\xff"[..],
178            &b"\x08\x01"[..],
179            &b"\x80"[..],
180        ] {
181            let error = codec.decode(payload).unwrap_err();
182            assert!(matches!(error, ProstCodecError::Decode(_)));
183            assert!(error.source().unwrap().is::<prost::DecodeError>());
184        }
185    }
186
187    #[test]
188    fn scenario_decode_failure_does_not_contaminate_later_messages() {
189        let mut codec = ProstCodec::<String, String>::new();
190        assert!(codec.decode(b"\x0a\x05bad").is_err());
191        assert_eq!(codec.decode(b"\x0a\x02ok").unwrap(), "ok");
192        assert_eq!(codec.decode(&[]).unwrap(), "");
193    }
194
195    #[test]
196    fn scenario_encode_errors_preserve_backend_details() {
197        let mut empty: &mut [u8] = &mut [];
198        let backend_error = String::from("value").encode(&mut empty).unwrap_err();
199        let error = ProstCodecError::from(backend_error);
200        let ProstCodecError::Encode(inner) = &error else {
201            panic!("expected an encoding error");
202        };
203        assert_eq!(inner.required_capacity(), 7);
204        assert_eq!(inner.remaining(), 0);
205        assert!(error.source().unwrap().is::<prost::EncodeError>());
206        assert!(error.to_string().contains(&backend_error.to_string()));
207    }
208
209    #[test]
210    fn scenario_decode_errors_preserve_backend_details() {
211        let backend_error = String::decode(&b"\x00"[..]).unwrap_err();
212        let description = backend_error.to_string();
213        let error = ProstCodecError::from(backend_error);
214        assert!(matches!(error, ProstCodecError::Decode(_)));
215        assert!(error.source().unwrap().is::<prost::DecodeError>());
216        assert!(error.to_string().contains(&description));
217    }
218
219    #[test]
220    fn scenario_content_type_accepts_only_protobuf_media_types() {
221        for value in [
222            "application/grpc",
223            "application/grpc+proto",
224            "Application/GRPC+Proto",
225            " application/grpc\t; charset=utf-8",
226        ] {
227            assert!(ProstCodec::<String, String>::accepts_content_type(value));
228        }
229        for value in [
230            "",
231            "application/grpc+capnp",
232            "application/grpc-web+proto",
233            "application/grpc+proto-extra",
234            "application/grpcsuffix",
235            "application/grpc, application/json",
236            "application/grpc\r\n",
237        ] {
238            assert!(!ProstCodec::<String, String>::accepts_content_type(value));
239        }
240    }
241}