1#![forbid(unsafe_code)]
4
5use crate::error::ImError;
6use crate::path::AttributePath;
7#[cfg(test)]
8use crate::read_container_members;
9use crate::status::ImStatus;
10use crate::{expect_message_struct, skip_container, IM_REVISION};
11use matter_codec::{ContainerKind, Element, Tag, TlvReader, TlvWriter, Value};
12
13#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct AttributeWriteRequest {
16 pub path: AttributePath,
18 pub value_tlv: Vec<u8>,
21}
22
23#[must_use]
35pub fn build_write_request(writes: &[AttributeWriteRequest]) -> Vec<u8> {
36 build_write_request_inner(writes, false)
37}
38
39#[must_use]
43pub fn build_write_request_timed(writes: &[AttributeWriteRequest]) -> Vec<u8> {
44 build_write_request_inner(writes, true)
45}
46
47#[allow(clippy::expect_used)] fn build_write_request_inner(writes: &[AttributeWriteRequest], timed: bool) -> Vec<u8> {
49 let mut buf = Vec::with_capacity(
50 48 + writes
51 .iter()
52 .map(|wr| 24 + wr.value_tlv.len())
53 .sum::<usize>(),
54 );
55 let mut w = TlvWriter::new(&mut buf);
56 w.start_structure(Tag::Anonymous)
57 .expect("infallible: vec writer");
58 w.put_bool(Tag::Context(0), false)
59 .expect("infallible: vec writer"); w.put_bool(Tag::Context(1), timed)
61 .expect("infallible: vec writer"); w.start_array(Tag::Context(2))
63 .expect("infallible: vec writer"); for wr in writes {
65 w.start_structure(Tag::Anonymous)
66 .expect("infallible: vec writer"); w.start_list(Tag::Context(1))
68 .expect("infallible: vec writer"); w.put_uint(Tag::Context(2), u64::from(wr.path.endpoint))
70 .expect("infallible: vec writer");
71 w.put_uint(Tag::Context(3), u64::from(wr.path.cluster))
72 .expect("infallible: vec writer");
73 w.put_uint(Tag::Context(4), u64::from(wr.path.attribute))
74 .expect("infallible: vec writer");
75 w.end_container().expect("infallible: vec writer"); w.put_preencoded(Tag::Context(2), &wr.value_tlv)
77 .expect("infallible: caller passes a valid anonymous-tagged element"); w.end_container().expect("infallible: vec writer"); }
80 w.end_container().expect("infallible: vec writer"); w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
82 .expect("infallible: vec writer");
83 w.end_container().expect("infallible: vec writer"); buf
85}
86
87pub fn parse_write_response(bytes: &[u8]) -> Result<Vec<(AttributePath, ImStatus)>, ImError> {
100 let mut r = TlvReader::new(bytes);
101 expect_message_struct(&mut r)?;
102
103 let mut out = Vec::new();
104
105 loop {
107 match r.next()? {
108 None | Some(Element::ContainerEnd) => return Ok(out),
109 Some(Element::ContainerStart {
110 tag: Tag::Context(0),
111 kind: ContainerKind::Array,
112 }) => break,
113 Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
114 Some(_) => {}
115 }
116 }
117
118 loop {
120 match r.next()? {
121 None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
122 Some(Element::ContainerEnd) => break, Some(Element::ContainerStart {
124 kind: ContainerKind::Structure,
125 ..
126 }) => out.push(parse_attribute_status_ib(&mut r)?),
127 Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
128 Some(_) => {}
129 }
130 }
131
132 Ok(out)
133}
134
135pub(crate) fn parse_attribute_status_ib(
142 r: &mut TlvReader<'_>,
143) -> Result<(AttributePath, ImStatus), ImError> {
144 let mut path = None;
145 let mut status = None;
146 loop {
147 match r.next()? {
148 None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
149 Some(Element::ContainerEnd) => break,
150 Some(Element::ContainerStart {
151 tag: Tag::Context(0),
152 kind: ContainerKind::List,
153 }) => {
154 let (p, _) = crate::path::attribute_path_from_reader(r)?;
155 path = Some(p);
156 }
157 Some(Element::ContainerStart {
158 tag: Tag::Context(1),
159 kind: ContainerKind::Structure,
160 }) => {
161 if let Some(s) = parse_status_ib_body(r)? {
162 status = Some(s);
163 }
164 }
165 Some(Element::ContainerStart { .. }) => skip_container(r)?,
166 Some(_) => {}
167 }
168 }
169 Ok((
170 path.ok_or(ImError::MissingField("AttributeStatusIB.Path"))?,
171 status.ok_or(ImError::MissingField("AttributeStatusIB.Status"))?,
172 ))
173}
174
175fn parse_status_ib_body(r: &mut TlvReader<'_>) -> Result<Option<ImStatus>, ImError> {
179 let mut status = None;
180 loop {
181 match r.next()? {
182 None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
183 Some(Element::ContainerEnd) => return Ok(status),
184 Some(Element::Scalar {
185 tag: Tag::Context(0),
186 value: Value::Uint(n),
187 }) => {
188 let code = u8::try_from(n).map_err(|_| ImError::InvalidStatusCode { code: n })?;
189 status = Some(ImStatus::from_u8(code));
190 }
191 Some(Element::ContainerStart { .. }) => skip_container(r)?,
192 Some(_) => {}
193 }
194 }
195}
196
197const CHUNK_FLAG_RESERVE: usize = 4;
202
203#[must_use]
229pub fn build_list_write_chunks(
230 path: AttributePath,
231 element_tlvs: &[Vec<u8>],
232 budget: usize,
233 timed: bool,
234) -> Vec<Vec<u8>> {
235 const PROBE: &[u8] = &[0x14];
238
239 let replace_base = encoded_replace_all_len(path, &[], timed);
248 let append_base = encoded_append_len(path, &[], timed);
249 let append_per_elem_overhead =
250 encoded_append_len(path, &[PROBE], timed) - append_base - PROBE.len();
251
252 let mut idx = 0usize;
254 let mut first_batch: Vec<&[u8]> = Vec::new();
255 let mut size = replace_base;
256 while idx < element_tlvs.len() {
257 let cost = element_tlvs[idx].len();
258 if size + cost + CHUNK_FLAG_RESERVE > budget && !first_batch.is_empty() {
259 break;
260 }
261 size += cost;
262 first_batch.push(element_tlvs[idx].as_slice());
263 idx += 1;
264 }
265
266 let mut append_batches: Vec<Vec<&[u8]>> = Vec::new();
268 while idx < element_tlvs.len() {
269 let mut batch: Vec<&[u8]> = Vec::new();
270 let mut size = append_base;
271 while idx < element_tlvs.len() {
272 let cost = append_per_elem_overhead + element_tlvs[idx].len();
273 if size + cost + CHUNK_FLAG_RESERVE > budget && !batch.is_empty() {
274 break;
275 }
276 size += cost;
277 batch.push(element_tlvs[idx].as_slice());
278 idx += 1;
279 }
280 append_batches.push(batch);
281 }
282
283 let total = 1 + append_batches.len();
291 let mut messages: Vec<Vec<u8>> = Vec::with_capacity(total);
292 let chunked = total > 1;
293 let first_more = if chunked { Some(true) } else { None };
294 messages.push(encode_replace_all(path, &first_batch, timed, first_more));
295 for (i, batch) in append_batches.iter().enumerate() {
296 let more = Some(i + 1 < append_batches.len());
297 messages.push(encode_append_items(path, batch, timed, more));
298 }
299 messages
300}
301
302#[allow(clippy::expect_used)] fn encode_replace_all(
312 path: AttributePath,
313 elems: &[&[u8]],
314 timed: bool,
315 more_chunked: Option<bool>,
316) -> Vec<u8> {
317 let mut buf = Vec::new();
318 let mut w = TlvWriter::new(&mut buf);
319 w.start_structure(Tag::Anonymous)
320 .expect("infallible: vec writer");
321 w.put_bool(Tag::Context(0), false)
322 .expect("infallible: vec writer"); w.put_bool(Tag::Context(1), timed)
324 .expect("infallible: vec writer"); w.start_array(Tag::Context(2))
326 .expect("infallible: vec writer"); w.start_structure(Tag::Anonymous)
330 .expect("infallible: vec writer");
331 w.start_list(Tag::Context(1))
332 .expect("infallible: vec writer"); w.put_uint(Tag::Context(2), u64::from(path.endpoint))
334 .expect("infallible: vec writer");
335 w.put_uint(Tag::Context(3), u64::from(path.cluster))
336 .expect("infallible: vec writer");
337 w.put_uint(Tag::Context(4), u64::from(path.attribute))
338 .expect("infallible: vec writer");
339 w.end_container().expect("infallible: vec writer"); w.start_array(Tag::Context(2))
342 .expect("infallible: vec writer");
343 for e in elems {
344 w.put_preencoded(Tag::Anonymous, e)
345 .expect("infallible: caller passes valid anonymous-tagged elements");
346 }
347 w.end_container().expect("infallible: vec writer"); w.end_container().expect("infallible: vec writer"); w.end_container().expect("infallible: vec writer"); if let Some(v) = more_chunked {
352 w.put_bool(Tag::Context(3), v)
353 .expect("infallible: vec writer"); }
355 w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
356 .expect("infallible: vec writer");
357 w.end_container().expect("infallible: vec writer"); buf
359}
360
361#[allow(clippy::expect_used)] fn encode_append_items(
368 path: AttributePath,
369 elems: &[&[u8]],
370 timed: bool,
371 more_chunked: Option<bool>,
372) -> Vec<u8> {
373 let mut buf = Vec::new();
374 let mut w = TlvWriter::new(&mut buf);
375 w.start_structure(Tag::Anonymous)
376 .expect("infallible: vec writer");
377 w.put_bool(Tag::Context(0), false)
378 .expect("infallible: vec writer"); w.put_bool(Tag::Context(1), timed)
380 .expect("infallible: vec writer"); w.start_array(Tag::Context(2))
382 .expect("infallible: vec writer"); for e in elems {
385 w.start_structure(Tag::Anonymous)
386 .expect("infallible: vec writer"); w.start_list(Tag::Context(1))
388 .expect("infallible: vec writer"); w.put_uint(Tag::Context(2), u64::from(path.endpoint))
390 .expect("infallible: vec writer");
391 w.put_uint(Tag::Context(3), u64::from(path.cluster))
392 .expect("infallible: vec writer");
393 w.put_uint(Tag::Context(4), u64::from(path.attribute))
394 .expect("infallible: vec writer");
395 w.put_null(Tag::Context(5)).expect("infallible: vec writer"); w.end_container().expect("infallible: vec writer"); w.put_preencoded(Tag::Context(2), e)
398 .expect("infallible: caller passes valid anonymous-tagged elements"); w.end_container().expect("infallible: vec writer"); }
401
402 w.end_container().expect("infallible: vec writer"); if let Some(v) = more_chunked {
404 w.put_bool(Tag::Context(3), v)
405 .expect("infallible: vec writer"); }
407 w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
408 .expect("infallible: vec writer");
409 w.end_container().expect("infallible: vec writer"); buf
411}
412
413fn encoded_replace_all_len(path: AttributePath, elems: &[&[u8]], timed: bool) -> usize {
417 encode_replace_all(path, elems, timed, None).len()
418}
419
420fn encoded_append_len(path: AttributePath, elems: &[&[u8]], timed: bool) -> usize {
423 encode_append_items(path, elems, timed, None).len()
424}
425
426#[cfg(test)]
436pub(crate) fn reassemble_list_write(chunks: &[Vec<u8>]) -> Vec<Vec<u8>> {
437 let mut out = Vec::new();
438 for chunk in chunks {
439 collect_elements_from_chunk(chunk, &mut out);
440 }
441 out
442}
443
444#[cfg(test)]
449#[allow(clippy::expect_used)]
450fn collect_elements_from_chunk(chunk: &[u8], out: &mut Vec<Vec<u8>>) {
451 let mut r = TlvReader::new(chunk);
452 let Ok(Some(Element::ContainerStart {
454 tag: Tag::Anonymous,
455 kind: ContainerKind::Structure,
456 })) = r.next()
457 else {
458 return;
459 };
460
461 loop {
463 match r.next() {
464 Ok(Some(Element::ContainerStart {
465 tag: Tag::Context(2),
466 kind: ContainerKind::Array,
467 })) => break,
468 Ok(Some(Element::ContainerStart { .. })) => {
469 let _ = skip_container(&mut r);
470 }
471 Ok(Some(Element::ContainerEnd) | None) | Err(_) => return,
472 Ok(Some(_)) => {}
473 }
474 }
475
476 loop {
479 match r.next() {
480 Ok(Some(Element::ContainerStart {
481 kind: ContainerKind::Structure,
482 ..
483 })) => {
484 if let Ok(members) = read_container_members(&mut r) {
485 collect_elements_from_ib_members(&members, out);
486 }
487 }
488 Ok(Some(Element::ContainerEnd) | None) => break,
489 Ok(Some(Element::ContainerStart { .. })) => {
490 let _ = skip_container(&mut r);
491 }
492 Ok(Some(_)) | Err(_) => {}
493 }
494 }
495}
496
497#[cfg(test)]
504#[allow(clippy::expect_used)]
505fn collect_elements_from_ib_members(members: &[(Tag, Value)], out: &mut Vec<Vec<u8>>) {
506 let mut is_append = false;
508 let mut data_value: Option<&Value> = None;
509
510 for (tag, value) in members {
511 match tag {
512 Tag::Context(1) => {
513 if let Value::List(path_members) = value {
515 for (pt, pv) in path_members {
516 if *pt == Tag::Context(5) && *pv == Value::Null {
517 is_append = true;
518 }
519 }
520 }
521 }
522 Tag::Context(2) => {
523 data_value = Some(value);
524 }
525 _ => {}
526 }
527 }
528
529 let Some(data) = data_value else { return };
530
531 if is_append {
532 let mut elem_bytes = Vec::new();
535 let mut w = TlvWriter::new(&mut elem_bytes);
536 w.write_value(Tag::Anonymous, data)
537 .expect("infallible: vec writer");
538 out.push(elem_bytes);
539 } else {
540 if let Value::Array(elems) = data {
542 for elem in elems {
543 let mut elem_bytes = Vec::new();
544 let mut w = TlvWriter::new(&mut elem_bytes);
545 w.write_value(Tag::Anonymous, elem)
546 .expect("infallible: vec writer");
547 out.push(elem_bytes);
548 }
549 }
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 #![allow(clippy::unwrap_used, clippy::expect_used)]
556 use super::*;
557 use matter_codec::{ContainerKind, Element, Tag, TlvReader, TlvWriter, Value};
558
559 fn anon_string(s: &str) -> Vec<u8> {
562 let mut buf = Vec::new();
563 let mut w = TlvWriter::new(&mut buf);
564 w.put_utf8(Tag::Anonymous, s).unwrap();
565 buf
566 }
567
568 #[test]
569 fn write_request_has_expected_structure() {
570 let bytes = build_write_request(&[AttributeWriteRequest {
571 path: AttributePath {
572 endpoint: 0,
573 cluster: 0x28,
574 attribute: 0x05, },
576 value_tlv: anon_string("matter-rust"),
577 }]);
578 let mut r = TlvReader::new(&bytes);
579 assert!(matches!(
581 r.next().unwrap(),
582 Some(Element::ContainerStart {
583 tag: Tag::Anonymous,
584 kind: ContainerKind::Structure
585 })
586 ));
587 assert!(matches!(
589 r.next().unwrap(),
590 Some(Element::Scalar {
591 tag: Tag::Context(0),
592 value: Value::Bool(false)
593 })
594 ));
595 assert!(matches!(
597 r.next().unwrap(),
598 Some(Element::Scalar {
599 tag: Tag::Context(1),
600 value: Value::Bool(false)
601 })
602 ));
603 assert!(matches!(
605 r.next().unwrap(),
606 Some(Element::ContainerStart {
607 tag: Tag::Context(2),
608 kind: ContainerKind::Array
609 })
610 ));
611 assert!(matches!(
613 r.next().unwrap(),
614 Some(Element::ContainerStart {
615 tag: Tag::Anonymous,
616 kind: ContainerKind::Structure
617 })
618 ));
619 assert!(matches!(
621 r.next().unwrap(),
622 Some(Element::ContainerStart {
623 tag: Tag::Context(1),
624 kind: ContainerKind::List
625 })
626 ));
627 assert!(matches!(
628 r.next().unwrap(),
629 Some(Element::Scalar {
630 tag: Tag::Context(2),
631 value: Value::Uint(0)
632 })
633 ));
634 assert!(matches!(
635 r.next().unwrap(),
636 Some(Element::Scalar {
637 tag: Tag::Context(3),
638 value: Value::Uint(0x28)
639 })
640 ));
641 assert!(matches!(
642 r.next().unwrap(),
643 Some(Element::Scalar {
644 tag: Tag::Context(4),
645 value: Value::Uint(0x05)
646 })
647 ));
648 }
649
650 fn echo_write_response(entries: &[(AttributePath, u8)]) -> Vec<u8> {
652 let mut buf = Vec::new();
653 let mut w = TlvWriter::new(&mut buf);
654 w.start_structure(Tag::Anonymous).unwrap();
655 w.start_array(Tag::Context(0)).unwrap(); for (p, code) in entries {
657 w.start_structure(Tag::Anonymous).unwrap(); w.start_list(Tag::Context(0)).unwrap(); w.put_uint(Tag::Context(2), u64::from(p.endpoint)).unwrap();
660 w.put_uint(Tag::Context(3), u64::from(p.cluster)).unwrap();
661 w.put_uint(Tag::Context(4), u64::from(p.attribute)).unwrap();
662 w.end_container().unwrap();
663 w.start_structure(Tag::Context(1)).unwrap(); w.put_uint(Tag::Context(0), u64::from(*code)).unwrap();
665 w.end_container().unwrap();
666 w.end_container().unwrap(); }
668 w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
670 w.end_container().unwrap();
671 buf
672 }
673
674 #[test]
675 fn parses_success_and_failure_statuses() {
676 let p1 = AttributePath {
677 endpoint: 0,
678 cluster: 0x28,
679 attribute: 0x05,
680 };
681 let p2 = AttributePath {
682 endpoint: 0,
683 cluster: 0x28,
684 attribute: 0x06,
685 };
686 let msg = echo_write_response(&[(p1, 0x00), (p2, 0x01)]);
687 let statuses = parse_write_response(&msg).unwrap();
688 assert_eq!(statuses.len(), 2);
689 assert_eq!(statuses[0], (p1, ImStatus::Success));
690 assert_eq!(statuses[1], (p2, ImStatus::Failure(0x01)));
691 }
692
693 #[test]
694 fn missing_status_is_an_error() {
695 let mut buf = Vec::new();
697 let mut w = TlvWriter::new(&mut buf);
698 w.start_structure(Tag::Anonymous).unwrap();
699 w.start_array(Tag::Context(0)).unwrap();
700 w.start_structure(Tag::Anonymous).unwrap();
701 w.start_list(Tag::Context(0)).unwrap();
702 w.put_uint(Tag::Context(2), 0).unwrap();
703 w.put_uint(Tag::Context(3), 0x28).unwrap();
704 w.put_uint(Tag::Context(4), 0x05).unwrap();
705 w.end_container().unwrap();
706 w.end_container().unwrap();
707 w.end_container().unwrap();
708 w.put_uint(Tag::Context(0xFF), 11).unwrap();
709 w.end_container().unwrap();
710
711 let result = parse_write_response(&buf);
712 assert!(
713 matches!(
714 result,
715 Err(ImError::MissingField("AttributeStatusIB.Status"))
716 ),
717 "expected MissingField, got {result:?}"
718 );
719 }
720
721 #[test]
722 fn empty_message_yields_empty_statuses() {
723 let mut buf = Vec::new();
724 let mut w = TlvWriter::new(&mut buf);
725 w.start_structure(Tag::Anonymous).unwrap();
726 w.put_uint(Tag::Context(0xFF), 11).unwrap();
727 w.end_container().unwrap();
728 let statuses = parse_write_response(&buf).unwrap();
729 assert!(statuses.is_empty());
730 }
731
732 fn parse_status(build: impl FnOnce(&mut TlvWriter<'_>)) -> Result<Option<ImStatus>, ImError> {
734 let mut buf = Vec::new();
735 let mut w = TlvWriter::new(&mut buf);
736 w.start_structure(Tag::Anonymous).unwrap();
737 build(&mut w);
738 w.end_container().unwrap();
739 let mut r = TlvReader::new(&buf);
740 assert!(matches!(
741 r.next().unwrap(),
742 Some(Element::ContainerStart { .. })
743 ));
744 parse_status_ib_body(&mut r)
745 }
746
747 #[test]
748 fn status_ib_body_parses_status_none_and_range_error() {
749 assert!(matches!(
750 parse_status(|w| w.put_uint(Tag::Context(0), 0).unwrap()),
751 Ok(Some(ImStatus::Success))
752 ));
753 assert!(matches!(parse_status(|_| {}), Ok(None)));
755 assert!(matches!(
757 parse_status(|w| w.put_uint(Tag::Context(0), 0x1_00).unwrap()),
758 Err(ImError::InvalidStatusCode { code: 0x100 })
759 ));
760 assert!(matches!(
762 parse_status(|w| {
763 w.start_structure(Tag::Context(7)).unwrap();
764 w.put_uint(Tag::Context(0), 9).unwrap();
765 w.end_container().unwrap();
766 w.put_uint(Tag::Context(0), 0).unwrap();
767 }),
768 Ok(Some(ImStatus::Success))
769 ));
770 }
771}
772
773#[cfg(test)]
774mod chunk_tests {
775 #![allow(clippy::unwrap_used, clippy::expect_used)]
776 use super::*;
777 use matter_codec::{Tag, TlvWriter, Value};
778 use proptest::prelude::*;
779
780 fn entry_tlv(n: u64) -> Vec<u8> {
781 let mut b = Vec::new();
783 let mut w = TlvWriter::new(&mut b);
784 w.write_value(
785 Tag::Anonymous,
786 &Value::Structure(vec![(Tag::Context(1), Value::Uint(n))]),
787 )
788 .unwrap();
789 b
790 }
791
792 fn p() -> AttributePath {
793 AttributePath {
794 endpoint: 0,
795 cluster: 0x001F,
796 attribute: 0x0000,
797 }
798 }
799
800 fn build_list_write_chunks_reference(
805 path: AttributePath,
806 element_tlvs: &[Vec<u8>],
807 budget: usize,
808 timed: bool,
809 ) -> Vec<Vec<u8>> {
810 let mut idx = 0usize;
812 let mut first_batch: Vec<&[u8]> = Vec::new();
813 while idx < element_tlvs.len() {
814 let candidate: Vec<&[u8]> = first_batch
815 .iter()
816 .copied()
817 .chain(std::iter::once(element_tlvs[idx].as_slice()))
818 .collect();
819 if encoded_replace_all_len(path, &candidate, timed) + CHUNK_FLAG_RESERVE > budget
820 && !first_batch.is_empty()
821 {
822 break;
823 }
824 first_batch.push(element_tlvs[idx].as_slice());
825 idx += 1;
826 }
827
828 let mut append_batches: Vec<Vec<&[u8]>> = Vec::new();
830 while idx < element_tlvs.len() {
831 let mut batch: Vec<&[u8]> = Vec::new();
832 while idx < element_tlvs.len() {
833 let candidate: Vec<&[u8]> = batch
834 .iter()
835 .copied()
836 .chain(std::iter::once(element_tlvs[idx].as_slice()))
837 .collect();
838 if encoded_append_len(path, &candidate, timed) + CHUNK_FLAG_RESERVE > budget
839 && !batch.is_empty()
840 {
841 break;
842 }
843 batch.push(element_tlvs[idx].as_slice());
844 idx += 1;
845 }
846 append_batches.push(batch);
847 }
848
849 let total = 1 + append_batches.len();
853 let mut messages: Vec<Vec<u8>> = Vec::with_capacity(total);
854 let chunked = total > 1;
855 let first_more = if chunked { Some(true) } else { None };
856 messages.push(encode_replace_all(path, &first_batch, timed, first_more));
857 for (i, batch) in append_batches.iter().enumerate() {
858 let more = Some(i + 1 < append_batches.len());
859 messages.push(encode_append_items(path, batch, timed, more));
860 }
861 messages
862 }
863
864 proptest! {
865 #[test]
868 fn incremental_packer_matches_reference(
869 lens in proptest::collection::vec(0usize..120, 0..30),
870 budget in 60usize..600,
871 timed: bool,
872 ) {
873 let elems: Vec<Vec<u8>> = lens.iter().map(|&n| {
874 let mut buf = Vec::new();
875 let mut w = TlvWriter::new(&mut buf);
876 w.put_bytes(Tag::Anonymous, &vec![0x5A; n]).unwrap();
877 buf
878 }).collect();
879 let p = p(); prop_assert_eq!(
881 build_list_write_chunks(p, &elems, budget, timed),
882 build_list_write_chunks_reference(p, &elems, budget, timed)
883 );
884 }
885 }
886
887 #[test]
888 fn single_chunk_equals_replace_all_build_write_request() {
889 let elems = vec![entry_tlv(1), entry_tlv(2)];
890 let chunks = build_list_write_chunks(p(), &elems, 4096, false);
891 assert_eq!(chunks.len(), 1);
892 let mut arr = Vec::new();
894 let mut w = TlvWriter::new(&mut arr);
895 w.write_value(
896 Tag::Anonymous,
897 &Value::Array(vec![
898 Value::Structure(vec![(Tag::Context(1), Value::Uint(1))]),
899 Value::Structure(vec![(Tag::Context(1), Value::Uint(2))]),
900 ]),
901 )
902 .unwrap();
903 let expected = build_write_request(&[AttributeWriteRequest {
904 path: p(),
905 value_tlv: arr,
906 }]);
907 assert_eq!(
908 chunks[0], expected,
909 "single-chunk output must be byte-identical to build_write_request"
910 );
911 }
912
913 #[test]
914 fn overflow_splits_and_sets_more_chunked() {
915 let elems = vec![entry_tlv(1), entry_tlv(2), entry_tlv(3)];
917 let chunks = build_list_write_chunks(p(), &elems, 40, false);
918 assert!(
919 chunks.len() >= 2,
920 "expected multiple chunks, got {}",
921 chunks.len()
922 );
923 for (i, c) in chunks.iter().enumerate() {
928 assert_eq!(
929 more_chunked_flag(c),
930 Some(i + 1 != chunks.len()),
931 "chunk {i}"
932 );
933 }
934 }
935
936 #[test]
937 fn reassemble_roundtrips() {
938 let elems: Vec<Vec<u8>> = (0..7).map(entry_tlv).collect();
939 let chunks = build_list_write_chunks(p(), &elems, 48, false);
940 assert_eq!(reassemble_list_write(&chunks), elems);
941 }
942
943 #[test]
947 fn multi_chunk_carries_explicit_flag_final_chunk_explicit_false() {
948 let elems = vec![entry_tlv(1), entry_tlv(2), entry_tlv(3)];
952 let chunks = build_list_write_chunks(p(), &elems, 40, false);
953 assert_eq!(
954 chunks.len(),
955 3,
956 "expected exactly 3 chunks, got {}",
957 chunks.len()
958 );
959 assert_eq!(more_chunked_flag(&chunks[0]), Some(true), "chunk 0");
960 assert_eq!(more_chunked_flag(&chunks[1]), Some(true), "chunk 1");
961 assert_eq!(
962 more_chunked_flag(&chunks[2]),
963 Some(false),
964 "final chunk must carry an EXPLICIT MoreChunkedMessages=false, not omit it"
965 );
966
967 let single = build_list_write_chunks(p(), &[entry_tlv(1)], 4096, false);
969 assert_eq!(single.len(), 1);
970 assert_eq!(
971 more_chunked_flag(&single[0]),
972 None,
973 "single-chunk output must omit MoreChunkedMessages entirely"
974 );
975 }
976
977 fn more_chunked_flag(msg: &[u8]) -> Option<bool> {
980 use matter_codec::{Element, TlvReader};
981 let mut r = TlvReader::new(msg);
982 let _ = r.next();
984 loop {
985 match r.next() {
986 Ok(Some(Element::Scalar {
987 tag: Tag::Context(3),
988 value: Value::Bool(b),
989 })) => return Some(b),
990 Ok(Some(Element::ContainerStart { .. })) => {
991 let _ = super::skip_container(&mut r);
992 }
993 Ok(Some(Element::ContainerEnd) | None) | Err(_) => return None,
994 Ok(Some(_)) => {}
995 }
996 }
997 }
998
999 proptest! {
1000 #[test]
1001 fn split_reassemble_identity(count in 0usize..30, budget in 30usize..200) {
1002 let elems: Vec<Vec<u8>> = (0..count as u64).map(entry_tlv).collect();
1003 let chunks = build_list_write_chunks(p(), &elems, budget, false);
1004 prop_assert_eq!(reassemble_list_write(&chunks), elems.clone());
1005 for (i, c) in chunks.iter().enumerate() {
1008 let expected = if chunks.len() > 1 {
1009 Some(i + 1 != chunks.len())
1010 } else {
1011 None
1012 };
1013 prop_assert_eq!(more_chunked_flag(c), expected);
1014 }
1015 }
1016 }
1017}