Skip to main content

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}