rs_matter/dm/clusters/user_label.rs
1/*
2 *
3 * Copyright (c) 2026 Project CHIP Authors
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18//! UserLabel cluster handler (Matter Application Cluster spec).
19//!
20//! The UserLabel cluster lets a commissioner persist a free-form list of
21//! `(label, value)` string pairs on an endpoint — typical use is the
22//! commissioner letting the user tag a device with metadata such as
23//! `"room"` → `"living room"` or `"orientation"` → `"east"`. The list is
24//! per-endpoint and *not* fabric-scoped: every fabric that has access
25//! sees the same list (subject to the usual ACL rules — `LabelList` reads
26//! at view-privilege, writes at manage-privilege).
27//!
28//! Spec requires the `LabelList` to persist across reboots.
29//! Persistence is implemented by [`UserLabels`], a registry that holds
30//! every endpoint's `LabelList` and serialises the lot under a single
31//! KV key ([`USER_LABELS_KEY`]) on every mutation. Each endpoint that
32//! advertises the UserLabel cluster gets its own [`UserLabelHandler`]
33//! facade (so the per-cluster-instance `Dataver` stays granular per
34//! Matter Core spec), and all facades share one `UserLabels`
35//! by reference.
36//!
37//! Application wiring:
38//!
39//! ```ignore
40//! // One registry, sized for up to `E` endpoints, each holding up to `N` labels.
41//! let labels = UserLabels::<2, 4>::new();
42//!
43//! let ep0_handler = UserLabelHandler::new(Dataver::new_rand(rand), 0, &labels);
44//! let ep1_handler = UserLabelHandler::new(Dataver::new_rand(rand), 1, &labels);
45//!
46//! // ... and once the `InteractionModel` is constructed, re-hydrate the registry
47//! // (and every other persistent cluster) by delivering the `Startup` lifecycle op:
48//! im.lifecycle(LifecycleOp::Startup).await?;
49//! ```
50
51use crate::dm::{
52 ArrayAttributeRead, ArrayAttributeWrite, Cluster, Dataver, EndptId, HandlerContext,
53 LifecycleOp, ReadContext, WriteContext,
54};
55use crate::error::{Error, ErrorCode};
56use crate::persist::{KvBlobStore, KvBlobStoreAccess, Persist};
57use crate::tlv::{FromTLV, TLVArray, TLVBuilderParent, TLVElement, TLVTag, TLVWrite, ToTLV, TLV};
58use crate::utils::cell::RefCell;
59use crate::utils::init::{init, Init};
60use crate::utils::storage::Vec;
61use crate::utils::sync::blocking::Mutex;
62use crate::with;
63
64pub use crate::dm::clusters::decl::globals::{
65 LabelStruct, LabelStructArrayBuilder, LabelStructBuilder,
66};
67pub use crate::dm::clusters::decl::user_label::*;
68pub use crate::persist::USER_LABELS_KEY;
69
70/// Cluster metadata exposed by [`UserLabelHandler`] regardless of the
71/// const-generic parameters.
72///
73/// Exposed as a free constant so callers don't have to spell out the
74/// generic parameters of [`UserLabelHandler`] when they just want the
75/// cluster ID for an `EpClMatcher` or a `clusters!(...)` literal.
76pub const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_attrs(with!(required));
77
78/// Maximum length of a single `label` string, in characters.
79/// Per Matter Application Cluster spec (`LabelStruct`): 16 chars.
80pub const MAX_LABEL_LEN: usize = 16;
81
82/// Maximum length of a single `value` string, in characters.
83/// Per Matter Application Cluster spec (`LabelStruct`): 16 chars.
84pub const MAX_VALUE_LEN: usize = 16;
85
86/// One entry in a `LabelList`.
87///
88/// Named struct (rather than a tuple) so the `derive(FromTLV, ToTLV)`
89/// pair gives us a stable persisted shape for free — the on-disk
90/// representation matches the Matter spec's `LabelStruct` field layout
91/// (`label` at tag 0, `value` at tag 1).
92#[derive(Debug, Clone, PartialEq, Eq, FromTLV, ToTLV)]
93pub struct LabelEntry {
94 pub label: heapless::String<MAX_LABEL_LEN>,
95 pub value: heapless::String<MAX_VALUE_LEN>,
96}
97
98/// One slot in the persisted [`UserLabels`] blob: the `LabelList` for a
99/// specific endpoint. The TLV impls are hand-rolled (rather than
100/// `#[derive(FromTLV, ToTLV)]`) because the derive macro doesn't yet
101/// support const-generic structs. Wire format is a struct with two
102/// context-tagged fields:
103/// - `0`: `endpoint_id` (`u16`)
104/// - `1`: `entries` (TLV array of `LabelEntry`)
105///
106/// No spec dictates this layout; it just needs to be internally
107/// consistent between [`Self::to_tlv`] and [`Self::from_tlv`].
108#[derive(Debug, Clone)]
109struct EndpointLabels<const N: usize> {
110 endpoint_id: EndptId,
111 entries: Vec<LabelEntry, N>,
112}
113
114impl<const N: usize> ToTLV for EndpointLabels<N> {
115 fn to_tlv<W: TLVWrite>(&self, tag: &TLVTag, mut tw: W) -> Result<(), Error> {
116 tw.start_struct(tag)?;
117 self.endpoint_id.to_tlv(&TLVTag::Context(0), &mut tw)?;
118 self.entries.to_tlv(&TLVTag::Context(1), &mut tw)?;
119 tw.end_container()
120 }
121
122 fn tlv_iter(&self, _tag: TLVTag) -> impl Iterator<Item = Result<TLV<'_>, Error>> {
123 // Not used by `Persist::store_tlv` — that goes through
124 // `to_tlv` above. Returning an empty iterator keeps the trait
125 // bound satisfied without dragging in extra machinery.
126 core::iter::empty()
127 }
128}
129
130impl<'a, const N: usize> FromTLV<'a> for EndpointLabels<N> {
131 fn from_tlv(element: &TLVElement<'a>) -> Result<Self, Error> {
132 let s = element.structure()?;
133 Ok(Self {
134 endpoint_id: EndptId::from_tlv(&s.ctx(0)?)?,
135 entries: Vec::<LabelEntry, N>::from_tlv(&s.ctx(1)?)?,
136 })
137 }
138}
139
140/// Shared registry of every endpoint's UserLabel `LabelList`.
141///
142/// Persisted as a single TLV blob under [`USER_LABELS_KEY`] — every
143/// successful write to any [`UserLabelHandler`] re-serialises the whole
144/// registry and stores it. Read paths take the lock briefly to copy
145/// labels into the TLV builder.
146///
147/// Const generics:
148/// - `E` — max number of endpoints that may have a `LabelList`. Bound
149/// on the in-memory `Vec` of per-endpoint slots.
150/// - `N` — max number of `LabelEntry` rows per endpoint. Bound on each
151/// slot's inner `Vec`. Defaults to 4.
152///
153/// Lock holds are bounded — the `Mutex<RefCell<_>>` is never held
154/// across an `.await`, so the registry is sound under a work-stealing
155/// executor.
156pub struct UserLabels<const E: usize, const N: usize = 4> {
157 state: Mutex<RefCell<Vec<EndpointLabels<N>, E>>>,
158}
159
160impl<const E: usize, const N: usize> UserLabels<E, N> {
161 /// Create an empty registry. It is populated from KV at startup
162 /// via [`Self::load_persist`], driven by the [`LifecycleOp::Startup`]
163 /// lifecycle operation delivered to the data model.
164 ///
165 /// Prefer [`Self::init`] for non-trivial `E` * `N` so the
166 /// registry's storage can be initialised in-place (typically in a
167 /// `StaticCell`) instead of being constructed on the stack and
168 /// moved. The two paths produce structurally identical instances.
169 pub const fn new() -> Self {
170 Self {
171 state: Mutex::new(RefCell::new(Vec::new())),
172 }
173 }
174
175 /// Return an in-place initialiser for an empty registry. Use this
176 /// when `E` * `N` * `size_of::<LabelEntry>` is large enough that
177 /// stack-constructing a [`Self::new`] instance and moving it into
178 /// long-lived storage would overflow the stack or bloat
179 /// `.rodata`. Typical usage:
180 ///
181 /// ```ignore
182 /// static USER_LABELS: StaticCell<UserLabels<8, 4>> = StaticCell::new();
183 /// let user_labels = USER_LABELS.uninit().init_with(UserLabels::init());
184 /// ```
185 pub fn init() -> impl Init<Self> {
186 init!(Self {
187 state <- Mutex::init(RefCell::init(Vec::init())),
188 })
189 }
190
191 /// Re-hydrate the registry from `store` under [`USER_LABELS_KEY`],
192 /// so subsequent reads see the labels written before the last reboot.
193 ///
194 /// Called on startup via the [`LifecycleOp::Startup`] lifecycle operation
195 /// delivered to the [`UserLabelHandler`] instance(s) borrowing this registry.
196 ///
197 /// Missing key (first boot, or persistence cleared) is not an
198 /// error — the registry simply stays empty.
199 pub fn load_persist<S: KvBlobStore>(&self, mut store: S, buf: &mut [u8]) -> Result<(), Error> {
200 let Some(data) = store.load(USER_LABELS_KEY, buf)? else {
201 // No prior persistence — reset to empty so re-calling
202 // `load_persist` after a `remove` of the key behaves
203 // deterministically.
204 self.state.lock(|cell| cell.borrow_mut().clear());
205 return Ok(());
206 };
207
208 let loaded = Vec::<EndpointLabels<N>, E>::from_tlv(&TLVElement::new(data))?;
209
210 self.state.lock(|cell| *cell.borrow_mut() = loaded);
211
212 info!("Loaded UserLabel entries for all endpoints from storage");
213
214 Ok(())
215 }
216
217 /// Reset the registry to empty and remove its persisted blob from `store`
218 /// (under [`USER_LABELS_KEY`]).
219 ///
220 /// Called on factory reset via the [`LifecycleOp::FactoryReset`] lifecycle
221 /// operation delivered to the [`UserLabelHandler`] instance(s) borrowing
222 /// this registry.
223 pub fn reset_persist<S: KvBlobStore>(&self, mut store: S, buf: &mut [u8]) -> Result<(), Error> {
224 self.state.lock(|cell| cell.borrow_mut().clear());
225
226 store.remove(USER_LABELS_KEY, buf)
227 }
228
229 /// Serialise the current registry to `ctx.kv()` under
230 /// [`USER_LABELS_KEY`]. Called from every mutating handler path
231 /// after the in-memory change is committed.
232 fn store_persist<C: WriteContext>(&self, ctx: &C) -> Result<(), Error> {
233 let mut persist = Persist::new(ctx.kv());
234
235 self.state.lock(|cell| {
236 let state = cell.borrow();
237 persist.store_tlv(USER_LABELS_KEY, &*state)
238 })?;
239
240 persist.run()
241 }
242
243 /// Run a closure with read-only access to the `LabelList` for
244 /// `endpoint_id`. The closure receives an empty slice if the
245 /// endpoint has no entries (or isn't registered yet).
246 fn with_entries<R>(
247 &self,
248 endpoint_id: EndptId,
249 f: impl FnOnce(&[LabelEntry]) -> Result<R, Error>,
250 ) -> Result<R, Error> {
251 self.state.lock(|cell| {
252 let state = cell.borrow();
253 match state.iter().find(|slot| slot.endpoint_id == endpoint_id) {
254 Some(slot) => f(slot.entries.as_slice()),
255 None => f(&[]),
256 }
257 })
258 }
259
260 /// Replace this endpoint's entire `LabelList` with the entries
261 /// produced by the provided iterator. The new list is validated
262 /// up front (each entry against the spec length limits) before
263 /// any in-memory state changes; on success the whole registry is
264 /// re-persisted.
265 fn replace_entries<'a, C: WriteContext>(
266 &self,
267 ctx: &C,
268 endpoint_id: EndptId,
269 list: &TLVArray<'a, LabelStruct<'a>>,
270 ) -> Result<(), Error> {
271 // Two-pass validation: count + spec checks first, so a
272 // malformed input never partially mutates the registry.
273 let mut count = 0usize;
274 for entry in list {
275 let entry = entry?;
276 Self::validate_entry(&entry)?;
277 count += 1;
278 if count > N {
279 return Err(ErrorCode::ResourceExhausted.into());
280 }
281 }
282
283 self.state.lock(|cell| {
284 let mut state = cell.borrow_mut();
285 let slot = Self::slot_mut(&mut state, endpoint_id)?;
286 slot.entries.clear();
287 for entry in list {
288 let entry = entry?;
289 let (label, value) = Self::validate_entry(&entry)?;
290 Self::push_into(&mut slot.entries, label, value)?;
291 }
292 Ok::<_, Error>(())
293 })?;
294
295 self.store_persist(ctx)
296 }
297
298 /// Append one entry to this endpoint's `LabelList`. Validates the
299 /// spec length limits, then persists.
300 fn add_entry<'a, C: WriteContext>(
301 &self,
302 ctx: &C,
303 endpoint_id: EndptId,
304 entry: &LabelStruct<'a>,
305 ) -> Result<(), Error> {
306 let (label, value) = Self::validate_entry(entry)?;
307
308 self.state.lock(|cell| {
309 let mut state = cell.borrow_mut();
310 let slot = Self::slot_mut(&mut state, endpoint_id)?;
311 Self::push_into(&mut slot.entries, label, value)
312 })?;
313
314 self.store_persist(ctx)
315 }
316
317 /// Return a mutable reference to the slot for `endpoint_id`,
318 /// inserting an empty slot if necessary.
319 fn slot_mut(
320 state: &mut Vec<EndpointLabels<N>, E>,
321 endpoint_id: EndptId,
322 ) -> Result<&mut EndpointLabels<N>, Error> {
323 if let Some(idx) = state.iter().position(|s| s.endpoint_id == endpoint_id) {
324 return Ok(&mut state[idx]);
325 }
326 state
327 .push(EndpointLabels {
328 endpoint_id,
329 entries: Vec::new(),
330 })
331 .map_err(|_| ErrorCode::ResourceExhausted)?;
332 // `push` succeeded → the new slot is the last element.
333 Ok(state.last_mut().expect("just pushed"))
334 }
335
336 /// Validate a single `LabelStruct` against the spec length limits.
337 /// Returns `ConstraintError` when the entry violates the limits —
338 /// `TestUserLabelClusterConstraints` writes oversized entries and
339 /// expects this exact failure mode.
340 fn validate_entry<'a>(entry: &'a LabelStruct<'a>) -> Result<(&'a str, &'a str), Error> {
341 let label = entry.label()?;
342 let value = entry.value()?;
343 if label.len() > MAX_LABEL_LEN || value.len() > MAX_VALUE_LEN {
344 return Err(ErrorCode::ConstraintError.into());
345 }
346 Ok((label, value))
347 }
348
349 /// Push a `(label, value)` pair into a bounded vec. Returns
350 /// `ResourceExhausted` when the vec is full.
351 fn push_into(list: &mut Vec<LabelEntry, N>, label: &str, value: &str) -> Result<(), Error> {
352 let label = heapless::String::try_from(label).map_err(|_| ErrorCode::ConstraintError)?;
353 let value = heapless::String::try_from(value).map_err(|_| ErrorCode::ConstraintError)?;
354 list.push(LabelEntry { label, value })
355 .map_err(|_| ErrorCode::ResourceExhausted)?;
356 Ok(())
357 }
358}
359
360impl<const E: usize, const N: usize> Default for UserLabels<E, N> {
361 fn default() -> Self {
362 Self::new()
363 }
364}
365
366/// Per-`(endpoint, UserLabel)`-instance handler facade. Owns the
367/// cluster's `Dataver` and is bound to one `endpoint_id`; the actual
368/// state lives in the shared [`UserLabels`] registry the facade points
369/// at. Multiple facades may reference the same registry — that's how
370/// a multi-endpoint device shares one persisted blob.
371pub struct UserLabelHandler<'a, const E: usize, const N: usize = 4> {
372 dataver: Dataver,
373 endpoint_id: EndptId,
374 labels: &'a UserLabels<E, N>,
375}
376
377impl<'a, const E: usize, const N: usize> UserLabelHandler<'a, E, N> {
378 /// Construct a facade for `(endpoint_id, UserLabel)` backed by the
379 /// shared `labels` registry.
380 pub const fn new(dataver: Dataver, endpoint_id: EndptId, labels: &'a UserLabels<E, N>) -> Self {
381 Self {
382 dataver,
383 endpoint_id,
384 labels,
385 }
386 }
387
388 /// Adapt the handler instance to the generic `rs-matter` `Handler` trait.
389 pub const fn adapt(self) -> HandlerAdaptor<Self> {
390 HandlerAdaptor(self)
391 }
392}
393
394impl<const E: usize, const N: usize> ClusterHandler for UserLabelHandler<'_, E, N> {
395 const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_attrs(with!(required));
396
397 fn dataver(&self) -> u32 {
398 self.dataver.get()
399 }
400
401 fn dataver_changed(&self) {
402 self.dataver.changed();
403 }
404
405 fn lifecycle(&self, ctx: impl HandlerContext, op: LifecycleOp) -> Result<(), Error> {
406 ctx.kv().access(|store, buf| match op {
407 LifecycleOp::Startup => self.labels.load_persist(store, buf),
408 LifecycleOp::FactoryReset => self.labels.reset_persist(store, buf),
409 LifecycleOp::FabricRemoval { .. } => Ok(()),
410 })
411 }
412
413 fn label_list<P: TLVBuilderParent>(
414 &self,
415 _ctx: impl ReadContext,
416 builder: ArrayAttributeRead<LabelStructArrayBuilder<P>, LabelStructBuilder<P>>,
417 ) -> Result<P, Error> {
418 self.labels
419 .with_entries(self.endpoint_id, |entries| match builder {
420 ArrayAttributeRead::ReadAll(mut array) => {
421 for entry in entries {
422 array = array
423 .push()?
424 .label(entry.label.as_str())?
425 .value(entry.value.as_str())?
426 .end()?;
427 }
428 array.end()
429 }
430 ArrayAttributeRead::ReadOne(index, item) => {
431 let Some(entry) = entries.get(index as usize) else {
432 return Err(ErrorCode::ConstraintError.into());
433 };
434 item.label(entry.label.as_str())?
435 .value(entry.value.as_str())?
436 .end()
437 }
438 ArrayAttributeRead::ReadNone(array) => array.end(),
439 })
440 }
441
442 fn set_label_list(
443 &self,
444 ctx: impl WriteContext,
445 value: ArrayAttributeWrite<TLVArray<'_, LabelStruct<'_>>, LabelStruct<'_>>,
446 ) -> Result<(), Error> {
447 match value {
448 ArrayAttributeWrite::Replace(list) => {
449 self.labels.replace_entries(&ctx, self.endpoint_id, &list)
450 }
451 ArrayAttributeWrite::Add(entry) => {
452 self.labels.add_entry(&ctx, self.endpoint_id, &entry)
453 }
454 // The Matter Core spec does not yet
455 // support per-element list update / remove writes — the
456 // framework already converts these to `InvalidAction` before
457 // the value reaches us, but match exhaustively to be safe.
458 ArrayAttributeWrite::Update(_, _) | ArrayAttributeWrite::Remove(_) => {
459 Err(ErrorCode::InvalidAction.into())
460 }
461 }
462 }
463}