#![forbid(unsafe_code)]
use crate::error::ImError;
use crate::path::AttributePath;
#[cfg(test)]
use crate::read_container_members;
use crate::status::ImStatus;
use crate::{expect_message_struct, skip_container, IM_REVISION};
use matter_codec::{ContainerKind, Element, Tag, TlvReader, TlvWriter, Value};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AttributeWriteRequest {
pub path: AttributePath,
pub value_tlv: Vec<u8>,
}
#[must_use]
pub fn build_write_request(writes: &[AttributeWriteRequest]) -> Vec<u8> {
build_write_request_inner(writes, false)
}
#[must_use]
pub fn build_write_request_timed(writes: &[AttributeWriteRequest]) -> Vec<u8> {
build_write_request_inner(writes, true)
}
#[allow(clippy::expect_used)] fn build_write_request_inner(writes: &[AttributeWriteRequest], timed: bool) -> Vec<u8> {
let mut buf = Vec::with_capacity(
48 + writes
.iter()
.map(|wr| 24 + wr.value_tlv.len())
.sum::<usize>(),
);
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
w.put_bool(Tag::Context(0), false)
.expect("infallible: vec writer"); w.put_bool(Tag::Context(1), timed)
.expect("infallible: vec writer"); w.start_array(Tag::Context(2))
.expect("infallible: vec writer"); for wr in writes {
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer"); w.start_list(Tag::Context(1))
.expect("infallible: vec writer"); w.put_uint(Tag::Context(2), u64::from(wr.path.endpoint))
.expect("infallible: vec writer");
w.put_uint(Tag::Context(3), u64::from(wr.path.cluster))
.expect("infallible: vec writer");
w.put_uint(Tag::Context(4), u64::from(wr.path.attribute))
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer"); w.put_preencoded(Tag::Context(2), &wr.value_tlv)
.expect("infallible: caller passes a valid anonymous-tagged element"); w.end_container().expect("infallible: vec writer"); }
w.end_container().expect("infallible: vec writer"); w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer"); buf
}
pub fn parse_write_response(bytes: &[u8]) -> Result<Vec<(AttributePath, ImStatus)>, ImError> {
let mut r = TlvReader::new(bytes);
expect_message_struct(&mut r)?;
let mut out = Vec::new();
loop {
match r.next()? {
None | Some(Element::ContainerEnd) => return Ok(out),
Some(Element::ContainerStart {
tag: Tag::Context(0),
kind: ContainerKind::Array,
}) => break,
Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
Some(_) => {}
}
}
loop {
match r.next()? {
None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
Some(Element::ContainerEnd) => break, Some(Element::ContainerStart {
kind: ContainerKind::Structure,
..
}) => out.push(parse_attribute_status_ib(&mut r)?),
Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
Some(_) => {}
}
}
Ok(out)
}
pub(crate) fn parse_attribute_status_ib(
r: &mut TlvReader<'_>,
) -> Result<(AttributePath, ImStatus), ImError> {
let mut path = None;
let mut status = None;
loop {
match r.next()? {
None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
Some(Element::ContainerEnd) => break,
Some(Element::ContainerStart {
tag: Tag::Context(0),
kind: ContainerKind::List,
}) => {
let (p, _) = crate::path::attribute_path_from_reader(r)?;
path = Some(p);
}
Some(Element::ContainerStart {
tag: Tag::Context(1),
kind: ContainerKind::Structure,
}) => {
if let Some(s) = parse_status_ib_body(r)? {
status = Some(s);
}
}
Some(Element::ContainerStart { .. }) => skip_container(r)?,
Some(_) => {}
}
}
Ok((
path.ok_or(ImError::MissingField("AttributeStatusIB.Path"))?,
status.ok_or(ImError::MissingField("AttributeStatusIB.Status"))?,
))
}
fn parse_status_ib_body(r: &mut TlvReader<'_>) -> Result<Option<ImStatus>, ImError> {
let mut status = None;
loop {
match r.next()? {
None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
Some(Element::ContainerEnd) => return Ok(status),
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Uint(n),
}) => {
let code = u8::try_from(n).map_err(|_| ImError::InvalidStatusCode { code: n })?;
status = Some(ImStatus::from_u8(code));
}
Some(Element::ContainerStart { .. }) => skip_container(r)?,
Some(_) => {}
}
}
}
const CHUNK_FLAG_RESERVE: usize = 4;
#[must_use]
pub fn build_list_write_chunks(
path: AttributePath,
element_tlvs: &[Vec<u8>],
budget: usize,
timed: bool,
) -> Vec<Vec<u8>> {
const PROBE: &[u8] = &[0x14];
let replace_base = encoded_replace_all_len(path, &[], timed);
let append_base = encoded_append_len(path, &[], timed);
let append_per_elem_overhead =
encoded_append_len(path, &[PROBE], timed) - append_base - PROBE.len();
let mut idx = 0usize;
let mut first_batch: Vec<&[u8]> = Vec::new();
let mut size = replace_base;
while idx < element_tlvs.len() {
let cost = element_tlvs[idx].len();
if size + cost + CHUNK_FLAG_RESERVE > budget && !first_batch.is_empty() {
break;
}
size += cost;
first_batch.push(element_tlvs[idx].as_slice());
idx += 1;
}
let mut append_batches: Vec<Vec<&[u8]>> = Vec::new();
while idx < element_tlvs.len() {
let mut batch: Vec<&[u8]> = Vec::new();
let mut size = append_base;
while idx < element_tlvs.len() {
let cost = append_per_elem_overhead + element_tlvs[idx].len();
if size + cost + CHUNK_FLAG_RESERVE > budget && !batch.is_empty() {
break;
}
size += cost;
batch.push(element_tlvs[idx].as_slice());
idx += 1;
}
append_batches.push(batch);
}
let total = 1 + append_batches.len();
let mut messages: Vec<Vec<u8>> = Vec::with_capacity(total);
let chunked = total > 1;
let first_more = if chunked { Some(true) } else { None };
messages.push(encode_replace_all(path, &first_batch, timed, first_more));
for (i, batch) in append_batches.iter().enumerate() {
let more = Some(i + 1 < append_batches.len());
messages.push(encode_append_items(path, batch, timed, more));
}
messages
}
#[allow(clippy::expect_used)] fn encode_replace_all(
path: AttributePath,
elems: &[&[u8]],
timed: bool,
more_chunked: Option<bool>,
) -> Vec<u8> {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
w.put_bool(Tag::Context(0), false)
.expect("infallible: vec writer"); w.put_bool(Tag::Context(1), timed)
.expect("infallible: vec writer"); w.start_array(Tag::Context(2))
.expect("infallible: vec writer");
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
w.start_list(Tag::Context(1))
.expect("infallible: vec writer"); w.put_uint(Tag::Context(2), u64::from(path.endpoint))
.expect("infallible: vec writer");
w.put_uint(Tag::Context(3), u64::from(path.cluster))
.expect("infallible: vec writer");
w.put_uint(Tag::Context(4), u64::from(path.attribute))
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer"); w.start_array(Tag::Context(2))
.expect("infallible: vec writer");
for e in elems {
w.put_preencoded(Tag::Anonymous, e)
.expect("infallible: caller passes valid anonymous-tagged elements");
}
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 {
w.put_bool(Tag::Context(3), v)
.expect("infallible: vec writer"); }
w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer"); buf
}
#[allow(clippy::expect_used)] fn encode_append_items(
path: AttributePath,
elems: &[&[u8]],
timed: bool,
more_chunked: Option<bool>,
) -> Vec<u8> {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
w.put_bool(Tag::Context(0), false)
.expect("infallible: vec writer"); w.put_bool(Tag::Context(1), timed)
.expect("infallible: vec writer"); w.start_array(Tag::Context(2))
.expect("infallible: vec writer");
for e in elems {
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer"); w.start_list(Tag::Context(1))
.expect("infallible: vec writer"); w.put_uint(Tag::Context(2), u64::from(path.endpoint))
.expect("infallible: vec writer");
w.put_uint(Tag::Context(3), u64::from(path.cluster))
.expect("infallible: vec writer");
w.put_uint(Tag::Context(4), u64::from(path.attribute))
.expect("infallible: vec writer");
w.put_null(Tag::Context(5)).expect("infallible: vec writer"); w.end_container().expect("infallible: vec writer"); w.put_preencoded(Tag::Context(2), e)
.expect("infallible: caller passes valid anonymous-tagged elements"); w.end_container().expect("infallible: vec writer"); }
w.end_container().expect("infallible: vec writer"); if let Some(v) = more_chunked {
w.put_bool(Tag::Context(3), v)
.expect("infallible: vec writer"); }
w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer"); buf
}
fn encoded_replace_all_len(path: AttributePath, elems: &[&[u8]], timed: bool) -> usize {
encode_replace_all(path, elems, timed, None).len()
}
fn encoded_append_len(path: AttributePath, elems: &[&[u8]], timed: bool) -> usize {
encode_append_items(path, elems, timed, None).len()
}
#[cfg(test)]
pub(crate) fn reassemble_list_write(chunks: &[Vec<u8>]) -> Vec<Vec<u8>> {
let mut out = Vec::new();
for chunk in chunks {
collect_elements_from_chunk(chunk, &mut out);
}
out
}
#[cfg(test)]
#[allow(clippy::expect_used)]
fn collect_elements_from_chunk(chunk: &[u8], out: &mut Vec<Vec<u8>>) {
let mut r = TlvReader::new(chunk);
let Ok(Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure,
})) = r.next()
else {
return;
};
loop {
match r.next() {
Ok(Some(Element::ContainerStart {
tag: Tag::Context(2),
kind: ContainerKind::Array,
})) => break,
Ok(Some(Element::ContainerStart { .. })) => {
let _ = skip_container(&mut r);
}
Ok(Some(Element::ContainerEnd) | None) | Err(_) => return,
Ok(Some(_)) => {}
}
}
loop {
match r.next() {
Ok(Some(Element::ContainerStart {
kind: ContainerKind::Structure,
..
})) => {
if let Ok(members) = read_container_members(&mut r) {
collect_elements_from_ib_members(&members, out);
}
}
Ok(Some(Element::ContainerEnd) | None) => break,
Ok(Some(Element::ContainerStart { .. })) => {
let _ = skip_container(&mut r);
}
Ok(Some(_)) | Err(_) => {}
}
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
fn collect_elements_from_ib_members(members: &[(Tag, Value)], out: &mut Vec<Vec<u8>>) {
let mut is_append = false;
let mut data_value: Option<&Value> = None;
for (tag, value) in members {
match tag {
Tag::Context(1) => {
if let Value::List(path_members) = value {
for (pt, pv) in path_members {
if *pt == Tag::Context(5) && *pv == Value::Null {
is_append = true;
}
}
}
}
Tag::Context(2) => {
data_value = Some(value);
}
_ => {}
}
}
let Some(data) = data_value else { return };
if is_append {
let mut elem_bytes = Vec::new();
let mut w = TlvWriter::new(&mut elem_bytes);
w.write_value(Tag::Anonymous, data)
.expect("infallible: vec writer");
out.push(elem_bytes);
} else {
if let Value::Array(elems) = data {
for elem in elems {
let mut elem_bytes = Vec::new();
let mut w = TlvWriter::new(&mut elem_bytes);
w.write_value(Tag::Anonymous, elem)
.expect("infallible: vec writer");
out.push(elem_bytes);
}
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use matter_codec::{ContainerKind, Element, Tag, TlvReader, TlvWriter, Value};
fn anon_string(s: &str) -> Vec<u8> {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.put_utf8(Tag::Anonymous, s).unwrap();
buf
}
#[test]
fn write_request_has_expected_structure() {
let bytes = build_write_request(&[AttributeWriteRequest {
path: AttributePath {
endpoint: 0,
cluster: 0x28,
attribute: 0x05, },
value_tlv: anon_string("matter-rust"),
}]);
let mut r = TlvReader::new(&bytes);
assert!(matches!(
r.next().unwrap(),
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Bool(false)
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::Scalar {
tag: Tag::Context(1),
value: Value::Bool(false)
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::ContainerStart {
tag: Tag::Context(2),
kind: ContainerKind::Array
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::ContainerStart {
tag: Tag::Context(1),
kind: ContainerKind::List
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::Scalar {
tag: Tag::Context(2),
value: Value::Uint(0)
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::Scalar {
tag: Tag::Context(3),
value: Value::Uint(0x28)
})
));
assert!(matches!(
r.next().unwrap(),
Some(Element::Scalar {
tag: Tag::Context(4),
value: Value::Uint(0x05)
})
));
}
fn echo_write_response(entries: &[(AttributePath, u8)]) -> Vec<u8> {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.start_array(Tag::Context(0)).unwrap(); for (p, code) in entries {
w.start_structure(Tag::Anonymous).unwrap(); w.start_list(Tag::Context(0)).unwrap(); w.put_uint(Tag::Context(2), u64::from(p.endpoint)).unwrap();
w.put_uint(Tag::Context(3), u64::from(p.cluster)).unwrap();
w.put_uint(Tag::Context(4), u64::from(p.attribute)).unwrap();
w.end_container().unwrap();
w.start_structure(Tag::Context(1)).unwrap(); w.put_uint(Tag::Context(0), u64::from(*code)).unwrap();
w.end_container().unwrap();
w.end_container().unwrap(); }
w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
buf
}
#[test]
fn parses_success_and_failure_statuses() {
let p1 = AttributePath {
endpoint: 0,
cluster: 0x28,
attribute: 0x05,
};
let p2 = AttributePath {
endpoint: 0,
cluster: 0x28,
attribute: 0x06,
};
let msg = echo_write_response(&[(p1, 0x00), (p2, 0x01)]);
let statuses = parse_write_response(&msg).unwrap();
assert_eq!(statuses.len(), 2);
assert_eq!(statuses[0], (p1, ImStatus::Success));
assert_eq!(statuses[1], (p2, ImStatus::Failure(0x01)));
}
#[test]
fn missing_status_is_an_error() {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.start_array(Tag::Context(0)).unwrap();
w.start_structure(Tag::Anonymous).unwrap();
w.start_list(Tag::Context(0)).unwrap();
w.put_uint(Tag::Context(2), 0).unwrap();
w.put_uint(Tag::Context(3), 0x28).unwrap();
w.put_uint(Tag::Context(4), 0x05).unwrap();
w.end_container().unwrap();
w.end_container().unwrap();
w.end_container().unwrap();
w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
let result = parse_write_response(&buf);
assert!(
matches!(
result,
Err(ImError::MissingField("AttributeStatusIB.Status"))
),
"expected MissingField, got {result:?}"
);
}
#[test]
fn empty_message_yields_empty_statuses() {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_uint(Tag::Context(0xFF), 11).unwrap();
w.end_container().unwrap();
let statuses = parse_write_response(&buf).unwrap();
assert!(statuses.is_empty());
}
fn parse_status(build: impl FnOnce(&mut TlvWriter<'_>)) -> Result<Option<ImStatus>, ImError> {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
build(&mut w);
w.end_container().unwrap();
let mut r = TlvReader::new(&buf);
assert!(matches!(
r.next().unwrap(),
Some(Element::ContainerStart { .. })
));
parse_status_ib_body(&mut r)
}
#[test]
fn status_ib_body_parses_status_none_and_range_error() {
assert!(matches!(
parse_status(|w| w.put_uint(Tag::Context(0), 0).unwrap()),
Ok(Some(ImStatus::Success))
));
assert!(matches!(parse_status(|_| {}), Ok(None)));
assert!(matches!(
parse_status(|w| w.put_uint(Tag::Context(0), 0x1_00).unwrap()),
Err(ImError::InvalidStatusCode { code: 0x100 })
));
assert!(matches!(
parse_status(|w| {
w.start_structure(Tag::Context(7)).unwrap();
w.put_uint(Tag::Context(0), 9).unwrap();
w.end_container().unwrap();
w.put_uint(Tag::Context(0), 0).unwrap();
}),
Ok(Some(ImStatus::Success))
));
}
}
#[cfg(test)]
mod chunk_tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use matter_codec::{Tag, TlvWriter, Value};
use proptest::prelude::*;
fn entry_tlv(n: u64) -> Vec<u8> {
let mut b = Vec::new();
let mut w = TlvWriter::new(&mut b);
w.write_value(
Tag::Anonymous,
&Value::Structure(vec![(Tag::Context(1), Value::Uint(n))]),
)
.unwrap();
b
}
fn p() -> AttributePath {
AttributePath {
endpoint: 0,
cluster: 0x001F,
attribute: 0x0000,
}
}
fn build_list_write_chunks_reference(
path: AttributePath,
element_tlvs: &[Vec<u8>],
budget: usize,
timed: bool,
) -> Vec<Vec<u8>> {
let mut idx = 0usize;
let mut first_batch: Vec<&[u8]> = Vec::new();
while idx < element_tlvs.len() {
let candidate: Vec<&[u8]> = first_batch
.iter()
.copied()
.chain(std::iter::once(element_tlvs[idx].as_slice()))
.collect();
if encoded_replace_all_len(path, &candidate, timed) + CHUNK_FLAG_RESERVE > budget
&& !first_batch.is_empty()
{
break;
}
first_batch.push(element_tlvs[idx].as_slice());
idx += 1;
}
let mut append_batches: Vec<Vec<&[u8]>> = Vec::new();
while idx < element_tlvs.len() {
let mut batch: Vec<&[u8]> = Vec::new();
while idx < element_tlvs.len() {
let candidate: Vec<&[u8]> = batch
.iter()
.copied()
.chain(std::iter::once(element_tlvs[idx].as_slice()))
.collect();
if encoded_append_len(path, &candidate, timed) + CHUNK_FLAG_RESERVE > budget
&& !batch.is_empty()
{
break;
}
batch.push(element_tlvs[idx].as_slice());
idx += 1;
}
append_batches.push(batch);
}
let total = 1 + append_batches.len();
let mut messages: Vec<Vec<u8>> = Vec::with_capacity(total);
let chunked = total > 1;
let first_more = if chunked { Some(true) } else { None };
messages.push(encode_replace_all(path, &first_batch, timed, first_more));
for (i, batch) in append_batches.iter().enumerate() {
let more = Some(i + 1 < append_batches.len());
messages.push(encode_append_items(path, batch, timed, more));
}
messages
}
proptest! {
#[test]
fn incremental_packer_matches_reference(
lens in proptest::collection::vec(0usize..120, 0..30),
budget in 60usize..600,
timed: bool,
) {
let elems: Vec<Vec<u8>> = lens.iter().map(|&n| {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.put_bytes(Tag::Anonymous, &vec![0x5A; n]).unwrap();
buf
}).collect();
let p = p(); prop_assert_eq!(
build_list_write_chunks(p, &elems, budget, timed),
build_list_write_chunks_reference(p, &elems, budget, timed)
);
}
}
#[test]
fn single_chunk_equals_replace_all_build_write_request() {
let elems = vec![entry_tlv(1), entry_tlv(2)];
let chunks = build_list_write_chunks(p(), &elems, 4096, false);
assert_eq!(chunks.len(), 1);
let mut arr = Vec::new();
let mut w = TlvWriter::new(&mut arr);
w.write_value(
Tag::Anonymous,
&Value::Array(vec![
Value::Structure(vec![(Tag::Context(1), Value::Uint(1))]),
Value::Structure(vec![(Tag::Context(1), Value::Uint(2))]),
]),
)
.unwrap();
let expected = build_write_request(&[AttributeWriteRequest {
path: p(),
value_tlv: arr,
}]);
assert_eq!(
chunks[0], expected,
"single-chunk output must be byte-identical to build_write_request"
);
}
#[test]
fn overflow_splits_and_sets_more_chunked() {
let elems = vec![entry_tlv(1), entry_tlv(2), entry_tlv(3)];
let chunks = build_list_write_chunks(p(), &elems, 40, false);
assert!(
chunks.len() >= 2,
"expected multiple chunks, got {}",
chunks.len()
);
for (i, c) in chunks.iter().enumerate() {
assert_eq!(
more_chunked_flag(c),
Some(i + 1 != chunks.len()),
"chunk {i}"
);
}
}
#[test]
fn reassemble_roundtrips() {
let elems: Vec<Vec<u8>> = (0..7).map(entry_tlv).collect();
let chunks = build_list_write_chunks(p(), &elems, 48, false);
assert_eq!(reassemble_list_write(&chunks), elems);
}
#[test]
fn multi_chunk_carries_explicit_flag_final_chunk_explicit_false() {
let elems = vec![entry_tlv(1), entry_tlv(2), entry_tlv(3)];
let chunks = build_list_write_chunks(p(), &elems, 40, false);
assert_eq!(
chunks.len(),
3,
"expected exactly 3 chunks, got {}",
chunks.len()
);
assert_eq!(more_chunked_flag(&chunks[0]), Some(true), "chunk 0");
assert_eq!(more_chunked_flag(&chunks[1]), Some(true), "chunk 1");
assert_eq!(
more_chunked_flag(&chunks[2]),
Some(false),
"final chunk must carry an EXPLICIT MoreChunkedMessages=false, not omit it"
);
let single = build_list_write_chunks(p(), &[entry_tlv(1)], 4096, false);
assert_eq!(single.len(), 1);
assert_eq!(
more_chunked_flag(&single[0]),
None,
"single-chunk output must omit MoreChunkedMessages entirely"
);
}
fn more_chunked_flag(msg: &[u8]) -> Option<bool> {
use matter_codec::{Element, TlvReader};
let mut r = TlvReader::new(msg);
let _ = r.next();
loop {
match r.next() {
Ok(Some(Element::Scalar {
tag: Tag::Context(3),
value: Value::Bool(b),
})) => return Some(b),
Ok(Some(Element::ContainerStart { .. })) => {
let _ = super::skip_container(&mut r);
}
Ok(Some(Element::ContainerEnd) | None) | Err(_) => return None,
Ok(Some(_)) => {}
}
}
}
proptest! {
#[test]
fn split_reassemble_identity(count in 0usize..30, budget in 30usize..200) {
let elems: Vec<Vec<u8>> = (0..count as u64).map(entry_tlv).collect();
let chunks = build_list_write_chunks(p(), &elems, budget, false);
prop_assert_eq!(reassemble_list_write(&chunks), elems.clone());
for (i, c) in chunks.iter().enumerate() {
let expected = if chunks.len() > 1 {
Some(i + 1 != chunks.len())
} else {
None
};
prop_assert_eq!(more_chunked_flag(c), expected);
}
}
}
}