Skip to main content

limnifs_core/
error.rs

1//! Errors returned by the limnifs-core parsers.
2//!
3//! All errors surface the kind of structural problem and enough detail
4//! to produce a precise user-facing message. The `limni` CLI maps
5//! these to stable exit codes; other consumers (e.g. mount layer,
6//! adapters) can match on the variant for policy decisions.
7
8use core::fmt;
9
10/// Error reading a manifest header or section.
11#[derive(Debug, Clone, Eq, PartialEq)]
12pub enum CoreError {
13    /// Fewer than the required bytes available.
14    TooShort { have: usize, need: usize },
15    /// Magic bytes did not match the expected constant.
16    BadMagic { found: [u8; 4] },
17    /// A structural invariant was violated (nonzero reserved, bad
18    /// section version, duplicate flag id, out-of-range value, etc.).
19    Corrupt { reason: String },
20    /// The image uses a feature the reader does not implement.
21    /// `feature` carries enough context for the caller to report or
22    /// match (flag id, section version, etc.).
23    UnsupportedFeature { feature: String },
24}
25
26impl fmt::Display for CoreError {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::TooShort { have, need } => {
30                write!(
31                    f,
32                    "manifest header truncated: have {have} bytes, need {need}"
33                )
34            }
35            Self::BadMagic { found } => write!(
36                f,
37                "bad manifest magic: expected LMFS ({:x?}), found {:?} ({:x?})",
38                *b"LMFS",
39                core::str::from_utf8(found).unwrap_or("<non-utf8>"),
40                found
41            ),
42            Self::Corrupt { reason } => write!(f, "manifest corrupt: {reason}"),
43            Self::UnsupportedFeature { feature } => {
44                write!(f, "unsupported feature: {feature}")
45            }
46        }
47    }
48}
49
50impl std::error::Error for CoreError {}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn display_covers_every_variant() {
58        let cases = [
59            (
60                CoreError::TooShort { have: 4, need: 16 },
61                vec!["truncated", "16"],
62            ),
63            (
64                CoreError::BadMagic { found: *b"XXXX" },
65                vec!["LMFS", "XXXX"],
66            ),
67            (
68                CoreError::Corrupt {
69                    reason: "broken".into(),
70                },
71                vec!["corrupt", "broken"],
72            ),
73            (
74                CoreError::UnsupportedFeature {
75                    feature: "feature_flags section version 7".into(),
76                },
77                vec!["unsupported", "version 7"],
78            ),
79        ];
80        for (error, needles) in cases {
81            let s = error.to_string();
82            for needle in needles {
83                assert!(s.contains(needle), "display {s:?} missing {needle:?}");
84            }
85        }
86    }
87
88    #[test]
89    fn variants_are_eq_comparable() {
90        assert_eq!(
91            CoreError::TooShort { have: 4, need: 5 },
92            CoreError::TooShort { have: 4, need: 5 }
93        );
94        assert_ne!(
95            CoreError::TooShort { have: 4, need: 5 },
96            CoreError::TooShort { have: 4, need: 6 }
97        );
98    }
99}