Skip to main content

matter_interaction/
status.rs

1//! Interaction Model status codes — Matter Core Spec §8.10 (Status Codes).
2
3#![forbid(unsafe_code)]
4
5use crate::error::ImError;
6use crate::{expect_message_struct, skip_container};
7use matter_codec::{Element, Tag, TlvReader, Value};
8
9/// Parse a bare `StatusResponseMessage` (`{ 0: Status (u8), 0xFF: IMRev }`) into
10/// its status code, or `None` if the message is **not** a bare status response.
11///
12/// This disambiguates a message-level `StatusResponse` from a `WriteResponse`
13/// (whose tag 0 is the `WriteResponses` array) and an `InvokeResponse` (whose tag
14/// 0 is the `SuppressResponse` bool): only a `StatusResponse` carries a **scalar
15/// uint** at context tag 0. The write/invoke verbs use this to detect a
16/// `NEEDS_TIMED_INTERACTION (0xc6)` rejection, which the device returns as a
17/// message-level `StatusResponse` rather than a per-path status.
18///
19/// # Errors
20///
21/// Returns [`ImError`] if `bytes` is not a valid IM message struct, or the
22/// status value exceeds a single octet ([`ImError::InvalidStatusCode`]).
23pub fn parse_status_response(bytes: &[u8]) -> Result<Option<u8>, ImError> {
24    let mut r = TlvReader::new(bytes);
25    expect_message_struct(&mut r)?;
26    loop {
27        match r.next()? {
28            // A bare StatusResponse carries Status as a scalar uint at ctx 0.
29            Some(Element::Scalar {
30                tag: Tag::Context(0),
31                value: Value::Uint(n),
32            }) => {
33                let code = u8::try_from(n).map_err(|_| ImError::InvalidStatusCode { code: n })?;
34                return Ok(Some(code));
35            }
36            // Not a bare status response: end of message, or tag 0 is a bool
37            // (InvokeResponse SuppressResponse) / a container (WriteResponse array).
38            None
39            | Some(
40                Element::ContainerEnd
41                | Element::Scalar {
42                    tag: Tag::Context(0),
43                    ..
44                }
45                | Element::ContainerStart {
46                    tag: Tag::Context(0),
47                    ..
48                },
49            ) => return Ok(None),
50            Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
51            Some(_) => {}
52        }
53    }
54}
55
56/// An Interaction Model status, as carried by a `StatusIB`.
57#[derive(Copy, Clone, Debug, PartialEq, Eq)]
58#[non_exhaustive]
59pub enum ImStatus {
60    /// `SUCCESS` (0x00).
61    Success,
62    /// Any non-zero IM status code (e.g. 0x01 `FAILURE`, 0x86
63    /// `UNSUPPORTED_ATTRIBUTE`, 0x88 `INVALID_ACTION`). The raw code is
64    /// preserved so callers can log or branch on it.
65    Failure(u8),
66}
67
68impl ImStatus {
69    /// Map a raw IM status byte to [`ImStatus`].
70    #[must_use]
71    pub fn from_u8(code: u8) -> Self {
72        if code == 0x00 {
73            Self::Success
74        } else {
75            Self::Failure(code)
76        }
77    }
78
79    /// The raw IM status byte: `0x00` for [`Success`](Self::Success), the
80    /// preserved code for [`Failure`](Self::Failure). Inverse of [`Self::from_u8`].
81    #[must_use]
82    pub fn to_u8(self) -> u8 {
83        match self {
84            Self::Success => 0x00,
85            Self::Failure(code) => code,
86        }
87    }
88
89    /// `true` iff this is [`ImStatus::Success`].
90    #[must_use]
91    pub fn is_success(self) -> bool {
92        matches!(self, Self::Success)
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    #![allow(clippy::unwrap_used)] // Test code: CLAUDE.md test-code carve-out.
99    use super::*;
100
101    #[test]
102    fn maps_success_and_failure_codes() {
103        assert_eq!(ImStatus::from_u8(0x00), ImStatus::Success);
104        assert!(matches!(ImStatus::from_u8(0x01), ImStatus::Failure(0x01)));
105        assert!(matches!(ImStatus::from_u8(0x88), ImStatus::Failure(0x88)));
106    }
107
108    #[test]
109    fn is_success_only_for_zero() {
110        assert!(ImStatus::from_u8(0x00).is_success());
111        assert!(!ImStatus::from_u8(0x01).is_success());
112    }
113
114    #[test]
115    fn parse_status_response_reads_bare_status() {
116        let bytes = crate::build_status_response(0xc6);
117        assert_eq!(parse_status_response(&bytes).unwrap(), Some(0xc6));
118        let ok = crate::build_status_response(0x00);
119        assert_eq!(parse_status_response(&ok).unwrap(), Some(0x00));
120    }
121
122    #[test]
123    fn parse_status_response_none_for_write_response() {
124        // A WriteRequest's tag 0 is a bool (SuppressResponse); a WriteResponse's
125        // tag 0 is the WriteResponses array. Neither is a bare status response.
126        let write = crate::build_write_request(&[]);
127        assert_eq!(parse_status_response(&write).unwrap(), None);
128    }
129
130    #[test]
131    fn parse_status_response_none_for_invoke_request() {
132        // InvokeRequest tag 0 is a bool (SuppressResponse) ⇒ not a status response.
133        let inv = crate::build_invoke_request(
134            crate::CommandPath {
135                endpoint: 1,
136                cluster: 0x06,
137                command: 0x02,
138            },
139            &[0x15, 0x18],
140        );
141        assert_eq!(parse_status_response(&inv).unwrap(), None);
142    }
143}