use std::collections::HashSet;
use std::io::Cursor;
use ciborium::Value;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use thiserror::Error;
use crate::codec::{MAX_FRAME_SIZE, RawFrame};
#[derive(Clone, Serialize, Deserialize)]
pub struct Envelope {
pub v: u8,
pub t: String,
#[serde(with = "serde_bytes")]
pub p: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum WireError {
#[error("invalid or trailing CBOR data")]
InvalidCbor,
#[error("CBOR data exceeds the frame limit")]
TooLarge,
#[error("invalid protocol record")]
InvalidRecord,
#[error("duplicate protocol record key")]
DuplicateKey,
#[error("could not encode protocol record")]
Encode,
}
impl Envelope {
pub fn new(v: u8, t: impl Into<String>, payload: &impl Serialize) -> Result<Self, WireError> {
Ok(Self {
v,
t: t.into(),
p: encode(payload)?,
})
}
pub fn encode(&self) -> Result<Vec<u8>, WireError> {
encode(self)
}
pub fn decode(bytes: &[u8]) -> Result<Self, WireError> {
let value = decode_value(bytes)?;
validate_record(&value)?;
let Value::Map(fields) = &value else {
unreachable!()
};
if !fields
.iter()
.any(|(key, value)| key.as_text() == Some("p") && matches!(value, Value::Bytes(_)))
{
return Err(WireError::InvalidRecord);
}
value.deserialized().map_err(|_| WireError::InvalidRecord)
}
pub fn payload<T: DeserializeOwned>(&self) -> Result<T, WireError> {
decode_record(&self.p)
}
pub fn frame(&self, id: u32, flags: u8) -> Result<RawFrame, WireError> {
Ok(RawFrame {
id,
flags,
body: self.encode()?,
})
}
}
impl std::fmt::Debug for Envelope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Envelope")
.field("generation", &self.v)
.field("payload_bytes", &self.p.len())
.finish_non_exhaustive()
}
}
pub fn encode(value: &impl Serialize) -> Result<Vec<u8>, WireError> {
let mut bytes = Vec::new();
ciborium::ser::into_writer(value, &mut bytes).map_err(|_| WireError::Encode)?;
if bytes.len() > MAX_FRAME_SIZE as usize {
return Err(WireError::TooLarge);
}
Ok(bytes)
}
pub fn decode_record<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, WireError> {
let value = decode_value(bytes)?;
validate_record(&value)?;
value.deserialized().map_err(|_| WireError::InvalidRecord)
}
pub fn decode_value(bytes: &[u8]) -> Result<Value, WireError> {
if bytes.len() > MAX_FRAME_SIZE as usize {
return Err(WireError::TooLarge);
}
let mut reader = Cursor::new(bytes);
let value = ciborium::de::from_reader(&mut reader).map_err(|_| WireError::InvalidCbor)?;
if reader.position() != bytes.len() as u64 {
return Err(WireError::InvalidCbor);
}
Ok(value)
}
pub fn validate_record(value: &Value) -> Result<(), WireError> {
let Value::Map(fields) = value else {
return Err(WireError::InvalidRecord);
};
let mut names = HashSet::with_capacity(fields.len());
for (key, _) in fields {
let Value::Text(name) = key else {
return Err(WireError::InvalidRecord);
};
if !names.insert(name.as_str()) {
return Err(WireError::DuplicateKey);
}
}
Ok(())
}