Skip to main content

matter_interaction/
write.rs

1//! `WriteRequestMessage` / `WriteResponseMessage` framing — Matter §10.6.
2
3#![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/// One attribute write: a concrete path plus the pre-encoded data value.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct AttributeWriteRequest {
14    /// Concrete attribute path to write.
15    pub path: AttributePath,
16    /// The attribute value as a standalone anonymous-tagged TLV element
17    /// (e.g. the output of a `matter-clusters` attribute encoder).
18    pub value_tlv: Vec<u8>,
19}
20
21/// Build a `WriteRequestMessage` for one or more concrete attribute writes.
22///
23/// `SuppressResponse` and `TimedRequest` are both `false`; `DataVersion`
24/// and `MoreChunkedMessages` are omitted (no chunking — single-MTU writes
25/// only, per the M7 scope).
26///
27/// # Panics
28///
29/// Panics if a `value_tlv` is not a valid anonymous-tagged TLV element
30/// (i.e. not the output of a codec encode call). The function is
31/// otherwise infallible; `Vec`-backed `TlvWriter` never fails.
32#[must_use]
33pub fn build_write_request(writes: &[AttributeWriteRequest]) -> Vec<u8> {
34    build_write_request_inner(writes, false)
35}
36
37/// Like [`build_write_request`] but sets `TimedRequest = true` — the action half
38/// of a timed interaction, sent on the same exchange after a `TimedRequest`
39/// message (see [`crate::build_timed_request`]).
40#[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)] // Vec-backed TlvWriter is infallible.
46fn 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"); // SuppressResponse
53    w.put_bool(Tag::Context(1), timed)
54        .expect("infallible: vec writer"); // TimedRequest
55    w.start_array(Tag::Context(2))
56        .expect("infallible: vec writer"); // WriteRequests
57    for wr in writes {
58        w.start_structure(Tag::Anonymous)
59            .expect("infallible: vec writer"); // AttributeDataIB
60        w.start_list(Tag::Context(1))
61            .expect("infallible: vec writer"); // Path (AttributePathIB)
62        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"); // Path
69        w.put_preencoded(Tag::Context(2), &wr.value_tlv)
70            .expect("infallible: caller passes a valid anonymous-tagged element"); // Data
71        w.end_container().expect("infallible: vec writer"); // AttributeDataIB
72    }
73    w.end_container().expect("infallible: vec writer"); // WriteRequests array
74    w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
75        .expect("infallible: vec writer");
76    w.end_container().expect("infallible: vec writer"); // message struct
77    buf
78}
79
80/// Parse a `WriteResponseMessage` into per-path statuses.
81///
82/// The write response carries one `AttributeStatusIB` per written path —
83/// **including the success case** — so the result is a status per path,
84/// not a single message-level status. A message with no `WriteResponses`
85/// member yields an empty result.
86///
87/// # Errors
88///
89/// Returns [`ImError`] if the message is not a struct, an
90/// `AttributeStatusIB` is missing its path or status, or a path value is
91/// out of range.
92pub 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    // Find WriteResponses [0] (array). Absent → empty result.
99    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    // Iterate AttributeStatusIB structs in the array.
112    loop {
113        match r.next()? {
114            None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
115            Some(Element::ContainerEnd) => break, // end of array
116            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
128/// Parse one `AttributeStatusIB` body (reader positioned just after the
129/// struct start): `{ 0: Path(list), 1: StatusIB struct { 0: Status } }`.
130fn 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                // StatusIB = { 0: Status (uint), 1: ClusterStatus (ignored) }
149                let members = read_container_members(r)?;
150                // Last value wins for duplicate tags (lenient parsing); real devices never duplicate Status.
151                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
169/// Reserve for the `MoreChunkedMessages`(ctx3) bool we may add after packing.
170const CHUNK_FLAG_RESERVE: usize = 4;
171
172/// Build one or more `WriteRequestMessage`s that write `element_tlvs` (each a
173/// pre-encoded anonymous-tagged list element) to `path` as a list, splitting
174/// across messages so each stays within `budget` unencrypted bytes.
175///
176/// Chunk 0 is a `ReplaceAll` (path without `ListIndex`; `Data` = an array of
177/// the elements that fit). Remaining elements are emitted as `AppendItem` IBs
178/// (path with `ListIndex`=null). `MoreChunkedMessages` (ctx3) is set on every
179/// message except the last.
180///
181/// When everything fits one message the result is a single `ReplaceAll`
182/// byte-identical to `build_write_request(&[AttributeWriteRequest{path,
183/// value_tlv: <the full array encoded>}])`.
184///
185/// An empty `element_tlvs` yields a single empty-array `ReplaceAll`.
186#[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    // 1) Greedily fill chunk 0's ReplaceAll array.
194    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    // Collect remaining elements as AppendItem batches.
212    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    // 2) Encode each chunk, setting MoreChunkedMessages on all but the last.
233    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/// Encode a `ReplaceAll` `WriteRequestMessage` containing one `AttributeDataIB`
245/// whose `Data` is an anonymous array of `elems`.
246#[allow(clippy::expect_used)] // Vec-backed TlvWriter is infallible.
247fn 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"); // SuppressResponse
259    w.put_bool(Tag::Context(1), timed)
260        .expect("infallible: vec writer"); // TimedRequest
261    w.start_array(Tag::Context(2))
262        .expect("infallible: vec writer"); // WriteRequests
263
264    // One AttributeDataIB — ReplaceAll (no ListIndex in path).
265    w.start_structure(Tag::Anonymous)
266        .expect("infallible: vec writer");
267    w.start_list(Tag::Context(1))
268        .expect("infallible: vec writer"); // Path (AttributePathIB)
269    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"); // Path
276                                                        // Data = anonymous array containing the pre-encoded elements.
277    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"); // Data array
284    w.end_container().expect("infallible: vec writer"); // AttributeDataIB
285
286    w.end_container().expect("infallible: vec writer"); // WriteRequests array
287    if more_chunked {
288        w.put_bool(Tag::Context(3), true)
289            .expect("infallible: vec writer"); // MoreChunkedMessages
290    }
291    w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
292        .expect("infallible: vec writer");
293    w.end_container().expect("infallible: vec writer"); // message struct
294    buf
295}
296
297/// Encode an `AppendItem` `WriteRequestMessage` containing one
298/// `AttributeDataIB` per element — each IB has `ListIndex`=null in its path.
299#[allow(clippy::expect_used)] // Vec-backed TlvWriter is infallible.
300fn 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"); // SuppressResponse
312    w.put_bool(Tag::Context(1), timed)
313        .expect("infallible: vec writer"); // TimedRequest
314    w.start_array(Tag::Context(2))
315        .expect("infallible: vec writer"); // WriteRequests
316
317    for e in elems {
318        w.start_structure(Tag::Anonymous)
319            .expect("infallible: vec writer"); // AttributeDataIB
320        w.start_list(Tag::Context(1))
321            .expect("infallible: vec writer"); // Path (AttributePathIB)
322        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"); // ListIndex=null → AppendItem
329        w.end_container().expect("infallible: vec writer"); // Path
330        w.put_preencoded(Tag::Context(2), e)
331            .expect("infallible: caller passes valid anonymous-tagged elements"); // Data
332        w.end_container().expect("infallible: vec writer"); // AttributeDataIB
333    }
334
335    w.end_container().expect("infallible: vec writer"); // WriteRequests array
336    if more_chunked {
337        w.put_bool(Tag::Context(3), true)
338            .expect("infallible: vec writer"); // MoreChunkedMessages
339    }
340    w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
341        .expect("infallible: vec writer");
342    w.end_container().expect("infallible: vec writer"); // message struct
343    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/// Parse the element TLVs out of a sequence of `WriteRequestMessage`s produced
355/// by [`build_list_write_chunks`], returning them in order.
356///
357/// For a `ReplaceAll` IB (no `ListIndex` in path) the `Data` is an array;
358/// each anonymous array element is re-encoded and pushed. For `AppendItem`
359/// IBs (`ListIndex`=null) the `Data` element (ctx2-tagged) is re-encoded as
360/// anonymous and pushed.
361///
362/// This function is provided for test validation only.
363#[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/// Extract element TLVs from one `WriteRequestMessage` chunk into `out`.
373///
374/// Uses `read_value` to decode each `AttributeDataIB` as a typed `Value`,
375/// then walks the structure to extract elements without needing raw-byte seeks.
376#[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    // Enter anonymous message struct.
381    let Ok(Some(Element::ContainerStart {
382        tag: Tag::Anonymous,
383        kind: ContainerKind::Structure,
384    })) = r.next()
385    else {
386        return;
387    };
388
389    // Find WriteRequests (ctx2 array).
390    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    // Iterate AttributeDataIBs — read each as a full Value so we can inspect
405    // the path and data without a forward-only byte-position API.
406    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/// Walk the decoded `AttributeDataIB` members `[(Tag, Value)]` and push
426/// re-encoded anonymous element TLVs into `out`.
427///
428/// An `AttributeDataIB` has:
429/// - `ctx1` → Path (list): may include `ctx5 Null` (`ListIndex`=null) for `AppendItem`
430/// - `ctx2` → Data
431#[cfg(test)]
432#[allow(clippy::expect_used)]
433fn collect_elements_from_ib_members(members: &[(Tag, Value)], out: &mut Vec<Vec<u8>>) {
434    // Determine whether this is ReplaceAll or AppendItem by inspecting path (ctx1).
435    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                // Path is a list: check for ListIndex=null (ctx5).
442                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        // AppendItem: Data is the element itself (re-tagged ctx2 by put_preencoded).
461        // Re-encode it as anonymous-tagged.
462        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        // ReplaceAll: Data is an Array; each element is a list element.
469        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    /// Encode a string as a standalone anonymous TLV element (stand-in for
488    /// a matter-clusters attribute encoder).
489    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, // NodeLabel
503            },
504            value_tlv: anon_string("matter-rust"),
505        }]);
506        let mut r = TlvReader::new(&bytes);
507        // message struct
508        assert!(matches!(
509            r.next().unwrap(),
510            Some(Element::ContainerStart {
511                tag: Tag::Anonymous,
512                kind: ContainerKind::Structure
513            })
514        ));
515        // SuppressResponse [0] = false
516        assert!(matches!(
517            r.next().unwrap(),
518            Some(Element::Scalar {
519                tag: Tag::Context(0),
520                value: Value::Bool(false)
521            })
522        ));
523        // TimedRequest [1] = false
524        assert!(matches!(
525            r.next().unwrap(),
526            Some(Element::Scalar {
527                tag: Tag::Context(1),
528                value: Value::Bool(false)
529            })
530        ));
531        // WriteRequests [2] array
532        assert!(matches!(
533            r.next().unwrap(),
534            Some(Element::ContainerStart {
535                tag: Tag::Context(2),
536                kind: ContainerKind::Array
537            })
538        ));
539        // AttributeDataIB struct
540        assert!(matches!(
541            r.next().unwrap(),
542            Some(Element::ContainerStart {
543                tag: Tag::Anonymous,
544                kind: ContainerKind::Structure
545            })
546        ));
547        // Path [1] list with 2/3/4
548        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    /// Build a `WriteResponseMessage` by hand and parse it back.
579    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(); // WriteResponses
584        for (p, code) in entries {
585            w.start_structure(Tag::Anonymous).unwrap(); // AttributeStatusIB
586            w.start_list(Tag::Context(0)).unwrap(); // Path
587            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(); // StatusIB
592            w.put_uint(Tag::Context(0), u64::from(*code)).unwrap();
593            w.end_container().unwrap();
594            w.end_container().unwrap(); // AttributeStatusIB
595        }
596        w.end_container().unwrap(); // array
597        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        // AttributeStatusIB with a path but no StatusIB.
624        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        // a small anonymous-tagged struct standing in for an ACL entry
670        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        // Byte-identical to a single ReplaceAll write of the full array.
694        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        // tiny budget forces one element per message
717        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        // all but last carry MoreChunkedMessages (ctx3 == true); last does not
725        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    // test helper: does this WriteRequestMessage carry MoreChunkedMessages(ctx3)=true?
738    fn has_more_chunked(msg: &[u8]) -> bool {
739        use matter_codec::{Element, TlvReader};
740        let mut r = TlvReader::new(msg);
741        // enter the anonymous message struct
742        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            // MoreChunked invariant
765            for (i, c) in chunks.iter().enumerate() {
766                prop_assert_eq!(has_more_chunked(c), i + 1 != chunks.len());
767            }
768        }
769    }
770}