#![forbid(unsafe_code)]
use crate::invoke::{command_path_from_reader, retag_container_anonymous, write_command_path};
use crate::path::CommandPath;
use crate::status::ImStatus;
use crate::{expect_message_struct, skip_container, IM_REVISION};
use matter_codec::{ContainerKind, Element, Tag, TlvReader, TlvWriter, Value};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InvokedCommand {
pub path: CommandPath,
pub fields_tlv: Vec<u8>,
pub command_ref: Option<u16>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParsedInvokeRequest {
pub suppress_response: bool,
pub timed: bool,
pub commands: Vec<InvokedCommand>,
}
pub fn parse_invoke_request(bytes: &[u8]) -> Result<ParsedInvokeRequest, crate::ImError> {
let mut r = TlvReader::new(bytes);
expect_message_struct(&mut r)?;
let mut suppress_response = false;
let mut timed = false;
let mut commands = Vec::new();
loop {
match r.next()? {
None | Some(Element::ContainerEnd) => break,
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Bool(b),
}) => suppress_response = b,
Some(Element::Scalar {
tag: Tag::Context(1),
value: Value::Bool(b),
}) => timed = b,
Some(Element::ContainerStart {
tag: Tag::Context(2),
kind: ContainerKind::Array,
}) => {
read_invoke_requests(&mut r, &mut commands)?;
}
Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
Some(_) => {}
}
}
Ok(ParsedInvokeRequest {
suppress_response,
timed,
commands,
})
}
fn read_invoke_requests(
r: &mut TlvReader<'_>,
out: &mut Vec<InvokedCommand>,
) -> Result<(), crate::ImError> {
loop {
match r.next()? {
None | Some(Element::ContainerEnd) => return Ok(()),
Some(Element::ContainerStart {
kind: ContainerKind::Structure,
..
}) => out.push(read_command_data(r)?),
Some(Element::ContainerStart { .. }) => skip_container(r)?,
Some(_) => {}
}
}
}
fn read_command_data(r: &mut TlvReader<'_>) -> Result<InvokedCommand, crate::ImError> {
let mut path = None;
let mut fields_tlv = None;
let mut command_ref = None;
loop {
match r.next()? {
None | Some(Element::ContainerEnd) => break,
Some(Element::ContainerStart {
tag: Tag::Context(0),
kind: ContainerKind::List,
}) => {
path = Some(command_path_from_reader(r)?);
}
Some(Element::ContainerStart {
tag: Tag::Context(1),
kind: ContainerKind::Structure,
}) => {
fields_tlv = Some(retag_container_anonymous(r, ContainerKind::Structure)?);
}
Some(Element::Scalar {
tag: Tag::Context(2),
value: Value::Uint(n),
}) => {
command_ref = u16::try_from(n).ok();
}
Some(Element::ContainerStart { .. }) => skip_container(r)?,
Some(_) => {}
}
}
Ok(InvokedCommand {
path: path.ok_or(crate::ImError::MissingField("CommandDataIB.CommandPath"))?,
fields_tlv: fields_tlv
.ok_or(crate::ImError::MissingField("CommandDataIB.CommandFields"))?,
command_ref,
})
}
#[must_use]
#[allow(clippy::expect_used)] pub fn build_invoke_response_command(path: CommandPath, response_fields_tlv: &[u8]) -> Vec<u8> {
let mut buf = Vec::with_capacity(48 + response_fields_tlv.len());
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
w.put_bool(Tag::Context(0), false)
.expect("infallible: vec writer"); w.start_array(Tag::Context(1))
.expect("infallible: vec writer"); {
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer"); w.start_structure(Tag::Context(0))
.expect("infallible: vec writer"); write_command_path(&mut w, Tag::Context(0), path);
w.put_preencoded(Tag::Context(1), response_fields_tlv)
.expect("infallible: caller passes a valid anonymous-tagged struct");
w.end_container().expect("infallible: vec writer"); w.end_container().expect("infallible: vec writer"); }
w.end_container().expect("infallible: vec writer"); w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer"); buf
}
#[must_use]
#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn build_invoke_response_status(path: CommandPath, status: ImStatus) -> Vec<u8> {
let mut buf = Vec::with_capacity(64);
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
w.put_bool(Tag::Context(0), false)
.expect("infallible: vec writer"); w.start_array(Tag::Context(1))
.expect("infallible: vec writer"); {
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer"); w.start_structure(Tag::Context(1))
.expect("infallible: vec writer"); write_command_path(&mut w, Tag::Context(0), path);
w.start_structure(Tag::Context(1))
.expect("infallible: vec writer"); w.put_uint(Tag::Context(0), u64::from(status.to_u8()))
.expect("infallible: vec writer"); w.end_container().expect("infallible: vec writer"); w.end_container().expect("infallible: vec writer"); w.end_container().expect("infallible: vec writer"); }
w.end_container().expect("infallible: vec writer"); w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer"); buf
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)] use super::*;
use crate::invoke::{build_invoke_request, parse_invoke_response, InvokeResponse};
fn anon_struct_ctx0(value: u64) -> Vec<u8> {
let mut b = Vec::new();
let mut w = TlvWriter::new(&mut b);
w.start_structure(Tag::Anonymous).unwrap();
w.put_uint(Tag::Context(0), value).unwrap();
w.end_container().unwrap();
b
}
#[test]
fn parse_invoke_request_roundtrips_builder() {
let fields = anon_struct_ctx0(0xFFF1);
let path = CommandPath {
endpoint: 0,
cluster: 0x0029,
command: 0x00,
};
let msg = build_invoke_request(path, &fields);
let parsed = parse_invoke_request(&msg).expect("parse");
assert!(!parsed.suppress_response);
assert!(!parsed.timed);
assert_eq!(parsed.commands.len(), 1);
assert_eq!(parsed.commands[0].path, path);
assert_eq!(parsed.commands[0].fields_tlv, fields);
assert_eq!(parsed.commands[0].command_ref, None);
}
#[test]
fn build_invoke_response_command_roundtrips() {
let fields = anon_struct_ctx0(7);
let path = CommandPath {
endpoint: 0,
cluster: 0x0029,
command: 0x01,
};
let msg = build_invoke_response_command(path, &fields);
match parse_invoke_response(&msg).expect("parse") {
InvokeResponse::Command {
path: p,
fields_tlv,
} => {
assert_eq!(p, path);
assert_eq!(fields_tlv, fields);
}
InvokeResponse::Status(s) => panic!("expected Command, got Status({s:?})"),
}
}
#[test]
fn command_fields_preserve_device_integer_widths() {
let nonminimal_fields = [0x15u8, 0x25, 0x00, 0x2A, 0x00, 0x18];
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_bool(Tag::Context(0), false).unwrap(); w.put_bool(Tag::Context(1), false).unwrap(); w.start_array(Tag::Context(2)).unwrap(); w.start_structure(Tag::Anonymous).unwrap(); w.start_list(Tag::Context(0)).unwrap(); w.put_uint(Tag::Context(0), 0).unwrap();
w.put_uint(Tag::Context(1), 0x0029).unwrap();
w.put_uint(Tag::Context(2), 0x00).unwrap();
w.end_container().unwrap();
w.put_preencoded(Tag::Context(1), &nonminimal_fields)
.unwrap();
w.end_container().unwrap(); w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
let parsed = parse_invoke_request(&buf).expect("parse");
assert_eq!(parsed.commands.len(), 1);
assert_eq!(
parsed.commands[0].fields_tlv, nonminimal_fields,
"device widths must be preserved verbatim"
);
}
#[test]
fn build_invoke_response_status_roundtrips() {
let path = CommandPath {
endpoint: 0,
cluster: 0x0029,
command: 0x04,
};
let msg = build_invoke_response_status(path, ImStatus::Success);
assert!(matches!(
parse_invoke_response(&msg),
Ok(InvokeResponse::Status(ImStatus::Success))
));
}
}