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