Skip to main content

vole_document/
error.rs

1//! Typed errors with stable classes and documented CLI exit codes.
2//!
3//! Malformed data is never reported as an "internal invariant"; each failure
4//! class is distinct so callers and the CLI can react precisely.
5
6use core::fmt;
7
8/// Convenience alias for the crate's fallible operations.
9pub type Result<T> = core::result::Result<T, Error>;
10
11/// Stable error classes. These distinguish failure *kinds* rather than prose.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ErrorClass {
14    /// Host I/O failure (read/write/rename/fsync).
15    Io,
16    /// Command-line or API misuse.
17    Usage,
18    /// The container framing/header is malformed.
19    InvalidContainer,
20    /// The container declares a version this build does not implement.
21    UnsupportedVersion,
22    /// The container declares a mandatory feature this build does not support.
23    UnsupportedFeature,
24    /// A digest, length, or checksum disagreed with the declared value.
25    IntegrityMismatch,
26    /// A declared or configured resource bound was exceeded.
27    ResourceLimit,
28    /// The reconstruction graph is malformed (cycles, bad references, gaps).
29    InvalidGraph,
30    /// An entropy model descriptor is malformed.
31    InvalidModel,
32    /// An entropy channel failed to decode.
33    EntropyDecode,
34    /// A PDF physical/lexical structure error (Phase 3+).
35    InvalidPdfStructure,
36    /// An exact codec-replay candidate failed (Phase 6+).
37    CodecReplay,
38    /// A referenced external store object is missing (Phase 9+).
39    MissingExternalObject,
40    /// The coverage certificate is invalid (gaps or overlapping authorities).
41    CoverageViolation,
42    /// Materialization completed but did not equal the source.
43    ReconstructionMismatch,
44    /// The operation was cooperatively cancelled.
45    Cancelled,
46    /// A bug in this implementation; never a description of malformed input.
47    InternalInvariant,
48}
49
50impl ErrorClass {
51    /// Stable, documented CLI exit code for this class.
52    pub const fn exit_code(self) -> i32 {
53        match self {
54            ErrorClass::Io => 3,
55            ErrorClass::Usage => 2,
56            ErrorClass::InvalidContainer => 4,
57            ErrorClass::UnsupportedVersion => 5,
58            ErrorClass::UnsupportedFeature => 6,
59            ErrorClass::IntegrityMismatch => 7,
60            ErrorClass::ResourceLimit => 8,
61            ErrorClass::InvalidGraph => 9,
62            ErrorClass::InvalidModel => 10,
63            ErrorClass::EntropyDecode => 11,
64            ErrorClass::InvalidPdfStructure => 12,
65            ErrorClass::CodecReplay => 13,
66            ErrorClass::MissingExternalObject => 14,
67            ErrorClass::CoverageViolation => 15,
68            ErrorClass::ReconstructionMismatch => 16,
69            ErrorClass::Cancelled => 17,
70            ErrorClass::InternalInvariant => 70,
71        }
72    }
73
74    /// Short stable identifier used in receipts and machine-readable output.
75    pub const fn as_str(self) -> &'static str {
76        match self {
77            ErrorClass::Io => "Io",
78            ErrorClass::Usage => "Usage",
79            ErrorClass::InvalidContainer => "InvalidContainer",
80            ErrorClass::UnsupportedVersion => "UnsupportedVersion",
81            ErrorClass::UnsupportedFeature => "UnsupportedFeature",
82            ErrorClass::IntegrityMismatch => "IntegrityMismatch",
83            ErrorClass::ResourceLimit => "ResourceLimit",
84            ErrorClass::InvalidGraph => "InvalidGraph",
85            ErrorClass::InvalidModel => "InvalidModel",
86            ErrorClass::EntropyDecode => "EntropyDecode",
87            ErrorClass::InvalidPdfStructure => "InvalidPdfStructure",
88            ErrorClass::CodecReplay => "CodecReplay",
89            ErrorClass::MissingExternalObject => "MissingExternalObject",
90            ErrorClass::CoverageViolation => "CoverageViolation",
91            ErrorClass::ReconstructionMismatch => "ReconstructionMismatch",
92            ErrorClass::Cancelled => "Cancelled",
93            ErrorClass::InternalInvariant => "InternalInvariant",
94        }
95    }
96}
97
98/// A typed, classified error.
99#[derive(Debug, Clone)]
100pub struct Error {
101    class: ErrorClass,
102    message: String,
103}
104
105impl Error {
106    /// Construct an error of the given class with a human-readable message.
107    pub fn new(class: ErrorClass, message: impl Into<String>) -> Self {
108        Error {
109            class,
110            message: message.into(),
111        }
112    }
113
114    /// The failure class.
115    pub fn class(&self) -> ErrorClass {
116        self.class
117    }
118
119    /// The stable CLI exit code for this error's class.
120    pub fn exit_code(&self) -> i32 {
121        self.class.exit_code()
122    }
123
124    /// The human-readable message.
125    pub fn message(&self) -> &str {
126        &self.message
127    }
128}
129
130impl fmt::Display for Error {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        write!(f, "{}: {}", self.class.as_str(), self.message)
133    }
134}
135
136impl std::error::Error for Error {}
137
138impl From<std::io::Error> for Error {
139    fn from(e: std::io::Error) -> Self {
140        Error::new(ErrorClass::Io, e.to_string())
141    }
142}
143
144macro_rules! ctor {
145    ($name:ident, $class:ident) => {
146        /// Construct an error of this class.
147        pub fn $name(message: impl Into<String>) -> Self {
148            Error::new(ErrorClass::$class, message)
149        }
150    };
151}
152
153impl Error {
154    ctor!(io, Io);
155    ctor!(usage, Usage);
156    ctor!(invalid_container, InvalidContainer);
157    ctor!(unsupported_version, UnsupportedVersion);
158    ctor!(unsupported_feature, UnsupportedFeature);
159    ctor!(integrity_mismatch, IntegrityMismatch);
160    ctor!(resource_limit, ResourceLimit);
161    ctor!(invalid_graph, InvalidGraph);
162    ctor!(invalid_model, InvalidModel);
163    ctor!(entropy_decode, EntropyDecode);
164    ctor!(invalid_pdf_structure, InvalidPdfStructure);
165    ctor!(codec_replay, CodecReplay);
166    ctor!(missing_external_object, MissingExternalObject);
167    ctor!(coverage_violation, CoverageViolation);
168    ctor!(reconstruction_mismatch, ReconstructionMismatch);
169    ctor!(cancelled, Cancelled);
170    ctor!(internal_invariant, InternalInvariant);
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn exit_codes_are_distinct_and_stable() {
179        // A representative set; guards accidental collisions.
180        let classes = [
181            ErrorClass::Io,
182            ErrorClass::Usage,
183            ErrorClass::InvalidContainer,
184            ErrorClass::UnsupportedVersion,
185            ErrorClass::UnsupportedFeature,
186            ErrorClass::IntegrityMismatch,
187            ErrorClass::ResourceLimit,
188            ErrorClass::InvalidGraph,
189            ErrorClass::InvalidModel,
190            ErrorClass::EntropyDecode,
191            ErrorClass::InvalidPdfStructure,
192            ErrorClass::CodecReplay,
193            ErrorClass::MissingExternalObject,
194            ErrorClass::CoverageViolation,
195            ErrorClass::ReconstructionMismatch,
196            ErrorClass::Cancelled,
197            ErrorClass::InternalInvariant,
198        ];
199        let mut codes: Vec<i32> = classes.iter().map(|c| c.exit_code()).collect();
200        let n = codes.len();
201        codes.sort_unstable();
202        codes.dedup();
203        assert_eq!(codes.len(), n, "exit codes must be unique");
204        assert_eq!(ErrorClass::InvalidContainer.exit_code(), 4);
205    }
206
207    #[test]
208    fn display_includes_class() {
209        let e = Error::invalid_container("bad magic");
210        assert_eq!(e.to_string(), "InvalidContainer: bad magic");
211    }
212}