#![forbid(unsafe_code)]
use crate::error::ImError;
use crate::event::{EventFilter, EventPath};
pub use crate::path::{AttributePath, ReadPath};
use crate::{expect_message_struct, read_container_value, skip_container, IM_REVISION};
use matter_codec::{ContainerKind, Element, Tag, TlvReader, TlvWriter, Value};
#[must_use]
#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn build_read_request_full(
attr_paths: &[ReadPath],
event_paths: &[EventPath],
event_filters: &[EventFilter],
) -> Vec<u8> {
let mut buf = Vec::with_capacity(32 + attr_paths.len() * 24 + event_paths.len() * 24);
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
if !attr_paths.is_empty() {
w.start_array(Tag::Context(0))
.expect("infallible: vec writer"); for p in attr_paths {
w.start_list(Tag::Anonymous)
.expect("infallible: vec writer");
if let Some(ep) = p.endpoint {
w.put_uint(Tag::Context(2), u64::from(ep))
.expect("infallible: vec writer");
}
if let Some(cl) = p.cluster {
w.put_uint(Tag::Context(3), u64::from(cl))
.expect("infallible: vec writer");
}
if let Some(at) = p.attribute {
w.put_uint(Tag::Context(4), u64::from(at))
.expect("infallible: vec writer");
}
w.end_container().expect("infallible: vec writer");
}
w.end_container().expect("infallible: vec writer"); }
if !event_paths.is_empty() {
w.start_array(Tag::Context(1))
.expect("infallible: vec writer"); for p in event_paths {
p.write(&mut w).expect("infallible: vec writer");
}
w.end_container().expect("infallible: vec writer");
}
if !event_filters.is_empty() {
w.start_array(Tag::Context(2))
.expect("infallible: vec writer"); for f in event_filters {
f.write(&mut w).expect("infallible: vec writer");
}
w.end_container().expect("infallible: vec writer");
}
w.put_bool(Tag::Context(3), false)
.expect("infallible: vec writer"); w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer");
buf
}
#[must_use]
pub fn build_read_request_paths(paths: &[ReadPath]) -> Vec<u8> {
build_read_request_full(paths, &[], &[])
}
#[must_use]
pub fn build_read_request(paths: &[AttributePath]) -> Vec<u8> {
let read_paths: Vec<ReadPath> = paths.iter().map(|&p| ReadPath::from(p)).collect();
build_read_request_paths(&read_paths)
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct ReportData {
pub items: Vec<AttributeReportItem>,
pub subscription_id: Option<u32>,
pub more_chunked_messages: bool,
pub suppress_response: bool,
pub events: Vec<crate::event::EventReport>,
pub statuses: Vec<(AttributePath, crate::status::ImStatus)>,
}
impl ReportData {
#[must_use]
pub fn new(
items: Vec<AttributeReportItem>,
subscription_id: Option<u32>,
more_chunked_messages: bool,
suppress_response: bool,
) -> Self {
Self {
items,
subscription_id,
more_chunked_messages,
suppress_response,
events: Vec::new(),
statuses: Vec::new(),
}
}
#[must_use]
pub fn events(&self) -> &[crate::event::EventReport] {
&self.events
}
pub fn attributes(&self) -> impl Iterator<Item = (&AttributePath, &Value)> {
self.items
.iter()
.filter(|it| it.op == ReportOp::Replace)
.map(|it| (&it.path, &it.value))
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct AttributeReportItem {
pub path: AttributePath,
pub op: ReportOp,
pub value: Value,
pub data_version: Option<u32>,
}
impl AttributeReportItem {
#[must_use]
pub fn new(path: AttributePath, op: ReportOp, value: Value, data_version: Option<u32>) -> Self {
Self {
path,
op,
value,
data_version,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ReportOp {
Replace,
Append,
}
pub fn parse_report_data(bytes: &[u8]) -> Result<ReportData, ImError> {
let mut r = TlvReader::new(bytes);
expect_message_struct(&mut r)?;
let mut items: Vec<AttributeReportItem> = Vec::new();
let mut statuses: Vec<(AttributePath, crate::status::ImStatus)> = Vec::new();
let mut events: Vec<crate::event::EventReport> = Vec::new();
let mut subscription_id: Option<u32> = None;
let mut more_chunked_messages = false;
let mut suppress_response = false;
loop {
match r.next()? {
None | Some(Element::ContainerEnd) => break,
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Uint(n),
}) => {
subscription_id = Some(u32::try_from(n).map_err(|_| {
ImError::UnexpectedValue("ReportData.subscriptionId exceeds u32")
})?);
}
Some(Element::ContainerStart {
tag: Tag::Context(1),
kind: ContainerKind::Array,
}) => parse_attribute_reports(&mut r, &mut items, &mut statuses)?,
Some(Element::Scalar {
tag: Tag::Context(3),
value: Value::Bool(b),
}) => more_chunked_messages = b,
Some(Element::Scalar {
tag: Tag::Context(4),
value: Value::Bool(b),
}) => suppress_response = b,
Some(Element::ContainerStart {
tag: Tag::Context(2),
kind: ContainerKind::Array,
}) => crate::event::parse_event_reports(&mut r, &mut events)?,
Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
Some(_) => {}
}
}
Ok(ReportData {
items,
subscription_id,
more_chunked_messages,
suppress_response,
events,
statuses,
})
}
enum ReportIb {
Data(AttributeReportItem),
Status(AttributePath, crate::status::ImStatus),
Empty,
}
fn parse_attribute_reports(
r: &mut TlvReader<'_>,
items: &mut Vec<AttributeReportItem>,
statuses: &mut Vec<(AttributePath, crate::status::ImStatus)>,
) -> Result<(), ImError> {
loop {
match r.next()? {
None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
Some(Element::ContainerEnd) => return Ok(()), Some(Element::ContainerStart {
kind: ContainerKind::Structure,
..
}) => match parse_attribute_report_ib(r)? {
ReportIb::Data(item) => items.push(item),
ReportIb::Status(path, status) => statuses.push((path, status)),
ReportIb::Empty => {}
},
Some(Element::ContainerStart { .. }) => skip_container(r)?,
Some(_) => {}
}
}
}
fn parse_attribute_report_ib(r: &mut TlvReader<'_>) -> Result<ReportIb, ImError> {
let mut path = None;
let mut value = None;
let mut data_version = None;
let mut append = false;
let mut status: Option<(AttributePath, crate::status::ImStatus)> = None;
loop {
match r.next()? {
None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
Some(Element::ContainerEnd) => break,
Some(Element::ContainerStart {
tag: Tag::Context(1),
kind: ContainerKind::Structure,
}) => {
parse_attribute_data(r, &mut path, &mut value, &mut data_version, &mut append)?;
}
Some(Element::ContainerStart {
tag: Tag::Context(0),
kind: ContainerKind::Structure,
}) => {
status = Some(crate::write::parse_attribute_status_ib(r)?);
}
Some(Element::ContainerStart { .. }) => skip_container(r)?,
Some(_) => {}
}
}
if let Some((p, s)) = status {
return Ok(ReportIb::Status(p, s));
}
match (path, value) {
(Some(p), Some(v)) => Ok(ReportIb::Data(AttributeReportItem {
path: p,
op: if append {
ReportOp::Append
} else {
ReportOp::Replace
},
value: v,
data_version,
})),
(None, None) => Ok(ReportIb::Empty), (Some(_), None) => Err(ImError::MissingField("AttributeData.Data")),
(None, Some(_)) => Err(ImError::MissingField("AttributeData.Path")),
}
}
fn parse_attribute_data(
r: &mut TlvReader<'_>,
path: &mut Option<AttributePath>,
value: &mut Option<Value>,
data_version: &mut Option<u32>,
append: &mut bool,
) -> Result<(), ImError> {
loop {
match r.next()? {
None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
Some(Element::ContainerEnd) => return Ok(()),
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Uint(n),
}) => {
*data_version = Some(u32::try_from(n).map_err(|_| {
ImError::UnexpectedValue("AttributeData.DataVersion exceeds u32")
})?);
}
Some(Element::ContainerStart {
tag: Tag::Context(1),
kind: ContainerKind::List,
}) => {
let (p, is_append) = crate::path::attribute_path_from_reader(r)?;
*path = Some(p);
*append = is_append;
}
Some(Element::Scalar {
tag: Tag::Context(2),
value: v,
}) => *value = Some(v),
Some(Element::ContainerStart {
tag: Tag::Context(2),
kind,
}) => *value = Some(read_container_value(r, kind)?),
Some(Element::ContainerStart { .. }) => skip_container(r)?,
Some(_) => {}
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
#[test]
fn read_request_has_attribute_requests_array() {
let bytes = build_read_request(&[AttributePath {
endpoint: 0,
cluster: 0x0031,
attribute: 0xFFFC, }]);
let mut r = TlvReader::new(&bytes);
assert!(matches!(
r.next().unwrap(),
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::ContainerStart {
tag: Tag::Context(0),
kind: ContainerKind::Array
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::List
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::Scalar {
tag: Tag::Context(2),
value: Value::Uint(0)
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::Scalar {
tag: Tag::Context(3),
value: Value::Uint(0x0031)
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::Scalar {
tag: Tag::Context(4),
value: Value::Uint(0xFFFC)
})
));
}
#[test]
fn parses_single_attribute_value() {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.start_array(Tag::Context(1)).unwrap(); {
w.start_structure(Tag::Anonymous).unwrap(); w.start_structure(Tag::Context(1)).unwrap(); w.start_list(Tag::Context(1)).unwrap(); w.put_uint(Tag::Context(2), 0).unwrap();
w.put_uint(Tag::Context(3), 0x0031).unwrap();
w.put_uint(Tag::Context(4), 0xFFFC).unwrap();
w.end_container().unwrap();
w.put_uint(Tag::Context(2), 0x0001).unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); }
w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
let report = parse_report_data(&buf).unwrap();
let attrs: Vec<_> = report.attributes().collect();
assert_eq!(attrs.len(), 1);
let (path, value) = attrs[0];
assert_eq!(path.endpoint, 0);
assert_eq!(path.cluster, 0x0031);
assert_eq!(path.attribute, 0xFFFC);
assert_eq!(*value, matter_codec::Value::Uint(0x0001));
}
#[test]
fn attribute_status_report_is_surfaced() {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.start_array(Tag::Context(1)).unwrap(); w.start_structure(Tag::Anonymous).unwrap(); w.start_structure(Tag::Context(0)).unwrap(); w.start_list(Tag::Context(0)).unwrap(); w.put_uint(Tag::Context(2), 1).unwrap(); w.put_uint(Tag::Context(3), 0x0006).unwrap(); w.put_uint(Tag::Context(4), 0x4242).unwrap(); w.end_container().unwrap(); w.start_structure(Tag::Context(1)).unwrap(); w.put_uint(Tag::Context(0), 0x86).unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
let report = parse_report_data(&buf).unwrap();
assert_eq!(report.attributes().count(), 0, "no data items");
assert_eq!(report.statuses.len(), 1, "the status IB must be surfaced");
let (path, status) = &report.statuses[0];
assert_eq!(path.endpoint, 1);
assert_eq!(path.cluster, 0x0006);
assert_eq!(path.attribute, 0x4242);
assert_eq!(*status, crate::status::ImStatus::Failure(0x86));
}
#[test]
fn multi_attribute_report_accumulates_all_entries() {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.start_array(Tag::Context(1)).unwrap();
w.start_structure(Tag::Anonymous).unwrap();
w.start_structure(Tag::Context(1)).unwrap(); w.start_list(Tag::Context(1)).unwrap(); w.put_uint(Tag::Context(2), 0).unwrap();
w.put_uint(Tag::Context(3), 0x0028).unwrap();
w.put_uint(Tag::Context(4), 0x0000).unwrap();
w.end_container().unwrap();
w.put_uint(Tag::Context(2), 42).unwrap(); w.end_container().unwrap(); w.end_container().unwrap();
w.start_structure(Tag::Anonymous).unwrap();
w.start_structure(Tag::Context(1)).unwrap(); w.start_list(Tag::Context(1)).unwrap(); w.put_uint(Tag::Context(2), 1).unwrap();
w.put_uint(Tag::Context(3), 0x0006).unwrap();
w.put_uint(Tag::Context(4), 0x0000).unwrap();
w.end_container().unwrap();
w.put_uint(Tag::Context(2), 1).unwrap(); w.end_container().unwrap(); w.end_container().unwrap();
w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
let report = parse_report_data(&buf).unwrap();
let attrs: Vec<_> = report.attributes().collect();
assert_eq!(attrs.len(), 2);
let (path0, val0) = attrs[0];
assert_eq!(path0.endpoint, 0);
assert_eq!(path0.cluster, 0x0028);
assert_eq!(path0.attribute, 0x0000);
assert_eq!(*val0, matter_codec::Value::Uint(42));
let (path1, val1) = attrs[1];
assert_eq!(path1.endpoint, 1);
assert_eq!(path1.cluster, 0x0006);
assert_eq!(path1.attribute, 0x0000);
assert_eq!(*val1, matter_codec::Value::Uint(1));
}
#[test]
fn out_of_range_endpoint_yields_unexpected_value() {
use crate::error::ImError;
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.start_array(Tag::Context(1)).unwrap(); w.start_structure(Tag::Anonymous).unwrap(); w.start_structure(Tag::Context(1)).unwrap(); w.start_list(Tag::Context(1)).unwrap(); w.put_uint(Tag::Context(2), 0x0001_0000).unwrap(); w.put_uint(Tag::Context(3), 0x0031).unwrap();
w.put_uint(Tag::Context(4), 0xFFFC).unwrap();
w.end_container().unwrap();
w.put_uint(Tag::Context(2), 0x0001).unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
let result = parse_report_data(&buf);
assert!(
matches!(result, Err(ImError::UnexpectedValue(_))),
"expected UnexpectedValue, got {result:?}"
);
}
#[test]
fn parses_more_chunked_and_suppress_response_flags() {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.start_array(Tag::Context(1)).unwrap(); w.end_container().unwrap();
w.put_bool(Tag::Context(3), true).unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
let report = parse_report_data(&buf).unwrap();
assert!(
report.more_chunked_messages,
"tag 3 must be read after the array"
);
assert!(!report.suppress_response);
}
#[test]
fn parses_suppress_response_after_array() {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.start_array(Tag::Context(1)).unwrap();
w.end_container().unwrap();
w.put_bool(Tag::Context(4), true).unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
let report = parse_report_data(&buf).unwrap();
assert!(report.suppress_response);
assert!(!report.more_chunked_messages);
}
#[test]
fn captures_data_version_and_append_op() {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.start_array(Tag::Context(1)).unwrap(); w.start_structure(Tag::Anonymous).unwrap(); w.start_structure(Tag::Context(1)).unwrap(); w.put_uint(Tag::Context(0), 7).unwrap(); w.start_list(Tag::Context(1)).unwrap(); w.put_uint(Tag::Context(2), 0).unwrap();
w.put_uint(Tag::Context(3), 0x1d).unwrap();
w.put_uint(Tag::Context(4), 0x0003).unwrap();
w.put_null(Tag::Context(5)).unwrap(); w.end_container().unwrap();
w.put_uint(Tag::Context(2), 42).unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
let report = parse_report_data(&buf).unwrap();
assert_eq!(report.items.len(), 1);
let it = &report.items[0];
assert_eq!(it.op, ReportOp::Append);
assert_eq!(it.data_version, Some(7));
assert_eq!(it.value, Value::Uint(42));
assert_eq!(report.attributes().count(), 0);
}
#[test]
fn attributes_view_matches_items_filtered_to_replace() {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.start_array(Tag::Context(1)).unwrap();
w.start_structure(Tag::Anonymous).unwrap();
w.start_structure(Tag::Context(1)).unwrap();
w.start_list(Tag::Context(1)).unwrap();
w.put_uint(Tag::Context(2), 0).unwrap();
w.put_uint(Tag::Context(3), 0x0028).unwrap();
w.put_uint(Tag::Context(4), 0x0000).unwrap();
w.end_container().unwrap();
w.put_uint(Tag::Context(2), 42).unwrap();
w.end_container().unwrap();
w.end_container().unwrap();
w.start_structure(Tag::Anonymous).unwrap();
w.start_structure(Tag::Context(1)).unwrap();
w.start_list(Tag::Context(1)).unwrap();
w.put_uint(Tag::Context(2), 0).unwrap();
w.put_uint(Tag::Context(3), 0x001d).unwrap();
w.put_uint(Tag::Context(4), 0x0003).unwrap();
w.put_null(Tag::Context(5)).unwrap(); w.end_container().unwrap();
w.put_uint(Tag::Context(2), 7).unwrap();
w.end_container().unwrap();
w.end_container().unwrap();
w.start_structure(Tag::Anonymous).unwrap();
w.start_structure(Tag::Context(1)).unwrap();
w.start_list(Tag::Context(1)).unwrap();
w.put_uint(Tag::Context(2), 1).unwrap();
w.put_uint(Tag::Context(3), 0x0006).unwrap();
w.put_uint(Tag::Context(4), 0x0000).unwrap();
w.end_container().unwrap();
w.put_bool(Tag::Context(2), true).unwrap();
w.end_container().unwrap();
w.end_container().unwrap();
w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
let report = parse_report_data(&buf).unwrap();
let expected: Vec<(&AttributePath, &Value)> = report
.items
.iter()
.filter(|it| it.op == ReportOp::Replace)
.map(|it| (&it.path, &it.value))
.collect();
let got: Vec<(&AttributePath, &Value)> = report.attributes().collect();
assert_eq!(got, expected);
assert_eq!(got.len(), 2);
assert_eq!(got[0].1, &Value::Uint(42));
assert_eq!(got[1].1, &Value::Bool(true));
}
}