1#![forbid(unsafe_code)]
10
11use crate::state_machine::{CommissioningError, RemediationHint, Stage};
12
13pub const CLUSTER_ID: u32 = 0x0031;
15
16pub mod command_id {
18 pub const ADD_OR_UPDATE_WIFI_NETWORK: u32 = 0x02;
20 pub const ADD_OR_UPDATE_THREAD_NETWORK: u32 = 0x03;
22 pub const CONNECT_NETWORK: u32 = 0x06;
24}
25
26pub mod response_id {
28 pub const NETWORK_CONFIG_RESPONSE: u32 = 0x05;
31 pub const CONNECT_NETWORK_RESPONSE: u32 = 0x07;
33}
34
35pub mod attribute_id {
37 pub const FEATURE_MAP: u32 = 0xFFFC;
39 pub const CONNECT_MAX_TIME_SECONDS: u32 = 0x0003;
46}
47
48bitflags::bitflags! {
49 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
54 pub struct NetworkCommissioningFeature: u32 {
55 const WIFI = 1 << 0;
57 const THREAD = 1 << 1;
59 const ETHERNET = 1 << 2;
61 }
62}
63
64#[must_use]
70#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_add_or_update_wifi_network(
72 ssid: &[u8],
73 credentials: &[u8],
74 breadcrumb: u64,
75) -> Vec<u8> {
76 use matter_codec::{Tag, TlvWriter};
77 let mut buf = Vec::new();
78 let mut w = TlvWriter::new(&mut buf);
79 w.start_structure(Tag::Anonymous)
80 .expect("infallible: vec writer");
81 w.put_bytes(Tag::Context(0), ssid)
82 .expect("infallible: vec writer");
83 w.put_bytes(Tag::Context(1), credentials)
84 .expect("infallible: vec writer");
85 w.put_uint(Tag::Context(2), breadcrumb)
86 .expect("infallible: vec writer");
87 w.end_container().expect("infallible: vec writer");
88 buf
89}
90
91#[must_use]
101#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_add_or_update_thread_network(operational_dataset: &[u8], breadcrumb: u64) -> Vec<u8> {
103 use matter_codec::{Tag, TlvWriter};
104 let mut buf = Vec::new();
105 let mut w = TlvWriter::new(&mut buf);
106 w.start_structure(Tag::Anonymous)
107 .expect("infallible: vec writer");
108 w.put_bytes(Tag::Context(0), operational_dataset)
109 .expect("infallible: vec writer");
110 w.put_uint(Tag::Context(1), breadcrumb)
111 .expect("infallible: vec writer");
112 w.end_container().expect("infallible: vec writer");
113 buf
114}
115
116#[must_use]
123#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_connect_network(network_id: &[u8], breadcrumb: u64) -> Vec<u8> {
125 use matter_codec::{Tag, TlvWriter};
126 let mut buf = Vec::new();
127 let mut w = TlvWriter::new(&mut buf);
128 w.start_structure(Tag::Anonymous)
129 .expect("infallible: vec writer");
130 w.put_bytes(Tag::Context(0), network_id)
131 .expect("infallible: vec writer");
132 w.put_uint(Tag::Context(1), breadcrumb)
133 .expect("infallible: vec writer");
134 w.end_container().expect("infallible: vec writer");
135 buf
136}
137
138pub fn decode_feature_map(tlv: &[u8]) -> Result<NetworkCommissioningFeature, CommissioningError> {
150 use matter_codec::{Element, TlvReader, Value};
151 let mut reader = TlvReader::new(tlv);
152 match reader
153 .next()
154 .map_err(|_| CommissioningError::MalformedResponse(Stage::ReadNetworkCommissioningInfo))?
155 {
156 Some(Element::Scalar {
157 value: Value::Uint(raw),
158 ..
159 }) => {
160 let truncated = u32::try_from(raw).map_err(|_| {
161 CommissioningError::MalformedResponse(Stage::ReadNetworkCommissioningInfo)
162 })?;
163 Ok(NetworkCommissioningFeature::from_bits_truncate(truncated))
164 }
165 _ => Err(CommissioningError::MalformedResponse(
166 Stage::ReadNetworkCommissioningInfo,
167 )),
168 }
169}
170
171pub fn decode_connect_max_time_seconds(tlv: &[u8]) -> Result<u16, CommissioningError> {
192 use matter_codec::{Element, TlvReader, Value};
193 let mut reader = TlvReader::new(tlv);
194 match reader
195 .next()
196 .map_err(|_| CommissioningError::MalformedResponse(Stage::ReadNetworkCommissioningInfo))?
197 {
198 Some(Element::Scalar {
199 value: Value::Uint(raw),
200 ..
201 }) => Ok(u16::try_from(raw).unwrap_or(u16::MAX)),
202 _ => Err(CommissioningError::MalformedResponse(
203 Stage::ReadNetworkCommissioningInfo,
204 )),
205 }
206}
207
208pub(crate) fn truncate_utf8(mut s: String, max_bytes: usize) -> String {
212 if s.len() <= max_bytes {
213 return s;
214 }
215 let mut end = max_bytes;
216 while end > 0 && !s.is_char_boundary(end) {
217 end -= 1;
218 }
219 s.truncate(end);
220 s
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
226#[non_exhaustive]
227pub struct NetworkConfigResponse {
228 pub networking_status: u8,
230 pub debug_text: Option<String>,
234 }
237
238#[derive(Debug, Clone, PartialEq, Eq)]
240#[non_exhaustive]
241pub struct ConnectNetworkResponse {
242 pub networking_status: u8,
244 pub debug_text: Option<String>,
248 pub error_value: Option<i32>,
250}
251
252pub fn decode_network_config_response(
266 stage: Stage,
267 tlv: &[u8],
268) -> Result<NetworkConfigResponse, CommissioningError> {
269 use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
270 let mut reader = TlvReader::new(tlv);
271 match reader
272 .next()
273 .map_err(|_| CommissioningError::MalformedResponse(stage))?
274 {
275 Some(Element::ContainerStart {
276 tag: Tag::Anonymous,
277 kind: ContainerKind::Structure,
278 }) => {}
279 _ => return Err(CommissioningError::MalformedResponse(stage)),
280 }
281 let mut networking_status: Option<u8> = None;
282 let mut debug_text: Option<String> = None;
283 loop {
284 match reader
285 .next()
286 .map_err(|_| CommissioningError::MalformedResponse(stage))?
287 {
288 Some(Element::ContainerEnd) => break,
289 Some(Element::Scalar {
290 tag: Tag::Context(0),
291 value: Value::Uint(v),
292 }) => {
293 if networking_status.is_some() {
294 return Err(CommissioningError::MalformedResponse(stage));
295 }
296 networking_status = Some(
297 u8::try_from(v).map_err(|_| CommissioningError::MalformedResponse(stage))?,
298 );
299 }
300 Some(Element::Scalar {
301 tag: Tag::Context(1),
302 value: Value::Utf8(s),
303 }) => {
304 if debug_text.is_some() {
305 return Err(CommissioningError::MalformedResponse(stage));
306 }
307 debug_text = Some(truncate_utf8(s, 512));
309 }
310 Some(Element::Scalar { .. } | Element::ContainerStart { .. }) => {}
313 None | Some(_) => return Err(CommissioningError::MalformedResponse(stage)),
314 }
315 }
316 let networking_status =
317 networking_status.ok_or(CommissioningError::MalformedResponse(stage))?;
318 Ok(NetworkConfigResponse {
319 networking_status,
320 debug_text,
321 })
322}
323
324pub fn decode_connect_network_response(
334 stage: Stage,
335 tlv: &[u8],
336) -> Result<ConnectNetworkResponse, CommissioningError> {
337 use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
338 let mut reader = TlvReader::new(tlv);
339 match reader
340 .next()
341 .map_err(|_| CommissioningError::MalformedResponse(stage))?
342 {
343 Some(Element::ContainerStart {
344 tag: Tag::Anonymous,
345 kind: ContainerKind::Structure,
346 }) => {}
347 _ => return Err(CommissioningError::MalformedResponse(stage)),
348 }
349 let mut networking_status: Option<u8> = None;
350 let mut debug_text: Option<String> = None;
351 let mut error_value: Option<i32> = None;
352 loop {
353 match reader
354 .next()
355 .map_err(|_| CommissioningError::MalformedResponse(stage))?
356 {
357 Some(Element::ContainerEnd) => break,
358 Some(Element::Scalar {
359 tag: Tag::Context(0),
360 value: Value::Uint(v),
361 }) => {
362 if networking_status.is_some() {
363 return Err(CommissioningError::MalformedResponse(stage));
364 }
365 networking_status = Some(
366 u8::try_from(v).map_err(|_| CommissioningError::MalformedResponse(stage))?,
367 );
368 }
369 Some(Element::Scalar {
370 tag: Tag::Context(1),
371 value: Value::Utf8(s),
372 }) => {
373 if debug_text.is_some() {
374 return Err(CommissioningError::MalformedResponse(stage));
375 }
376 debug_text = Some(truncate_utf8(s, 512));
378 }
379 Some(Element::Scalar {
380 tag: Tag::Context(2),
381 value: Value::Int(v),
382 }) => {
383 if error_value.is_some() {
384 return Err(CommissioningError::MalformedResponse(stage));
385 }
386 error_value = Some(
387 i32::try_from(v).map_err(|_| CommissioningError::MalformedResponse(stage))?,
388 );
389 }
390 Some(Element::Scalar { .. } | Element::ContainerStart { .. }) => {}
392 None | Some(_) => return Err(CommissioningError::MalformedResponse(stage)),
393 }
394 }
395 let networking_status =
396 networking_status.ok_or(CommissioningError::MalformedResponse(stage))?;
397 Ok(ConnectNetworkResponse {
398 networking_status,
399 debug_text,
400 error_value,
401 })
402}
403
404#[must_use]
412pub const fn remediation_for(networking_status: u8) -> RemediationHint {
413 match networking_status {
414 2 => RemediationHint::DeviceNetworkSlotsFull,
415 3 | 5 => RemediationHint::CheckSsid,
416 6 => RemediationHint::CheckRegulatoryRegion,
417 7 => RemediationHint::CheckPassphrase,
418 8 => RemediationHint::UpgradeSecurityMode,
419 10 | 11 => RemediationHint::DeviceIpStackFailure,
420 _ => RemediationHint::None,
421 }
422}
423
424#[cfg(test)]
425#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
427 use super::*;
428
429 #[test]
430 fn feature_bits_disjoint() {
431 assert_eq!(NetworkCommissioningFeature::WIFI.bits(), 0b001);
432 assert_eq!(NetworkCommissioningFeature::THREAD.bits(), 0b010);
433 assert_eq!(NetworkCommissioningFeature::ETHERNET.bits(), 0b100);
434 }
435
436 #[test]
437 fn cluster_id_is_0x0031() {
438 assert_eq!(CLUSTER_ID, 0x0031);
439 }
440
441 #[test]
442 fn add_or_update_wifi_network_matter_no_creds_matches_spec_bytes() {
443 let bytes = encode_add_or_update_wifi_network(b"matter", b"", 0);
444 assert_eq!(
445 bytes,
446 vec![
447 0x15, 0x30, 0x00, 0x06, b'm', b'a', b't', b't', b'e', b'r', 0x30, 0x01, 0x00, 0x24,
448 0x02, 0x00, 0x18,
449 ],
450 "encoded bytes: {bytes:02x?}",
451 );
452 }
453
454 #[test]
455 fn add_or_update_wifi_network_with_creds_includes_passphrase_bytes() {
456 let bytes = encode_add_or_update_wifi_network(b"matter", b"hunter22", 1);
457 assert_eq!(bytes.first(), Some(&0x15));
459 assert_eq!(bytes.last(), Some(&0x18));
460 let window = b"hunter22";
461 assert!(
462 bytes.windows(window.len()).any(|w| w == window),
463 "credentials should appear in the payload literal",
464 );
465 }
466
467 #[test]
468 fn add_or_update_thread_network_bytes_match_vector() {
469 let ds = hex::decode(
474 "0e08000000000001000000030000184a0300001235060004001fffe0020878\
475 96217f787f6ebe0708fdec3f34f3cd2020051071dccee3f164f15da92254e0\
476 b9c8a3a5030f4f70656e5468726561642d3839643701\
477 0289d70410dc4b544c7a58671a2ce4f876f5d6dcd90c0402a0f7f8",
478 )
479 .expect("valid hex literal");
480 assert_eq!(ds.len(), 111, "reference dataset must be 111 bytes");
481 let got = encode_add_or_update_thread_network(&ds, 1);
482 assert_eq!(
483 hex::encode(&got),
484 "1530006f0e08000000000001000000030000184a0300001235060004001fff\
485 e002087896217f787f6ebe0708fdec3f34f3cd2020051071dccee3f164f15d\
486 a92254e0b9c8a3a5030f4f70656e5468726561642d383964370102\
487 89d70410dc4b544c7a58671a2ce4f876f5d6dcd90c0402a0f7f824010118",
488 "encoded bytes: {got:02x?}",
489 );
490 }
491
492 #[test]
493 fn connect_network_thread_bytes_match_vector() {
494 let ext_pan_id = [0x78, 0x96, 0x21, 0x7f, 0x78, 0x7f, 0x6e, 0xbe];
498 let got = encode_connect_network(&ext_pan_id, 1);
499 assert_eq!(
500 hex::encode(&got),
501 "153000087896217f787f6ebe24010118",
502 "encoded bytes: {got:02x?}",
503 );
504 }
505
506 #[test]
507 fn connect_network_matter_matches_spec_bytes() {
508 let bytes = encode_connect_network(b"matter", 0);
509 assert_eq!(
510 bytes,
511 vec![
512 0x15, 0x30, 0x00, 0x06, b'm', b'a', b't', b't', b'e', b'r', 0x24, 0x01, 0x00, 0x18,
513 ],
514 "encoded bytes: {bytes:02x?}",
515 );
516 }
517
518 #[test]
519 fn decode_feature_map_round_trips_all_8_combinations() {
520 for raw in 0u8..8 {
525 let tlv = vec![0x04, raw];
526 let decoded = decode_feature_map(&tlv).expect("happy path decodes");
527 assert_eq!(decoded.bits(), u32::from(raw));
528 }
529 }
530
531 #[test]
532 fn decode_feature_map_rejects_non_uint_tlv() {
533 let tlv = vec![0x10, 0x00];
535 let err = decode_feature_map(&tlv).expect_err("should fail");
536 assert!(
537 matches!(err, CommissioningError::MalformedResponse(_)),
538 "got {err:?}",
539 );
540 }
541
542 #[test]
543 fn decode_feature_map_truncates_high_bits_safely() {
544 let tlv = vec![0x04, 0x0F];
548 let decoded = decode_feature_map(&tlv).expect("decodes");
549 assert_eq!(
550 decoded,
551 NetworkCommissioningFeature::WIFI
552 | NetworkCommissioningFeature::THREAD
553 | NetworkCommissioningFeature::ETHERNET,
554 );
555 }
556
557 #[test]
558 fn decode_connect_max_time_seconds_round_trips() {
559 assert_eq!(decode_connect_max_time_seconds(&[0x04, 30]).unwrap(), 30);
561 assert_eq!(
563 decode_connect_max_time_seconds(&[0x05, 0x2C, 0x01]).unwrap(),
564 300
565 );
566 }
567
568 #[test]
569 fn decode_connect_max_time_seconds_clamps_oversize_to_u16_max() {
570 let tlv = vec![0x06, 0x00, 0x00, 0x01, 0x00];
572 assert_eq!(decode_connect_max_time_seconds(&tlv).unwrap(), u16::MAX);
573 }
574
575 #[test]
576 fn decode_connect_max_time_seconds_rejects_non_uint() {
577 let err = decode_connect_max_time_seconds(&[0x10, 0x00]).expect_err("should fail");
579 assert!(
580 matches!(err, CommissioningError::MalformedResponse(_)),
581 "got {err:?}",
582 );
583 }
584
585 #[test]
586 fn network_config_response_ok_round_trips() {
587 let tlv = vec![0x15, 0x24, 0x00, 0x00, 0x18];
589 let decoded =
590 decode_network_config_response(Stage::NetworkSetup, &tlv).expect("happy path decodes");
591 assert_eq!(decoded.networking_status, 0);
592 assert_eq!(decoded.debug_text, None);
593 }
594
595 #[test]
596 fn network_config_response_auth_failure_with_debug_text() {
597 let tlv = vec![
599 0x15, 0x24, 0x00, 0x07, 0x2C, 0x01, 0x08, b'w', b'r', b'o', b'n', b'g', b'-', b'p',
600 b'w', 0x18,
601 ];
602 let decoded =
603 decode_network_config_response(Stage::NetworkSetup, &tlv).expect("happy path decodes");
604 assert_eq!(decoded.networking_status, 7);
605 assert_eq!(decoded.debug_text.as_deref(), Some("wrong-pw"));
606 }
607
608 #[test]
609 fn network_config_response_malformed_returns_error() {
610 let err =
611 decode_network_config_response(Stage::NetworkSetup, &[0xFF]).expect_err("should fail");
612 assert!(
613 matches!(err, CommissioningError::MalformedResponse(_)),
614 "got {err:?}"
615 );
616 }
617
618 #[test]
619 fn connect_network_response_ok_round_trips() {
620 let tlv = vec![0x15, 0x24, 0x00, 0x00, 0x18];
621 let decoded = decode_connect_network_response(Stage::NetworkEnable, &tlv)
622 .expect("happy path decodes");
623 assert_eq!(decoded.networking_status, 0);
624 assert_eq!(decoded.debug_text, None);
625 assert_eq!(decoded.error_value, None);
626 }
627
628 #[test]
629 fn connect_network_response_carries_error_value() {
630 let tlv = vec![
632 0x15, 0x24, 0x00, 0x09, 0x20, 0x02, 0x0A, 0x18,
635 ];
636 let decoded = decode_connect_network_response(Stage::NetworkEnable, &tlv).expect("decodes");
637 assert_eq!(decoded.networking_status, 9);
638 assert_eq!(decoded.error_value, Some(10));
639 }
640
641 #[test]
642 fn connect_network_response_malformed_returns_error() {
643 let err = decode_connect_network_response(Stage::NetworkEnable, &[0xFF])
644 .expect_err("should fail");
645 assert!(
646 matches!(err, CommissioningError::MalformedResponse(_)),
647 "got {err:?}"
648 );
649 }
650
651 #[test]
652 fn truncate_utf8_caps_bytes_without_splitting_chars() {
653 let s = "a".repeat(600);
655 assert_eq!(truncate_utf8(s, 512).len(), 512);
656
657 assert_eq!(truncate_utf8("short".to_string(), 512), "short");
659
660 let mut s = "a".repeat(511);
664 s.push('é');
665 let t = truncate_utf8(s, 512);
666 assert_eq!(t.len(), 511, "must floor to the char boundary");
667 assert!(t.is_char_boundary(t.len()));
668 }
669
670 #[test]
671 fn decode_caps_debug_text_at_512_bytes() {
672 use matter_codec::{Tag, TlvWriter};
673 let long_text = "x".repeat(600);
675 let mut buf = Vec::new();
676 let mut w = TlvWriter::new(&mut buf);
677 w.start_structure(Tag::Anonymous).unwrap();
678 w.put_uint(Tag::Context(0), 5).unwrap();
679 w.put_utf8(Tag::Context(1), &long_text).unwrap();
680 w.end_container().unwrap();
681
682 let resp = decode_network_config_response(Stage::NetworkSetup, &buf).unwrap();
683 assert_eq!(resp.networking_status, 5);
684 assert_eq!(resp.debug_text.unwrap().len(), 512, "capped at spec bound");
685 }
686
687 #[test]
688 fn decode_connect_network_response_caps_debug_text_at_512_bytes() {
689 use matter_codec::{Tag, TlvWriter};
690 let long_text = "x".repeat(600);
692 let mut buf = Vec::new();
693 let mut w = TlvWriter::new(&mut buf);
694 w.start_structure(Tag::Anonymous).unwrap();
695 w.put_uint(Tag::Context(0), 5).unwrap();
696 w.put_utf8(Tag::Context(1), &long_text).unwrap();
697 w.end_container().unwrap();
698
699 let resp = decode_connect_network_response(Stage::NetworkEnable, &buf).unwrap();
700 assert_eq!(resp.networking_status, 5);
701 assert_eq!(resp.debug_text.unwrap().len(), 512, "capped at spec bound");
702 }
703
704 #[test]
705 fn remediation_for_table_matches_spec() {
706 use RemediationHint::*;
707 let table: &[(u8, RemediationHint)] = &[
708 (0, None), (1, None), (2, DeviceNetworkSlotsFull), (3, CheckSsid), (4, None), (5, CheckSsid), (6, CheckRegulatoryRegion), (7, CheckPassphrase), (8, UpgradeSecurityMode), (9, None), (10, DeviceIpStackFailure), (11, DeviceIpStackFailure), (12, None), ];
722 for (code, expected) in table {
723 assert_eq!(
724 remediation_for(*code),
725 *expected,
726 "remediation_for({code}) mismatch",
727 );
728 }
729 assert_eq!(remediation_for(99), RemediationHint::None);
731 assert_eq!(remediation_for(u8::MAX), RemediationHint::None);
732 }
733}