1#![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#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub(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#[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#[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#[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)] fn 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"); w.put_bool(Tag::Context(1), timed)
87 .expect("infallible: vec writer"); w.start_array(Tag::Context(2))
89 .expect("infallible: vec writer"); {
91 w.start_structure(Tag::Anonymous)
92 .expect("infallible: vec writer"); 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"); }
98 w.end_container().expect("infallible: vec writer"); w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
100 .expect("infallible: vec writer");
101 w.end_container().expect("infallible: vec writer"); buf
103}
104
105#[must_use]
120#[allow(clippy::expect_used)] pub 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"); w.put_bool(Tag::Context(1), false)
129 .expect("infallible: vec writer"); w.start_array(Tag::Context(2))
131 .expect("infallible: vec writer"); for (i, (path, fields)) in commands.iter().enumerate() {
133 w.start_structure(Tag::Anonymous)
134 .expect("infallible: vec writer"); 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 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"); }
145 w.end_container().expect("infallible: vec writer"); w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
147 .expect("infallible: vec writer");
148 w.end_container().expect("infallible: vec writer"); buf
150}
151
152#[derive(Clone, Debug, PartialEq, Eq)]
154pub enum InvokeResponse {
155 Command {
159 path: CommandPath,
161 fields_tlv: Vec<u8>,
163 },
164 Status(ImStatus),
166}
167
168#[derive(Clone, Debug, PartialEq, Eq)]
171pub struct InvokeResponseEntry {
172 pub command_ref: Option<u16>,
174 pub response: InvokeResponse,
176}
177
178pub(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)] w.write_value(Tag::Anonymous, value)
190 .expect("infallible: vec writer");
191 buf
192}
193
194pub(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
229pub 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
289pub 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 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), 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
329fn 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, 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 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
370fn 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
378fn 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 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 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
427fn parse_command_status(r: &mut TlvReader<'_>) -> Result<ImStatus, ImError> {
431 let (status, _ref) = parse_command_status_ref(r)?;
432 Ok(status)
433}
434
435fn parse_command_status_ref(r: &mut TlvReader<'_>) -> Result<(ImStatus, Option<u16>), ImError> {
437 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 for (tag, v) in &members {
454 if let (Tag::Context(0), Value::Uint(n)) = (tag, v) {
455 status = Some(*n);
456 }
457 }
458 }
459 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 #![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 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 assert!(matches!(
498 r.next().unwrap(),
499 Some(Element::ContainerStart {
500 tag: Tag::Anonymous,
501 kind: ContainerKind::Structure
502 })
503 ));
504 assert!(matches!(
506 r.next().unwrap(),
507 Some(Element::Scalar {
508 tag: Tag::Context(0),
509 value: Value::Bool(false)
510 })
511 ));
512 assert!(matches!(
514 r.next().unwrap(),
515 Some(Element::Scalar {
516 tag: Tag::Context(1),
517 value: Value::Bool(false)
518 })
519 ));
520 assert!(matches!(
522 r.next().unwrap(),
523 Some(Element::ContainerStart {
524 tag: Tag::Context(2),
525 kind: ContainerKind::Array
526 })
527 ));
528 assert!(matches!(
530 r.next().unwrap(),
531 Some(Element::ContainerStart {
532 tag: Tag::Anonymous,
533 kind: ContainerKind::Structure
534 })
535 ));
536 assert!(matches!(
538 r.next().unwrap(),
539 Some(Element::ContainerStart {
540 tag: Tag::Context(0),
541 kind: ContainerKind::List
542 })
543 ));
544 assert!(matches!(
546 r.next().unwrap(),
547 Some(Element::Scalar {
548 tag: Tag::Context(0),
549 value: Value::Uint(0)
550 })
551 ));
552 assert!(matches!(
554 r.next().unwrap(),
555 Some(Element::Scalar {
556 tag: Tag::Context(1),
557 value: Value::Uint(0x0030)
558 })
559 ));
560 assert!(matches!(
562 r.next().unwrap(),
563 Some(Element::Scalar {
564 tag: Tag::Context(2),
565 value: Value::Uint(0)
566 })
567 ));
568 assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
570 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 assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
581 assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
583 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 assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
591 assert!(r.next().unwrap().is_none());
593 }
594
595 #[test]
596 fn invoke_request_carries_command_path_and_fields() {
597 let fields = vec![0x15u8, 0x18]; let bytes = build_invoke_request(
603 CommandPath {
604 endpoint: 1,
605 cluster: 0x0031,
606 command: 0x06,
607 },
608 &fields,
609 );
610 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(); w.start_array(Tag::Context(1)).unwrap(); {
627 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();
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(); w.end_container().unwrap();
636 w.end_container().unwrap(); w.end_container().unwrap(); }
639 w.end_container().unwrap(); 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]); }
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 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 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(); w.start_array(Tag::Context(1)).unwrap(); {
676 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();
685 w.put_uint(Tag::Context(0), 0x2A).unwrap();
686 w.end_container().unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); }
690 w.end_container().unwrap(); 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(); 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();
725 w.put_uint(Tag::Context(2), 0x00).unwrap();
726 w.end_container().unwrap();
727 w.start_structure(Tag::Context(1)).unwrap(); w.end_container().unwrap();
729 w.end_container().unwrap(); w.end_container().unwrap(); }
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(); 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(); w.start_structure(Tag::Context(1)).unwrap(); w.start_list(Tag::Context(0)).unwrap(); 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(); w.put_uint(Tag::Context(0), 0x01).unwrap(); w.end_container().unwrap();
783 w.end_container().unwrap(); w.end_container().unwrap(); }
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 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(); w.start_structure(Tag::Context(1)).unwrap(); w.start_list(Tag::Context(0)).unwrap(); 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(); 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(); w.end_container().unwrap(); w.end_container().unwrap(); 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 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 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(); w.start_structure(Tag::Anonymous).unwrap(); w.put_uint(Tag::Context(7), 0).unwrap(); w.end_container().unwrap();
869 w.end_container().unwrap(); 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]; 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 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 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 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(); w.start_array(Tag::Context(1)).unwrap(); {
932 w.start_structure(Tag::Anonymous).unwrap();
934 w.start_structure(Tag::Context(0)).unwrap(); 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(); w.put_uint(Tag::Context(2), 0).unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); w.start_structure(Tag::Anonymous).unwrap();
947 w.start_structure(Tag::Context(1)).unwrap(); 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(); w.put_uint(Tag::Context(0), 0).unwrap(); w.end_container().unwrap();
956 w.put_uint(Tag::Context(2), 1).unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); }
960 w.end_container().unwrap(); 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 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}