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