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
208#[derive(Debug, Clone, PartialEq, Eq)]
211#[non_exhaustive]
212pub struct NetworkConfigResponse {
213 pub networking_status: u8,
215 pub debug_text: Option<String>,
217 }
220
221#[derive(Debug, Clone, PartialEq, Eq)]
223#[non_exhaustive]
224pub struct ConnectNetworkResponse {
225 pub networking_status: u8,
227 pub debug_text: Option<String>,
229 pub error_value: Option<i32>,
231}
232
233pub fn decode_network_config_response(
247 stage: Stage,
248 tlv: &[u8],
249) -> Result<NetworkConfigResponse, CommissioningError> {
250 use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
251 let mut reader = TlvReader::new(tlv);
252 match reader
253 .next()
254 .map_err(|_| CommissioningError::MalformedResponse(stage))?
255 {
256 Some(Element::ContainerStart {
257 tag: Tag::Anonymous,
258 kind: ContainerKind::Structure,
259 }) => {}
260 _ => return Err(CommissioningError::MalformedResponse(stage)),
261 }
262 let mut networking_status: Option<u8> = None;
263 let mut debug_text: Option<String> = None;
264 loop {
265 match reader
266 .next()
267 .map_err(|_| CommissioningError::MalformedResponse(stage))?
268 {
269 Some(Element::ContainerEnd) => break,
270 Some(Element::Scalar {
271 tag: Tag::Context(0),
272 value: Value::Uint(v),
273 }) => {
274 if networking_status.is_some() {
275 return Err(CommissioningError::MalformedResponse(stage));
276 }
277 networking_status = Some(
278 u8::try_from(v).map_err(|_| CommissioningError::MalformedResponse(stage))?,
279 );
280 }
281 Some(Element::Scalar {
282 tag: Tag::Context(1),
283 value: Value::Utf8(s),
284 }) => {
285 if debug_text.is_some() {
286 return Err(CommissioningError::MalformedResponse(stage));
287 }
288 debug_text = Some(s);
289 }
290 Some(Element::Scalar { .. } | Element::ContainerStart { .. }) => {}
293 None | Some(_) => return Err(CommissioningError::MalformedResponse(stage)),
294 }
295 }
296 let networking_status =
297 networking_status.ok_or(CommissioningError::MalformedResponse(stage))?;
298 Ok(NetworkConfigResponse {
299 networking_status,
300 debug_text,
301 })
302}
303
304pub fn decode_connect_network_response(
314 stage: Stage,
315 tlv: &[u8],
316) -> Result<ConnectNetworkResponse, CommissioningError> {
317 use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
318 let mut reader = TlvReader::new(tlv);
319 match reader
320 .next()
321 .map_err(|_| CommissioningError::MalformedResponse(stage))?
322 {
323 Some(Element::ContainerStart {
324 tag: Tag::Anonymous,
325 kind: ContainerKind::Structure,
326 }) => {}
327 _ => return Err(CommissioningError::MalformedResponse(stage)),
328 }
329 let mut networking_status: Option<u8> = None;
330 let mut debug_text: Option<String> = None;
331 let mut error_value: Option<i32> = None;
332 loop {
333 match reader
334 .next()
335 .map_err(|_| CommissioningError::MalformedResponse(stage))?
336 {
337 Some(Element::ContainerEnd) => break,
338 Some(Element::Scalar {
339 tag: Tag::Context(0),
340 value: Value::Uint(v),
341 }) => {
342 if networking_status.is_some() {
343 return Err(CommissioningError::MalformedResponse(stage));
344 }
345 networking_status = Some(
346 u8::try_from(v).map_err(|_| CommissioningError::MalformedResponse(stage))?,
347 );
348 }
349 Some(Element::Scalar {
350 tag: Tag::Context(1),
351 value: Value::Utf8(s),
352 }) => {
353 if debug_text.is_some() {
354 return Err(CommissioningError::MalformedResponse(stage));
355 }
356 debug_text = Some(s);
357 }
358 Some(Element::Scalar {
359 tag: Tag::Context(2),
360 value: Value::Int(v),
361 }) => {
362 if error_value.is_some() {
363 return Err(CommissioningError::MalformedResponse(stage));
364 }
365 error_value = Some(
366 i32::try_from(v).map_err(|_| CommissioningError::MalformedResponse(stage))?,
367 );
368 }
369 Some(Element::Scalar { .. } | Element::ContainerStart { .. }) => {}
371 None | Some(_) => return Err(CommissioningError::MalformedResponse(stage)),
372 }
373 }
374 let networking_status =
375 networking_status.ok_or(CommissioningError::MalformedResponse(stage))?;
376 Ok(ConnectNetworkResponse {
377 networking_status,
378 debug_text,
379 error_value,
380 })
381}
382
383#[must_use]
391pub const fn remediation_for(networking_status: u8) -> RemediationHint {
392 match networking_status {
393 2 => RemediationHint::DeviceNetworkSlotsFull,
394 3 | 5 => RemediationHint::CheckSsid,
395 6 => RemediationHint::CheckRegulatoryRegion,
396 7 => RemediationHint::CheckPassphrase,
397 8 => RemediationHint::UpgradeSecurityMode,
398 10 | 11 => RemediationHint::DeviceIpStackFailure,
399 _ => RemediationHint::None,
400 }
401}
402
403#[cfg(test)]
404#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
406 use super::*;
407
408 #[test]
409 fn feature_bits_disjoint() {
410 assert_eq!(NetworkCommissioningFeature::WIFI.bits(), 0b001);
411 assert_eq!(NetworkCommissioningFeature::THREAD.bits(), 0b010);
412 assert_eq!(NetworkCommissioningFeature::ETHERNET.bits(), 0b100);
413 }
414
415 #[test]
416 fn cluster_id_is_0x0031() {
417 assert_eq!(CLUSTER_ID, 0x0031);
418 }
419
420 #[test]
421 fn add_or_update_wifi_network_matter_no_creds_matches_spec_bytes() {
422 let bytes = encode_add_or_update_wifi_network(b"matter", b"", 0);
423 assert_eq!(
424 bytes,
425 vec![
426 0x15, 0x30, 0x00, 0x06, b'm', b'a', b't', b't', b'e', b'r', 0x30, 0x01, 0x00, 0x24,
427 0x02, 0x00, 0x18,
428 ],
429 "encoded bytes: {bytes:02x?}",
430 );
431 }
432
433 #[test]
434 fn add_or_update_wifi_network_with_creds_includes_passphrase_bytes() {
435 let bytes = encode_add_or_update_wifi_network(b"matter", b"hunter22", 1);
436 assert_eq!(bytes.first(), Some(&0x15));
438 assert_eq!(bytes.last(), Some(&0x18));
439 let window = b"hunter22";
440 assert!(
441 bytes.windows(window.len()).any(|w| w == window),
442 "credentials should appear in the payload literal",
443 );
444 }
445
446 #[test]
447 fn add_or_update_thread_network_bytes_match_vector() {
448 let ds = hex::decode(
453 "0e08000000000001000000030000184a0300001235060004001fffe0020878\
454 96217f787f6ebe0708fdec3f34f3cd2020051071dccee3f164f15da92254e0\
455 b9c8a3a5030f4f70656e5468726561642d3839643701\
456 0289d70410dc4b544c7a58671a2ce4f876f5d6dcd90c0402a0f7f8",
457 )
458 .expect("valid hex literal");
459 assert_eq!(ds.len(), 111, "reference dataset must be 111 bytes");
460 let got = encode_add_or_update_thread_network(&ds, 1);
461 assert_eq!(
462 hex::encode(&got),
463 "1530006f0e08000000000001000000030000184a0300001235060004001fff\
464 e002087896217f787f6ebe0708fdec3f34f3cd2020051071dccee3f164f15d\
465 a92254e0b9c8a3a5030f4f70656e5468726561642d383964370102\
466 89d70410dc4b544c7a58671a2ce4f876f5d6dcd90c0402a0f7f824010118",
467 "encoded bytes: {got:02x?}",
468 );
469 }
470
471 #[test]
472 fn connect_network_thread_bytes_match_vector() {
473 let ext_pan_id = [0x78, 0x96, 0x21, 0x7f, 0x78, 0x7f, 0x6e, 0xbe];
477 let got = encode_connect_network(&ext_pan_id, 1);
478 assert_eq!(
479 hex::encode(&got),
480 "153000087896217f787f6ebe24010118",
481 "encoded bytes: {got:02x?}",
482 );
483 }
484
485 #[test]
486 fn connect_network_matter_matches_spec_bytes() {
487 let bytes = encode_connect_network(b"matter", 0);
488 assert_eq!(
489 bytes,
490 vec![
491 0x15, 0x30, 0x00, 0x06, b'm', b'a', b't', b't', b'e', b'r', 0x24, 0x01, 0x00, 0x18,
492 ],
493 "encoded bytes: {bytes:02x?}",
494 );
495 }
496
497 #[test]
498 fn decode_feature_map_round_trips_all_8_combinations() {
499 for raw in 0u8..8 {
504 let tlv = vec![0x04, raw];
505 let decoded = decode_feature_map(&tlv).expect("happy path decodes");
506 assert_eq!(decoded.bits(), u32::from(raw));
507 }
508 }
509
510 #[test]
511 fn decode_feature_map_rejects_non_uint_tlv() {
512 let tlv = vec![0x10, 0x00];
514 let err = decode_feature_map(&tlv).expect_err("should fail");
515 assert!(
516 matches!(err, CommissioningError::MalformedResponse(_)),
517 "got {err:?}",
518 );
519 }
520
521 #[test]
522 fn decode_feature_map_truncates_high_bits_safely() {
523 let tlv = vec![0x04, 0x0F];
527 let decoded = decode_feature_map(&tlv).expect("decodes");
528 assert_eq!(
529 decoded,
530 NetworkCommissioningFeature::WIFI
531 | NetworkCommissioningFeature::THREAD
532 | NetworkCommissioningFeature::ETHERNET,
533 );
534 }
535
536 #[test]
537 fn decode_connect_max_time_seconds_round_trips() {
538 assert_eq!(decode_connect_max_time_seconds(&[0x04, 30]).unwrap(), 30);
540 assert_eq!(
542 decode_connect_max_time_seconds(&[0x05, 0x2C, 0x01]).unwrap(),
543 300
544 );
545 }
546
547 #[test]
548 fn decode_connect_max_time_seconds_clamps_oversize_to_u16_max() {
549 let tlv = vec![0x06, 0x00, 0x00, 0x01, 0x00];
551 assert_eq!(decode_connect_max_time_seconds(&tlv).unwrap(), u16::MAX);
552 }
553
554 #[test]
555 fn decode_connect_max_time_seconds_rejects_non_uint() {
556 let err = decode_connect_max_time_seconds(&[0x10, 0x00]).expect_err("should fail");
558 assert!(
559 matches!(err, CommissioningError::MalformedResponse(_)),
560 "got {err:?}",
561 );
562 }
563
564 #[test]
565 fn network_config_response_ok_round_trips() {
566 let tlv = vec![0x15, 0x24, 0x00, 0x00, 0x18];
568 let decoded =
569 decode_network_config_response(Stage::NetworkSetup, &tlv).expect("happy path decodes");
570 assert_eq!(decoded.networking_status, 0);
571 assert_eq!(decoded.debug_text, None);
572 }
573
574 #[test]
575 fn network_config_response_auth_failure_with_debug_text() {
576 let tlv = vec![
578 0x15, 0x24, 0x00, 0x07, 0x2C, 0x01, 0x08, b'w', b'r', b'o', b'n', b'g', b'-', b'p',
579 b'w', 0x18,
580 ];
581 let decoded =
582 decode_network_config_response(Stage::NetworkSetup, &tlv).expect("happy path decodes");
583 assert_eq!(decoded.networking_status, 7);
584 assert_eq!(decoded.debug_text.as_deref(), Some("wrong-pw"));
585 }
586
587 #[test]
588 fn network_config_response_malformed_returns_error() {
589 let err =
590 decode_network_config_response(Stage::NetworkSetup, &[0xFF]).expect_err("should fail");
591 assert!(
592 matches!(err, CommissioningError::MalformedResponse(_)),
593 "got {err:?}"
594 );
595 }
596
597 #[test]
598 fn connect_network_response_ok_round_trips() {
599 let tlv = vec![0x15, 0x24, 0x00, 0x00, 0x18];
600 let decoded = decode_connect_network_response(Stage::NetworkEnable, &tlv)
601 .expect("happy path decodes");
602 assert_eq!(decoded.networking_status, 0);
603 assert_eq!(decoded.debug_text, None);
604 assert_eq!(decoded.error_value, None);
605 }
606
607 #[test]
608 fn connect_network_response_carries_error_value() {
609 let tlv = vec![
611 0x15, 0x24, 0x00, 0x09, 0x20, 0x02, 0x0A, 0x18,
614 ];
615 let decoded = decode_connect_network_response(Stage::NetworkEnable, &tlv).expect("decodes");
616 assert_eq!(decoded.networking_status, 9);
617 assert_eq!(decoded.error_value, Some(10));
618 }
619
620 #[test]
621 fn connect_network_response_malformed_returns_error() {
622 let err = decode_connect_network_response(Stage::NetworkEnable, &[0xFF])
623 .expect_err("should fail");
624 assert!(
625 matches!(err, CommissioningError::MalformedResponse(_)),
626 "got {err:?}"
627 );
628 }
629
630 #[test]
631 fn remediation_for_table_matches_spec() {
632 use RemediationHint::*;
633 let table: &[(u8, RemediationHint)] = &[
634 (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), ];
648 for (code, expected) in table {
649 assert_eq!(
650 remediation_for(*code),
651 *expected,
652 "remediation_for({code}) mismatch",
653 );
654 }
655 assert_eq!(remediation_for(99), RemediationHint::None);
657 assert_eq!(remediation_for(u8::MAX), RemediationHint::None);
658 }
659}