rs_matter/dm/clusters/ota_prov.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//! The OTA Software Update Provider cluster, plus a BDX handler that streams the
19//! offered image to requestors.
20//!
21//! The node hosts the OTA Provider cluster (server role): it answers
22//! `QueryImage` with the location of a newer image (a `bdx://` URI), authorizes
23//! the apply via `ApplyUpdateRequest`, and notes completion via
24//! `NotifyUpdateApplied`. The image bytes are served over BDX by
25//! [`OtaBdxHandler`], a [`BdxHandler`] that the application wraps in a
26//! [`Bdx`](crate::bdx::Bdx) handler and chains into its responder for the BDX
27//! protocol. The two delegate to separate user-supplied sources - the cluster
28//! handler to an [`OtaImagesRegistry`] (which image to offer), the BDX handler to
29//! an [`OtaImages`] (the image bytes) - so either can be used on its own.
30//!
31//! Update *policy* - including whether a user must consent before an image is
32//! applied - lives in those user implementations, not here: the registry decides
33//! per query (see [`OtaImagesRegistry::query`] and
34//! [`OtaImageMeta::user_consent_needed`]), and consent can be layered onto an
35//! existing registry (e.g. the [`dcl`] sample) with a thin wrapping proxy.
36
37use core::fmt::Write as _;
38use core::num::NonZeroU8;
39
40use crate::bdx::{BdxHandler, BdxResponder, BdxStatus};
41use crate::dm::{Cluster, Dataver, InvokeContext};
42use crate::error::{Error, ErrorCode};
43use crate::tlv::{Octets, TLVBuilderParent};
44use crate::transport::exchange::MAX_EXCHANGE_RX_BUF_SIZE;
45use crate::utils::storage::pooled::Buffers;
46use crate::with;
47
48/// The buffer an [`OtaBdxHandler`] stages each BDX block in.
49///
50/// Re-exported from the [`bdx`](crate::bdx) module, where it now lives, so an
51/// application can size a [`PooledBuffers`] pool for the OTA BDX handler without
52/// reaching into the BDX module directly.
53///
54/// [`PooledBuffers`]: crate::utils::storage::pooled::PooledBuffers
55pub use crate::bdx::BdxBuffer;
56
57pub use crate::dm::clusters::decl::ota_software_update_provider::*;
58
59/// A sample [`OtaImagesRegistry`] + [`OtaImages`] implementation backed by the
60/// CSA-IOT Distributed Compliance Ledger and a CDN, over a pluggable HTTPS client.
61#[cfg(feature = "ota-dcl")]
62pub mod dcl;
63
64/// The maximum supported BDX file designator length.
65const MAX_FILE_DESIGNATOR: usize = 128;
66
67/// Metadata describing an OTA image that a provider is willing to offer.
68#[derive(Debug, Clone, Eq, PartialEq, Hash)]
69#[cfg_attr(feature = "defmt", derive(defmt::Format))]
70pub struct OtaImageMeta<'a> {
71 /// The version of the offered image. Must be newer than the requestor's.
72 pub version: u32,
73 /// The BDX file designator that identifies this image when downloaded.
74 pub file_designator: &'a str,
75 /// The opaque `UpdateToken` (8..=32 bytes) the provider assigns to this offer.
76 /// The requestor echoes it verbatim on `ApplyUpdateRequest` /
77 /// `NotifyUpdateApplied`, where it is handed back to
78 /// [`OtaImagesRegistry::apply`] - so a registry can use it to correlate the
79 /// apply/notify phase with this query (e.g. an image id, or a key into its own
80 /// per-flow state). It is *not* used for the download (that's the
81 /// [`file_designator`](Self::file_designator) carried in the `bdx://` URL).
82 pub update_token: &'a [u8],
83 /// The total image size in bytes, if known (enables a definite-length
84 /// transfer and download-progress reporting on the requestor).
85 pub size: Option<u64>,
86 /// Whether the requestor must obtain user consent before applying this image.
87 /// Surfaced to the requestor as `UserConsentNeeded` in the `QueryImage`
88 /// response. See [`OtaImagesRegistry::query`] for how this interacts with the
89 /// requestor's `requestor_can_consent` capability - consent *policy* is the
90 /// registry's to decide.
91 pub user_consent_needed: bool,
92}
93
94/// The outcome of an [`OtaImagesRegistry::query`] - the three `QueryImage`
95/// responses an OTA Requestor acts on (per the Matter spec).
96#[derive(Debug, Clone, Eq, PartialEq, Hash)]
97#[cfg_attr(feature = "defmt", derive(defmt::Format))]
98pub enum OtaQueryOutcome<'a> {
99 /// An applicable image is available; offer it. Maps to `Status =
100 /// UpdateAvailable`.
101 Available(OtaImageMeta<'a>),
102 /// The provider may have an update but cannot answer definitively yet - e.g.
103 /// it is still determining availability, or awaiting user consent it obtains
104 /// itself. Maps to `Status = Busy`: the requestor retries the *same* provider
105 /// after at least `delay_secs` (never sooner than the spec's 120-second floor).
106 Busy {
107 /// Minimum seconds before the requestor re-queries (`DelayedActionTime`).
108 delay_secs: u32,
109 },
110 /// Definitely no update is available. Maps to `Status = NotAvailable`: the
111 /// requestor may instead try a different provider.
112 NotAvailable,
113}
114
115/// How the OTA Requestor should proceed with applying an already-downloaded
116/// image, returned from [`OtaImagesRegistry::apply`] in response to
117/// `ApplyUpdateRequest` (per the Matter spec).
118#[derive(Debug, Clone, Eq, PartialEq, Hash)]
119#[cfg_attr(feature = "defmt", derive(defmt::Format))]
120pub enum OtaApplyOutcome {
121 /// Apply now, or after `delay_secs`. Maps to `Action = Proceed`.
122 Proceed {
123 /// Seconds to wait before applying (`DelayedActionTime`; `0` = at once).
124 delay_secs: u32,
125 },
126 /// Not yet: the requestor waits `delay_secs` and re-sends `ApplyUpdateRequest`
127 /// - e.g. provider-side user consent is still pending. Maps to `Action =
128 /// AwaitNextAction` (the requestor enforces a 120-second floor).
129 Await {
130 /// Seconds to wait before asking again (`DelayedActionTime`).
131 delay_secs: u32,
132 },
133 /// Rescind the image; the requestor should discard it. Maps to `Action =
134 /// Discontinue`.
135 Discontinue,
136}
137
138/// A device-specific registry of OTA images: it decides which image (if any) to
139/// offer a querying requestor, and authorizes applying it. Used by
140/// [`OtaProviderHandler`].
141///
142/// # User consent
143///
144/// Update *policy*, including user consent, lives here, not in the cluster
145/// handler. The Matter spec lets a provider obtain consent before offering an
146/// image and/or before letting the requestor apply it; a registry expresses that
147/// at two points (no consent is ever gated during the BDX transfer itself):
148///
149/// - **Delegation** - when the requestor can prompt the user
150/// (`requestor_can_consent`), [`query`](Self::query) may offer the image with
151/// [`OtaImageMeta::user_consent_needed`] set, and the requestor prompts before
152/// downloading.
153/// - **Provider-side, at query** - while the provider obtains consent itself,
154/// [`query`](Self::query) returns [`OtaQueryOutcome::Busy`] so the requestor retries
155/// later (do *not* return [`OtaQueryOutcome::NotAvailable`] - that means "no update").
156/// - **Provider-side, at apply** - [`apply`](Self::apply) returns
157/// [`OtaApplyOutcome::Await`] until consent is granted, then [`OtaApplyOutcome::Proceed`].
158///
159/// A common way to add consent on top of an existing registry (e.g. the [`dcl`]
160/// sample) is a thin proxy that wraps it and overrides these decisions.
161pub trait OtaImagesRegistry {
162 /// Decide what to offer a requestor querying for an image newer than
163 /// `current_version` for `(vendor_id, product_id)`: [`OtaQueryOutcome::Available`]
164 /// with the image to offer, [`OtaQueryOutcome::Busy`] to retry later (e.g. consent
165 /// pending), or [`OtaQueryOutcome::NotAvailable`].
166 ///
167 /// `requestor_can_consent` reports whether the requestor can obtain user
168 /// consent itself; set [`OtaImageMeta::user_consent_needed`] to delegate
169 /// consent to it (only meaningful when it can). See the [trait
170 /// docs](OtaImagesRegistry#user-consent).
171 ///
172 /// The returned [`OtaImageMeta::file_designator`] is written into (and
173 /// borrows) `designator_buf`, so a registry can mint a designator computed at
174 /// runtime rather than being limited to `'static` strings.
175 async fn query<'b>(
176 &self,
177 vendor_id: u16,
178 product_id: u16,
179 current_version: u32,
180 requestor_can_consent: bool,
181 designator_buf: &'b mut [u8],
182 ) -> OtaQueryOutcome<'b>;
183
184 /// Authorize the requestor to apply the already-downloaded image, upgrading to
185 /// `new_version`. `update_token` is the exact [`OtaImageMeta::update_token`]
186 /// this registry assigned in [`query`](Self::query) and the requestor echoed
187 /// back, so the registry can correlate this call with that offer.
188 ///
189 /// The default authorizes an immediate apply. Override it to defer (e.g. until
190 /// provider-side consent is granted, with [`OtaApplyOutcome::Await`]) or to rescind a
191 /// previously offered image ([`OtaApplyOutcome::Discontinue`]).
192 async fn apply(&self, _update_token: &[u8], _new_version: u32) -> OtaApplyOutcome {
193 OtaApplyOutcome::Proceed { delay_secs: 0 }
194 }
195}
196
197impl<T> OtaImagesRegistry for &T
198where
199 T: OtaImagesRegistry,
200{
201 async fn query<'b>(
202 &self,
203 vendor_id: u16,
204 product_id: u16,
205 current_version: u32,
206 requestor_can_consent: bool,
207 designator_buf: &'b mut [u8],
208 ) -> OtaQueryOutcome<'b> {
209 T::query(
210 self,
211 vendor_id,
212 product_id,
213 current_version,
214 requestor_can_consent,
215 designator_buf,
216 )
217 .await
218 }
219
220 async fn apply(&self, update_token: &[u8], new_version: u32) -> OtaApplyOutcome {
221 T::apply(self, update_token, new_version).await
222 }
223}
224
225/// The image bytes behind a BDX file designator: looked up and streamed during a
226/// download. Used by [`OtaBdxHandler`].
227pub trait OtaImages {
228 /// The total size of the image identified by `file_designator`. `None` means
229 /// the designator is unknown and the BDX transfer is rejected
230 /// (`FileDesignatorUnknown`).
231 async fn size(&self, file_designator: &[u8]) -> Option<u64>;
232
233 /// Read up to `buf.len()` bytes of the image identified by `file_designator`
234 /// at `offset`, returning the number of bytes read (`0` marks the end). An
235 /// unknown designator should return an error.
236 async fn read(
237 &self,
238 file_designator: &[u8],
239 offset: u64,
240 buf: &mut [u8],
241 ) -> Result<usize, Error>;
242}
243
244impl<T> OtaImages for &T
245where
246 T: OtaImages,
247{
248 async fn size(&self, file_designator: &[u8]) -> Option<u64> {
249 T::size(self, file_designator).await
250 }
251
252 async fn read(
253 &self,
254 file_designator: &[u8],
255 offset: u64,
256 buf: &mut [u8],
257 ) -> Result<usize, Error> {
258 T::read(self, file_designator, offset, buf).await
259 }
260}
261
262/// The valid `UpdateToken` length range, per the Matter spec.
263const UPDATE_TOKEN_LEN: core::ops::RangeInclusive<usize> = 8..=32;
264
265/// The server-side handler for the OTA Software Update Provider cluster.
266pub struct OtaProviderHandler<I> {
267 dataver: Dataver,
268 images: I,
269}
270
271impl<I> OtaProviderHandler<I> {
272 /// Create a new handler backed by the given image registry.
273 pub const fn new(dataver: Dataver, images: I) -> Self {
274 Self { dataver, images }
275 }
276
277 /// Adapt this handler to the generic `rs-matter` `AsyncHandler` trait.
278 pub const fn adapt(self) -> HandlerAsyncAdaptor<Self> {
279 HandlerAsyncAdaptor(self)
280 }
281}
282
283impl<I: OtaImagesRegistry> ClusterAsyncHandler for OtaProviderHandler<I> {
284 const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_attrs(with!(required));
285
286 fn dataver(&self) -> u32 {
287 self.dataver.get()
288 }
289
290 fn dataver_changed(&self) {
291 self.dataver.changed();
292 }
293
294 async fn handle_query_image<P: TLVBuilderParent>(
295 &self,
296 ctx: impl InvokeContext,
297 request: QueryImageRequest<'_>,
298 response: QueryImageResponseBuilder<P>,
299 ) -> Result<P, Error> {
300 let vendor_id = request.vendor_id()?;
301 let product_id = request.product_id()?;
302 let current_version = request.software_version()?;
303 // Absent means the requestor cannot obtain user consent on its own.
304 let requestor_can_consent = request.requestor_can_consent()?.unwrap_or(false);
305
306 let mut designator_buf = [0u8; MAX_FILE_DESIGNATOR];
307 let image = match self
308 .images
309 .query(
310 vendor_id,
311 product_id,
312 current_version,
313 requestor_can_consent,
314 &mut designator_buf,
315 )
316 .await
317 {
318 OtaQueryOutcome::Available(image) => image,
319 // The provider may have an update but isn't ready (e.g. consent
320 // pending); tell the requestor to retry the same provider later.
321 OtaQueryOutcome::Busy { delay_secs } => {
322 return response
323 .status(StatusEnum::Busy)?
324 .delayed_action_time(Some(delay_secs))?
325 .image_uri(None)?
326 .software_version(None)?
327 .software_version_string(None)?
328 .update_token(None)?
329 .user_consent_needed(None)?
330 .metadata_for_requestor(None)?
331 .end();
332 }
333 // No applicable image (already up to date).
334 OtaQueryOutcome::NotAvailable => {
335 return response
336 .status(StatusEnum::NotAvailable)?
337 .delayed_action_time(None)?
338 .image_uri(None)?
339 .software_version(None)?
340 .software_version_string(None)?
341 .update_token(None)?
342 .user_consent_needed(None)?
343 .metadata_for_requestor(None)?
344 .end();
345 }
346 };
347
348 // The download URI points at this node (on the accessing fabric) and
349 // carries the file designator as its path.
350 let fab_idx = NonZeroU8::new(ctx.cmd().fab_idx).ok_or(ErrorCode::Invalid)?;
351 let node_id = ctx
352 .exchange()
353 .with_state(|state| Ok(state.fabrics.fabric(fab_idx)?.node_id()))?;
354
355 let mut uri = heapless::String::<200>::new();
356 write!(uri, "bdx://{:016X}/{}", node_id, image.file_designator)
357 .map_err(|_| ErrorCode::NoSpace)?;
358
359 let mut version_str = heapless::String::<16>::new();
360 write!(version_str, "{}", image.version).map_err(|_| ErrorCode::NoSpace)?;
361
362 // The registry owns the (opaque) update token; enforce the spec's bound.
363 if !UPDATE_TOKEN_LEN.contains(&image.update_token.len()) {
364 return Err(ErrorCode::ConstraintError.into());
365 }
366
367 response
368 .status(StatusEnum::UpdateAvailable)?
369 .delayed_action_time(None)?
370 .image_uri(Some(uri.as_str()))?
371 .software_version(Some(image.version))?
372 .software_version_string(Some(version_str.as_str()))?
373 .update_token(Some(Octets(image.update_token)))?
374 // Consent policy is the registry's; forward its decision verbatim.
375 .user_consent_needed(Some(image.user_consent_needed))?
376 .metadata_for_requestor(None)?
377 .end()
378 }
379
380 async fn handle_apply_update_request<P: TLVBuilderParent>(
381 &self,
382 _ctx: impl InvokeContext,
383 request: ApplyUpdateRequestRequest<'_>,
384 response: ApplyUpdateResponseBuilder<P>,
385 ) -> Result<P, Error> {
386 let update_token = request.update_token()?;
387 let new_version = request.new_version()?;
388
389 // The registry owns apply policy (e.g. deferring until consent is granted).
390 let (action, delay) = match self.images.apply(update_token.0, new_version).await {
391 OtaApplyOutcome::Proceed { delay_secs } => (ApplyUpdateActionEnum::Proceed, delay_secs),
392 OtaApplyOutcome::Await { delay_secs } => {
393 (ApplyUpdateActionEnum::AwaitNextAction, delay_secs)
394 }
395 OtaApplyOutcome::Discontinue => (ApplyUpdateActionEnum::Discontinue, 0),
396 };
397
398 response.action(action)?.delayed_action_time(delay)?.end()
399 }
400
401 async fn handle_notify_update_applied(
402 &self,
403 _ctx: impl InvokeContext,
404 _request: NotifyUpdateAppliedRequest<'_>,
405 ) -> Result<(), Error> {
406 // Stateless provider: nothing to clean up.
407 Ok(())
408 }
409}
410
411/// A [`BdxHandler`] that serves OTA images. Wrap it in a [`Bdx`](crate::bdx::Bdx)
412/// handler and chain that into your responder for the BDX protocol, so requestors
413/// can download the image advertised by the OTA Provider cluster's `QueryImage`
414/// response.
415///
416/// Given the exchange handler for the rest of your protocols (e.g. the default
417/// Interaction Model + Secure Channel chain), add BDX with
418/// [`ExchangeHandler::chain`](crate::respond::ExchangeHandler::chain):
419///
420/// ```ignore
421/// use rs_matter::bdx::{Bdx, PROTO_ID_BDX};
422/// use rs_matter::dm::clusters::ota_prov::BdxBuffer;
423/// use rs_matter::respond::Responder;
424/// use rs_matter::utils::storage::pooled::PooledBuffers;
425///
426/// // One staging buffer per concurrent download (here: two).
427/// let buffers = PooledBuffers::<BdxBuffer, 2>::new();
428/// let bdx = Bdx::new(OtaBdxHandler::new(&buffers, &images));
429/// let handler = im_and_sc_handler.chain(PROTO_ID_BDX, bdx);
430/// let responder = Responder::new("ota-provider", handler, matter, 0);
431/// ```
432pub struct OtaBdxHandler<B, I> {
433 buffers: B,
434 images: I,
435}
436
437impl<B, I> OtaBdxHandler<B, I> {
438 /// Create a new BDX image handler backed by the given image data source.
439 ///
440 /// `buffers` is a [`Buffers`] pool ([`BdxBuffer`]-sized): one buffer is
441 /// leased per in-flight download to stage the BDX blocks (the image bytes are
442 /// read straight into it), so the pool's size caps how many downloads run
443 /// concurrently. When the pool is exhausted, further downloads are rejected
444 /// with [`ResponderBusy`](BdxStatus::ResponderBusy).
445 pub const fn new(buffers: B, images: I) -> Self {
446 Self { buffers, images }
447 }
448}
449
450impl<B, I: OtaImages> OtaBdxHandler<B, I> {
451 /// Fill `buf` from the image `fd` starting at `offset`, looping until it is
452 /// full or the image ends, so that only the final block of a transfer is ever
453 /// short. Returns the number of bytes read (`< buf.len()` only at end-of-image).
454 async fn fill(&self, fd: &[u8], offset: u64, buf: &mut [u8]) -> Result<usize, Error> {
455 let mut filled = 0;
456
457 while filled < buf.len() {
458 // `checked_add`: a buggy `OtaImages::size` could let a peer's
459 // start offset sit near `u64::MAX`, where `offset + filled` overflows.
460 let read_offset = offset
461 .checked_add(filled as u64)
462 .ok_or(ErrorCode::Invalid)?;
463 let n = self
464 .images
465 .read(fd, read_offset, &mut buf[filled..])
466 .await?;
467 if n == 0 {
468 break;
469 }
470 // Guard a misbehaving `OtaImages::read` that reports reading more than
471 // the slice it was handed - otherwise `filled` overruns `buf` and the
472 // next `&mut buf[filled..]` panics.
473 if n > buf.len() - filled {
474 return Err(ErrorCode::Invalid.into());
475 }
476
477 filled += n;
478 }
479
480 Ok(filled)
481 }
482}
483
484impl<B, I> BdxHandler for OtaBdxHandler<B, I>
485where
486 B: Buffers<BdxBuffer>,
487 I: OtaImages,
488{
489 async fn handles(&self, responder: &BdxResponder<'_>) -> bool {
490 // We only serve downloads, and only of images we actually have.
491 matches!(responder, BdxResponder::Download(_))
492 && self.images.size(responder.fd()).await.is_some()
493 }
494
495 async fn handle(&self, responder: BdxResponder<'_>) -> Result<(), Error> {
496 // We only handle downloads; anything else is rejected.
497 let responder = match responder {
498 BdxResponder::Download(responder) => responder,
499 other => return other.reject(BdxStatus::FileDesignatorUnknown).await,
500 };
501
502 // Copy the requested designator out (the held init is released by
503 // `reply`/`reject`), and reject anything we don't have.
504 let mut fd = heapless::Vec::<u8, MAX_FILE_DESIGNATOR>::new();
505 if fd.extend_from_slice(responder.fd()).is_err() {
506 return responder.reject(BdxStatus::FileDesignatorUnknown).await;
507 }
508
509 let Some(size) = self.images.size(&fd).await else {
510 return responder.reject(BdxStatus::FileDesignatorUnknown).await;
511 };
512
513 // Honor a requested resume offset: send from there, advertising only the
514 // remaining bytes. An offset past the end cannot be served.
515 let start_offset = responder.start_offset();
516 if start_offset > size {
517 return responder.reject(BdxStatus::StartOffsetNotSupported).await;
518 }
519 let remaining = size - start_offset;
520
521 // Lease a staging buffer for the duration of this transfer; if the pool is
522 // exhausted, tell the peer we are busy so it can retry later.
523 let Some(mut buf) = self.buffers.get().await else {
524 return responder.reject(BdxStatus::ResponderBusy).await;
525 };
526
527 // Expose the whole buffer as the writer's block-staging slice (its length
528 // caps the block size, which the BDX layer further clamps to the TX limit).
529 unwrap!(buf.resize_default(MAX_EXCHANGE_RX_BUF_SIZE));
530
531 // Hand it to the writer, which sends each block straight out of it - the
532 // image bytes are read directly into the writer's block buffer, no copy.
533 let mut writer = responder.reply(buf.as_mut_slice(), Some(remaining)).await?;
534
535 let mut offset = start_offset;
536
537 loop {
538 let n = self.fill(&fd, offset, writer.block_buf()).await?;
539 if n == 0 {
540 break;
541 }
542
543 writer.commit(n).await?;
544
545 offset += n as u64;
546 }
547
548 writer.finish().await
549 }
550}