1use crate::error::Error;
5use matter_codec::{Tag, Value};
6use matter_interaction::AttributePath;
7
8pub(crate) const OPERATIONAL_CREDENTIALS_CLUSTER: u32 = 0x003E;
10pub(crate) const CMD_UPDATE_FABRIC_LABEL: u32 = 0x09;
12pub(crate) const CMD_REMOVE_FABRIC: u32 = 0x0A;
14pub(crate) const ATTR_FABRICS: u32 = 0x0001;
16pub(crate) const ATTR_CURRENT_FABRIC_INDEX: u32 = 0x0005;
18
19const TAG_ROOT_PUBLIC_KEY: u8 = 1;
21const TAG_VENDOR_ID: u8 = 2;
22const TAG_FABRIC_ID: u8 = 3;
23const TAG_NODE_ID: u8 = 4;
24const TAG_LABEL: u8 = 5;
25const TAG_FABRIC_INDEX: u8 = 254;
26
27const TAG_NOC_STATUS: u8 = 0;
29const TAG_NOC_FABRIC_INDEX: u8 = 1;
30const TAG_NOC_DEBUG_TEXT: u8 = 2;
31
32#[derive(Clone, Debug, PartialEq, Eq)]
38#[non_exhaustive]
39pub struct FabricDescriptor {
40 pub root_public_key: Vec<u8>,
42 pub vendor_id: u16,
44 pub fabric_id: u64,
46 pub node_id: u64,
48 pub label: String,
50 pub fabric_index: u8,
52}
53
54#[derive(Clone, Debug, PartialEq, Eq)]
59pub(crate) struct NocStatus {
60 pub status: u8,
62 pub fabric_index: Option<u8>,
64 pub debug_text: Option<String>,
66}
67
68fn struct_members(v: &Value) -> Option<&[(Tag, Value)]> {
69 match v {
70 Value::Structure(m) | Value::List(m) => Some(m),
71 _ => None,
72 }
73}
74
75fn ctx(members: &[(Tag, Value)], tag: u8) -> Option<&Value> {
76 members
77 .iter()
78 .find(|(t, _)| *t == Tag::Context(tag))
79 .map(|(_, v)| v)
80}
81
82fn parse_fabric_descriptor(v: &Value) -> Option<FabricDescriptor> {
83 let m = struct_members(v)?;
84 #[allow(clippy::cast_possible_truncation)]
85 Some(FabricDescriptor {
88 root_public_key: match ctx(m, TAG_ROOT_PUBLIC_KEY)? {
89 Value::Bytes(b) => b.clone(),
90 _ => return None,
91 },
92 vendor_id: match ctx(m, TAG_VENDOR_ID)? {
93 Value::Uint(u) => *u as u16,
94 _ => return None,
95 },
96 fabric_id: match ctx(m, TAG_FABRIC_ID)? {
97 Value::Uint(u) => *u,
98 _ => return None,
99 },
100 node_id: match ctx(m, TAG_NODE_ID)? {
101 Value::Uint(u) => *u,
102 _ => return None,
103 },
104 label: match ctx(m, TAG_LABEL) {
105 Some(Value::Utf8(s)) => s.clone(),
106 _ => String::new(),
107 },
108 fabric_index: match ctx(m, TAG_FABRIC_INDEX)? {
109 Value::Uint(u) => *u as u8,
110 _ => return None,
111 },
112 })
113}
114
115pub(crate) fn parse_fabrics(reports: &[(AttributePath, Value)]) -> Vec<FabricDescriptor> {
120 for (path, value) in reports {
121 if path.attribute == ATTR_FABRICS {
122 if let Value::Array(items) = value {
123 return items.iter().filter_map(parse_fabric_descriptor).collect();
124 }
125 }
126 }
127 Vec::new()
128}
129
130pub(crate) fn parse_current_fabric_index(reports: &[(AttributePath, Value)]) -> Option<u8> {
134 for (path, value) in reports {
135 if path.attribute == ATTR_CURRENT_FABRIC_INDEX {
136 if let Value::Uint(u) = value {
137 #[allow(clippy::cast_possible_truncation)]
138 return Some(*u as u8);
140 }
141 }
142 }
143 None
144}
145
146pub(crate) fn parse_noc_response(fields: &Value) -> NocStatus {
151 let m = struct_members(fields).unwrap_or(&[]);
152 #[allow(clippy::cast_possible_truncation)]
153 NocStatus {
156 status: match ctx(m, TAG_NOC_STATUS) {
157 Some(Value::Uint(u)) => *u as u8,
158 _ => u8::MAX,
159 },
160 fabric_index: match ctx(m, TAG_NOC_FABRIC_INDEX) {
161 Some(Value::Uint(u)) => Some(*u as u8),
162 _ => None,
163 },
164 debug_text: match ctx(m, TAG_NOC_DEBUG_TEXT) {
165 Some(Value::Utf8(s)) => Some(s.clone()),
166 _ => None,
167 },
168 }
169}
170
171pub(crate) fn noc_status_to_result(s: &NocStatus) -> Result<(), Error> {
177 if s.status == 0 {
178 Ok(())
179 } else {
180 Err(Error::OperationalCredentialsRejected(s.status))
181 }
182}
183
184#[cfg(test)]
185#[allow(clippy::unwrap_used)] mod tests {
187 use super::*;
188
189 fn ap(a: u32) -> AttributePath {
190 AttributePath {
191 endpoint: 0,
192 cluster: OPERATIONAL_CREDENTIALS_CLUSTER,
193 attribute: a,
194 }
195 }
196
197 fn fabric_struct(idx: u8, fid: u64, label: &str) -> Value {
198 Value::Structure(vec![
199 (
200 Tag::Context(TAG_ROOT_PUBLIC_KEY),
201 Value::Bytes(vec![4u8; 65]),
202 ),
203 (Tag::Context(TAG_VENDOR_ID), Value::Uint(0xFFF1)),
204 (Tag::Context(TAG_FABRIC_ID), Value::Uint(fid)),
205 (Tag::Context(TAG_NODE_ID), Value::Uint(0x1122_3344)),
206 (Tag::Context(TAG_LABEL), Value::Utf8(label.into())),
207 (Tag::Context(TAG_FABRIC_INDEX), Value::Uint(u64::from(idx))),
208 ])
209 }
210
211 #[test]
212 fn parse_fabrics_decodes_array_of_structs() {
213 let reports = vec![(
214 ap(ATTR_FABRICS),
215 Value::Array(vec![
216 fabric_struct(1, 100, "home"),
217 fabric_struct(2, 200, ""),
218 ]),
219 )];
220 let f = parse_fabrics(&reports);
221 assert_eq!(f.len(), 2);
222 assert_eq!(f[0].fabric_index, 1);
223 assert_eq!(f[0].fabric_id, 100);
224 assert_eq!(f[0].label, "home");
225 assert_eq!(f[0].root_public_key.len(), 65);
226 assert_eq!(f[1].fabric_index, 2);
227 assert_eq!(f[1].label, "");
228 }
229
230 #[test]
231 fn parse_current_fabric_index_reads_u8() {
232 let reports = vec![(ap(ATTR_CURRENT_FABRIC_INDEX), Value::Uint(3))];
233 assert_eq!(parse_current_fabric_index(&reports), Some(3));
234 assert_eq!(parse_current_fabric_index(&[]), None);
235 }
236
237 #[test]
238 fn noc_response_success_and_failure() {
239 let ok = Value::Structure(vec![
240 (Tag::Context(0), Value::Uint(0)),
241 (Tag::Context(1), Value::Uint(2)),
242 ]);
243 let s = parse_noc_response(&ok);
244 assert_eq!(s.status, 0);
245 assert_eq!(s.fabric_index, Some(2));
246 assert!(noc_status_to_result(&s).is_ok());
247
248 let bad = Value::Structure(vec![(Tag::Context(0), Value::Uint(7))]);
249 let s2 = parse_noc_response(&bad);
250 assert!(matches!(
251 noc_status_to_result(&s2),
252 Err(Error::OperationalCredentialsRejected(7))
253 ));
254 }
255}