1use core::fmt::Write;
19use core::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6};
20
21use domain::base::name::{Label, ToLabelIter};
22
23use crate::dm::clusters::basic_info::BasicInfoConfig;
24use crate::error::{Error, ErrorCode};
25use crate::tlv::EitherIter;
26use crate::utils::storage::{write_split, Vec, WriteBuf};
27
28use super::{MatterLocalService, MatterRemoteService};
29
30#[cfg(feature = "astro-dnssd")]
31pub mod astro;
32#[cfg(feature = "zbus")]
33pub mod avahi;
34pub mod builtin;
35#[cfg(feature = "zbus")]
36pub mod resolve;
37#[cfg(feature = "zeroconf")]
38pub mod zeroconf;
39
40pub const MDNS_IPV6_BROADCAST_ADDR: Ipv6Addr = Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0x00fb);
42
43pub const MDNS_IPV4_BROADCAST_ADDR: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251);
45
46pub const MDNS_PORT: u16 = 5353;
48
49pub const MDNS_SOCKET_DEFAULT_BIND_ADDR: SocketAddr =
51 SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, MDNS_PORT, 0, 0));
52
53impl MatterLocalService {
54 #[allow(clippy::type_complexity)]
57 pub fn service<'a>(
58 &self,
59 dev_det: &BasicInfoConfig<'_>,
60 matter_port: u16,
61 buf: &'a mut [u8],
62 ) -> Result<
63 (
64 MdnsLocalService<
65 'a,
66 impl Iterator<Item = &'a str> + Clone,
67 impl Iterator<Item = (&'a str, &'a str)> + Clone,
68 >,
69 &'a mut [u8],
70 ),
71 Error,
72 > {
73 match self {
74 Self::Commissioned {
75 compressed_fabric_id,
76 node_id,
77 } => {
78 let mut wb = WriteBuf::new(buf);
79
80 let (name, mut wb) =
81 write_split!(wb, "{:016X}-{:016X}", compressed_fabric_id, node_id)?;
82
83 let (subtype_i, mut wb) = write_split!(wb, "_I{:016X}", compressed_fabric_id)?;
88 let (txt_sai, mut wb) = if let Some(sai) = dev_det.sai {
89 write_split!(wb, "{}", sai)?
90 } else {
91 ("", wb)
92 };
93 let (txt_sii, wb) = if let Some(sii) = dev_det.sii {
94 write_split!(wb, "{}", sii)?
95 } else {
96 ("", wb)
97 };
98
99 let txt_kvs = [
102 ("SAI", txt_sai),
103 ("SII", txt_sii),
104 ("T", if dev_det.tcp_supported { "6" } else { "" }),
105 ("DUMMY", "DUMMY"),
107 ]
108 .into_iter()
109 .filter(|(_, v)| !v.is_empty());
110
111 Ok((
112 MdnsLocalService {
113 name,
114 service: "_matter",
115 protocol: "_tcp",
116 service_protocol: "_matter._tcp",
117 port: matter_port,
118 service_subtypes: EitherIter::First(core::iter::once(subtype_i)),
119 txt_kvs: EitherIter::First(txt_kvs),
120 },
121 wb.into_buf(),
122 ))
123 }
124 Self::Commissionable {
125 id,
126 discriminator,
127 enhanced,
128 } => {
129 let mut wb = WriteBuf::new(buf);
130
131 let (name, mut wb) = write_split!(wb, "{:016X}", id)?;
132
133 let (subtype_discr, mut wb) = write_split!(wb, "_L{}", *discriminator)?;
134 let (subtype_short_discr, mut wb) = write_split!(
135 wb,
136 "_S{}",
137 Self::compute_short_discriminator(*discriminator)
138 )?;
139 let (subtype_v, mut wb) = write_split!(wb, "_V{}", dev_det.vid)?;
140 let (subtype_t, mut wb) = if let Some(dt) = dev_det.device_type {
141 write_split!(wb, "_T{}", dt)?
142 } else {
143 ("", wb)
144 };
145
146 let service_subtypes = [
147 subtype_discr,
148 subtype_short_discr,
149 subtype_v,
150 subtype_t,
151 "_CM",
152 ]
153 .into_iter()
154 .filter(|s| !s.is_empty());
155
156 let (txt_discr, mut wb) = write_split!(wb, "{}", *discriminator)?;
157 let (txt_vid_pid, mut wb) = write_split!(wb, "{}+{}", dev_det.vid, dev_det.pid)?;
158 let (txt_sai, mut wb) = if let Some(sai) = dev_det.sai {
159 write_split!(wb, "{}", sai)?
160 } else {
161 ("", wb)
162 };
163 let (txt_sii, mut wb) = if let Some(sii) = dev_det.sii {
164 write_split!(wb, "{}", sii)?
165 } else {
166 ("", wb)
167 };
168 let (txt_dn, mut wb) = write_split!(wb, "{}", dev_det.device_name)?;
169 let (txt_pi, mut wb) = write_split!(wb, "{}", dev_det.pairing_instruction)?;
170 let (txt_ph, mut wb) = write_split!(wb, "{}", dev_det.pairing_hint.bits())?;
171 let (txt_dt, mut wb) = if let Some(dt) = dev_det.device_type {
172 write_split!(wb, "{}", dt)?
173 } else {
174 ("", wb)
175 };
176 let (txt_tcp, wb) = if dev_det.tcp_supported {
177 write_split!(wb, "6")?
178 } else {
179 ("", wb)
180 };
181
182 let txt_kvs = [
183 ("D", txt_discr),
184 ("CM", if *enhanced { "2" } else { "1" }),
185 ("VP", txt_vid_pid),
186 ("SAI", txt_sai),
187 ("SII", txt_sii),
188 ("DN", txt_dn),
189 ("PI", txt_pi),
190 ("PH", txt_ph),
191 ("DT", txt_dt),
192 ("T", txt_tcp),
193 ]
194 .into_iter()
195 .filter(|(_, v)| !v.is_empty());
196
197 Ok((
198 MdnsLocalService {
199 name,
200 service: "_matterc",
201 protocol: "_udp",
202 service_protocol: "_matterc._udp",
203 port: matter_port,
204 service_subtypes: EitherIter::Second(service_subtypes),
205 txt_kvs: EitherIter::Second(txt_kvs),
206 },
207 wb.into_buf(),
208 ))
209 }
210 }
211 }
212
213 fn compute_short_discriminator(discriminator: u16) -> u16 {
214 const SHORT_DISCRIMINATOR_MASK: u16 = 0xF00;
215 const SHORT_DISCRIMINATOR_SHIFT: u16 = 8;
216
217 (discriminator & SHORT_DISCRIMINATOR_MASK) >> SHORT_DISCRIMINATOR_SHIFT
218 }
219}
220
221impl MatterRemoteService {
222 pub fn service_type(&self) -> &'static str {
227 match self {
228 Self::Operational { .. } => "_matter._tcp",
229 Self::Commissionable { .. } => "_matterc._udp",
230 }
231 }
232
233 pub fn instance_name(&self, buf: &mut heapless::String<128>) {
237 buf.clear();
238
239 match self {
240 Self::Operational {
241 compressed_fabric_id,
242 node_id,
243 } => {
244 write_unwrap!(
245 buf,
246 "{:016X}-{:016X}._matter._tcp.local",
247 compressed_fabric_id,
248 node_id
249 );
250 }
251 Self::Commissionable { id } => {
252 write_unwrap!(buf, "{:016X}._matterc._udp.local", id);
253 }
254 }
255 }
256
257 fn suffix_labels(&self) -> &'static [&'static str] {
261 match self {
262 Self::Operational { .. } => &["_matter", "_tcp", "local"],
263 Self::Commissionable { .. } => &["_matterc", "_udp", "local"],
264 }
265 }
266
267 pub(crate) fn query_name_labels<'b>(&self, buf: &'b mut heapless::String<33>) -> [&'b str; 4] {
272 buf.clear();
273
274 match self {
275 Self::Operational {
276 compressed_fabric_id,
277 node_id,
278 } => {
279 write_unwrap!(buf, "{:016X}-{:016X}", compressed_fabric_id, node_id);
280 [buf.as_str(), "_matter", "_tcp", "local"]
281 }
282 Self::Commissionable { id } => {
283 write_unwrap!(buf, "{:016X}", id);
284 [buf.as_str(), "_matterc", "_udp", "local"]
285 }
286 }
287 }
288
289 pub fn matches_instance<I: ToLabelIter>(&self, instance: &I) -> bool {
295 let mut labels = instance.iter_labels().filter(|l| !l.is_empty());
297
298 let Some(first) = labels.next() else {
299 return false;
300 };
301
302 let mut suffix = self.suffix_labels().iter();
304 for label in labels {
305 match suffix.next() {
306 Some(expected) if label.as_slice().eq_ignore_ascii_case(expected.as_bytes()) => {}
307 _ => return false,
308 }
309 }
310 if suffix.next().is_some() {
311 return false;
312 }
313
314 let Ok(first) = core::str::from_utf8(first.as_slice()) else {
316 return false;
317 };
318
319 match self {
320 Self::Operational {
321 compressed_fabric_id,
322 node_id,
323 } => {
324 let Some((fabric, node)) = first.split_once('-') else {
325 return false;
326 };
327
328 parse_hex_u64(fabric) == Some(*compressed_fabric_id)
329 && parse_hex_u64(node) == Some(*node_id)
330 }
331 Self::Commissionable { id } => parse_hex_u64(first) == Some(*id),
332 }
333 }
334}
335
336pub struct MdnsLocalService<'a, S, T>
340where
341 S: Iterator<Item = &'a str> + Clone,
342 T: Iterator<Item = (&'a str, &'a str)> + Clone,
343{
344 pub name: &'a str,
346 pub service: &'a str,
348 pub protocol: &'a str,
350 pub service_protocol: &'a str,
352 pub port: u16,
354 pub service_subtypes: S,
356 pub txt_kvs: T,
358}
359
360#[derive(Debug, Clone, Copy)]
375pub struct MdnsRemoteService<I, A, T> {
376 pub instance_name: I,
380 pub port: Option<u16>,
382 pub addrs: A,
384 pub txt: T,
386 pub scope_id: u32,
392}
393
394impl<'a, I, A, T> MdnsRemoteService<I, A, T>
395where
396 T: Iterator<Item = (&'a str, &'a str)> + Clone,
397{
398 pub fn session_params(&self) -> (Option<u32>, Option<u32>, Option<u16>) {
402 let (mut sii, mut sai, mut sat) = (None, None, None);
403
404 for (key, value) in self.txt.clone() {
405 if key.eq_ignore_ascii_case("SII") {
406 sii = value.parse().ok();
407 } else if key.eq_ignore_ascii_case("SAI") {
408 sai = value.parse().ok();
409 } else if key.eq_ignore_ascii_case("SAT") {
410 sat = value.parse().ok();
411 }
412 }
413
414 (sii, sai, sat)
415 }
416}
417
418#[derive(Debug, Clone, Default, Eq, PartialEq)]
427#[cfg_attr(feature = "defmt", derive(defmt::Format))]
428pub struct CommissionableFilter {
429 pub discriminator: Option<u16>,
431 pub short_discriminator: Option<u8>,
433 pub vendor_id: Option<u16>,
435 pub product_id: Option<u16>,
437 pub device_type: Option<u32>,
439 pub commissioning_mode_only: bool,
441}
442
443impl CommissionableFilter {
444 pub fn service_type(&self, buf: &mut heapless::String<64>, include_local: bool) {
464 buf.clear();
465 let suffix = if include_local { ".local" } else { "" };
466
467 let mut sbuf = heapless::String::<24>::new();
468 if let Some(sub) = self.subtype(&mut sbuf) {
469 write_unwrap!(buf, "{}._sub._matterc._udp{}", sub, suffix);
470 } else {
471 write_unwrap!(buf, "_matterc._udp{}", suffix);
472 }
473 }
474
475 pub(crate) fn subtype<'b>(&self, buf: &'b mut heapless::String<24>) -> Option<&'b str> {
481 buf.clear();
482
483 if let Some(disc) = self.discriminator {
484 write_unwrap!(buf, "_L{}", disc);
485 } else if let Some(short_disc) = self.short_discriminator {
486 write_unwrap!(buf, "_S{}", short_disc);
487 } else if let Some(vid) = self.vendor_id {
488 write_unwrap!(buf, "_V{}", vid);
489 } else if let Some(dt) = self.device_type {
490 write_unwrap!(buf, "_T{}", dt);
491 } else if self.commissioning_mode_only {
492 write_unwrap!(buf, "_CM");
493 } else {
494 return None;
495 }
496
497 Some(buf.as_str())
498 }
499
500 pub fn matches<'a, I, A, T>(&self, service: &MdnsRemoteService<I, A, T>) -> bool
503 where
504 T: Iterator<Item = (&'a str, &'a str)> + Clone,
505 {
506 self.matches_txt(service.txt.clone())
507 }
508
509 fn matches_txt<'a, I>(&self, txt: I) -> bool
518 where
519 I: IntoIterator<Item = (&'a str, &'a str)>,
520 {
521 let mut discriminator: Option<u16> = None;
522 let mut vendor_id: Option<u16> = None;
523 let mut product_id: Option<u16> = None;
524 let mut device_type: Option<u32> = None;
525 let mut commissioning = CommissioningMode::Disabled;
526
527 for (key, value) in txt {
528 if key.eq_ignore_ascii_case("D") {
529 discriminator = value.parse::<u16>().ok().filter(|d| *d <= 0xFFF);
530 } else if key.eq_ignore_ascii_case("VP") {
531 if let Some(plus) = value.find('+') {
532 vendor_id = value[..plus].parse::<u16>().ok();
533 product_id = value[plus + 1..].parse::<u16>().ok();
534 } else {
535 vendor_id = value.parse::<u16>().ok();
536 }
537 } else if key.eq_ignore_ascii_case("CM") {
538 commissioning = CommissioningMode::from_txt_value(value);
539 } else if key.eq_ignore_ascii_case("DT") {
540 device_type = value.parse::<u32>().ok();
541 }
542 }
543
544 if let Some(want) = self.discriminator {
545 if discriminator != Some(want) {
546 return false;
547 }
548 }
549
550 if let Some(want) = self.short_discriminator {
551 if discriminator.map(|d| (d >> 8) as u8) != Some(want) {
553 return false;
554 }
555 }
556
557 if let Some(want) = self.vendor_id {
558 if vendor_id != Some(want) {
559 return false;
560 }
561 }
562
563 if let Some(want) = self.product_id {
564 if product_id != Some(want) {
565 return false;
566 }
567 }
568
569 if let Some(want) = self.device_type {
570 if device_type != Some(want) {
571 return false;
572 }
573 }
574
575 if self.commissioning_mode_only && !commissioning.is_commissionable() {
576 return false;
577 }
578
579 true
580 }
581}
582
583#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
588#[cfg_attr(feature = "defmt", derive(defmt::Format))]
589#[repr(u8)]
590enum CommissioningMode {
591 #[default]
593 Disabled = 0,
594 Basic = 1,
596 Enhanced = 2,
598}
599
600impl CommissioningMode {
601 fn from_txt_value(value: &str) -> Self {
603 match value {
604 "1" => Self::Basic,
605 "2" => Self::Enhanced,
606 _ => Self::Disabled,
607 }
608 }
609
610 fn is_commissionable(&self) -> bool {
612 !matches!(self, Self::Disabled)
613 }
614}
615
616#[derive(Debug, Clone, Copy)]
623pub struct DottedName<'a>(pub &'a str);
624
625impl ToLabelIter for DottedName<'_> {
626 type LabelIter<'t>
627 = core::iter::Map<core::str::Split<'t, char>, fn(&str) -> &Label>
628 where
629 Self: 't;
630
631 fn iter_labels(&self) -> Self::LabelIter<'_> {
632 fn str_to_label(s: &str) -> &Label {
635 Label::from_slice(s.as_bytes()).unwrap_or_else(|_| Label::root())
636 }
637
638 self.0
639 .trim_end_matches('.')
640 .split('.')
641 .map(str_to_label as fn(&str) -> &Label)
642 }
643}
644
645#[derive(Debug, Clone, Copy)]
652pub struct ResolvedNode {
653 pub addr: SocketAddr,
655 pub sii: Option<u32>,
657 pub sai: Option<u32>,
659 pub sat: Option<u16>,
661}
662
663#[derive(Debug, Clone)]
670pub(crate) enum MdnsResolveState {
671 Idle,
673 Requested { service: MatterRemoteService },
675 InFlight { service: MatterRemoteService },
677 Resolved {
682 ip: IpAddr,
683 port: u16,
684 scope_id: u32,
687 sii: Option<u32>,
688 sai: Option<u32>,
689 sat: Option<u16>,
690 },
691}
692
693pub(crate) const MAX_BROWSE_EXCLUDE: usize = 6;
698
699pub(crate) type BrowseExclude = Vec<u64, MAX_BROWSE_EXCLUDE>;
701
702#[derive(Debug, Clone)]
712pub(crate) enum MdnsBrowseState {
713 Idle,
715 Requested {
717 filter: CommissionableFilter,
718 exclude: BrowseExclude,
719 },
720 InFlight {
722 filter: CommissionableFilter,
723 exclude: BrowseExclude,
724 },
725 Found {
727 ip: IpAddr,
728 port: u16,
729 scope_id: u32,
732 id: u64,
733 },
734}
735
736pub fn score_ip_address(addr: &IpAddr) -> u8 {
749 match addr {
750 IpAddr::V6(ipv6) => {
751 if ipv6.is_unicast_link_local() {
752 100
754 } else if ipv6.is_unique_local() {
755 80
757 } else if is_ipv6_global_unicast(ipv6) {
758 60
760 } else {
761 40
763 }
764 }
765 IpAddr::V4(_) => {
766 20
768 }
769 }
770}
771
772fn is_ipv6_global_unicast(addr: &Ipv6Addr) -> bool {
774 let segments = addr.segments();
775 (segments[0] & 0xe000) == 0x2000
776}
777
778pub(crate) fn commissionable_instance_id<I: ToLabelIter>(instance: &I) -> Option<u64> {
782 let first = instance.iter_labels().find(|l| !l.is_empty())?;
783 parse_hex_u64(core::str::from_utf8(first.as_slice()).ok()?)
784}
785
786fn parse_hex_u64(s: &str) -> Option<u64> {
789 if s.bytes().all(|b| b.is_ascii_hexdigit()) {
792 u64::from_str_radix(s, 16).ok()
793 } else {
794 None
795 }
796}
797
798#[cfg(test)]
799mod tests {
800 use super::*;
801
802 #[test]
803 fn can_compute_short_discriminator() {
804 let discriminator: u16 = 0b0000_1111_0000_0000;
805 let short = MatterLocalService::compute_short_discriminator(discriminator);
806 assert_eq!(short, 0b1111);
807
808 let discriminator: u16 = 840;
809 let short = MatterLocalService::compute_short_discriminator(discriminator);
810 assert_eq!(short, 3);
811 }
812
813 #[test]
816 fn matches_txt_empty_filter_matches_all() {
817 let filter = CommissionableFilter::default();
818 assert!(filter.matches_txt([("D", "1234"), ("VP", "65521+32768"), ("CM", "1")]));
819 assert!(filter.matches_txt(core::iter::empty::<(&str, &str)>())); }
821
822 #[test]
823 fn matches_txt_discriminator_and_short() {
824 let filter = CommissionableFilter {
825 discriminator: Some(1234),
826 ..Default::default()
827 };
828 assert!(filter.matches_txt([("D", "1234")]));
829 assert!(!filter.matches_txt([("D", "5678")]));
830
831 let filter = CommissionableFilter {
833 short_discriminator: Some(3),
834 ..Default::default()
835 };
836 assert!(filter.matches_txt([("D", "840")]));
837 assert!(!filter.matches_txt([("D", "1024")])); }
839
840 #[test]
841 fn matches_txt_vendor_product_and_combined() {
842 let filter = CommissionableFilter {
843 vendor_id: Some(0xFFF1),
844 product_id: Some(0x8000),
845 ..Default::default()
846 };
847 assert!(filter.matches_txt([("VP", "65521+32768")]));
848 assert!(!filter.matches_txt([("VP", "65521+1")])); assert!(!filter.matches_txt([("VP", "1+32768")])); let filter = CommissionableFilter {
853 discriminator: Some(1234),
854 vendor_id: Some(0xFFF1),
855 ..Default::default()
856 };
857 assert!(filter.matches_txt([("D", "1234"), ("VP", "65521+1")]));
858 assert!(!filter.matches_txt([("D", "1234"), ("VP", "1+1")]));
859 assert!(!filter.matches_txt([("D", "9999"), ("VP", "65521+1")]));
860 }
861
862 #[test]
863 fn matches_txt_device_type_and_commissioning_mode() {
864 let filter = CommissionableFilter {
865 device_type: Some(257),
866 ..Default::default()
867 };
868 assert!(filter.matches_txt([("DT", "257")]));
869 assert!(!filter.matches_txt([("DT", "256")]));
870
871 let filter = CommissionableFilter {
872 commissioning_mode_only: true,
873 ..Default::default()
874 };
875 assert!(filter.matches_txt([("CM", "1")]));
876 assert!(filter.matches_txt([("CM", "2")]));
877 assert!(!filter.matches_txt([("CM", "0")]));
878 assert!(!filter.matches_txt(core::iter::empty::<(&str, &str)>())); }
880
881 #[test]
884 fn service_type_no_filter() {
885 let filter = CommissionableFilter::default();
886 let mut buf = heapless::String::<64>::new();
887
888 filter.service_type(&mut buf, false);
889 assert_eq!(buf.as_str(), "_matterc._udp");
890
891 filter.service_type(&mut buf, true);
892 assert_eq!(buf.as_str(), "_matterc._udp.local");
893 }
894
895 #[test]
896 fn service_type_with_discriminator() {
897 let filter = CommissionableFilter {
898 discriminator: Some(1234),
899 ..Default::default()
900 };
901 let mut buf = heapless::String::<64>::new();
902
903 filter.service_type(&mut buf, false);
904 assert_eq!(buf.as_str(), "_L1234._sub._matterc._udp");
905
906 filter.service_type(&mut buf, true);
907 assert_eq!(buf.as_str(), "_L1234._sub._matterc._udp.local");
908 }
909
910 #[test]
911 fn service_type_with_short_discriminator() {
912 let filter = CommissionableFilter {
913 short_discriminator: Some(3),
914 ..Default::default()
915 };
916 let mut buf = heapless::String::<64>::new();
917
918 filter.service_type(&mut buf, false);
919 assert_eq!(buf.as_str(), "_S3._sub._matterc._udp");
920 }
921
922 #[test]
923 fn service_type_with_vendor_id() {
924 let filter = CommissionableFilter {
925 vendor_id: Some(0xFFF1),
926 ..Default::default()
927 };
928 let mut buf = heapless::String::<64>::new();
929
930 filter.service_type(&mut buf, false);
931 assert_eq!(buf.as_str(), "_V65521._sub._matterc._udp");
932 }
933
934 #[test]
935 fn service_type_with_device_type() {
936 let filter = CommissionableFilter {
937 device_type: Some(257),
938 ..Default::default()
939 };
940 let mut buf = heapless::String::<64>::new();
941
942 filter.service_type(&mut buf, false);
943 assert_eq!(buf.as_str(), "_T257._sub._matterc._udp");
944 }
945
946 #[test]
947 fn service_type_with_commissioning_mode_only() {
948 let filter = CommissionableFilter {
949 commissioning_mode_only: true,
950 ..Default::default()
951 };
952 let mut buf = heapless::String::<64>::new();
953
954 filter.service_type(&mut buf, false);
955 assert_eq!(buf.as_str(), "_CM._sub._matterc._udp");
956 }
957
958 #[test]
959 fn service_type_priority_order() {
960 let mut buf = heapless::String::<64>::new();
962
963 CommissionableFilter {
964 discriminator: Some(1234),
965 short_discriminator: Some(3),
966 vendor_id: Some(0xFFF1),
967 device_type: Some(257),
968 commissioning_mode_only: true,
969 product_id: Some(0x8000),
970 }
971 .service_type(&mut buf, false);
972 assert_eq!(buf.as_str(), "_L1234._sub._matterc._udp");
973
974 CommissionableFilter {
975 short_discriminator: Some(3),
976 vendor_id: Some(0xFFF1),
977 ..Default::default()
978 }
979 .service_type(&mut buf, false);
980 assert_eq!(buf.as_str(), "_S3._sub._matterc._udp");
981
982 CommissionableFilter {
983 vendor_id: Some(0xFFF1),
984 device_type: Some(257),
985 ..Default::default()
986 }
987 .service_type(&mut buf, false);
988 assert_eq!(buf.as_str(), "_V65521._sub._matterc._udp");
989
990 CommissionableFilter {
991 device_type: Some(257),
992 commissioning_mode_only: true,
993 ..Default::default()
994 }
995 .service_type(&mut buf, false);
996 assert_eq!(buf.as_str(), "_T257._sub._matterc._udp");
997
998 CommissionableFilter {
1000 product_id: Some(0x8000),
1001 ..Default::default()
1002 }
1003 .service_type(&mut buf, false);
1004 assert_eq!(buf.as_str(), "_matterc._udp");
1005 }
1006
1007 #[test]
1010 fn commissioning_mode_from_txt_value() {
1011 assert_eq!(
1012 CommissioningMode::from_txt_value("0"),
1013 CommissioningMode::Disabled
1014 );
1015 assert_eq!(
1016 CommissioningMode::from_txt_value("1"),
1017 CommissioningMode::Basic
1018 );
1019 assert_eq!(
1020 CommissioningMode::from_txt_value("2"),
1021 CommissioningMode::Enhanced
1022 );
1023 assert_eq!(
1024 CommissioningMode::from_txt_value("x"),
1025 CommissioningMode::Disabled
1026 );
1027
1028 assert!(!CommissioningMode::Disabled.is_commissionable());
1029 assert!(CommissioningMode::Basic.is_commissionable());
1030 assert!(CommissioningMode::Enhanced.is_commissionable());
1031 }
1032
1033 #[test]
1034 fn score_ip_address_priority_order() {
1035 let link_local = IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1));
1036 let ula = IpAddr::V6(Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 1));
1037 let global = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1));
1038 let ipv4 = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
1039
1040 assert!(score_ip_address(&link_local) > score_ip_address(&ula));
1041 assert!(score_ip_address(&ula) > score_ip_address(&global));
1042 assert!(score_ip_address(&global) > score_ip_address(&ipv4));
1043 }
1044
1045 #[test]
1046 fn is_ipv6_global_unicast_correct() {
1047 assert!(is_ipv6_global_unicast(&Ipv6Addr::new(
1048 0x2001, 0xdb8, 0, 0, 0, 0, 0, 1
1049 )));
1050 assert!(is_ipv6_global_unicast(&Ipv6Addr::new(
1051 0x3fff, 0xffff, 0, 0, 0, 0, 0, 1
1052 )));
1053 assert!(!is_ipv6_global_unicast(&Ipv6Addr::new(
1054 0xfe80, 0, 0, 0, 0, 0, 0, 1
1055 )));
1056 assert!(!is_ipv6_global_unicast(&Ipv6Addr::new(
1057 0xfc00, 0, 0, 0, 0, 0, 0, 1
1058 )));
1059 }
1060}