1#![forbid(unsafe_code)]
4
5use crate::error::ImError;
6use crate::path::CommandPath;
7use crate::status::ImStatus;
8use crate::{expect_message_struct, skip_container, IM_REVISION};
9use matter_codec::{ContainerKind, Element, Tag, TlvReader, TlvWriter, Value};
10
11#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub(crate) fn write_command_path(w: &mut TlvWriter<'_>, tag: Tag, path: CommandPath) {
15 w.start_list(tag).expect("infallible: vec writer");
16 w.put_uint(Tag::Context(0), u64::from(path.endpoint))
17 .expect("infallible: vec writer");
18 w.put_uint(Tag::Context(1), u64::from(path.cluster))
19 .expect("infallible: vec writer");
20 w.put_uint(Tag::Context(2), u64::from(path.command))
21 .expect("infallible: vec writer");
22 w.end_container().expect("infallible: vec writer");
23}
24
25#[must_use]
38pub fn build_invoke_request(path: CommandPath, command_fields_tlv: &[u8]) -> Vec<u8> {
39 build_invoke_request_inner(path, command_fields_tlv, false, false)
40}
41
42#[must_use]
50pub fn build_invoke_request_timed(path: CommandPath, command_fields_tlv: &[u8]) -> Vec<u8> {
51 build_invoke_request_inner(path, command_fields_tlv, true, false)
52}
53
54#[must_use]
66pub fn build_invoke_request_group(path: CommandPath, command_fields_tlv: &[u8]) -> Vec<u8> {
67 build_invoke_request_inner(path, command_fields_tlv, false, true)
68}
69
70#[allow(clippy::expect_used)] fn build_invoke_request_inner(
72 path: CommandPath,
73 command_fields_tlv: &[u8],
74 timed: bool,
75 suppress_response: bool,
76) -> Vec<u8> {
77 let mut buf = Vec::with_capacity(48 + command_fields_tlv.len());
78 let mut w = TlvWriter::new(&mut buf);
79 w.start_structure(Tag::Anonymous)
80 .expect("infallible: vec writer");
81 w.put_bool(Tag::Context(0), suppress_response)
82 .expect("infallible: vec writer"); w.put_bool(Tag::Context(1), timed)
84 .expect("infallible: vec writer"); w.start_array(Tag::Context(2))
86 .expect("infallible: vec writer"); {
88 w.start_structure(Tag::Anonymous)
89 .expect("infallible: vec writer"); write_command_path(&mut w, Tag::Context(0), path);
91 w.put_preencoded(Tag::Context(1), command_fields_tlv)
92 .expect("infallible: caller passes a valid anonymous-tagged struct");
93 w.end_container().expect("infallible: vec writer"); }
95 w.end_container().expect("infallible: vec writer"); w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
97 .expect("infallible: vec writer");
98 w.end_container().expect("infallible: vec writer"); buf
100}
101
102#[must_use]
117#[allow(clippy::expect_used)] pub fn build_invoke_request_batch(commands: &[(CommandPath, &[u8])]) -> Vec<u8> {
119 let mut buf = Vec::with_capacity(32 + commands.iter().map(|c| 32 + c.1.len()).sum::<usize>());
120 let mut w = TlvWriter::new(&mut buf);
121 w.start_structure(Tag::Anonymous)
122 .expect("infallible: vec writer");
123 w.put_bool(Tag::Context(0), false)
124 .expect("infallible: vec writer"); w.put_bool(Tag::Context(1), false)
126 .expect("infallible: vec writer"); w.start_array(Tag::Context(2))
128 .expect("infallible: vec writer"); for (i, (path, fields)) in commands.iter().enumerate() {
130 w.start_structure(Tag::Anonymous)
131 .expect("infallible: vec writer"); write_command_path(&mut w, Tag::Context(0), *path);
133 w.put_preencoded(Tag::Context(1), fields)
134 .expect("infallible: caller passes a valid anonymous-tagged struct");
135 let cref = u16::try_from(i).unwrap_or(u16::MAX);
138 w.put_uint(Tag::Context(2), u64::from(cref))
139 .expect("infallible: vec writer");
140 w.end_container().expect("infallible: vec writer"); }
142 w.end_container().expect("infallible: vec writer"); w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
144 .expect("infallible: vec writer");
145 w.end_container().expect("infallible: vec writer"); buf
147}
148
149#[derive(Clone, Debug, PartialEq, Eq)]
151pub enum InvokeResponse {
152 Command {
156 path: CommandPath,
158 fields_tlv: Vec<u8>,
176 },
177 Status(ImStatus),
179}
180
181#[derive(Clone, Debug, PartialEq, Eq)]
184pub struct InvokeResponseEntry {
185 pub command_ref: Option<u16>,
187 pub response: InvokeResponse,
189}
190
191#[allow(clippy::expect_used)] pub(crate) fn retag_container_anonymous(
210 r: &mut TlvReader<'_>,
211 kind: ContainerKind,
212) -> Result<Vec<u8>, ImError> {
213 let span = r.skip_container_span().map_err(ImError::Codec)?;
214 let body = r.span_bytes(span.body());
215 let mut out = Vec::with_capacity(1 + body.len());
216 {
217 let mut w = TlvWriter::new(&mut out);
218 match kind {
219 ContainerKind::Structure => w.start_structure(Tag::Anonymous),
220 ContainerKind::Array => w.start_array(Tag::Anonymous),
221 _ => w.start_list(Tag::Anonymous),
224 }
225 .expect("infallible: vec writer");
226 }
227 out.extend_from_slice(body);
228 Ok(out)
229}
230
231#[allow(clippy::expect_used)] fn empty_anonymous_struct() -> Vec<u8> {
236 let mut out = Vec::with_capacity(2);
237 {
238 let mut w = TlvWriter::new(&mut out);
239 w.start_structure(Tag::Anonymous)
240 .expect("infallible: vec writer");
241 w.end_container().expect("infallible: vec writer");
242 }
243 out
244}
245
246pub(crate) fn command_path_from_reader(r: &mut TlvReader<'_>) -> Result<CommandPath, ImError> {
250 let mut endpoint = None;
251 let mut cluster = None;
252 let mut command = None;
253 loop {
254 match r.next()? {
255 None => {
256 return Err(ImError::Codec(matter_codec::Error::UnclosedContainer));
257 }
258 Some(Element::ContainerEnd) => break,
259 Some(Element::Scalar {
260 tag: Tag::Context(0),
261 value: Value::Uint(n),
262 }) => {
263 endpoint =
264 Some(u16::try_from(n).map_err(|_| {
265 ImError::UnexpectedValue("CommandPath.endpoint exceeds u16")
266 })?);
267 }
268 Some(Element::Scalar {
269 tag: Tag::Context(1),
270 value: Value::Uint(n),
271 }) => {
272 cluster =
273 Some(u32::try_from(n).map_err(|_| {
274 ImError::UnexpectedValue("CommandPath.cluster exceeds u32")
275 })?);
276 }
277 Some(Element::Scalar {
278 tag: Tag::Context(2),
279 value: Value::Uint(n),
280 }) => {
281 command =
282 Some(u32::try_from(n).map_err(|_| {
283 ImError::UnexpectedValue("CommandPath.command exceeds u32")
284 })?);
285 }
286 Some(Element::ContainerStart { .. }) => crate::skip_container(r)?,
287 Some(_) => {}
288 }
289 }
290 Ok(CommandPath {
291 endpoint: endpoint.ok_or(ImError::MissingField("CommandPath.endpoint"))?,
292 cluster: cluster.ok_or(ImError::MissingField("CommandPath.cluster"))?,
293 command: command.ok_or(ImError::MissingField("CommandPath.command"))?,
294 })
295}
296
297pub fn parse_invoke_response(bytes: &[u8]) -> Result<InvokeResponse, ImError> {
308 let mut r = TlvReader::new(bytes);
309 expect_message_struct(&mut r)?;
310
311 loop {
312 match r.next()? {
313 None | Some(Element::ContainerEnd) => {
314 return Err(ImError::MissingField("InvokeResponses"))
315 }
316 Some(Element::ContainerStart {
317 tag: Tag::Context(1),
318 kind: ContainerKind::Array,
319 }) => break,
320 Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
321 Some(_) => {}
322 }
323 }
324
325 match r.next()? {
326 Some(Element::ContainerStart {
327 kind: ContainerKind::Structure,
328 ..
329 }) => {}
330 _ => return Err(ImError::MissingField("InvokeResponseIB")),
331 }
332
333 loop {
334 match r.next()? {
335 None | Some(Element::ContainerEnd) => return Err(ImError::EmptyInvokeResponse),
336 Some(Element::ContainerStart {
337 tag: Tag::Context(0),
338 kind: ContainerKind::Structure,
339 }) => {
340 return parse_command_data(&mut r).map(|(path, fields)| InvokeResponse::Command {
341 path,
342 fields_tlv: fields,
343 });
344 }
345 Some(Element::ContainerStart {
346 tag: Tag::Context(1),
347 kind: ContainerKind::Structure,
348 }) => {
349 return parse_command_status(&mut r).map(InvokeResponse::Status);
350 }
351 Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
352 Some(_) => {}
353 }
354 }
355}
356
357pub fn parse_invoke_response_batch(bytes: &[u8]) -> Result<Vec<InvokeResponseEntry>, ImError> {
366 let mut r = TlvReader::new(bytes);
367 expect_message_struct(&mut r)?;
368 loop {
370 match r.next()? {
371 None | Some(Element::ContainerEnd) => {
372 return Err(ImError::MissingField("InvokeResponses"))
373 }
374 Some(Element::ContainerStart {
375 tag: Tag::Context(1),
376 kind: ContainerKind::Array,
377 }) => break,
378 Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
379 Some(_) => {}
380 }
381 }
382 let mut out = Vec::new();
383 loop {
384 match r.next()? {
385 None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
386 Some(Element::ContainerEnd) => return Ok(out), Some(Element::ContainerStart {
388 kind: ContainerKind::Structure,
389 ..
390 }) => out.push(parse_invoke_response_ib(&mut r)?),
391 Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
392 Some(_) => {}
393 }
394 }
395}
396
397fn parse_invoke_response_ib(r: &mut TlvReader<'_>) -> Result<InvokeResponseEntry, ImError> {
401 let mut entry: Option<InvokeResponseEntry> = None;
402 loop {
403 match r.next()? {
404 None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
405 Some(Element::ContainerEnd) => break, Some(Element::ContainerStart {
408 tag: Tag::Context(0),
409 kind: ContainerKind::Structure,
410 }) => {
411 let (path, fields, command_ref) = parse_command_data_ref(r)?;
412 entry = Some(InvokeResponseEntry {
413 command_ref,
414 response: InvokeResponse::Command {
415 path,
416 fields_tlv: fields,
417 },
418 });
419 }
420 Some(Element::ContainerStart {
422 tag: Tag::Context(1),
423 kind: ContainerKind::Structure,
424 }) => {
425 let (status, command_ref) = parse_command_status_ref(r)?;
426 entry = Some(InvokeResponseEntry {
427 command_ref,
428 response: InvokeResponse::Status(status),
429 });
430 }
431 Some(Element::ContainerStart { .. }) => skip_container(r)?,
432 Some(_) => {}
433 }
434 }
435 entry.ok_or(ImError::EmptyInvokeResponse)
436}
437
438fn parse_command_data(r: &mut TlvReader<'_>) -> Result<(CommandPath, Vec<u8>), ImError> {
442 let (path, fields, _ref) = parse_command_data_ref(r)?;
443 Ok((path, fields))
444}
445
446fn parse_command_data_ref(
448 r: &mut TlvReader<'_>,
449) -> Result<(CommandPath, Vec<u8>, Option<u16>), ImError> {
450 let mut path = None;
451 let mut fields = Vec::new();
452 let mut command_ref = None;
453 loop {
454 match r.next()? {
455 None => return Err(ImError::MissingField("CommandDataIB.body")),
456 Some(Element::ContainerEnd) => break,
457 Some(Element::ContainerStart {
458 tag: Tag::Context(0),
459 kind: ContainerKind::List,
460 }) => {
461 path = Some(command_path_from_reader(r)?);
462 }
463 Some(Element::ContainerStart {
464 tag: Tag::Context(1),
465 kind,
466 }) => {
467 fields = retag_container_anonymous(r, kind)?;
468 }
469 Some(Element::Scalar {
471 tag: Tag::Context(2),
472 value: Value::Uint(n),
473 }) => command_ref = u16::try_from(n).ok(),
474 Some(Element::ContainerStart { .. }) => skip_container(r)?,
475 Some(_) => {}
476 }
477 }
478 let fields = if fields.is_empty() {
482 empty_anonymous_struct()
483 } else {
484 fields
485 };
486 Ok((
487 path.ok_or(ImError::MissingField("CommandDataIB.CommandPath"))?,
488 fields,
489 command_ref,
490 ))
491}
492
493fn parse_command_status(r: &mut TlvReader<'_>) -> Result<ImStatus, ImError> {
497 let (status, _ref) = parse_command_status_ref(r)?;
498 Ok(status)
499}
500
501fn parse_command_status_ref(r: &mut TlvReader<'_>) -> Result<(ImStatus, Option<u16>), ImError> {
503 let mut status: Option<u64> = None;
508 let mut command_ref = None;
509 loop {
510 match r.next()? {
511 None => return Err(ImError::MissingField("CommandStatusIB.body")),
512 Some(Element::ContainerEnd) => break,
513 Some(Element::ContainerStart {
514 tag: Tag::Context(1),
515 kind: ContainerKind::Structure,
516 }) => {
517 loop {
520 match r.next()? {
521 None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
522 Some(Element::ContainerEnd) => break,
523 Some(Element::Scalar {
524 tag: Tag::Context(0),
525 value: Value::Uint(n),
526 }) => status = Some(n),
527 Some(Element::ContainerStart { .. }) => skip_container(r)?,
528 Some(_) => {}
529 }
530 }
531 }
532 Some(Element::Scalar {
534 tag: Tag::Context(2),
535 value: Value::Uint(n),
536 }) => command_ref = u16::try_from(n).ok(),
537 Some(Element::ContainerStart { .. }) => skip_container(r)?,
538 Some(_) => {}
539 }
540 }
541 let raw = status.ok_or(ImError::MissingField("StatusIB.Status"))?;
542 let code = u8::try_from(raw).map_err(|_| ImError::InvalidStatusCode { code: raw })?;
543 Ok((ImStatus::from_u8(code), command_ref))
544}
545
546#[cfg(test)]
547mod tests {
548 #![allow(clippy::unwrap_used, clippy::expect_used)]
550
551 use super::*;
552 use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
553
554 #[test]
555 fn invoke_request_has_expected_structure() {
556 let fields = vec![0x15, 0x18];
559 let bytes = build_invoke_request(
560 CommandPath {
561 endpoint: 0,
562 cluster: 0x0030,
563 command: 0x00,
564 },
565 &fields,
566 );
567
568 let mut r = TlvReader::new(&bytes);
569 assert!(matches!(
571 r.next().unwrap(),
572 Some(Element::ContainerStart {
573 tag: Tag::Anonymous,
574 kind: ContainerKind::Structure
575 })
576 ));
577 assert!(matches!(
579 r.next().unwrap(),
580 Some(Element::Scalar {
581 tag: Tag::Context(0),
582 value: Value::Bool(false)
583 })
584 ));
585 assert!(matches!(
587 r.next().unwrap(),
588 Some(Element::Scalar {
589 tag: Tag::Context(1),
590 value: Value::Bool(false)
591 })
592 ));
593 assert!(matches!(
595 r.next().unwrap(),
596 Some(Element::ContainerStart {
597 tag: Tag::Context(2),
598 kind: ContainerKind::Array
599 })
600 ));
601 assert!(matches!(
603 r.next().unwrap(),
604 Some(Element::ContainerStart {
605 tag: Tag::Anonymous,
606 kind: ContainerKind::Structure
607 })
608 ));
609 assert!(matches!(
611 r.next().unwrap(),
612 Some(Element::ContainerStart {
613 tag: Tag::Context(0),
614 kind: ContainerKind::List
615 })
616 ));
617 assert!(matches!(
619 r.next().unwrap(),
620 Some(Element::Scalar {
621 tag: Tag::Context(0),
622 value: Value::Uint(0)
623 })
624 ));
625 assert!(matches!(
627 r.next().unwrap(),
628 Some(Element::Scalar {
629 tag: Tag::Context(1),
630 value: Value::Uint(0x0030)
631 })
632 ));
633 assert!(matches!(
635 r.next().unwrap(),
636 Some(Element::Scalar {
637 tag: Tag::Context(2),
638 value: Value::Uint(0)
639 })
640 ));
641 assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
643 assert!(matches!(
645 r.next().unwrap(),
646 Some(Element::ContainerStart {
647 tag: Tag::Context(1),
648 kind: ContainerKind::Structure
649 })
650 ));
651 assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
652 assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
654 assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
656 assert!(matches!(
658 r.next().unwrap(),
659 Some(Element::Scalar { tag: Tag::Context(0xFF), value: Value::Uint(v) })
660 if v == u64::from(IM_REVISION)
661 ));
662 assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
664 assert!(r.next().unwrap().is_none());
666 }
667
668 #[test]
669 fn invoke_request_carries_command_path_and_fields() {
670 let fields = vec![0x15u8, 0x18]; let bytes = build_invoke_request(
676 CommandPath {
677 endpoint: 1,
678 cluster: 0x0031,
679 command: 0x06,
680 },
681 &fields,
682 );
683 let retagged = [0x35u8, 0x01, 0x18];
685 assert!(
686 bytes.windows(retagged.len()).any(|w| w == retagged),
687 "command fields not embedded (expected retagged bytes {retagged:02X?} in {bytes:02X?})",
688 );
689 }
690
691 #[test]
692 fn parses_command_response_payload() {
693 use matter_codec::{Tag, TlvWriter};
694 let mut buf = Vec::new();
695 let mut w = TlvWriter::new(&mut buf);
696 w.start_structure(Tag::Anonymous).unwrap();
697 w.put_bool(Tag::Context(0), false).unwrap(); w.start_array(Tag::Context(1)).unwrap(); {
700 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();
704 w.put_uint(Tag::Context(1), 0x0030).unwrap();
705 w.put_uint(Tag::Context(2), 0x05).unwrap();
706 w.end_container().unwrap();
707 w.start_structure(Tag::Context(1)).unwrap(); w.end_container().unwrap();
709 w.end_container().unwrap(); w.end_container().unwrap(); }
712 w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
714 w.end_container().unwrap();
715
716 let parsed = parse_invoke_response(&buf).unwrap();
717 match parsed {
718 InvokeResponse::Command { path, fields_tlv } => {
719 assert_eq!(path.endpoint, 0);
720 assert_eq!(path.cluster, 0x0030);
721 assert_eq!(path.command, 0x05);
722 assert_eq!(fields_tlv, vec![0x15, 0x18]); }
724 InvokeResponse::Status(_) => panic!("expected Command, got Status"),
725 }
726 }
727
728 #[test]
729 fn parses_command_with_nonempty_fields() {
730 use matter_codec::{Tag, TlvWriter};
731
732 let mut expected_buf = Vec::new();
735 {
736 let mut w = TlvWriter::new(&mut expected_buf);
737 w.start_structure(Tag::Anonymous).unwrap();
738 w.put_uint(Tag::Context(0), 0x2A).unwrap();
739 w.end_container().unwrap();
740 }
741
742 let mut buf = Vec::new();
744 let mut w = TlvWriter::new(&mut buf);
745 w.start_structure(Tag::Anonymous).unwrap();
746 w.put_bool(Tag::Context(0), false).unwrap(); w.start_array(Tag::Context(1)).unwrap(); {
749 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();
758 w.put_uint(Tag::Context(0), 0x2A).unwrap();
759 w.end_container().unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); }
763 w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
765 w.end_container().unwrap();
766
767 let parsed = parse_invoke_response(&buf).unwrap();
768 match parsed {
769 InvokeResponse::Command { path, fields_tlv } => {
770 assert_eq!(path.endpoint, 1);
771 assert_eq!(path.cluster, 0x0050);
772 assert_eq!(path.command, 0x01);
773 assert_eq!(
774 fields_tlv, expected_buf,
775 "fields_tlv should decode to the same struct content as the original"
776 );
777 }
778 InvokeResponse::Status(_) => panic!("expected Command, got Status"),
779 }
780 }
781
782 #[test]
783 fn rejects_out_of_range_endpoint() {
784 use crate::error::ImError;
785 use matter_codec::{Tag, TlvWriter};
786
787 let mut buf = Vec::new();
788 let mut w = TlvWriter::new(&mut buf);
789 w.start_structure(Tag::Anonymous).unwrap();
790 w.put_bool(Tag::Context(0), false).unwrap();
791 w.start_array(Tag::Context(1)).unwrap();
792 {
793 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();
798 w.put_uint(Tag::Context(2), 0x00).unwrap();
799 w.end_container().unwrap();
800 w.start_structure(Tag::Context(1)).unwrap(); w.end_container().unwrap();
802 w.end_container().unwrap(); w.end_container().unwrap(); }
805 w.end_container().unwrap();
806 w.put_uint(Tag::Context(0xFF), 11).unwrap();
807 w.end_container().unwrap();
808
809 let result = parse_invoke_response(&buf);
810 assert!(
811 matches!(result, Err(ImError::UnexpectedValue(_))),
812 "expected UnexpectedValue for out-of-range endpoint, got {result:?}"
813 );
814 }
815
816 #[test]
817 fn empty_invoke_responses_array_errors() {
818 use crate::error::ImError;
819 use matter_codec::{Tag, TlvWriter};
820
821 let mut buf = Vec::new();
822 let mut w = TlvWriter::new(&mut buf);
823 w.start_structure(Tag::Anonymous).unwrap();
824 w.put_bool(Tag::Context(0), false).unwrap();
825 w.start_array(Tag::Context(1)).unwrap(); w.end_container().unwrap();
827 w.put_uint(Tag::Context(0xFF), 11).unwrap();
828 w.end_container().unwrap();
829
830 let result = parse_invoke_response(&buf);
831 assert!(
832 matches!(result, Err(ImError::MissingField(_))),
833 "expected MissingField for empty InvokeResponses, got {result:?}"
834 );
835 }
836
837 #[test]
838 fn parses_status_response() {
839 use matter_codec::{Tag, TlvWriter};
840 let mut buf = Vec::new();
841 let mut w = TlvWriter::new(&mut buf);
842 w.start_structure(Tag::Anonymous).unwrap();
843 w.put_bool(Tag::Context(0), false).unwrap();
844 w.start_array(Tag::Context(1)).unwrap();
845 {
846 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();
850 w.put_uint(Tag::Context(1), 0x0030).unwrap();
851 w.put_uint(Tag::Context(2), 0x00).unwrap();
852 w.end_container().unwrap();
853 w.start_structure(Tag::Context(1)).unwrap(); w.put_uint(Tag::Context(0), 0x01).unwrap(); w.end_container().unwrap();
856 w.end_container().unwrap(); w.end_container().unwrap(); }
859 w.end_container().unwrap();
860 w.put_uint(Tag::Context(0xFF), 11).unwrap();
861 w.end_container().unwrap();
862
863 let parsed = parse_invoke_response(&buf).unwrap();
864 assert!(matches!(
865 parsed,
866 InvokeResponse::Status(ImStatus::Failure(0x01))
867 ));
868 }
869
870 fn invoke_status_response(status: Option<u64>) -> Vec<u8> {
874 use matter_codec::{Tag, TlvWriter};
875 let mut buf = Vec::new();
876 let mut w = TlvWriter::new(&mut buf);
877 w.start_structure(Tag::Anonymous).unwrap();
878 w.put_bool(Tag::Context(0), false).unwrap();
879 w.start_array(Tag::Context(1)).unwrap();
880 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();
884 w.put_uint(Tag::Context(1), 0x0030).unwrap();
885 w.put_uint(Tag::Context(2), 0x00).unwrap();
886 w.end_container().unwrap();
887 w.start_structure(Tag::Context(1)).unwrap(); if let Some(v) = status {
889 w.put_uint(Tag::Context(0), v).unwrap();
890 }
891 w.end_container().unwrap();
892 w.end_container().unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
896 w.end_container().unwrap();
897 buf
898 }
899
900 #[test]
901 fn command_status_out_of_range_is_invalid_status_code() {
902 let buf = invoke_status_response(Some(0x100));
906 match parse_invoke_response(&buf) {
907 Err(ImError::InvalidStatusCode { code }) => assert_eq!(code, 0x100),
908 other => panic!("expected InvalidStatusCode {{ code: 0x100 }}, got {other:?}"),
909 }
910 }
911
912 #[test]
913 fn command_status_valid_code_still_parses() {
914 let buf = invoke_status_response(Some(0x88));
915 assert!(matches!(
916 parse_invoke_response(&buf),
917 Ok(InvokeResponse::Status(ImStatus::Failure(0x88)))
918 ));
919 }
920
921 #[test]
922 fn command_status_missing_field_still_missing_field() {
923 let buf = invoke_status_response(None);
925 assert!(matches!(
926 parse_invoke_response(&buf),
927 Err(ImError::MissingField("StatusIB.Status"))
928 ));
929 }
930
931 #[test]
932 fn invoke_response_ib_with_no_command_or_status_errors() {
933 use matter_codec::{Tag, TlvWriter};
934 let mut buf = Vec::new();
935 let mut w = TlvWriter::new(&mut buf);
936 w.start_structure(Tag::Anonymous).unwrap();
937 w.put_bool(Tag::Context(0), false).unwrap();
938 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();
942 w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
944 w.end_container().unwrap();
945
946 assert!(matches!(
947 parse_invoke_response(&buf),
948 Err(ImError::EmptyInvokeResponse)
949 ));
950 }
951
952 #[test]
953 fn batch_request_carries_command_refs() {
954 let fields = vec![0x15u8, 0x18]; let bytes = build_invoke_request_batch(&[
956 (
957 CommandPath {
958 endpoint: 1,
959 cluster: 0x06,
960 command: 0x02,
961 },
962 &fields,
963 ),
964 (
965 CommandPath {
966 endpoint: 2,
967 cluster: 0x06,
968 command: 0x00,
969 },
970 &fields,
971 ),
972 ]);
973 let mut r = TlvReader::new(&bytes);
978 let mut refs = Vec::new();
979 let mut depth = 0i32;
980 while let Some(el) = r.next().unwrap() {
981 match el {
982 Element::ContainerStart { .. } => depth += 1,
983 Element::ContainerEnd => depth -= 1,
984 Element::Scalar {
986 tag: Tag::Context(2),
987 value: Value::Uint(n),
988 } if depth == 3 => refs.push(n),
989 _ => {}
990 }
991 }
992 assert_eq!(refs, vec![0, 1], "CommandRefs must be 0 then 1");
993 }
994
995 #[test]
996 fn command_fields_preserve_device_integer_widths() {
997 let nonminimal_fields = [0x15u8, 0x25, 0x00, 0x2A, 0x00, 0x18];
1004 let mut buf = Vec::new();
1005 let mut w = TlvWriter::new(&mut buf);
1006 w.start_structure(Tag::Anonymous).unwrap();
1007 w.put_bool(Tag::Context(0), false).unwrap();
1008 w.start_array(Tag::Context(1)).unwrap();
1009 w.start_structure(Tag::Anonymous).unwrap(); w.start_structure(Tag::Context(0)).unwrap(); w.start_list(Tag::Context(0)).unwrap();
1012 w.put_uint(Tag::Context(0), 0).unwrap();
1013 w.put_uint(Tag::Context(1), 0x0030).unwrap();
1014 w.put_uint(Tag::Context(2), 0x05).unwrap();
1015 w.end_container().unwrap();
1016 w.put_preencoded(Tag::Context(1), &nonminimal_fields)
1017 .unwrap();
1018 w.end_container().unwrap();
1019 w.end_container().unwrap();
1020 w.end_container().unwrap();
1021 w.put_uint(Tag::Context(0xFF), 11).unwrap();
1022 w.end_container().unwrap();
1023
1024 match parse_invoke_response(&buf).unwrap() {
1025 InvokeResponse::Command { fields_tlv, .. } => {
1026 assert_eq!(
1027 fields_tlv, nonminimal_fields,
1028 "device widths must be preserved verbatim"
1029 );
1030 }
1031 InvokeResponse::Status(_) => panic!("expected Command"),
1032 }
1033 }
1034
1035 #[test]
1036 fn batch_response_parses_all_ibs_with_refs() {
1037 use matter_codec::{Tag, TlvWriter};
1038 let mut buf = Vec::new();
1040 let mut w = TlvWriter::new(&mut buf);
1041 w.start_structure(Tag::Anonymous).unwrap();
1042 w.put_bool(Tag::Context(0), false).unwrap(); w.start_array(Tag::Context(1)).unwrap(); {
1045 w.start_structure(Tag::Anonymous).unwrap();
1047 w.start_structure(Tag::Context(0)).unwrap(); w.start_list(Tag::Context(0)).unwrap();
1049 w.put_uint(Tag::Context(0), 1).unwrap();
1050 w.put_uint(Tag::Context(1), 0x06).unwrap();
1051 w.put_uint(Tag::Context(2), 0x02).unwrap();
1052 w.end_container().unwrap();
1053 w.start_structure(Tag::Context(1)).unwrap();
1054 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();
1060 w.start_structure(Tag::Context(1)).unwrap(); w.start_list(Tag::Context(0)).unwrap();
1062 w.put_uint(Tag::Context(0), 2).unwrap();
1063 w.put_uint(Tag::Context(1), 0x06).unwrap();
1064 w.put_uint(Tag::Context(2), 0x00).unwrap();
1065 w.end_container().unwrap();
1066 w.start_structure(Tag::Context(1)).unwrap(); w.put_uint(Tag::Context(0), 0).unwrap(); w.end_container().unwrap();
1069 w.put_uint(Tag::Context(2), 1).unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); }
1073 w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
1075 w.end_container().unwrap();
1076
1077 let entries = parse_invoke_response_batch(&buf).unwrap();
1078 assert_eq!(entries.len(), 2);
1079 assert_eq!(entries[0].command_ref, Some(0));
1080 assert!(matches!(
1081 entries[0].response,
1082 InvokeResponse::Command { ref path, .. } if path.endpoint == 1 && path.command == 0x02
1083 ));
1084 assert_eq!(entries[1].command_ref, Some(1));
1085 assert_eq!(
1086 entries[1].response,
1087 InvokeResponse::Status(ImStatus::Success)
1088 );
1089
1090 match parse_invoke_response(&buf).unwrap() {
1092 InvokeResponse::Command { path, .. } => assert_eq!(path.endpoint, 1),
1093 InvokeResponse::Status(_) => panic!("expected the first IB (a Command)"),
1094 }
1095 }
1096
1097 fn parse_cmd_path(build: impl FnOnce(&mut TlvWriter<'_>)) -> Result<CommandPath, ImError> {
1099 let mut buf = Vec::new();
1100 let mut w = TlvWriter::new(&mut buf);
1101 w.start_list(Tag::Anonymous).unwrap();
1102 build(&mut w);
1103 w.end_container().unwrap();
1104 let mut r = TlvReader::new(&buf);
1105 assert!(matches!(
1106 r.next().unwrap(),
1107 Some(Element::ContainerStart { .. })
1108 ));
1109 command_path_from_reader(&mut r)
1110 }
1111
1112 #[test]
1113 fn command_path_parses_members_and_errors() {
1114 let p = parse_cmd_path(|w| {
1115 w.put_uint(Tag::Context(0), 1).unwrap();
1116 w.put_uint(Tag::Context(1), 6).unwrap();
1117 w.put_uint(Tag::Context(2), 2).unwrap();
1118 })
1119 .unwrap();
1120 assert_eq!((p.endpoint, p.cluster, p.command), (1, 6, 2));
1121
1122 assert!(matches!(
1123 parse_cmd_path(|w| {
1124 w.put_uint(Tag::Context(0), 1).unwrap();
1125 w.put_uint(Tag::Context(1), 6).unwrap();
1126 }),
1127 Err(ImError::MissingField("CommandPath.command"))
1128 ));
1129
1130 assert!(matches!(
1131 parse_cmd_path(|w| {
1132 w.put_uint(Tag::Context(0), u64::from(u16::MAX) + 1)
1133 .unwrap();
1134 w.put_uint(Tag::Context(1), 6).unwrap();
1135 w.put_uint(Tag::Context(2), 2).unwrap();
1136 }),
1137 Err(ImError::UnexpectedValue(_))
1138 ));
1139 }
1140
1141 #[test]
1142 fn empty_command_fields_fallback_to_anonymous_empty_struct() {
1143 let mut buf = Vec::new();
1148 let mut w = TlvWriter::new(&mut buf);
1149 w.start_structure(Tag::Anonymous).unwrap();
1150 w.put_bool(Tag::Context(0), false).unwrap(); w.start_array(Tag::Context(1)).unwrap(); {
1153 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();
1157 w.put_uint(Tag::Context(1), 0x0030).unwrap();
1158 w.put_uint(Tag::Context(2), 0x05).unwrap();
1159 w.end_container().unwrap();
1160 w.end_container().unwrap(); w.end_container().unwrap(); }
1164 w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
1166 w.end_container().unwrap();
1167
1168 let parsed = parse_invoke_response(&buf).unwrap();
1169 match parsed {
1170 InvokeResponse::Command { fields_tlv, .. } => {
1171 assert_eq!(fields_tlv, vec![0x15, 0x18]);
1172 }
1173 InvokeResponse::Status(_) => panic!("expected Command, got Status"),
1174 }
1175 }
1176}