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
128pub(crate) fn parse_attribute_status_ib(
135 r: &mut TlvReader<'_>,
136) -> Result<(AttributePath, ImStatus), ImError> {
137 let mut path = None;
138 let mut status = None;
139 loop {
140 match r.next()? {
141 None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
142 Some(Element::ContainerEnd) => break,
143 Some(Element::ContainerStart {
144 tag: Tag::Context(0),
145 kind: ContainerKind::List,
146 }) => {
147 let members = read_container_members(r)?;
148 path = Some(attribute_path_from_value(&members)?);
149 }
150 Some(Element::ContainerStart {
151 tag: Tag::Context(1),
152 kind: ContainerKind::Structure,
153 }) => {
154 let members = read_container_members(r)?;
156 for (tag, v) in &members {
158 if let (Tag::Context(0), Value::Uint(n)) = (tag, v) {
159 let code = u8::try_from(*n)
160 .map_err(|_| ImError::InvalidStatusCode { code: *n })?;
161 status = Some(ImStatus::from_u8(code));
162 }
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
175const CHUNK_FLAG_RESERVE: usize = 4;
177
178#[must_use]
193pub fn build_list_write_chunks(
194 path: AttributePath,
195 element_tlvs: &[Vec<u8>],
196 budget: usize,
197 timed: bool,
198) -> Vec<Vec<u8>> {
199 let mut idx = 0usize;
201 let mut first_batch: Vec<&[u8]> = Vec::new();
202 while idx < element_tlvs.len() {
203 let candidate: Vec<&[u8]> = first_batch
204 .iter()
205 .copied()
206 .chain(std::iter::once(element_tlvs[idx].as_slice()))
207 .collect();
208 if encoded_replace_all_len(path, &candidate, timed) + CHUNK_FLAG_RESERVE > budget
209 && !first_batch.is_empty()
210 {
211 break;
212 }
213 first_batch.push(element_tlvs[idx].as_slice());
214 idx += 1;
215 }
216
217 let mut append_batches: Vec<Vec<&[u8]>> = Vec::new();
219 while idx < element_tlvs.len() {
220 let mut batch: Vec<&[u8]> = Vec::new();
221 while idx < element_tlvs.len() {
222 let candidate: Vec<&[u8]> = batch
223 .iter()
224 .copied()
225 .chain(std::iter::once(element_tlvs[idx].as_slice()))
226 .collect();
227 if encoded_append_len(path, &candidate, timed) + CHUNK_FLAG_RESERVE > budget
228 && !batch.is_empty()
229 {
230 break;
231 }
232 batch.push(element_tlvs[idx].as_slice());
233 idx += 1;
234 }
235 append_batches.push(batch);
236 }
237
238 let total = 1 + append_batches.len();
240 let mut messages: Vec<Vec<u8>> = Vec::with_capacity(total);
241 let first_more = total > 1;
242 messages.push(encode_replace_all(path, &first_batch, timed, first_more));
243 for (i, batch) in append_batches.iter().enumerate() {
244 let more = i + 1 < append_batches.len();
245 messages.push(encode_append_items(path, batch, timed, more));
246 }
247 messages
248}
249
250#[allow(clippy::expect_used)] fn encode_replace_all(
254 path: AttributePath,
255 elems: &[&[u8]],
256 timed: bool,
257 more_chunked: bool,
258) -> Vec<u8> {
259 let mut buf = Vec::new();
260 let mut w = TlvWriter::new(&mut buf);
261 w.start_structure(Tag::Anonymous)
262 .expect("infallible: vec writer");
263 w.put_bool(Tag::Context(0), false)
264 .expect("infallible: vec writer"); w.put_bool(Tag::Context(1), timed)
266 .expect("infallible: vec writer"); w.start_array(Tag::Context(2))
268 .expect("infallible: vec writer"); w.start_structure(Tag::Anonymous)
272 .expect("infallible: vec writer");
273 w.start_list(Tag::Context(1))
274 .expect("infallible: vec writer"); w.put_uint(Tag::Context(2), u64::from(path.endpoint))
276 .expect("infallible: vec writer");
277 w.put_uint(Tag::Context(3), u64::from(path.cluster))
278 .expect("infallible: vec writer");
279 w.put_uint(Tag::Context(4), u64::from(path.attribute))
280 .expect("infallible: vec writer");
281 w.end_container().expect("infallible: vec writer"); w.start_array(Tag::Context(2))
284 .expect("infallible: vec writer");
285 for e in elems {
286 w.put_preencoded(Tag::Anonymous, e)
287 .expect("infallible: caller passes valid anonymous-tagged elements");
288 }
289 w.end_container().expect("infallible: vec writer"); w.end_container().expect("infallible: vec writer"); w.end_container().expect("infallible: vec writer"); if more_chunked {
294 w.put_bool(Tag::Context(3), true)
295 .expect("infallible: vec writer"); }
297 w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
298 .expect("infallible: vec writer");
299 w.end_container().expect("infallible: vec writer"); buf
301}
302
303#[allow(clippy::expect_used)] fn encode_append_items(
307 path: AttributePath,
308 elems: &[&[u8]],
309 timed: bool,
310 more_chunked: bool,
311) -> Vec<u8> {
312 let mut buf = Vec::new();
313 let mut w = TlvWriter::new(&mut buf);
314 w.start_structure(Tag::Anonymous)
315 .expect("infallible: vec writer");
316 w.put_bool(Tag::Context(0), false)
317 .expect("infallible: vec writer"); w.put_bool(Tag::Context(1), timed)
319 .expect("infallible: vec writer"); w.start_array(Tag::Context(2))
321 .expect("infallible: vec writer"); for e in elems {
324 w.start_structure(Tag::Anonymous)
325 .expect("infallible: vec writer"); w.start_list(Tag::Context(1))
327 .expect("infallible: vec writer"); w.put_uint(Tag::Context(2), u64::from(path.endpoint))
329 .expect("infallible: vec writer");
330 w.put_uint(Tag::Context(3), u64::from(path.cluster))
331 .expect("infallible: vec writer");
332 w.put_uint(Tag::Context(4), u64::from(path.attribute))
333 .expect("infallible: vec writer");
334 w.put_null(Tag::Context(5)).expect("infallible: vec writer"); w.end_container().expect("infallible: vec writer"); w.put_preencoded(Tag::Context(2), e)
337 .expect("infallible: caller passes valid anonymous-tagged elements"); w.end_container().expect("infallible: vec writer"); }
340
341 w.end_container().expect("infallible: vec writer"); if more_chunked {
343 w.put_bool(Tag::Context(3), true)
344 .expect("infallible: vec writer"); }
346 w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
347 .expect("infallible: vec writer");
348 w.end_container().expect("infallible: vec writer"); buf
350}
351
352fn encoded_replace_all_len(path: AttributePath, elems: &[&[u8]], timed: bool) -> usize {
353 encode_replace_all(path, elems, timed, false).len()
354}
355
356fn encoded_append_len(path: AttributePath, elems: &[&[u8]], timed: bool) -> usize {
357 encode_append_items(path, elems, timed, false).len()
358}
359
360#[cfg(test)]
370pub(crate) fn reassemble_list_write(chunks: &[Vec<u8>]) -> Vec<Vec<u8>> {
371 let mut out = Vec::new();
372 for chunk in chunks {
373 collect_elements_from_chunk(chunk, &mut out);
374 }
375 out
376}
377
378#[cfg(test)]
383#[allow(clippy::expect_used)]
384fn collect_elements_from_chunk(chunk: &[u8], out: &mut Vec<Vec<u8>>) {
385 let mut r = TlvReader::new(chunk);
386 let Ok(Some(Element::ContainerStart {
388 tag: Tag::Anonymous,
389 kind: ContainerKind::Structure,
390 })) = r.next()
391 else {
392 return;
393 };
394
395 loop {
397 match r.next() {
398 Ok(Some(Element::ContainerStart {
399 tag: Tag::Context(2),
400 kind: ContainerKind::Array,
401 })) => break,
402 Ok(Some(Element::ContainerStart { .. })) => {
403 let _ = skip_container(&mut r);
404 }
405 Ok(Some(Element::ContainerEnd) | None) | Err(_) => return,
406 Ok(Some(_)) => {}
407 }
408 }
409
410 loop {
413 match r.next() {
414 Ok(Some(Element::ContainerStart {
415 kind: ContainerKind::Structure,
416 ..
417 })) => {
418 if let Ok(members) = read_container_members(&mut r) {
419 collect_elements_from_ib_members(&members, out);
420 }
421 }
422 Ok(Some(Element::ContainerEnd) | None) => break,
423 Ok(Some(Element::ContainerStart { .. })) => {
424 let _ = skip_container(&mut r);
425 }
426 Ok(Some(_)) | Err(_) => {}
427 }
428 }
429}
430
431#[cfg(test)]
438#[allow(clippy::expect_used)]
439fn collect_elements_from_ib_members(members: &[(Tag, Value)], out: &mut Vec<Vec<u8>>) {
440 let mut is_append = false;
442 let mut data_value: Option<&Value> = None;
443
444 for (tag, value) in members {
445 match tag {
446 Tag::Context(1) => {
447 if let Value::List(path_members) = value {
449 for (pt, pv) in path_members {
450 if *pt == Tag::Context(5) && *pv == Value::Null {
451 is_append = true;
452 }
453 }
454 }
455 }
456 Tag::Context(2) => {
457 data_value = Some(value);
458 }
459 _ => {}
460 }
461 }
462
463 let Some(data) = data_value else { return };
464
465 if is_append {
466 let mut elem_bytes = Vec::new();
469 let mut w = TlvWriter::new(&mut elem_bytes);
470 w.write_value(Tag::Anonymous, data)
471 .expect("infallible: vec writer");
472 out.push(elem_bytes);
473 } else {
474 if let Value::Array(elems) = data {
476 for elem in elems {
477 let mut elem_bytes = Vec::new();
478 let mut w = TlvWriter::new(&mut elem_bytes);
479 w.write_value(Tag::Anonymous, elem)
480 .expect("infallible: vec writer");
481 out.push(elem_bytes);
482 }
483 }
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 #![allow(clippy::unwrap_used, clippy::expect_used)]
490 use super::*;
491 use matter_codec::{ContainerKind, Element, Tag, TlvReader, TlvWriter, Value};
492
493 fn anon_string(s: &str) -> Vec<u8> {
496 let mut buf = Vec::new();
497 let mut w = TlvWriter::new(&mut buf);
498 w.put_utf8(Tag::Anonymous, s).unwrap();
499 buf
500 }
501
502 #[test]
503 fn write_request_has_expected_structure() {
504 let bytes = build_write_request(&[AttributeWriteRequest {
505 path: AttributePath {
506 endpoint: 0,
507 cluster: 0x28,
508 attribute: 0x05, },
510 value_tlv: anon_string("matter-rust"),
511 }]);
512 let mut r = TlvReader::new(&bytes);
513 assert!(matches!(
515 r.next().unwrap(),
516 Some(Element::ContainerStart {
517 tag: Tag::Anonymous,
518 kind: ContainerKind::Structure
519 })
520 ));
521 assert!(matches!(
523 r.next().unwrap(),
524 Some(Element::Scalar {
525 tag: Tag::Context(0),
526 value: Value::Bool(false)
527 })
528 ));
529 assert!(matches!(
531 r.next().unwrap(),
532 Some(Element::Scalar {
533 tag: Tag::Context(1),
534 value: Value::Bool(false)
535 })
536 ));
537 assert!(matches!(
539 r.next().unwrap(),
540 Some(Element::ContainerStart {
541 tag: Tag::Context(2),
542 kind: ContainerKind::Array
543 })
544 ));
545 assert!(matches!(
547 r.next().unwrap(),
548 Some(Element::ContainerStart {
549 tag: Tag::Anonymous,
550 kind: ContainerKind::Structure
551 })
552 ));
553 assert!(matches!(
555 r.next().unwrap(),
556 Some(Element::ContainerStart {
557 tag: Tag::Context(1),
558 kind: ContainerKind::List
559 })
560 ));
561 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!(
569 r.next().unwrap(),
570 Some(Element::Scalar {
571 tag: Tag::Context(3),
572 value: Value::Uint(0x28)
573 })
574 ));
575 assert!(matches!(
576 r.next().unwrap(),
577 Some(Element::Scalar {
578 tag: Tag::Context(4),
579 value: Value::Uint(0x05)
580 })
581 ));
582 }
583
584 fn echo_write_response(entries: &[(AttributePath, u8)]) -> Vec<u8> {
586 let mut buf = Vec::new();
587 let mut w = TlvWriter::new(&mut buf);
588 w.start_structure(Tag::Anonymous).unwrap();
589 w.start_array(Tag::Context(0)).unwrap(); for (p, code) in entries {
591 w.start_structure(Tag::Anonymous).unwrap(); w.start_list(Tag::Context(0)).unwrap(); w.put_uint(Tag::Context(2), u64::from(p.endpoint)).unwrap();
594 w.put_uint(Tag::Context(3), u64::from(p.cluster)).unwrap();
595 w.put_uint(Tag::Context(4), u64::from(p.attribute)).unwrap();
596 w.end_container().unwrap();
597 w.start_structure(Tag::Context(1)).unwrap(); w.put_uint(Tag::Context(0), u64::from(*code)).unwrap();
599 w.end_container().unwrap();
600 w.end_container().unwrap(); }
602 w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
604 w.end_container().unwrap();
605 buf
606 }
607
608 #[test]
609 fn parses_success_and_failure_statuses() {
610 let p1 = AttributePath {
611 endpoint: 0,
612 cluster: 0x28,
613 attribute: 0x05,
614 };
615 let p2 = AttributePath {
616 endpoint: 0,
617 cluster: 0x28,
618 attribute: 0x06,
619 };
620 let msg = echo_write_response(&[(p1, 0x00), (p2, 0x01)]);
621 let statuses = parse_write_response(&msg).unwrap();
622 assert_eq!(statuses.len(), 2);
623 assert_eq!(statuses[0], (p1, ImStatus::Success));
624 assert_eq!(statuses[1], (p2, ImStatus::Failure(0x01)));
625 }
626
627 #[test]
628 fn missing_status_is_an_error() {
629 let mut buf = Vec::new();
631 let mut w = TlvWriter::new(&mut buf);
632 w.start_structure(Tag::Anonymous).unwrap();
633 w.start_array(Tag::Context(0)).unwrap();
634 w.start_structure(Tag::Anonymous).unwrap();
635 w.start_list(Tag::Context(0)).unwrap();
636 w.put_uint(Tag::Context(2), 0).unwrap();
637 w.put_uint(Tag::Context(3), 0x28).unwrap();
638 w.put_uint(Tag::Context(4), 0x05).unwrap();
639 w.end_container().unwrap();
640 w.end_container().unwrap();
641 w.end_container().unwrap();
642 w.put_uint(Tag::Context(0xFF), 11).unwrap();
643 w.end_container().unwrap();
644
645 let result = parse_write_response(&buf);
646 assert!(
647 matches!(
648 result,
649 Err(ImError::MissingField("AttributeStatusIB.Status"))
650 ),
651 "expected MissingField, got {result:?}"
652 );
653 }
654
655 #[test]
656 fn empty_message_yields_empty_statuses() {
657 let mut buf = Vec::new();
658 let mut w = TlvWriter::new(&mut buf);
659 w.start_structure(Tag::Anonymous).unwrap();
660 w.put_uint(Tag::Context(0xFF), 11).unwrap();
661 w.end_container().unwrap();
662 let statuses = parse_write_response(&buf).unwrap();
663 assert!(statuses.is_empty());
664 }
665}
666
667#[cfg(test)]
668mod chunk_tests {
669 #![allow(clippy::unwrap_used, clippy::expect_used)]
670 use super::*;
671 use matter_codec::{Tag, TlvWriter, Value};
672 use proptest::prelude::*;
673
674 fn entry_tlv(n: u64) -> Vec<u8> {
675 let mut b = Vec::new();
677 let mut w = TlvWriter::new(&mut b);
678 w.write_value(
679 Tag::Anonymous,
680 &Value::Structure(vec![(Tag::Context(1), Value::Uint(n))]),
681 )
682 .unwrap();
683 b
684 }
685
686 fn p() -> AttributePath {
687 AttributePath {
688 endpoint: 0,
689 cluster: 0x001F,
690 attribute: 0x0000,
691 }
692 }
693
694 #[test]
695 fn single_chunk_equals_replace_all_build_write_request() {
696 let elems = vec![entry_tlv(1), entry_tlv(2)];
697 let chunks = build_list_write_chunks(p(), &elems, 4096, false);
698 assert_eq!(chunks.len(), 1);
699 let mut arr = Vec::new();
701 let mut w = TlvWriter::new(&mut arr);
702 w.write_value(
703 Tag::Anonymous,
704 &Value::Array(vec![
705 Value::Structure(vec![(Tag::Context(1), Value::Uint(1))]),
706 Value::Structure(vec![(Tag::Context(1), Value::Uint(2))]),
707 ]),
708 )
709 .unwrap();
710 let expected = build_write_request(&[AttributeWriteRequest {
711 path: p(),
712 value_tlv: arr,
713 }]);
714 assert_eq!(
715 chunks[0], expected,
716 "single-chunk output must be byte-identical to build_write_request"
717 );
718 }
719
720 #[test]
721 fn overflow_splits_and_sets_more_chunked() {
722 let elems = vec![entry_tlv(1), entry_tlv(2), entry_tlv(3)];
724 let chunks = build_list_write_chunks(p(), &elems, 40, false);
725 assert!(
726 chunks.len() >= 2,
727 "expected multiple chunks, got {}",
728 chunks.len()
729 );
730 for (i, c) in chunks.iter().enumerate() {
732 assert_eq!(has_more_chunked(c), i + 1 != chunks.len(), "chunk {i}");
733 }
734 }
735
736 #[test]
737 fn reassemble_roundtrips() {
738 let elems: Vec<Vec<u8>> = (0..7).map(entry_tlv).collect();
739 let chunks = build_list_write_chunks(p(), &elems, 48, false);
740 assert_eq!(reassemble_list_write(&chunks), elems);
741 }
742
743 fn has_more_chunked(msg: &[u8]) -> bool {
745 use matter_codec::{Element, TlvReader};
746 let mut r = TlvReader::new(msg);
747 let _ = r.next();
749 loop {
750 match r.next() {
751 Ok(Some(Element::Scalar {
752 tag: Tag::Context(3),
753 value: Value::Bool(b),
754 })) => return b,
755 Ok(Some(Element::ContainerStart { .. })) => {
756 let _ = super::skip_container(&mut r);
757 }
758 Ok(Some(Element::ContainerEnd) | None) | Err(_) => return false,
759 Ok(Some(_)) => {}
760 }
761 }
762 }
763
764 proptest! {
765 #[test]
766 fn split_reassemble_identity(count in 0usize..30, budget in 30usize..200) {
767 let elems: Vec<Vec<u8>> = (0..count as u64).map(entry_tlv).collect();
768 let chunks = build_list_write_chunks(p(), &elems, budget, false);
769 prop_assert_eq!(reassemble_list_write(&chunks), elems.clone());
770 for (i, c) in chunks.iter().enumerate() {
772 prop_assert_eq!(has_more_chunked(c), i + 1 != chunks.len());
773 }
774 }
775 }
776}