Skip to main content

pamoja_update/
error.rs

1//! Why an update was refused.
2//!
3//! Every variant is a rule the update process enforces rather than a detail of how
4//! it is implemented, because the reason a device rejected an image is the part an
5//! operator has to act on. They map onto [`pamoja_core::Error`] so a caller that
6//! already handles the SDK's errors handles these too.
7
8use alloc::string::{String, ToString};
9
10use pamoja_core::Error;
11
12/// The reason an update was refused.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum Refusal {
15    /// The manifest is not valid CBOR, or ends before its fields do.
16    Malformed,
17    /// The manifest was written by a newer structure version than this device reads.
18    UnsupportedVersion,
19    /// The manifest signature is not from the key this device trusts.
20    Signature,
21    /// The image does not match the digest the manifest commits to.
22    Digest,
23    /// The image is not the size the manifest declares.
24    Size,
25    /// The manifest is for a different vendor or device class.
26    WrongDevice,
27    /// The sequence number is not greater than the one already installed, so this
28    /// is a replay or a downgrade.
29    Rollback,
30    /// The manifest's expiry has passed, so this release is no longer offered.
31    Expired,
32    /// The manifest expires, but this device cannot tell the time, so it has no
33    /// way to honour that.
34    NoClock,
35    /// The image does not fit the slot it is bound for.
36    SlotTooSmall,
37    /// The named slot does not exist on this device.
38    NoSuchSlot,
39    /// The operation does not apply to the slot's current state.
40    WrongState,
41    /// There is no confirmed image to fall back to.
42    NothingToRevert,
43}
44
45impl core::fmt::Display for Refusal {
46    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
47        f.write_str(self.reason())
48    }
49}
50
51impl Refusal {
52    /// Returns a human-readable description of the refusal.
53    ///
54    /// # Returns
55    ///
56    /// A short phrase naming the rule that refused the update.
57    pub fn reason(self) -> &'static str {
58        match self {
59            Self::Malformed => "the manifest is malformed",
60            Self::UnsupportedVersion => "the manifest structure version is not supported",
61            Self::Signature => "the manifest signature is not from the trusted key",
62            Self::Digest => "the image does not match the manifest digest",
63            Self::Size => "the image is not the size the manifest declares",
64            Self::WrongDevice => "the manifest is for a different vendor or device class",
65            Self::Rollback => "the sequence number would roll the device back",
66            Self::Expired => "the manifest has expired",
67            Self::NoClock => "the manifest expires and this device has no clock",
68            Self::SlotTooSmall => "the image does not fit the target slot",
69            Self::NoSuchSlot => "no such slot on this device",
70            Self::WrongState => "the slot is not in a state that allows this",
71            Self::NothingToRevert => "there is no confirmed image to revert to",
72        }
73    }
74}
75
76impl From<Refusal> for Error {
77    fn from(value: Refusal) -> Self {
78        let message: String = value.reason().to_string();
79        match value {
80            // A failed authenticity check is a security outcome, not a parse fault,
81            // so it is reported as one.
82            Refusal::Signature
83            | Refusal::Digest
84            | Refusal::WrongDevice
85            | Refusal::Rollback
86            | Refusal::Expired => Error::Auth(message),
87            Refusal::Malformed | Refusal::UnsupportedVersion => Error::Codec(message),
88            _ => Error::Io(message),
89        }
90    }
91}
92
93/// The result of an update operation.
94pub type Result<T> = core::result::Result<T, Refusal>;
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn authenticity_failures_map_to_auth_errors() {
102        for refusal in [
103            Refusal::Signature,
104            Refusal::Digest,
105            Refusal::WrongDevice,
106            Refusal::Rollback,
107        ] {
108            assert!(matches!(Error::from(refusal), Error::Auth(_)));
109        }
110    }
111
112    #[test]
113    fn parse_failures_map_to_codec_errors() {
114        assert!(matches!(Error::from(Refusal::Malformed), Error::Codec(_)));
115        assert!(matches!(
116            Error::from(Refusal::UnsupportedVersion),
117            Error::Codec(_)
118        ));
119    }
120
121    #[test]
122    fn every_refusal_names_its_rule() {
123        assert!(!Refusal::Rollback.reason().is_empty());
124        assert!(Refusal::Rollback.reason().contains("roll"));
125    }
126}