Skip to main content

matter_interaction/
read.rs

1//! `ReadRequestMessage` / `ReportDataMessage` framing — Matter §10.6.
2
3#![forbid(unsafe_code)]
4
5use crate::error::ImError;
6use crate::event::{EventFilter, EventPath};
7use crate::path::attribute_path_and_append_from_value;
8pub use crate::path::{AttributePath, ReadPath};
9use crate::{
10    expect_message_struct, read_container_members, read_container_value, skip_container,
11    IM_REVISION,
12};
13use matter_codec::{ContainerKind, Element, Tag, TlvReader, TlvWriter, Value};
14
15/// Build a `ReadRequestMessage` carrying attribute paths, event paths, and event
16/// filters.
17///
18/// Field order on the wire (Matter §10.6 / `ReadRequestMessage`):
19/// `AttributeRequests[0]`, `EventRequests[1]`, `EventFilters[2]`,
20/// `IsFabricFiltered[3]`, `InteractionModelRevision[0xFF]`. An empty slice omits
21/// its array entirely. Each `AttributePathIB` is a list (endpoint=2, cluster=3,
22/// attribute=4); `EventPathIB` is a list and `EventFilterIB` is a struct (see
23/// [`EventPath`]/[`EventFilter`]). `Some` fields are emitted, `None` are wildcards.
24#[must_use]
25#[allow(clippy::expect_used, clippy::missing_panics_doc)] // Vec-backed TlvWriter is infallible.
26pub fn build_read_request_full(
27    attr_paths: &[ReadPath],
28    event_paths: &[EventPath],
29    event_filters: &[EventFilter],
30) -> Vec<u8> {
31    let mut buf = Vec::new();
32    let mut w = TlvWriter::new(&mut buf);
33    w.start_structure(Tag::Anonymous)
34        .expect("infallible: vec writer");
35    if !attr_paths.is_empty() {
36        w.start_array(Tag::Context(0))
37            .expect("infallible: vec writer"); // AttributeRequests
38        for p in attr_paths {
39            w.start_list(Tag::Anonymous)
40                .expect("infallible: vec writer");
41            if let Some(ep) = p.endpoint {
42                w.put_uint(Tag::Context(2), u64::from(ep))
43                    .expect("infallible: vec writer");
44            }
45            if let Some(cl) = p.cluster {
46                w.put_uint(Tag::Context(3), u64::from(cl))
47                    .expect("infallible: vec writer");
48            }
49            if let Some(at) = p.attribute {
50                w.put_uint(Tag::Context(4), u64::from(at))
51                    .expect("infallible: vec writer");
52            }
53            w.end_container().expect("infallible: vec writer");
54        }
55        w.end_container().expect("infallible: vec writer"); // AttributeRequests array
56    }
57    if !event_paths.is_empty() {
58        w.start_array(Tag::Context(1))
59            .expect("infallible: vec writer"); // EventRequests
60        for p in event_paths {
61            p.write(&mut w).expect("infallible: vec writer");
62        }
63        w.end_container().expect("infallible: vec writer");
64    }
65    if !event_filters.is_empty() {
66        w.start_array(Tag::Context(2))
67            .expect("infallible: vec writer"); // EventFilters
68        for f in event_filters {
69            f.write(&mut w).expect("infallible: vec writer");
70        }
71        w.end_container().expect("infallible: vec writer");
72    }
73    w.put_bool(Tag::Context(3), false)
74        .expect("infallible: vec writer"); // IsFabricFiltered
75    w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
76        .expect("infallible: vec writer");
77    w.end_container().expect("infallible: vec writer");
78    buf
79}
80
81/// Build a `ReadRequestMessage` for the given (possibly wildcard) attribute paths.
82///
83/// Each [`ReadPath`] field that is `Some` is emitted as a context-tagged member of
84/// the `AttributePathIB` list (endpoint=2, cluster=3, attribute=4); `None` fields
85/// are omitted (wildcard). `IsFabricFiltered` is `false`. Delegates to
86/// [`build_read_request_full`] with no event paths/filters, so the output is
87/// byte-identical to the attribute-only encoding.
88#[must_use]
89pub fn build_read_request_paths(paths: &[ReadPath]) -> Vec<u8> {
90    build_read_request_full(paths, &[], &[])
91}
92
93/// Build a `ReadRequestMessage` for one or more concrete attribute paths.
94///
95/// Delegates to [`build_read_request_paths`] so that the output is
96/// byte-identical to before: same context tags 2/3/4, same order, same
97/// `isFabricFiltered`/`interactionModelRevision`.
98#[must_use]
99pub fn build_read_request(paths: &[AttributePath]) -> Vec<u8> {
100    let read_paths: Vec<ReadPath> = paths.iter().map(|&p| ReadPath::from(p)).collect();
101    build_read_request_paths(&read_paths)
102}
103
104/// Parsed `ReportDataMessage` (Matter §10.6.4).
105#[derive(Clone, Debug, PartialEq)]
106#[non_exhaustive]
107pub struct ReportData {
108    /// Every `AttributeReportIB` carrying `AttributeData`, with the
109    /// information needed to reassemble chunked and list-chunked reports.
110    ///
111    /// For the common single-message (non-chunked) `Replace`-only case, prefer
112    /// the [`attributes`](ReportData::attributes) borrowing view over this raw
113    /// list.
114    pub items: Vec<AttributeReportItem>,
115    /// Server-assigned subscription identifier, present only in
116    /// subscription `ReportData` messages (context tag 0); `None` in
117    /// plain `ReadResponse` messages.
118    pub subscription_id: Option<u32>,
119    /// `MoreChunkedMessages` (context tag 3): `true` ⇒ more `ReportData`
120    /// chunks follow on this exchange and must be solicited with a
121    /// `StatusResponse`. Absent on the wire ⇒ `false`.
122    pub more_chunked_messages: bool,
123    /// `SuppressResponse` (context tag 4): `true` ⇒ the sender does not expect
124    /// a `StatusResponse` for this message. Absent on the wire ⇒ `false`.
125    pub suppress_response: bool,
126    /// Every `EventReportIB` carried in `eventReports` (context tag 2), in wire
127    /// order. Empty for attribute-only reports.
128    pub events: Vec<crate::event::EventReport>,
129    /// Every `AttributeStatusIB` (a per-path status/error, not attribute data),
130    /// as `(path, status)` in wire order. IM-1: these were previously discarded,
131    /// so a device reporting e.g. `UnsupportedAttribute` for a requested path was
132    /// indistinguishable from the path simply being omitted. Populated by
133    /// [`parse_report_data`]; empty for all-data reports and for reports built
134    /// via [`ReportData::new`]. Mirrors the write path, which surfaces per-path
135    /// status via [`crate::parse_write_response`].
136    pub statuses: Vec<(AttributePath, crate::status::ImStatus)>,
137}
138
139impl ReportData {
140    /// Construct a [`ReportData`] from its decoded parts.
141    ///
142    /// Provided because the struct is `#[non_exhaustive]`: callers in other
143    /// crates cannot use a struct literal, so this constructor is the stable
144    /// way to build one (e.g. test fixtures that synthesize a report). Any
145    /// future spec-driven field will gain a default here without breaking
146    /// existing callers.
147    ///
148    /// Synthesizes an attribute-only report (`events` empty). Event reports are
149    /// populated only by [`parse_report_data`]; an external caller that needs to
150    /// synthesize events should construct via the parser from bytes.
151    #[must_use]
152    pub fn new(
153        items: Vec<AttributeReportItem>,
154        subscription_id: Option<u32>,
155        more_chunked_messages: bool,
156        suppress_response: bool,
157    ) -> Self {
158        Self {
159            items,
160            subscription_id,
161            more_chunked_messages,
162            suppress_response,
163            events: Vec::new(),
164            statuses: Vec::new(),
165        }
166    }
167
168    /// Borrowing view over the event reports carried in this message
169    /// (`eventReports`, context tag 2). Empty for attribute-only reports.
170    #[must_use]
171    pub fn events(&self) -> &[crate::event::EventReport] {
172        &self.events
173    }
174
175    /// Borrowing `(path, value)` view over the whole-attribute `Replace` reports
176    /// in [`items`](Self::items), as a flattened convenience for the common
177    /// single-message (non-chunked) case.
178    ///
179    /// List-append IBs (`ListIndex` = null, [`ReportOp::Append`]) are **not**
180    /// included — use [`items`](Self::items) +
181    /// [`ReportAccumulator`](crate::ReportAccumulator) for chunked / list
182    /// reassembly. `AttributeStatus` (error) reports never reach `items`, so they
183    /// are absent here too.
184    ///
185    /// This borrows from `items`; it neither allocates nor copies any [`Value`],
186    /// unlike materializing an owned `Vec`.
187    pub fn attributes(&self) -> impl Iterator<Item = (&AttributePath, &Value)> {
188        self.items
189            .iter()
190            .filter(|it| it.op == ReportOp::Replace)
191            .map(|it| (&it.path, &it.value))
192    }
193}
194
195/// One `AttributeReportIB` carrying `AttributeData`, retaining the list-merge
196/// metadata that the [`ReportData::attributes`] convenience view flattens away.
197#[derive(Clone, Debug, PartialEq)]
198#[non_exhaustive]
199pub struct AttributeReportItem {
200    /// Concrete `(endpoint, cluster, attribute)`.
201    pub path: AttributePath,
202    /// Whether this IB replaces the attribute value or appends a list element.
203    pub op: ReportOp,
204    /// The data value (whole attribute for `Replace`, one element for `Append`).
205    pub value: Value,
206    /// `DataVersion` (`AttributeData` context tag 0), if present.
207    pub data_version: Option<u32>,
208}
209
210impl AttributeReportItem {
211    /// Construct an [`AttributeReportItem`] from its decoded parts.
212    ///
213    /// Provided because the struct is `#[non_exhaustive]`: callers in other
214    /// crates cannot use a struct literal, so this constructor is the stable
215    /// way to build one. Any future spec-driven field will gain a default
216    /// here without breaking existing callers.
217    #[must_use]
218    pub fn new(path: AttributePath, op: ReportOp, value: Value, data_version: Option<u32>) -> Self {
219        Self {
220            path,
221            op,
222            value,
223            data_version,
224        }
225    }
226}
227
228/// How an [`AttributeReportItem`] merges into accumulated state.
229#[derive(Clone, Copy, Debug, PartialEq, Eq)]
230#[non_exhaustive]
231pub enum ReportOp {
232    /// Replace the attribute's value (path carried no `ListIndex`).
233    Replace,
234    /// Append `value` to the attribute's list (path carried `ListIndex` = null).
235    Append,
236}
237
238/// Parse a `ReportDataMessage` into concrete `(path, value)` pairs.
239///
240/// Walks the `AttributeReports` array; for each `AttributeReportIB` that
241/// carries `AttributeData [1]`, extracts the path (`AttributePathIB [1]`)
242/// and the data value (`[2]`). `AttributeStatus` error reports are
243/// skipped. A message with no `AttributeReports` yields an empty result.
244///
245/// # Errors
246///
247/// Returns [`ImError`] if the message is not a struct, a present
248/// `AttributeData` is missing its path or data, or a path value is out of
249/// range.
250pub fn parse_report_data(bytes: &[u8]) -> Result<ReportData, ImError> {
251    let mut r = TlvReader::new(bytes);
252    expect_message_struct(&mut r)?;
253
254    let mut items: Vec<AttributeReportItem> = Vec::new();
255    let mut statuses: Vec<(AttributePath, crate::status::ImStatus)> = Vec::new();
256    let mut events: Vec<crate::event::EventReport> = Vec::new();
257    let mut subscription_id: Option<u32> = None;
258    let mut more_chunked_messages = false;
259    let mut suppress_response = false;
260
261    // Scan ALL top-level fields. The AttributeReports array (ctx 1) is
262    // consumed inline so that the scan continues past it to MoreChunkedMessages
263    // (ctx 3) and SuppressResponse (ctx 4), which follow the array on the wire.
264    loop {
265        match r.next()? {
266            None | Some(Element::ContainerEnd) => break,
267            // subscriptionId [0]
268            Some(Element::Scalar {
269                tag: Tag::Context(0),
270                value: Value::Uint(n),
271            }) => {
272                subscription_id = Some(u32::try_from(n).map_err(|_| {
273                    ImError::UnexpectedValue("ReportData.subscriptionId exceeds u32")
274                })?);
275            }
276            // attributeReports [1] — consume the array inline.
277            Some(Element::ContainerStart {
278                tag: Tag::Context(1),
279                kind: ContainerKind::Array,
280            }) => parse_attribute_reports(&mut r, &mut items, &mut statuses)?,
281            // moreChunkedMessages [3]
282            Some(Element::Scalar {
283                tag: Tag::Context(3),
284                value: Value::Bool(b),
285            }) => more_chunked_messages = b,
286            // suppressResponse [4]
287            Some(Element::Scalar {
288                tag: Tag::Context(4),
289                value: Value::Bool(b),
290            }) => suppress_response = b,
291            // eventReports [2] — consume the array inline.
292            Some(Element::ContainerStart {
293                tag: Tag::Context(2),
294                kind: ContainerKind::Array,
295            }) => crate::event::parse_event_reports(&mut r, &mut events)?,
296            // Any other container — skip.
297            Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
298            Some(_) => {}
299        }
300    }
301
302    Ok(ReportData {
303        items,
304        subscription_id,
305        more_chunked_messages,
306        suppress_response,
307        events,
308        statuses,
309    })
310}
311
312/// One decoded `AttributeReportIB`: either attribute data, a per-path status
313/// (IM-1), or an empty IB.
314enum ReportIb {
315    Data(AttributeReportItem),
316    Status(AttributePath, crate::status::ImStatus),
317    Empty,
318}
319
320/// Consume the `AttributeReports` array body (reader positioned just after the
321/// array-start at context tag 1), pushing one [`AttributeReportItem`] per IB
322/// that carried `AttributeData`, and one `(path, status)` into `statuses` per
323/// `AttributeStatus` IB (IM-1).
324fn parse_attribute_reports(
325    r: &mut TlvReader<'_>,
326    items: &mut Vec<AttributeReportItem>,
327    statuses: &mut Vec<(AttributePath, crate::status::ImStatus)>,
328) -> Result<(), ImError> {
329    loop {
330        match r.next()? {
331            None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
332            Some(Element::ContainerEnd) => return Ok(()), // end of array
333            Some(Element::ContainerStart {
334                kind: ContainerKind::Structure,
335                ..
336            }) => match parse_attribute_report_ib(r)? {
337                ReportIb::Data(item) => items.push(item),
338                ReportIb::Status(path, status) => statuses.push((path, status)),
339                ReportIb::Empty => {}
340            },
341            Some(Element::ContainerStart { .. }) => skip_container(r)?,
342            Some(_) => {}
343        }
344    }
345}
346
347/// Parse one `AttributeReportIB` body — it carries EITHER `AttributeData [1]`
348/// or `AttributeStatus [0]` (IM-1: the status is surfaced, not skipped).
349fn parse_attribute_report_ib(r: &mut TlvReader<'_>) -> Result<ReportIb, ImError> {
350    let mut path = None;
351    let mut value = None;
352    let mut data_version = None;
353    let mut append = false;
354    let mut status: Option<(AttributePath, crate::status::ImStatus)> = None;
355    loop {
356        match r.next()? {
357            None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
358            Some(Element::ContainerEnd) => break,
359            Some(Element::ContainerStart {
360                tag: Tag::Context(1),
361                kind: ContainerKind::Structure,
362            }) => {
363                // AttributeData = struct { 0:DataVersion?, 1:Path(list), 2:Data }
364                parse_attribute_data(r, &mut path, &mut value, &mut data_version, &mut append)?;
365            }
366            // AttributeStatus [0] = struct { 0:Path(list), 1:StatusIB } — parse
367            // the per-path status instead of discarding it (IM-1). Reuses the
368            // write path's identical decoder.
369            Some(Element::ContainerStart {
370                tag: Tag::Context(0),
371                kind: ContainerKind::Structure,
372            }) => {
373                status = Some(crate::write::parse_attribute_status_ib(r)?);
374            }
375            // Any other container → skip.
376            Some(Element::ContainerStart { .. }) => skip_container(r)?,
377            Some(_) => {}
378        }
379    }
380    if let Some((p, s)) = status {
381        return Ok(ReportIb::Status(p, s));
382    }
383    match (path, value) {
384        (Some(p), Some(v)) => Ok(ReportIb::Data(AttributeReportItem {
385            path: p,
386            op: if append {
387                ReportOp::Append
388            } else {
389                ReportOp::Replace
390            },
391            value: v,
392            data_version,
393        })),
394        (None, None) => Ok(ReportIb::Empty), // no AttributeData/Status present
395        (Some(_), None) => Err(ImError::MissingField("AttributeData.Data")),
396        (None, Some(_)) => Err(ImError::MissingField("AttributeData.Path")),
397    }
398}
399
400/// Parse an `AttributeData` body (reader positioned just after the struct
401/// start at context tag 1 inside `AttributeReportIB`).
402///
403/// Populates `path` from the `AttributePathIB` list at tag `[1]`, `value` from
404/// the data element at tag `[2]`, `data_version` from tag `[0]`, and sets
405/// `append` when the path carried `ListIndex` (tag 5) = null. Either of `path`
406/// / `value` may be left `None` if absent; the caller
407/// (`parse_attribute_report_ib`) treats a partial result as a protocol error.
408fn parse_attribute_data(
409    r: &mut TlvReader<'_>,
410    path: &mut Option<AttributePath>,
411    value: &mut Option<Value>,
412    data_version: &mut Option<u32>,
413    append: &mut bool,
414) -> Result<(), ImError> {
415    loop {
416        match r.next()? {
417            None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
418            Some(Element::ContainerEnd) => return Ok(()),
419            Some(Element::Scalar {
420                tag: Tag::Context(0),
421                value: Value::Uint(n),
422            }) => {
423                *data_version = Some(u32::try_from(n).map_err(|_| {
424                    ImError::UnexpectedValue("AttributeData.DataVersion exceeds u32")
425                })?);
426            }
427            Some(Element::ContainerStart {
428                tag: Tag::Context(1),
429                kind: ContainerKind::List,
430            }) => {
431                let members = read_container_members(r)?;
432                let (p, is_append) = attribute_path_and_append_from_value(&members)?;
433                *path = Some(p);
434                *append = is_append;
435            }
436            Some(Element::Scalar {
437                tag: Tag::Context(2),
438                value: v,
439            }) => *value = Some(v),
440            Some(Element::ContainerStart {
441                tag: Tag::Context(2),
442                kind,
443            }) => *value = Some(read_container_value(r, kind)?),
444            Some(Element::ContainerStart { .. }) => skip_container(r)?,
445            Some(_) => {}
446        }
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    #![allow(clippy::unwrap_used, clippy::expect_used)]
453    use super::*;
454    use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
455
456    #[test]
457    fn read_request_has_attribute_requests_array() {
458        let bytes = build_read_request(&[AttributePath {
459            endpoint: 0,
460            cluster: 0x0031,
461            attribute: 0xFFFC, // FeatureMap
462        }]);
463        let mut r = TlvReader::new(&bytes);
464        assert!(matches!(
465            r.next().unwrap(),
466            Some(Element::ContainerStart {
467                tag: Tag::Anonymous,
468                kind: ContainerKind::Structure
469            })
470        ));
471        assert!(matches!(
472            r.next().unwrap(),
473            Some(Element::ContainerStart {
474                tag: Tag::Context(0),
475                kind: ContainerKind::Array
476            })
477        ));
478        assert!(matches!(
479            r.next().unwrap(),
480            Some(Element::ContainerStart {
481                tag: Tag::Anonymous,
482                kind: ContainerKind::List
483            })
484        ));
485        assert!(matches!(
486            r.next().unwrap(),
487            Some(Element::Scalar {
488                tag: Tag::Context(2),
489                value: Value::Uint(0)
490            })
491        ));
492        assert!(matches!(
493            r.next().unwrap(),
494            Some(Element::Scalar {
495                tag: Tag::Context(3),
496                value: Value::Uint(0x0031)
497            })
498        ));
499        assert!(matches!(
500            r.next().unwrap(),
501            Some(Element::Scalar {
502                tag: Tag::Context(4),
503                value: Value::Uint(0xFFFC)
504            })
505        ));
506    }
507
508    #[test]
509    fn parses_single_attribute_value() {
510        use matter_codec::{Tag, TlvWriter};
511        let mut buf = Vec::new();
512        let mut w = TlvWriter::new(&mut buf);
513        w.start_structure(Tag::Anonymous).unwrap();
514        w.start_array(Tag::Context(1)).unwrap(); // AttributeReports
515        {
516            w.start_structure(Tag::Anonymous).unwrap(); // AttributeReportIB
517            w.start_structure(Tag::Context(1)).unwrap(); // AttributeData
518            w.start_list(Tag::Context(1)).unwrap(); // Path (AttributePathIB)
519            w.put_uint(Tag::Context(2), 0).unwrap();
520            w.put_uint(Tag::Context(3), 0x0031).unwrap();
521            w.put_uint(Tag::Context(4), 0xFFFC).unwrap();
522            w.end_container().unwrap();
523            w.put_uint(Tag::Context(2), 0x0001).unwrap(); // Data
524            w.end_container().unwrap(); // AttributeData
525            w.end_container().unwrap(); // AttributeReportIB
526        }
527        w.end_container().unwrap(); // array
528        w.put_uint(Tag::Context(0xFF), 11).unwrap();
529        w.end_container().unwrap();
530
531        let report = parse_report_data(&buf).unwrap();
532        let attrs: Vec<_> = report.attributes().collect();
533        assert_eq!(attrs.len(), 1);
534        let (path, value) = attrs[0];
535        assert_eq!(path.endpoint, 0);
536        assert_eq!(path.cluster, 0x0031);
537        assert_eq!(path.attribute, 0xFFFC);
538        assert_eq!(*value, matter_codec::Value::Uint(0x0001));
539    }
540
541    #[test]
542    fn attribute_status_report_is_surfaced() {
543        // IM-1: a per-path AttributeStatus (here UnsupportedAttribute, 0x86)
544        // must be surfaced in `statuses`, not silently dropped — a caller must
545        // be able to tell "unsupported" from "omitted".
546        use matter_codec::{Tag, TlvWriter};
547        let mut buf = Vec::new();
548        let mut w = TlvWriter::new(&mut buf);
549        w.start_structure(Tag::Anonymous).unwrap();
550        w.start_array(Tag::Context(1)).unwrap(); // AttributeReports
551        w.start_structure(Tag::Anonymous).unwrap(); // AttributeReportIB
552        w.start_structure(Tag::Context(0)).unwrap(); // AttributeStatus
553        w.start_list(Tag::Context(0)).unwrap(); // AttributePathIB
554        w.put_uint(Tag::Context(2), 1).unwrap(); // endpoint
555        w.put_uint(Tag::Context(3), 0x0006).unwrap(); // cluster OnOff
556        w.put_uint(Tag::Context(4), 0x4242).unwrap(); // (bogus) attribute
557        w.end_container().unwrap(); // Path
558        w.start_structure(Tag::Context(1)).unwrap(); // StatusIB
559        w.put_uint(Tag::Context(0), 0x86).unwrap(); // UnsupportedAttribute
560        w.end_container().unwrap(); // StatusIB
561        w.end_container().unwrap(); // AttributeStatus
562        w.end_container().unwrap(); // AttributeReportIB
563        w.end_container().unwrap(); // array
564        w.put_uint(Tag::Context(0xFF), 11).unwrap();
565        w.end_container().unwrap();
566
567        let report = parse_report_data(&buf).unwrap();
568        assert_eq!(report.attributes().count(), 0, "no data items");
569        assert_eq!(report.statuses.len(), 1, "the status IB must be surfaced");
570        let (path, status) = &report.statuses[0];
571        assert_eq!(path.endpoint, 1);
572        assert_eq!(path.cluster, 0x0006);
573        assert_eq!(path.attribute, 0x4242);
574        assert_eq!(*status, crate::status::ImStatus::Failure(0x86));
575    }
576
577    #[test]
578    fn multi_attribute_report_accumulates_all_entries() {
579        use matter_codec::{Tag, TlvWriter};
580        let mut buf = Vec::new();
581        let mut w = TlvWriter::new(&mut buf);
582        w.start_structure(Tag::Anonymous).unwrap();
583        w.start_array(Tag::Context(1)).unwrap(); // AttributeReports
584
585        // First AttributeReportIB: endpoint=0, cluster=0x0028, attribute=0x0000, value=42
586        w.start_structure(Tag::Anonymous).unwrap();
587        w.start_structure(Tag::Context(1)).unwrap(); // AttributeData
588        w.start_list(Tag::Context(1)).unwrap(); // Path
589        w.put_uint(Tag::Context(2), 0).unwrap();
590        w.put_uint(Tag::Context(3), 0x0028).unwrap();
591        w.put_uint(Tag::Context(4), 0x0000).unwrap();
592        w.end_container().unwrap();
593        w.put_uint(Tag::Context(2), 42).unwrap(); // Data
594        w.end_container().unwrap(); // AttributeData
595        w.end_container().unwrap(); // AttributeReportIB
596
597        // Second AttributeReportIB: endpoint=1, cluster=0x0006, attribute=0x0000, value=1
598        w.start_structure(Tag::Anonymous).unwrap();
599        w.start_structure(Tag::Context(1)).unwrap(); // AttributeData
600        w.start_list(Tag::Context(1)).unwrap(); // Path
601        w.put_uint(Tag::Context(2), 1).unwrap();
602        w.put_uint(Tag::Context(3), 0x0006).unwrap();
603        w.put_uint(Tag::Context(4), 0x0000).unwrap();
604        w.end_container().unwrap();
605        w.put_uint(Tag::Context(2), 1).unwrap(); // Data
606        w.end_container().unwrap(); // AttributeData
607        w.end_container().unwrap(); // AttributeReportIB
608
609        w.end_container().unwrap(); // array
610        w.put_uint(Tag::Context(0xFF), 11).unwrap();
611        w.end_container().unwrap();
612
613        let report = parse_report_data(&buf).unwrap();
614        let attrs: Vec<_> = report.attributes().collect();
615        assert_eq!(attrs.len(), 2);
616
617        let (path0, val0) = attrs[0];
618        assert_eq!(path0.endpoint, 0);
619        assert_eq!(path0.cluster, 0x0028);
620        assert_eq!(path0.attribute, 0x0000);
621        assert_eq!(*val0, matter_codec::Value::Uint(42));
622
623        let (path1, val1) = attrs[1];
624        assert_eq!(path1.endpoint, 1);
625        assert_eq!(path1.cluster, 0x0006);
626        assert_eq!(path1.attribute, 0x0000);
627        assert_eq!(*val1, matter_codec::Value::Uint(1));
628    }
629
630    #[test]
631    fn out_of_range_endpoint_yields_unexpected_value() {
632        use crate::error::ImError;
633        use matter_codec::{Tag, TlvWriter};
634        let mut buf = Vec::new();
635        let mut w = TlvWriter::new(&mut buf);
636        w.start_structure(Tag::Anonymous).unwrap();
637        w.start_array(Tag::Context(1)).unwrap(); // AttributeReports
638        w.start_structure(Tag::Anonymous).unwrap(); // AttributeReportIB
639        w.start_structure(Tag::Context(1)).unwrap(); // AttributeData
640        w.start_list(Tag::Context(1)).unwrap(); // Path
641        w.put_uint(Tag::Context(2), 0x0001_0000).unwrap(); // endpoint exceeds u16
642        w.put_uint(Tag::Context(3), 0x0031).unwrap();
643        w.put_uint(Tag::Context(4), 0xFFFC).unwrap();
644        w.end_container().unwrap();
645        w.put_uint(Tag::Context(2), 0x0001).unwrap(); // Data
646        w.end_container().unwrap(); // AttributeData
647        w.end_container().unwrap(); // AttributeReportIB
648        w.end_container().unwrap(); // array
649        w.put_uint(Tag::Context(0xFF), 11).unwrap();
650        w.end_container().unwrap();
651
652        let result = parse_report_data(&buf);
653        assert!(
654            matches!(result, Err(ImError::UnexpectedValue(_))),
655            "expected UnexpectedValue, got {result:?}"
656        );
657    }
658
659    #[test]
660    fn parses_more_chunked_and_suppress_response_flags() {
661        use matter_codec::{Tag, TlvWriter};
662        // ReportData with attributeReports[1] array THEN moreChunkedMessages[3]=true.
663        let mut buf = Vec::new();
664        let mut w = TlvWriter::new(&mut buf);
665        w.start_structure(Tag::Anonymous).unwrap();
666        w.start_array(Tag::Context(1)).unwrap(); // AttributeReports (empty)
667        w.end_container().unwrap();
668        w.put_bool(Tag::Context(3), true).unwrap(); // MoreChunkedMessages
669        w.put_uint(Tag::Context(0xFF), 11).unwrap();
670        w.end_container().unwrap();
671
672        let report = parse_report_data(&buf).unwrap();
673        assert!(
674            report.more_chunked_messages,
675            "tag 3 must be read after the array"
676        );
677        assert!(!report.suppress_response);
678    }
679
680    #[test]
681    fn parses_suppress_response_after_array() {
682        use matter_codec::{Tag, TlvWriter};
683        let mut buf = Vec::new();
684        let mut w = TlvWriter::new(&mut buf);
685        w.start_structure(Tag::Anonymous).unwrap();
686        w.start_array(Tag::Context(1)).unwrap();
687        w.end_container().unwrap();
688        w.put_bool(Tag::Context(4), true).unwrap(); // SuppressResponse
689        w.put_uint(Tag::Context(0xFF), 11).unwrap();
690        w.end_container().unwrap();
691
692        let report = parse_report_data(&buf).unwrap();
693        assert!(report.suppress_response);
694        assert!(!report.more_chunked_messages);
695    }
696
697    #[test]
698    fn captures_data_version_and_append_op() {
699        use matter_codec::{Tag, TlvWriter};
700        let mut buf = Vec::new();
701        let mut w = TlvWriter::new(&mut buf);
702        w.start_structure(Tag::Anonymous).unwrap();
703        w.start_array(Tag::Context(1)).unwrap(); // AttributeReports
704        w.start_structure(Tag::Anonymous).unwrap(); // AttributeReportIB
705        w.start_structure(Tag::Context(1)).unwrap(); // AttributeData
706        w.put_uint(Tag::Context(0), 7).unwrap(); // DataVersion
707        w.start_list(Tag::Context(1)).unwrap(); // Path
708        w.put_uint(Tag::Context(2), 0).unwrap();
709        w.put_uint(Tag::Context(3), 0x1d).unwrap();
710        w.put_uint(Tag::Context(4), 0x0003).unwrap();
711        w.put_null(Tag::Context(5)).unwrap(); // ListIndex = null ⇒ append
712        w.end_container().unwrap();
713        w.put_uint(Tag::Context(2), 42).unwrap(); // Data (one element)
714        w.end_container().unwrap(); // AttributeData
715        w.end_container().unwrap(); // AttributeReportIB
716        w.end_container().unwrap(); // array
717        w.put_uint(Tag::Context(0xFF), 11).unwrap();
718        w.end_container().unwrap();
719
720        let report = parse_report_data(&buf).unwrap();
721        assert_eq!(report.items.len(), 1);
722        let it = &report.items[0];
723        assert_eq!(it.op, ReportOp::Append);
724        assert_eq!(it.data_version, Some(7));
725        assert_eq!(it.value, Value::Uint(42));
726        // Append items are excluded from the flattened convenience view.
727        assert_eq!(report.attributes().count(), 0);
728    }
729
730    /// The borrowing `attributes()` view yields exactly the `Replace` items'
731    /// `(path, value)` pairs — same content the removed owned `attributes` Vec
732    /// used to deep-clone — and skips `Append` items.
733    #[test]
734    fn attributes_view_matches_items_filtered_to_replace() {
735        use matter_codec::{Tag, TlvWriter};
736        let mut buf = Vec::new();
737        let mut w = TlvWriter::new(&mut buf);
738        w.start_structure(Tag::Anonymous).unwrap();
739        w.start_array(Tag::Context(1)).unwrap(); // AttributeReports
740
741        // Replace: ep0/0x0028/0x0000 = 42
742        w.start_structure(Tag::Anonymous).unwrap();
743        w.start_structure(Tag::Context(1)).unwrap();
744        w.start_list(Tag::Context(1)).unwrap();
745        w.put_uint(Tag::Context(2), 0).unwrap();
746        w.put_uint(Tag::Context(3), 0x0028).unwrap();
747        w.put_uint(Tag::Context(4), 0x0000).unwrap();
748        w.end_container().unwrap();
749        w.put_uint(Tag::Context(2), 42).unwrap();
750        w.end_container().unwrap();
751        w.end_container().unwrap();
752
753        // Append: ep0/0x001d/0x0003 list element (must be excluded from view).
754        w.start_structure(Tag::Anonymous).unwrap();
755        w.start_structure(Tag::Context(1)).unwrap();
756        w.start_list(Tag::Context(1)).unwrap();
757        w.put_uint(Tag::Context(2), 0).unwrap();
758        w.put_uint(Tag::Context(3), 0x001d).unwrap();
759        w.put_uint(Tag::Context(4), 0x0003).unwrap();
760        w.put_null(Tag::Context(5)).unwrap(); // ListIndex = null ⇒ append
761        w.end_container().unwrap();
762        w.put_uint(Tag::Context(2), 7).unwrap();
763        w.end_container().unwrap();
764        w.end_container().unwrap();
765
766        // Replace: ep1/0x0006/0x0000 = true
767        w.start_structure(Tag::Anonymous).unwrap();
768        w.start_structure(Tag::Context(1)).unwrap();
769        w.start_list(Tag::Context(1)).unwrap();
770        w.put_uint(Tag::Context(2), 1).unwrap();
771        w.put_uint(Tag::Context(3), 0x0006).unwrap();
772        w.put_uint(Tag::Context(4), 0x0000).unwrap();
773        w.end_container().unwrap();
774        w.put_bool(Tag::Context(2), true).unwrap();
775        w.end_container().unwrap();
776        w.end_container().unwrap();
777
778        w.end_container().unwrap(); // array
779        w.put_uint(Tag::Context(0xFF), 11).unwrap();
780        w.end_container().unwrap();
781
782        let report = parse_report_data(&buf).unwrap();
783
784        // Independently derive the expected pairs from `items`.
785        let expected: Vec<(&AttributePath, &Value)> = report
786            .items
787            .iter()
788            .filter(|it| it.op == ReportOp::Replace)
789            .map(|it| (&it.path, &it.value))
790            .collect();
791        let got: Vec<(&AttributePath, &Value)> = report.attributes().collect();
792        assert_eq!(got, expected);
793
794        // Concretely: the two Replace values, in order; the Append is excluded.
795        assert_eq!(got.len(), 2);
796        assert_eq!(got[0].1, &Value::Uint(42));
797        assert_eq!(got[1].1, &Value::Bool(true));
798    }
799}