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_value, reencode_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 command-fields struct, re-encoded with an anonymous tag (ready to
19    /// hand to a `matter-clusters` decoder).
20    pub fields_tlv: Vec<u8>,
21    /// The `CommandRef` (`CommandDataIB` tag 2), present only in batched invokes.
22    pub command_ref: Option<u16>,
23}
24
25/// A parsed inbound `InvokeRequestMessage`.
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct ParsedInvokeRequest {
28    /// `SuppressResponse` — set for group (multicast) invokes.
29    pub suppress_response: bool,
30    /// `TimedRequest` — the action half of a timed interaction.
31    pub timed: bool,
32    /// The invoked commands (one for a normal invoke; more for a batch).
33    pub commands: Vec<InvokedCommand>,
34}
35
36/// Parse an inbound `InvokeRequestMessage` (Matter Core §10.7) — the message a
37/// device sends to a server (e.g. a Requestor's `QueryImage` to an OTA Provider).
38///
39/// # Errors
40///
41/// Returns [`crate::ImError`] if the message is not a struct or lacks the
42/// `InvokeRequests` array, or a `CommandDataIB` lacks a `CommandPath`.
43pub fn parse_invoke_request(bytes: &[u8]) -> Result<ParsedInvokeRequest, crate::ImError> {
44    let mut r = TlvReader::new(bytes);
45    expect_message_struct(&mut r)?;
46
47    let mut suppress_response = false;
48    let mut timed = false;
49    let mut commands = Vec::new();
50
51    loop {
52        match r.next()? {
53            None | Some(Element::ContainerEnd) => break,
54            Some(Element::Scalar {
55                tag: Tag::Context(0),
56                value: Value::Bool(b),
57            }) => suppress_response = b,
58            Some(Element::Scalar {
59                tag: Tag::Context(1),
60                value: Value::Bool(b),
61            }) => timed = b,
62            Some(Element::ContainerStart {
63                tag: Tag::Context(2),
64                kind: ContainerKind::Array,
65            }) => {
66                read_invoke_requests(&mut r, &mut commands)?;
67            }
68            Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
69            Some(_) => {}
70        }
71    }
72
73    Ok(ParsedInvokeRequest {
74        suppress_response,
75        timed,
76        commands,
77    })
78}
79
80/// Read the `InvokeRequests` array body: a sequence of `CommandDataIB` structs,
81/// until the array's `ContainerEnd`.
82fn read_invoke_requests(
83    r: &mut TlvReader<'_>,
84    out: &mut Vec<InvokedCommand>,
85) -> Result<(), crate::ImError> {
86    loop {
87        match r.next()? {
88            None | Some(Element::ContainerEnd) => return Ok(()),
89            Some(Element::ContainerStart {
90                kind: ContainerKind::Structure,
91                ..
92            }) => out.push(read_command_data(r)?),
93            Some(Element::ContainerStart { .. }) => skip_container(r)?,
94            Some(_) => {}
95        }
96    }
97}
98
99/// Read one `CommandDataIB` body (reader positioned just after its struct start):
100/// ctx0 `CommandPath` (list), ctx1 `CommandFields` (struct), opt ctx2 `CommandRef`.
101fn read_command_data(r: &mut TlvReader<'_>) -> Result<InvokedCommand, crate::ImError> {
102    let mut path = None;
103    let mut fields_tlv = None;
104    let mut command_ref = None;
105
106    loop {
107        match r.next()? {
108            None | Some(Element::ContainerEnd) => break,
109            Some(Element::ContainerStart {
110                tag: Tag::Context(0),
111                kind: ContainerKind::List,
112            }) => {
113                let members = read_list_members(r)?;
114                path = Some(command_path_from_value(&members)?);
115            }
116            Some(Element::ContainerStart {
117                tag: Tag::Context(1),
118                kind: ContainerKind::Structure,
119            }) => {
120                // Re-anonymise the CommandFields struct as a standalone blob.
121                let value = read_struct_value(r)?;
122                fields_tlv = Some(reencode_anonymous(&value));
123            }
124            Some(Element::Scalar {
125                tag: Tag::Context(2),
126                value: Value::Uint(n),
127            }) => {
128                command_ref = u16::try_from(n).ok();
129            }
130            Some(Element::ContainerStart { .. }) => skip_container(r)?,
131            Some(_) => {}
132        }
133    }
134
135    Ok(InvokedCommand {
136        path: path.ok_or(crate::ImError::MissingField("CommandDataIB.CommandPath"))?,
137        fields_tlv: fields_tlv
138            .ok_or(crate::ImError::MissingField("CommandDataIB.CommandFields"))?,
139        command_ref,
140    })
141}
142
143/// Read a `Value::List`'s members (reader positioned after the list start).
144fn read_list_members(r: &mut TlvReader<'_>) -> Result<Vec<(Tag, Value)>, crate::ImError> {
145    match crate::read_container_value(r, ContainerKind::List)? {
146        Value::List(members) => Ok(members),
147        _ => Err(crate::ImError::UnexpectedValue("expected a list")),
148    }
149}
150
151/// Read a `Value::Structure` (reader positioned after the struct start) and
152/// return it as a `Value::Structure` for re-anonymising.
153fn read_struct_value(r: &mut TlvReader<'_>) -> Result<Value, crate::ImError> {
154    crate::read_container_value(r, ContainerKind::Structure)
155}
156
157/// Build a single-response `InvokeResponseMessage` carrying a response **command**
158/// (`InvokeResponseIB` → ctx0 `Command` → `CommandDataIB`: path + fields).
159/// `SuppressResponse = false`.
160///
161/// `response_fields_tlv` must be an anonymous-tagged struct (a `matter-clusters`
162/// response encoder output).
163///
164/// # Panics
165///
166/// Panics if `response_fields_tlv` is not valid anonymous-tagged TLV (as
167/// [`crate::build_invoke_request`]).
168#[must_use]
169#[allow(clippy::expect_used)] // Vec-backed TlvWriter is infallible.
170pub fn build_invoke_response_command(path: CommandPath, response_fields_tlv: &[u8]) -> Vec<u8> {
171    let mut buf = Vec::new();
172    let mut w = TlvWriter::new(&mut buf);
173    w.start_structure(Tag::Anonymous)
174        .expect("infallible: vec writer");
175    w.put_bool(Tag::Context(0), false)
176        .expect("infallible: vec writer"); // SuppressResponse
177    w.start_array(Tag::Context(1))
178        .expect("infallible: vec writer"); // InvokeResponses
179    {
180        w.start_structure(Tag::Anonymous)
181            .expect("infallible: vec writer"); // InvokeResponseIB
182        w.start_structure(Tag::Context(0))
183            .expect("infallible: vec writer"); // Command = CommandDataIB
184        write_command_path(&mut w, Tag::Context(0), path);
185        w.put_preencoded(Tag::Context(1), response_fields_tlv)
186            .expect("infallible: caller passes a valid anonymous-tagged struct");
187        w.end_container().expect("infallible: vec writer"); // CommandDataIB
188        w.end_container().expect("infallible: vec writer"); // InvokeResponseIB
189    }
190    w.end_container().expect("infallible: vec writer"); // InvokeResponses array
191    w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
192        .expect("infallible: vec writer");
193    w.end_container().expect("infallible: vec writer"); // message struct
194    buf
195}
196
197/// Build a single-response `InvokeResponseMessage` carrying a bare **status**
198/// for `path` (`InvokeResponseIB` → ctx1 `Status` → `CommandStatusIB`: path +
199/// `StatusIB` with `Status = status`). `SuppressResponse = false`.
200#[must_use]
201#[allow(clippy::expect_used, clippy::missing_panics_doc)] // Vec-backed TlvWriter is infallible.
202pub fn build_invoke_response_status(path: CommandPath, status: ImStatus) -> Vec<u8> {
203    let mut buf = Vec::new();
204    let mut w = TlvWriter::new(&mut buf);
205    w.start_structure(Tag::Anonymous)
206        .expect("infallible: vec writer");
207    w.put_bool(Tag::Context(0), false)
208        .expect("infallible: vec writer"); // SuppressResponse
209    w.start_array(Tag::Context(1))
210        .expect("infallible: vec writer"); // InvokeResponses
211    {
212        w.start_structure(Tag::Anonymous)
213            .expect("infallible: vec writer"); // InvokeResponseIB
214        w.start_structure(Tag::Context(1))
215            .expect("infallible: vec writer"); // Status = CommandStatusIB
216        write_command_path(&mut w, Tag::Context(0), path);
217        w.start_structure(Tag::Context(1))
218            .expect("infallible: vec writer"); // StatusIB
219        w.put_uint(Tag::Context(0), u64::from(status.to_u8()))
220            .expect("infallible: vec writer"); // Status
221        w.end_container().expect("infallible: vec writer"); // StatusIB
222        w.end_container().expect("infallible: vec writer"); // CommandStatusIB
223        w.end_container().expect("infallible: vec writer"); // InvokeResponseIB
224    }
225    w.end_container().expect("infallible: vec writer"); // InvokeResponses array
226    w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
227        .expect("infallible: vec writer");
228    w.end_container().expect("infallible: vec writer"); // message struct
229    buf
230}
231
232#[cfg(test)]
233mod tests {
234    #![allow(clippy::unwrap_used, clippy::expect_used)] // Test code: CLAUDE.md carve-out.
235    use super::*;
236    use crate::invoke::{build_invoke_request, parse_invoke_response, InvokeResponse};
237
238    fn anon_struct_ctx0(value: u64) -> Vec<u8> {
239        let mut b = Vec::new();
240        let mut w = TlvWriter::new(&mut b);
241        w.start_structure(Tag::Anonymous).unwrap();
242        w.put_uint(Tag::Context(0), value).unwrap();
243        w.end_container().unwrap();
244        b
245    }
246
247    #[test]
248    fn parse_invoke_request_roundtrips_builder() {
249        let fields = anon_struct_ctx0(0xFFF1);
250        let path = CommandPath {
251            endpoint: 0,
252            cluster: 0x0029,
253            command: 0x00,
254        };
255        let msg = build_invoke_request(path, &fields);
256        let parsed = parse_invoke_request(&msg).expect("parse");
257        assert!(!parsed.suppress_response);
258        assert!(!parsed.timed);
259        assert_eq!(parsed.commands.len(), 1);
260        assert_eq!(parsed.commands[0].path, path);
261        assert_eq!(parsed.commands[0].fields_tlv, fields);
262        assert_eq!(parsed.commands[0].command_ref, None);
263    }
264
265    #[test]
266    fn build_invoke_response_command_roundtrips() {
267        let fields = anon_struct_ctx0(7);
268        let path = CommandPath {
269            endpoint: 0,
270            cluster: 0x0029,
271            command: 0x01,
272        };
273        let msg = build_invoke_response_command(path, &fields);
274        match parse_invoke_response(&msg).expect("parse") {
275            InvokeResponse::Command {
276                path: p,
277                fields_tlv,
278            } => {
279                assert_eq!(p, path);
280                assert_eq!(fields_tlv, fields);
281            }
282            InvokeResponse::Status(s) => panic!("expected Command, got Status({s:?})"),
283        }
284    }
285
286    #[test]
287    fn build_invoke_response_status_roundtrips() {
288        let path = CommandPath {
289            endpoint: 0,
290            cluster: 0x0029,
291            command: 0x04,
292        };
293        let msg = build_invoke_response_status(path, ImStatus::Success);
294        assert!(matches!(
295            parse_invoke_response(&msg),
296            Ok(InvokeResponse::Status(ImStatus::Success))
297        ));
298    }
299}