Skip to main content

sofd_error/
lib.rs

1//! Unified error taxonomy.
2//! One enum per subsystem, each converting losslessly into [`OfdError`] and
3//! carrying context such as path, page and object id.
4
5#![forbid(unsafe_code)]
6#![warn(missing_docs)]
7
8use thiserror::Error;
9
10/// Top-level error that every failure converges to.
11#[derive(Debug, Error)]
12pub enum OfdError {
13    /// Filesystem or stream failure.
14    #[error("I/O error: {0}")]
15    Io(#[from] std::io::Error),
16
17    /// Failure inside the ZIP container layer.
18    #[error("container error: {0}")]
19    Package(#[from] PackageError),
20
21    /// XML parsing or serialization failure.
22    #[error("XML error: {0}")]
23    Xml(#[from] XmlError),
24
25    /// Document violates the format specification.
26    #[error("format error: {0}")]
27    Format(#[from] FormatError),
28
29    /// Rendering failure.
30    #[error("render error: {0}")]
31    Render(#[from] RenderError),
32
33    /// Font handling failure.
34    #[error("font error: {0}")]
35    Font(#[from] FontError),
36
37    /// Signature creation or verification failure.
38    #[error("signature error: {0}")]
39    Signature(#[from] SignatureError),
40
41    /// Cryptographic operation failure.
42    #[error("crypto error: {0}")]
43    Crypto(#[from] CryptoError),
44
45    /// Format conversion failure.
46    #[error("conversion error: {0}")]
47    Convert(#[from] ConvertError),
48
49    /// Document editing failure.
50    #[error("edit error: {0}")]
51    Edit(#[from] EditError),
52
53    /// Printing failure.
54    #[error("print error: {0}")]
55    Print(#[from] PrintError),
56
57    /// Document is encrypted and needs a password.
58    #[error("document is encrypted, password required")]
59    PasswordRequired,
60
61    /// Compliance validation failed.
62    #[error("compliance check failed: {0}")]
63    Compliance(String),
64
65    /// A resource budget was exceeded while guarding against hostile documents.
66    #[error("resource limit exceeded: {0}")]
67    ResourceLimit(String),
68}
69
70impl OfdError {
71    /// Whether this means the input document is damaged, so callers may degrade.
72    pub fn is_corruption(&self) -> bool {
73        matches!(
74            self,
75            OfdError::Format(_) | OfdError::Package(PackageError::Corrupted { .. })
76        )
77    }
78}
79
80/// Convenience result alias.
81pub type Result<T, E = OfdError> = std::result::Result<T, E>;
82
83/// Container layer errors.
84#[derive(Debug, Error)]
85pub enum PackageError {
86    /// Archive structure is damaged or malformed.
87    #[error("container structure is corrupted: {reason}")]
88    Corrupted {
89        /// What went wrong.
90        reason: String,
91    },
92    /// A required entry is absent.
93    #[error("container is missing required entry: {entry}")]
94    MissingEntry {
95        /// Entry path.
96        entry: String,
97    },
98    /// An entry path escaped the archive root, indicating Zip Slip.
99    #[error("illegal entry path (possible path traversal): {path}")]
100    IllegalPath {
101        /// Offending entry path.
102        path: String,
103    },
104    /// Decompressed size exceeded the configured ceiling.
105    #[error("decompressed size limit exceeded: {actual} bytes, limit {limit} bytes")]
106    DecompressionLimit {
107        /// Bytes actually produced.
108        actual: u64,
109        /// Configured ceiling.
110        limit: u64,
111    },
112    /// Entry count exceeded the configured ceiling.
113    #[error("entry count limit exceeded: {actual} > {limit}")]
114    EntryCountLimit {
115        /// Entries found.
116        actual: usize,
117        /// Configured ceiling.
118        limit: usize,
119    },
120}
121
122/// XML layer errors.
123#[derive(Debug, Error)]
124pub enum XmlError {
125    /// Well-formedness or syntax violation.
126    #[error("XML syntax error at line {line}, column {col}: {msg}")]
127    Syntax {
128        /// One-based line number.
129        line: u64,
130        /// One-based column number.
131        col: u64,
132        /// Description of the problem.
133        msg: String,
134    },
135    /// A required attribute is absent.
136    #[error("element <{element}> is missing required attribute {attr}")]
137    MissingAttribute {
138        /// Element name.
139        element: String,
140        /// Attribute name.
141        attr: String,
142    },
143    /// An attribute value could not be parsed into the expected type.
144    #[error("element <{element}> attribute {attr}={value:?} could not be parsed")]
145    InvalidAttribute {
146        /// Element name.
147        element: String,
148        /// Attribute name.
149        attr: String,
150        /// Raw value as written.
151        value: String,
152    },
153    /// An extension element is not supported.
154    #[error("unsupported extension element: {0}")]
155    UnsupportedExtension(String),
156}
157
158/// Specification conformance errors.
159#[derive(Debug, Error)]
160pub enum FormatError {
161    /// Not a valid OFD file; the entry point is absent.
162    #[error("not a valid OFD file (missing OFD.xml entry point)")]
163    NotOfd,
164    /// The document declares no root path to its own XML.
165    #[error("document body declares no root path")]
166    MissingRoot,
167    /// A path object carries no drawing commands.
168    #[error("path object declares no drawing data")]
169    MissingPathData,
170    /// Document version is not supported.
171    #[error("unsupported OFD version: {0}")]
172    UnsupportedVersion(String),
173    /// A referenced object or resource does not exist.
174    #[error("dangling reference: {kind} with id {id}")]
175    DanglingReference {
176        /// Kind of resource referenced.
177        kind: &'static str,
178        /// Resource id.
179        id: String,
180    },
181    /// References form a cycle.
182    #[error("circular reference detected: {chain}")]
183    CircularReference {
184        /// The cycle as a readable chain.
185        chain: String,
186    },
187    /// Requested page does not exist.
188    #[error("page index out of range: {index} (document has {total} pages)")]
189    PageOutOfRange {
190        /// Requested index.
191        index: usize,
192        /// Total pages available.
193        total: usize,
194    },
195    /// The document declares no signature list at all.
196    #[error("document declares no signatures")]
197    MissingSignatures,
198    /// Recursion went deeper than allowed.
199    #[error("parse recursion depth exceeded: {0}")]
200    MaxDepthExceeded(usize),
201}
202
203/// Rendering errors.
204#[derive(Debug, Error)]
205pub enum RenderError {
206    /// Backend cannot express this command.
207    #[error("backend {backend} does not support operation: {op}")]
208    UnsupportedOp {
209        /// Backend name.
210        backend: &'static str,
211        /// Command name.
212        op: &'static str,
213    },
214    /// Requested output size is invalid.
215    #[error("invalid output size: {w}x{h}")]
216    InvalidSize {
217        /// Width in pixels.
218        w: u32,
219        /// Height in pixels.
220        h: u32,
221    },
222    /// Image decoding failed.
223    #[error("image decode failed on page {page}: {reason}")]
224    ImageDecode {
225        /// Page index.
226        page: usize,
227        /// What went wrong.
228        reason: String,
229    },
230    /// Image encoding failed.
231    #[error("image encode failed on page {page}: {reason}")]
232    ImageEncode {
233        /// Page index.
234        page: usize,
235        /// What went wrong.
236        reason: String,
237    },
238    /// Render was aborted after exceeding its budget, degrading to partial output.
239    #[error("render aborted after exceeding budget: {0}")]
240    BudgetExceeded(String),
241}
242
243/// Font errors.
244#[derive(Debug, Error)]
245pub enum FontError {
246    /// Font data could not be parsed.
247    #[error("font parse failed: {0}")]
248    Parse(String),
249    /// Font is referenced but neither embedded nor substitutable.
250    #[error("font missing with no substitute available: {family}")]
251    Missing {
252        /// Font family name.
253        family: String,
254    },
255    /// The face lacks a glyph for this code point.
256    #[error("font {family} is missing glyph U+{codepoint:04X}")]
257    GlyphMissing {
258        /// Font family name.
259        family: String,
260        /// Unicode code point.
261        codepoint: u32,
262    },
263}
264
265/// Signature errors.
266#[derive(Debug, Error)]
267pub enum SignatureError {
268    /// Signature did not verify.
269    #[error("signature verification failed: {0}")]
270    Invalid(String),
271    /// Content changed after signing.
272    #[error("document content has been tampered with; signed range mismatch")]
273    Tampered,
274    /// Certificate chain could not be validated.
275    #[error("certificate chain validation failed: {0}")]
276    CertificateChain(String),
277    /// Certificate is past its validity period.
278    #[error("certificate has expired: {subject}")]
279    CertificateExpired {
280        /// Certificate subject.
281        subject: String,
282    },
283    /// Signature encoding is not recognized.
284    #[error("unsupported signature format: {0}")]
285    UnsupportedFormat(String),
286    /// Seal impression does not match the registered one.
287    #[error("seal impression verification failed")]
288    SealMismatch,
289}
290
291/// Cryptographic errors.
292#[derive(Debug, Error)]
293pub enum CryptoError {
294    /// A primitive failed to compute.
295    #[error("cryptographic operation failed: {0}")]
296    Operation(String),
297    /// Key material is malformed.
298    #[error("invalid key material: {0}")]
299    InvalidKey(String),
300    /// Decryption failed, meaning a wrong password or damaged data.
301    #[error("decryption failed: wrong password or corrupted data")]
302    DecryptFailed,
303    /// Algorithm is not implemented.
304    #[error("unsupported algorithm: {0}")]
305    UnsupportedAlgorithm(String),
306}
307
308/// Conversion errors.
309#[derive(Debug, Error)]
310pub enum ConvertError {
311    /// Source format is not handled.
312    #[error("unsupported source format: {0}")]
313    UnsupportedSource(String),
314    /// Target format is not handled.
315    #[error("unsupported target format: {0}")]
316    UnsupportedTarget(String),
317    /// A structure could not be mapped into the target format.
318    #[error("structure mapping failed on page {page}, object {object}: {reason}")]
319    MappingFailed {
320        /// Page index.
321        page: usize,
322        /// Object identifier.
323        object: String,
324        /// What went wrong.
325        reason: String,
326    },
327    /// Result quality fell below the accepted threshold.
328    #[error(
329        "conversion quality below threshold: difference ratio {diff_ratio:.4} exceeds {threshold:.4}"
330    )]
331    QualityBelowThreshold {
332        /// Measured difference ratio.
333        diff_ratio: f64,
334        /// Maximum accepted ratio.
335        threshold: f64,
336    },
337    /// Conversion was cancelled by the caller.
338    #[error("conversion was cancelled")]
339    Cancelled,
340}
341
342/// Editing errors.
343#[derive(Debug, Error)]
344pub enum EditError {
345    /// Target of the edit does not exist.
346    #[error("edit target not found: {0}")]
347    TargetNotFound(String),
348    /// Permission policy rejected the operation.
349    #[error("operation denied by permission policy: {0}")]
350    PermissionDenied(String),
351    /// Operation would break existing signatures.
352    #[error(
353        "this operation would invalidate {count} existing signature(s) and needs explicit confirmation"
354    )]
355    WouldInvalidateSignature {
356        /// Number of signatures affected.
357        count: usize,
358    },
359    /// Undo stack is empty.
360    #[error("nothing to undo")]
361    NothingToUndo,
362}
363
364/// Printing errors.
365#[derive(Debug, Error)]
366pub enum PrintError {
367    /// The page selection string could not be understood.
368    #[error("cannot parse page selection: {spec}")]
369    InvalidSelection {
370        /// The offending selection string.
371        spec: String,
372    },
373    /// A page range runs backwards or is empty.
374    #[error("invalid page range {start}-{end}")]
375    InvalidRange {
376        /// First page of the range.
377        start: usize,
378        /// Last page of the range.
379        end: usize,
380    },
381    /// A selected page does not exist in the document.
382    #[error("page {page} is out of range: document has {count} pages")]
383    PageOutOfRange {
384        /// The requested one-based page number.
385        page: usize,
386        /// Pages available in the document.
387        count: usize,
388    },
389    /// Zero copies is not a printable job.
390    #[error("copies must be at least one, got {copies}")]
391    InvalidCopies {
392        /// The requested copy count.
393        copies: usize,
394    },
395    /// A scale factor that cannot produce output.
396    #[error("scale factor must be positive and finite, got {scale}")]
397    InvalidScale {
398        /// The offending scale factor.
399        scale: f64,
400    },
401    /// A paper size with no area.
402    #[error("paper size must be positive, got {w}x{h} mm")]
403    InvalidPaper {
404        /// Paper width in millimetres.
405        w: f64,
406        /// Paper height in millimetres.
407        h: f64,
408    },
409}