hopr_transport_probe/
content.rs1use hopr_primitive_types::prelude::GeneralError;
2use hopr_protocol_app::prelude::{ApplicationData, ReservedTag, Tag};
3
4use crate::{
5 errors::ProbeError,
6 types::{NeighborProbe, PathTelemetry},
7};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumDiscriminants, strum::Display)]
10#[strum_discriminants(vis(pub(crate)))]
11#[strum_discriminants(derive(strum::FromRepr, strum::EnumCount), repr(u8))]
12pub enum Message {
13 Telemetry(PathTelemetry),
14 Probe(NeighborProbe),
15}
16
17impl Message {
18 pub const VERSION: u8 = 1;
19
20 pub fn to_bytes(self) -> Box<[u8]> {
21 let mut out = Vec::<u8>::with_capacity(1 + NeighborProbe::SIZE.max(PathTelemetry::SIZE));
22 out.push(Self::VERSION);
23 out.push(MessageDiscriminants::from(&self) as u8);
24
25 match self {
26 Message::Telemetry(telemetry) => out.extend(telemetry.to_bytes()),
27 Message::Probe(probe) => out.extend(probe.to_bytes()),
28 }
29
30 out.into_boxed_slice()
31 }
32}
33
34impl<'a> TryFrom<&'a [u8]> for Message {
35 type Error = GeneralError;
36
37 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
38 if value.is_empty() {
39 return Err(GeneralError::ParseError("Message.size".into()));
40 }
41
42 if value[0] != Self::VERSION {
43 return Err(GeneralError::ParseError("Message.version".into()));
44 }
45
46 match MessageDiscriminants::from_repr(value[1]).ok_or(GeneralError::ParseError("Message.disc".into()))? {
47 MessageDiscriminants::Telemetry => {
48 if value.len() == 2 + PathTelemetry::SIZE {
49 Ok(Self::Telemetry(
50 (&value[2..])
51 .try_into()
52 .map_err(|_| GeneralError::ParseError("Message.telemetry".into()))?,
53 ))
54 } else {
55 Err(GeneralError::ParseError("Message.telemetry.size".into()))?
56 }
57 }
58 MessageDiscriminants::Probe => {
59 if value.len() == 2 + NeighborProbe::SIZE {
60 Ok(Self::Probe(
61 (&value[2..])
62 .try_into()
63 .map_err(|_| GeneralError::ParseError("Message.probe".into()))?,
64 ))
65 } else {
66 Err(GeneralError::ParseError("Message.probe.size".into()))?
67 }
68 }
69 }
70 }
71}
72
73impl TryFrom<Message> for ApplicationData {
74 type Error = ProbeError;
75
76 fn try_from(message: Message) -> Result<Self, Self::Error> {
77 Ok(ApplicationData::new(ReservedTag::Ping, message.to_bytes().into_vec())?)
78 }
79}
80
81impl TryFrom<ApplicationData> for Message {
82 type Error = anyhow::Error;
83
84 fn try_from(value: ApplicationData) -> Result<Self, Self::Error> {
85 let reserved_probe_tag: Tag = ReservedTag::Ping.into();
86
87 if value.application_tag == reserved_probe_tag {
88 let message: Message = value.plain_text.as_ref().try_into()?;
89 Ok(message)
90 } else {
91 Err(anyhow::anyhow!("Non probing application tag found"))
92 }
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use hopr_platform::time::native::current_time;
99 use hopr_primitive_types::traits::AsUnixTimestamp;
100 use more_asserts::assert_lt;
101
102 use super::*;
103
104 #[test]
105 fn probe_message_should_serialize_and_deserialize() -> anyhow::Result<()> {
106 let m1 = Message::Probe(NeighborProbe::random_nonce());
107 let m2 = Message::try_from(m1.to_bytes().as_ref())?;
108
109 assert_eq!(m1, m2);
110
111 let m1 = Message::Telemetry(PathTelemetry {
112 id: hopr_crypto_random::random_bytes(),
113 path: hopr_crypto_random::random_bytes(),
114 timestamp: 1234567890,
115 });
116 let m2 = Message::try_from(m1.to_bytes().as_ref())?;
117
118 assert_eq!(m1, m2);
119 Ok(())
120 }
121
122 #[test]
123 fn random_generation_of_a_neighbor_probe_produces_a_ping() {
124 let ping = NeighborProbe::random_nonce();
125 assert!(matches!(ping, NeighborProbe::Ping(_)));
126 }
127
128 #[test]
129 fn check_for_complement_works_for_ping_and_pong_with_the_same_nonce() -> anyhow::Result<()> {
130 let ping = NeighborProbe::random_nonce();
131 let pong = { NeighborProbe::Pong(ping.as_ref().try_into()?) };
132
133 assert!(ping.is_complement_to(pong));
134 Ok(())
135 }
136
137 #[test]
138 fn check_for_complement_fails_for_ping_and_pong_with_different_nonce() -> anyhow::Result<()> {
139 let ping = NeighborProbe::random_nonce();
140 let pong = {
141 let mut other: [u8; NeighborProbe::NONCE_SIZE] = ping.as_ref().try_into()?;
142 other[0] = other[0].wrapping_add(1); NeighborProbe::Pong(other)
144 };
145
146 assert!(!ping.is_complement_to(pong));
147
148 Ok(())
149 }
150
151 #[test]
152 fn check_that_at_least_one_surb_can_fit_into_the_payload_for_direct_probing() -> anyhow::Result<()> {
153 let ping = NeighborProbe::random_nonce();
154 let as_data: ApplicationData = Message::Probe(ping).try_into()?;
155
156 assert_lt!(
157 as_data.plain_text.len(),
158 ApplicationData::PAYLOAD_SIZE - hopr_crypto_packet::HoprSurb::SIZE
159 );
160
161 Ok(())
162 }
163
164 #[test]
165 fn check_that_at_least_one_surb_can_fit_into_the_payload_for_path_telemetry() -> anyhow::Result<()> {
166 let telemetry = PathTelemetry {
167 id: [1; 10],
168 path: [1; 10],
169 timestamp: current_time().as_unix_timestamp().as_millis(),
170 };
171 let as_data: ApplicationData = Message::Telemetry(telemetry).try_into()?;
172
173 assert_lt!(
174 as_data.plain_text.len(),
175 ApplicationData::PAYLOAD_SIZE - hopr_crypto_packet::HoprSurb::SIZE
176 );
177
178 Ok(())
179 }
180}