Skip to main content

matter_interaction/
lib.rs

1//! Matter Interaction Model (IM) message framing — Matter Core Spec §10.
2//!
3//! Builders for the IM message envelopes the wire carries
4//! (`InvokeRequestMessage`, `ReadRequestMessage`, `WriteRequestMessage`,
5//! `SubscribeRequestMessage`, `StatusResponseMessage`)
6//! and parsers for the responses (`InvokeResponseMessage`,
7//! `ReportDataMessage`, `WriteResponseMessage`, `SubscribeResponseMessage`).
8//! Callers supply already-encoded cluster TLV payloads (e.g. from
9//! `matter-clusters` codecs) and compose them with the concrete paths in
10//! [`path`].
11//!
12//! Scope: single- and multi-command invoke (the latter via
13//! `build_invoke_request_batch` / `parse_invoke_response_batch` with `CommandRef`
14//! — the controller-side verb + `MaxPathsPerInvoke` gating are deferred until a
15//! batch-capable device exists), concrete and wildcard read paths, **events**
16//! (event paths/filters in `ReadRequest` and `SubscribeRequest`, `EventReportIB`
17//! parsing), **timed write/invoke** (the `TimedRequest` message + the
18//! `TimedRequest` flag), no chunked writes (deferred to the ACL/groups work).
19//!
20//! Lifted from `matter-commissioning` in M7.1 (the M6.6 design kept this
21//! module free of state-machine dependencies for exactly this move).
22//! Byte-parity with matter.js is enforced by `tests/im_byte_parity.rs`
23//! against fixtures captured via `cargo xtask capture-im`.
24
25#![forbid(unsafe_code)]
26
27mod accumulator;
28pub mod error;
29pub mod event;
30pub mod invoke;
31pub mod invoke_server;
32pub mod path;
33pub mod read;
34pub mod status;
35pub mod subscription;
36pub mod timed;
37pub mod write;
38
39pub use accumulator::{ReportAccumulator, DEFAULT_MAX_BYTES, DEFAULT_MAX_ELEMENTS};
40pub use error::ImError;
41pub use event::{
42    EventFilter, EventPath, EventPriority, EventReport, EventReportItem, EventTimestamp,
43};
44pub use invoke::{
45    build_invoke_request, build_invoke_request_batch, build_invoke_request_group,
46    build_invoke_request_timed, parse_invoke_response, parse_invoke_response_batch, InvokeResponse,
47    InvokeResponseEntry,
48};
49pub use invoke_server::{
50    build_invoke_response_command, build_invoke_response_status, parse_invoke_request,
51    InvokedCommand, ParsedInvokeRequest,
52};
53pub use path::{AttributePath, CommandPath, ReadPath};
54pub use read::{
55    build_read_request, build_read_request_full, build_read_request_paths, parse_report_data,
56    AttributeReportItem, ReportData, ReportOp,
57};
58pub use status::{parse_status_response, ImStatus};
59pub use subscription::{
60    build_status_response, build_subscribe_request, parse_subscribe_response, SubscribeRequest,
61    SubscribeResponse,
62};
63pub use timed::build_timed_request;
64pub use write::{
65    build_list_write_chunks, build_write_request, build_write_request_timed, parse_write_response,
66    AttributeWriteRequest,
67};
68
69/// Interaction Model protocol revision emitted at context tag `0xFF` in
70/// every top-level IM message. Confirmed against the matter.js byte-parity
71/// fixture (see `tests/im_byte_parity.rs`); bump only when a captured
72/// fixture proves matter.js changed it.
73pub const IM_REVISION: u8 = 11;
74
75use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
76
77/// Assert the reader's first element is an anonymous message struct and
78/// consume its start.
79///
80/// # Errors
81///
82/// Returns [`error::ImError::NotAStruct`] if the first element is not an
83/// anonymous structure start, or propagates any [`error::ImError::Codec`]
84/// error from the reader.
85pub fn expect_message_struct(r: &mut TlvReader<'_>) -> Result<(), error::ImError> {
86    match r.next()? {
87        Some(Element::ContainerStart {
88            tag: Tag::Anonymous,
89            kind: ContainerKind::Structure,
90        }) => Ok(()),
91        Some(_) | None => Err(error::ImError::NotAStruct),
92    }
93}
94
95/// Reader positioned just after a container start: consume to its matching
96/// end, returning the members as `(tag, value)` pairs (for List/Structure).
97/// Calling this with the reader in any other position yields an
98/// [`error::ImError`] or misattributed members — never a panic or UB.
99///
100/// # Errors
101///
102/// Returns [`error::ImError::Codec`] wrapping
103/// [`matter_codec::Error::UnclosedContainer`] if the input ends before a
104/// matching end-of-container, or propagates any other codec error.
105pub fn read_container_members(r: &mut TlvReader<'_>) -> Result<Vec<(Tag, Value)>, error::ImError> {
106    let mut out = Vec::new();
107    loop {
108        match r.next()? {
109            None => {
110                return Err(error::ImError::Codec(
111                    matter_codec::Error::UnclosedContainer,
112                ))
113            }
114            Some(Element::ContainerEnd) => return Ok(out),
115            Some(Element::Scalar { tag, value }) => out.push((tag, value)),
116            Some(Element::ContainerStart { tag, kind }) => {
117                let v = read_container_value(r, kind)?;
118                out.push((tag, v));
119            }
120            Some(_) => {}
121        }
122    }
123}
124
125/// Reader positioned just after a container start (of `kind`): read the
126/// whole sub-tree into a [`Value`]. Calling this with the reader in any other
127/// position yields an [`error::ImError`] or misattributed members — never a
128/// panic or UB.
129///
130/// # Errors
131///
132/// Propagates any error from [`read_container_members`].
133pub fn read_container_value(
134    r: &mut TlvReader<'_>,
135    kind: ContainerKind,
136) -> Result<Value, error::ImError> {
137    if matches!(kind, ContainerKind::Array) {
138        // Arrays: build the Vec<Value> directly instead of collecting
139        // (Tag, Value) members and re-collecting into a second Vec. Tags on
140        // array children are discarded, as before (lenient; the codec's
141        // tree-builder path is the strict one).
142        let mut elements = Vec::new();
143        loop {
144            match r.next()? {
145                None => {
146                    return Err(error::ImError::Codec(
147                        matter_codec::Error::UnclosedContainer,
148                    ))
149                }
150                Some(Element::ContainerEnd) => return Ok(Value::Array(elements)),
151                Some(Element::Scalar { value, .. }) => elements.push(value),
152                Some(Element::ContainerStart {
153                    kind: inner_kind, ..
154                }) => elements.push(read_container_value(r, inner_kind)?),
155                Some(_) => {}
156            }
157        }
158    }
159    let members = read_container_members(r)?;
160    Ok(match kind {
161        ContainerKind::Structure => Value::Structure(members),
162        // ContainerKind::List and any future non-exhaustive variants: preserve as List.
163        _ => Value::List(members),
164    })
165}
166
167/// Reader positioned just after a container start: discard the whole
168/// sub-tree (used to skip fields we do not consume). Calling this with the
169/// reader in any other position yields an [`error::ImError`] or misattributed
170/// members — never a panic or UB.
171///
172/// Streaming: the skipped bytes are walked structurally (tags, lengths,
173/// depth) but never decoded — string payloads in skipped data are not
174/// UTF-8 validated — and it does not charge the codec's tree-builder
175/// element budget; a discarded payload is bounded by its input size only.
176/// (Deliberate: see the 2026-08-09 performance-remediation spec §3.1.)
177///
178/// # Errors
179///
180/// Propagates any [`matter_codec::Error`] from the underlying streaming
181/// walk (e.g. `UnclosedContainer` on truncated input) as
182/// [`error::ImError::Codec`].
183pub fn skip_container(r: &mut TlvReader<'_>) -> Result<(), error::ImError> {
184    r.skip_container().map_err(error::ImError::Codec)
185}
186
187#[cfg(test)]
188mod tests {
189    #![allow(clippy::unwrap_used)] // Test code: CLAUDE.md carve-out.
190    use super::*;
191    use matter_codec::{Tag, TlvWriter};
192
193    #[test]
194    fn skip_container_leaves_reader_at_next_sibling() {
195        // { deep nested container with mixed scalars } followed by a sentinel uint.
196        let mut buf = Vec::new();
197        let mut w = TlvWriter::new(&mut buf);
198        w.start_structure(Tag::Anonymous).unwrap();
199        w.start_structure(Tag::Context(1)).unwrap(); // the container we skip
200        w.put_uint(Tag::Context(0), 7).unwrap();
201        w.start_array(Tag::Context(1)).unwrap();
202        w.put_bytes(Tag::Anonymous, &[0xAA; 40]).unwrap();
203        w.end_container().unwrap();
204        w.end_container().unwrap(); // ctx1 struct
205        w.put_uint(Tag::Context(2), 42).unwrap(); // sentinel sibling
206        w.end_container().unwrap();
207
208        let mut r = TlvReader::new(&buf);
209        assert!(matches!(
210            r.next().unwrap(),
211            Some(Element::ContainerStart { .. })
212        )); // outer
213        assert!(matches!(
214            r.next().unwrap(),
215            Some(Element::ContainerStart { .. })
216        )); // ctx1
217        skip_container(&mut r).unwrap();
218        // Reader must now be positioned at the sentinel.
219        match r.next().unwrap() {
220            Some(Element::Scalar {
221                tag: Tag::Context(2),
222                value: Value::Uint(42),
223            }) => {}
224            other => panic!("expected sentinel after skip, got {other:?}"),
225        }
226    }
227
228    #[test]
229    fn read_container_value_array_matches_codec_tree_builder_shape() {
230        // Array of mixed scalars + a nested array; tags on array children are
231        // discarded (lenient, unchanged behavior).
232        let mut buf = Vec::new();
233        let mut w = TlvWriter::new(&mut buf);
234        w.start_array(Tag::Anonymous).unwrap();
235        w.put_uint(Tag::Anonymous, 1).unwrap();
236        w.start_array(Tag::Anonymous).unwrap();
237        w.put_uint(Tag::Anonymous, 2).unwrap();
238        w.end_container().unwrap();
239        w.put_utf8(Tag::Anonymous, "x").unwrap();
240        w.end_container().unwrap();
241
242        let mut r = TlvReader::new(&buf);
243        let Some(Element::ContainerStart { kind, .. }) = r.next().unwrap() else {
244            panic!("expected array start");
245        };
246        let v = read_container_value(&mut r, kind).unwrap();
247        assert_eq!(
248            v,
249            Value::Array(vec![
250                Value::Uint(1),
251                Value::Array(vec![Value::Uint(2)]),
252                Value::Utf8(String::from("x")),
253            ])
254        );
255    }
256}