use std::any::type_name;
use std::collections::BTreeMap;
use std::sync::Arc;
use bytes::Bytes;
use ruststream::HeaderMap;
use ruststream::codec::Codec;
use serde::{Deserialize, Serialize};
pub(crate) type SharedEnvelope = Arc<dyn EnvelopeCodec>;
pub(crate) trait EnvelopeCodec: Send + Sync {
fn encode(&self, payload: &[u8], headers: &HeaderMap) -> Vec<u8>;
fn decode(&self, bytes: &[u8]) -> (Bytes, HeaderMap);
fn framing(&self) -> &'static str;
#[cfg(feature = "asyncapi")]
fn content_type(&self) -> &'static str;
}
impl<C: Codec> EnvelopeCodec for C {
fn framing(&self) -> &'static str {
type_name::<C>()
}
#[cfg(feature = "asyncapi")]
fn content_type(&self) -> &'static str {
C::CONTENT_TYPE
}
fn encode(&self, payload: &[u8], headers: &HeaderMap) -> Vec<u8> {
let envelope = Envelope::from_parts(payload, headers);
Codec::encode(self, &envelope)
.map_or_else(|_| binary_encode(payload, headers), |b| b.to_vec())
}
fn decode(&self, bytes: &[u8]) -> (Bytes, HeaderMap) {
Codec::decode::<Envelope>(self, bytes)
.map_or_else(|_| binary_decode(bytes), Envelope::into_parts)
}
}
pub(crate) fn frame(
codec: Option<&SharedEnvelope>,
payload: &[u8],
headers: &HeaderMap,
) -> Vec<u8> {
codec.map_or_else(
|| binary_encode(payload, headers),
|codec| codec.encode(payload, headers),
)
}
pub(crate) fn unframe(codec: Option<&SharedEnvelope>, bytes: &[u8]) -> (Bytes, HeaderMap) {
codec.map_or_else(|| binary_decode(bytes), |codec| codec.decode(bytes))
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum Content {
Text(String),
Bytes(Vec<u8>),
}
impl Content {
fn from_bytes(bytes: &[u8]) -> Self {
std::str::from_utf8(bytes).map_or_else(
|_| Self::Bytes(bytes.to_vec()),
|text| Self::Text(text.to_owned()),
)
}
fn into_bytes(self) -> Bytes {
match self {
Self::Text(text) => Bytes::from(text.into_bytes()),
Self::Bytes(bytes) => Bytes::from(bytes),
}
}
}
#[derive(Serialize, Deserialize)]
struct Envelope {
#[serde(default)]
headers: BTreeMap<String, Content>,
payload: Content,
}
impl Envelope {
fn from_parts(payload: &[u8], headers: &HeaderMap) -> Self {
let headers = headers
.iter()
.map(|(name, value)| (name.to_string(), Content::from_bytes(value)))
.collect();
Self {
headers,
payload: Content::from_bytes(payload),
}
}
fn into_parts(self) -> (Bytes, HeaderMap) {
let mut headers = HeaderMap::new();
for (name, value) in self.headers {
headers.insert(name, value.into_bytes());
}
(self.payload.into_bytes(), headers)
}
}
fn len_prefix(n: usize) -> [u8; 4] {
u32::try_from(n).unwrap_or(u32::MAX).to_be_bytes()
}
fn binary_encode(payload: &[u8], headers: &HeaderMap) -> Vec<u8> {
let mut buf = Vec::with_capacity(4 + payload.len());
buf.extend_from_slice(&len_prefix(headers.len()));
for (name, value) in headers.iter() {
let name = name.as_bytes();
buf.extend_from_slice(&len_prefix(name.len()));
buf.extend_from_slice(name);
buf.extend_from_slice(&len_prefix(value.len()));
buf.extend_from_slice(value);
}
buf.extend_from_slice(payload);
buf
}
fn binary_decode(bytes: &[u8]) -> (Bytes, HeaderMap) {
try_binary_decode(bytes).unwrap_or_else(|| (Bytes::copy_from_slice(bytes), HeaderMap::new()))
}
fn read_u32(bytes: &[u8], pos: &mut usize) -> Option<usize> {
let end = pos.checked_add(4)?;
let raw = bytes.get(*pos..end)?;
*pos = end;
Some(u32::from_be_bytes(raw.try_into().ok()?) as usize)
}
fn read_slice<'a>(bytes: &'a [u8], pos: &mut usize, len: usize) -> Option<&'a [u8]> {
let end = pos.checked_add(len)?;
let slice = bytes.get(*pos..end)?;
*pos = end;
Some(slice)
}
fn try_binary_decode(bytes: &[u8]) -> Option<(Bytes, HeaderMap)> {
let mut pos = 0;
let count = read_u32(bytes, &mut pos)?;
let mut headers = HeaderMap::new();
for _ in 0..count {
let name_len = read_u32(bytes, &mut pos)?;
let name = read_slice(bytes, &mut pos, name_len)?;
let name = std::str::from_utf8(name).ok()?;
let value_len = read_u32(bytes, &mut pos)?;
let value = read_slice(bytes, &mut pos, value_len)?;
headers.insert(name.to_owned(), Bytes::copy_from_slice(value));
}
Some((Bytes::copy_from_slice(&bytes[pos..]), headers))
}
#[cfg(test)]
mod tests {
use super::*;
use ruststream::codec::{CborCodec, JsonCodec, MsgpackCodec};
fn sample_headers() -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert("content-type", "application/json");
headers.insert("correlation-id", "abc-1");
headers
}
#[test]
fn binary_round_trips() {
let framed = frame(None, b"{}", &sample_headers());
let (payload, decoded) = unframe(None, &framed);
assert_eq!(payload.as_ref(), b"{}");
assert_eq!(decoded.content_type(), Some("application/json"));
assert_eq!(decoded.correlation_id(), Some("abc-1"));
}
#[test]
fn binary_raw_value_falls_back_to_payload() {
let (payload, headers) = unframe(None, b"hi");
assert_eq!(payload.as_ref(), b"hi");
assert_eq!(headers, HeaderMap::new());
}
#[test]
fn codec_round_trips_and_is_readable() {
let codec: SharedEnvelope = Arc::new(JsonCodec);
let framed = frame(Some(&codec), br#"{"id":1}"#, &sample_headers());
let text = String::from_utf8(framed.clone()).expect("utf8");
assert!(text.contains("\"payload\""));
assert!(text.contains("application/json"));
let (payload, decoded) = unframe(Some(&codec), &framed);
assert_eq!(payload.as_ref(), br#"{"id":1}"#);
assert_eq!(decoded.content_type(), Some("application/json"));
}
#[test]
fn codec_decode_of_raw_value_falls_back() {
let codec: SharedEnvelope = Arc::new(JsonCodec);
let (payload, headers) = unframe(Some(&codec), b"not-json");
assert_eq!(payload.as_ref(), b"not-json");
assert_eq!(headers, HeaderMap::new());
}
#[test]
fn codec_round_trips_a_payload_that_is_not_text() {
let codec: SharedEnvelope = Arc::new(JsonCodec);
let blob: &[u8] = &[0xff, 0x00, 0x1f, 0xfe, b'{'];
let framed = frame(Some(&codec), blob, &sample_headers());
let (payload, decoded) = unframe(Some(&codec), &framed);
assert_eq!(payload.as_ref(), blob);
assert_eq!(decoded.content_type(), Some("application/json"));
}
#[test]
fn codec_round_trips_a_header_value_that_is_not_text() {
let codec: SharedEnvelope = Arc::new(JsonCodec);
let mut headers = HeaderMap::new();
headers.insert("signature", Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]));
let framed = frame(Some(&codec), b"{}", &headers);
let (payload, decoded) = unframe(Some(&codec), &framed);
assert_eq!(payload.as_ref(), b"{}");
assert_eq!(
decoded.get("signature").map(<[u8]>::to_vec),
Some(vec![0xde, 0xad, 0xbe, 0xef])
);
}
#[test]
fn a_text_envelope_keeps_its_wire_form() {
let codec: SharedEnvelope = Arc::new(JsonCodec);
let framed = frame(Some(&codec), br#"{"id":1}"#, &sample_headers());
let text = String::from_utf8(framed).expect("utf8");
assert!(text.contains(r#""payload":"{\"id\":1}""#), "{text}");
assert!(
text.contains(r#""content-type":"application/json""#),
"{text}"
);
}
#[test]
fn a_text_envelope_from_an_earlier_version_still_decodes() {
let codec: SharedEnvelope = Arc::new(JsonCodec);
let wire = br#"{"headers":{"content-type":"application/json"},"payload":"{\"id\":1}"}"#;
let (payload, decoded) = unframe(Some(&codec), wire);
assert_eq!(payload.as_ref(), br#"{"id":1}"#);
assert_eq!(decoded.content_type(), Some("application/json"));
}
#[test]
fn a_codec_subscriber_reads_a_binary_frame() {
let codec: SharedEnvelope = Arc::new(JsonCodec);
let framed = frame(None, b"{}", &sample_headers());
let (payload, decoded) = unframe(Some(&codec), &framed);
assert_eq!(payload.as_ref(), b"{}");
assert_eq!(decoded.content_type(), Some("application/json"));
}
#[test]
fn every_codec_round_trips_bytes_that_are_not_text() {
let blob: &[u8] = &[0xff, 0x00, 0xfe];
for (name, codec) in [
("json", Arc::new(JsonCodec) as SharedEnvelope),
("cbor", Arc::new(CborCodec) as SharedEnvelope),
("msgpack", Arc::new(MsgpackCodec) as SharedEnvelope),
] {
let framed = frame(Some(&codec), blob, &sample_headers());
let (payload, decoded) = unframe(Some(&codec), &framed);
assert_eq!(
payload.as_ref(),
blob,
"the {name} envelope lost the payload"
);
assert_eq!(
decoded.content_type(),
Some("application/json"),
"the {name} envelope lost a header"
);
}
}
}