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}