1use std::marker::PhantomData;
8
9use grpc_core::codec::Codec;
10pub use prost as backend;
12use prost::Message;
13
14#[derive(Debug)]
20pub struct ProstCodec<E, D> {
21 message_types: PhantomData<fn(E) -> D>,
22}
23
24impl<E, D> ProstCodec<E, D> {
25 pub const CONTENT_TYPE: &'static str = "application/grpc";
27
28 #[must_use]
30 pub const fn new() -> Self {
31 Self {
32 message_types: PhantomData,
33 }
34 }
35
36 #[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#[derive(Debug, thiserror::Error)]
75pub enum ProstCodecError {
76 #[error("Protobuf encoding failed: {0}")]
78 Encode(#[from] prost::EncodeError),
79 #[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}