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}
130
131impl ReportData {
132    /// Construct a [`ReportData`] from its decoded parts.
133    ///
134    /// Provided because the struct is `#[non_exhaustive]`: callers in other
135    /// crates cannot use a struct literal, so this constructor is the stable
136    /// way to build one (e.g. test fixtures that synthesize a report). Any
137    /// future spec-driven field will gain a default here without breaking
138    /// existing callers.
139    ///
140    /// Synthesizes an attribute-only report (`events` empty). Event reports are
141    /// populated only by [`parse_report_data`]; an external caller that needs to
142    /// synthesize events should construct via the parser from bytes.
143    #[must_use]
144    pub fn new(
145        items: Vec<AttributeReportItem>,
146        subscription_id: Option<u32>,
147        more_chunked_messages: bool,
148        suppress_response: bool,
149    ) -> Self {
150        Self {
151            items,
152            subscription_id,
153            more_chunked_messages,
154            suppress_response,
155            events: Vec::new(),
156        }
157    }
158
159    /// Borrowing view over the event reports carried in this message
160    /// (`eventReports`, context tag 2). Empty for attribute-only reports.
161    #[must_use]
162    pub fn events(&self) -> &[crate::event::EventReport] {
163        &self.events
164    }
165
166    /// Borrowing `(path, value)` view over the whole-attribute `Replace` reports
167    /// in [`items`](Self::items), as a flattened convenience for the common
168    /// single-message (non-chunked) case.
169    ///
170    /// List-append IBs (`ListIndex` = null, [`ReportOp::Append`]) are **not**
171    /// included — use [`items`](Self::items) +
172    /// [`ReportAccumulator`](crate::ReportAccumulator) for chunked / list
173    /// reassembly. `AttributeStatus` (error) reports never reach `items`, so they
174    /// are absent here too.
175    ///
176    /// This borrows from `items`; it neither allocates nor copies any [`Value`],
177    /// unlike materializing an owned `Vec`.
178    pub fn attributes(&self) -> impl Iterator<Item = (&AttributePath, &Value)> {
179        self.items
180            .iter()
181            .filter(|it| it.op == ReportOp::Replace)
182            .map(|it| (&it.path, &it.value))
183    }
184}
185
186/// One `AttributeReportIB` carrying `AttributeData`, retaining the list-merge
187/// metadata that the [`ReportData::attributes`] convenience view flattens away.
188#[derive(Clone, Debug, PartialEq)]
189#[non_exhaustive]
190pub struct AttributeReportItem {
191    /// Concrete `(endpoint, cluster, attribute)`.
192    pub path: AttributePath,
193    /// Whether this IB replaces the attribute value or appends a list element.
194    pub op: ReportOp,
195    /// The data value (whole attribute for `Replace`, one element for `Append`).
196    pub value: Value,
197    /// `DataVersion` (`AttributeData` context tag 0), if present.
198    pub data_version: Option<u32>,
199}
200
201impl AttributeReportItem {
202    /// Construct an [`AttributeReportItem`] from its decoded parts.
203    ///
204    /// Provided because the struct is `#[non_exhaustive]`: callers in other
205    /// crates cannot use a struct literal, so this constructor is the stable
206    /// way to build one. Any future spec-driven field will gain a default
207    /// here without breaking existing callers.
208    #[must_use]
209    pub fn new(path: AttributePath, op: ReportOp, value: Value, data_version: Option<u32>) -> Self {
210        Self {
211            path,
212            op,
213            value,
214            data_version,
215        }
216    }
217}
218
219/// How an [`AttributeReportItem`] merges into accumulated state.
220#[derive(Clone, Copy, Debug, PartialEq, Eq)]
221#[non_exhaustive]
222pub enum ReportOp {
223    /// Replace the attribute's value (path carried no `ListIndex`).
224    Replace,
225    /// Append `value` to the attribute's list (path carried `ListIndex` = null).
226    Append,
227}
228
229/// Parse a `ReportDataMessage` into concrete `(path, value)` pairs.
230///
231/// Walks the `AttributeReports` array; for each `AttributeReportIB` that
232/// carries `AttributeData [1]`, extracts the path (`AttributePathIB [1]`)
233/// and the data value (`[2]`). `AttributeStatus` error reports are
234/// skipped. A message with no `AttributeReports` yields an empty result.
235///
236/// # Errors
237///
238/// Returns [`ImError`] if the message is not a struct, a present
239/// `AttributeData` is missing its path or data, or a path value is out of
240/// range.
241pub fn parse_report_data(bytes: &[u8]) -> Result<ReportData, ImError> {
242    let mut r = TlvReader::new(bytes);
243    expect_message_struct(&mut r)?;
244
245    let mut items: Vec<AttributeReportItem> = Vec::new();
246    let mut events: Vec<crate::event::EventReport> = Vec::new();
247    let mut subscription_id: Option<u32> = None;
248    let mut more_chunked_messages = false;
249    let mut suppress_response = false;
250
251    // Scan ALL top-level fields. The AttributeReports array (ctx 1) is
252    // consumed inline so that the scan continues past it to MoreChunkedMessages
253    // (ctx 3) and SuppressResponse (ctx 4), which follow the array on the wire.
254    loop {
255        match r.next()? {
256            None | Some(Element::ContainerEnd) => break,
257            // subscriptionId [0]
258            Some(Element::Scalar {
259                tag: Tag::Context(0),
260                value: Value::Uint(n),
261            }) => {
262                subscription_id = Some(u32::try_from(n).map_err(|_| {
263                    ImError::UnexpectedValue("ReportData.subscriptionId exceeds u32")
264                })?);
265            }
266            // attributeReports [1] — consume the array inline.
267            Some(Element::ContainerStart {
268                tag: Tag::Context(1),
269                kind: ContainerKind::Array,
270            }) => parse_attribute_reports(&mut r, &mut items)?,
271            // moreChunkedMessages [3]
272            Some(Element::Scalar {
273                tag: Tag::Context(3),
274                value: Value::Bool(b),
275            }) => more_chunked_messages = b,
276            // suppressResponse [4]
277            Some(Element::Scalar {
278                tag: Tag::Context(4),
279                value: Value::Bool(b),
280            }) => suppress_response = b,
281            // eventReports [2] — consume the array inline.
282            Some(Element::ContainerStart {
283                tag: Tag::Context(2),
284                kind: ContainerKind::Array,
285            }) => crate::event::parse_event_reports(&mut r, &mut events)?,
286            // Any other container — skip.
287            Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
288            Some(_) => {}
289        }
290    }
291
292    Ok(ReportData {
293        items,
294        subscription_id,
295        more_chunked_messages,
296        suppress_response,
297        events,
298    })
299}
300
301/// Consume the `AttributeReports` array body (reader positioned just after the
302/// array-start at context tag 1), pushing one [`AttributeReportItem`] per IB
303/// that carried `AttributeData`. `AttributeStatus` (error) IBs are skipped.
304fn parse_attribute_reports(
305    r: &mut TlvReader<'_>,
306    items: &mut Vec<AttributeReportItem>,
307) -> Result<(), ImError> {
308    loop {
309        match r.next()? {
310            None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
311            Some(Element::ContainerEnd) => return Ok(()), // end of array
312            Some(Element::ContainerStart {
313                kind: ContainerKind::Structure,
314                ..
315            }) => {
316                if let Some(item) = parse_attribute_report_ib(r)? {
317                    items.push(item);
318                }
319            }
320            Some(Element::ContainerStart { .. }) => skip_container(r)?,
321            Some(_) => {}
322        }
323    }
324}
325
326/// Parse one `AttributeReportIB` body. Returns `Some(item)` if it carried
327/// `AttributeData`, `None` if it was an `AttributeStatus` (error) report.
328fn parse_attribute_report_ib(
329    r: &mut TlvReader<'_>,
330) -> Result<Option<AttributeReportItem>, ImError> {
331    let mut path = None;
332    let mut value = None;
333    let mut data_version = None;
334    let mut append = false;
335    loop {
336        match r.next()? {
337            None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
338            Some(Element::ContainerEnd) => break,
339            Some(Element::ContainerStart {
340                tag: Tag::Context(1),
341                kind: ContainerKind::Structure,
342            }) => {
343                // AttributeData = struct { 0:DataVersion?, 1:Path(list), 2:Data }
344                parse_attribute_data(r, &mut path, &mut value, &mut data_version, &mut append)?;
345            }
346            // AttributeStatus [0] → skip (error entry).
347            Some(Element::ContainerStart { .. }) => skip_container(r)?,
348            Some(_) => {}
349        }
350    }
351    match (path, value) {
352        (Some(p), Some(v)) => Ok(Some(AttributeReportItem {
353            path: p,
354            op: if append {
355                ReportOp::Append
356            } else {
357                ReportOp::Replace
358            },
359            value: v,
360            data_version,
361        })),
362        (None, None) => Ok(None), // no AttributeData present (AttributeStatus report or empty IB)
363        (Some(_), None) => Err(ImError::MissingField("AttributeData.Data")),
364        (None, Some(_)) => Err(ImError::MissingField("AttributeData.Path")),
365    }
366}
367
368/// Parse an `AttributeData` body (reader positioned just after the struct
369/// start at context tag 1 inside `AttributeReportIB`).
370///
371/// Populates `path` from the `AttributePathIB` list at tag `[1]`, `value` from
372/// the data element at tag `[2]`, `data_version` from tag `[0]`, and sets
373/// `append` when the path carried `ListIndex` (tag 5) = null. Either of `path`
374/// / `value` may be left `None` if absent; the caller
375/// (`parse_attribute_report_ib`) treats a partial result as a protocol error.
376fn parse_attribute_data(
377    r: &mut TlvReader<'_>,
378    path: &mut Option<AttributePath>,
379    value: &mut Option<Value>,
380    data_version: &mut Option<u32>,
381    append: &mut bool,
382) -> Result<(), ImError> {
383    loop {
384        match r.next()? {
385            None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
386            Some(Element::ContainerEnd) => return Ok(()),
387            Some(Element::Scalar {
388                tag: Tag::Context(0),
389                value: Value::Uint(n),
390            }) => {
391                *data_version = Some(u32::try_from(n).map_err(|_| {
392                    ImError::UnexpectedValue("AttributeData.DataVersion exceeds u32")
393                })?);
394            }
395            Some(Element::ContainerStart {
396                tag: Tag::Context(1),
397                kind: ContainerKind::List,
398            }) => {
399                let members = read_container_members(r)?;
400                let (p, is_append) = attribute_path_and_append_from_value(&members)?;
401                *path = Some(p);
402                *append = is_append;
403            }
404            Some(Element::Scalar {
405                tag: Tag::Context(2),
406                value: v,
407            }) => *value = Some(v),
408            Some(Element::ContainerStart {
409                tag: Tag::Context(2),
410                kind,
411            }) => *value = Some(read_container_value(r, kind)?),
412            Some(Element::ContainerStart { .. }) => skip_container(r)?,
413            Some(_) => {}
414        }
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    #![allow(clippy::unwrap_used, clippy::expect_used)]
421    use super::*;
422    use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
423
424    #[test]
425    fn read_request_has_attribute_requests_array() {
426        let bytes = build_read_request(&[AttributePath {
427            endpoint: 0,
428            cluster: 0x0031,
429            attribute: 0xFFFC, // FeatureMap
430        }]);
431        let mut r = TlvReader::new(&bytes);
432        assert!(matches!(
433            r.next().unwrap(),
434            Some(Element::ContainerStart {
435                tag: Tag::Anonymous,
436                kind: ContainerKind::Structure
437            })
438        ));
439        assert!(matches!(
440            r.next().unwrap(),
441            Some(Element::ContainerStart {
442                tag: Tag::Context(0),
443                kind: ContainerKind::Array
444            })
445        ));
446        assert!(matches!(
447            r.next().unwrap(),
448            Some(Element::ContainerStart {
449                tag: Tag::Anonymous,
450                kind: ContainerKind::List
451            })
452        ));
453        assert!(matches!(
454            r.next().unwrap(),
455            Some(Element::Scalar {
456                tag: Tag::Context(2),
457                value: Value::Uint(0)
458            })
459        ));
460        assert!(matches!(
461            r.next().unwrap(),
462            Some(Element::Scalar {
463                tag: Tag::Context(3),
464                value: Value::Uint(0x0031)
465            })
466        ));
467        assert!(matches!(
468            r.next().unwrap(),
469            Some(Element::Scalar {
470                tag: Tag::Context(4),
471                value: Value::Uint(0xFFFC)
472            })
473        ));
474    }
475
476    #[test]
477    fn parses_single_attribute_value() {
478        use matter_codec::{Tag, TlvWriter};
479        let mut buf = Vec::new();
480        let mut w = TlvWriter::new(&mut buf);
481        w.start_structure(Tag::Anonymous).unwrap();
482        w.start_array(Tag::Context(1)).unwrap(); // AttributeReports
483        {
484            w.start_structure(Tag::Anonymous).unwrap(); // AttributeReportIB
485            w.start_structure(Tag::Context(1)).unwrap(); // AttributeData
486            w.start_list(Tag::Context(1)).unwrap(); // Path (AttributePathIB)
487            w.put_uint(Tag::Context(2), 0).unwrap();
488            w.put_uint(Tag::Context(3), 0x0031).unwrap();
489            w.put_uint(Tag::Context(4), 0xFFFC).unwrap();
490            w.end_container().unwrap();
491            w.put_uint(Tag::Context(2), 0x0001).unwrap(); // Data
492            w.end_container().unwrap(); // AttributeData
493            w.end_container().unwrap(); // AttributeReportIB
494        }
495        w.end_container().unwrap(); // array
496        w.put_uint(Tag::Context(0xFF), 11).unwrap();
497        w.end_container().unwrap();
498
499        let report = parse_report_data(&buf).unwrap();
500        let attrs: Vec<_> = report.attributes().collect();
501        assert_eq!(attrs.len(), 1);
502        let (path, value) = attrs[0];
503        assert_eq!(path.endpoint, 0);
504        assert_eq!(path.cluster, 0x0031);
505        assert_eq!(path.attribute, 0xFFFC);
506        assert_eq!(*value, matter_codec::Value::Uint(0x0001));
507    }
508
509    #[test]
510    fn attribute_status_report_is_skipped() {
511        use matter_codec::{Tag, TlvWriter};
512        let mut buf = Vec::new();
513        let mut w = TlvWriter::new(&mut buf);
514        w.start_structure(Tag::Anonymous).unwrap();
515        w.start_array(Tag::Context(1)).unwrap(); // AttributeReports
516        w.start_structure(Tag::Anonymous).unwrap(); // AttributeReportIB
517        w.start_structure(Tag::Context(0)).unwrap(); // AttributeStatus (no AttributeData)
518        w.put_uint(Tag::Context(0), 0x01).unwrap(); // some status field
519        w.end_container().unwrap();
520        w.end_container().unwrap(); // AttributeReportIB
521        w.end_container().unwrap(); // array
522        w.put_uint(Tag::Context(0xFF), 11).unwrap();
523        w.end_container().unwrap();
524
525        let report = parse_report_data(&buf).unwrap();
526        assert_eq!(report.attributes().count(), 0);
527    }
528
529    #[test]
530    fn multi_attribute_report_accumulates_all_entries() {
531        use matter_codec::{Tag, TlvWriter};
532        let mut buf = Vec::new();
533        let mut w = TlvWriter::new(&mut buf);
534        w.start_structure(Tag::Anonymous).unwrap();
535        w.start_array(Tag::Context(1)).unwrap(); // AttributeReports
536
537        // First AttributeReportIB: endpoint=0, cluster=0x0028, attribute=0x0000, value=42
538        w.start_structure(Tag::Anonymous).unwrap();
539        w.start_structure(Tag::Context(1)).unwrap(); // AttributeData
540        w.start_list(Tag::Context(1)).unwrap(); // Path
541        w.put_uint(Tag::Context(2), 0).unwrap();
542        w.put_uint(Tag::Context(3), 0x0028).unwrap();
543        w.put_uint(Tag::Context(4), 0x0000).unwrap();
544        w.end_container().unwrap();
545        w.put_uint(Tag::Context(2), 42).unwrap(); // Data
546        w.end_container().unwrap(); // AttributeData
547        w.end_container().unwrap(); // AttributeReportIB
548
549        // Second AttributeReportIB: endpoint=1, cluster=0x0006, attribute=0x0000, value=1
550        w.start_structure(Tag::Anonymous).unwrap();
551        w.start_structure(Tag::Context(1)).unwrap(); // AttributeData
552        w.start_list(Tag::Context(1)).unwrap(); // Path
553        w.put_uint(Tag::Context(2), 1).unwrap();
554        w.put_uint(Tag::Context(3), 0x0006).unwrap();
555        w.put_uint(Tag::Context(4), 0x0000).unwrap();
556        w.end_container().unwrap();
557        w.put_uint(Tag::Context(2), 1).unwrap(); // Data
558        w.end_container().unwrap(); // AttributeData
559        w.end_container().unwrap(); // AttributeReportIB
560
561        w.end_container().unwrap(); // array
562        w.put_uint(Tag::Context(0xFF), 11).unwrap();
563        w.end_container().unwrap();
564
565        let report = parse_report_data(&buf).unwrap();
566        let attrs: Vec<_> = report.attributes().collect();
567        assert_eq!(attrs.len(), 2);
568
569        let (path0, val0) = attrs[0];
570        assert_eq!(path0.endpoint, 0);
571        assert_eq!(path0.cluster, 0x0028);
572        assert_eq!(path0.attribute, 0x0000);
573        assert_eq!(*val0, matter_codec::Value::Uint(42));
574
575        let (path1, val1) = attrs[1];
576        assert_eq!(path1.endpoint, 1);
577        assert_eq!(path1.cluster, 0x0006);
578        assert_eq!(path1.attribute, 0x0000);
579        assert_eq!(*val1, matter_codec::Value::Uint(1));
580    }
581
582    #[test]
583    fn out_of_range_endpoint_yields_unexpected_value() {
584        use crate::error::ImError;
585        use matter_codec::{Tag, TlvWriter};
586        let mut buf = Vec::new();
587        let mut w = TlvWriter::new(&mut buf);
588        w.start_structure(Tag::Anonymous).unwrap();
589        w.start_array(Tag::Context(1)).unwrap(); // AttributeReports
590        w.start_structure(Tag::Anonymous).unwrap(); // AttributeReportIB
591        w.start_structure(Tag::Context(1)).unwrap(); // AttributeData
592        w.start_list(Tag::Context(1)).unwrap(); // Path
593        w.put_uint(Tag::Context(2), 0x0001_0000).unwrap(); // endpoint exceeds u16
594        w.put_uint(Tag::Context(3), 0x0031).unwrap();
595        w.put_uint(Tag::Context(4), 0xFFFC).unwrap();
596        w.end_container().unwrap();
597        w.put_uint(Tag::Context(2), 0x0001).unwrap(); // Data
598        w.end_container().unwrap(); // AttributeData
599        w.end_container().unwrap(); // AttributeReportIB
600        w.end_container().unwrap(); // array
601        w.put_uint(Tag::Context(0xFF), 11).unwrap();
602        w.end_container().unwrap();
603
604        let result = parse_report_data(&buf);
605        assert!(
606            matches!(result, Err(ImError::UnexpectedValue(_))),
607            "expected UnexpectedValue, got {result:?}"
608        );
609    }
610
611    #[test]
612    fn parses_more_chunked_and_suppress_response_flags() {
613        use matter_codec::{Tag, TlvWriter};
614        // ReportData with attributeReports[1] array THEN moreChunkedMessages[3]=true.
615        let mut buf = Vec::new();
616        let mut w = TlvWriter::new(&mut buf);
617        w.start_structure(Tag::Anonymous).unwrap();
618        w.start_array(Tag::Context(1)).unwrap(); // AttributeReports (empty)
619        w.end_container().unwrap();
620        w.put_bool(Tag::Context(3), true).unwrap(); // MoreChunkedMessages
621        w.put_uint(Tag::Context(0xFF), 11).unwrap();
622        w.end_container().unwrap();
623
624        let report = parse_report_data(&buf).unwrap();
625        assert!(
626            report.more_chunked_messages,
627            "tag 3 must be read after the array"
628        );
629        assert!(!report.suppress_response);
630    }
631
632    #[test]
633    fn parses_suppress_response_after_array() {
634        use matter_codec::{Tag, TlvWriter};
635        let mut buf = Vec::new();
636        let mut w = TlvWriter::new(&mut buf);
637        w.start_structure(Tag::Anonymous).unwrap();
638        w.start_array(Tag::Context(1)).unwrap();
639        w.end_container().unwrap();
640        w.put_bool(Tag::Context(4), true).unwrap(); // SuppressResponse
641        w.put_uint(Tag::Context(0xFF), 11).unwrap();
642        w.end_container().unwrap();
643
644        let report = parse_report_data(&buf).unwrap();
645        assert!(report.suppress_response);
646        assert!(!report.more_chunked_messages);
647    }
648
649    #[test]
650    fn captures_data_version_and_append_op() {
651        use matter_codec::{Tag, TlvWriter};
652        let mut buf = Vec::new();
653        let mut w = TlvWriter::new(&mut buf);
654        w.start_structure(Tag::Anonymous).unwrap();
655        w.start_array(Tag::Context(1)).unwrap(); // AttributeReports
656        w.start_structure(Tag::Anonymous).unwrap(); // AttributeReportIB
657        w.start_structure(Tag::Context(1)).unwrap(); // AttributeData
658        w.put_uint(Tag::Context(0), 7).unwrap(); // DataVersion
659        w.start_list(Tag::Context(1)).unwrap(); // Path
660        w.put_uint(Tag::Context(2), 0).unwrap();
661        w.put_uint(Tag::Context(3), 0x1d).unwrap();
662        w.put_uint(Tag::Context(4), 0x0003).unwrap();
663        w.put_null(Tag::Context(5)).unwrap(); // ListIndex = null ⇒ append
664        w.end_container().unwrap();
665        w.put_uint(Tag::Context(2), 42).unwrap(); // Data (one element)
666        w.end_container().unwrap(); // AttributeData
667        w.end_container().unwrap(); // AttributeReportIB
668        w.end_container().unwrap(); // array
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_eq!(report.items.len(), 1);
674        let it = &report.items[0];
675        assert_eq!(it.op, ReportOp::Append);
676        assert_eq!(it.data_version, Some(7));
677        assert_eq!(it.value, Value::Uint(42));
678        // Append items are excluded from the flattened convenience view.
679        assert_eq!(report.attributes().count(), 0);
680    }
681
682    /// The borrowing `attributes()` view yields exactly the `Replace` items'
683    /// `(path, value)` pairs — same content the removed owned `attributes` Vec
684    /// used to deep-clone — and skips `Append` items.
685    #[test]
686    fn attributes_view_matches_items_filtered_to_replace() {
687        use matter_codec::{Tag, TlvWriter};
688        let mut buf = Vec::new();
689        let mut w = TlvWriter::new(&mut buf);
690        w.start_structure(Tag::Anonymous).unwrap();
691        w.start_array(Tag::Context(1)).unwrap(); // AttributeReports
692
693        // Replace: ep0/0x0028/0x0000 = 42
694        w.start_structure(Tag::Anonymous).unwrap();
695        w.start_structure(Tag::Context(1)).unwrap();
696        w.start_list(Tag::Context(1)).unwrap();
697        w.put_uint(Tag::Context(2), 0).unwrap();
698        w.put_uint(Tag::Context(3), 0x0028).unwrap();
699        w.put_uint(Tag::Context(4), 0x0000).unwrap();
700        w.end_container().unwrap();
701        w.put_uint(Tag::Context(2), 42).unwrap();
702        w.end_container().unwrap();
703        w.end_container().unwrap();
704
705        // Append: ep0/0x001d/0x0003 list element (must be excluded from view).
706        w.start_structure(Tag::Anonymous).unwrap();
707        w.start_structure(Tag::Context(1)).unwrap();
708        w.start_list(Tag::Context(1)).unwrap();
709        w.put_uint(Tag::Context(2), 0).unwrap();
710        w.put_uint(Tag::Context(3), 0x001d).unwrap();
711        w.put_uint(Tag::Context(4), 0x0003).unwrap();
712        w.put_null(Tag::Context(5)).unwrap(); // ListIndex = null ⇒ append
713        w.end_container().unwrap();
714        w.put_uint(Tag::Context(2), 7).unwrap();
715        w.end_container().unwrap();
716        w.end_container().unwrap();
717
718        // Replace: ep1/0x0006/0x0000 = true
719        w.start_structure(Tag::Anonymous).unwrap();
720        w.start_structure(Tag::Context(1)).unwrap();
721        w.start_list(Tag::Context(1)).unwrap();
722        w.put_uint(Tag::Context(2), 1).unwrap();
723        w.put_uint(Tag::Context(3), 0x0006).unwrap();
724        w.put_uint(Tag::Context(4), 0x0000).unwrap();
725        w.end_container().unwrap();
726        w.put_bool(Tag::Context(2), true).unwrap();
727        w.end_container().unwrap();
728        w.end_container().unwrap();
729
730        w.end_container().unwrap(); // array
731        w.put_uint(Tag::Context(0xFF), 11).unwrap();
732        w.end_container().unwrap();
733
734        let report = parse_report_data(&buf).unwrap();
735
736        // Independently derive the expected pairs from `items`.
737        let expected: Vec<(&AttributePath, &Value)> = report
738            .items
739            .iter()
740            .filter(|it| it.op == ReportOp::Replace)
741            .map(|it| (&it.path, &it.value))
742            .collect();
743        let got: Vec<(&AttributePath, &Value)> = report.attributes().collect();
744        assert_eq!(got, expected);
745
746        // Concretely: the two Replace values, in order; the Append is excluded.
747        assert_eq!(got.len(), 2);
748        assert_eq!(got[0].1, &Value::Uint(42));
749        assert_eq!(got[1].1, &Value::Bool(true));
750    }
751}