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