1use alloc::string::String;
2use alloc::vec::Vec;
3
4use bitflags::bitflags;
5use umsh_core::options::{OptionDecoder, OptionEncoder, parse_be_i32, parse_be_u32};
6
7use crate::app_util::parse_utf8;
8use crate::location::NodeLocation;
9use crate::{AppEncodeError, AppParseError};
10
11mod opt {
12 pub const NAME: u16 = 0;
13 pub const LOCATION: u16 = 1;
14 pub const ALTITUDE: u16 = 2;
15 pub const TIMESTAMP: u16 = 3;
16 pub const SUPPORTED_REGIONS: u16 = 4;
17 pub const NONCE: u16 = 5;
18}
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum NodeRole {
22 Unspecified,
23 Repeater,
24 Chat,
25 Tracker,
26 Sensor,
27 Bridge,
28 ChatRoom,
29 TemporarySession,
30 Unknown(u8),
32}
33
34impl NodeRole {
35 pub fn from_byte(value: u8) -> Self {
36 match value {
37 0 => Self::Unspecified,
38 1 => Self::Repeater,
39 2 => Self::Chat,
40 3 => Self::Tracker,
41 4 => Self::Sensor,
42 5 => Self::Bridge,
43 6 => Self::ChatRoom,
44 7 => Self::TemporarySession,
45 n => Self::Unknown(n),
46 }
47 }
48
49 pub fn as_byte(self) -> u8 {
50 match self {
51 Self::Unspecified => 0,
52 Self::Repeater => 1,
53 Self::Chat => 2,
54 Self::Tracker => 3,
55 Self::Sensor => 4,
56 Self::Bridge => 5,
57 Self::ChatRoom => 6,
58 Self::TemporarySession => 7,
59 Self::Unknown(n) => n,
60 }
61 }
62}
63
64bitflags! {
65 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
66 pub struct NodeCapabilities: u8 {
67 const REPEATER = 0x01;
68 const MOBILE = 0x02;
69 const TEXT_MESSAGES = 0x04;
70 const TELEMETRY = 0x08;
71 const CHAT_ROOM = 0x10;
72 const COAP = 0x20;
73 }
74}
75
76#[derive(Clone, Debug, PartialEq, Eq)]
77pub struct NodeIdentityPayload {
78 pub role: NodeRole,
79 pub capabilities: NodeCapabilities,
80 pub name: Option<String>,
82 pub location: Option<NodeLocation>,
84 pub altitude_m: Option<i32>,
86 pub timestamp: Option<u32>,
88 pub supported_regions: Option<Vec<u8>>,
90 pub nonce: Option<u32>,
93 pub signature: Option<[u8; 64]>,
102}
103
104impl NodeIdentityPayload {
105 pub fn from_bytes(payload: &[u8]) -> Result<NodeIdentityPayload, AppParseError> {
106 if payload.len() < 2 {
107 return Err(AppParseError::Core(umsh_core::ParseError::Truncated));
108 }
109
110 let role = NodeRole::from_byte(payload[0]);
111 let capabilities = NodeCapabilities::from_bits_truncate(payload[1]);
112 let remaining = &payload[2..];
113
114 let mut name = None;
115 let mut location = None;
116 let mut altitude_m = None;
117 let mut timestamp = None;
118 let mut supported_regions = None;
119 let mut nonce = None;
120
121 let mut decoder = OptionDecoder::new(remaining);
122 for result in decoder.by_ref() {
123 let (number, value) = result?;
124 match number {
125 opt::NAME => name = Some(String::from(parse_utf8(value)?)),
126 opt::LOCATION => {
127 location = Some(NodeLocation::from_bytes(value));
129 }
130 opt::ALTITUDE => altitude_m = Some(parse_be_i32(value)?),
131 opt::TIMESTAMP => timestamp = Some(parse_be_u32(value)?),
132 opt::SUPPORTED_REGIONS => {
133 if value.len() % 2 != 0 {
134 return Err(AppParseError::InvalidOptionValue);
135 }
136 supported_regions = Some(Vec::from(value));
137 }
138 opt::NONCE => {
139 let bytes: [u8; 4] = value
142 .try_into()
143 .map_err(|_| AppParseError::InvalidOptionValue)?;
144 nonce = Some(u32::from_be_bytes(bytes));
145 }
146 _ => {} }
148 }
149
150 let sig_bytes = decoder.remainder();
151 let signature = match sig_bytes.len() {
152 0 => None,
153 64 => Some(
154 sig_bytes
155 .try_into()
156 .map_err(|_| AppParseError::InvalidLength {
157 expected: 64,
158 actual: sig_bytes.len(),
159 })?,
160 ),
161 n => {
162 return Err(AppParseError::InvalidLength {
163 expected: 64,
164 actual: n,
165 });
166 }
167 };
168
169 Ok(NodeIdentityPayload {
170 role,
171 capabilities,
172 name,
173 location,
174 altitude_m,
175 timestamp,
176 supported_regions,
177 nonce,
178 signature,
179 })
180 }
181
182 pub fn encode(&self, buf: &mut [u8]) -> Result<usize, AppEncodeError> {
183 if buf.len() < 2 {
184 return Err(AppEncodeError::BufferTooSmall);
185 }
186 buf[0] = self.role.as_byte();
187 buf[1] = self.capabilities.bits();
188 let mut pos = 2;
189
190 {
191 let mut enc = OptionEncoder::new(&mut buf[pos..]);
192 if let Some(name) = self.name.as_deref() {
193 enc.put(opt::NAME, name.as_bytes())?;
194 }
195 if let Some(loc) = self.location {
196 enc.put(opt::LOCATION, loc.as_bytes())?;
197 }
198 if let Some(alt) = self.altitude_m {
199 enc.put_i32(opt::ALTITUDE, alt)?;
200 }
201 if let Some(ts) = self.timestamp {
202 enc.put_u32(opt::TIMESTAMP, ts)?;
203 }
204 if let Some(regions) = self.supported_regions.as_deref() {
205 enc.put(opt::SUPPORTED_REGIONS, regions)?;
206 }
207 if let Some(nonce) = self.nonce {
208 enc.put(opt::NONCE, &nonce.to_be_bytes())?;
209 }
210 if self.signature.is_some() {
211 enc.end_marker()?;
212 }
213 pos += enc.finish();
214 }
215
216 if let Some(sig) = &self.signature {
217 if pos + 64 > buf.len() {
218 return Err(AppEncodeError::BufferTooSmall);
219 }
220 buf[pos..pos + 64].copy_from_slice(sig);
221 pos += 64;
222 }
223
224 Ok(pos)
225 }
226
227 pub fn encode_for_signing(&self, buf: &mut [u8]) -> Result<usize, AppEncodeError> {
240 let unsigned = Self {
241 signature: None,
242 ..self.clone()
243 };
244 let mut pos = unsigned.encode(buf)?;
245 let mut enc = OptionEncoder::new(&mut buf[pos..]);
246 enc.end_marker()?;
247 pos += enc.finish();
248 Ok(pos)
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 fn round_trip(id: &NodeIdentityPayload) -> bool {
257 let mut buf = [0u8; 256];
258 let len = id.encode(&mut buf).expect("encode failed");
259 let decoded = NodeIdentityPayload::from_bytes(&buf[..len]).expect("parse failed");
260 decoded == *id
261 }
262
263 #[test]
264 fn minimal_two_bytes() {
265 let id = NodeIdentityPayload {
266 role: NodeRole::Chat,
267 capabilities: NodeCapabilities::TEXT_MESSAGES,
268 name: None,
269 location: None,
270 altitude_m: None,
271 timestamp: None,
272 supported_regions: None,
273 nonce: None,
274 signature: None,
275 };
276 let mut buf = [0u8; 16];
277 let len = id.encode(&mut buf).unwrap();
278 assert_eq!(len, 2);
279 assert_eq!(buf[0], 2); assert_eq!(buf[1], NodeCapabilities::TEXT_MESSAGES.bits());
281 assert!(round_trip(&id));
282 }
283
284 #[test]
285 fn name_only() {
286 let id = NodeIdentityPayload {
287 role: NodeRole::Unspecified,
288 capabilities: NodeCapabilities::empty(),
289 name: Some("Alice".into()),
290 location: None,
291 altitude_m: None,
292 timestamp: None,
293 supported_regions: None,
294 nonce: None,
295 signature: None,
296 };
297 assert!(round_trip(&id));
298 }
299
300 #[test]
301 fn all_options() {
302 let loc = NodeLocation::from_bytes(&[0x2B, 0x95, 0x51]);
303 let id = NodeIdentityPayload {
304 role: NodeRole::Repeater,
305 capabilities: NodeCapabilities::REPEATER | NodeCapabilities::TEXT_MESSAGES,
306 name: Some("tower".into()),
307 location: Some(loc),
308 altitude_m: Some(1500),
309 timestamp: Some(1_700_000_000),
310 supported_regions: Some(vec![0x00, 0x01, 0x00, 0x02]),
311 nonce: None,
312 signature: None,
313 };
314 assert!(round_trip(&id));
315 }
316
317 #[test]
318 fn negative_altitude() {
319 let id = NodeIdentityPayload {
320 role: NodeRole::Sensor,
321 capabilities: NodeCapabilities::empty(),
322 name: None,
323 location: None,
324 altitude_m: Some(-430), timestamp: None,
326 supported_regions: None,
327 nonce: None,
328 signature: None,
329 };
330 assert!(round_trip(&id));
331 }
332
333 #[test]
334 fn altitude_zero() {
335 let id = NodeIdentityPayload {
336 role: NodeRole::Sensor,
337 capabilities: NodeCapabilities::empty(),
338 name: None,
339 location: None,
340 altitude_m: Some(0),
341 timestamp: None,
342 supported_regions: None,
343 nonce: None,
344 signature: None,
345 };
346 assert!(round_trip(&id));
347 }
348
349 #[test]
350 fn nonce_round_trips_as_fixed_four_bytes() {
351 let id = NodeIdentityPayload {
352 role: NodeRole::Tracker,
353 capabilities: NodeCapabilities::MOBILE,
354 name: Some("UMSH TRACKER 1".into()),
355 location: None,
356 altitude_m: None,
357 timestamp: None,
358 supported_regions: None,
359 nonce: Some(0x0000_0042), signature: None,
361 };
362 assert!(round_trip(&id));
363 let mut buf = [0u8; 64];
365 let len = id.encode(&mut buf).unwrap();
366 let window = &buf[..len];
367 assert!(
368 window.windows(4).any(|w| w == [0x00, 0x00, 0x00, 0x42]),
369 "nonce not fixed-width on the wire"
370 );
371 let mut manual = [0u8; 8];
373 manual[0] = 0; manual[1] = 0; manual[2] = 0x52;
377 manual[3] = 0xAA;
378 manual[4] = 0xBB;
379 assert!(NodeIdentityPayload::from_bytes(&manual[..5]).is_err());
380 }
381
382 #[test]
383 fn encode_for_signing_matches_signed_wire_form() {
384 let id = NodeIdentityPayload {
385 role: NodeRole::Tracker,
386 capabilities: NodeCapabilities::empty(),
387 name: Some("advert".into()),
388 location: None,
389 altitude_m: None,
390 timestamp: None,
391 supported_regions: None,
392 nonce: Some(0xDEAD_BEEF),
393 signature: None,
394 };
395 let mut buf = [0u8; 256];
396 let len = id.encode_for_signing(&mut buf).unwrap();
397 assert_eq!(buf[len - 1], 0xFF);
399 buf[len..len + 64].copy_from_slice(&[0xA5; 64]);
402 let mut reference = [0u8; 256];
403 let mut signed = id.clone();
404 signed.signature = Some([0xA5; 64]);
405 let ref_len = signed.encode(&mut reference).unwrap();
406 assert_eq!(&buf[..len + 64], &reference[..ref_len]);
407 let parsed = NodeIdentityPayload::from_bytes(&buf[..len + 64]).unwrap();
409 assert_eq!(parsed, signed);
410 }
411
412 #[test]
413 fn with_signature() {
414 let id = NodeIdentityPayload {
415 role: NodeRole::Chat,
416 capabilities: NodeCapabilities::empty(),
417 name: Some("Bob".into()),
418 location: None,
419 altitude_m: None,
420 timestamp: Some(1_700_000_000),
421 supported_regions: None,
422 nonce: None,
423 signature: Some([0xAAu8; 64]),
424 };
425 assert!(round_trip(&id));
426 }
427}