1#[cfg(not(feature = "std"))]
2extern crate alloc;
3
4use crate::{Frame, Message};
5
6#[cfg(feature = "derive")]
7use crate::Errorizable;
8
9#[cfg(not(feature = "std"))]
12pub use alloc::sync::Arc;
13#[cfg(feature = "std")]
14pub use std::sync::Arc;
15
16pub type Result<T> = core::result::Result<T, RouterError>;
17
18#[cfg_attr(feature = "derive", derive(Errorizable))]
19#[derive(Debug)]
20pub enum RouterError {
21 #[cfg_attr(feature = "derive", error("No route configured for provided message"))]
22 UnknownRoute,
23 #[cfg_attr(
24 feature = "derive",
25 error("Frame body failed to decode as the dispatched type: {0}")
26 )]
27 DecodeFailed(crate::der::Error),
28 #[cfg_attr(feature = "derive", error("Frame body is encrypted; decrypt before routing"))]
29 ConfidentialFrame,
30 #[cfg_attr(feature = "derive", error("Frame body is compressed; inflate before routing"))]
31 CompressedFrame,
32}
33
34crate::impl_error_display!(RouterError {
35 UnknownRoute => "No route configured for provided message",
36 DecodeFailed(source) => "Frame body failed to decode as the dispatched type: {source}",
37 ConfidentialFrame => "Frame body is encrypted; decrypt before routing",
38 CompressedFrame => "Frame body is compressed; inflate before routing",
39});
40
41crate::impl_from!(crate::der::Error => RouterError::DecodeFailed);
42
43pub fn ensure_cleartext(frame: &Frame) -> Result<()> {
56 if frame.metadata.confidentiality.is_some() {
57 return Err(RouterError::ConfidentialFrame);
58 }
59 if frame.metadata.compactness.is_some() {
60 return Err(RouterError::CompressedFrame);
61 }
62
63 Ok(())
64}
65
66pub trait RouterPolicy: Send + Sync {
67 fn dispatch<T: Message + Send + 'static>(&self, frame: Arc<Frame>) -> Result<()>;
88}
89
90#[macro_export]
97macro_rules! routes {
98 (
99 $RouterName:ident { $( $field:ident : $fty:ty ),* $(,)? } :
100 $(
101 $MsgTy:ty | $router:ident, $frame:ident, $msg:ident | $handler:block
102 )+
103 ) => {
104 struct $RouterName { $( $field : $fty ),* }
105
106 impl $crate::router::RouterPolicy for $RouterName {
107 fn dispatch<T: $crate::Message + Send + 'static>(
108 &self,
109 frame: $crate::router::Arc<$crate::Frame>,
110 ) -> $crate::router::Result<()> {
111 $crate::router::ensure_cleartext(&frame)?;
112 $(
113 if core::any::TypeId::of::<T>() == core::any::TypeId::of::<$MsgTy>() {
114 let decoded: $MsgTy = $crate::der::Decode::from_der(frame.message.as_slice())?;
115 let ($router, $frame, $msg) = (self, frame, decoded);
116 { $handler }
117 return Ok(());
118 }
119 )*
120 Err($crate::router::RouterError::UnknownRoute)
121 }
122 }
123 };
124}
125
126#[cfg(all(test, feature = "builder"))]
127mod tests {
128 use std::sync::{mpsc, Arc};
129 use std::time::Duration;
130
131 use crate::cms::compressed_data::CompressedData;
132 use crate::cms::content_info::CmsVersion;
133 use crate::cms::enveloped_data::EncryptedContentInfo;
134 use crate::cms::signed_data::EncapsulatedContentInfo;
135 use crate::der::asn1::OctetString;
136 use crate::der::{Decode, Encode, Sequence};
137 use crate::oids::{COMPRESSION_ZSTD, DATA};
138 use crate::router::{RouterError, RouterPolicy};
139 use crate::spki::AlgorithmIdentifier;
140 use crate::Beamable;
141 use crate::Frame;
142
143 #[derive(Beamable, Sequence, Clone, Debug, PartialEq)]
144 pub struct HealthCheck {
145 pub uptime: u64,
146 }
147
148 #[derive(Beamable, Sequence, Clone, Debug, PartialEq)]
149 pub struct Payment {
150 pub from: String,
151 pub amount: u64,
152 }
153
154 #[derive(Beamable, Sequence, Clone, Debug, PartialEq)]
155 pub struct Unrouted {
156 pub note: String,
157 }
158
159 type Delivery<T> = (Arc<Frame>, T);
160
161 routes! {
162 ChannelRouter {
163 payment_tx: mpsc::Sender<Delivery<Payment>>,
164 health_tx: mpsc::Sender<Delivery<HealthCheck>>,
165 }:
166 Payment |router, frame, msg| {
167 let _ = router.payment_tx.send((frame, msg));
168 }
169 HealthCheck |router, frame, msg| {
170 let _ = router.health_tx.send((frame, msg));
171 }
172 }
173
174 fn build_router() -> (
175 ChannelRouter,
176 mpsc::Receiver<Delivery<Payment>>,
177 mpsc::Receiver<Delivery<HealthCheck>>,
178 ) {
179 let (payment_tx, payment_rx) = mpsc::channel();
180 let (health_tx, health_rx) = mpsc::channel();
181 (ChannelRouter { payment_tx, health_tx }, payment_rx, health_rx)
182 }
183
184 fn compose_payment(index: u64) -> Result<Frame, Box<dyn std::error::Error>> {
185 let frame = compose! {
186 V0: id: format!("p-{index}"),
187 order: 1u64,
188 message: Payment {
189 from: "alice".into(),
190 amount: index
191 }
192 }?;
193 Ok(frame)
194 }
195
196 fn compose_health(index: u64) -> Result<Frame, Box<dyn std::error::Error>> {
197 let frame = compose! {
198 V0: id: format!("h-{index}"),
199 order: 1u64,
200 message: HealthCheck { uptime: index }
201 }?;
202 Ok(frame)
203 }
204
205 #[test]
206 fn dispatch_delivers_decoded_message_per_route() -> Result<(), Box<dyn std::error::Error>> {
207 let (router, payment_rx, health_rx) = build_router();
208
209 let n = 5u64;
210 for i in 0..n {
211 router.dispatch::<Payment>(Arc::new(compose_payment(i)?))?;
212 router.dispatch::<HealthCheck>(Arc::new(compose_health(i)?))?;
213 }
214
215 let timeout = Duration::from_millis(200);
216 for i in 0..n {
217 let (payment_frame, payment) = payment_rx.recv_timeout(timeout)?;
218 assert_eq!(&payment_frame.metadata.id, &format!("p-{i}").as_bytes());
219 assert_eq!(payment, Payment { from: "alice".into(), amount: i });
220
221 let (health_frame, health) = health_rx.recv_timeout(timeout)?;
222 assert_eq!(&health_frame.metadata.id, &format!("h-{i}").as_bytes());
223 assert_eq!(health, HealthCheck { uptime: i });
224 }
225
226 Ok(())
227 }
228
229 #[test]
230 fn dispatch_rejects_misdelivered_type() -> Result<(), Box<dyn std::error::Error>> {
231 let (router, payment_rx, _health_rx) = build_router();
232
233 let result = router.dispatch::<Payment>(Arc::new(compose_health(0)?));
234 assert!(matches!(result, Err(RouterError::DecodeFailed(_))));
235 assert!(matches!(
236 payment_rx.recv_timeout(Duration::from_millis(50)),
237 Err(mpsc::RecvTimeoutError::Timeout)
238 ));
239 Ok(())
240 }
241
242 #[test]
243 fn dispatch_rejects_unregistered_type() -> Result<(), Box<dyn std::error::Error>> {
244 let (router, _payment_rx, _health_rx) = build_router();
245
246 let result = router.dispatch::<Unrouted>(Arc::new(compose_payment(0)?));
247 assert!(matches!(result, Err(RouterError::UnknownRoute)));
248 Ok(())
249 }
250
251 #[test]
252 fn dispatch_rejects_confidential_frame() -> Result<(), Box<dyn std::error::Error>> {
253 let (router, payment_rx, _health_rx) = build_router();
254
255 let mut frame = compose_payment(0)?;
256 frame.metadata.confidentiality = Some(EncryptedContentInfo {
257 content_type: DATA,
258 content_enc_alg: AlgorithmIdentifier { oid: DATA, parameters: None },
259 encrypted_content: Some(OctetString::new(vec![0; 16])?),
260 });
261
262 let result = router.dispatch::<Payment>(Arc::new(frame));
263 assert!(matches!(result, Err(RouterError::ConfidentialFrame)));
264 assert!(matches!(
265 payment_rx.recv_timeout(Duration::from_millis(50)),
266 Err(mpsc::RecvTimeoutError::Timeout)
267 ));
268 Ok(())
269 }
270
271 #[test]
272 fn dispatch_rejects_compressed_frame() -> Result<(), Box<dyn std::error::Error>> {
273 let (router, _payment_rx, _health_rx) = build_router();
274
275 let mut frame = compose_payment(0)?;
276 frame.metadata.compactness = Some(CompressedData {
277 version: CmsVersion::V0,
278 compression_alg: AlgorithmIdentifier { oid: COMPRESSION_ZSTD, parameters: None },
279 encap_content_info: EncapsulatedContentInfo {
280 econtent_type: DATA,
281 econtent: Some(crate::der::Any::from_der(&OctetString::new(vec![0; 8])?.to_der()?)?),
282 },
283 });
284
285 let result = router.dispatch::<Payment>(Arc::new(frame));
286 assert!(matches!(result, Err(RouterError::CompressedFrame)));
287 Ok(())
288 }
289}