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