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 ZIP container structure error (Phase 12+): malformed or contradictory
47    /// local/central/ZIP64 records, an ambiguous byte cover, a broken descriptor,
48    /// multi-disk layout, or a hostile member name.
49    InvalidZipStructure,
50    /// An XML part structure error (Phase 12+, reserved): malformed or forbidden
51    /// constructs (DOCTYPE/XXE, encoding tricks) in a package part.
52    InvalidXmlStructure,
53    /// An OPC/OCF package structure error (Phase 12+, reserved): a missing or
54    /// ambiguous main part, content-type abuse, `mimetype` trick, or an ambiguous
55    /// part-name identity.
56    InvalidPackageStructure,
57    /// A bug in this implementation; never a description of malformed input.
58    InternalInvariant,
59}
60
61impl ErrorClass {
62    /// Stable, documented CLI exit code for this class.
63    pub const fn exit_code(self) -> i32 {
64        match self {
65            ErrorClass::Io => 3,
66            ErrorClass::Usage => 2,
67            ErrorClass::InvalidContainer => 4,
68            ErrorClass::UnsupportedVersion => 5,
69            ErrorClass::UnsupportedFeature => 6,
70            ErrorClass::IntegrityMismatch => 7,
71            ErrorClass::ResourceLimit => 8,
72            ErrorClass::InvalidGraph => 9,
73            ErrorClass::InvalidModel => 10,
74            ErrorClass::EntropyDecode => 11,
75            ErrorClass::InvalidPdfStructure => 12,
76            ErrorClass::CodecReplay => 13,
77            ErrorClass::MissingExternalObject => 14,
78            ErrorClass::CoverageViolation => 15,
79            ErrorClass::ReconstructionMismatch => 16,
80            ErrorClass::Cancelled => 17,
81            ErrorClass::InvalidZipStructure => 18,
82            ErrorClass::InvalidXmlStructure => 19,
83            ErrorClass::InvalidPackageStructure => 20,
84            ErrorClass::InternalInvariant => 70,
85        }
86    }
87
88    /// Short stable identifier used in receipts and machine-readable output.
89    pub const fn as_str(self) -> &'static str {
90        match self {
91            ErrorClass::Io => "Io",
92            ErrorClass::Usage => "Usage",
93            ErrorClass::InvalidContainer => "InvalidContainer",
94            ErrorClass::UnsupportedVersion => "UnsupportedVersion",
95            ErrorClass::UnsupportedFeature => "UnsupportedFeature",
96            ErrorClass::IntegrityMismatch => "IntegrityMismatch",
97            ErrorClass::ResourceLimit => "ResourceLimit",
98            ErrorClass::InvalidGraph => "InvalidGraph",
99            ErrorClass::InvalidModel => "InvalidModel",
100            ErrorClass::EntropyDecode => "EntropyDecode",
101            ErrorClass::InvalidPdfStructure => "InvalidPdfStructure",
102            ErrorClass::CodecReplay => "CodecReplay",
103            ErrorClass::MissingExternalObject => "MissingExternalObject",
104            ErrorClass::CoverageViolation => "CoverageViolation",
105            ErrorClass::ReconstructionMismatch => "ReconstructionMismatch",
106            ErrorClass::Cancelled => "Cancelled",
107            ErrorClass::InvalidZipStructure => "InvalidZipStructure",
108            ErrorClass::InvalidXmlStructure => "InvalidXmlStructure",
109            ErrorClass::InvalidPackageStructure => "InvalidPackageStructure",
110            ErrorClass::InternalInvariant => "InternalInvariant",
111        }
112    }
113}
114
115/// A typed, classified error.
116#[derive(Debug, Clone)]
117pub struct Error {
118    class: ErrorClass,
119    message: String,
120}
121
122impl Error {
123    /// Construct an error of the given class with a human-readable message.
124    pub fn new(class: ErrorClass, message: impl Into<String>) -> Self {
125        Error {
126            class,
127            message: message.into(),
128        }
129    }
130
131    /// The failure class.
132    pub fn class(&self) -> ErrorClass {
133        self.class
134    }
135
136    /// The stable CLI exit code for this error's class.
137    pub fn exit_code(&self) -> i32 {
138        self.class.exit_code()
139    }
140
141    /// The human-readable message.
142    pub fn message(&self) -> &str {
143        &self.message
144    }
145}
146
147impl fmt::Display for Error {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        write!(f, "{}: {}", self.class.as_str(), self.message)
150    }
151}
152
153impl std::error::Error for Error {}
154
155impl From<std::io::Error> for Error {
156    fn from(e: std::io::Error) -> Self {
157        Error::new(ErrorClass::Io, e.to_string())
158    }
159}
160
161macro_rules! ctor {
162    ($name:ident, $class:ident) => {
163        /// Construct an error of this class.
164        pub fn $name(message: impl Into<String>) -> Self {
165            Error::new(ErrorClass::$class, message)
166        }
167    };
168}
169
170impl Error {
171    ctor!(io, Io);
172    ctor!(usage, Usage);
173    ctor!(invalid_container, InvalidContainer);
174    ctor!(unsupported_version, UnsupportedVersion);
175    ctor!(unsupported_feature, UnsupportedFeature);
176    ctor!(integrity_mismatch, IntegrityMismatch);
177    ctor!(resource_limit, ResourceLimit);
178    ctor!(invalid_graph, InvalidGraph);
179    ctor!(invalid_model, InvalidModel);
180    ctor!(entropy_decode, EntropyDecode);
181    ctor!(invalid_pdf_structure, InvalidPdfStructure);
182    ctor!(codec_replay, CodecReplay);
183    ctor!(missing_external_object, MissingExternalObject);
184    ctor!(coverage_violation, CoverageViolation);
185    ctor!(reconstruction_mismatch, ReconstructionMismatch);
186    ctor!(cancelled, Cancelled);
187    ctor!(invalid_zip_structure, InvalidZipStructure);
188    ctor!(invalid_xml_structure, InvalidXmlStructure);
189    ctor!(invalid_package_structure, InvalidPackageStructure);
190    ctor!(internal_invariant, InternalInvariant);
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn exit_codes_are_distinct_and_stable() {
199        // A representative set; guards accidental collisions.
200        let classes = [
201            ErrorClass::Io,
202            ErrorClass::Usage,
203            ErrorClass::InvalidContainer,
204            ErrorClass::UnsupportedVersion,
205            ErrorClass::UnsupportedFeature,
206            ErrorClass::IntegrityMismatch,
207            ErrorClass::ResourceLimit,
208            ErrorClass::InvalidGraph,
209            ErrorClass::InvalidModel,
210            ErrorClass::EntropyDecode,
211            ErrorClass::InvalidPdfStructure,
212            ErrorClass::CodecReplay,
213            ErrorClass::MissingExternalObject,
214            ErrorClass::CoverageViolation,
215            ErrorClass::ReconstructionMismatch,
216            ErrorClass::Cancelled,
217            ErrorClass::InvalidZipStructure,
218            ErrorClass::InvalidXmlStructure,
219            ErrorClass::InvalidPackageStructure,
220            ErrorClass::InternalInvariant,
221        ];
222        let mut codes: Vec<i32> = classes.iter().map(|c| c.exit_code()).collect();
223        let n = codes.len();
224        codes.sort_unstable();
225        codes.dedup();
226        assert_eq!(codes.len(), n, "exit codes must be unique");
227        assert_eq!(ErrorClass::InvalidContainer.exit_code(), 4);
228    }
229
230    #[test]
231    fn display_includes_class() {
232        let e = Error::invalid_container("bad magic");
233        assert_eq!(e.to_string(), "InvalidContainer: bad magic");
234    }
235}