1use alloc::string::String;
2use alloc::vec::Vec;
3
4use bitflags::bitflags;
5use umsh_core::REGION_NAME_MAX_LEN as REGION_STRING_MAX_LEN;
6use umsh_core::options::{OptionDecoder, OptionEncoder, parse_be_i32, parse_be_u32};
7
8use crate::app_util::parse_utf8;
9use crate::location::NodeLocation;
10use crate::{AppEncodeError, AppParseError};
11
12pub const MAX_SUPPORTED_REGIONS: usize = 10;
18
19mod opt {
20 pub const NAME: u16 = 0;
21 pub const LOCATION: u16 = 1;
22 pub const ALTITUDE: u16 = 2;
23 pub const TIMESTAMP: u16 = 3;
24 pub const SUPPORTED_REGIONS: u16 = 4;
25 pub const NONCE: u16 = 5;
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum NodeRole {
30 Unspecified,
31 Repeater,
32 Chat,
33 Tracker,
34 Sensor,
35 Bridge,
36 ChatRoom,
37 TemporarySession,
38 Unknown(u8),
40}
41
42impl NodeRole {
43 pub fn from_byte(value: u8) -> Self {
44 match value {
45 0 => Self::Unspecified,
46 1 => Self::Repeater,
47 2 => Self::Chat,
48 3 => Self::Tracker,
49 4 => Self::Sensor,
50 5 => Self::Bridge,
51 6 => Self::ChatRoom,
52 7 => Self::TemporarySession,
53 n => Self::Unknown(n),
54 }
55 }
56
57 pub fn as_byte(self) -> u8 {
58 match self {
59 Self::Unspecified => 0,
60 Self::Repeater => 1,
61 Self::Chat => 2,
62 Self::Tracker => 3,
63 Self::Sensor => 4,
64 Self::Bridge => 5,
65 Self::ChatRoom => 6,
66 Self::TemporarySession => 7,
67 Self::Unknown(n) => n,
68 }
69 }
70}
71
72bitflags! {
73 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
74 pub struct NodeCapabilities: u8 {
75 const REPEATER = 0x01;
76 const MOBILE = 0x02;
77 const TEXT_MESSAGES = 0x04;
78 const TELEMETRY = 0x08;
79 const CHAT_ROOM = 0x10;
80 const COAP = 0x20;
81 }
82}
83
84#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct NodeIdentityPayload {
86 pub role: NodeRole,
87 pub capabilities: NodeCapabilities,
88 pub name: Option<String>,
90 pub location: Option<NodeLocation>,
92 pub altitude_m: Option<i32>,
94 pub timestamp: Option<u32>,
96 pub supported_regions: Option<Vec<String>>,
107 pub nonce: Option<u32>,
110 pub signature: Option<[u8; 64]>,
119}
120
121impl NodeIdentityPayload {
122 pub fn from_bytes(payload: &[u8]) -> Result<NodeIdentityPayload, AppParseError> {
123 if payload.len() < 2 {
124 return Err(AppParseError::Core(umsh_core::ParseError::Truncated));
125 }
126
127 let role = NodeRole::from_byte(payload[0]);
128 let capabilities = NodeCapabilities::from_bits_truncate(payload[1]);
129 let remaining = &payload[2..];
130
131 let mut name = None;
132 let mut location = None;
133 let mut altitude_m = None;
134 let mut timestamp = None;
135 let mut supported_regions = None;
136 let mut nonce = None;
137
138 let mut decoder = OptionDecoder::new(remaining);
139 for result in decoder.by_ref() {
140 let (number, value) = result?;
141 match number {
142 opt::NAME => name = Some(String::from(parse_utf8(value)?)),
143 opt::LOCATION => {
144 location = Some(NodeLocation::from_bytes(value));
146 }
147 opt::ALTITUDE => altitude_m = Some(parse_be_i32(value)?),
148 opt::TIMESTAMP => timestamp = Some(parse_be_u32(value)?),
149 opt::SUPPORTED_REGIONS => {
150 let regions = supported_regions.get_or_insert_with(|| Vec::with_capacity(1));
158 if regions.len() < MAX_SUPPORTED_REGIONS
159 && (1..=REGION_STRING_MAX_LEN).contains(&value.len())
160 && let Ok(text) = core::str::from_utf8(value)
161 {
162 regions.push(String::from(text));
163 }
164 }
165 opt::NONCE => {
166 let bytes: [u8; 4] = value
169 .try_into()
170 .map_err(|_| AppParseError::InvalidOptionValue)?;
171 nonce = Some(u32::from_be_bytes(bytes));
172 }
173 _ => {} }
175 }
176
177 let sig_bytes = decoder.remainder();
178 let signature = match sig_bytes.len() {
179 0 => None,
180 64 => Some(
181 sig_bytes
182 .try_into()
183 .map_err(|_| AppParseError::InvalidLength {
184 expected: 64,
185 actual: sig_bytes.len(),
186 })?,
187 ),
188 n => {
189 return Err(AppParseError::InvalidLength {
190 expected: 64,
191 actual: n,
192 });
193 }
194 };
195
196 Ok(NodeIdentityPayload {
197 role,
198 capabilities,
199 name,
200 location,
201 altitude_m,
202 timestamp,
203 supported_regions,
204 nonce,
205 signature,
206 })
207 }
208
209 pub fn encode(&self, buf: &mut [u8]) -> Result<usize, AppEncodeError> {
210 if buf.len() < 2 {
211 return Err(AppEncodeError::BufferTooSmall);
212 }
213 buf[0] = self.role.as_byte();
214 buf[1] = self.capabilities.bits();
215 let mut pos = 2;
216
217 {
218 let mut enc = OptionEncoder::new(&mut buf[pos..]);
219 if let Some(name) = self.name.as_deref() {
220 enc.put(opt::NAME, name.as_bytes())?;
221 }
222 if let Some(loc) = self.location {
223 enc.put(opt::LOCATION, loc.as_bytes())?;
224 }
225 if let Some(alt) = self.altitude_m {
226 enc.put_i32(opt::ALTITUDE, alt)?;
227 }
228 if let Some(ts) = self.timestamp {
229 enc.put_u32(opt::TIMESTAMP, ts)?;
230 }
231 if let Some(regions) = self.supported_regions.as_deref() {
232 for region in regions.iter().take(MAX_SUPPORTED_REGIONS) {
233 if !(1..=REGION_STRING_MAX_LEN).contains(®ion.len()) {
234 return Err(AppEncodeError::InvalidField);
235 }
236 enc.put(opt::SUPPORTED_REGIONS, region.as_bytes())?;
237 }
238 }
239 if let Some(nonce) = self.nonce {
240 enc.put(opt::NONCE, &nonce.to_be_bytes())?;
241 }
242 if self.signature.is_some() {
243 enc.end_marker()?;
244 }
245 pos += enc.finish();
246 }
247
248 if let Some(sig) = &self.signature {
249 if pos + 64 > buf.len() {
250 return Err(AppEncodeError::BufferTooSmall);
251 }
252 buf[pos..pos + 64].copy_from_slice(sig);
253 pos += 64;
254 }
255
256 Ok(pos)
257 }
258
259 pub fn encode_fitting(&self, buf: &mut [u8]) -> Result<usize, AppEncodeError> {
268 fn out_of_room(error: &AppEncodeError) -> bool {
269 matches!(
270 error,
271 AppEncodeError::BufferTooSmall
272 | AppEncodeError::Core(umsh_core::EncodeError::BufferTooSmall)
273 )
274 }
275
276 match self.encode(buf) {
277 Err(error) if out_of_room(&error) => {}
278 result => return result,
279 }
280
281 let Some(regions) = self.supported_regions.as_deref() else {
282 return Err(AppEncodeError::BufferTooSmall);
283 };
284 let mut shortened = self.clone();
285 for keep in (0..regions.len()).rev() {
286 shortened.supported_regions = (keep > 0).then(|| Vec::from(®ions[..keep]));
287 match shortened.encode(buf) {
288 Err(error) if out_of_room(&error) => continue,
289 result => return result,
290 }
291 }
292 Err(AppEncodeError::BufferTooSmall)
293 }
294
295 pub fn encode_for_signing(&self, buf: &mut [u8]) -> Result<usize, AppEncodeError> {
308 let unsigned = Self {
309 signature: None,
310 ..self.clone()
311 };
312 let mut pos = unsigned.encode_fitting(buf)?;
316 let mut enc = OptionEncoder::new(&mut buf[pos..]);
317 enc.end_marker()?;
318 pos += enc.finish();
319 Ok(pos)
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 fn round_trip(id: &NodeIdentityPayload) -> bool {
328 let mut buf = [0u8; 256];
329 let len = id.encode(&mut buf).expect("encode failed");
330 let decoded = NodeIdentityPayload::from_bytes(&buf[..len]).expect("parse failed");
331 decoded == *id
332 }
333
334 #[test]
335 fn minimal_two_bytes() {
336 let id = NodeIdentityPayload {
337 role: NodeRole::Chat,
338 capabilities: NodeCapabilities::TEXT_MESSAGES,
339 name: None,
340 location: None,
341 altitude_m: None,
342 timestamp: None,
343 supported_regions: None,
344 nonce: None,
345 signature: None,
346 };
347 let mut buf = [0u8; 16];
348 let len = id.encode(&mut buf).unwrap();
349 assert_eq!(len, 2);
350 assert_eq!(buf[0], 2); assert_eq!(buf[1], NodeCapabilities::TEXT_MESSAGES.bits());
352 assert!(round_trip(&id));
353 }
354
355 #[test]
356 fn name_only() {
357 let id = NodeIdentityPayload {
358 role: NodeRole::Unspecified,
359 capabilities: NodeCapabilities::empty(),
360 name: Some("Alice".into()),
361 location: None,
362 altitude_m: None,
363 timestamp: None,
364 supported_regions: None,
365 nonce: None,
366 signature: None,
367 };
368 assert!(round_trip(&id));
369 }
370
371 #[test]
372 fn all_options() {
373 let loc = NodeLocation::from_bytes(&[0x2B, 0x95, 0x51]);
374 let id = NodeIdentityPayload {
375 role: NodeRole::Repeater,
376 capabilities: NodeCapabilities::REPEATER | NodeCapabilities::TEXT_MESSAGES,
377 name: Some("tower".into()),
378 location: Some(loc),
379 altitude_m: Some(1500),
380 timestamp: Some(1_700_000_000),
381 supported_regions: Some(vec!["SJC".into(), "Rogue Valley".into()]),
382 nonce: None,
383 signature: None,
384 };
385 assert!(round_trip(&id));
386 }
387
388 #[test]
391 fn each_region_is_its_own_option() {
392 let id = NodeIdentityPayload {
393 role: NodeRole::Repeater,
394 capabilities: NodeCapabilities::REPEATER,
395 name: None,
396 location: None,
397 altitude_m: None,
398 timestamp: None,
399 supported_regions: Some(vec!["SJC".into(), "MFR".into(), "0x31d9".into()]),
400 nonce: None,
401 signature: None,
402 };
403 let mut buf = [0u8; 64];
404 let len = id.encode(&mut buf).unwrap();
405
406 let mut seen = Vec::new();
407 for entry in OptionDecoder::new(&buf[2..len]) {
408 let (number, value) = entry.unwrap();
409 if number == opt::SUPPORTED_REGIONS {
410 seen.push(String::from(core::str::from_utf8(value).unwrap()));
411 }
412 }
413 assert_eq!(seen, ["SJC", "MFR", "0x31d9"]);
414 assert!(round_trip(&id));
415 }
416
417 #[test]
420 fn skips_regions_it_cannot_use_and_keeps_the_identity() {
421 let mut buf = [0u8; 256];
422 buf[0] = NodeRole::Repeater.as_byte();
423 buf[1] = NodeCapabilities::REPEATER.bits();
424 let long = "R".repeat(REGION_STRING_MAX_LEN + 1);
425 let mut pos = 2;
426 {
427 let mut enc = OptionEncoder::new(&mut buf[pos..]);
428 enc.put(opt::NAME, b"tower").unwrap();
429 enc.put(opt::SUPPORTED_REGIONS, b"SJC").unwrap();
430 enc.put(opt::SUPPORTED_REGIONS, b"").unwrap();
431 enc.put(opt::SUPPORTED_REGIONS, long.as_bytes()).unwrap();
432 enc.put(opt::SUPPORTED_REGIONS, &[0xFF, 0xFE]).unwrap();
433 enc.put(opt::SUPPORTED_REGIONS, b"MFR").unwrap();
434 pos += enc.finish();
435 }
436
437 let decoded = NodeIdentityPayload::from_bytes(&buf[..pos]).unwrap();
438 assert_eq!(decoded.name.as_deref(), Some("tower"));
439 assert_eq!(decoded.supported_regions.unwrap(), ["SJC", "MFR"]);
440 }
441
442 #[test]
443 fn keeps_only_the_first_ten_regions() {
444 let mut buf = [0u8; 256];
445 buf[0] = NodeRole::Repeater.as_byte();
446 buf[1] = NodeCapabilities::REPEATER.bits();
447 let mut pos = 2;
448 {
449 let mut enc = OptionEncoder::new(&mut buf[pos..]);
450 for index in 0..MAX_SUPPORTED_REGIONS + 4 {
451 let name = alloc::format!("R{index:02}");
452 enc.put(opt::SUPPORTED_REGIONS, name.as_bytes()).unwrap();
453 }
454 pos += enc.finish();
455 }
456
457 let regions = NodeIdentityPayload::from_bytes(&buf[..pos])
458 .unwrap()
459 .supported_regions
460 .unwrap();
461 assert_eq!(regions.len(), MAX_SUPPORTED_REGIONS);
462 assert_eq!(regions[0], "R00");
463 assert_eq!(regions[MAX_SUPPORTED_REGIONS - 1], "R09");
464 }
465
466 #[test]
469 fn drops_trailing_regions_to_fit_the_buffer() {
470 let id = NodeIdentityPayload {
471 role: NodeRole::Repeater,
472 capabilities: NodeCapabilities::REPEATER,
473 name: Some("tower".into()),
474 location: None,
475 altitude_m: None,
476 timestamp: None,
477 supported_regions: Some(vec![
478 "Rogue Valley".into(),
479 "SF Bay Area".into(),
480 "Southern Oregon".into(),
481 ]),
482 nonce: None,
483 signature: None,
484 };
485
486 let mut full = [0u8; 128];
487 let full_len = id.encode(&mut full).unwrap();
488
489 let mut clipped = [0u8; 128];
490 let clipped_len = id.encode_fitting(&mut clipped[..full_len - 8]).unwrap();
491 let decoded = NodeIdentityPayload::from_bytes(&clipped[..clipped_len]).unwrap();
492
493 assert_eq!(decoded.name.as_deref(), Some("tower"));
494 assert_eq!(
495 decoded.supported_regions.unwrap(),
496 ["Rogue Valley", "SF Bay Area"],
497 "only the entries that did not fit are dropped, and from the end"
498 );
499 }
500
501 #[test]
504 fn refuses_to_shorten_anything_but_the_regions() {
505 let id = NodeIdentityPayload {
506 role: NodeRole::Repeater,
507 capabilities: NodeCapabilities::REPEATER,
508 name: Some("a rather long tower name".into()),
509 location: None,
510 altitude_m: None,
511 timestamp: None,
512 supported_regions: Some(vec!["SJC".into()]),
513 nonce: None,
514 signature: None,
515 };
516 let mut buf = [0u8; 12];
517 assert!(matches!(
518 id.encode_fitting(&mut buf),
519 Err(AppEncodeError::BufferTooSmall)
520 ));
521 }
522
523 #[test]
524 fn negative_altitude() {
525 let id = NodeIdentityPayload {
526 role: NodeRole::Sensor,
527 capabilities: NodeCapabilities::empty(),
528 name: None,
529 location: None,
530 altitude_m: Some(-430), timestamp: None,
532 supported_regions: None,
533 nonce: None,
534 signature: None,
535 };
536 assert!(round_trip(&id));
537 }
538
539 #[test]
540 fn altitude_zero() {
541 let id = NodeIdentityPayload {
542 role: NodeRole::Sensor,
543 capabilities: NodeCapabilities::empty(),
544 name: None,
545 location: None,
546 altitude_m: Some(0),
547 timestamp: None,
548 supported_regions: None,
549 nonce: None,
550 signature: None,
551 };
552 assert!(round_trip(&id));
553 }
554
555 #[test]
556 fn nonce_round_trips_as_fixed_four_bytes() {
557 let id = NodeIdentityPayload {
558 role: NodeRole::Tracker,
559 capabilities: NodeCapabilities::MOBILE,
560 name: Some("UMSH TRACKER 1".into()),
561 location: None,
562 altitude_m: None,
563 timestamp: None,
564 supported_regions: None,
565 nonce: Some(0x0000_0042), signature: None,
567 };
568 assert!(round_trip(&id));
569 let mut buf = [0u8; 64];
571 let len = id.encode(&mut buf).unwrap();
572 let window = &buf[..len];
573 assert!(
574 window.windows(4).any(|w| w == [0x00, 0x00, 0x00, 0x42]),
575 "nonce not fixed-width on the wire"
576 );
577 let mut manual = [0u8; 8];
579 manual[0] = 0; manual[1] = 0; manual[2] = 0x52;
583 manual[3] = 0xAA;
584 manual[4] = 0xBB;
585 assert!(NodeIdentityPayload::from_bytes(&manual[..5]).is_err());
586 }
587
588 #[test]
589 fn encode_for_signing_matches_signed_wire_form() {
590 let id = NodeIdentityPayload {
591 role: NodeRole::Tracker,
592 capabilities: NodeCapabilities::empty(),
593 name: Some("advert".into()),
594 location: None,
595 altitude_m: None,
596 timestamp: None,
597 supported_regions: None,
598 nonce: Some(0xDEAD_BEEF),
599 signature: None,
600 };
601 let mut buf = [0u8; 256];
602 let len = id.encode_for_signing(&mut buf).unwrap();
603 assert_eq!(buf[len - 1], 0xFF);
605 buf[len..len + 64].copy_from_slice(&[0xA5; 64]);
608 let mut reference = [0u8; 256];
609 let mut signed = id.clone();
610 signed.signature = Some([0xA5; 64]);
611 let ref_len = signed.encode(&mut reference).unwrap();
612 assert_eq!(&buf[..len + 64], &reference[..ref_len]);
613 let parsed = NodeIdentityPayload::from_bytes(&buf[..len + 64]).unwrap();
615 assert_eq!(parsed, signed);
616 }
617
618 #[test]
619 fn with_signature() {
620 let id = NodeIdentityPayload {
621 role: NodeRole::Chat,
622 capabilities: NodeCapabilities::empty(),
623 name: Some("Bob".into()),
624 location: None,
625 altitude_m: None,
626 timestamp: Some(1_700_000_000),
627 supported_regions: None,
628 nonce: None,
629 signature: Some([0xAAu8; 64]),
630 };
631 assert!(round_trip(&id));
632 }
633}