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