1use alloc::vec::Vec;
2
3use umsh_core::options::{OptionDecoder, OptionEncoder, parse_be_u32};
4use umsh_core::{NodeHint, PublicKey};
5
6use crate::app_util::{copy_into, fixed, push_byte};
7use crate::identity::{NodeCapabilities, NodeRole};
8use crate::{AppEncodeError, AppParseError};
9
10pub mod identity_filter {
17 pub const NONCE: u16 = 1;
20 pub const FILTER_NODE_HINT: u16 = 3;
23 pub const FILTER_NODE_ROLE: u16 = 5;
25 pub const FILTER_NODE_CAPS: u16 = 7;
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31#[repr(u8)]
32pub enum CommandId {
33 IdentityRequest = 1,
34 SignalReportRequest = 2,
35 SignalReportResponse = 3,
36 EchoRequest = 4,
37 EchoResponse = 5,
38 PfsSessionRequest = 6,
39 PfsSessionResponse = 7,
40 EndPfsSession = 8,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum MacCommand<'a> {
45 IdentityRequest {
51 options: &'a [u8],
52 },
53 SignalReportRequest,
54 SignalReportResponse {
55 rssi: u8,
56 snr: i8,
57 },
58 EchoRequest {
59 data: &'a [u8],
60 },
61 EchoResponse {
62 data: &'a [u8],
63 },
64 PfsSessionRequest {
65 ephemeral_key: umsh_core::PublicKey,
66 duration_minutes: u16,
67 },
68 PfsSessionResponse {
69 ephemeral_key: umsh_core::PublicKey,
70 duration_minutes: u16,
71 },
72 EndPfsSession,
73}
74
75pub fn parse(payload: &[u8]) -> Result<MacCommand<'_>, AppParseError> {
76 let (&command_id, body) = payload
77 .split_first()
78 .ok_or(AppParseError::Core(umsh_core::ParseError::Truncated))?;
79
80 match command_id {
81 1 => {
82 for item in OptionDecoder::new(body) {
86 item.map_err(AppParseError::Core)?;
87 }
88 Ok(MacCommand::IdentityRequest { options: body })
89 }
90 2 => {
91 if body.is_empty() {
92 Ok(MacCommand::SignalReportRequest)
93 } else {
94 Err(AppParseError::InvalidOptionValue)
95 }
96 }
97 3 => match body {
98 [rssi, snr] => Ok(MacCommand::SignalReportResponse {
99 rssi: *rssi,
100 snr: *snr as i8,
101 }),
102 _ => Err(AppParseError::InvalidLength {
103 expected: 2,
104 actual: body.len(),
105 }),
106 },
107 4 => Ok(MacCommand::EchoRequest { data: body }),
108 5 => Ok(MacCommand::EchoResponse { data: body }),
109 6 => parse_pfs(body, true),
110 7 => parse_pfs(body, false),
111 8 => {
112 if body.is_empty() {
113 Ok(MacCommand::EndPfsSession)
114 } else {
115 Err(AppParseError::InvalidOptionValue)
116 }
117 }
118 other => Err(AppParseError::InvalidCommandId(other)),
119 }
120}
121
122fn parse_pfs(payload: &[u8], request: bool) -> Result<MacCommand<'_>, AppParseError> {
123 if payload.len() != 34 {
124 return Err(AppParseError::InvalidLength {
125 expected: 34,
126 actual: payload.len(),
127 });
128 }
129 let ephemeral_key = umsh_core::PublicKey(*fixed(&payload[..32])?);
130 let duration_minutes = u16::from_be_bytes(*fixed(&payload[32..34])?);
131 Ok(if request {
132 MacCommand::PfsSessionRequest {
133 ephemeral_key,
134 duration_minutes,
135 }
136 } else {
137 MacCommand::PfsSessionResponse {
138 ephemeral_key,
139 duration_minutes,
140 }
141 })
142}
143
144#[derive(Clone, Debug, PartialEq, Eq)]
145pub enum OwnedMacCommand {
146 IdentityRequest {
147 options: Vec<u8>,
148 },
149 SignalReportRequest,
150 SignalReportResponse {
151 rssi: u8,
152 snr: i8,
153 },
154 EchoRequest {
155 data: Vec<u8>,
156 },
157 EchoResponse {
158 data: Vec<u8>,
159 },
160 PfsSessionRequest {
161 ephemeral_key: PublicKey,
162 duration_minutes: u16,
163 },
164 PfsSessionResponse {
165 ephemeral_key: PublicKey,
166 duration_minutes: u16,
167 },
168 EndPfsSession,
169}
170
171impl From<MacCommand<'_>> for OwnedMacCommand {
172 fn from(value: MacCommand<'_>) -> Self {
173 match value {
174 MacCommand::IdentityRequest { options } => Self::IdentityRequest {
175 options: Vec::from(options),
176 },
177 MacCommand::SignalReportRequest => Self::SignalReportRequest,
178 MacCommand::SignalReportResponse { rssi, snr } => {
179 Self::SignalReportResponse { rssi, snr }
180 }
181 MacCommand::EchoRequest { data } => Self::EchoRequest {
182 data: Vec::from(data),
183 },
184 MacCommand::EchoResponse { data } => Self::EchoResponse {
185 data: Vec::from(data),
186 },
187 MacCommand::PfsSessionRequest {
188 ephemeral_key,
189 duration_minutes,
190 } => Self::PfsSessionRequest {
191 ephemeral_key,
192 duration_minutes,
193 },
194 MacCommand::PfsSessionResponse {
195 ephemeral_key,
196 duration_minutes,
197 } => Self::PfsSessionResponse {
198 ephemeral_key,
199 duration_minutes,
200 },
201 MacCommand::EndPfsSession => Self::EndPfsSession,
202 }
203 }
204}
205
206pub fn encode(cmd: &MacCommand<'_>, buf: &mut [u8]) -> Result<usize, AppEncodeError> {
207 let mut pos = 0usize;
208 match cmd {
209 MacCommand::IdentityRequest { options } => {
210 push_byte(buf, &mut pos, CommandId::IdentityRequest as u8)?;
211 copy_into(buf, &mut pos, options)?;
212 }
213 MacCommand::SignalReportRequest => {
214 push_byte(buf, &mut pos, CommandId::SignalReportRequest as u8)?;
215 }
216 MacCommand::SignalReportResponse { rssi, snr } => {
217 push_byte(buf, &mut pos, CommandId::SignalReportResponse as u8)?;
218 push_byte(buf, &mut pos, *rssi)?;
219 push_byte(buf, &mut pos, *snr as u8)?;
220 }
221 MacCommand::EchoRequest { data } => {
222 push_byte(buf, &mut pos, CommandId::EchoRequest as u8)?;
223 copy_into(buf, &mut pos, data)?;
224 }
225 MacCommand::EchoResponse { data } => {
226 push_byte(buf, &mut pos, CommandId::EchoResponse as u8)?;
227 copy_into(buf, &mut pos, data)?;
228 }
229 MacCommand::PfsSessionRequest {
230 ephemeral_key,
231 duration_minutes,
232 } => {
233 push_byte(buf, &mut pos, CommandId::PfsSessionRequest as u8)?;
234 copy_into(buf, &mut pos, &ephemeral_key.0)?;
235 copy_into(buf, &mut pos, &duration_minutes.to_be_bytes())?;
236 }
237 MacCommand::PfsSessionResponse {
238 ephemeral_key,
239 duration_minutes,
240 } => {
241 push_byte(buf, &mut pos, CommandId::PfsSessionResponse as u8)?;
242 copy_into(buf, &mut pos, &ephemeral_key.0)?;
243 copy_into(buf, &mut pos, &duration_minutes.to_be_bytes())?;
244 }
245 MacCommand::EndPfsSession => push_byte(buf, &mut pos, CommandId::EndPfsSession as u8)?,
246 }
247 Ok(pos)
248}
249
250#[derive(Clone, Copy, Debug)]
257pub struct IdentityRequestFilters<'a> {
258 options: &'a [u8],
259}
260
261impl<'a> IdentityRequestFilters<'a> {
262 pub fn new(options: &'a [u8]) -> Self {
264 Self { options }
265 }
266
267 pub fn nonce(&self) -> Result<Option<u32>, AppParseError> {
272 for item in OptionDecoder::new(self.options) {
273 let (number, value) = item.map_err(AppParseError::Core)?;
274 if number == identity_filter::NONCE {
275 return parse_be_u32(value).map(Some).map_err(AppParseError::Core);
276 }
277 }
278 Ok(None)
279 }
280
281 pub fn hint_filtered(&self) -> bool {
292 OptionDecoder::new(self.options)
293 .map_while(Result::ok)
294 .any(|(number, _)| number == identity_filter::FILTER_NODE_HINT)
295 }
296
297 pub fn selects(
305 &self,
306 role: NodeRole,
307 capabilities: NodeCapabilities,
308 hint: &NodeHint,
309 ) -> Result<bool, AppParseError> {
310 let mut hint_present = false;
312 let mut hint_match = false;
313 let mut role_present = false;
314 let mut role_match = false;
315 let mut caps_present = false;
316 let mut caps_match = false;
317
318 for item in OptionDecoder::new(self.options) {
319 let (number, value) = item.map_err(AppParseError::Core)?;
320 match number {
321 identity_filter::NONCE => {} identity_filter::FILTER_NODE_HINT => {
323 hint_present = true;
324 hint_match |= value == hint.0.as_slice();
325 }
326 identity_filter::FILTER_NODE_ROLE => {
327 role_present = true;
328 role_match |= value == [role.as_byte()];
329 }
330 identity_filter::FILTER_NODE_CAPS => {
331 caps_present = true;
332 caps_match |= value.len() == 1 && (capabilities.bits() & value[0]) == value[0];
334 }
335 other if other & 1 == 1 => {
336 return Ok(false);
338 }
339 _ => {} }
341 }
342
343 Ok((!hint_present || hint_match)
344 && (!role_present || role_match)
345 && (!caps_present || caps_match))
346 }
347}
348
349#[derive(Debug, Default)]
355pub struct IdentityRequestBuilder {
356 buf: Vec<u8>,
357 last_number: u16,
358}
359
360impl IdentityRequestBuilder {
361 pub fn new() -> Self {
363 Self::default()
364 }
365
366 fn put(mut self, number: u16, value: &[u8]) -> Result<Self, AppEncodeError> {
367 let mut scratch = [0u8; 8 + 4];
370 let mut enc = OptionEncoder::with_last_number(&mut scratch, self.last_number);
371 enc.put(number, value).map_err(AppEncodeError::Core)?;
372 let n = enc.finish();
373 self.buf.extend_from_slice(&scratch[..n]);
374 self.last_number = number;
375 Ok(self)
376 }
377
378 pub fn nonce(self, nonce: u32) -> Result<Self, AppEncodeError> {
380 self.put(identity_filter::NONCE, &nonce.to_be_bytes())
381 }
382
383 pub fn filter_hint(self, hint: &NodeHint) -> Result<Self, AppEncodeError> {
385 self.put(identity_filter::FILTER_NODE_HINT, &hint.0)
386 }
387
388 pub fn filter_role(self, role: NodeRole) -> Result<Self, AppEncodeError> {
390 self.put(identity_filter::FILTER_NODE_ROLE, &[role.as_byte()])
391 }
392
393 pub fn filter_caps(self, caps: NodeCapabilities) -> Result<Self, AppEncodeError> {
395 self.put(identity_filter::FILTER_NODE_CAPS, &[caps.bits()])
396 }
397
398 pub fn build(self) -> Vec<u8> {
400 self.buf
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 fn encode_decode(cmd: MacCommand<'_>) {
409 let mut buf = [0u8; 64];
410 let len = encode(&cmd, &mut buf).expect("encode failed");
411 let decoded = parse(&buf[..len]).expect("parse failed");
412 assert_eq!(cmd, decoded, "round-trip failed for {cmd:?}");
413 }
414
415 #[test]
418 fn identity_request_unicast_no_options() {
419 encode_decode(MacCommand::IdentityRequest { options: &[] });
420 let mut buf = [0u8; 4];
421 let len = encode(&MacCommand::IdentityRequest { options: &[] }, &mut buf).unwrap();
422 assert_eq!(&buf[..len], &[0x01]);
423 }
424
425 #[test]
426 fn identity_request_with_options_round_trips() {
427 let options = IdentityRequestBuilder::new()
428 .nonce(0x12345678)
429 .unwrap()
430 .filter_hint(&NodeHint([0xAA, 0xBB, 0xCC]))
431 .unwrap()
432 .filter_role(NodeRole::Repeater)
433 .unwrap()
434 .build();
435 encode_decode(MacCommand::IdentityRequest { options: &options });
436 }
437
438 #[test]
439 fn identity_request_options_are_appended_verbatim() {
440 let options = IdentityRequestBuilder::new()
441 .nonce(0x01020304)
442 .unwrap()
443 .build();
444 let mut buf = [0u8; 16];
445 let len = encode(&MacCommand::IdentityRequest { options: &options }, &mut buf).unwrap();
446 assert_eq!(buf[0], 0x01);
447 assert_eq!(&buf[1..len], options.as_slice());
448 }
449
450 #[test]
451 fn request_identity_framing_carries_readable_nonce() {
452 let options = IdentityRequestBuilder::new()
456 .nonce(0xCAFEF00D)
457 .unwrap()
458 .build();
459 let mut buf = [0u8; 128];
460 buf[0] = umsh_core::PayloadType::MacCommand as u8;
461 let n = encode(
462 &MacCommand::IdentityRequest { options: &options },
463 &mut buf[1..],
464 )
465 .unwrap()
466 + 1;
467
468 assert_eq!(buf[0], umsh_core::PayloadType::MacCommand as u8);
469 let decoded = parse(&buf[1..n]).expect("command body should parse");
470 let MacCommand::IdentityRequest { options: body } = decoded else {
471 panic!("expected IdentityRequest, got {decoded:?}");
472 };
473 assert_eq!(
474 IdentityRequestFilters::new(body).nonce().unwrap(),
475 Some(0xCAFEF00D)
476 );
477 }
478
479 #[test]
480 fn identity_filters_nonce_round_trips() {
481 let options = IdentityRequestBuilder::new()
482 .nonce(0xDEADBEEF)
483 .unwrap()
484 .build();
485 let filters = IdentityRequestFilters::new(&options);
486 assert_eq!(filters.nonce().unwrap(), Some(0xDEADBEEF));
487
488 let empty = IdentityRequestFilters::new(&[]);
489 assert_eq!(empty.nonce().unwrap(), None);
490 }
491
492 #[test]
493 fn identity_filters_no_filters_selects_everyone() {
494 let filters = IdentityRequestFilters::new(&[]);
495 assert!(
496 filters
497 .selects(
498 NodeRole::Sensor,
499 NodeCapabilities::empty(),
500 &NodeHint([1, 2, 3])
501 )
502 .unwrap()
503 );
504 }
505
506 #[test]
507 fn identity_filters_hint_match_and_mismatch() {
508 let options = IdentityRequestBuilder::new()
509 .filter_hint(&NodeHint([0xAA, 0xBB, 0xCC]))
510 .unwrap()
511 .build();
512 let filters = IdentityRequestFilters::new(&options);
513 let caps = NodeCapabilities::empty();
514 assert!(
515 filters
516 .selects(NodeRole::Chat, caps, &NodeHint([0xAA, 0xBB, 0xCC]))
517 .unwrap()
518 );
519 assert!(
520 !filters
521 .selects(NodeRole::Chat, caps, &NodeHint([0xAA, 0xBB, 0xCD]))
522 .unwrap()
523 );
524 }
525
526 #[test]
527 fn identity_filters_report_hint_presence() {
528 let hinted = IdentityRequestBuilder::new()
529 .nonce(0x0102_0304)
530 .unwrap()
531 .filter_hint(&NodeHint([0xAA, 0xBB, 0xCC]))
532 .unwrap()
533 .build();
534 assert!(IdentityRequestFilters::new(&hinted).hint_filtered());
535
536 let by_role = IdentityRequestBuilder::new()
537 .filter_role(NodeRole::Repeater)
538 .unwrap()
539 .build();
540 assert!(!IdentityRequestFilters::new(&by_role).hint_filtered());
541
542 assert!(!IdentityRequestFilters::new(&[]).hint_filtered());
544 assert!(!IdentityRequestFilters::new(&[0x33, 0xAA]).hint_filtered());
545 }
546
547 #[test]
548 fn identity_filters_repeated_type_is_or() {
549 let options = IdentityRequestBuilder::new()
550 .filter_role(NodeRole::Repeater)
551 .unwrap()
552 .filter_role(NodeRole::Chat)
553 .unwrap()
554 .build();
555 let filters = IdentityRequestFilters::new(&options);
556 let caps = NodeCapabilities::empty();
557 let hint = NodeHint([1, 2, 3]);
558 assert!(filters.selects(NodeRole::Repeater, caps, &hint).unwrap());
559 assert!(filters.selects(NodeRole::Chat, caps, &hint).unwrap());
560 assert!(!filters.selects(NodeRole::Sensor, caps, &hint).unwrap());
561 }
562
563 #[test]
564 fn identity_filters_distinct_types_are_and() {
565 let options = IdentityRequestBuilder::new()
566 .filter_role(NodeRole::Repeater)
567 .unwrap()
568 .filter_caps(NodeCapabilities::REPEATER)
569 .unwrap()
570 .build();
571 let filters = IdentityRequestFilters::new(&options);
572 let hint = NodeHint([1, 2, 3]);
573 assert!(
575 filters
576 .selects(NodeRole::Repeater, NodeCapabilities::REPEATER, &hint)
577 .unwrap()
578 );
579 assert!(
581 !filters
582 .selects(NodeRole::Repeater, NodeCapabilities::empty(), &hint)
583 .unwrap()
584 );
585 }
586
587 #[test]
588 fn identity_filters_caps_requires_all_requested_bits() {
589 let options = IdentityRequestBuilder::new()
590 .filter_caps(NodeCapabilities::REPEATER | NodeCapabilities::TEXT_MESSAGES)
591 .unwrap()
592 .build();
593 let filters = IdentityRequestFilters::new(&options);
594 let hint = NodeHint([1, 2, 3]);
595 assert!(
597 filters
598 .selects(
599 NodeRole::Chat,
600 NodeCapabilities::REPEATER
601 | NodeCapabilities::TEXT_MESSAGES
602 | NodeCapabilities::MOBILE,
603 &hint,
604 )
605 .unwrap()
606 );
607 assert!(
609 !filters
610 .selects(NodeRole::Chat, NodeCapabilities::REPEATER, &hint)
611 .unwrap()
612 );
613 }
614
615 #[test]
616 fn identity_filters_unknown_critical_option_excludes() {
617 let mut buf = [0u8; 8];
619 let mut enc = OptionEncoder::new(&mut buf);
620 enc.put(9, &[0x01]).unwrap();
621 let n = enc.finish();
622 let filters = IdentityRequestFilters::new(&buf[..n]);
623 assert!(
624 !filters
625 .selects(
626 NodeRole::Chat,
627 NodeCapabilities::empty(),
628 &NodeHint([1, 2, 3])
629 )
630 .unwrap()
631 );
632 }
633
634 #[test]
635 fn identity_filters_unknown_elective_option_ignored() {
636 let mut buf = [0u8; 16];
638 let mut enc = OptionEncoder::new(&mut buf);
639 enc.put(5, &[NodeRole::Repeater.as_byte()]).unwrap();
640 enc.put(8, &[0xFE]).unwrap();
641 let n = enc.finish();
642 let filters = IdentityRequestFilters::new(&buf[..n]);
643 assert!(
644 filters
645 .selects(
646 NodeRole::Repeater,
647 NodeCapabilities::empty(),
648 &NodeHint([1, 2, 3])
649 )
650 .unwrap()
651 );
652 }
653
654 #[test]
655 fn signal_report_request() {
656 encode_decode(MacCommand::SignalReportRequest);
657 }
658
659 #[test]
660 fn signal_report_response() {
661 encode_decode(MacCommand::SignalReportResponse {
662 rssi: 200,
663 snr: -10,
664 });
665 let mut buf = [0u8; 8];
666 let len = encode(
667 &MacCommand::SignalReportResponse {
668 rssi: 0xAB,
669 snr: -1,
670 },
671 &mut buf,
672 )
673 .unwrap();
674 assert_eq!(&buf[..len], &[0x03, 0xAB, 0xFF]);
675 }
676
677 #[test]
678 fn echo_request() {
679 encode_decode(MacCommand::EchoRequest {
680 data: &[0x01, 0x02, 0x03],
681 });
682 encode_decode(MacCommand::EchoRequest { data: &[] });
683 }
684
685 #[test]
686 fn echo_response() {
687 encode_decode(MacCommand::EchoResponse {
688 data: &[0xDE, 0xAD],
689 });
690 }
691
692 #[test]
693 fn pfs_session_request() {
694 let key = PublicKey([0xABu8; 32]);
695 encode_decode(MacCommand::PfsSessionRequest {
696 ephemeral_key: key,
697 duration_minutes: 60,
698 });
699 let mut buf = [0u8; 40];
700 let len = encode(
701 &MacCommand::PfsSessionRequest {
702 ephemeral_key: key,
703 duration_minutes: 0x0102,
704 },
705 &mut buf,
706 )
707 .unwrap();
708 assert_eq!(len, 1 + 32 + 2);
709 assert_eq!(buf[0], 0x06);
710 assert_eq!(&buf[1..33], &[0xABu8; 32]);
711 assert_eq!(&buf[33..35], &[0x01, 0x02]);
712 }
713
714 #[test]
715 fn pfs_session_response() {
716 let key = PublicKey([0x55u8; 32]);
717 encode_decode(MacCommand::PfsSessionResponse {
718 ephemeral_key: key,
719 duration_minutes: 120,
720 });
721 }
722
723 #[test]
724 fn end_pfs_session() {
725 encode_decode(MacCommand::EndPfsSession);
726 let mut buf = [0u8; 4];
727 let len = encode(&MacCommand::EndPfsSession, &mut buf).unwrap();
728 assert_eq!(&buf[..len], &[0x08]);
729 }
730
731 #[test]
734 fn owned_from_borrowed_echo() {
735 let cmd = MacCommand::EchoRequest {
736 data: &[0x01, 0x02],
737 };
738 let owned = OwnedMacCommand::from(cmd);
739 assert_eq!(
740 owned,
741 OwnedMacCommand::EchoRequest {
742 data: alloc::vec![0x01, 0x02]
743 }
744 );
745 }
746
747 #[test]
750 fn parse_empty_returns_truncated() {
751 assert!(matches!(
752 parse(&[]),
753 Err(crate::AppParseError::Core(umsh_core::ParseError::Truncated))
754 ));
755 }
756
757 #[test]
758 fn parse_unknown_command_id() {
759 assert!(matches!(
760 parse(&[0xFF]),
761 Err(crate::AppParseError::InvalidCommandId(0xFF))
762 ));
763 }
764
765 #[test]
766 fn parse_command_zero_is_unallocated() {
767 assert!(matches!(
768 parse(&[0x00]),
769 Err(crate::AppParseError::InvalidCommandId(0))
770 ));
771 }
772
773 #[test]
774 fn parse_identity_request_accepts_option_block() {
775 let decoded = parse(&[0x01, 0x00]).expect("valid options should parse");
777 assert!(matches!(decoded, MacCommand::IdentityRequest { .. }));
778 }
779
780 #[test]
781 fn parse_identity_request_rejects_malformed_options() {
782 assert!(parse(&[0x01, 0x41]).is_err());
784 }
785
786 #[test]
787 fn parse_signal_report_response_wrong_length() {
788 assert!(parse(&[0x03, 0x01]).is_err()); }
790
791 #[test]
792 fn parse_pfs_request_wrong_length() {
793 assert!(parse(&[0x06, 0x00]).is_err()); }
795
796 #[test]
797 fn parse_end_pfs_nonempty_body() {
798 assert!(parse(&[0x08, 0x00]).is_err());
799 }
800}