1#![forbid(unsafe_code)]
4
5use crate::error::ImError;
6use crate::path::{attribute_path_from_value, AttributePath};
7use crate::status::ImStatus;
8use crate::{expect_message_struct, read_container_members, skip_container, IM_REVISION};
9use matter_codec::{ContainerKind, Element, Tag, TlvReader, TlvWriter, Value};
10
11#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct AttributeWriteRequest {
14 pub path: AttributePath,
16 pub value_tlv: Vec<u8>,
19}
20
21#[must_use]
33pub fn build_write_request(writes: &[AttributeWriteRequest]) -> Vec<u8> {
34 build_write_request_inner(writes, false)
35}
36
37#[must_use]
41pub fn build_write_request_timed(writes: &[AttributeWriteRequest]) -> Vec<u8> {
42 build_write_request_inner(writes, true)
43}
44
45#[allow(clippy::expect_used)] fn build_write_request_inner(writes: &[AttributeWriteRequest], timed: bool) -> Vec<u8> {
47 let mut buf = Vec::new();
48 let mut w = TlvWriter::new(&mut buf);
49 w.start_structure(Tag::Anonymous)
50 .expect("infallible: vec writer");
51 w.put_bool(Tag::Context(0), false)
52 .expect("infallible: vec writer"); w.put_bool(Tag::Context(1), timed)
54 .expect("infallible: vec writer"); w.start_array(Tag::Context(2))
56 .expect("infallible: vec writer"); for wr in writes {
58 w.start_structure(Tag::Anonymous)
59 .expect("infallible: vec writer"); w.start_list(Tag::Context(1))
61 .expect("infallible: vec writer"); w.put_uint(Tag::Context(2), u64::from(wr.path.endpoint))
63 .expect("infallible: vec writer");
64 w.put_uint(Tag::Context(3), u64::from(wr.path.cluster))
65 .expect("infallible: vec writer");
66 w.put_uint(Tag::Context(4), u64::from(wr.path.attribute))
67 .expect("infallible: vec writer");
68 w.end_container().expect("infallible: vec writer"); w.put_preencoded(Tag::Context(2), &wr.value_tlv)
70 .expect("infallible: caller passes a valid anonymous-tagged element"); w.end_container().expect("infallible: vec writer"); }
73 w.end_container().expect("infallible: vec writer"); w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
75 .expect("infallible: vec writer");
76 w.end_container().expect("infallible: vec writer"); buf
78}
79
80pub fn parse_write_response(bytes: &[u8]) -> Result<Vec<(AttributePath, ImStatus)>, ImError> {
93 let mut r = TlvReader::new(bytes);
94 expect_message_struct(&mut r)?;
95
96 let mut out = Vec::new();
97
98 loop {
100 match r.next()? {
101 None | Some(Element::ContainerEnd) => return Ok(out),
102 Some(Element::ContainerStart {
103 tag: Tag::Context(0),
104 kind: ContainerKind::Array,
105 }) => break,
106 Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
107 Some(_) => {}
108 }
109 }
110
111 loop {
113 match r.next()? {
114 None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
115 Some(Element::ContainerEnd) => break, Some(Element::ContainerStart {
117 kind: ContainerKind::Structure,
118 ..
119 }) => out.push(parse_attribute_status_ib(&mut r)?),
120 Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
121 Some(_) => {}
122 }
123 }
124
125 Ok(out)
126}
127
128fn parse_attribute_status_ib(r: &mut TlvReader<'_>) -> Result<(AttributePath, ImStatus), ImError> {
131 let mut path = None;
132 let mut status = None;
133 loop {
134 match r.next()? {
135 None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
136 Some(Element::ContainerEnd) => break,
137 Some(Element::ContainerStart {
138 tag: Tag::Context(0),
139 kind: ContainerKind::List,
140 }) => {
141 let members = read_container_members(r)?;
142 path = Some(attribute_path_from_value(&members)?);
143 }
144 Some(Element::ContainerStart {
145 tag: Tag::Context(1),
146 kind: ContainerKind::Structure,
147 }) => {
148 let members = read_container_members(r)?;
150 for (tag, v) in &members {
152 if let (Tag::Context(0), Value::Uint(n)) = (tag, v) {
153 let code = u8::try_from(*n)
154 .map_err(|_| ImError::InvalidStatusCode { code: *n })?;
155 status = Some(ImStatus::from_u8(code));
156 }
157 }
158 }
159 Some(Element::ContainerStart { .. }) => skip_container(r)?,
160 Some(_) => {}
161 }
162 }
163 Ok((
164 path.ok_or(ImError::MissingField("AttributeStatusIB.Path"))?,
165 status.ok_or(ImError::MissingField("AttributeStatusIB.Status"))?,
166 ))
167}
168
169const CHUNK_FLAG_RESERVE: usize = 4;
171
172#[must_use]
187pub fn build_list_write_chunks(
188 path: AttributePath,
189 element_tlvs: &[Vec<u8>],
190 budget: usize,
191 timed: bool,
192) -> Vec<Vec<u8>> {
193 let mut idx = 0usize;
195 let mut first_batch: Vec<&[u8]> = Vec::new();
196 while idx < element_tlvs.len() {
197 let candidate: Vec<&[u8]> = first_batch
198 .iter()
199 .copied()
200 .chain(std::iter::once(element_tlvs[idx].as_slice()))
201 .collect();
202 if encoded_replace_all_len(path, &candidate, timed) + CHUNK_FLAG_RESERVE > budget
203 && !first_batch.is_empty()
204 {
205 break;
206 }
207 first_batch.push(element_tlvs[idx].as_slice());
208 idx += 1;
209 }
210
211 let mut append_batches: Vec<Vec<&[u8]>> = Vec::new();
213 while idx < element_tlvs.len() {
214 let mut batch: Vec<&[u8]> = Vec::new();
215 while idx < element_tlvs.len() {
216 let candidate: Vec<&[u8]> = batch
217 .iter()
218 .copied()
219 .chain(std::iter::once(element_tlvs[idx].as_slice()))
220 .collect();
221 if encoded_append_len(path, &candidate, timed) + CHUNK_FLAG_RESERVE > budget
222 && !batch.is_empty()
223 {
224 break;
225 }
226 batch.push(element_tlvs[idx].as_slice());
227 idx += 1;
228 }
229 append_batches.push(batch);
230 }
231
232 let total = 1 + append_batches.len();
234 let mut messages: Vec<Vec<u8>> = Vec::with_capacity(total);
235 let first_more = total > 1;
236 messages.push(encode_replace_all(path, &first_batch, timed, first_more));
237 for (i, batch) in append_batches.iter().enumerate() {
238 let more = i + 1 < append_batches.len();
239 messages.push(encode_append_items(path, batch, timed, more));
240 }
241 messages
242}
243
244#[allow(clippy::expect_used)] fn encode_replace_all(
248 path: AttributePath,
249 elems: &[&[u8]],
250 timed: bool,
251 more_chunked: bool,
252) -> Vec<u8> {
253 let mut buf = Vec::new();
254 let mut w = TlvWriter::new(&mut buf);
255 w.start_structure(Tag::Anonymous)
256 .expect("infallible: vec writer");
257 w.put_bool(Tag::Context(0), false)
258 .expect("infallible: vec writer"); w.put_bool(Tag::Context(1), timed)
260 .expect("infallible: vec writer"); w.start_array(Tag::Context(2))
262 .expect("infallible: vec writer"); w.start_structure(Tag::Anonymous)
266 .expect("infallible: vec writer");
267 w.start_list(Tag::Context(1))
268 .expect("infallible: vec writer"); w.put_uint(Tag::Context(2), u64::from(path.endpoint))
270 .expect("infallible: vec writer");
271 w.put_uint(Tag::Context(3), u64::from(path.cluster))
272 .expect("infallible: vec writer");
273 w.put_uint(Tag::Context(4), u64::from(path.attribute))
274 .expect("infallible: vec writer");
275 w.end_container().expect("infallible: vec writer"); w.start_array(Tag::Context(2))
278 .expect("infallible: vec writer");
279 for e in elems {
280 w.put_preencoded(Tag::Anonymous, e)
281 .expect("infallible: caller passes valid anonymous-tagged elements");
282 }
283 w.end_container().expect("infallible: vec writer"); w.end_container().expect("infallible: vec writer"); w.end_container().expect("infallible: vec writer"); if more_chunked {
288 w.put_bool(Tag::Context(3), true)
289 .expect("infallible: vec writer"); }
291 w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
292 .expect("infallible: vec writer");
293 w.end_container().expect("infallible: vec writer"); buf
295}
296
297#[allow(clippy::expect_used)] fn encode_append_items(
301 path: AttributePath,
302 elems: &[&[u8]],
303 timed: bool,
304 more_chunked: bool,
305) -> Vec<u8> {
306 let mut buf = Vec::new();
307 let mut w = TlvWriter::new(&mut buf);
308 w.start_structure(Tag::Anonymous)
309 .expect("infallible: vec writer");
310 w.put_bool(Tag::Context(0), false)
311 .expect("infallible: vec writer"); w.put_bool(Tag::Context(1), timed)
313 .expect("infallible: vec writer"); w.start_array(Tag::Context(2))
315 .expect("infallible: vec writer"); for e in elems {
318 w.start_structure(Tag::Anonymous)
319 .expect("infallible: vec writer"); w.start_list(Tag::Context(1))
321 .expect("infallible: vec writer"); w.put_uint(Tag::Context(2), u64::from(path.endpoint))
323 .expect("infallible: vec writer");
324 w.put_uint(Tag::Context(3), u64::from(path.cluster))
325 .expect("infallible: vec writer");
326 w.put_uint(Tag::Context(4), u64::from(path.attribute))
327 .expect("infallible: vec writer");
328 w.put_null(Tag::Context(5)).expect("infallible: vec writer"); w.end_container().expect("infallible: vec writer"); w.put_preencoded(Tag::Context(2), e)
331 .expect("infallible: caller passes valid anonymous-tagged elements"); w.end_container().expect("infallible: vec writer"); }
334
335 w.end_container().expect("infallible: vec writer"); if more_chunked {
337 w.put_bool(Tag::Context(3), true)
338 .expect("infallible: vec writer"); }
340 w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
341 .expect("infallible: vec writer");
342 w.end_container().expect("infallible: vec writer"); buf
344}
345
346fn encoded_replace_all_len(path: AttributePath, elems: &[&[u8]], timed: bool) -> usize {
347 encode_replace_all(path, elems, timed, false).len()
348}
349
350fn encoded_append_len(path: AttributePath, elems: &[&[u8]], timed: bool) -> usize {
351 encode_append_items(path, elems, timed, false).len()
352}
353
354#[cfg(test)]
364pub(crate) fn reassemble_list_write(chunks: &[Vec<u8>]) -> Vec<Vec<u8>> {
365 let mut out = Vec::new();
366 for chunk in chunks {
367 collect_elements_from_chunk(chunk, &mut out);
368 }
369 out
370}
371
372#[cfg(test)]
377#[allow(clippy::expect_used)]
378fn collect_elements_from_chunk(chunk: &[u8], out: &mut Vec<Vec<u8>>) {
379 let mut r = TlvReader::new(chunk);
380 let Ok(Some(Element::ContainerStart {
382 tag: Tag::Anonymous,
383 kind: ContainerKind::Structure,
384 })) = r.next()
385 else {
386 return;
387 };
388
389 loop {
391 match r.next() {
392 Ok(Some(Element::ContainerStart {
393 tag: Tag::Context(2),
394 kind: ContainerKind::Array,
395 })) => break,
396 Ok(Some(Element::ContainerStart { .. })) => {
397 let _ = skip_container(&mut r);
398 }
399 Ok(Some(Element::ContainerEnd) | None) | Err(_) => return,
400 Ok(Some(_)) => {}
401 }
402 }
403
404 loop {
407 match r.next() {
408 Ok(Some(Element::ContainerStart {
409 kind: ContainerKind::Structure,
410 ..
411 })) => {
412 if let Ok(members) = read_container_members(&mut r) {
413 collect_elements_from_ib_members(&members, out);
414 }
415 }
416 Ok(Some(Element::ContainerEnd) | None) => break,
417 Ok(Some(Element::ContainerStart { .. })) => {
418 let _ = skip_container(&mut r);
419 }
420 Ok(Some(_)) | Err(_) => {}
421 }
422 }
423}
424
425#[cfg(test)]
432#[allow(clippy::expect_used)]
433fn collect_elements_from_ib_members(members: &[(Tag, Value)], out: &mut Vec<Vec<u8>>) {
434 let mut is_append = false;
436 let mut data_value: Option<&Value> = None;
437
438 for (tag, value) in members {
439 match tag {
440 Tag::Context(1) => {
441 if let Value::List(path_members) = value {
443 for (pt, pv) in path_members {
444 if *pt == Tag::Context(5) && *pv == Value::Null {
445 is_append = true;
446 }
447 }
448 }
449 }
450 Tag::Context(2) => {
451 data_value = Some(value);
452 }
453 _ => {}
454 }
455 }
456
457 let Some(data) = data_value else { return };
458
459 if is_append {
460 let mut elem_bytes = Vec::new();
463 let mut w = TlvWriter::new(&mut elem_bytes);
464 w.write_value(Tag::Anonymous, data)
465 .expect("infallible: vec writer");
466 out.push(elem_bytes);
467 } else {
468 if let Value::Array(elems) = data {
470 for elem in elems {
471 let mut elem_bytes = Vec::new();
472 let mut w = TlvWriter::new(&mut elem_bytes);
473 w.write_value(Tag::Anonymous, elem)
474 .expect("infallible: vec writer");
475 out.push(elem_bytes);
476 }
477 }
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 #![allow(clippy::unwrap_used, clippy::expect_used)]
484 use super::*;
485 use matter_codec::{ContainerKind, Element, Tag, TlvReader, TlvWriter, Value};
486
487 fn anon_string(s: &str) -> Vec<u8> {
490 let mut buf = Vec::new();
491 let mut w = TlvWriter::new(&mut buf);
492 w.put_utf8(Tag::Anonymous, s).unwrap();
493 buf
494 }
495
496 #[test]
497 fn write_request_has_expected_structure() {
498 let bytes = build_write_request(&[AttributeWriteRequest {
499 path: AttributePath {
500 endpoint: 0,
501 cluster: 0x28,
502 attribute: 0x05, },
504 value_tlv: anon_string("matter-rust"),
505 }]);
506 let mut r = TlvReader::new(&bytes);
507 assert!(matches!(
509 r.next().unwrap(),
510 Some(Element::ContainerStart {
511 tag: Tag::Anonymous,
512 kind: ContainerKind::Structure
513 })
514 ));
515 assert!(matches!(
517 r.next().unwrap(),
518 Some(Element::Scalar {
519 tag: Tag::Context(0),
520 value: Value::Bool(false)
521 })
522 ));
523 assert!(matches!(
525 r.next().unwrap(),
526 Some(Element::Scalar {
527 tag: Tag::Context(1),
528 value: Value::Bool(false)
529 })
530 ));
531 assert!(matches!(
533 r.next().unwrap(),
534 Some(Element::ContainerStart {
535 tag: Tag::Context(2),
536 kind: ContainerKind::Array
537 })
538 ));
539 assert!(matches!(
541 r.next().unwrap(),
542 Some(Element::ContainerStart {
543 tag: Tag::Anonymous,
544 kind: ContainerKind::Structure
545 })
546 ));
547 assert!(matches!(
549 r.next().unwrap(),
550 Some(Element::ContainerStart {
551 tag: Tag::Context(1),
552 kind: ContainerKind::List
553 })
554 ));
555 assert!(matches!(
556 r.next().unwrap(),
557 Some(Element::Scalar {
558 tag: Tag::Context(2),
559 value: Value::Uint(0)
560 })
561 ));
562 assert!(matches!(
563 r.next().unwrap(),
564 Some(Element::Scalar {
565 tag: Tag::Context(3),
566 value: Value::Uint(0x28)
567 })
568 ));
569 assert!(matches!(
570 r.next().unwrap(),
571 Some(Element::Scalar {
572 tag: Tag::Context(4),
573 value: Value::Uint(0x05)
574 })
575 ));
576 }
577
578 fn echo_write_response(entries: &[(AttributePath, u8)]) -> Vec<u8> {
580 let mut buf = Vec::new();
581 let mut w = TlvWriter::new(&mut buf);
582 w.start_structure(Tag::Anonymous).unwrap();
583 w.start_array(Tag::Context(0)).unwrap(); for (p, code) in entries {
585 w.start_structure(Tag::Anonymous).unwrap(); w.start_list(Tag::Context(0)).unwrap(); w.put_uint(Tag::Context(2), u64::from(p.endpoint)).unwrap();
588 w.put_uint(Tag::Context(3), u64::from(p.cluster)).unwrap();
589 w.put_uint(Tag::Context(4), u64::from(p.attribute)).unwrap();
590 w.end_container().unwrap();
591 w.start_structure(Tag::Context(1)).unwrap(); w.put_uint(Tag::Context(0), u64::from(*code)).unwrap();
593 w.end_container().unwrap();
594 w.end_container().unwrap(); }
596 w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
598 w.end_container().unwrap();
599 buf
600 }
601
602 #[test]
603 fn parses_success_and_failure_statuses() {
604 let p1 = AttributePath {
605 endpoint: 0,
606 cluster: 0x28,
607 attribute: 0x05,
608 };
609 let p2 = AttributePath {
610 endpoint: 0,
611 cluster: 0x28,
612 attribute: 0x06,
613 };
614 let msg = echo_write_response(&[(p1, 0x00), (p2, 0x01)]);
615 let statuses = parse_write_response(&msg).unwrap();
616 assert_eq!(statuses.len(), 2);
617 assert_eq!(statuses[0], (p1, ImStatus::Success));
618 assert_eq!(statuses[1], (p2, ImStatus::Failure(0x01)));
619 }
620
621 #[test]
622 fn missing_status_is_an_error() {
623 let mut buf = Vec::new();
625 let mut w = TlvWriter::new(&mut buf);
626 w.start_structure(Tag::Anonymous).unwrap();
627 w.start_array(Tag::Context(0)).unwrap();
628 w.start_structure(Tag::Anonymous).unwrap();
629 w.start_list(Tag::Context(0)).unwrap();
630 w.put_uint(Tag::Context(2), 0).unwrap();
631 w.put_uint(Tag::Context(3), 0x28).unwrap();
632 w.put_uint(Tag::Context(4), 0x05).unwrap();
633 w.end_container().unwrap();
634 w.end_container().unwrap();
635 w.end_container().unwrap();
636 w.put_uint(Tag::Context(0xFF), 11).unwrap();
637 w.end_container().unwrap();
638
639 let result = parse_write_response(&buf);
640 assert!(
641 matches!(
642 result,
643 Err(ImError::MissingField("AttributeStatusIB.Status"))
644 ),
645 "expected MissingField, got {result:?}"
646 );
647 }
648
649 #[test]
650 fn empty_message_yields_empty_statuses() {
651 let mut buf = Vec::new();
652 let mut w = TlvWriter::new(&mut buf);
653 w.start_structure(Tag::Anonymous).unwrap();
654 w.put_uint(Tag::Context(0xFF), 11).unwrap();
655 w.end_container().unwrap();
656 let statuses = parse_write_response(&buf).unwrap();
657 assert!(statuses.is_empty());
658 }
659}
660
661#[cfg(test)]
662mod chunk_tests {
663 #![allow(clippy::unwrap_used, clippy::expect_used)]
664 use super::*;
665 use matter_codec::{Tag, TlvWriter, Value};
666 use proptest::prelude::*;
667
668 fn entry_tlv(n: u64) -> Vec<u8> {
669 let mut b = Vec::new();
671 let mut w = TlvWriter::new(&mut b);
672 w.write_value(
673 Tag::Anonymous,
674 &Value::Structure(vec![(Tag::Context(1), Value::Uint(n))]),
675 )
676 .unwrap();
677 b
678 }
679
680 fn p() -> AttributePath {
681 AttributePath {
682 endpoint: 0,
683 cluster: 0x001F,
684 attribute: 0x0000,
685 }
686 }
687
688 #[test]
689 fn single_chunk_equals_replace_all_build_write_request() {
690 let elems = vec![entry_tlv(1), entry_tlv(2)];
691 let chunks = build_list_write_chunks(p(), &elems, 4096, false);
692 assert_eq!(chunks.len(), 1);
693 let mut arr = Vec::new();
695 let mut w = TlvWriter::new(&mut arr);
696 w.write_value(
697 Tag::Anonymous,
698 &Value::Array(vec![
699 Value::Structure(vec![(Tag::Context(1), Value::Uint(1))]),
700 Value::Structure(vec![(Tag::Context(1), Value::Uint(2))]),
701 ]),
702 )
703 .unwrap();
704 let expected = build_write_request(&[AttributeWriteRequest {
705 path: p(),
706 value_tlv: arr,
707 }]);
708 assert_eq!(
709 chunks[0], expected,
710 "single-chunk output must be byte-identical to build_write_request"
711 );
712 }
713
714 #[test]
715 fn overflow_splits_and_sets_more_chunked() {
716 let elems = vec![entry_tlv(1), entry_tlv(2), entry_tlv(3)];
718 let chunks = build_list_write_chunks(p(), &elems, 40, false);
719 assert!(
720 chunks.len() >= 2,
721 "expected multiple chunks, got {}",
722 chunks.len()
723 );
724 for (i, c) in chunks.iter().enumerate() {
726 assert_eq!(has_more_chunked(c), i + 1 != chunks.len(), "chunk {i}");
727 }
728 }
729
730 #[test]
731 fn reassemble_roundtrips() {
732 let elems: Vec<Vec<u8>> = (0..7).map(entry_tlv).collect();
733 let chunks = build_list_write_chunks(p(), &elems, 48, false);
734 assert_eq!(reassemble_list_write(&chunks), elems);
735 }
736
737 fn has_more_chunked(msg: &[u8]) -> bool {
739 use matter_codec::{Element, TlvReader};
740 let mut r = TlvReader::new(msg);
741 let _ = r.next();
743 loop {
744 match r.next() {
745 Ok(Some(Element::Scalar {
746 tag: Tag::Context(3),
747 value: Value::Bool(b),
748 })) => return b,
749 Ok(Some(Element::ContainerStart { .. })) => {
750 let _ = super::skip_container(&mut r);
751 }
752 Ok(Some(Element::ContainerEnd) | None) | Err(_) => return false,
753 Ok(Some(_)) => {}
754 }
755 }
756 }
757
758 proptest! {
759 #[test]
760 fn split_reassemble_identity(count in 0usize..30, budget in 30usize..200) {
761 let elems: Vec<Vec<u8>> = (0..count as u64).map(entry_tlv).collect();
762 let chunks = build_list_write_chunks(p(), &elems, budget, false);
763 prop_assert_eq!(reassemble_list_write(&chunks), elems.clone());
764 for (i, c) in chunks.iter().enumerate() {
766 prop_assert_eq!(has_more_chunked(c), i + 1 != chunks.len());
767 }
768 }
769 }
770}