Skip to main content

matter_interaction/
invoke_server.rs

1//! Server-side `Invoke` framing — the inverse of [`crate::invoke`]'s client
2//! codecs. A controller never had to *read* an inbound `InvokeRequestMessage` or
3//! *write* an `InvokeResponseMessage`; an OTA Provider (M9-F) does both.
4
5#![forbid(unsafe_code)]
6
7use crate::invoke::{command_path_from_reader, retag_container_anonymous, write_command_path};
8use crate::path::CommandPath;
9use crate::status::ImStatus;
10use crate::{expect_message_struct, skip_container, IM_REVISION};
11use matter_codec::{ContainerKind, Element, Tag, TlvReader, TlvWriter, Value};
12
13/// One command parsed out of an inbound `InvokeRequestMessage`.
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct InvokedCommand {
16    /// `(endpoint, cluster, command)` the requester invoked.
17    pub path: CommandPath,
18    /// The requester's original `CommandFields` bytes, **verbatim**, under a
19    /// fresh anonymous tag (ready to hand to a `matter-clusters` decoder):
20    /// only the container's own control/tag bytes are replaced, the body is
21    /// copied unexamined. Original integer widths are preserved, and so is
22    /// everything else the peer sent — which has three consequences for
23    /// consumers:
24    ///
25    /// - A localized-string suffix (element type `0x1F`, IS1) survives in the
26    ///   blob rather than being dropped by a re-encode. Decoded `Value`s are
27    ///   unchanged: the downstream decoder still truncates at the IS1
28    ///   separator.
29    /// - Invalid UTF-8 inside `CommandFields` is **not** rejected here — the
30    ///   copy never decodes it — so it surfaces from your own decoder instead
31    ///   of from IM parsing.
32    /// - An off-spec `Array` whose children carry non-anonymous tags is copied
33    ///   through as-is and fails in your decoder with `NonAnonymousArrayTag`;
34    ///   the older decode-then-re-encode path silently normalised those tags
35    ///   away.
36    pub fields_tlv: Vec<u8>,
37    /// The `CommandRef` (`CommandDataIB` tag 2), present only in batched invokes.
38    pub command_ref: Option<u16>,
39}
40
41/// A parsed inbound `InvokeRequestMessage`.
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct ParsedInvokeRequest {
44    /// `SuppressResponse` — set for group (multicast) invokes.
45    pub suppress_response: bool,
46    /// `TimedRequest` — the action half of a timed interaction.
47    pub timed: bool,
48    /// The invoked commands (one for a normal invoke; more for a batch).
49    pub commands: Vec<InvokedCommand>,
50}
51
52/// Parse an inbound `InvokeRequestMessage` (Matter Core §10.7) — the message a
53/// device sends to a server (e.g. a Requestor's `QueryImage` to an OTA Provider).
54///
55/// # Errors
56///
57/// Returns [`crate::ImError`] if the message is not a struct or lacks the
58/// `InvokeRequests` array, or a `CommandDataIB` lacks a `CommandPath`.
59pub fn parse_invoke_request(bytes: &[u8]) -> Result<ParsedInvokeRequest, crate::ImError> {
60    let mut r = TlvReader::new(bytes);
61    expect_message_struct(&mut r)?;
62
63    let mut suppress_response = false;
64    let mut timed = false;
65    let mut commands = Vec::new();
66
67    loop {
68        match r.next()? {
69            None | Some(Element::ContainerEnd) => break,
70            Some(Element::Scalar {
71                tag: Tag::Context(0),
72                value: Value::Bool(b),
73            }) => suppress_response = b,
74            Some(Element::Scalar {
75                tag: Tag::Context(1),
76                value: Value::Bool(b),
77            }) => timed = b,
78            Some(Element::ContainerStart {
79                tag: Tag::Context(2),
80                kind: ContainerKind::Array,
81            }) => {
82                read_invoke_requests(&mut r, &mut commands)?;
83            }
84            Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
85            Some(_) => {}
86        }
87    }
88
89    Ok(ParsedInvokeRequest {
90        suppress_response,
91        timed,
92        commands,
93    })
94}
95
96/// Read the `InvokeRequests` array body: a sequence of `CommandDataIB` structs,
97/// until the array's `ContainerEnd`.
98fn read_invoke_requests(
99    r: &mut TlvReader<'_>,
100    out: &mut Vec<InvokedCommand>,
101) -> Result<(), crate::ImError> {
102    loop {
103        match r.next()? {
104            None | Some(Element::ContainerEnd) => return Ok(()),
105            Some(Element::ContainerStart {
106                kind: ContainerKind::Structure,
107                ..
108            }) => out.push(read_command_data(r)?),
109            Some(Element::ContainerStart { .. }) => skip_container(r)?,
110            Some(_) => {}
111        }
112    }
113}
114
115/// Read one `CommandDataIB` body (reader positioned just after its struct start):
116/// ctx0 `CommandPath` (list), ctx1 `CommandFields` (struct), opt ctx2 `CommandRef`.
117fn read_command_data(r: &mut TlvReader<'_>) -> Result<InvokedCommand, crate::ImError> {
118    let mut path = None;
119    let mut fields_tlv = None;
120    let mut command_ref = None;
121
122    loop {
123        match r.next()? {
124            None | Some(Element::ContainerEnd) => break,
125            Some(Element::ContainerStart {
126                tag: Tag::Context(0),
127                kind: ContainerKind::List,
128            }) => {
129                path = Some(command_path_from_reader(r)?);
130            }
131            Some(Element::ContainerStart {
132                tag: Tag::Context(1),
133                kind: ContainerKind::Structure,
134            }) => {
135                fields_tlv = Some(retag_container_anonymous(r, ContainerKind::Structure)?);
136            }
137            Some(Element::Scalar {
138                tag: Tag::Context(2),
139                value: Value::Uint(n),
140            }) => {
141                command_ref = u16::try_from(n).ok();
142            }
143            Some(Element::ContainerStart { .. }) => skip_container(r)?,
144            Some(_) => {}
145        }
146    }
147
148    Ok(InvokedCommand {
149        path: path.ok_or(crate::ImError::MissingField("CommandDataIB.CommandPath"))?,
150        fields_tlv: fields_tlv
151            .ok_or(crate::ImError::MissingField("CommandDataIB.CommandFields"))?,
152        command_ref,
153    })
154}
155
156/// Build a single-response `InvokeResponseMessage` carrying a response **command**
157/// (`InvokeResponseIB` → ctx0 `Command` → `CommandDataIB`: path + fields).
158/// `SuppressResponse = false`.
159///
160/// `response_fields_tlv` must be an anonymous-tagged struct (a `matter-clusters`
161/// response encoder output).
162///
163/// # Panics
164///
165/// Panics if `response_fields_tlv` is not valid anonymous-tagged TLV (as
166/// [`crate::build_invoke_request`]).
167#[must_use]
168#[allow(clippy::expect_used)] // Vec-backed TlvWriter is infallible.
169pub fn build_invoke_response_command(path: CommandPath, response_fields_tlv: &[u8]) -> Vec<u8> {
170    let mut buf = Vec::with_capacity(48 + response_fields_tlv.len());
171    let mut w = TlvWriter::new(&mut buf);
172    w.start_structure(Tag::Anonymous)
173        .expect("infallible: vec writer");
174    w.put_bool(Tag::Context(0), false)
175        .expect("infallible: vec writer"); // SuppressResponse
176    w.start_array(Tag::Context(1))
177        .expect("infallible: vec writer"); // InvokeResponses
178    {
179        w.start_structure(Tag::Anonymous)
180            .expect("infallible: vec writer"); // InvokeResponseIB
181        w.start_structure(Tag::Context(0))
182            .expect("infallible: vec writer"); // Command = CommandDataIB
183        write_command_path(&mut w, Tag::Context(0), path);
184        w.put_preencoded(Tag::Context(1), response_fields_tlv)
185            .expect("infallible: caller passes a valid anonymous-tagged struct");
186        w.end_container().expect("infallible: vec writer"); // CommandDataIB
187        w.end_container().expect("infallible: vec writer"); // InvokeResponseIB
188    }
189    w.end_container().expect("infallible: vec writer"); // InvokeResponses array
190    w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
191        .expect("infallible: vec writer");
192    w.end_container().expect("infallible: vec writer"); // message struct
193    buf
194}
195
196/// Build a single-response `InvokeResponseMessage` carrying a bare **status**
197/// for `path` (`InvokeResponseIB` → ctx1 `Status` → `CommandStatusIB`: path +
198/// `StatusIB` with `Status = status`). `SuppressResponse = false`.
199#[must_use]
200#[allow(clippy::expect_used, clippy::missing_panics_doc)] // Vec-backed TlvWriter is infallible.
201pub fn build_invoke_response_status(path: CommandPath, status: ImStatus) -> Vec<u8> {
202    let mut buf = Vec::with_capacity(64);
203    let mut w = TlvWriter::new(&mut buf);
204    w.start_structure(Tag::Anonymous)
205        .expect("infallible: vec writer");
206    w.put_bool(Tag::Context(0), false)
207        .expect("infallible: vec writer"); // SuppressResponse
208    w.start_array(Tag::Context(1))
209        .expect("infallible: vec writer"); // InvokeResponses
210    {
211        w.start_structure(Tag::Anonymous)
212            .expect("infallible: vec writer"); // InvokeResponseIB
213        w.start_structure(Tag::Context(1))
214            .expect("infallible: vec writer"); // Status = CommandStatusIB
215        write_command_path(&mut w, Tag::Context(0), path);
216        w.start_structure(Tag::Context(1))
217            .expect("infallible: vec writer"); // StatusIB
218        w.put_uint(Tag::Context(0), u64::from(status.to_u8()))
219            .expect("infallible: vec writer"); // Status
220        w.end_container().expect("infallible: vec writer"); // StatusIB
221        w.end_container().expect("infallible: vec writer"); // CommandStatusIB
222        w.end_container().expect("infallible: vec writer"); // InvokeResponseIB
223    }
224    w.end_container().expect("infallible: vec writer"); // InvokeResponses array
225    w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
226        .expect("infallible: vec writer");
227    w.end_container().expect("infallible: vec writer"); // message struct
228    buf
229}
230
231#[cfg(test)]
232mod tests {
233    #![allow(clippy::unwrap_used, clippy::expect_used)] // Test code: CLAUDE.md carve-out.
234    use super::*;
235    use crate::invoke::{build_invoke_request, parse_invoke_response, InvokeResponse};
236
237    fn anon_struct_ctx0(value: u64) -> Vec<u8> {
238        let mut b = Vec::new();
239        let mut w = TlvWriter::new(&mut b);
240        w.start_structure(Tag::Anonymous).unwrap();
241        w.put_uint(Tag::Context(0), value).unwrap();
242        w.end_container().unwrap();
243        b
244    }
245
246    #[test]
247    fn parse_invoke_request_roundtrips_builder() {
248        let fields = anon_struct_ctx0(0xFFF1);
249        let path = CommandPath {
250            endpoint: 0,
251            cluster: 0x0029,
252            command: 0x00,
253        };
254        let msg = build_invoke_request(path, &fields);
255        let parsed = parse_invoke_request(&msg).expect("parse");
256        assert!(!parsed.suppress_response);
257        assert!(!parsed.timed);
258        assert_eq!(parsed.commands.len(), 1);
259        assert_eq!(parsed.commands[0].path, path);
260        assert_eq!(parsed.commands[0].fields_tlv, fields);
261        assert_eq!(parsed.commands[0].command_ref, None);
262    }
263
264    #[test]
265    fn build_invoke_response_command_roundtrips() {
266        let fields = anon_struct_ctx0(7);
267        let path = CommandPath {
268            endpoint: 0,
269            cluster: 0x0029,
270            command: 0x01,
271        };
272        let msg = build_invoke_response_command(path, &fields);
273        match parse_invoke_response(&msg).expect("parse") {
274            InvokeResponse::Command {
275                path: p,
276                fields_tlv,
277            } => {
278                assert_eq!(p, path);
279                assert_eq!(fields_tlv, fields);
280            }
281            InvokeResponse::Status(s) => panic!("expected Command, got Status({s:?})"),
282        }
283    }
284
285    #[test]
286    fn command_fields_preserve_device_integer_widths() {
287        // Mirror of invoke.rs's test on the server-side (request) parse path.
288        // Device encodes CommandFields with a NON-minimal width (uint16 42);
289        // span-copy + retag must return those bytes verbatim.
290        let nonminimal_fields = [0x15u8, 0x25, 0x00, 0x2A, 0x00, 0x18];
291        let mut buf = Vec::new();
292        let mut w = TlvWriter::new(&mut buf);
293        w.start_structure(Tag::Anonymous).unwrap();
294        w.put_bool(Tag::Context(0), false).unwrap(); // SuppressResponse
295        w.put_bool(Tag::Context(1), false).unwrap(); // TimedRequest
296        w.start_array(Tag::Context(2)).unwrap(); // InvokeRequests
297        w.start_structure(Tag::Anonymous).unwrap(); // CommandDataIB
298        w.start_list(Tag::Context(0)).unwrap(); // CommandPath
299        w.put_uint(Tag::Context(0), 0).unwrap();
300        w.put_uint(Tag::Context(1), 0x0029).unwrap();
301        w.put_uint(Tag::Context(2), 0x00).unwrap();
302        w.end_container().unwrap();
303        w.put_preencoded(Tag::Context(1), &nonminimal_fields)
304            .unwrap();
305        w.end_container().unwrap(); // CommandDataIB
306        w.end_container().unwrap(); // InvokeRequests array
307        w.put_uint(Tag::Context(0xFF), 11).unwrap();
308        w.end_container().unwrap();
309
310        let parsed = parse_invoke_request(&buf).expect("parse");
311        assert_eq!(parsed.commands.len(), 1);
312        assert_eq!(
313            parsed.commands[0].fields_tlv, nonminimal_fields,
314            "device widths must be preserved verbatim"
315        );
316    }
317
318    #[test]
319    fn build_invoke_response_status_roundtrips() {
320        let path = CommandPath {
321            endpoint: 0,
322            cluster: 0x0029,
323            command: 0x04,
324        };
325        let msg = build_invoke_response_status(path, ImStatus::Success);
326        assert!(matches!(
327            parse_invoke_response(&msg),
328            Ok(InvokeResponse::Status(ImStatus::Success))
329        ));
330    }
331}