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;
24 pub const FILTER_NODE_ROLE: u16 = 5;
26 pub const FILTER_NODE_CAPS: u16 = 7;
29}
30
31pub mod peer_repeaters_request {
36 pub const NONCE: u16 = 0;
38 pub const CURSOR: u16 = 1;
41}
42
43pub mod peer_repeaters_response {
46 pub const NONCE: u16 = 0;
49 pub const CURSOR: u16 = 1;
51 pub const TOTAL: u16 = 2;
53}
54
55pub mod peer_repeater_entry {
60 pub const NODE_HINT: u16 = 0;
63 pub const NODE_NAME: u16 = 1;
66 pub const RSSI_SNR: u16 = 2;
69 pub const LAST_HEARD: u16 = 3;
71 pub const LOCATION: u16 = 4;
73 pub const REGIONS: u16 = 5;
75}
76
77const NODE_HINT_LEN: usize = 3;
79
80const ROUTER_HINT_LEN: usize = 2;
83
84pub const PEER_REPEATER_NAME_MAX_LEN: usize = 24;
86
87const UNIQUE_HINT_PREFIX_LEN: usize = 2;
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96#[repr(u8)]
97pub enum CommandId {
98 IdentityRequest = 1,
99 SignalReportRequest = 2,
100 SignalReportResponse = 3,
101 EchoRequest = 4,
102 EchoResponse = 5,
103 PfsSessionRequest = 6,
104 PfsSessionResponse = 7,
105 EndPfsSession = 8,
106 Noop = 9,
107 PeerRepeatersRequest = 10,
108 PeerRepeatersResponse = 11,
109}
110
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub enum MacCommand<'a> {
113 IdentityRequest {
119 options: &'a [u8],
120 },
121 SignalReportRequest,
122 SignalReportResponse {
123 rssi: u8,
124 snr: i8,
125 },
126 EchoRequest {
127 data: &'a [u8],
128 },
129 EchoResponse {
130 data: &'a [u8],
131 },
132 PfsSessionRequest {
133 ephemeral_key: umsh_core::PublicKey,
134 duration_minutes: u16,
135 },
136 PfsSessionResponse {
137 ephemeral_key: umsh_core::PublicKey,
138 duration_minutes: u16,
139 },
140 EndPfsSession,
141 Noop,
144 PeerRepeatersRequest {
150 options: &'a [u8],
151 },
152 PeerRepeatersResponse {
158 body: &'a [u8],
159 },
160}
161
162pub fn parse(payload: &[u8]) -> Result<MacCommand<'_>, AppParseError> {
163 let (&command_id, body) = payload
164 .split_first()
165 .ok_or(AppParseError::Core(umsh_core::ParseError::Truncated))?;
166
167 match command_id {
168 1 => {
169 for item in OptionDecoder::new(body) {
173 item.map_err(AppParseError::Core)?;
174 }
175 Ok(MacCommand::IdentityRequest { options: body })
176 }
177 2 => {
178 if body.is_empty() {
179 Ok(MacCommand::SignalReportRequest)
180 } else {
181 Err(AppParseError::InvalidOptionValue)
182 }
183 }
184 3 => match body {
185 [rssi, snr] => Ok(MacCommand::SignalReportResponse {
186 rssi: *rssi,
187 snr: *snr as i8,
188 }),
189 _ => Err(AppParseError::InvalidLength {
190 expected: 2,
191 actual: body.len(),
192 }),
193 },
194 4 => Ok(MacCommand::EchoRequest { data: body }),
195 5 => Ok(MacCommand::EchoResponse { data: body }),
196 6 => parse_pfs(body, true),
197 7 => parse_pfs(body, false),
198 8 => {
199 if body.is_empty() {
200 Ok(MacCommand::EndPfsSession)
201 } else {
202 Err(AppParseError::InvalidOptionValue)
203 }
204 }
205 9 => Ok(MacCommand::Noop),
208 10 => {
209 for item in OptionDecoder::new(body) {
212 item.map_err(AppParseError::Core)?;
213 }
214 Ok(MacCommand::PeerRepeatersRequest { options: body })
215 }
216 11 => Ok(MacCommand::PeerRepeatersResponse { body }),
219 other => Err(AppParseError::InvalidCommandId(other)),
220 }
221}
222
223fn parse_pfs(payload: &[u8], request: bool) -> Result<MacCommand<'_>, AppParseError> {
224 if payload.len() != 34 {
225 return Err(AppParseError::InvalidLength {
226 expected: 34,
227 actual: payload.len(),
228 });
229 }
230 let ephemeral_key = umsh_core::PublicKey(*fixed(&payload[..32])?);
231 let duration_minutes = u16::from_be_bytes(*fixed(&payload[32..34])?);
232 Ok(if request {
233 MacCommand::PfsSessionRequest {
234 ephemeral_key,
235 duration_minutes,
236 }
237 } else {
238 MacCommand::PfsSessionResponse {
239 ephemeral_key,
240 duration_minutes,
241 }
242 })
243}
244
245#[derive(Clone, Debug, PartialEq, Eq)]
246pub enum OwnedMacCommand {
247 IdentityRequest {
248 options: Vec<u8>,
249 },
250 SignalReportRequest,
251 SignalReportResponse {
252 rssi: u8,
253 snr: i8,
254 },
255 EchoRequest {
256 data: Vec<u8>,
257 },
258 EchoResponse {
259 data: Vec<u8>,
260 },
261 PfsSessionRequest {
262 ephemeral_key: PublicKey,
263 duration_minutes: u16,
264 },
265 PfsSessionResponse {
266 ephemeral_key: PublicKey,
267 duration_minutes: u16,
268 },
269 EndPfsSession,
270 Noop,
271 PeerRepeatersRequest {
272 options: Vec<u8>,
273 },
274 PeerRepeatersResponse {
275 body: Vec<u8>,
276 },
277}
278
279impl From<MacCommand<'_>> for OwnedMacCommand {
280 fn from(value: MacCommand<'_>) -> Self {
281 match value {
282 MacCommand::IdentityRequest { options } => Self::IdentityRequest {
283 options: Vec::from(options),
284 },
285 MacCommand::SignalReportRequest => Self::SignalReportRequest,
286 MacCommand::SignalReportResponse { rssi, snr } => {
287 Self::SignalReportResponse { rssi, snr }
288 }
289 MacCommand::EchoRequest { data } => Self::EchoRequest {
290 data: Vec::from(data),
291 },
292 MacCommand::EchoResponse { data } => Self::EchoResponse {
293 data: Vec::from(data),
294 },
295 MacCommand::PfsSessionRequest {
296 ephemeral_key,
297 duration_minutes,
298 } => Self::PfsSessionRequest {
299 ephemeral_key,
300 duration_minutes,
301 },
302 MacCommand::PfsSessionResponse {
303 ephemeral_key,
304 duration_minutes,
305 } => Self::PfsSessionResponse {
306 ephemeral_key,
307 duration_minutes,
308 },
309 MacCommand::EndPfsSession => Self::EndPfsSession,
310 MacCommand::Noop => Self::Noop,
311 MacCommand::PeerRepeatersRequest { options } => Self::PeerRepeatersRequest {
312 options: Vec::from(options),
313 },
314 MacCommand::PeerRepeatersResponse { body } => Self::PeerRepeatersResponse {
315 body: Vec::from(body),
316 },
317 }
318 }
319}
320
321pub fn encode(cmd: &MacCommand<'_>, buf: &mut [u8]) -> Result<usize, AppEncodeError> {
322 let mut pos = 0usize;
323 match cmd {
324 MacCommand::IdentityRequest { options } => {
325 push_byte(buf, &mut pos, CommandId::IdentityRequest as u8)?;
326 copy_into(buf, &mut pos, options)?;
327 }
328 MacCommand::SignalReportRequest => {
329 push_byte(buf, &mut pos, CommandId::SignalReportRequest as u8)?;
330 }
331 MacCommand::SignalReportResponse { rssi, snr } => {
332 push_byte(buf, &mut pos, CommandId::SignalReportResponse as u8)?;
333 push_byte(buf, &mut pos, *rssi)?;
334 push_byte(buf, &mut pos, *snr as u8)?;
335 }
336 MacCommand::EchoRequest { data } => {
337 push_byte(buf, &mut pos, CommandId::EchoRequest as u8)?;
338 copy_into(buf, &mut pos, data)?;
339 }
340 MacCommand::EchoResponse { data } => {
341 push_byte(buf, &mut pos, CommandId::EchoResponse as u8)?;
342 copy_into(buf, &mut pos, data)?;
343 }
344 MacCommand::PfsSessionRequest {
345 ephemeral_key,
346 duration_minutes,
347 } => {
348 push_byte(buf, &mut pos, CommandId::PfsSessionRequest as u8)?;
349 copy_into(buf, &mut pos, &ephemeral_key.0)?;
350 copy_into(buf, &mut pos, &duration_minutes.to_be_bytes())?;
351 }
352 MacCommand::PfsSessionResponse {
353 ephemeral_key,
354 duration_minutes,
355 } => {
356 push_byte(buf, &mut pos, CommandId::PfsSessionResponse as u8)?;
357 copy_into(buf, &mut pos, &ephemeral_key.0)?;
358 copy_into(buf, &mut pos, &duration_minutes.to_be_bytes())?;
359 }
360 MacCommand::EndPfsSession => push_byte(buf, &mut pos, CommandId::EndPfsSession as u8)?,
361 MacCommand::Noop => push_byte(buf, &mut pos, CommandId::Noop as u8)?,
362 MacCommand::PeerRepeatersRequest { options } => {
363 push_byte(buf, &mut pos, CommandId::PeerRepeatersRequest as u8)?;
364 copy_into(buf, &mut pos, options)?;
365 }
366 MacCommand::PeerRepeatersResponse { body } => {
367 push_byte(buf, &mut pos, CommandId::PeerRepeatersResponse as u8)?;
368 copy_into(buf, &mut pos, body)?;
369 }
370 }
371 Ok(pos)
372}
373
374#[derive(Clone, Copy, Debug)]
381pub struct IdentityRequestFilters<'a> {
382 options: &'a [u8],
383}
384
385impl<'a> IdentityRequestFilters<'a> {
386 pub fn new(options: &'a [u8]) -> Self {
388 Self { options }
389 }
390
391 pub fn nonce(&self) -> Result<Option<u32>, AppParseError> {
396 for item in OptionDecoder::new(self.options) {
397 let (number, value) = item.map_err(AppParseError::Core)?;
398 if number == identity_filter::NONCE {
399 return parse_be_u32(value).map(Some).map_err(AppParseError::Core);
400 }
401 }
402 Ok(None)
403 }
404
405 pub fn hint_filtered(&self) -> bool {
416 OptionDecoder::new(self.options)
417 .map_while(Result::ok)
418 .any(|(number, _)| number == identity_filter::FILTER_NODE_HINT)
419 }
420
421 pub fn hint_names_one_node(&self) -> bool {
439 let mut named = false;
440 for item in OptionDecoder::new(self.options) {
441 let Ok((number, value)) = item else {
442 return false;
443 };
444 if number == identity_filter::FILTER_NODE_HINT {
445 if value.len() < UNIQUE_HINT_PREFIX_LEN {
446 return false;
447 }
448 named = true;
449 }
450 }
451 named
452 }
453
454 pub fn selects(
462 &self,
463 role: NodeRole,
464 capabilities: NodeCapabilities,
465 hint: &NodeHint,
466 ) -> Result<bool, AppParseError> {
467 let mut hint_present = false;
469 let mut hint_match = false;
470 let mut role_present = false;
471 let mut role_match = false;
472 let mut caps_present = false;
473 let mut caps_match = false;
474
475 for item in OptionDecoder::new(self.options) {
476 let (number, value) = item.map_err(AppParseError::Core)?;
477 match number {
478 identity_filter::NONCE => {} identity_filter::FILTER_NODE_HINT => {
480 hint_present = true;
481 hint_match |=
488 (1..=hint.0.len()).contains(&value.len()) && hint.0.starts_with(value);
489 }
490 identity_filter::FILTER_NODE_ROLE => {
491 role_present = true;
492 role_match |= value == [role.as_byte()];
493 }
494 identity_filter::FILTER_NODE_CAPS => {
495 caps_present = true;
496 caps_match |= value.len() == 1 && (capabilities.bits() & value[0]) == value[0];
498 }
499 other if other & 1 == 1 => {
500 return Ok(false);
502 }
503 _ => {} }
505 }
506
507 Ok((!hint_present || hint_match)
508 && (!role_present || role_match)
509 && (!caps_present || caps_match))
510 }
511}
512
513#[derive(Debug, Default)]
519pub struct IdentityRequestBuilder {
520 buf: Vec<u8>,
521 last_number: u16,
522}
523
524impl IdentityRequestBuilder {
525 pub fn new() -> Self {
527 Self::default()
528 }
529
530 fn put(mut self, number: u16, value: &[u8]) -> Result<Self, AppEncodeError> {
531 let mut scratch = [0u8; 8 + 4];
534 let mut enc = OptionEncoder::with_last_number(&mut scratch, self.last_number);
535 enc.put(number, value).map_err(AppEncodeError::Core)?;
536 let n = enc.finish();
537 self.buf.extend_from_slice(&scratch[..n]);
538 self.last_number = number;
539 Ok(self)
540 }
541
542 pub fn nonce(self, nonce: u32) -> Result<Self, AppEncodeError> {
544 self.put(identity_filter::NONCE, &nonce.to_be_bytes())
545 }
546
547 pub fn filter_hint(self, hint: &NodeHint) -> Result<Self, AppEncodeError> {
549 self.put(identity_filter::FILTER_NODE_HINT, &hint.0)
550 }
551
552 pub fn filter_hint_prefix(self, prefix: &[u8]) -> Result<Self, AppEncodeError> {
560 if !(1..=NODE_HINT_LEN).contains(&prefix.len()) {
561 return Err(AppEncodeError::InvalidField);
562 }
563 self.put(identity_filter::FILTER_NODE_HINT, prefix)
564 }
565
566 pub fn filter_role(self, role: NodeRole) -> Result<Self, AppEncodeError> {
568 self.put(identity_filter::FILTER_NODE_ROLE, &[role.as_byte()])
569 }
570
571 pub fn filter_caps(self, caps: NodeCapabilities) -> Result<Self, AppEncodeError> {
573 self.put(identity_filter::FILTER_NODE_CAPS, &[caps.bits()])
574 }
575
576 pub fn build(self) -> Vec<u8> {
578 self.buf
579 }
580}
581
582#[derive(Clone, Copy, Debug)]
587pub struct PeerRepeatersRequestView<'a> {
588 options: &'a [u8],
589}
590
591impl<'a> PeerRepeatersRequestView<'a> {
592 pub fn new(options: &'a [u8]) -> Self {
593 Self { options }
594 }
595
596 pub fn nonce(&self) -> Option<u16> {
599 self.find(peer_repeaters_request::NONCE)
600 .and_then(parse_minimal_be_u16)
601 }
602
603 pub fn cursor(&self) -> Option<&'a [u8]> {
606 self.find(peer_repeaters_request::CURSOR)
607 }
608
609 fn find(&self, number: u16) -> Option<&'a [u8]> {
610 OptionDecoder::new(self.options)
611 .map_while(Result::ok)
612 .find(|(key, _)| *key == number)
613 .map(|(_, value)| value)
614 }
615}
616
617#[derive(Clone, Copy, Debug)]
620pub struct PeerRepeatersResponseView<'a> {
621 body: &'a [u8],
622}
623
624impl<'a> PeerRepeatersResponseView<'a> {
625 pub fn new(body: &'a [u8]) -> Self {
626 Self { body }
627 }
628
629 pub fn nonce(&self) -> Option<u16> {
632 self.find(peer_repeaters_response::NONCE)
633 .and_then(parse_minimal_be_u16)
634 }
635
636 pub fn cursor(&self) -> Option<&'a [u8]> {
639 self.find(peer_repeaters_response::CURSOR)
640 }
641
642 pub fn total(&self) -> Option<u8> {
645 match self.find(peer_repeaters_response::TOTAL) {
646 Some([total]) => Some(*total),
647 _ => None,
648 }
649 }
650
651 pub fn entries(&self) -> PeerRepeaterEntries<'a> {
656 let mut decoder = OptionDecoder::new(self.body);
657 while decoder.next().is_some() {}
658 PeerRepeaterEntries {
659 rest: decoder.remainder(),
660 }
661 }
662
663 fn find(&self, number: u16) -> Option<&'a [u8]> {
664 OptionDecoder::new(self.body)
665 .map_while(Result::ok)
666 .find(|(key, _)| *key == number)
667 .map(|(_, value)| value)
668 }
669}
670
671#[derive(Clone, Debug)]
673pub struct PeerRepeaterEntries<'a> {
674 rest: &'a [u8],
675}
676
677impl<'a> Iterator for PeerRepeaterEntries<'a> {
678 type Item = PeerRepeaterEntryView<'a>;
679
680 fn next(&mut self) -> Option<Self::Item> {
681 if self.rest.is_empty() {
682 return None;
683 }
684 let mut decoder = OptionDecoder::new(self.rest);
685 while decoder.next().is_some() {}
686 let consumed = self.rest.len() - decoder.remainder().len();
690 let (entry, rest) = self.rest.split_at(consumed);
691 self.rest = rest;
692 Some(PeerRepeaterEntryView { options: entry })
693 }
694}
695
696#[derive(Clone, Copy, Debug)]
702pub struct PeerRepeaterEntryView<'a> {
703 options: &'a [u8],
704}
705
706impl<'a> PeerRepeaterEntryView<'a> {
707 pub fn hint(&self) -> Option<&'a [u8]> {
710 self.find(peer_repeater_entry::NODE_HINT)
711 .filter(|value| (ROUTER_HINT_LEN..=NODE_HINT_LEN).contains(&value.len()))
712 }
713
714 pub fn name(&self) -> Option<&'a str> {
716 self.find(peer_repeater_entry::NODE_NAME)
717 .filter(|value| !value.is_empty() && value.len() <= PEER_REPEATER_NAME_MAX_LEN)
718 .and_then(|value| core::str::from_utf8(value).ok())
719 }
720
721 pub fn rssi_snr(&self) -> Option<(i16, umsh_hal::Snr)> {
723 match self.find(peer_repeater_entry::RSSI_SNR) {
724 Some([rssi, snr]) => Some((
725 -i16::from(*rssi),
726 umsh_hal::Snr::from_quarter_db_steps(i16::from(*snr as i8)),
727 )),
728 _ => None,
729 }
730 }
731
732 pub fn last_heard_min(&self) -> Option<u16> {
734 self.find(peer_repeater_entry::LAST_HEARD)
735 .and_then(parse_minimal_be_u16)
736 }
737
738 pub fn location(&self) -> Option<crate::location::NodeLocation> {
740 self.find(peer_repeater_entry::LOCATION)
741 .filter(|value| !value.is_empty())
742 .map(crate::location::NodeLocation::from_bytes)
743 }
744
745 pub fn regions(&self) -> impl Iterator<Item = [u8; 2]> + 'a {
751 self.find(peer_repeater_entry::REGIONS)
752 .unwrap_or(&[])
753 .chunks_exact(2)
754 .map(|code| [code[0], code[1]])
755 }
756
757 fn find(&self, number: u16) -> Option<&'a [u8]> {
758 OptionDecoder::new(self.options)
759 .map_while(Result::ok)
760 .find(|(key, _)| *key == number)
761 .map(|(_, value)| value)
762 }
763}
764
765fn parse_minimal_be_u16(value: &[u8]) -> Option<u16> {
770 match value {
771 [] => Some(0),
772 [low] => Some(u16::from(*low)),
773 [high, low] => Some(u16::from_be_bytes([*high, *low])),
774 _ => None,
775 }
776}
777
778#[derive(Debug, Default)]
783pub struct PeerRepeatersRequestBuilder {
784 buf: Vec<u8>,
785 last_number: u16,
786}
787
788impl PeerRepeatersRequestBuilder {
789 pub fn new() -> Self {
791 Self::default()
792 }
793
794 pub fn nonce(mut self, nonce: u16) -> Result<Self, AppEncodeError> {
796 put_option(
797 &mut self.buf,
798 &mut self.last_number,
799 peer_repeaters_request::NONCE,
800 &nonce.to_be_bytes(),
801 )?;
802 Ok(self)
803 }
804
805 pub fn cursor(mut self, cursor: &[u8]) -> Result<Self, AppEncodeError> {
807 put_option(
808 &mut self.buf,
809 &mut self.last_number,
810 peer_repeaters_request::CURSOR,
811 cursor,
812 )?;
813 Ok(self)
814 }
815
816 pub fn build(self) -> Vec<u8> {
817 self.buf
818 }
819}
820
821#[derive(Debug)]
829pub struct PeerRepeatersResponseBuilder {
830 nonce: Option<u16>,
831 cursor: Option<Vec<u8>>,
832 total: Option<u8>,
833 cursor_reserve: usize,
834 entries: Vec<u8>,
835 budget: usize,
836}
837
838impl PeerRepeatersResponseBuilder {
839 pub fn new(budget: usize) -> Self {
841 Self {
842 nonce: None,
843 cursor: None,
844 total: None,
845 cursor_reserve: 0,
846 entries: Vec::new(),
847 budget,
848 }
849 }
850
851 pub fn nonce(mut self, nonce: u16) -> Self {
853 self.nonce = Some(nonce);
854 self
855 }
856
857 pub fn total(mut self, total: u8) -> Self {
859 self.total = Some(total);
860 self
861 }
862
863 pub fn reserve_cursor(mut self, octets: usize) -> Self {
867 self.cursor_reserve = match octets {
868 0 => 0,
869 octets => octets + 1,
871 };
872 self
873 }
874
875 pub fn cursor(mut self, cursor: &[u8]) -> Self {
877 self.cursor = Some(Vec::from(cursor));
878 self
879 }
880
881 pub fn try_push(&mut self, entry: &PeerRepeaterEntry<'_>) -> Result<bool, AppEncodeError> {
886 let encoded = entry.encode()?;
887 let separator = usize::from(!self.entries.is_empty());
891 let options = self.encode_options()?.len() + self.cursor_reserve;
892 if options + 1 + self.entries.len() + separator + encoded.len() > self.budget {
893 return Ok(false);
894 }
895 if separator == 1 {
896 self.entries.push(0xFF);
897 }
898 self.entries.extend_from_slice(&encoded);
899 Ok(true)
900 }
901
902 fn encode_options(&self) -> Result<Vec<u8>, AppEncodeError> {
903 let mut buf = Vec::new();
904 let mut last_number = 0u16;
905 if let Some(nonce) = self.nonce {
906 put_option(
907 &mut buf,
908 &mut last_number,
909 peer_repeaters_response::NONCE,
910 &nonce.to_be_bytes(),
911 )?;
912 }
913 if let Some(cursor) = &self.cursor {
914 put_option(
915 &mut buf,
916 &mut last_number,
917 peer_repeaters_response::CURSOR,
918 cursor,
919 )?;
920 }
921 if let Some(total) = self.total {
922 put_option(
923 &mut buf,
924 &mut last_number,
925 peer_repeaters_response::TOTAL,
926 &[total],
927 )?;
928 }
929 Ok(buf)
930 }
931
932 pub fn build(self) -> Result<Vec<u8>, AppEncodeError> {
934 let mut body = self.encode_options()?;
935 body.push(0xFF);
936 body.extend_from_slice(&self.entries);
937 Ok(body)
938 }
939}
940
941#[derive(Clone, Copy, Debug, Default)]
944pub struct PeerRepeaterEntry<'a> {
945 pub hint: &'a [u8],
947 pub name: Option<&'a str>,
948 pub rssi_snr: Option<(i16, umsh_hal::Snr)>,
950 pub last_heard_min: Option<u16>,
951 pub location: Option<crate::location::NodeLocation>,
952 pub regions: &'a [u8],
954}
955
956impl PeerRepeaterEntry<'_> {
957 fn encode(&self) -> Result<Vec<u8>, AppEncodeError> {
958 if !(ROUTER_HINT_LEN..=NODE_HINT_LEN).contains(&self.hint.len()) {
959 return Err(AppEncodeError::InvalidField);
960 }
961 let mut buf = Vec::new();
962 let mut last_number = 0u16;
963 put_option(
964 &mut buf,
965 &mut last_number,
966 peer_repeater_entry::NODE_HINT,
967 self.hint,
968 )?;
969 if let Some(name) = self.name {
970 if name.len() > PEER_REPEATER_NAME_MAX_LEN {
971 return Err(AppEncodeError::InvalidField);
972 }
973 put_option(
974 &mut buf,
975 &mut last_number,
976 peer_repeater_entry::NODE_NAME,
977 name.as_bytes(),
978 )?;
979 }
980 if let Some((rssi_dbm, snr)) = self.rssi_snr {
981 let rssi =
985 u8::try_from(-rssi_dbm.clamp(-255, 0)).map_err(|_| AppEncodeError::InvalidField)?;
986 let snr = i8::try_from(snr.as_quarter_db_steps().clamp(-128, 127))
987 .map_err(|_| AppEncodeError::InvalidField)?;
988 put_option(
989 &mut buf,
990 &mut last_number,
991 peer_repeater_entry::RSSI_SNR,
992 &[rssi, snr as u8],
993 )?;
994 }
995 if let Some(minutes) = self.last_heard_min {
996 let bytes = minutes.to_be_bytes();
997 let minimal = match bytes[0] {
998 0 => &bytes[1..],
999 _ => &bytes[..],
1000 };
1001 put_option(
1002 &mut buf,
1003 &mut last_number,
1004 peer_repeater_entry::LAST_HEARD,
1005 minimal,
1006 )?;
1007 }
1008 if let Some(location) = self.location
1009 && !location.is_unspecified()
1010 {
1011 put_option(
1012 &mut buf,
1013 &mut last_number,
1014 peer_repeater_entry::LOCATION,
1015 location.as_bytes(),
1016 )?;
1017 }
1018 if !self.regions.is_empty() {
1019 if self.regions.len() % 2 != 0 {
1020 return Err(AppEncodeError::InvalidField);
1021 }
1022 put_option(
1023 &mut buf,
1024 &mut last_number,
1025 peer_repeater_entry::REGIONS,
1026 self.regions,
1027 )?;
1028 }
1029 Ok(buf)
1030 }
1031}
1032
1033fn put_option(
1035 buf: &mut Vec<u8>,
1036 last_number: &mut u16,
1037 number: u16,
1038 value: &[u8],
1039) -> Result<(), AppEncodeError> {
1040 let mut scratch = alloc::vec![0u8; value.len() + 8];
1041 let mut encoder = OptionEncoder::with_last_number(&mut scratch, *last_number);
1042 encoder.put(number, value).map_err(AppEncodeError::Core)?;
1043 let written = encoder.finish();
1044 buf.extend_from_slice(&scratch[..written]);
1045 *last_number = number;
1046 Ok(())
1047}
1048
1049#[cfg(test)]
1050mod tests {
1051 use super::*;
1052
1053 fn encode_decode(cmd: MacCommand<'_>) {
1054 let mut buf = [0u8; 64];
1055 let len = encode(&cmd, &mut buf).expect("encode failed");
1056 let decoded = parse(&buf[..len]).expect("parse failed");
1057 assert_eq!(cmd, decoded, "round-trip failed for {cmd:?}");
1058 }
1059
1060 #[test]
1063 fn identity_request_unicast_no_options() {
1064 encode_decode(MacCommand::IdentityRequest { options: &[] });
1065 let mut buf = [0u8; 4];
1066 let len = encode(&MacCommand::IdentityRequest { options: &[] }, &mut buf).unwrap();
1067 assert_eq!(&buf[..len], &[0x01]);
1068 }
1069
1070 #[test]
1071 fn identity_request_with_options_round_trips() {
1072 let options = IdentityRequestBuilder::new()
1073 .nonce(0x12345678)
1074 .unwrap()
1075 .filter_hint(&NodeHint([0xAA, 0xBB, 0xCC]))
1076 .unwrap()
1077 .filter_role(NodeRole::Repeater)
1078 .unwrap()
1079 .build();
1080 encode_decode(MacCommand::IdentityRequest { options: &options });
1081 }
1082
1083 #[test]
1084 fn identity_request_options_are_appended_verbatim() {
1085 let options = IdentityRequestBuilder::new()
1086 .nonce(0x01020304)
1087 .unwrap()
1088 .build();
1089 let mut buf = [0u8; 16];
1090 let len = encode(&MacCommand::IdentityRequest { options: &options }, &mut buf).unwrap();
1091 assert_eq!(buf[0], 0x01);
1092 assert_eq!(&buf[1..len], options.as_slice());
1093 }
1094
1095 #[test]
1096 fn request_identity_framing_carries_readable_nonce() {
1097 let options = IdentityRequestBuilder::new()
1101 .nonce(0xCAFEF00D)
1102 .unwrap()
1103 .build();
1104 let mut buf = [0u8; 128];
1105 buf[0] = umsh_core::PayloadType::MacCommand as u8;
1106 let n = encode(
1107 &MacCommand::IdentityRequest { options: &options },
1108 &mut buf[1..],
1109 )
1110 .unwrap()
1111 + 1;
1112
1113 assert_eq!(buf[0], umsh_core::PayloadType::MacCommand as u8);
1114 let decoded = parse(&buf[1..n]).expect("command body should parse");
1115 let MacCommand::IdentityRequest { options: body } = decoded else {
1116 panic!("expected IdentityRequest, got {decoded:?}");
1117 };
1118 assert_eq!(
1119 IdentityRequestFilters::new(body).nonce().unwrap(),
1120 Some(0xCAFEF00D)
1121 );
1122 }
1123
1124 #[test]
1125 fn identity_filters_nonce_round_trips() {
1126 let options = IdentityRequestBuilder::new()
1127 .nonce(0xDEADBEEF)
1128 .unwrap()
1129 .build();
1130 let filters = IdentityRequestFilters::new(&options);
1131 assert_eq!(filters.nonce().unwrap(), Some(0xDEADBEEF));
1132
1133 let empty = IdentityRequestFilters::new(&[]);
1134 assert_eq!(empty.nonce().unwrap(), None);
1135 }
1136
1137 #[test]
1138 fn identity_filters_no_filters_selects_everyone() {
1139 let filters = IdentityRequestFilters::new(&[]);
1140 assert!(
1141 filters
1142 .selects(
1143 NodeRole::Sensor,
1144 NodeCapabilities::empty(),
1145 &NodeHint([1, 2, 3])
1146 )
1147 .unwrap()
1148 );
1149 }
1150
1151 #[test]
1152 fn identity_filters_hint_match_and_mismatch() {
1153 let options = IdentityRequestBuilder::new()
1154 .filter_hint(&NodeHint([0xAA, 0xBB, 0xCC]))
1155 .unwrap()
1156 .build();
1157 let filters = IdentityRequestFilters::new(&options);
1158 let caps = NodeCapabilities::empty();
1159 assert!(
1160 filters
1161 .selects(NodeRole::Chat, caps, &NodeHint([0xAA, 0xBB, 0xCC]))
1162 .unwrap()
1163 );
1164 assert!(
1165 !filters
1166 .selects(NodeRole::Chat, caps, &NodeHint([0xAA, 0xBB, 0xCD]))
1167 .unwrap()
1168 );
1169 }
1170
1171 #[test]
1172 fn identity_filters_hint_matches_on_a_prefix() {
1173 let caps = NodeCapabilities::empty();
1174 let selects = |prefix: &[u8], hint: NodeHint| {
1175 let options = IdentityRequestBuilder::new()
1176 .filter_hint_prefix(prefix)
1177 .unwrap()
1178 .build();
1179 IdentityRequestFilters::new(&options)
1180 .selects(NodeRole::Chat, caps, &hint)
1181 .unwrap()
1182 };
1183
1184 assert!(selects(&[0xAA, 0xBB], NodeHint([0xAA, 0xBB, 0xCC])));
1186 assert!(!selects(&[0xAA, 0xBC], NodeHint([0xAA, 0xBB, 0xCC])));
1187 assert!(selects(&[0xAA], NodeHint([0xAA, 0xBB, 0xCC])));
1188 assert!(!selects(&[0xAB], NodeHint([0xAA, 0xBB, 0xCC])));
1189
1190 assert!(!selects(&[0xBB, 0xCC], NodeHint([0xAA, 0xBB, 0xCC])));
1192 }
1193
1194 #[test]
1195 fn identity_filters_reject_unusable_hint_lengths() {
1196 assert!(
1199 IdentityRequestBuilder::new()
1200 .filter_hint_prefix(&[])
1201 .is_err()
1202 );
1203 assert!(
1204 IdentityRequestBuilder::new()
1205 .filter_hint_prefix(&[0xAA, 0xBB, 0xCC, 0xDD])
1206 .is_err()
1207 );
1208
1209 let caps = NodeCapabilities::empty();
1212 let hint = NodeHint([0xAA, 0xBB, 0xCC]);
1213 for value in [[].as_slice(), &[0xAA, 0xBB, 0xCC, 0xDD]] {
1214 let mut options = Vec::new();
1215 let mut scratch = [0u8; 8];
1216 let mut enc = OptionEncoder::new(&mut scratch);
1217 enc.put(identity_filter::FILTER_NODE_HINT, value).unwrap();
1218 let n = enc.finish();
1219 options.extend_from_slice(&scratch[..n]);
1220 let filters = IdentityRequestFilters::new(&options);
1221 assert!(filters.hint_filtered());
1222 assert!(!filters.selects(NodeRole::Chat, caps, &hint).unwrap());
1223 }
1224 }
1225
1226 #[test]
1227 fn identity_filters_report_whether_a_hint_names_one_node() {
1228 let named = |prefixes: &[&[u8]]| {
1229 let mut builder = IdentityRequestBuilder::new();
1230 for prefix in prefixes {
1231 builder = builder.filter_hint_prefix(prefix).unwrap();
1232 }
1233 IdentityRequestFilters::new(&builder.build()).hint_names_one_node()
1234 };
1235
1236 assert!(named(&[&[0xAA, 0xBB, 0xCC]]));
1238 assert!(named(&[&[0xAA, 0xBB]]));
1239 assert!(!named(&[&[0xAA]]));
1241
1242 assert!(named(&[&[0xAA, 0xBB], &[0xCC, 0xDD]]));
1244 assert!(!named(&[&[0xAA, 0xBB], &[0xCC]]));
1245
1246 let by_role = IdentityRequestBuilder::new()
1249 .filter_role(NodeRole::Chat)
1250 .unwrap()
1251 .build();
1252 assert!(!IdentityRequestFilters::new(&by_role).hint_names_one_node());
1253
1254 let truncated = [0x35u8];
1260 let filters = IdentityRequestFilters::new(&truncated);
1261 assert!(
1262 filters
1263 .selects(NodeRole::Chat, NodeCapabilities::empty(), &NodeHint([0; 3]))
1264 .is_err(),
1265 "the block has to be undecodable for this to test the error path"
1266 );
1267 assert!(!filters.hint_names_one_node());
1268 }
1269
1270 #[test]
1271 fn identity_filters_report_hint_presence() {
1272 let hinted = IdentityRequestBuilder::new()
1273 .nonce(0x0102_0304)
1274 .unwrap()
1275 .filter_hint(&NodeHint([0xAA, 0xBB, 0xCC]))
1276 .unwrap()
1277 .build();
1278 assert!(IdentityRequestFilters::new(&hinted).hint_filtered());
1279
1280 let by_role = IdentityRequestBuilder::new()
1281 .filter_role(NodeRole::Repeater)
1282 .unwrap()
1283 .build();
1284 assert!(!IdentityRequestFilters::new(&by_role).hint_filtered());
1285
1286 assert!(!IdentityRequestFilters::new(&[]).hint_filtered());
1288 assert!(!IdentityRequestFilters::new(&[0x33, 0xAA]).hint_filtered());
1289 }
1290
1291 #[test]
1292 fn identity_filters_repeated_type_is_or() {
1293 let options = IdentityRequestBuilder::new()
1294 .filter_role(NodeRole::Repeater)
1295 .unwrap()
1296 .filter_role(NodeRole::Chat)
1297 .unwrap()
1298 .build();
1299 let filters = IdentityRequestFilters::new(&options);
1300 let caps = NodeCapabilities::empty();
1301 let hint = NodeHint([1, 2, 3]);
1302 assert!(filters.selects(NodeRole::Repeater, caps, &hint).unwrap());
1303 assert!(filters.selects(NodeRole::Chat, caps, &hint).unwrap());
1304 assert!(!filters.selects(NodeRole::Sensor, caps, &hint).unwrap());
1305 }
1306
1307 #[test]
1308 fn identity_filters_distinct_types_are_and() {
1309 let options = IdentityRequestBuilder::new()
1310 .filter_role(NodeRole::Repeater)
1311 .unwrap()
1312 .filter_caps(NodeCapabilities::REPEATER)
1313 .unwrap()
1314 .build();
1315 let filters = IdentityRequestFilters::new(&options);
1316 let hint = NodeHint([1, 2, 3]);
1317 assert!(
1319 filters
1320 .selects(NodeRole::Repeater, NodeCapabilities::REPEATER, &hint)
1321 .unwrap()
1322 );
1323 assert!(
1325 !filters
1326 .selects(NodeRole::Repeater, NodeCapabilities::empty(), &hint)
1327 .unwrap()
1328 );
1329 }
1330
1331 #[test]
1332 fn identity_filters_caps_requires_all_requested_bits() {
1333 let options = IdentityRequestBuilder::new()
1334 .filter_caps(NodeCapabilities::REPEATER | NodeCapabilities::TEXT_MESSAGES)
1335 .unwrap()
1336 .build();
1337 let filters = IdentityRequestFilters::new(&options);
1338 let hint = NodeHint([1, 2, 3]);
1339 assert!(
1341 filters
1342 .selects(
1343 NodeRole::Chat,
1344 NodeCapabilities::REPEATER
1345 | NodeCapabilities::TEXT_MESSAGES
1346 | NodeCapabilities::MOBILE,
1347 &hint,
1348 )
1349 .unwrap()
1350 );
1351 assert!(
1353 !filters
1354 .selects(NodeRole::Chat, NodeCapabilities::REPEATER, &hint)
1355 .unwrap()
1356 );
1357 }
1358
1359 #[test]
1360 fn identity_filters_unknown_critical_option_excludes() {
1361 let mut buf = [0u8; 8];
1363 let mut enc = OptionEncoder::new(&mut buf);
1364 enc.put(9, &[0x01]).unwrap();
1365 let n = enc.finish();
1366 let filters = IdentityRequestFilters::new(&buf[..n]);
1367 assert!(
1368 !filters
1369 .selects(
1370 NodeRole::Chat,
1371 NodeCapabilities::empty(),
1372 &NodeHint([1, 2, 3])
1373 )
1374 .unwrap()
1375 );
1376 }
1377
1378 #[test]
1379 fn identity_filters_unknown_elective_option_ignored() {
1380 let mut buf = [0u8; 16];
1382 let mut enc = OptionEncoder::new(&mut buf);
1383 enc.put(5, &[NodeRole::Repeater.as_byte()]).unwrap();
1384 enc.put(8, &[0xFE]).unwrap();
1385 let n = enc.finish();
1386 let filters = IdentityRequestFilters::new(&buf[..n]);
1387 assert!(
1388 filters
1389 .selects(
1390 NodeRole::Repeater,
1391 NodeCapabilities::empty(),
1392 &NodeHint([1, 2, 3])
1393 )
1394 .unwrap()
1395 );
1396 }
1397
1398 #[test]
1399 fn signal_report_request() {
1400 encode_decode(MacCommand::SignalReportRequest);
1401 }
1402
1403 #[test]
1404 fn signal_report_response() {
1405 encode_decode(MacCommand::SignalReportResponse {
1406 rssi: 200,
1407 snr: -10,
1408 });
1409 let mut buf = [0u8; 8];
1410 let len = encode(
1411 &MacCommand::SignalReportResponse {
1412 rssi: 0xAB,
1413 snr: -1,
1414 },
1415 &mut buf,
1416 )
1417 .unwrap();
1418 assert_eq!(&buf[..len], &[0x03, 0xAB, 0xFF]);
1419 }
1420
1421 #[test]
1422 fn echo_request() {
1423 encode_decode(MacCommand::EchoRequest {
1424 data: &[0x01, 0x02, 0x03],
1425 });
1426 encode_decode(MacCommand::EchoRequest { data: &[] });
1427 }
1428
1429 #[test]
1430 fn echo_response() {
1431 encode_decode(MacCommand::EchoResponse {
1432 data: &[0xDE, 0xAD],
1433 });
1434 }
1435
1436 #[test]
1437 fn pfs_session_request() {
1438 let key = PublicKey([0xABu8; 32]);
1439 encode_decode(MacCommand::PfsSessionRequest {
1440 ephemeral_key: key,
1441 duration_minutes: 60,
1442 });
1443 let mut buf = [0u8; 40];
1444 let len = encode(
1445 &MacCommand::PfsSessionRequest {
1446 ephemeral_key: key,
1447 duration_minutes: 0x0102,
1448 },
1449 &mut buf,
1450 )
1451 .unwrap();
1452 assert_eq!(len, 1 + 32 + 2);
1453 assert_eq!(buf[0], 0x06);
1454 assert_eq!(&buf[1..33], &[0xABu8; 32]);
1455 assert_eq!(&buf[33..35], &[0x01, 0x02]);
1456 }
1457
1458 #[test]
1459 fn pfs_session_response() {
1460 let key = PublicKey([0x55u8; 32]);
1461 encode_decode(MacCommand::PfsSessionResponse {
1462 ephemeral_key: key,
1463 duration_minutes: 120,
1464 });
1465 }
1466
1467 #[test]
1468 fn end_pfs_session() {
1469 encode_decode(MacCommand::EndPfsSession);
1470 let mut buf = [0u8; 4];
1471 let len = encode(&MacCommand::EndPfsSession, &mut buf).unwrap();
1472 assert_eq!(&buf[..len], &[0x08]);
1473 }
1474
1475 #[test]
1476 fn noop() {
1477 encode_decode(MacCommand::Noop);
1478 let mut buf = [0u8; 4];
1479 let len = encode(&MacCommand::Noop, &mut buf).unwrap();
1480 assert_eq!(&buf[..len], &[0x09]);
1481 }
1482
1483 #[test]
1484 fn noop_tolerates_a_body_it_has_no_use_for() {
1485 assert_eq!(parse(&[0x09, 0xDE, 0xAD]).unwrap(), MacCommand::Noop);
1486 }
1487
1488 #[test]
1491 fn owned_from_borrowed_echo() {
1492 let cmd = MacCommand::EchoRequest {
1493 data: &[0x01, 0x02],
1494 };
1495 let owned = OwnedMacCommand::from(cmd);
1496 assert_eq!(
1497 owned,
1498 OwnedMacCommand::EchoRequest {
1499 data: alloc::vec![0x01, 0x02]
1500 }
1501 );
1502 }
1503
1504 #[test]
1507 fn parse_empty_returns_truncated() {
1508 assert!(matches!(
1509 parse(&[]),
1510 Err(crate::AppParseError::Core(umsh_core::ParseError::Truncated))
1511 ));
1512 }
1513
1514 #[test]
1515 fn parse_unknown_command_id() {
1516 assert!(matches!(
1517 parse(&[0xFF]),
1518 Err(crate::AppParseError::InvalidCommandId(0xFF))
1519 ));
1520 }
1521
1522 #[test]
1523 fn parse_command_zero_is_unallocated() {
1524 assert!(matches!(
1525 parse(&[0x00]),
1526 Err(crate::AppParseError::InvalidCommandId(0))
1527 ));
1528 }
1529
1530 #[test]
1531 fn parse_identity_request_accepts_option_block() {
1532 let decoded = parse(&[0x01, 0x00]).expect("valid options should parse");
1534 assert!(matches!(decoded, MacCommand::IdentityRequest { .. }));
1535 }
1536
1537 #[test]
1538 fn parse_identity_request_rejects_malformed_options() {
1539 assert!(parse(&[0x01, 0x41]).is_err());
1541 }
1542
1543 #[test]
1544 fn parse_signal_report_response_wrong_length() {
1545 assert!(parse(&[0x03, 0x01]).is_err()); }
1547
1548 #[test]
1549 fn parse_pfs_request_wrong_length() {
1550 assert!(parse(&[0x06, 0x00]).is_err()); }
1552
1553 #[test]
1554 fn parse_end_pfs_nonempty_body() {
1555 assert!(parse(&[0x08, 0x00]).is_err());
1556 }
1557
1558 #[test]
1561 fn a_cursorless_peer_repeaters_request_is_one_byte() {
1562 let mut buf = [0u8; 4];
1563 let len = encode(
1564 &MacCommand::PeerRepeatersRequest {
1565 options: &PeerRepeatersRequestBuilder::new().build(),
1566 },
1567 &mut buf,
1568 )
1569 .unwrap();
1570 assert_eq!(&buf[..len], &[0x0A], "options-only, so no end marker");
1571 }
1572
1573 #[test]
1574 fn a_peer_repeaters_request_carries_its_nonce_and_cursor() {
1575 let options = PeerRepeatersRequestBuilder::new()
1576 .nonce(0xBEEF)
1577 .unwrap()
1578 .cursor(&[0x01, 0x02, 0x03])
1579 .unwrap()
1580 .build();
1581 encode_decode(MacCommand::PeerRepeatersRequest { options: &options });
1582
1583 let view = PeerRepeatersRequestView::new(&options);
1584 assert_eq!(view.nonce(), Some(0xBEEF));
1585 assert_eq!(view.cursor(), Some(&[0x01, 0x02, 0x03][..]));
1586
1587 let empty = PeerRepeatersRequestView::new(&[]);
1588 assert_eq!(empty.nonce(), None);
1589 assert_eq!(empty.cursor(), None);
1590 }
1591
1592 #[test]
1595 fn a_peer_repeaters_response_round_trips_every_entry_field() {
1596 let mut builder = PeerRepeatersResponseBuilder::new(200)
1597 .nonce(0xBEEF)
1598 .total(2);
1599 let location = crate::location::NodeLocation::from_lat_lon(44.05, -123.09, 4);
1600 assert!(
1601 builder
1602 .try_push(&PeerRepeaterEntry {
1603 hint: &[0xAA, 0xBB, 0xCC],
1604 name: Some("Ridge"),
1605 rssi_snr: Some((-97, umsh_hal::Snr::from_decibels(-7))),
1606 last_heard_min: Some(400),
1607 location: Some(location),
1608 regions: &[0x78, 0x53, 0x31, 0xD9],
1609 })
1610 .unwrap()
1611 );
1612 assert!(
1616 builder
1617 .try_push(&PeerRepeaterEntry {
1618 hint: &[0x11, 0x22],
1619 ..PeerRepeaterEntry::default()
1620 })
1621 .unwrap()
1622 );
1623 let body = builder.build().unwrap();
1624
1625 let view = PeerRepeatersResponseView::new(&body);
1626 assert_eq!(view.nonce(), Some(0xBEEF));
1627 assert_eq!(view.total(), Some(2));
1628 assert_eq!(view.cursor(), None, "the final page names no resume point");
1629
1630 let entries: Vec<_> = view.entries().collect();
1631 assert_eq!(entries.len(), 2);
1632 assert_eq!(entries[0].hint(), Some(&[0xAA, 0xBB, 0xCC][..]));
1633 assert_eq!(entries[0].name(), Some("Ridge"));
1634 assert_eq!(
1635 entries[0].rssi_snr(),
1636 Some((-97, umsh_hal::Snr::from_decibels(-7)))
1637 );
1638 assert_eq!(entries[0].last_heard_min(), Some(400));
1639 assert_eq!(entries[0].location(), Some(location));
1640 assert_eq!(
1641 entries[0].regions().collect::<Vec<_>>(),
1642 [[0x78, 0x53], [0x31, 0xD9]]
1643 );
1644 assert_eq!(entries[1].hint(), Some(&[0x11, 0x22][..]));
1645 assert_eq!(entries[1].name(), None);
1646 assert_eq!(entries[1].rssi_snr(), None);
1647 assert_eq!(entries[1].last_heard_min(), None);
1648 assert_eq!(entries[1].location(), None);
1649 assert_eq!(entries[1].regions().count(), 0);
1650
1651 encode_decode_long(MacCommand::PeerRepeatersResponse { body: &body });
1652 }
1653
1654 #[test]
1657 fn the_final_entry_needs_no_terminator() {
1658 let mut builder = PeerRepeatersResponseBuilder::new(64);
1659 for hint in [[0x11u8, 0x22], [0x33, 0x44]] {
1660 assert!(
1661 builder
1662 .try_push(&PeerRepeaterEntry {
1663 hint: &hint,
1664 ..PeerRepeaterEntry::default()
1665 })
1666 .unwrap()
1667 );
1668 }
1669 let body = builder.build().unwrap();
1670 assert_ne!(
1671 body.last(),
1672 Some(&0xFF),
1673 "nothing follows the last entry, so nothing marks its end"
1674 );
1675 assert_eq!(PeerRepeatersResponseView::new(&body).entries().count(), 2);
1676
1677 let mut terminated = body.clone();
1680 terminated.push(0xFF);
1681 assert_eq!(
1682 PeerRepeatersResponseView::new(&terminated)
1683 .entries()
1684 .count(),
1685 2
1686 );
1687 }
1688
1689 #[test]
1690 fn a_response_stops_packing_at_its_budget_rather_than_overrunning_it() {
1691 let mut builder = PeerRepeatersResponseBuilder::new(5);
1694 let entry = PeerRepeaterEntry {
1695 hint: &[0x11, 0x22, 0x33],
1696 ..PeerRepeaterEntry::default()
1697 };
1698 assert!(builder.try_push(&entry).unwrap());
1699 assert!(!builder.try_push(&entry).unwrap());
1700 let body = builder.build().unwrap();
1701 assert!(body.len() <= 5, "body is {} octets", body.len());
1702 assert_eq!(PeerRepeatersResponseView::new(&body).entries().count(), 1);
1703 }
1704
1705 #[test]
1708 fn a_reserved_cursor_still_fits_after_the_page_is_packed() {
1709 const BUDGET: usize = 16;
1710 let mut builder = PeerRepeatersResponseBuilder::new(BUDGET).reserve_cursor(3);
1711 let entry = PeerRepeaterEntry {
1712 hint: &[0x11, 0x22, 0x33],
1713 ..PeerRepeaterEntry::default()
1714 };
1715 let mut packed = 0;
1716 while builder.try_push(&entry).unwrap() {
1717 packed += 1;
1718 }
1719 assert!(packed > 0);
1720 let body = builder.cursor(&[0x00, 0x01, 0x02]).build().unwrap();
1721 assert!(body.len() <= BUDGET, "body is {} octets", body.len());
1722 let view = PeerRepeatersResponseView::new(&body);
1723 assert_eq!(view.cursor(), Some(&[0x00, 0x01, 0x02][..]));
1724 assert_eq!(view.entries().count(), packed);
1725 }
1726
1727 #[test]
1728 fn a_paged_response_names_where_to_resume() {
1729 let body = PeerRepeatersResponseBuilder::new(64)
1730 .total(9)
1731 .cursor(&[0x00, 0x07, 0x04])
1732 .build()
1733 .unwrap();
1734 let view = PeerRepeatersResponseView::new(&body);
1735 assert_eq!(view.total(), Some(9));
1736 assert_eq!(view.cursor(), Some(&[0x00, 0x07, 0x04][..]));
1737 assert_eq!(view.entries().count(), 0);
1738 }
1739
1740 #[test]
1741 fn an_entry_without_a_usable_hint_names_nobody() {
1742 let mut buf = Vec::new();
1743 let mut last = 0u16;
1744 put_option(&mut buf, &mut last, peer_repeater_entry::NODE_HINT, &[0x11]).unwrap();
1746 let mut body = alloc::vec![0xFFu8];
1747 body.extend_from_slice(&buf);
1748 let entries: Vec<_> = PeerRepeatersResponseView::new(&body).entries().collect();
1749 assert_eq!(entries.len(), 1);
1750 assert_eq!(entries[0].hint(), None);
1751 }
1752
1753 #[test]
1758 fn a_broken_entry_ends_the_walk_but_keeps_what_came_before() {
1759 let mut body = alloc::vec![0xFFu8];
1760 body.extend_from_slice(&[0x03, 0xAA, 0xBB, 0xCC, 0xFF]);
1763 body.extend_from_slice(&[0x03, 0x11, 0x22, 0x33, 0x0D, 0xF0, 0xFF]);
1764 body.extend_from_slice(&[0x03, 0x44, 0x55, 0x66]);
1765 let entries: Vec<_> = PeerRepeatersResponseView::new(&body).entries().collect();
1766 assert_eq!(entries.len(), 2);
1767 assert_eq!(entries[0].hint(), Some(&[0xAA, 0xBB, 0xCC][..]));
1768 assert_eq!(entries[1].hint(), Some(&[0x11, 0x22, 0x33][..]));
1771 }
1772
1773 #[test]
1774 fn an_entry_hint_outside_two_or_three_octets_is_refused_at_the_encoder() {
1775 let mut builder = PeerRepeatersResponseBuilder::new(64);
1776 for hint in [&[0x11u8][..], &[0x11, 0x22, 0x33, 0x44][..], &[][..]] {
1777 assert!(
1778 builder
1779 .try_push(&PeerRepeaterEntry {
1780 hint,
1781 ..PeerRepeaterEntry::default()
1782 })
1783 .is_err()
1784 );
1785 }
1786 }
1787
1788 #[test]
1789 fn last_heard_uses_the_shortest_encoding_that_holds_it() {
1790 for (minutes, expected) in [(0u16, 1usize), (255, 1), (256, 2), (65535, 2)] {
1791 let mut builder = PeerRepeatersResponseBuilder::new(64);
1792 builder
1793 .try_push(&PeerRepeaterEntry {
1794 hint: &[0x11, 0x22],
1795 last_heard_min: Some(minutes),
1796 ..PeerRepeaterEntry::default()
1797 })
1798 .unwrap();
1799 let body = builder.build().unwrap();
1800 let view = PeerRepeatersResponseView::new(&body);
1801 let entry = view.entries().next().unwrap();
1802 assert_eq!(entry.last_heard_min(), Some(minutes));
1803 assert_eq!(
1804 entry.find(peer_repeater_entry::LAST_HEARD).unwrap().len(),
1805 expected,
1806 "minutes {minutes}"
1807 );
1808 }
1809 }
1810
1811 fn encode_decode_long(cmd: MacCommand<'_>) {
1812 let mut buf = [0u8; 256];
1813 let len = encode(&cmd, &mut buf).expect("encode failed");
1814 assert_eq!(parse(&buf[..len]).expect("parse failed"), cmd);
1815 }
1816}