Skip to main content

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}