#![forbid(unsafe_code)]
use crate::state_machine::{CommissioningError, RemediationHint, Stage};
pub const CLUSTER_ID: u32 = 0x0031;
pub mod command_id {
pub const ADD_OR_UPDATE_WIFI_NETWORK: u32 = 0x02;
pub const ADD_OR_UPDATE_THREAD_NETWORK: u32 = 0x03;
pub const CONNECT_NETWORK: u32 = 0x06;
}
pub mod response_id {
pub const NETWORK_CONFIG_RESPONSE: u32 = 0x05;
pub const CONNECT_NETWORK_RESPONSE: u32 = 0x07;
}
pub mod attribute_id {
pub const FEATURE_MAP: u32 = 0xFFFC;
pub const CONNECT_MAX_TIME_SECONDS: u32 = 0x0003;
}
bitflags::bitflags! {
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct NetworkCommissioningFeature: u32 {
const WIFI = 1 << 0;
const THREAD = 1 << 1;
const ETHERNET = 1 << 2;
}
}
#[must_use]
#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_add_or_update_wifi_network(
ssid: &[u8],
credentials: &[u8],
breadcrumb: u64,
) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
w.put_bytes(Tag::Context(0), ssid)
.expect("infallible: vec writer");
w.put_bytes(Tag::Context(1), credentials)
.expect("infallible: vec writer");
w.put_uint(Tag::Context(2), breadcrumb)
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer");
buf
}
#[must_use]
#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_add_or_update_thread_network(operational_dataset: &[u8], breadcrumb: u64) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
w.put_bytes(Tag::Context(0), operational_dataset)
.expect("infallible: vec writer");
w.put_uint(Tag::Context(1), breadcrumb)
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer");
buf
}
#[must_use]
#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_connect_network(network_id: &[u8], breadcrumb: u64) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
w.put_bytes(Tag::Context(0), network_id)
.expect("infallible: vec writer");
w.put_uint(Tag::Context(1), breadcrumb)
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer");
buf
}
pub fn decode_feature_map(tlv: &[u8]) -> Result<NetworkCommissioningFeature, CommissioningError> {
use matter_codec::{Element, TlvReader, Value};
let mut reader = TlvReader::new(tlv);
match reader
.next()
.map_err(|_| CommissioningError::MalformedResponse(Stage::ReadNetworkCommissioningInfo))?
{
Some(Element::Scalar {
value: Value::Uint(raw),
..
}) => {
let truncated = u32::try_from(raw).map_err(|_| {
CommissioningError::MalformedResponse(Stage::ReadNetworkCommissioningInfo)
})?;
Ok(NetworkCommissioningFeature::from_bits_truncate(truncated))
}
_ => Err(CommissioningError::MalformedResponse(
Stage::ReadNetworkCommissioningInfo,
)),
}
}
pub fn decode_connect_max_time_seconds(tlv: &[u8]) -> Result<u16, CommissioningError> {
use matter_codec::{Element, TlvReader, Value};
let mut reader = TlvReader::new(tlv);
match reader
.next()
.map_err(|_| CommissioningError::MalformedResponse(Stage::ReadNetworkCommissioningInfo))?
{
Some(Element::Scalar {
value: Value::Uint(raw),
..
}) => Ok(u16::try_from(raw).unwrap_or(u16::MAX)),
_ => Err(CommissioningError::MalformedResponse(
Stage::ReadNetworkCommissioningInfo,
)),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct NetworkConfigResponse {
pub networking_status: u8,
pub debug_text: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ConnectNetworkResponse {
pub networking_status: u8,
pub debug_text: Option<String>,
pub error_value: Option<i32>,
}
pub fn decode_network_config_response(
stage: Stage,
tlv: &[u8],
) -> Result<NetworkConfigResponse, CommissioningError> {
use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
let mut reader = TlvReader::new(tlv);
match reader
.next()
.map_err(|_| CommissioningError::MalformedResponse(stage))?
{
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure,
}) => {}
_ => return Err(CommissioningError::MalformedResponse(stage)),
}
let mut networking_status: Option<u8> = None;
let mut debug_text: Option<String> = None;
loop {
match reader
.next()
.map_err(|_| CommissioningError::MalformedResponse(stage))?
{
Some(Element::ContainerEnd) => break,
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Uint(v),
}) => {
if networking_status.is_some() {
return Err(CommissioningError::MalformedResponse(stage));
}
networking_status = Some(
u8::try_from(v).map_err(|_| CommissioningError::MalformedResponse(stage))?,
);
}
Some(Element::Scalar {
tag: Tag::Context(1),
value: Value::Utf8(s),
}) => {
if debug_text.is_some() {
return Err(CommissioningError::MalformedResponse(stage));
}
debug_text = Some(s);
}
Some(Element::Scalar { .. } | Element::ContainerStart { .. }) => {}
None | Some(_) => return Err(CommissioningError::MalformedResponse(stage)),
}
}
let networking_status =
networking_status.ok_or(CommissioningError::MalformedResponse(stage))?;
Ok(NetworkConfigResponse {
networking_status,
debug_text,
})
}
pub fn decode_connect_network_response(
stage: Stage,
tlv: &[u8],
) -> Result<ConnectNetworkResponse, CommissioningError> {
use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
let mut reader = TlvReader::new(tlv);
match reader
.next()
.map_err(|_| CommissioningError::MalformedResponse(stage))?
{
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure,
}) => {}
_ => return Err(CommissioningError::MalformedResponse(stage)),
}
let mut networking_status: Option<u8> = None;
let mut debug_text: Option<String> = None;
let mut error_value: Option<i32> = None;
loop {
match reader
.next()
.map_err(|_| CommissioningError::MalformedResponse(stage))?
{
Some(Element::ContainerEnd) => break,
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Uint(v),
}) => {
if networking_status.is_some() {
return Err(CommissioningError::MalformedResponse(stage));
}
networking_status = Some(
u8::try_from(v).map_err(|_| CommissioningError::MalformedResponse(stage))?,
);
}
Some(Element::Scalar {
tag: Tag::Context(1),
value: Value::Utf8(s),
}) => {
if debug_text.is_some() {
return Err(CommissioningError::MalformedResponse(stage));
}
debug_text = Some(s);
}
Some(Element::Scalar {
tag: Tag::Context(2),
value: Value::Int(v),
}) => {
if error_value.is_some() {
return Err(CommissioningError::MalformedResponse(stage));
}
error_value = Some(
i32::try_from(v).map_err(|_| CommissioningError::MalformedResponse(stage))?,
);
}
Some(Element::Scalar { .. } | Element::ContainerStart { .. }) => {}
None | Some(_) => return Err(CommissioningError::MalformedResponse(stage)),
}
}
let networking_status =
networking_status.ok_or(CommissioningError::MalformedResponse(stage))?;
Ok(ConnectNetworkResponse {
networking_status,
debug_text,
error_value,
})
}
#[must_use]
pub const fn remediation_for(networking_status: u8) -> RemediationHint {
match networking_status {
2 => RemediationHint::DeviceNetworkSlotsFull,
3 | 5 => RemediationHint::CheckSsid,
6 => RemediationHint::CheckRegulatoryRegion,
7 => RemediationHint::CheckPassphrase,
8 => RemediationHint::UpgradeSecurityMode,
10 | 11 => RemediationHint::DeviceIpStackFailure,
_ => RemediationHint::None,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
use super::*;
#[test]
fn feature_bits_disjoint() {
assert_eq!(NetworkCommissioningFeature::WIFI.bits(), 0b001);
assert_eq!(NetworkCommissioningFeature::THREAD.bits(), 0b010);
assert_eq!(NetworkCommissioningFeature::ETHERNET.bits(), 0b100);
}
#[test]
fn cluster_id_is_0x0031() {
assert_eq!(CLUSTER_ID, 0x0031);
}
#[test]
fn add_or_update_wifi_network_matter_no_creds_matches_spec_bytes() {
let bytes = encode_add_or_update_wifi_network(b"matter", b"", 0);
assert_eq!(
bytes,
vec![
0x15, 0x30, 0x00, 0x06, b'm', b'a', b't', b't', b'e', b'r', 0x30, 0x01, 0x00, 0x24,
0x02, 0x00, 0x18,
],
"encoded bytes: {bytes:02x?}",
);
}
#[test]
fn add_or_update_wifi_network_with_creds_includes_passphrase_bytes() {
let bytes = encode_add_or_update_wifi_network(b"matter", b"hunter22", 1);
assert_eq!(bytes.first(), Some(&0x15));
assert_eq!(bytes.last(), Some(&0x18));
let window = b"hunter22";
assert!(
bytes.windows(window.len()).any(|w| w == window),
"credentials should appear in the payload literal",
);
}
#[test]
fn add_or_update_thread_network_bytes_match_vector() {
let ds = hex::decode(
"0e08000000000001000000030000184a0300001235060004001fffe0020878\
96217f787f6ebe0708fdec3f34f3cd2020051071dccee3f164f15da92254e0\
b9c8a3a5030f4f70656e5468726561642d3839643701\
0289d70410dc4b544c7a58671a2ce4f876f5d6dcd90c0402a0f7f8",
)
.expect("valid hex literal");
assert_eq!(ds.len(), 111, "reference dataset must be 111 bytes");
let got = encode_add_or_update_thread_network(&ds, 1);
assert_eq!(
hex::encode(&got),
"1530006f0e08000000000001000000030000184a0300001235060004001fff\
e002087896217f787f6ebe0708fdec3f34f3cd2020051071dccee3f164f15d\
a92254e0b9c8a3a5030f4f70656e5468726561642d383964370102\
89d70410dc4b544c7a58671a2ce4f876f5d6dcd90c0402a0f7f824010118",
"encoded bytes: {got:02x?}",
);
}
#[test]
fn connect_network_thread_bytes_match_vector() {
let ext_pan_id = [0x78, 0x96, 0x21, 0x7f, 0x78, 0x7f, 0x6e, 0xbe];
let got = encode_connect_network(&ext_pan_id, 1);
assert_eq!(
hex::encode(&got),
"153000087896217f787f6ebe24010118",
"encoded bytes: {got:02x?}",
);
}
#[test]
fn connect_network_matter_matches_spec_bytes() {
let bytes = encode_connect_network(b"matter", 0);
assert_eq!(
bytes,
vec![
0x15, 0x30, 0x00, 0x06, b'm', b'a', b't', b't', b'e', b'r', 0x24, 0x01, 0x00, 0x18,
],
"encoded bytes: {bytes:02x?}",
);
}
#[test]
fn decode_feature_map_round_trips_all_8_combinations() {
for raw in 0u8..8 {
let tlv = vec![0x04, raw];
let decoded = decode_feature_map(&tlv).expect("happy path decodes");
assert_eq!(decoded.bits(), u32::from(raw));
}
}
#[test]
fn decode_feature_map_rejects_non_uint_tlv() {
let tlv = vec![0x10, 0x00];
let err = decode_feature_map(&tlv).expect_err("should fail");
assert!(
matches!(err, CommissioningError::MalformedResponse(_)),
"got {err:?}",
);
}
#[test]
fn decode_feature_map_truncates_high_bits_safely() {
let tlv = vec![0x04, 0x0F];
let decoded = decode_feature_map(&tlv).expect("decodes");
assert_eq!(
decoded,
NetworkCommissioningFeature::WIFI
| NetworkCommissioningFeature::THREAD
| NetworkCommissioningFeature::ETHERNET,
);
}
#[test]
fn decode_connect_max_time_seconds_round_trips() {
assert_eq!(decode_connect_max_time_seconds(&[0x04, 30]).unwrap(), 30);
assert_eq!(
decode_connect_max_time_seconds(&[0x05, 0x2C, 0x01]).unwrap(),
300
);
}
#[test]
fn decode_connect_max_time_seconds_clamps_oversize_to_u16_max() {
let tlv = vec![0x06, 0x00, 0x00, 0x01, 0x00];
assert_eq!(decode_connect_max_time_seconds(&tlv).unwrap(), u16::MAX);
}
#[test]
fn decode_connect_max_time_seconds_rejects_non_uint() {
let err = decode_connect_max_time_seconds(&[0x10, 0x00]).expect_err("should fail");
assert!(
matches!(err, CommissioningError::MalformedResponse(_)),
"got {err:?}",
);
}
#[test]
fn network_config_response_ok_round_trips() {
let tlv = vec![0x15, 0x24, 0x00, 0x00, 0x18];
let decoded =
decode_network_config_response(Stage::NetworkSetup, &tlv).expect("happy path decodes");
assert_eq!(decoded.networking_status, 0);
assert_eq!(decoded.debug_text, None);
}
#[test]
fn network_config_response_auth_failure_with_debug_text() {
let tlv = vec![
0x15, 0x24, 0x00, 0x07, 0x2C, 0x01, 0x08, b'w', b'r', b'o', b'n', b'g', b'-', b'p',
b'w', 0x18,
];
let decoded =
decode_network_config_response(Stage::NetworkSetup, &tlv).expect("happy path decodes");
assert_eq!(decoded.networking_status, 7);
assert_eq!(decoded.debug_text.as_deref(), Some("wrong-pw"));
}
#[test]
fn network_config_response_malformed_returns_error() {
let err =
decode_network_config_response(Stage::NetworkSetup, &[0xFF]).expect_err("should fail");
assert!(
matches!(err, CommissioningError::MalformedResponse(_)),
"got {err:?}"
);
}
#[test]
fn connect_network_response_ok_round_trips() {
let tlv = vec![0x15, 0x24, 0x00, 0x00, 0x18];
let decoded = decode_connect_network_response(Stage::NetworkEnable, &tlv)
.expect("happy path decodes");
assert_eq!(decoded.networking_status, 0);
assert_eq!(decoded.debug_text, None);
assert_eq!(decoded.error_value, None);
}
#[test]
fn connect_network_response_carries_error_value() {
let tlv = vec![
0x15, 0x24, 0x00, 0x09, 0x20, 0x02, 0x0A, 0x18,
];
let decoded = decode_connect_network_response(Stage::NetworkEnable, &tlv).expect("decodes");
assert_eq!(decoded.networking_status, 9);
assert_eq!(decoded.error_value, Some(10));
}
#[test]
fn connect_network_response_malformed_returns_error() {
let err = decode_connect_network_response(Stage::NetworkEnable, &[0xFF])
.expect_err("should fail");
assert!(
matches!(err, CommissioningError::MalformedResponse(_)),
"got {err:?}"
);
}
#[test]
fn remediation_for_table_matches_spec() {
use RemediationHint::*;
let table: &[(u8, RemediationHint)] = &[
(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), ];
for (code, expected) in table {
assert_eq!(
remediation_for(*code),
*expected,
"remediation_for({code}) mismatch",
);
}
assert_eq!(remediation_for(99), RemediationHint::None);
assert_eq!(remediation_for(u8::MAX), RemediationHint::None);
}
}