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