Skip to main content

microsandbox_protocol/
wire.rs

1//! Protocol-independent envelopes. Raw routing does not need to decode these.
2
3use std::collections::HashSet;
4use std::io::Cursor;
5
6use ciborium::Value;
7use serde::{Deserialize, Serialize, de::DeserializeOwned};
8use thiserror::Error;
9
10use crate::codec::{MAX_FRAME_SIZE, RawFrame};
11
12//--------------------------------------------------------------------------------------------------
13// Types
14//--------------------------------------------------------------------------------------------------
15
16/// A decoded envelope without a closed application-message enum.
17///
18/// Keep the original frame when forwarding: re-encoding this view would discard
19/// unknown envelope fields. Debug output deliberately omits the payload.
20#[derive(Clone, Serialize, Deserialize)]
21pub struct Envelope {
22    /// Protocol generation.
23    pub v: u8,
24    /// Application-defined wire name, including unknown future names.
25    pub t: String,
26    /// Independently CBOR-encoded payload bytes.
27    #[serde(with = "serde_bytes")]
28    pub p: Vec<u8>,
29}
30
31/// Errors from checked envelope/payload decoding, with no untrusted values.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
33pub enum WireError {
34    /// The input is not exactly one complete CBOR value.
35    #[error("invalid or trailing CBOR data")]
36    InvalidCbor,
37    /// Input exceeds the outer frame's byte limit.
38    #[error("CBOR data exceeds the frame limit")]
39    TooLarge,
40    /// A checked record does not match its field contract.
41    #[error("invalid protocol record")]
42    InvalidRecord,
43    /// Duplicate keys make a checked record ambiguous.
44    #[error("duplicate protocol record key")]
45    DuplicateKey,
46    /// A native payload could not be serialized.
47    #[error("could not encode protocol record")]
48    Encode,
49}
50
51//--------------------------------------------------------------------------------------------------
52// Methods
53//--------------------------------------------------------------------------------------------------
54
55impl Envelope {
56    /// Encode a native payload while retaining an explicit wire name.
57    pub fn new(v: u8, t: impl Into<String>, payload: &impl Serialize) -> Result<Self, WireError> {
58        Ok(Self {
59            v,
60            t: t.into(),
61            p: encode(payload)?,
62        })
63    }
64
65    /// Encode just the envelope, preserving its already-encoded payload.
66    pub fn encode(&self) -> Result<Vec<u8>, WireError> {
67        encode(self)
68    }
69
70    /// Decode an envelope without restricting application message names.
71    pub fn decode(bytes: &[u8]) -> Result<Self, WireError> {
72        let value = decode_value(bytes)?;
73        validate_record(&value)?;
74        let Value::Map(fields) = &value else {
75            unreachable!()
76        };
77        // serde_bytes intentionally accepts integer arrays too. The actual wire
78        // envelope requires a CBOR byte string, so enforce that before serde.
79        if !fields
80            .iter()
81            .any(|(key, value)| key.as_text() == Some("p") && matches!(value, Value::Bytes(_)))
82        {
83            return Err(WireError::InvalidRecord);
84        }
85        value.deserialized().map_err(|_| WireError::InvalidRecord)
86    }
87
88    /// Decode the payload as a checked record. Never required for raw routing.
89    pub fn payload<T: DeserializeOwned>(&self) -> Result<T, WireError> {
90        decode_record(&self.p)
91    }
92
93    /// Attach caller-selected frame routing metadata.
94    pub fn frame(&self, id: u32, flags: u8) -> Result<RawFrame, WireError> {
95        Ok(RawFrame {
96            id,
97            flags,
98            body: self.encode()?,
99        })
100    }
101}
102
103//--------------------------------------------------------------------------------------------------
104// Trait Implementations
105//--------------------------------------------------------------------------------------------------
106
107impl std::fmt::Debug for Envelope {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        f.debug_struct("Envelope")
110            .field("generation", &self.v)
111            .field("payload_bytes", &self.p.len())
112            .finish_non_exhaustive()
113    }
114}
115
116//--------------------------------------------------------------------------------------------------
117// Functions
118//--------------------------------------------------------------------------------------------------
119
120/// Serialize a record without including serializer diagnostics in errors.
121pub fn encode(value: &impl Serialize) -> Result<Vec<u8>, WireError> {
122    let mut bytes = Vec::new();
123    ciborium::ser::into_writer(value, &mut bytes).map_err(|_| WireError::Encode)?;
124    if bytes.len() > MAX_FRAME_SIZE as usize {
125        return Err(WireError::TooLarge);
126    }
127    Ok(bytes)
128}
129
130/// Decode exactly one record, rejecting duplicate keys and trailing data.
131///
132/// Unknown fields are ignored by the target type. Their values are not
133/// interpreted as application records; this permits future extension values.
134pub fn decode_record<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, WireError> {
135    let value = decode_value(bytes)?;
136    validate_record(&value)?;
137    value.deserialized().map_err(|_| WireError::InvalidRecord)
138}
139
140/// Decode one CBOR value with the same outer byte bound as framing.
141pub fn decode_value(bytes: &[u8]) -> Result<Value, WireError> {
142    if bytes.len() > MAX_FRAME_SIZE as usize {
143        return Err(WireError::TooLarge);
144    }
145    let mut reader = Cursor::new(bytes);
146    let value = ciborium::de::from_reader(&mut reader).map_err(|_| WireError::InvalidCbor)?;
147    if reader.position() != bytes.len() as u64 {
148        return Err(WireError::InvalidCbor);
149    }
150    Ok(value)
151}
152
153/// Validate keys of a single record without interpreting unknown field values.
154pub fn validate_record(value: &Value) -> Result<(), WireError> {
155    let Value::Map(fields) = value else {
156        return Err(WireError::InvalidRecord);
157    };
158    let mut names = HashSet::with_capacity(fields.len());
159    for (key, _) in fields {
160        let Value::Text(name) = key else {
161            return Err(WireError::InvalidRecord);
162        };
163        if !names.insert(name.as_str()) {
164            return Err(WireError::DuplicateKey);
165        }
166    }
167    Ok(())
168}