Skip to main content

matter_interaction/
invoke.rs

1//! `InvokeRequestMessage` / `InvokeResponseMessage` framing — Matter §10.7.
2
3#![forbid(unsafe_code)]
4
5use crate::error::ImError;
6use crate::path::CommandPath;
7use crate::status::ImStatus;
8use crate::{
9    expect_message_struct, read_container_members, read_container_value, skip_container,
10    IM_REVISION,
11};
12use matter_codec::{ContainerKind, Element, Tag, TlvReader, TlvWriter, Value};
13
14/// Write a `CommandPathIB` (a TLV **list**: 0=endpoint, 1=cluster,
15/// 2=command) under `tag`.
16#[allow(clippy::expect_used, clippy::missing_panics_doc)] // Vec-backed TlvWriter is infallible.
17pub(crate) fn write_command_path(w: &mut TlvWriter<'_>, tag: Tag, path: CommandPath) {
18    w.start_list(tag).expect("infallible: vec writer");
19    w.put_uint(Tag::Context(0), u64::from(path.endpoint))
20        .expect("infallible: vec writer");
21    w.put_uint(Tag::Context(1), u64::from(path.cluster))
22        .expect("infallible: vec writer");
23    w.put_uint(Tag::Context(2), u64::from(path.command))
24        .expect("infallible: vec writer");
25    w.end_container().expect("infallible: vec writer");
26}
27
28/// Build an `InvokeRequestMessage` carrying a single command.
29///
30/// `command_fields_tlv` is the already-encoded command-fields struct
31/// (e.g. the output of `crate::noc::encode_csr_request`); it is embedded
32/// verbatim as the `CommandFields` member. `SuppressResponse` and
33/// `TimedRequest` are both `false`.
34///
35/// # Panics
36///
37/// Panics if `command_fields_tlv` is not a valid anonymous-tagged TLV
38/// element (i.e. not the output of a codec encode call). The function is
39/// otherwise infallible; `Vec`-backed `TlvWriter` never fails.
40#[must_use]
41pub fn build_invoke_request(path: CommandPath, command_fields_tlv: &[u8]) -> Vec<u8> {
42    build_invoke_request_inner(path, command_fields_tlv, false, false)
43}
44
45/// Like [`build_invoke_request`] but sets `TimedRequest = true` — the action half
46/// of a timed interaction, sent on the same exchange after a `TimedRequest`
47/// message (see [`crate::build_timed_request`]).
48///
49/// # Panics
50///
51/// As [`build_invoke_request`] (invalid `command_fields_tlv`).
52#[must_use]
53pub fn build_invoke_request_timed(path: CommandPath, command_fields_tlv: &[u8]) -> Vec<u8> {
54    build_invoke_request_inner(path, command_fields_tlv, true, false)
55}
56
57/// Like [`build_invoke_request`] but sets `SuppressResponse = true` — the form
58/// used for **group** (multicast) invokes. Group commands are unacknowledged at
59/// the IM layer: there is no return path for a multicast send, so the request
60/// must instruct the receiving devices to suppress any `InvokeResponse`
61/// (Matter Core Spec §8.9.2 / §10.7.2 — group commands carry `SuppressResponse`).
62/// `TimedRequest` is `false` (timed interactions are not available on group
63/// sends).
64///
65/// # Panics
66///
67/// As [`build_invoke_request`] (invalid `command_fields_tlv`).
68#[must_use]
69pub fn build_invoke_request_group(path: CommandPath, command_fields_tlv: &[u8]) -> Vec<u8> {
70    build_invoke_request_inner(path, command_fields_tlv, false, true)
71}
72
73#[allow(clippy::expect_used)] // Vec-backed TlvWriter is infallible.
74fn build_invoke_request_inner(
75    path: CommandPath,
76    command_fields_tlv: &[u8],
77    timed: bool,
78    suppress_response: bool,
79) -> Vec<u8> {
80    let mut buf = Vec::new();
81    let mut w = TlvWriter::new(&mut buf);
82    w.start_structure(Tag::Anonymous)
83        .expect("infallible: vec writer");
84    w.put_bool(Tag::Context(0), suppress_response)
85        .expect("infallible: vec writer"); // SuppressResponse
86    w.put_bool(Tag::Context(1), timed)
87        .expect("infallible: vec writer"); // TimedRequest
88    w.start_array(Tag::Context(2))
89        .expect("infallible: vec writer"); // InvokeRequests
90    {
91        w.start_structure(Tag::Anonymous)
92            .expect("infallible: vec writer"); // CommandDataIB
93        write_command_path(&mut w, Tag::Context(0), path);
94        w.put_preencoded(Tag::Context(1), command_fields_tlv)
95            .expect("infallible: caller passes a valid anonymous-tagged struct");
96        w.end_container().expect("infallible: vec writer"); // CommandDataIB
97    }
98    w.end_container().expect("infallible: vec writer"); // InvokeRequests array
99    w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
100        .expect("infallible: vec writer");
101    w.end_container().expect("infallible: vec writer"); // message struct
102    buf
103}
104
105/// Build an `InvokeRequestMessage` carrying **multiple** commands, each tagged
106/// with a sequential `CommandRef` (`CommandDataIB` tag 2) so the device's
107/// responses can be matched back. `SuppressResponse` and `TimedRequest` are
108/// `false`. Each tuple is `(path, command_fields_tlv)`, the fields an
109/// anonymous-tagged TLV blob (e.g. a `matter-clusters` command encoder output).
110///
111/// NB: the wire format permits a batch, but a device only accepts more than one
112/// command if it advertises `MaxPathsPerInvoke > 1` in its `SessionParameters`;
113/// the controller-side gating is deferred (M9-B5 scope) — callers must respect it.
114///
115/// # Panics
116///
117/// As [`build_invoke_request`] (a `command_fields_tlv` that is not a valid
118/// anonymous-tagged TLV element).
119#[must_use]
120#[allow(clippy::expect_used)] // Vec-backed TlvWriter is infallible.
121pub fn build_invoke_request_batch(commands: &[(CommandPath, &[u8])]) -> Vec<u8> {
122    let mut buf = Vec::new();
123    let mut w = TlvWriter::new(&mut buf);
124    w.start_structure(Tag::Anonymous)
125        .expect("infallible: vec writer");
126    w.put_bool(Tag::Context(0), false)
127        .expect("infallible: vec writer"); // SuppressResponse
128    w.put_bool(Tag::Context(1), false)
129        .expect("infallible: vec writer"); // TimedRequest
130    w.start_array(Tag::Context(2))
131        .expect("infallible: vec writer"); // InvokeRequests
132    for (i, (path, fields)) in commands.iter().enumerate() {
133        w.start_structure(Tag::Anonymous)
134            .expect("infallible: vec writer"); // CommandDataIB
135        write_command_path(&mut w, Tag::Context(0), *path);
136        w.put_preencoded(Tag::Context(1), fields)
137            .expect("infallible: caller passes a valid anonymous-tagged struct");
138        // CommandRef (tag 2): the index. `try_from` is total for any realistic
139        // batch; cap defensively rather than panic on an absurd one.
140        let cref = u16::try_from(i).unwrap_or(u16::MAX);
141        w.put_uint(Tag::Context(2), u64::from(cref))
142            .expect("infallible: vec writer");
143        w.end_container().expect("infallible: vec writer"); // CommandDataIB
144    }
145    w.end_container().expect("infallible: vec writer"); // InvokeRequests array
146    w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
147        .expect("infallible: vec writer");
148    w.end_container().expect("infallible: vec writer"); // message struct
149    buf
150}
151
152/// Outcome of parsing a single-command `InvokeResponseMessage`.
153#[derive(Clone, Debug, PartialEq, Eq)]
154pub enum InvokeResponse {
155    /// The device returned a response command. `fields_tlv` is the
156    /// re-anonymised `CommandFields` struct, ready to hand to
157    /// `Commissioner::on_response`.
158    Command {
159        /// Path of the response command (`(endpoint, cluster, command)`).
160        path: CommandPath,
161        /// The command-fields struct, re-encoded with an anonymous tag.
162        fields_tlv: Vec<u8>,
163    },
164    /// The device returned a bare status (no response command payload).
165    Status(ImStatus),
166}
167
168/// One parsed `InvokeResponseIB` from a batched response, with its `CommandRef`
169/// (`CommandDataIB` / `CommandStatusIB` tag 2) for matching to the request command.
170#[derive(Clone, Debug, PartialEq, Eq)]
171pub struct InvokeResponseEntry {
172    /// The `CommandRef` echoed by the device, if present.
173    pub command_ref: Option<u16>,
174    /// The response: a command payload or a status.
175    pub response: InvokeResponse,
176}
177
178/// Re-encode a [`Value`] as a standalone, anonymous-tagged TLV blob. Used
179/// to lift an embedded `CommandFields` struct back out as bytes the state
180/// machine can consume.
181///
182/// Integer wire widths are normalized to the minimal width, so the re-encoded
183/// bytes are not guaranteed byte-identical to the device's original encoding
184/// (this path is consumed locally, never retransmitted).
185pub(crate) fn reencode_anonymous(value: &Value) -> Vec<u8> {
186    let mut buf = Vec::new();
187    let mut w = TlvWriter::new(&mut buf);
188    #[allow(clippy::expect_used)] // Vec-backed writer is infallible.
189    w.write_value(Tag::Anonymous, value)
190        .expect("infallible: vec writer");
191    buf
192}
193
194/// Read a `CommandPathIB` list (`Value::List` members) into a [`CommandPath`].
195pub(crate) fn command_path_from_value(members: &[(Tag, Value)]) -> Result<CommandPath, ImError> {
196    let mut endpoint = None;
197    let mut cluster = None;
198    let mut command = None;
199    for (tag, v) in members {
200        match (tag, v) {
201            (Tag::Context(0), Value::Uint(n)) => {
202                endpoint =
203                    Some(u16::try_from(*n).map_err(|_| {
204                        ImError::UnexpectedValue("CommandPath.endpoint exceeds u16")
205                    })?);
206            }
207            (Tag::Context(1), Value::Uint(n)) => {
208                cluster =
209                    Some(u32::try_from(*n).map_err(|_| {
210                        ImError::UnexpectedValue("CommandPath.cluster exceeds u32")
211                    })?);
212            }
213            (Tag::Context(2), Value::Uint(n)) => {
214                command =
215                    Some(u32::try_from(*n).map_err(|_| {
216                        ImError::UnexpectedValue("CommandPath.command exceeds u32")
217                    })?);
218            }
219            _ => {}
220        }
221    }
222    Ok(CommandPath {
223        endpoint: endpoint.ok_or(ImError::MissingField("CommandPath.endpoint"))?,
224        cluster: cluster.ok_or(ImError::MissingField("CommandPath.cluster"))?,
225        command: command.ok_or(ImError::MissingField("CommandPath.command"))?,
226    })
227}
228
229/// Parse a single-command `InvokeResponseMessage`.
230///
231/// Reads the first `InvokeResponseIB` in the `InvokeResponses` array and
232/// returns either its response-command payload or its status. Additional
233/// `InvokeResponseIB`s (not produced by commissioning) are ignored.
234///
235/// # Errors
236///
237/// Returns [`ImError`] if the message is not a struct, lacks the
238/// `InvokeResponses` array, or the first IB has neither Command nor Status.
239pub fn parse_invoke_response(bytes: &[u8]) -> Result<InvokeResponse, ImError> {
240    let mut r = TlvReader::new(bytes);
241    expect_message_struct(&mut r)?;
242
243    loop {
244        match r.next()? {
245            None | Some(Element::ContainerEnd) => {
246                return Err(ImError::MissingField("InvokeResponses"))
247            }
248            Some(Element::ContainerStart {
249                tag: Tag::Context(1),
250                kind: ContainerKind::Array,
251            }) => break,
252            Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
253            Some(_) => {}
254        }
255    }
256
257    match r.next()? {
258        Some(Element::ContainerStart {
259            kind: ContainerKind::Structure,
260            ..
261        }) => {}
262        _ => return Err(ImError::MissingField("InvokeResponseIB")),
263    }
264
265    loop {
266        match r.next()? {
267            None | Some(Element::ContainerEnd) => return Err(ImError::EmptyInvokeResponse),
268            Some(Element::ContainerStart {
269                tag: Tag::Context(0),
270                kind: ContainerKind::Structure,
271            }) => {
272                return parse_command_data(&mut r).map(|(path, fields)| InvokeResponse::Command {
273                    path,
274                    fields_tlv: fields,
275                });
276            }
277            Some(Element::ContainerStart {
278                tag: Tag::Context(1),
279                kind: ContainerKind::Structure,
280            }) => {
281                return parse_command_status(&mut r).map(InvokeResponse::Status);
282            }
283            Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
284            Some(_) => {}
285        }
286    }
287}
288
289/// Parse a multi-command `InvokeResponseMessage`: every `InvokeResponseIB`, each
290/// with its `CommandRef`. The single-command [`parse_invoke_response`] is retained
291/// for the commissioning path (reads only the first IB, ignores `CommandRef`).
292///
293/// # Errors
294///
295/// Returns [`ImError`] if the message is not a struct, lacks the
296/// `InvokeResponses` array, or an IB has neither Command nor Status.
297pub fn parse_invoke_response_batch(bytes: &[u8]) -> Result<Vec<InvokeResponseEntry>, ImError> {
298    let mut r = TlvReader::new(bytes);
299    expect_message_struct(&mut r)?;
300    // Advance to the InvokeResponses array (context tag 1).
301    loop {
302        match r.next()? {
303            None | Some(Element::ContainerEnd) => {
304                return Err(ImError::MissingField("InvokeResponses"))
305            }
306            Some(Element::ContainerStart {
307                tag: Tag::Context(1),
308                kind: ContainerKind::Array,
309            }) => break,
310            Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
311            Some(_) => {}
312        }
313    }
314    let mut out = Vec::new();
315    loop {
316        match r.next()? {
317            None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
318            Some(Element::ContainerEnd) => return Ok(out), // end of array
319            Some(Element::ContainerStart {
320                kind: ContainerKind::Structure,
321                ..
322            }) => out.push(parse_invoke_response_ib(&mut r)?),
323            Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
324            Some(_) => {}
325        }
326    }
327}
328
329/// Parse one `InvokeResponseIB` body into an [`InvokeResponseEntry`] (reader
330/// positioned just after its struct start). Drains the **entire** IB (through its
331/// matching `ContainerEnd`) so the caller's array walk stays in sync.
332fn parse_invoke_response_ib(r: &mut TlvReader<'_>) -> Result<InvokeResponseEntry, ImError> {
333    let mut entry: Option<InvokeResponseEntry> = None;
334    loop {
335        match r.next()? {
336            None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
337            Some(Element::ContainerEnd) => break, // end of this InvokeResponseIB
338            // Command = CommandDataIB
339            Some(Element::ContainerStart {
340                tag: Tag::Context(0),
341                kind: ContainerKind::Structure,
342            }) => {
343                let (path, fields, command_ref) = parse_command_data_ref(r)?;
344                entry = Some(InvokeResponseEntry {
345                    command_ref,
346                    response: InvokeResponse::Command {
347                        path,
348                        fields_tlv: fields,
349                    },
350                });
351            }
352            // Status = CommandStatusIB
353            Some(Element::ContainerStart {
354                tag: Tag::Context(1),
355                kind: ContainerKind::Structure,
356            }) => {
357                let (status, command_ref) = parse_command_status_ref(r)?;
358                entry = Some(InvokeResponseEntry {
359                    command_ref,
360                    response: InvokeResponse::Status(status),
361                });
362            }
363            Some(Element::ContainerStart { .. }) => skip_container(r)?,
364            Some(_) => {}
365        }
366    }
367    entry.ok_or(ImError::EmptyInvokeResponse)
368}
369
370/// Parse a `CommandDataIB` body (reader positioned just after its struct
371/// start), returning `(path, anonymous-tagged CommandFields bytes)`. The single-
372/// command path ignores the `CommandRef`; [`parse_command_data_ref`] captures it.
373fn parse_command_data(r: &mut TlvReader<'_>) -> Result<(CommandPath, Vec<u8>), ImError> {
374    let (path, fields, _ref) = parse_command_data_ref(r)?;
375    Ok((path, fields))
376}
377
378/// Like [`parse_command_data`] but also captures the `CommandRef` (tag 2).
379fn parse_command_data_ref(
380    r: &mut TlvReader<'_>,
381) -> Result<(CommandPath, Vec<u8>, Option<u16>), ImError> {
382    let mut path = None;
383    let mut fields = Vec::new();
384    let mut command_ref = None;
385    loop {
386        match r.next()? {
387            None => return Err(ImError::MissingField("CommandDataIB.body")),
388            Some(Element::ContainerEnd) => break,
389            Some(Element::ContainerStart {
390                tag: Tag::Context(0),
391                kind: ContainerKind::List,
392            }) => {
393                let body = read_container_members(r)?;
394                path = Some(command_path_from_value(&body)?);
395            }
396            Some(Element::ContainerStart {
397                tag: Tag::Context(1),
398                kind,
399            }) => {
400                let v = read_container_value(r, kind)?;
401                fields = reencode_anonymous(&v);
402            }
403            // CommandRef (tag 2), a scalar uint.
404            Some(Element::Scalar {
405                tag: Tag::Context(2),
406                value: Value::Uint(n),
407            }) => command_ref = u16::try_from(n).ok(),
408            Some(Element::ContainerStart { .. }) => skip_container(r)?,
409            Some(_) => {}
410        }
411    }
412    // If no CommandFields member was present, `fields` is an empty Vec, which
413    // is not valid TLV. Canonicalize to an anonymous empty struct so callers
414    // always receive a valid TLV blob.
415    let fields = if fields.is_empty() {
416        reencode_anonymous(&Value::Structure(Vec::new()))
417    } else {
418        fields
419    };
420    Ok((
421        path.ok_or(ImError::MissingField("CommandDataIB.CommandPath"))?,
422        fields,
423        command_ref,
424    ))
425}
426
427/// Parse a `CommandStatusIB` body, returning the `StatusIB.Status` mapped
428/// to [`ImStatus`]. The single-command path ignores the `CommandRef`;
429/// [`parse_command_status_ref`] captures it.
430fn parse_command_status(r: &mut TlvReader<'_>) -> Result<ImStatus, ImError> {
431    let (status, _ref) = parse_command_status_ref(r)?;
432    Ok(status)
433}
434
435/// Like [`parse_command_status`] but also captures the `CommandRef` (tag 2).
436fn parse_command_status_ref(r: &mut TlvReader<'_>) -> Result<(ImStatus, Option<u16>), ImError> {
437    // `None` ⇒ the Status member was never seen (genuinely missing).
438    // `Some(raw)` ⇒ the member was present; `raw` is the verbatim wire value,
439    // which we range-check to a `u8` only after the parse loop so an
440    // out-of-range value reports as `InvalidStatusCode`, not `MissingField`.
441    let mut status: Option<u64> = None;
442    let mut command_ref = None;
443    loop {
444        match r.next()? {
445            None => return Err(ImError::MissingField("CommandStatusIB.body")),
446            Some(Element::ContainerEnd) => break,
447            Some(Element::ContainerStart {
448                tag: Tag::Context(1),
449                kind: ContainerKind::Structure,
450            }) => {
451                let members = read_container_members(r)?;
452                // Last value wins for duplicate tags (lenient parsing); real devices never duplicate Status.
453                for (tag, v) in &members {
454                    if let (Tag::Context(0), Value::Uint(n)) = (tag, v) {
455                        status = Some(*n);
456                    }
457                }
458            }
459            // CommandRef (tag 2), a scalar uint.
460            Some(Element::Scalar {
461                tag: Tag::Context(2),
462                value: Value::Uint(n),
463            }) => command_ref = u16::try_from(n).ok(),
464            Some(Element::ContainerStart { .. }) => skip_container(r)?,
465            Some(_) => {}
466        }
467    }
468    let raw = status.ok_or(ImError::MissingField("StatusIB.Status"))?;
469    let code = u8::try_from(raw).map_err(|_| ImError::InvalidStatusCode { code: raw })?;
470    Ok((ImStatus::from_u8(code), command_ref))
471}
472
473#[cfg(test)]
474mod tests {
475    // Test-code carve-out: see CLAUDE.md.
476    #![allow(clippy::unwrap_used, clippy::expect_used)]
477
478    use super::*;
479    use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
480
481    #[test]
482    fn invoke_request_has_expected_structure() {
483        // ArmFailSafe-like: endpoint 0, cluster 0x0030, command 0x00,
484        // command fields = an empty anonymous struct (0x15 0x18).
485        let fields = vec![0x15, 0x18];
486        let bytes = build_invoke_request(
487            CommandPath {
488                endpoint: 0,
489                cluster: 0x0030,
490                command: 0x00,
491            },
492            &fields,
493        );
494
495        let mut r = TlvReader::new(&bytes);
496        // Top-level InvokeRequestMessage struct (anonymous).
497        assert!(matches!(
498            r.next().unwrap(),
499            Some(Element::ContainerStart {
500                tag: Tag::Anonymous,
501                kind: ContainerKind::Structure
502            })
503        ));
504        // SuppressResponse = false.
505        assert!(matches!(
506            r.next().unwrap(),
507            Some(Element::Scalar {
508                tag: Tag::Context(0),
509                value: Value::Bool(false)
510            })
511        ));
512        // TimedRequest = false.
513        assert!(matches!(
514            r.next().unwrap(),
515            Some(Element::Scalar {
516                tag: Tag::Context(1),
517                value: Value::Bool(false)
518            })
519        ));
520        // InvokeRequests array start.
521        assert!(matches!(
522            r.next().unwrap(),
523            Some(Element::ContainerStart {
524                tag: Tag::Context(2),
525                kind: ContainerKind::Array
526            })
527        ));
528        // CommandDataIB anonymous struct start.
529        assert!(matches!(
530            r.next().unwrap(),
531            Some(Element::ContainerStart {
532                tag: Tag::Anonymous,
533                kind: ContainerKind::Structure
534            })
535        ));
536        // CommandPathIB list at context tag 0.
537        assert!(matches!(
538            r.next().unwrap(),
539            Some(Element::ContainerStart {
540                tag: Tag::Context(0),
541                kind: ContainerKind::List
542            })
543        ));
544        // Endpoint = 0 at context tag 0.
545        assert!(matches!(
546            r.next().unwrap(),
547            Some(Element::Scalar {
548                tag: Tag::Context(0),
549                value: Value::Uint(0)
550            })
551        ));
552        // Cluster = 0x0030 at context tag 1.
553        assert!(matches!(
554            r.next().unwrap(),
555            Some(Element::Scalar {
556                tag: Tag::Context(1),
557                value: Value::Uint(0x0030)
558            })
559        ));
560        // Command = 0x00 at context tag 2.
561        assert!(matches!(
562            r.next().unwrap(),
563            Some(Element::Scalar {
564                tag: Tag::Context(2),
565                value: Value::Uint(0)
566            })
567        ));
568        // End CommandPathIB list.
569        assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
570        // CommandFields (empty struct) at context tag 1 — ContainerStart then end.
571        assert!(matches!(
572            r.next().unwrap(),
573            Some(Element::ContainerStart {
574                tag: Tag::Context(1),
575                kind: ContainerKind::Structure
576            })
577        ));
578        assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
579        // End CommandDataIB struct.
580        assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
581        // End InvokeRequests array.
582        assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
583        // InteractionModelRevision = IM_REVISION at context tag 0xFF.
584        assert!(matches!(
585            r.next().unwrap(),
586            Some(Element::Scalar { tag: Tag::Context(0xFF), value: Value::Uint(v) })
587                if v == u64::from(IM_REVISION)
588        ));
589        // End top-level struct.
590        assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
591        // No more elements.
592        assert!(r.next().unwrap().is_none());
593    }
594
595    #[test]
596    fn invoke_request_carries_command_path_and_fields() {
597        // `put_preencoded` re-tags the anonymous-struct control byte (0x15)
598        // to a context-1 struct (0x35 0x01), then appends the body (0x18).
599        // Verify that the re-tagged representation [0x35, 0x01, 0x18] is
600        // present in the output (i.e. the fields blob was embedded).
601        let fields = vec![0x15u8, 0x18]; // anonymous empty struct
602        let bytes = build_invoke_request(
603            CommandPath {
604                endpoint: 1,
605                cluster: 0x0031,
606                command: 0x06,
607            },
608            &fields,
609        );
610        // Retagged form: context-1 struct start (0x35, 0x01) then body (0x18).
611        let retagged = [0x35u8, 0x01, 0x18];
612        assert!(
613            bytes.windows(retagged.len()).any(|w| w == retagged),
614            "command fields not embedded (expected retagged bytes {retagged:02X?} in {bytes:02X?})",
615        );
616    }
617
618    #[test]
619    fn parses_command_response_payload() {
620        use matter_codec::{Tag, TlvWriter};
621        let mut buf = Vec::new();
622        let mut w = TlvWriter::new(&mut buf);
623        w.start_structure(Tag::Anonymous).unwrap();
624        w.put_bool(Tag::Context(0), false).unwrap(); // SuppressResponse
625        w.start_array(Tag::Context(1)).unwrap(); // InvokeResponses
626        {
627            w.start_structure(Tag::Anonymous).unwrap(); // InvokeResponseIB
628            w.start_structure(Tag::Context(0)).unwrap(); // Command = CommandDataIB
629            w.start_list(Tag::Context(0)).unwrap(); // CommandPath
630            w.put_uint(Tag::Context(0), 0).unwrap();
631            w.put_uint(Tag::Context(1), 0x0030).unwrap();
632            w.put_uint(Tag::Context(2), 0x05).unwrap();
633            w.end_container().unwrap();
634            w.start_structure(Tag::Context(1)).unwrap(); // CommandFields (empty)
635            w.end_container().unwrap();
636            w.end_container().unwrap(); // CommandDataIB
637            w.end_container().unwrap(); // InvokeResponseIB
638        }
639        w.end_container().unwrap(); // array
640        w.put_uint(Tag::Context(0xFF), 11).unwrap();
641        w.end_container().unwrap();
642
643        let parsed = parse_invoke_response(&buf).unwrap();
644        match parsed {
645            InvokeResponse::Command { path, fields_tlv } => {
646                assert_eq!(path.endpoint, 0);
647                assert_eq!(path.cluster, 0x0030);
648                assert_eq!(path.command, 0x05);
649                assert_eq!(fields_tlv, vec![0x15, 0x18]); // re-anonymised empty struct
650            }
651            InvokeResponse::Status(_) => panic!("expected Command, got Status"),
652        }
653    }
654
655    #[test]
656    fn parses_command_with_nonempty_fields() {
657        use matter_codec::{Tag, TlvWriter};
658
659        // Build the expected anonymous struct bytes independently for comparison:
660        // anonymous struct containing one scalar: Context(0) = 0x2A (uint).
661        let mut expected_buf = Vec::new();
662        {
663            let mut w = TlvWriter::new(&mut expected_buf);
664            w.start_structure(Tag::Anonymous).unwrap();
665            w.put_uint(Tag::Context(0), 0x2A).unwrap();
666            w.end_container().unwrap();
667        }
668
669        // Build an InvokeResponseMessage whose CommandFields is that same struct.
670        let mut buf = Vec::new();
671        let mut w = TlvWriter::new(&mut buf);
672        w.start_structure(Tag::Anonymous).unwrap();
673        w.put_bool(Tag::Context(0), false).unwrap(); // SuppressResponse
674        w.start_array(Tag::Context(1)).unwrap(); // InvokeResponses
675        {
676            w.start_structure(Tag::Anonymous).unwrap(); // InvokeResponseIB
677            w.start_structure(Tag::Context(0)).unwrap(); // CommandDataIB
678            w.start_list(Tag::Context(0)).unwrap(); // CommandPath
679            w.put_uint(Tag::Context(0), 1).unwrap(); // endpoint
680            w.put_uint(Tag::Context(1), 0x0050).unwrap(); // cluster
681            w.put_uint(Tag::Context(2), 0x01).unwrap(); // command
682            w.end_container().unwrap(); // CommandPath
683                                        // CommandFields at Context(1): a struct with one member
684            w.start_structure(Tag::Context(1)).unwrap();
685            w.put_uint(Tag::Context(0), 0x2A).unwrap();
686            w.end_container().unwrap(); // CommandFields
687            w.end_container().unwrap(); // CommandDataIB
688            w.end_container().unwrap(); // InvokeResponseIB
689        }
690        w.end_container().unwrap(); // array
691        w.put_uint(Tag::Context(0xFF), 11).unwrap();
692        w.end_container().unwrap();
693
694        let parsed = parse_invoke_response(&buf).unwrap();
695        match parsed {
696            InvokeResponse::Command { path, fields_tlv } => {
697                assert_eq!(path.endpoint, 1);
698                assert_eq!(path.cluster, 0x0050);
699                assert_eq!(path.command, 0x01);
700                assert_eq!(
701                    fields_tlv, expected_buf,
702                    "fields_tlv should decode to the same struct content as the original"
703                );
704            }
705            InvokeResponse::Status(_) => panic!("expected Command, got Status"),
706        }
707    }
708
709    #[test]
710    fn rejects_out_of_range_endpoint() {
711        use crate::error::ImError;
712        use matter_codec::{Tag, TlvWriter};
713
714        let mut buf = Vec::new();
715        let mut w = TlvWriter::new(&mut buf);
716        w.start_structure(Tag::Anonymous).unwrap();
717        w.put_bool(Tag::Context(0), false).unwrap();
718        w.start_array(Tag::Context(1)).unwrap();
719        {
720            w.start_structure(Tag::Anonymous).unwrap(); // InvokeResponseIB
721            w.start_structure(Tag::Context(0)).unwrap(); // CommandDataIB
722            w.start_list(Tag::Context(0)).unwrap(); // CommandPath
723            w.put_uint(Tag::Context(0), 0x0001_0000).unwrap(); // endpoint exceeds u16
724            w.put_uint(Tag::Context(1), 0x0030).unwrap();
725            w.put_uint(Tag::Context(2), 0x00).unwrap();
726            w.end_container().unwrap();
727            w.start_structure(Tag::Context(1)).unwrap(); // CommandFields (empty)
728            w.end_container().unwrap();
729            w.end_container().unwrap(); // CommandDataIB
730            w.end_container().unwrap(); // InvokeResponseIB
731        }
732        w.end_container().unwrap();
733        w.put_uint(Tag::Context(0xFF), 11).unwrap();
734        w.end_container().unwrap();
735
736        let result = parse_invoke_response(&buf);
737        assert!(
738            matches!(result, Err(ImError::UnexpectedValue(_))),
739            "expected UnexpectedValue for out-of-range endpoint, got {result:?}"
740        );
741    }
742
743    #[test]
744    fn empty_invoke_responses_array_errors() {
745        use crate::error::ImError;
746        use matter_codec::{Tag, TlvWriter};
747
748        let mut buf = Vec::new();
749        let mut w = TlvWriter::new(&mut buf);
750        w.start_structure(Tag::Anonymous).unwrap();
751        w.put_bool(Tag::Context(0), false).unwrap();
752        w.start_array(Tag::Context(1)).unwrap(); // empty InvokeResponses array
753        w.end_container().unwrap();
754        w.put_uint(Tag::Context(0xFF), 11).unwrap();
755        w.end_container().unwrap();
756
757        let result = parse_invoke_response(&buf);
758        assert!(
759            matches!(result, Err(ImError::MissingField(_))),
760            "expected MissingField for empty InvokeResponses, got {result:?}"
761        );
762    }
763
764    #[test]
765    fn parses_status_response() {
766        use matter_codec::{Tag, TlvWriter};
767        let mut buf = Vec::new();
768        let mut w = TlvWriter::new(&mut buf);
769        w.start_structure(Tag::Anonymous).unwrap();
770        w.put_bool(Tag::Context(0), false).unwrap();
771        w.start_array(Tag::Context(1)).unwrap();
772        {
773            w.start_structure(Tag::Anonymous).unwrap(); // InvokeResponseIB
774            w.start_structure(Tag::Context(1)).unwrap(); // Status = CommandStatusIB
775            w.start_list(Tag::Context(0)).unwrap(); // CommandPath
776            w.put_uint(Tag::Context(0), 0).unwrap();
777            w.put_uint(Tag::Context(1), 0x0030).unwrap();
778            w.put_uint(Tag::Context(2), 0x00).unwrap();
779            w.end_container().unwrap();
780            w.start_structure(Tag::Context(1)).unwrap(); // StatusIB
781            w.put_uint(Tag::Context(0), 0x01).unwrap(); // Status = FAILURE
782            w.end_container().unwrap();
783            w.end_container().unwrap(); // CommandStatusIB
784            w.end_container().unwrap(); // InvokeResponseIB
785        }
786        w.end_container().unwrap();
787        w.put_uint(Tag::Context(0xFF), 11).unwrap();
788        w.end_container().unwrap();
789
790        let parsed = parse_invoke_response(&buf).unwrap();
791        assert!(matches!(
792            parsed,
793            InvokeResponse::Status(ImStatus::Failure(0x01))
794        ));
795    }
796
797    /// Build an `InvokeResponseMessage` whose single `InvokeResponseIB` carries
798    /// a `CommandStatusIB`. `status` controls the `StatusIB.Status` member:
799    /// `Some(v)` writes that raw uint, `None` omits the member entirely.
800    fn invoke_status_response(status: Option<u64>) -> Vec<u8> {
801        use matter_codec::{Tag, TlvWriter};
802        let mut buf = Vec::new();
803        let mut w = TlvWriter::new(&mut buf);
804        w.start_structure(Tag::Anonymous).unwrap();
805        w.put_bool(Tag::Context(0), false).unwrap();
806        w.start_array(Tag::Context(1)).unwrap();
807        w.start_structure(Tag::Anonymous).unwrap(); // InvokeResponseIB
808        w.start_structure(Tag::Context(1)).unwrap(); // Status = CommandStatusIB
809        w.start_list(Tag::Context(0)).unwrap(); // CommandPath
810        w.put_uint(Tag::Context(0), 0).unwrap();
811        w.put_uint(Tag::Context(1), 0x0030).unwrap();
812        w.put_uint(Tag::Context(2), 0x00).unwrap();
813        w.end_container().unwrap();
814        w.start_structure(Tag::Context(1)).unwrap(); // StatusIB
815        if let Some(v) = status {
816            w.put_uint(Tag::Context(0), v).unwrap();
817        }
818        w.end_container().unwrap();
819        w.end_container().unwrap(); // CommandStatusIB
820        w.end_container().unwrap(); // InvokeResponseIB
821        w.end_container().unwrap(); // array
822        w.put_uint(Tag::Context(0xFF), 11).unwrap();
823        w.end_container().unwrap();
824        buf
825    }
826
827    #[test]
828    fn command_status_out_of_range_is_invalid_status_code() {
829        // StatusIB.Status = 0x100 — present on the wire but exceeds the single
830        // octet a Matter status code occupies. Must surface as the distinct
831        // InvalidStatusCode error, NOT MissingField.
832        let buf = invoke_status_response(Some(0x100));
833        match parse_invoke_response(&buf) {
834            Err(ImError::InvalidStatusCode { code }) => assert_eq!(code, 0x100),
835            other => panic!("expected InvalidStatusCode {{ code: 0x100 }}, got {other:?}"),
836        }
837    }
838
839    #[test]
840    fn command_status_valid_code_still_parses() {
841        let buf = invoke_status_response(Some(0x88));
842        assert!(matches!(
843            parse_invoke_response(&buf),
844            Ok(InvokeResponse::Status(ImStatus::Failure(0x88)))
845        ));
846    }
847
848    #[test]
849    fn command_status_missing_field_still_missing_field() {
850        // No Status member at all — genuinely missing, so MissingField is right.
851        let buf = invoke_status_response(None);
852        assert!(matches!(
853            parse_invoke_response(&buf),
854            Err(ImError::MissingField("StatusIB.Status"))
855        ));
856    }
857
858    #[test]
859    fn invoke_response_ib_with_no_command_or_status_errors() {
860        use matter_codec::{Tag, TlvWriter};
861        let mut buf = Vec::new();
862        let mut w = TlvWriter::new(&mut buf);
863        w.start_structure(Tag::Anonymous).unwrap();
864        w.put_bool(Tag::Context(0), false).unwrap();
865        w.start_array(Tag::Context(1)).unwrap(); // InvokeResponses
866        w.start_structure(Tag::Anonymous).unwrap(); // InvokeResponseIB with no Command/Status
867        w.put_uint(Tag::Context(7), 0).unwrap(); // unrelated field
868        w.end_container().unwrap();
869        w.end_container().unwrap(); // array
870        w.put_uint(Tag::Context(0xFF), 11).unwrap();
871        w.end_container().unwrap();
872
873        assert!(matches!(
874            parse_invoke_response(&buf),
875            Err(ImError::EmptyInvokeResponse)
876        ));
877    }
878
879    #[test]
880    fn batch_request_carries_command_refs() {
881        let fields = vec![0x15u8, 0x18]; // anonymous empty struct
882        let bytes = build_invoke_request_batch(&[
883            (
884                CommandPath {
885                    endpoint: 1,
886                    cluster: 0x06,
887                    command: 0x02,
888                },
889                &fields,
890            ),
891            (
892                CommandPath {
893                    endpoint: 2,
894                    cluster: 0x06,
895                    command: 0x00,
896                },
897                &fields,
898            ),
899        ]);
900        // The InvokeRequests array (ctx 2) holds two CommandDataIB structs, each
901        // ending with CommandRef (ctx 2) = 0 then 1. Parse the whole thing back
902        // through the batch response parser shape is not applicable (this is a
903        // request), so just confirm both refs appear in order in the stream.
904        let mut r = TlvReader::new(&bytes);
905        let mut refs = Vec::new();
906        let mut depth = 0i32;
907        while let Some(el) = r.next().unwrap() {
908            match el {
909                Element::ContainerStart { .. } => depth += 1,
910                Element::ContainerEnd => depth -= 1,
911                // CommandRef sits at depth 2 (struct > array > CommandDataIB), tag 2.
912                Element::Scalar {
913                    tag: Tag::Context(2),
914                    value: Value::Uint(n),
915                } if depth == 3 => refs.push(n),
916                _ => {}
917            }
918        }
919        assert_eq!(refs, vec![0, 1], "CommandRefs must be 0 then 1");
920    }
921
922    #[test]
923    fn batch_response_parses_all_ibs_with_refs() {
924        use matter_codec::{Tag, TlvWriter};
925        // Two InvokeResponseIBs: a Command (ref 0) and a Status (ref 1).
926        let mut buf = Vec::new();
927        let mut w = TlvWriter::new(&mut buf);
928        w.start_structure(Tag::Anonymous).unwrap();
929        w.put_bool(Tag::Context(0), false).unwrap(); // SuppressResponse
930        w.start_array(Tag::Context(1)).unwrap(); // InvokeResponses
931        {
932            // IB 1: Command = CommandDataIB { path, fields(empty), ref=0 }
933            w.start_structure(Tag::Anonymous).unwrap();
934            w.start_structure(Tag::Context(0)).unwrap(); // Command
935            w.start_list(Tag::Context(0)).unwrap();
936            w.put_uint(Tag::Context(0), 1).unwrap();
937            w.put_uint(Tag::Context(1), 0x06).unwrap();
938            w.put_uint(Tag::Context(2), 0x02).unwrap();
939            w.end_container().unwrap();
940            w.start_structure(Tag::Context(1)).unwrap();
941            w.end_container().unwrap(); // empty fields
942            w.put_uint(Tag::Context(2), 0).unwrap(); // CommandRef
943            w.end_container().unwrap(); // Command
944            w.end_container().unwrap(); // IB 1
945                                        // IB 2: Status = CommandStatusIB { path, status=SUCCESS, ref=1 }
946            w.start_structure(Tag::Anonymous).unwrap();
947            w.start_structure(Tag::Context(1)).unwrap(); // Status
948            w.start_list(Tag::Context(0)).unwrap();
949            w.put_uint(Tag::Context(0), 2).unwrap();
950            w.put_uint(Tag::Context(1), 0x06).unwrap();
951            w.put_uint(Tag::Context(2), 0x00).unwrap();
952            w.end_container().unwrap();
953            w.start_structure(Tag::Context(1)).unwrap(); // StatusIB
954            w.put_uint(Tag::Context(0), 0).unwrap(); // SUCCESS
955            w.end_container().unwrap();
956            w.put_uint(Tag::Context(2), 1).unwrap(); // CommandRef
957            w.end_container().unwrap(); // Status
958            w.end_container().unwrap(); // IB 2
959        }
960        w.end_container().unwrap(); // array
961        w.put_uint(Tag::Context(0xFF), 11).unwrap();
962        w.end_container().unwrap();
963
964        let entries = parse_invoke_response_batch(&buf).unwrap();
965        assert_eq!(entries.len(), 2);
966        assert_eq!(entries[0].command_ref, Some(0));
967        assert!(matches!(
968            entries[0].response,
969            InvokeResponse::Command { ref path, .. } if path.endpoint == 1 && path.command == 0x02
970        ));
971        assert_eq!(entries[1].command_ref, Some(1));
972        assert_eq!(
973            entries[1].response,
974            InvokeResponse::Status(ImStatus::Success)
975        );
976
977        // Back-compat: the single-command parser reads the first IB only.
978        match parse_invoke_response(&buf).unwrap() {
979            InvokeResponse::Command { path, .. } => assert_eq!(path.endpoint, 1),
980            InvokeResponse::Status(_) => panic!("expected the first IB (a Command)"),
981        }
982    }
983}