hopr_transport_probe/
content.rs1use hopr_api::types::primitive::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.len() < 2 {
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 anyhow::Context;
99 use hopr_api::types::primitive::traits::AsUnixTimestamp;
100 use hopr_utils::platform::time::native::current_time;
101 use more_asserts::assert_lt;
102
103 use super::*;
104
105 #[test]
106 fn probe_message_variant_probe_should_serialize_and_deserialize() -> anyhow::Result<()> {
107 let m1 = Message::Probe(NeighborProbe::random_nonce());
108 let m2 = Message::try_from(m1.to_bytes().as_ref())?;
109
110 assert_eq!(m1, m2);
111
112 Ok(())
113 }
114
115 #[test]
116 fn probe_message_variant_telemetry_should_serialize_and_deserialize() -> anyhow::Result<()> {
117 let m1 = Message::Telemetry(PathTelemetry {
118 id: hopr_api::types::crypto_random::random_bytes(),
119 path: hopr_api::types::crypto_random::random_bytes(),
120 timestamp: 1234567890,
121 });
122 let m2 = Message::try_from(m1.to_bytes().as_ref())?;
123
124 assert_eq!(m1, m2);
125 Ok(())
126 }
127
128 #[test]
129 fn random_generation_of_a_neighbor_probe_produces_a_ping() -> anyhow::Result<()> {
130 let ping = NeighborProbe::random_nonce();
131 anyhow::ensure!(matches!(ping, NeighborProbe::Ping(_)), "expected Ping variant");
132 Ok(())
133 }
134
135 #[test]
136 fn check_for_complement_works_for_ping_and_pong_with_the_same_nonce() -> anyhow::Result<()> {
137 let ping = NeighborProbe::random_nonce();
138 let pong = { NeighborProbe::Pong(ping.as_ref().try_into()?) };
139
140 assert!(ping.is_complement_to(pong));
141 Ok(())
142 }
143
144 #[test]
145 fn check_for_complement_fails_for_ping_and_pong_with_different_nonce() -> anyhow::Result<()> {
146 let ping = NeighborProbe::random_nonce();
147 let pong = {
148 let mut other: [u8; NeighborProbe::NONCE_SIZE] = ping.as_ref().try_into()?;
149 other[0] = other[0].wrapping_add(1); NeighborProbe::Pong(other)
151 };
152
153 assert!(!ping.is_complement_to(pong));
154
155 Ok(())
156 }
157
158 #[test]
159 fn check_that_at_least_one_surb_can_fit_into_the_payload_for_direct_probing() -> anyhow::Result<()> {
160 let ping = NeighborProbe::random_nonce();
161 let as_data: ApplicationData = Message::Probe(ping).try_into()?;
162
163 assert_lt!(
164 as_data.plain_text.len(),
165 ApplicationData::PAYLOAD_SIZE - hopr_crypto_packet::HoprSurb::SIZE
166 );
167
168 Ok(())
169 }
170
171 #[test]
172 fn check_that_at_least_one_surb_can_fit_into_the_payload_for_path_telemetry() -> anyhow::Result<()> {
173 let telemetry = PathTelemetry {
174 id: [1; 8],
175 path: [1; 5 * size_of::<u64>()],
176 timestamp: current_time().as_unix_timestamp().as_millis(),
177 };
178 let as_data: ApplicationData = Message::Telemetry(telemetry).try_into()?;
179
180 assert_lt!(
181 as_data.plain_text.len(),
182 ApplicationData::PAYLOAD_SIZE - hopr_crypto_packet::HoprSurb::SIZE
183 );
184
185 Ok(())
186 }
187
188 #[test]
189 fn message_from_empty_bytes_fails() -> anyhow::Result<()> {
190 let err = Message::try_from([].as_slice())
191 .err()
192 .context("expected error for empty bytes")?;
193 anyhow::ensure!(
194 matches!(&err, GeneralError::ParseError(s) if s.contains("size")),
195 "expected ParseError about size, got: {err}"
196 );
197 Ok(())
198 }
199
200 #[test]
201 fn message_from_truncated_header_fails() -> anyhow::Result<()> {
202 let err = Message::try_from([Message::VERSION].as_slice())
203 .err()
204 .context("expected error for truncated header")?;
205 anyhow::ensure!(
206 matches!(&err, GeneralError::ParseError(s) if s.contains("size")),
207 "expected ParseError about size, got: {err}"
208 );
209 Ok(())
210 }
211
212 #[test]
213 fn message_from_wrong_version_fails() -> anyhow::Result<()> {
214 let data = [0xFF, 0x00];
216 let err = Message::try_from(data.as_slice())
217 .err()
218 .context("expected error for wrong version")?;
219 anyhow::ensure!(
220 matches!(&err, GeneralError::ParseError(s) if s.contains("version")),
221 "expected ParseError about version, got: {err}"
222 );
223 Ok(())
224 }
225
226 #[test]
227 fn message_from_invalid_discriminant_fails() -> anyhow::Result<()> {
228 let data = [Message::VERSION, 0xFF];
230 let err = Message::try_from(data.as_slice())
231 .err()
232 .context("expected error for invalid discriminant")?;
233 anyhow::ensure!(
234 matches!(&err, GeneralError::ParseError(s) if s.contains("disc")),
235 "expected ParseError about discriminant, got: {err}"
236 );
237 Ok(())
238 }
239
240 #[test]
241 fn message_from_wrong_probe_size_fails() -> anyhow::Result<()> {
242 let data = [Message::VERSION, MessageDiscriminants::Probe as u8, 0x00];
244 let err = Message::try_from(data.as_slice())
245 .err()
246 .context("expected error for wrong probe size")?;
247 anyhow::ensure!(
248 matches!(&err, GeneralError::ParseError(s) if s.contains("probe.size")),
249 "expected ParseError about probe size, got: {err}"
250 );
251 Ok(())
252 }
253
254 #[test]
255 fn message_application_data_roundtrip() -> anyhow::Result<()> {
256 let probe = Message::Probe(NeighborProbe::random_nonce());
257 let app_data: ApplicationData = probe.try_into()?;
258 let restored: Message = app_data.try_into()?;
259
260 anyhow::ensure!(
261 matches!(restored, Message::Probe(NeighborProbe::Ping(_))),
262 "expected Probe(Ping) variant"
263 );
264
265 Ok(())
266 }
267
268 #[test]
269 fn message_application_data_wrong_tag_fails() -> anyhow::Result<()> {
270 let app_data = ApplicationData::new(Tag::MAX, b"not a probe")?;
271 let err = Message::try_from(app_data)
272 .err()
273 .context("expected error for wrong tag")?;
274 anyhow::ensure!(err.to_string().contains("tag"), "expected error about tag, got: {err}");
275 Ok(())
276 }
277}