Skip to main content

pamoja_update/
update.rs

1//! The rules an update passes before a device will run it.
2//!
3//! The order matters and is deliberate. The signature is checked before the
4//! manifest is interpreted, the manifest is checked before a byte of image is
5//! accepted, and the image is checked before the slot is marked bootable. At no
6//! point is something unverified recorded as usable, so a device interrupted at
7//! any moment comes back up on the last image it confirmed.
8
9use pamoja_security::PublicIdentity;
10
11use crate::error::{Refusal, Result};
12use crate::manifest::{Envelope, Manifest, ID_LEN};
13use crate::slots::{SlotRecord, SlotState, SlotStore};
14use crate::trust::Delegation;
15use crate::verify::ImageVerifier;
16
17/// What the bootloader should do with this boot.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum Boot {
20    /// Nothing new to try; run the confirmed image in this slot.
21    Confirmed(u8),
22    /// A staged image is being tried for the first time. It is now pending, so if
23    /// it does not confirm itself, the next boot will revert.
24    Trying(u8),
25    /// A pending image never confirmed, so it has been failed and will not be
26    /// tried again. Run the fallback.
27    Reverted {
28        /// The slot whose image did not confirm.
29        failed: u8,
30        /// The confirmed slot to run instead.
31        fallback: u8,
32    },
33}
34
35impl Boot {
36    /// Returns the name of the decision, without the slots it names.
37    ///
38    /// The bindings carry a boot decision as an action and its slots, so this is the same
39    /// word in every language, which is what lets one boot be logged the same way
40    /// wherever a device's code is written.
41    ///
42    /// # Returns
43    ///
44    /// One of `"Confirmed"`, `"Trying"`, or `"Reverted"`.
45    pub fn action(self) -> &'static str {
46        match self {
47            Boot::Confirmed(_) => "Confirmed",
48            Boot::Trying(_) => "Trying",
49            Boot::Reverted { .. } => "Reverted",
50        }
51    }
52}
53
54/// Who this device is, and who it trusts to update it.
55#[derive(Clone, Copy, Debug)]
56pub struct Device {
57    /// Who built this device's firmware.
58    pub vendor_id: [u8; ID_LEN],
59    /// What kind of device this is.
60    pub class_id: [u8; ID_LEN],
61    /// The key this device anchors its trust in.
62    ///
63    /// It is the root of every decision about who may update the device, so it is
64    /// used almost never: either to sign releases directly, or to sign a
65    /// [`Delegation`] naming a release key that does. The second arrangement is
66    /// the one to prefer, because it lets the anchor stay somewhere it is hard to
67    /// steal.
68    pub anchor: PublicIdentity,
69}
70
71/// Applies the update rules against a device's slots.
72pub struct Updater<S> {
73    device: Device,
74    store: S,
75    delegation: Option<Delegation>,
76    opened: u64,
77}
78
79impl<S: SlotStore> Updater<S> {
80    /// Creates an updater over a device's slots.
81    ///
82    /// # Arguments
83    ///
84    /// * `device` - the device's identity and trusted author.
85    /// * `store` - where its images live.
86    ///
87    /// # Returns
88    ///
89    /// The updater.
90    pub fn new(device: Device, store: S) -> Self {
91        Self {
92            device,
93            store,
94            delegation: None,
95            opened: 0,
96        }
97    }
98
99    /// Adopts a delegation the device already held, after a restart.
100    ///
101    /// A delegation is small and its envelope is self-authenticating, so the
102    /// simplest place to keep one is wherever the caller already keeps device
103    /// settings. Hand it back here on the way up.
104    ///
105    /// # Arguments
106    ///
107    /// * `envelope` - the stored delegation envelope.
108    /// * `now` - seconds since the Unix epoch, or `None` on a device with no clock.
109    ///
110    /// # Returns
111    ///
112    /// The updater, now accepting releases signed by the delegated key.
113    ///
114    /// # Errors
115    ///
116    /// Returns whatever [`adopt`](Self::adopt) refuses.
117    pub fn with_delegation(mut self, envelope: &[u8], now: Option<u64>) -> Result<Self> {
118        self.adopt(envelope, now)?;
119        Ok(self)
120    }
121
122    /// Takes on a delegation, moving which key may sign this device's updates.
123    ///
124    /// The caller should persist the envelope it just passed, so the device comes
125    /// back up trusting the same key.
126    ///
127    /// # Arguments
128    ///
129    /// * `envelope` - a delegation signed by the device's trust anchor.
130    /// * `now` - seconds since the Unix epoch, or `None` on a device with no clock.
131    ///
132    /// # Returns
133    ///
134    /// The delegation now in force.
135    ///
136    /// # Errors
137    ///
138    /// Returns [`Refusal::Signature`] if it was not signed by the trust anchor,
139    /// [`Refusal::Rollback`] if its epoch does not rise above the one already
140    /// held, so a retired key cannot be reinstated by replay, and
141    /// [`Refusal::Expired`] or [`Refusal::NoClock`] on the same terms as a
142    /// manifest.
143    pub fn adopt(&mut self, envelope: &[u8], now: Option<u64>) -> Result<Delegation> {
144        let delegation = Delegation::open(envelope, &self.device.anchor)?;
145
146        if let Some(held) = self.delegation {
147            if delegation.epoch <= held.epoch {
148                return Err(Refusal::Rollback);
149            }
150        }
151
152        if delegation.expires != 0 {
153            match now {
154                Some(now) if now < delegation.expires => {}
155                Some(_) => return Err(Refusal::Expired),
156                None => return Err(Refusal::NoClock),
157            }
158        }
159
160        // Refuse a delegation naming something that is not a usable key, rather
161        // than adopting it and discovering at the next release that nothing can
162        // sign for this device any more.
163        delegation.signer()?;
164
165        self.delegation = Some(delegation);
166        Ok(delegation)
167    }
168
169    /// Returns the delegation in force, if the device holds one.
170    ///
171    /// # Returns
172    ///
173    /// The delegation, or `None` when releases are signed by the anchor itself.
174    pub fn delegation(&self) -> Option<Delegation> {
175        self.delegation
176    }
177
178    /// Returns the key a manifest must be signed by right now.
179    ///
180    /// # Returns
181    ///
182    /// The delegated release key when one is in force, and the trust anchor
183    /// otherwise.
184    ///
185    /// # Errors
186    ///
187    /// Returns [`Refusal::Signature`] if a held delegation names an unusable key.
188    fn signing_key(&self) -> Result<PublicIdentity> {
189        match self.delegation {
190            Some(delegation) => delegation.signer(),
191            None => Ok(self.device.anchor),
192        }
193    }
194
195    /// Borrows the underlying slot store.
196    ///
197    /// # Returns
198    ///
199    /// The store, for inspecting slot records.
200    pub fn store(&self) -> &S {
201        &self.store
202    }
203
204    /// Returns the highest sequence number any slot holds.
205    ///
206    /// A new manifest must beat this, not merely the running image, so an older
207    /// release cannot be slipped in alongside a newer one that is already staged.
208    /// Failed slots count too: re-releasing a sequence that already failed would
209    /// let a captured image be replayed.
210    ///
211    /// # Returns
212    ///
213    /// The highest sequence number present, or `0` if every slot is empty.
214    ///
215    /// # Errors
216    ///
217    /// Returns a refusal if a slot record cannot be read.
218    pub fn installed_sequence(&self) -> Result<u64> {
219        let mut highest = 0;
220        for slot in 0..self.store.slot_count() {
221            let record = self.store.record(slot)?;
222            if !matches!(record.state, SlotState::Empty | SlotState::Receiving) {
223                highest = highest.max(record.sequence);
224            }
225        }
226        Ok(highest)
227    }
228
229    /// Checks a manifest and opens the slot it names for writing.
230    ///
231    /// # Arguments
232    ///
233    /// * `envelope` - the signed manifest offered to this device.
234    ///
235    /// # Returns
236    ///
237    /// A [`Staging`] ready to take the image, once every check that can be made
238    /// without the image has passed.
239    ///
240    /// # Errors
241    ///
242    /// Returns whatever [`begin_at`](Self::begin_at) refuses. A manifest that
243    /// carries an expiry is refused, because a device with no clock cannot honor
244    /// one; call [`begin_at`](Self::begin_at) with the time if it has one.
245    pub fn begin(&mut self, envelope: &[u8]) -> Result<Staging<'_, S>> {
246        self.begin_at(envelope, None)
247    }
248
249    /// Checks a manifest against the current time and opens the slot it names.
250    ///
251    /// # Arguments
252    ///
253    /// * `envelope` - the signed manifest offered to this device.
254    /// * `now` - seconds since the Unix epoch, or `None` on a device with no
255    ///   clock.
256    ///
257    /// # Returns
258    ///
259    /// A [`Staging`] ready to take the image, once every check that can be made
260    /// without the image has passed.
261    ///
262    /// # Errors
263    ///
264    /// Returns [`Refusal::Signature`] if the envelope is not from the trusted
265    /// author, [`Refusal::WrongDevice`] if it is for a different vendor or class,
266    /// [`Refusal::Expired`] if its expiry has passed, [`Refusal::NoClock`] if it
267    /// expires and `now` is `None`, [`Refusal::Rollback`] if it would not move the
268    /// device forward, [`Refusal::SlotTooSmall`] if the image cannot fit, or
269    /// [`Refusal::WrongState`] if it names the slot the device would fall back to.
270    pub fn begin_at(&mut self, envelope: &[u8], now: Option<u64>) -> Result<Staging<'_, S>> {
271        let manifest = Envelope::decode(envelope)?.verify(&self.signing_key()?)?;
272        self.check(&manifest, now)?;
273        self.open(manifest)
274    }
275
276    /// Runs every check that can be made before a byte of image arrives.
277    fn check(&self, manifest: &Manifest, now: Option<u64>) -> Result<()> {
278        if manifest.vendor_id != self.device.vendor_id || manifest.class_id != self.device.class_id
279        {
280            return Err(Refusal::WrongDevice);
281        }
282
283        // A sequence number cannot protect a device that has been offline a long
284        // time: an attacker can offer it a release genuinely newer than the one it
285        // runs, but old enough to have a known flaw. An expiry bounds that window.
286        if manifest.expires != 0 {
287            match now {
288                Some(now) if now < manifest.expires => {}
289                Some(_) => return Err(Refusal::Expired),
290                None => return Err(Refusal::NoClock),
291            }
292        }
293
294        if manifest.sequence <= self.installed_sequence()? {
295            return Err(Refusal::Rollback);
296        }
297
298        let slot = manifest.storage;
299        if manifest.size > self.store.capacity(slot)? {
300            return Err(Refusal::SlotTooSmall);
301        }
302
303        // Writing over the confirmed image would leave nothing to fall back to, so
304        // an update that names that slot is refused rather than obeyed.
305        if self.store.record(slot)?.state == SlotState::Confirmed {
306            return Err(Refusal::WrongState);
307        }
308
309        Ok(())
310    }
311
312    /// Clears the target slot and opens it for a transfer starting at zero.
313    ///
314    /// Every opening is counted, so a [`Transfer`] detached before it can tell that
315    /// the bytes it hashed may no longer be the ones in the slot.
316    fn open(&mut self, manifest: Manifest) -> Result<Staging<'_, S>> {
317        let slot = manifest.storage;
318        self.opened = self.opened.wrapping_add(1);
319        self.store.erase(slot)?;
320        // Recording the target before any bytes arrive is what makes the transfer
321        // resumable: after a reset the device can tell what it was receiving.
322        self.store.set_record(
323            slot,
324            SlotRecord {
325                state: SlotState::Receiving,
326                sequence: manifest.sequence,
327                size: manifest.size,
328                digest: manifest.digest,
329                written: 0,
330            },
331        )?;
332        Ok(Staging {
333            store: &mut self.store,
334            slot,
335            verifier: ImageVerifier::new(&manifest),
336            manifest,
337            offset: 0,
338            opened: self.opened,
339        })
340    }
341
342    /// Opens a slot for an image, continuing a transfer that was cut off.
343    ///
344    /// A slow radio can spend half an hour on a single image, so a link that drops
345    /// near the end must not mean starting again. If the slot already holds part of
346    /// exactly this image, the transfer picks up where it stopped; anything else
347    /// starts over, because mixing two images produces neither. An image whose last
348    /// byte arrived but which was never settled counts as picking up where it
349    /// stopped, so a reset in that gap costs nothing.
350    ///
351    /// # Arguments
352    ///
353    /// * `envelope` - the signed manifest offered to this device.
354    /// * `now` - seconds since the Unix epoch, or `None` on a device with no clock.
355    ///
356    /// # Returns
357    ///
358    /// A [`Staging`] positioned after whatever already arrived, which
359    /// [`progress`](Staging::progress) reports.
360    ///
361    /// # Errors
362    ///
363    /// Returns whatever [`begin_at`](Self::begin_at) refuses.
364    pub fn resume_at(&mut self, envelope: &[u8], now: Option<u64>) -> Result<Staging<'_, S>> {
365        let manifest = Envelope::decode(envelope)?.verify(&self.signing_key()?)?;
366        self.check(&manifest, now)?;
367        self.reopen(manifest)
368    }
369
370    /// Continues a transfer from where a detached one stopped, carrying its hash.
371    ///
372    /// Every rule [`resume_at`](Self::resume_at) applies is applied again, so a release
373    /// that has been overtaken, has expired, or is no longer signed by the key in force
374    /// is refused mid-transfer just the same. What changes is the cost: when the slot
375    /// still holds exactly what `transfer` left in it, the hash of those bytes is
376    /// carried along rather than rebuilt by reading them back, so taking an image in
377    /// many small calls costs no more than taking it in one. When the slot has been
378    /// opened for anything since, the transfer falls back to reading it back, and the
379    /// digest check at the end still settles every byte.
380    ///
381    /// # Arguments
382    ///
383    /// * `envelope` - the signed manifest the transfer is fulfilling.
384    /// * `now` - seconds since the Unix epoch, or `None` on a device with no clock.
385    /// * `transfer` - what [`Staging::detach`] returned for the last piece.
386    ///
387    /// # Returns
388    ///
389    /// A [`Staging`] positioned after whatever already arrived.
390    ///
391    /// # Errors
392    ///
393    /// Returns whatever [`begin_at`](Self::begin_at) refuses.
394    ///
395    /// # Examples
396    ///
397    /// ```
398    /// use pamoja_security::DeviceIdentity;
399    /// use pamoja_update::{
400    ///     image_digest, Device, Manifest, MemoryStore, PayloadFormat, Updater, ENVELOPE_MAX,
401    ///     STRUCTURE_VERSION,
402    /// };
403    ///
404    /// const VENDOR: [u8; 16] = [10; 16];
405    /// const SOIL_PROBE: [u8; 16] = [14; 16];
406    ///
407    /// let author = DeviceIdentity::from_seed(&[1u8; 32]);
408    /// let image = b"an image taken one piece per call";
409    /// let manifest = Manifest {
410    ///     structure_version: STRUCTURE_VERSION,
411    ///     sequence: 2,
412    ///     vendor_id: VENDOR,
413    ///     class_id: SOIL_PROBE,
414    ///     format: PayloadFormat::Raw,
415    ///     storage: 1,
416    ///     digest: image_digest(image),
417    ///     size: image.len() as u32,
418    ///     expires: 0,
419    /// };
420    /// let mut buf = [0u8; ENVELOPE_MAX];
421    /// let written = manifest.sign(&author, &mut buf).unwrap();
422    /// let envelope = &buf[..written];
423    ///
424    /// let device = Device {
425    ///     vendor_id: VENDOR,
426    ///     class_id: SOIL_PROBE,
427    ///     anchor: author.public(),
428    /// };
429    /// let mut updater = Updater::new(device, MemoryStore::new(2, 4096));
430    /// updater.provision(0, 1).unwrap();
431    ///
432    /// let mut transfer = updater.begin(envelope).unwrap().detach();
433    /// for piece in image.chunks(8) {
434    ///     let mut staging = updater.resume_from(envelope, None, transfer).unwrap();
435    ///     staging.write(piece).unwrap();
436    ///     transfer = staging.detach();
437    /// }
438    /// let staging = updater.resume_from(envelope, None, transfer).unwrap();
439    /// assert_eq!(staging.finish().unwrap(), 1);
440    /// ```
441    pub fn resume_from(
442        &mut self,
443        envelope: &[u8],
444        now: Option<u64>,
445        transfer: Transfer,
446    ) -> Result<Staging<'_, S>> {
447        let manifest = Envelope::decode(envelope)?.verify(&self.signing_key()?)?;
448        self.check(&manifest, now)?;
449
450        let record = self.store.record(manifest.storage)?;
451        let held = transfer.opened == self.opened
452            && transfer.manifest == manifest
453            && record.state == SlotState::Receiving
454            && record.digest == manifest.digest
455            && record.size == manifest.size
456            && record.written == transfer.offset;
457        if !held {
458            return self.reopen(manifest);
459        }
460
461        Ok(Staging {
462            store: &mut self.store,
463            slot: transfer.slot,
464            manifest,
465            verifier: transfer.verifier,
466            offset: transfer.offset,
467            opened: self.opened,
468        })
469    }
470
471    /// Opens the slot a checked manifest names, continuing a partial transfer of the
472    /// same image by reading back and hashing what the slot already holds.
473    fn reopen(&mut self, manifest: Manifest) -> Result<Staging<'_, S>> {
474        let slot = manifest.storage;
475        let record = self.store.record(slot)?;
476        let resumable = record.state == SlotState::Receiving
477            && record.digest == manifest.digest
478            && record.size == manifest.size
479            && record.written <= manifest.size;
480
481        if !resumable {
482            return self.open(manifest);
483        }
484
485        // The hash cannot be carried across a reset, so it is rebuilt by reading
486        // back what the slot already holds. Those bytes are still unproven; the
487        // digest check at the end settles them, exactly as for a fresh transfer.
488        let mut verifier = ImageVerifier::new(&manifest);
489        let mut buf = [0u8; 256];
490        let mut at = 0u32;
491        while at < record.written {
492            let want = buf.len().min((record.written - at) as usize);
493            let read = self.store.read(slot, at, &mut buf[..want])?;
494            if read == 0 {
495                return Err(Refusal::Malformed);
496            }
497            verifier.update(&buf[..read])?;
498            at += read as u32;
499        }
500
501        Ok(Staging {
502            store: &mut self.store,
503            slot,
504            verifier,
505            manifest,
506            offset: record.written,
507            opened: self.opened,
508        })
509    }
510
511    /// Checks a manifest and stages an image already held whole.
512    ///
513    /// # Arguments
514    ///
515    /// * `envelope` - the signed manifest.
516    /// * `image` - the whole image.
517    ///
518    /// # Returns
519    ///
520    /// The slot the image was staged into.
521    ///
522    /// # Errors
523    ///
524    /// Returns whatever [`begin`](Self::begin) or [`Staging::finish`] refuses.
525    pub fn stage(&mut self, envelope: &[u8], image: &[u8]) -> Result<u8> {
526        self.stage_at(envelope, image, None)
527    }
528
529    /// Checks a manifest against the current time and stages an image held whole.
530    ///
531    /// # Arguments
532    ///
533    /// * `envelope` - the signed manifest.
534    /// * `image` - the whole image.
535    /// * `now` - seconds since the Unix epoch, or `None` on a device with no clock.
536    ///
537    /// # Returns
538    ///
539    /// The slot the image was staged into.
540    ///
541    /// # Errors
542    ///
543    /// Returns whatever [`begin_at`](Self::begin_at) or [`Staging::finish`]
544    /// refuses.
545    pub fn stage_at(&mut self, envelope: &[u8], image: &[u8], now: Option<u64>) -> Result<u8> {
546        let mut staging = self.begin_at(envelope, now)?;
547        staging.write(image)?;
548        staging.finish()
549    }
550
551    /// Decides what to run, and records that decision before returning it.
552    ///
553    /// Call this once per boot, before jumping to an image. A staged image
554    /// becomes pending here, so if the device resets before confirming, the next
555    /// call sees a pending slot and reverts.
556    ///
557    /// # Returns
558    ///
559    /// What the bootloader should run.
560    ///
561    /// # Errors
562    ///
563    /// Returns [`Refusal::NothingToRevert`] if there is no image to fall back to.
564    pub fn on_boot(&mut self) -> Result<Boot> {
565        if let Some(pending) = self.find(SlotState::Pending)? {
566            // It was booted last time and never said it was healthy.
567            self.set_state(pending, SlotState::Failed)?;
568            let fallback = self
569                .find(SlotState::Confirmed)?
570                .ok_or(Refusal::NothingToRevert)?;
571            return Ok(Boot::Reverted {
572                failed: pending,
573                fallback,
574            });
575        }
576
577        if let Some(staged) = self.find(SlotState::Staged)? {
578            self.set_state(staged, SlotState::Pending)?;
579            return Ok(Boot::Trying(staged));
580        }
581
582        self.find(SlotState::Confirmed)?
583            .map(Boot::Confirmed)
584            .ok_or(Refusal::NothingToRevert)
585    }
586
587    /// Reports the running image healthy, making it the one to fall back to.
588    ///
589    /// The slot the device previously fell back to is erased, which is what frees
590    /// it to receive the next update.
591    ///
592    /// # Returns
593    ///
594    /// The slot that is now confirmed.
595    ///
596    /// # Errors
597    ///
598    /// Returns [`Refusal::WrongState`] if no image is pending, so a confirmation
599    /// that arrives twice, or from an image nobody is trying, does nothing.
600    pub fn confirm(&mut self) -> Result<u8> {
601        let pending = self.find(SlotState::Pending)?.ok_or(Refusal::WrongState)?;
602
603        if let Some(previous) = self.find(SlotState::Confirmed)? {
604            self.store.erase(previous)?;
605        }
606        self.set_state(pending, SlotState::Confirmed)?;
607        Ok(pending)
608    }
609
610    /// Gives up on the pending image and goes back to the confirmed one.
611    ///
612    /// # Returns
613    ///
614    /// The slot the device falls back to.
615    ///
616    /// # Errors
617    ///
618    /// Returns [`Refusal::WrongState`] if no image is pending, or
619    /// [`Refusal::NothingToRevert`] if there is nothing to fall back to.
620    pub fn revert(&mut self) -> Result<u8> {
621        let pending = self.find(SlotState::Pending)?.ok_or(Refusal::WrongState)?;
622        let fallback = self
623            .find(SlotState::Confirmed)?
624            .ok_or(Refusal::NothingToRevert)?;
625        self.set_state(pending, SlotState::Failed)?;
626        Ok(fallback)
627    }
628
629    /// Marks a slot confirmed at first provisioning, when nothing was staged.
630    ///
631    /// A device leaves the factory already running an image that no update
632    /// installed. Without this there is no fallback for the first update to
633    /// return to.
634    ///
635    /// # Arguments
636    ///
637    /// * `slot` - the slot the factory image occupies.
638    /// * `sequence` - the sequence number of that image.
639    ///
640    /// # Returns
641    ///
642    /// `Ok(())` once the slot is confirmed.
643    ///
644    /// # Errors
645    ///
646    /// Returns [`Refusal::WrongState`] if any slot is already confirmed.
647    pub fn provision(&mut self, slot: u8, sequence: u64) -> Result<()> {
648        if self.find(SlotState::Confirmed)?.is_some() {
649            return Err(Refusal::WrongState);
650        }
651        let mut record = self.store.record(slot)?;
652        record.state = SlotState::Confirmed;
653        record.sequence = sequence;
654        self.store.set_record(slot, record)
655    }
656
657    /// Returns the first slot in the given state.
658    fn find(&self, state: SlotState) -> Result<Option<u8>> {
659        for slot in 0..self.store.slot_count() {
660            if self.store.record(slot)?.state == state {
661                return Ok(Some(slot));
662            }
663        }
664        Ok(None)
665    }
666
667    /// Changes a slot's state, leaving the rest of its record alone.
668    fn set_state(&mut self, slot: u8, state: SlotState) -> Result<()> {
669        let mut record = self.store.record(slot)?;
670        record.state = state;
671        self.store.set_record(slot, record)
672    }
673}
674
675/// A slot open for an image, with the manifest's promises still to be met.
676///
677/// The slot's record is only written once the image has been verified whole, so
678/// an interrupted transfer leaves a slot that is never bootable rather than one
679/// holding half an image.
680pub struct Staging<'a, S: SlotStore> {
681    store: &'a mut S,
682    slot: u8,
683    manifest: Manifest,
684    verifier: ImageVerifier,
685    offset: u32,
686    opened: u64,
687}
688
689/// A transfer in progress, held apart from its updater between pieces.
690///
691/// A [`Staging`] borrows its updater, which suits code that takes an image in one
692/// loop. Code that takes each piece in a call of its own, such as a binding or an
693/// event loop, [`detach`](Staging::detach)es it between pieces and hands it to
694/// [`Updater::resume_from`] for the next, which carries the hash of what has
695/// arrived instead of reading it back from the slot.
696pub struct Transfer {
697    slot: u8,
698    manifest: Manifest,
699    verifier: ImageVerifier,
700    offset: u32,
701    opened: u64,
702}
703
704impl Transfer {
705    /// Reports how much of the image has arrived.
706    ///
707    /// # Returns
708    ///
709    /// The bytes stored so far and the total the manifest declares.
710    pub fn progress(&self) -> (u32, u32) {
711        (self.offset, self.manifest.size)
712    }
713}
714
715impl<S: SlotStore> Staging<'_, S> {
716    /// Takes the next piece of the image.
717    ///
718    /// # Arguments
719    ///
720    /// * `chunk` - the next bytes of the image, in order.
721    ///
722    /// # Returns
723    ///
724    /// `Ok(())` once the chunk is hashed and stored.
725    ///
726    /// # Errors
727    ///
728    /// Returns [`Refusal::Size`] if more bytes arrive than the manifest declared,
729    /// or [`Refusal::SlotTooSmall`] if the slot cannot take them.
730    pub fn write(&mut self, chunk: &[u8]) -> Result<()> {
731        self.verifier.update(chunk)?;
732        self.store.write(self.slot, self.offset, chunk)?;
733        self.offset += chunk.len() as u32;
734
735        // Progress is recorded as it is made, so a reset costs at most the chunk in
736        // flight. How much that costs is the caller's to choose: a larger chunk
737        // means fewer record writes and so less flash wear, but more to redo.
738        let mut record = self.store.record(self.slot)?;
739        record.written = self.offset;
740        self.store.set_record(self.slot, record)
741    }
742
743    /// Reports how much of the image has arrived.
744    ///
745    /// # Returns
746    ///
747    /// The bytes stored so far and the total the manifest declares.
748    pub fn progress(&self) -> (u32, u32) {
749        (self.offset, self.manifest.size)
750    }
751
752    /// Finishes the image and marks the slot bootable if it matched.
753    ///
754    /// # Returns
755    ///
756    /// The slot now holding a staged image.
757    ///
758    /// # Errors
759    ///
760    /// Returns [`Refusal::Size`] or [`Refusal::Digest`] if the image is not the
761    /// one the manifest described, leaving the slot unbootable.
762    pub fn finish(self) -> Result<u8> {
763        let verified = self.verifier.finish()?;
764        self.store.set_record(
765            self.slot,
766            SlotRecord {
767                state: SlotState::Staged,
768                sequence: self.manifest.sequence,
769                size: verified.size(),
770                digest: verified.digest(),
771                written: verified.size(),
772            },
773        )?;
774        Ok(self.slot)
775    }
776
777    /// Returns the manifest this staging is fulfilling.
778    ///
779    /// # Returns
780    ///
781    /// The verified manifest.
782    pub fn manifest(&self) -> &Manifest {
783        &self.manifest
784    }
785
786    /// Lets go of the updater, keeping the hash of what has arrived for the next piece.
787    ///
788    /// Hand the result to [`Updater::resume_from`] to continue. After a piece is
789    /// refused, drop the staging instead of detaching it: the refused piece may already
790    /// be counted, so resume with [`Updater::resume_at`], which reads back what the
791    /// slot holds.
792    ///
793    /// # Returns
794    ///
795    /// The transfer, positioned after the last piece written.
796    pub fn detach(self) -> Transfer {
797        Transfer {
798            slot: self.slot,
799            manifest: self.manifest,
800            verifier: self.verifier,
801            offset: self.offset,
802            opened: self.opened,
803        }
804    }
805}