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 Markdown prose structure error (Phase 21.8+): a malformed or declined
70    /// construct (an unterminated fenced code block close, a bad reference
71    /// definition, or an input that carries no structural mark).
72    InvalidMarkdownStructure,
73    /// An HTML document structure error (Phase 21.10+): a forbidden construct
74    /// (a DOCTYPE with an internal subset), a non-UTF-8/UTF-16/NUL byte string, or
75    /// an input that carries no HTML structure. Malformed HTML is otherwise
76    /// **recovered** by the adapter, never a panic.
77    InvalidHtmlStructure,
78    /// A TOML structure error (Phase 21.11+): a malformed token, an unterminated
79    /// string/array/inline table, an invalid number or escape, or a violation of
80    /// TOML's duplicate-key/redefinition rules (which this adapter **enforces**).
81    InvalidTomlStructure,
82    /// A JSONL/NDJSON structure error (Phase 21.12+): a non-blank line that is not
83    /// exactly one JSON value, too few record lines to distinguish the source from a
84    /// single JSON value, or a non-newline-separated bag of JSON values.
85    InvalidJsonlStructure,
86    /// An EML/MIME message structure error (Phase 21.13+): no RFC 5322 header block,
87    /// a malformed header line, a `multipart/*` without a boundary, or an invalid
88    /// quoted-printable/base64 escape.
89    InvalidEmlStructure,
90    /// An Apache Parquet structure error (Phase 21.14+): missing/inconsistent `PAR1`
91    /// magic, an inconsistent footer length, a truncated Thrift-Compact footer, a
92    /// page or column-chunk span outside the file, or a value count that disagrees
93    /// with the page layout.
94    InvalidParquetStructure,
95    /// An Apache Arrow IPC structure error (Phase 21.16+): missing/inconsistent
96    /// `ARROW1` magic, a truncated or out-of-bounds Flatbuffers metadata table, an
97    /// invalid message framing, or a buffer span outside the message body.
98    InvalidArrowStructure,
99    /// A bug in this implementation; never a description of malformed input.
100    InternalInvariant,
101}
102
103impl ErrorClass {
104    /// Stable, documented CLI exit code for this class.
105    pub const fn exit_code(self) -> i32 {
106        match self {
107            ErrorClass::Io => 3,
108            ErrorClass::Usage => 2,
109            ErrorClass::InvalidContainer => 4,
110            ErrorClass::UnsupportedVersion => 5,
111            ErrorClass::UnsupportedFeature => 6,
112            ErrorClass::IntegrityMismatch => 7,
113            ErrorClass::ResourceLimit => 8,
114            ErrorClass::InvalidGraph => 9,
115            ErrorClass::InvalidModel => 10,
116            ErrorClass::EntropyDecode => 11,
117            ErrorClass::InvalidPdfStructure => 12,
118            ErrorClass::CodecReplay => 13,
119            ErrorClass::MissingExternalObject => 14,
120            ErrorClass::CoverageViolation => 15,
121            ErrorClass::ReconstructionMismatch => 16,
122            ErrorClass::Cancelled => 17,
123            ErrorClass::InvalidZipStructure => 18,
124            ErrorClass::InvalidXmlStructure => 19,
125            ErrorClass::InvalidPackageStructure => 20,
126            ErrorClass::InvalidJsonStructure => 21,
127            ErrorClass::InvalidYamlStructure => 22,
128            ErrorClass::InvalidCsvStructure => 23,
129            ErrorClass::InvalidMarkdownStructure => 24,
130            ErrorClass::InvalidHtmlStructure => 25,
131            ErrorClass::InvalidTomlStructure => 26,
132            ErrorClass::InvalidJsonlStructure => 27,
133            ErrorClass::InvalidEmlStructure => 28,
134            ErrorClass::InvalidParquetStructure => 29,
135            ErrorClass::InvalidArrowStructure => 30,
136            ErrorClass::InternalInvariant => 70,
137        }
138    }
139
140    /// Short stable identifier used in receipts and machine-readable output.
141    pub const fn as_str(self) -> &'static str {
142        match self {
143            ErrorClass::Io => "Io",
144            ErrorClass::Usage => "Usage",
145            ErrorClass::InvalidContainer => "InvalidContainer",
146            ErrorClass::UnsupportedVersion => "UnsupportedVersion",
147            ErrorClass::UnsupportedFeature => "UnsupportedFeature",
148            ErrorClass::IntegrityMismatch => "IntegrityMismatch",
149            ErrorClass::ResourceLimit => "ResourceLimit",
150            ErrorClass::InvalidGraph => "InvalidGraph",
151            ErrorClass::InvalidModel => "InvalidModel",
152            ErrorClass::EntropyDecode => "EntropyDecode",
153            ErrorClass::InvalidPdfStructure => "InvalidPdfStructure",
154            ErrorClass::CodecReplay => "CodecReplay",
155            ErrorClass::MissingExternalObject => "MissingExternalObject",
156            ErrorClass::CoverageViolation => "CoverageViolation",
157            ErrorClass::ReconstructionMismatch => "ReconstructionMismatch",
158            ErrorClass::Cancelled => "Cancelled",
159            ErrorClass::InvalidZipStructure => "InvalidZipStructure",
160            ErrorClass::InvalidXmlStructure => "InvalidXmlStructure",
161            ErrorClass::InvalidPackageStructure => "InvalidPackageStructure",
162            ErrorClass::InvalidJsonStructure => "InvalidJsonStructure",
163            ErrorClass::InvalidYamlStructure => "InvalidYamlStructure",
164            ErrorClass::InvalidCsvStructure => "InvalidCsvStructure",
165            ErrorClass::InvalidMarkdownStructure => "InvalidMarkdownStructure",
166            ErrorClass::InvalidHtmlStructure => "InvalidHtmlStructure",
167            ErrorClass::InvalidTomlStructure => "InvalidTomlStructure",
168            ErrorClass::InvalidJsonlStructure => "InvalidJsonlStructure",
169            ErrorClass::InvalidEmlStructure => "InvalidEmlStructure",
170            ErrorClass::InvalidParquetStructure => "InvalidParquetStructure",
171            ErrorClass::InvalidArrowStructure => "InvalidArrowStructure",
172            ErrorClass::InternalInvariant => "InternalInvariant",
173        }
174    }
175}
176
177/// A typed, classified error.
178#[derive(Debug, Clone)]
179pub struct Error {
180    class: ErrorClass,
181    message: String,
182}
183
184impl Error {
185    /// Construct an error of the given class with a human-readable message.
186    pub fn new(class: ErrorClass, message: impl Into<String>) -> Self {
187        Error {
188            class,
189            message: message.into(),
190        }
191    }
192
193    /// The failure class.
194    pub fn class(&self) -> ErrorClass {
195        self.class
196    }
197
198    /// The stable CLI exit code for this error's class.
199    pub fn exit_code(&self) -> i32 {
200        self.class.exit_code()
201    }
202
203    /// The human-readable message.
204    pub fn message(&self) -> &str {
205        &self.message
206    }
207}
208
209impl fmt::Display for Error {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        write!(f, "{}: {}", self.class.as_str(), self.message)
212    }
213}
214
215impl std::error::Error for Error {}
216
217impl From<std::io::Error> for Error {
218    fn from(e: std::io::Error) -> Self {
219        Error::new(ErrorClass::Io, e.to_string())
220    }
221}
222
223macro_rules! ctor {
224    ($name:ident, $class:ident) => {
225        /// Construct an error of this class.
226        pub fn $name(message: impl Into<String>) -> Self {
227            Error::new(ErrorClass::$class, message)
228        }
229    };
230}
231
232impl Error {
233    ctor!(io, Io);
234    ctor!(usage, Usage);
235    ctor!(invalid_container, InvalidContainer);
236    ctor!(unsupported_version, UnsupportedVersion);
237    ctor!(unsupported_feature, UnsupportedFeature);
238    ctor!(integrity_mismatch, IntegrityMismatch);
239    ctor!(resource_limit, ResourceLimit);
240    ctor!(invalid_graph, InvalidGraph);
241    ctor!(invalid_model, InvalidModel);
242    ctor!(entropy_decode, EntropyDecode);
243    ctor!(invalid_pdf_structure, InvalidPdfStructure);
244    ctor!(codec_replay, CodecReplay);
245    ctor!(missing_external_object, MissingExternalObject);
246    ctor!(coverage_violation, CoverageViolation);
247    ctor!(reconstruction_mismatch, ReconstructionMismatch);
248    ctor!(cancelled, Cancelled);
249    ctor!(invalid_zip_structure, InvalidZipStructure);
250    ctor!(invalid_xml_structure, InvalidXmlStructure);
251    ctor!(invalid_package_structure, InvalidPackageStructure);
252    ctor!(invalid_json_structure, InvalidJsonStructure);
253    ctor!(invalid_yaml_structure, InvalidYamlStructure);
254    ctor!(invalid_csv_structure, InvalidCsvStructure);
255    ctor!(invalid_markdown_structure, InvalidMarkdownStructure);
256    ctor!(invalid_html_structure, InvalidHtmlStructure);
257    ctor!(invalid_toml_structure, InvalidTomlStructure);
258    ctor!(invalid_jsonl_structure, InvalidJsonlStructure);
259    ctor!(invalid_eml_structure, InvalidEmlStructure);
260    ctor!(invalid_parquet_structure, InvalidParquetStructure);
261    ctor!(invalid_arrow_structure, InvalidArrowStructure);
262    ctor!(internal_invariant, InternalInvariant);
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn exit_codes_are_distinct_and_stable() {
271        // A representative set; guards accidental collisions.
272        let classes = [
273            ErrorClass::Io,
274            ErrorClass::Usage,
275            ErrorClass::InvalidContainer,
276            ErrorClass::UnsupportedVersion,
277            ErrorClass::UnsupportedFeature,
278            ErrorClass::IntegrityMismatch,
279            ErrorClass::ResourceLimit,
280            ErrorClass::InvalidGraph,
281            ErrorClass::InvalidModel,
282            ErrorClass::EntropyDecode,
283            ErrorClass::InvalidPdfStructure,
284            ErrorClass::CodecReplay,
285            ErrorClass::MissingExternalObject,
286            ErrorClass::CoverageViolation,
287            ErrorClass::ReconstructionMismatch,
288            ErrorClass::Cancelled,
289            ErrorClass::InvalidZipStructure,
290            ErrorClass::InvalidXmlStructure,
291            ErrorClass::InvalidPackageStructure,
292            ErrorClass::InvalidJsonStructure,
293            ErrorClass::InvalidYamlStructure,
294            ErrorClass::InvalidCsvStructure,
295            ErrorClass::InvalidMarkdownStructure,
296            ErrorClass::InvalidHtmlStructure,
297            ErrorClass::InvalidTomlStructure,
298            ErrorClass::InternalInvariant,
299        ];
300        let mut codes: Vec<i32> = classes.iter().map(|c| c.exit_code()).collect();
301        let n = codes.len();
302        codes.sort_unstable();
303        codes.dedup();
304        assert_eq!(codes.len(), n, "exit codes must be unique");
305        assert_eq!(ErrorClass::InvalidContainer.exit_code(), 4);
306    }
307
308    #[test]
309    fn display_includes_class() {
310        let e = Error::invalid_container("bad magic");
311        assert_eq!(e.to_string(), "InvalidContainer: bad magic");
312    }
313}