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}