#![forbid(unsafe_code)]
use crate::error::ImError;
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};
#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub(crate) fn write_command_path(w: &mut TlvWriter<'_>, tag: Tag, path: CommandPath) {
w.start_list(tag).expect("infallible: vec writer");
w.put_uint(Tag::Context(0), u64::from(path.endpoint))
.expect("infallible: vec writer");
w.put_uint(Tag::Context(1), u64::from(path.cluster))
.expect("infallible: vec writer");
w.put_uint(Tag::Context(2), u64::from(path.command))
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer");
}
#[must_use]
pub fn build_invoke_request(path: CommandPath, command_fields_tlv: &[u8]) -> Vec<u8> {
build_invoke_request_inner(path, command_fields_tlv, false, false)
}
#[must_use]
pub fn build_invoke_request_timed(path: CommandPath, command_fields_tlv: &[u8]) -> Vec<u8> {
build_invoke_request_inner(path, command_fields_tlv, true, false)
}
#[must_use]
pub fn build_invoke_request_group(path: CommandPath, command_fields_tlv: &[u8]) -> Vec<u8> {
build_invoke_request_inner(path, command_fields_tlv, false, true)
}
#[allow(clippy::expect_used)] fn build_invoke_request_inner(
path: CommandPath,
command_fields_tlv: &[u8],
timed: bool,
suppress_response: bool,
) -> Vec<u8> {
let mut buf = Vec::with_capacity(48 + command_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), suppress_response)
.expect("infallible: vec writer"); w.put_bool(Tag::Context(1), timed)
.expect("infallible: vec writer"); w.start_array(Tag::Context(2))
.expect("infallible: vec writer"); {
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer"); write_command_path(&mut w, Tag::Context(0), path);
w.put_preencoded(Tag::Context(1), command_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.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)] pub fn build_invoke_request_batch(commands: &[(CommandPath, &[u8])]) -> Vec<u8> {
let mut buf = Vec::with_capacity(32 + commands.iter().map(|c| 32 + c.1.len()).sum::<usize>());
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.put_bool(Tag::Context(1), false)
.expect("infallible: vec writer"); w.start_array(Tag::Context(2))
.expect("infallible: vec writer"); for (i, (path, fields)) in commands.iter().enumerate() {
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer"); write_command_path(&mut w, Tag::Context(0), *path);
w.put_preencoded(Tag::Context(1), fields)
.expect("infallible: caller passes a valid anonymous-tagged struct");
let cref = u16::try_from(i).unwrap_or(u16::MAX);
w.put_uint(Tag::Context(2), u64::from(cref))
.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
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InvokeResponse {
Command {
path: CommandPath,
fields_tlv: Vec<u8>,
},
Status(ImStatus),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InvokeResponseEntry {
pub command_ref: Option<u16>,
pub response: InvokeResponse,
}
#[allow(clippy::expect_used)] pub(crate) fn retag_container_anonymous(
r: &mut TlvReader<'_>,
kind: ContainerKind,
) -> Result<Vec<u8>, ImError> {
let span = r.skip_container_span().map_err(ImError::Codec)?;
let body = r.span_bytes(span.body());
let mut out = Vec::with_capacity(1 + body.len());
{
let mut w = TlvWriter::new(&mut out);
match kind {
ContainerKind::Structure => w.start_structure(Tag::Anonymous),
ContainerKind::Array => w.start_array(Tag::Anonymous),
_ => w.start_list(Tag::Anonymous),
}
.expect("infallible: vec writer");
}
out.extend_from_slice(body);
Ok(out)
}
#[allow(clippy::expect_used)] fn empty_anonymous_struct() -> Vec<u8> {
let mut out = Vec::with_capacity(2);
{
let mut w = TlvWriter::new(&mut out);
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer");
}
out
}
pub(crate) fn command_path_from_reader(r: &mut TlvReader<'_>) -> Result<CommandPath, ImError> {
let mut endpoint = None;
let mut cluster = None;
let mut command = None;
loop {
match r.next()? {
None => {
return Err(ImError::Codec(matter_codec::Error::UnclosedContainer));
}
Some(Element::ContainerEnd) => break,
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Uint(n),
}) => {
endpoint =
Some(u16::try_from(n).map_err(|_| {
ImError::UnexpectedValue("CommandPath.endpoint exceeds u16")
})?);
}
Some(Element::Scalar {
tag: Tag::Context(1),
value: Value::Uint(n),
}) => {
cluster =
Some(u32::try_from(n).map_err(|_| {
ImError::UnexpectedValue("CommandPath.cluster exceeds u32")
})?);
}
Some(Element::Scalar {
tag: Tag::Context(2),
value: Value::Uint(n),
}) => {
command =
Some(u32::try_from(n).map_err(|_| {
ImError::UnexpectedValue("CommandPath.command exceeds u32")
})?);
}
Some(Element::ContainerStart { .. }) => crate::skip_container(r)?,
Some(_) => {}
}
}
Ok(CommandPath {
endpoint: endpoint.ok_or(ImError::MissingField("CommandPath.endpoint"))?,
cluster: cluster.ok_or(ImError::MissingField("CommandPath.cluster"))?,
command: command.ok_or(ImError::MissingField("CommandPath.command"))?,
})
}
pub fn parse_invoke_response(bytes: &[u8]) -> Result<InvokeResponse, ImError> {
let mut r = TlvReader::new(bytes);
expect_message_struct(&mut r)?;
loop {
match r.next()? {
None | Some(Element::ContainerEnd) => {
return Err(ImError::MissingField("InvokeResponses"))
}
Some(Element::ContainerStart {
tag: Tag::Context(1),
kind: ContainerKind::Array,
}) => break,
Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
Some(_) => {}
}
}
match r.next()? {
Some(Element::ContainerStart {
kind: ContainerKind::Structure,
..
}) => {}
_ => return Err(ImError::MissingField("InvokeResponseIB")),
}
loop {
match r.next()? {
None | Some(Element::ContainerEnd) => return Err(ImError::EmptyInvokeResponse),
Some(Element::ContainerStart {
tag: Tag::Context(0),
kind: ContainerKind::Structure,
}) => {
return parse_command_data(&mut r).map(|(path, fields)| InvokeResponse::Command {
path,
fields_tlv: fields,
});
}
Some(Element::ContainerStart {
tag: Tag::Context(1),
kind: ContainerKind::Structure,
}) => {
return parse_command_status(&mut r).map(InvokeResponse::Status);
}
Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
Some(_) => {}
}
}
}
pub fn parse_invoke_response_batch(bytes: &[u8]) -> Result<Vec<InvokeResponseEntry>, ImError> {
let mut r = TlvReader::new(bytes);
expect_message_struct(&mut r)?;
loop {
match r.next()? {
None | Some(Element::ContainerEnd) => {
return Err(ImError::MissingField("InvokeResponses"))
}
Some(Element::ContainerStart {
tag: Tag::Context(1),
kind: ContainerKind::Array,
}) => break,
Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
Some(_) => {}
}
}
let mut out = Vec::new();
loop {
match r.next()? {
None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
Some(Element::ContainerEnd) => return Ok(out), Some(Element::ContainerStart {
kind: ContainerKind::Structure,
..
}) => out.push(parse_invoke_response_ib(&mut r)?),
Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
Some(_) => {}
}
}
}
fn parse_invoke_response_ib(r: &mut TlvReader<'_>) -> Result<InvokeResponseEntry, ImError> {
let mut entry: Option<InvokeResponseEntry> = None;
loop {
match r.next()? {
None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
Some(Element::ContainerEnd) => break, Some(Element::ContainerStart {
tag: Tag::Context(0),
kind: ContainerKind::Structure,
}) => {
let (path, fields, command_ref) = parse_command_data_ref(r)?;
entry = Some(InvokeResponseEntry {
command_ref,
response: InvokeResponse::Command {
path,
fields_tlv: fields,
},
});
}
Some(Element::ContainerStart {
tag: Tag::Context(1),
kind: ContainerKind::Structure,
}) => {
let (status, command_ref) = parse_command_status_ref(r)?;
entry = Some(InvokeResponseEntry {
command_ref,
response: InvokeResponse::Status(status),
});
}
Some(Element::ContainerStart { .. }) => skip_container(r)?,
Some(_) => {}
}
}
entry.ok_or(ImError::EmptyInvokeResponse)
}
fn parse_command_data(r: &mut TlvReader<'_>) -> Result<(CommandPath, Vec<u8>), ImError> {
let (path, fields, _ref) = parse_command_data_ref(r)?;
Ok((path, fields))
}
fn parse_command_data_ref(
r: &mut TlvReader<'_>,
) -> Result<(CommandPath, Vec<u8>, Option<u16>), ImError> {
let mut path = None;
let mut fields = Vec::new();
let mut command_ref = None;
loop {
match r.next()? {
None => return Err(ImError::MissingField("CommandDataIB.body")),
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,
}) => {
fields = retag_container_anonymous(r, kind)?;
}
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(_) => {}
}
}
let fields = if fields.is_empty() {
empty_anonymous_struct()
} else {
fields
};
Ok((
path.ok_or(ImError::MissingField("CommandDataIB.CommandPath"))?,
fields,
command_ref,
))
}
fn parse_command_status(r: &mut TlvReader<'_>) -> Result<ImStatus, ImError> {
let (status, _ref) = parse_command_status_ref(r)?;
Ok(status)
}
fn parse_command_status_ref(r: &mut TlvReader<'_>) -> Result<(ImStatus, Option<u16>), ImError> {
let mut status: Option<u64> = None;
let mut command_ref = None;
loop {
match r.next()? {
None => return Err(ImError::MissingField("CommandStatusIB.body")),
Some(Element::ContainerEnd) => break,
Some(Element::ContainerStart {
tag: Tag::Context(1),
kind: ContainerKind::Structure,
}) => {
loop {
match r.next()? {
None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
Some(Element::ContainerEnd) => break,
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Uint(n),
}) => status = Some(n),
Some(Element::ContainerStart { .. }) => skip_container(r)?,
Some(_) => {}
}
}
}
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(_) => {}
}
}
let raw = status.ok_or(ImError::MissingField("StatusIB.Status"))?;
let code = u8::try_from(raw).map_err(|_| ImError::InvalidStatusCode { code: raw })?;
Ok((ImStatus::from_u8(code), command_ref))
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
#[test]
fn invoke_request_has_expected_structure() {
let fields = vec![0x15, 0x18];
let bytes = build_invoke_request(
CommandPath {
endpoint: 0,
cluster: 0x0030,
command: 0x00,
},
&fields,
);
let mut r = TlvReader::new(&bytes);
assert!(matches!(
r.next().unwrap(),
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Bool(false)
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::Scalar {
tag: Tag::Context(1),
value: Value::Bool(false)
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::ContainerStart {
tag: Tag::Context(2),
kind: ContainerKind::Array
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::ContainerStart {
tag: Tag::Context(0),
kind: ContainerKind::List
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Uint(0)
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::Scalar {
tag: Tag::Context(1),
value: Value::Uint(0x0030)
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::Scalar {
tag: Tag::Context(2),
value: Value::Uint(0)
})
));
assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
assert!(matches!(
r.next().unwrap(),
Some(Element::ContainerStart {
tag: Tag::Context(1),
kind: ContainerKind::Structure
})
));
assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
assert!(matches!(
r.next().unwrap(),
Some(Element::Scalar { tag: Tag::Context(0xFF), value: Value::Uint(v) })
if v == u64::from(IM_REVISION)
));
assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
assert!(r.next().unwrap().is_none());
}
#[test]
fn invoke_request_carries_command_path_and_fields() {
let fields = vec![0x15u8, 0x18]; let bytes = build_invoke_request(
CommandPath {
endpoint: 1,
cluster: 0x0031,
command: 0x06,
},
&fields,
);
let retagged = [0x35u8, 0x01, 0x18];
assert!(
bytes.windows(retagged.len()).any(|w| w == retagged),
"command fields not embedded (expected retagged bytes {retagged:02X?} in {bytes:02X?})",
);
}
#[test]
fn parses_command_response_payload() {
use matter_codec::{Tag, TlvWriter};
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.start_array(Tag::Context(1)).unwrap(); {
w.start_structure(Tag::Anonymous).unwrap(); w.start_structure(Tag::Context(0)).unwrap(); w.start_list(Tag::Context(0)).unwrap(); w.put_uint(Tag::Context(0), 0).unwrap();
w.put_uint(Tag::Context(1), 0x0030).unwrap();
w.put_uint(Tag::Context(2), 0x05).unwrap();
w.end_container().unwrap();
w.start_structure(Tag::Context(1)).unwrap(); w.end_container().unwrap();
w.end_container().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_response(&buf).unwrap();
match parsed {
InvokeResponse::Command { path, fields_tlv } => {
assert_eq!(path.endpoint, 0);
assert_eq!(path.cluster, 0x0030);
assert_eq!(path.command, 0x05);
assert_eq!(fields_tlv, vec![0x15, 0x18]); }
InvokeResponse::Status(_) => panic!("expected Command, got Status"),
}
}
#[test]
fn parses_command_with_nonempty_fields() {
use matter_codec::{Tag, TlvWriter};
let mut expected_buf = Vec::new();
{
let mut w = TlvWriter::new(&mut expected_buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_uint(Tag::Context(0), 0x2A).unwrap();
w.end_container().unwrap();
}
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.start_array(Tag::Context(1)).unwrap(); {
w.start_structure(Tag::Anonymous).unwrap(); w.start_structure(Tag::Context(0)).unwrap(); w.start_list(Tag::Context(0)).unwrap(); w.put_uint(Tag::Context(0), 1).unwrap(); w.put_uint(Tag::Context(1), 0x0050).unwrap(); w.put_uint(Tag::Context(2), 0x01).unwrap(); w.end_container().unwrap(); w.start_structure(Tag::Context(1)).unwrap();
w.put_uint(Tag::Context(0), 0x2A).unwrap();
w.end_container().unwrap(); w.end_container().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_response(&buf).unwrap();
match parsed {
InvokeResponse::Command { path, fields_tlv } => {
assert_eq!(path.endpoint, 1);
assert_eq!(path.cluster, 0x0050);
assert_eq!(path.command, 0x01);
assert_eq!(
fields_tlv, expected_buf,
"fields_tlv should decode to the same struct content as the original"
);
}
InvokeResponse::Status(_) => panic!("expected Command, got Status"),
}
}
#[test]
fn rejects_out_of_range_endpoint() {
use crate::error::ImError;
use matter_codec::{Tag, TlvWriter};
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.start_array(Tag::Context(1)).unwrap();
{
w.start_structure(Tag::Anonymous).unwrap(); w.start_structure(Tag::Context(0)).unwrap(); w.start_list(Tag::Context(0)).unwrap(); w.put_uint(Tag::Context(0), 0x0001_0000).unwrap(); w.put_uint(Tag::Context(1), 0x0030).unwrap();
w.put_uint(Tag::Context(2), 0x00).unwrap();
w.end_container().unwrap();
w.start_structure(Tag::Context(1)).unwrap(); w.end_container().unwrap();
w.end_container().unwrap(); w.end_container().unwrap(); }
w.end_container().unwrap();
w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
let result = parse_invoke_response(&buf);
assert!(
matches!(result, Err(ImError::UnexpectedValue(_))),
"expected UnexpectedValue for out-of-range endpoint, got {result:?}"
);
}
#[test]
fn empty_invoke_responses_array_errors() {
use crate::error::ImError;
use matter_codec::{Tag, TlvWriter};
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.start_array(Tag::Context(1)).unwrap(); w.end_container().unwrap();
w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
let result = parse_invoke_response(&buf);
assert!(
matches!(result, Err(ImError::MissingField(_))),
"expected MissingField for empty InvokeResponses, got {result:?}"
);
}
#[test]
fn parses_status_response() {
use matter_codec::{Tag, TlvWriter};
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.start_array(Tag::Context(1)).unwrap();
{
w.start_structure(Tag::Anonymous).unwrap(); w.start_structure(Tag::Context(1)).unwrap(); w.start_list(Tag::Context(0)).unwrap(); w.put_uint(Tag::Context(0), 0).unwrap();
w.put_uint(Tag::Context(1), 0x0030).unwrap();
w.put_uint(Tag::Context(2), 0x00).unwrap();
w.end_container().unwrap();
w.start_structure(Tag::Context(1)).unwrap(); w.put_uint(Tag::Context(0), 0x01).unwrap(); w.end_container().unwrap();
w.end_container().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_response(&buf).unwrap();
assert!(matches!(
parsed,
InvokeResponse::Status(ImStatus::Failure(0x01))
));
}
fn invoke_status_response(status: Option<u64>) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
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.start_array(Tag::Context(1)).unwrap();
w.start_structure(Tag::Anonymous).unwrap(); w.start_structure(Tag::Context(1)).unwrap(); w.start_list(Tag::Context(0)).unwrap(); w.put_uint(Tag::Context(0), 0).unwrap();
w.put_uint(Tag::Context(1), 0x0030).unwrap();
w.put_uint(Tag::Context(2), 0x00).unwrap();
w.end_container().unwrap();
w.start_structure(Tag::Context(1)).unwrap(); if let Some(v) = status {
w.put_uint(Tag::Context(0), v).unwrap();
}
w.end_container().unwrap();
w.end_container().unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
buf
}
#[test]
fn command_status_out_of_range_is_invalid_status_code() {
let buf = invoke_status_response(Some(0x100));
match parse_invoke_response(&buf) {
Err(ImError::InvalidStatusCode { code }) => assert_eq!(code, 0x100),
other => panic!("expected InvalidStatusCode {{ code: 0x100 }}, got {other:?}"),
}
}
#[test]
fn command_status_valid_code_still_parses() {
let buf = invoke_status_response(Some(0x88));
assert!(matches!(
parse_invoke_response(&buf),
Ok(InvokeResponse::Status(ImStatus::Failure(0x88)))
));
}
#[test]
fn command_status_missing_field_still_missing_field() {
let buf = invoke_status_response(None);
assert!(matches!(
parse_invoke_response(&buf),
Err(ImError::MissingField("StatusIB.Status"))
));
}
#[test]
fn invoke_response_ib_with_no_command_or_status_errors() {
use matter_codec::{Tag, TlvWriter};
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.start_array(Tag::Context(1)).unwrap(); w.start_structure(Tag::Anonymous).unwrap(); w.put_uint(Tag::Context(7), 0).unwrap(); w.end_container().unwrap();
w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
assert!(matches!(
parse_invoke_response(&buf),
Err(ImError::EmptyInvokeResponse)
));
}
#[test]
fn batch_request_carries_command_refs() {
let fields = vec![0x15u8, 0x18]; let bytes = build_invoke_request_batch(&[
(
CommandPath {
endpoint: 1,
cluster: 0x06,
command: 0x02,
},
&fields,
),
(
CommandPath {
endpoint: 2,
cluster: 0x06,
command: 0x00,
},
&fields,
),
]);
let mut r = TlvReader::new(&bytes);
let mut refs = Vec::new();
let mut depth = 0i32;
while let Some(el) = r.next().unwrap() {
match el {
Element::ContainerStart { .. } => depth += 1,
Element::ContainerEnd => depth -= 1,
Element::Scalar {
tag: Tag::Context(2),
value: Value::Uint(n),
} if depth == 3 => refs.push(n),
_ => {}
}
}
assert_eq!(refs, vec![0, 1], "CommandRefs must be 0 then 1");
}
#[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.start_array(Tag::Context(1)).unwrap();
w.start_structure(Tag::Anonymous).unwrap(); w.start_structure(Tag::Context(0)).unwrap(); w.start_list(Tag::Context(0)).unwrap();
w.put_uint(Tag::Context(0), 0).unwrap();
w.put_uint(Tag::Context(1), 0x0030).unwrap();
w.put_uint(Tag::Context(2), 0x05).unwrap();
w.end_container().unwrap();
w.put_preencoded(Tag::Context(1), &nonminimal_fields)
.unwrap();
w.end_container().unwrap();
w.end_container().unwrap();
w.end_container().unwrap();
w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
match parse_invoke_response(&buf).unwrap() {
InvokeResponse::Command { fields_tlv, .. } => {
assert_eq!(
fields_tlv, nonminimal_fields,
"device widths must be preserved verbatim"
);
}
InvokeResponse::Status(_) => panic!("expected Command"),
}
}
#[test]
fn batch_response_parses_all_ibs_with_refs() {
use matter_codec::{Tag, TlvWriter};
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.start_array(Tag::Context(1)).unwrap(); {
w.start_structure(Tag::Anonymous).unwrap();
w.start_structure(Tag::Context(0)).unwrap(); w.start_list(Tag::Context(0)).unwrap();
w.put_uint(Tag::Context(0), 1).unwrap();
w.put_uint(Tag::Context(1), 0x06).unwrap();
w.put_uint(Tag::Context(2), 0x02).unwrap();
w.end_container().unwrap();
w.start_structure(Tag::Context(1)).unwrap();
w.end_container().unwrap(); w.put_uint(Tag::Context(2), 0).unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); w.start_structure(Tag::Anonymous).unwrap();
w.start_structure(Tag::Context(1)).unwrap(); w.start_list(Tag::Context(0)).unwrap();
w.put_uint(Tag::Context(0), 2).unwrap();
w.put_uint(Tag::Context(1), 0x06).unwrap();
w.put_uint(Tag::Context(2), 0x00).unwrap();
w.end_container().unwrap();
w.start_structure(Tag::Context(1)).unwrap(); w.put_uint(Tag::Context(0), 0).unwrap(); w.end_container().unwrap();
w.put_uint(Tag::Context(2), 1).unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); }
w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
let entries = parse_invoke_response_batch(&buf).unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].command_ref, Some(0));
assert!(matches!(
entries[0].response,
InvokeResponse::Command { ref path, .. } if path.endpoint == 1 && path.command == 0x02
));
assert_eq!(entries[1].command_ref, Some(1));
assert_eq!(
entries[1].response,
InvokeResponse::Status(ImStatus::Success)
);
match parse_invoke_response(&buf).unwrap() {
InvokeResponse::Command { path, .. } => assert_eq!(path.endpoint, 1),
InvokeResponse::Status(_) => panic!("expected the first IB (a Command)"),
}
}
fn parse_cmd_path(build: impl FnOnce(&mut TlvWriter<'_>)) -> Result<CommandPath, ImError> {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_list(Tag::Anonymous).unwrap();
build(&mut w);
w.end_container().unwrap();
let mut r = TlvReader::new(&buf);
assert!(matches!(
r.next().unwrap(),
Some(Element::ContainerStart { .. })
));
command_path_from_reader(&mut r)
}
#[test]
fn command_path_parses_members_and_errors() {
let p = parse_cmd_path(|w| {
w.put_uint(Tag::Context(0), 1).unwrap();
w.put_uint(Tag::Context(1), 6).unwrap();
w.put_uint(Tag::Context(2), 2).unwrap();
})
.unwrap();
assert_eq!((p.endpoint, p.cluster, p.command), (1, 6, 2));
assert!(matches!(
parse_cmd_path(|w| {
w.put_uint(Tag::Context(0), 1).unwrap();
w.put_uint(Tag::Context(1), 6).unwrap();
}),
Err(ImError::MissingField("CommandPath.command"))
));
assert!(matches!(
parse_cmd_path(|w| {
w.put_uint(Tag::Context(0), u64::from(u16::MAX) + 1)
.unwrap();
w.put_uint(Tag::Context(1), 6).unwrap();
w.put_uint(Tag::Context(2), 2).unwrap();
}),
Err(ImError::UnexpectedValue(_))
));
}
#[test]
fn empty_command_fields_fallback_to_anonymous_empty_struct() {
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.start_array(Tag::Context(1)).unwrap(); {
w.start_structure(Tag::Anonymous).unwrap(); w.start_structure(Tag::Context(0)).unwrap(); w.start_list(Tag::Context(0)).unwrap(); w.put_uint(Tag::Context(0), 0).unwrap();
w.put_uint(Tag::Context(1), 0x0030).unwrap();
w.put_uint(Tag::Context(2), 0x05).unwrap();
w.end_container().unwrap();
w.end_container().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_response(&buf).unwrap();
match parsed {
InvokeResponse::Command { fields_tlv, .. } => {
assert_eq!(fields_tlv, vec![0x15, 0x18]);
}
InvokeResponse::Status(_) => panic!("expected Command, got Status"),
}
}
}