use std::marker::PhantomData;
use grpc_core::codec::Codec;
pub use prost as backend;
use prost::Message;
#[derive(Debug)]
pub struct ProstCodec<E, D> {
message_types: PhantomData<fn(E) -> D>,
}
impl<E, D> ProstCodec<E, D> {
pub const CONTENT_TYPE: &'static str = "application/grpc";
#[must_use]
pub const fn new() -> Self {
Self {
message_types: PhantomData,
}
}
#[must_use]
pub fn accepts_content_type(content_type: &str) -> bool {
let media_type = content_type
.split_once(';')
.map_or(content_type, |(media_type, _)| media_type)
.trim_matches([' ', '\t']);
media_type.eq_ignore_ascii_case(Self::CONTENT_TYPE)
|| media_type.eq_ignore_ascii_case("application/grpc+proto")
}
}
impl<E, D> Default for ProstCodec<E, D> {
fn default() -> Self {
Self::new()
}
}
impl<E: Message, D: Message + Default> Codec for ProstCodec<E, D> {
type EncodeItem = E;
type DecodeItem = D;
type Error = ProstCodecError;
const CONTENT_TYPE: &'static str = Self::CONTENT_TYPE;
fn encode(&mut self, item: &E, destination: &mut Vec<u8>) -> Result<(), Self::Error> {
item.encode(destination).map_err(ProstCodecError::Encode)
}
fn decode(&mut self, payload: &[u8]) -> Result<D, Self::Error> {
D::decode(payload).map_err(ProstCodecError::Decode)
}
}
#[derive(Debug, thiserror::Error)]
pub enum ProstCodecError {
#[error("Protobuf encoding failed: {0}")]
Encode(#[from] prost::EncodeError),
#[error("Protobuf decoding failed: {0}")]
Decode(#[from] prost::DecodeError),
}
#[cfg(test)]
mod unit {
use std::error::Error;
use super::*;
#[test]
fn scenario_encode_appends_wire_payload_to_reusable_buffer() {
let mut codec = ProstCodec::<String, String>::new();
let mut destination = Vec::with_capacity(64);
destination.extend_from_slice(&[0xaa, 0xbb]);
let pointer = destination.as_ptr();
let capacity = destination.capacity();
codec
.encode(&String::from("hello"), &mut destination)
.unwrap();
assert_eq!(destination, b"\xaa\xbb\x0a\x05hello");
assert_eq!(destination.as_ptr(), pointer);
assert_eq!(destination.capacity(), capacity);
assert_eq!(codec.decode(&destination[2..]).unwrap(), "hello");
destination.clear();
codec
.encode(&String::from("next"), &mut destination)
.unwrap();
assert_eq!(destination, b"\x0a\x04next");
assert_eq!(destination.as_ptr(), pointer);
assert_eq!(codec.decode(&destination).unwrap(), "next");
}
#[test]
fn scenario_encode_grows_destination_without_capacity_requirement() {
let mut codec = ProstCodec::<Vec<u8>, Vec<u8>>::new();
let value = vec![0xff; 512];
let mut destination = Vec::new();
codec.encode(&value, &mut destination).unwrap();
assert_eq!(destination, value.encode_to_vec());
assert_eq!(codec.decode(&destination).unwrap(), value);
}
#[test]
fn scenario_request_and_response_types_are_independent() {
let mut codec = ProstCodec::<String, u32>::default();
let mut destination = Vec::new();
codec
.encode(&String::from("request"), &mut destination)
.unwrap();
assert_eq!(destination, b"\x0a\x07request");
assert_eq!(codec.decode(&[0x08, 0x96, 0x01]).unwrap(), 150);
}
#[test]
fn scenario_empty_message_has_empty_payload() {
let mut codec = ProstCodec::<(), ()>::new();
let mut destination = vec![0xaa];
codec.encode(&(), &mut destination).unwrap();
assert_eq!(destination, [0xaa]);
codec.decode(&[]).unwrap();
}
#[test]
fn scenario_decode_preserves_protobuf_unknown_field_semantics() {
let mut codec = ProstCodec::<String, String>::new();
assert_eq!(codec.decode(b"\x0a\x02ok\x10\x01").unwrap(), "ok");
assert_eq!(codec.decode(b"\x10\x01").unwrap(), "");
}
#[test]
fn scenario_unknown_wire_types_do_not_hide_later_known_fields() {
let mut codec = ProstCodec::<String, String>::new();
let payload = b"\x0a\x03old\x11abcdefgh\x1dijkl\x22\x03xyz\x28\x96\x01\x0a\x03new";
assert_eq!(codec.decode(payload).unwrap(), "new");
for truncated_unknown in [
&b"\x11abcdefg"[..],
&b"\x1dabc"[..],
&b"\x22\x04xyz"[..],
&b"\x28\x80"[..],
] {
let mut payload = b"\x0a\x02ok".to_vec();
payload.extend_from_slice(truncated_unknown);
assert!(codec.decode(&payload).is_err());
}
}
#[test]
fn scenario_decode_rejects_malformed_payloads_and_trailing_bytes() {
let mut codec = ProstCodec::<String, String>::new();
for payload in [
&b"\x0a\x05short\x00"[..],
&b"\x0a\x05bad"[..],
&b"\x0a\x01\xff"[..],
&b"\x08\x01"[..],
&b"\x80"[..],
] {
let error = codec.decode(payload).unwrap_err();
assert!(matches!(error, ProstCodecError::Decode(_)));
assert!(error.source().unwrap().is::<prost::DecodeError>());
}
}
#[test]
fn scenario_decode_failure_does_not_contaminate_later_messages() {
let mut codec = ProstCodec::<String, String>::new();
assert!(codec.decode(b"\x0a\x05bad").is_err());
assert_eq!(codec.decode(b"\x0a\x02ok").unwrap(), "ok");
assert_eq!(codec.decode(&[]).unwrap(), "");
}
#[test]
fn scenario_encode_errors_preserve_backend_details() {
let mut empty: &mut [u8] = &mut [];
let backend_error = String::from("value").encode(&mut empty).unwrap_err();
let error = ProstCodecError::from(backend_error);
let ProstCodecError::Encode(inner) = &error else {
panic!("expected an encoding error");
};
assert_eq!(inner.required_capacity(), 7);
assert_eq!(inner.remaining(), 0);
assert!(error.source().unwrap().is::<prost::EncodeError>());
assert!(error.to_string().contains(&backend_error.to_string()));
}
#[test]
fn scenario_decode_errors_preserve_backend_details() {
let backend_error = String::decode(&b"\x00"[..]).unwrap_err();
let description = backend_error.to_string();
let error = ProstCodecError::from(backend_error);
assert!(matches!(error, ProstCodecError::Decode(_)));
assert!(error.source().unwrap().is::<prost::DecodeError>());
assert!(error.to_string().contains(&description));
}
#[test]
fn scenario_content_type_accepts_only_protobuf_media_types() {
for value in [
"application/grpc",
"application/grpc+proto",
"Application/GRPC+Proto",
" application/grpc\t; charset=utf-8",
] {
assert!(ProstCodec::<String, String>::accepts_content_type(value));
}
for value in [
"",
"application/grpc+capnp",
"application/grpc-web+proto",
"application/grpc+proto-extra",
"application/grpcsuffix",
"application/grpc, application/json",
"application/grpc\r\n",
] {
assert!(!ProstCodec::<String, String>::accepts_content_type(value));
}
}
}