Skip to main content

caj2pdf_core/
error.rs

1// SPDX-License-Identifier: MIT
2
3use crate::hnc8::Variant;
4use std::{fmt, io};
5
6/// A failure visible to native and JavaScript callers: what went wrong, the
7/// absolute input offset and the structure it was found in, when known, and
8/// a fixed description.
9#[derive(Debug)]
10pub struct Error {
11    pub kind: ErrorKind,
12    /// Absolute byte offset in the input when known. For a [`Context::Pdf`]
13    /// error it is in the whole input, not in an embedded PDF range.
14    pub offset: Option<u64>,
15    pub context: Context,
16    /// What was wrong or which field; empty when the kind says it all.
17    pub reason: &'static str,
18}
19
20/// What went wrong.
21#[derive(Debug)]
22pub enum ErrorKind {
23    /// The input's format or a feature of it is not supported.
24    UnsupportedFormat,
25    /// A structure or field the format does not allow. Without a context it
26    /// is an invalid caller-supplied range, count or option.
27    Malformed,
28    Encrypted,
29    /// The source ended before a required range was complete.
30    Truncated {
31        expected: u64,
32        available: u64,
33    },
34    /// A configured resource bound would be exceeded.
35    LimitExceeded {
36        resource: &'static str,
37        limit: u64,
38        attempted: u64,
39    },
40    /// A source or sink failed.
41    Io(io::Error),
42    /// The caller cancelled the operation.
43    Cancelled,
44}
45
46/// The structure a failure was located in.
47#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
48pub enum Context {
49    #[default]
50    None,
51    /// A CAJ container field, or a one-based TOC or page-table record.
52    Caj { record: Option<u32> },
53    /// A KDH wrapper or its encoded PDF boundary.
54    Kdh,
55    /// An embedded or standalone PDF. `repair` marks a damaged structure
56    /// whose repair would be ambiguous.
57    Pdf {
58        object: Option<(u32, u16)>,
59        repair: bool,
60    },
61    /// An HN/C8 container, page or image, with one-based numbers, the JBIG2
62    /// segment of a type-3 image, and the conversion stage when converting.
63    Hnc8 {
64        variant: Option<Variant>,
65        page: Option<u32>,
66        image: Option<u32>,
67        segment: Option<u32>,
68        stage: Option<Hnc8Stage>,
69    },
70    /// A JBIG2 segment, by its segment number.
71    Jbig2 { segment: Option<u32> },
72}
73
74/// The HN/C8 conversion stage a failure arose in.
75#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub enum Hnc8Stage {
77    Preflight,
78    Container,
79    Text,
80    Headers,
81    Geometry,
82    Decode,
83    Pdf,
84    Visitor,
85    /// A stage of type-3 JBIG2 image decoding.
86    Type3(Type3Stage),
87}
88
89/// The type-3 image decoding stage a failure arose in.
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
91pub enum Type3Stage {
92    Directory,
93    PageInfo,
94    TextHeader,
95    GenericHeader,
96    Profile,
97    FirstDictionary,
98    SecondDictionary,
99    TextInstances,
100    TextCompose,
101    GenericRegion,
102    PageCompose,
103    Contexts,
104}
105
106pub type Result<T> = std::result::Result<T, Error>;
107
108impl From<ErrorKind> for Error {
109    fn from(kind: ErrorKind) -> Self {
110        Self {
111            kind,
112            offset: None,
113            context: Context::None,
114            reason: "",
115        }
116    }
117}
118
119impl Error {
120    /// An invalid caller-supplied range, count or option.
121    pub fn invalid(reason: &'static str) -> Self {
122        Self::from(ErrorKind::Malformed).because(reason)
123    }
124
125    /// A structure or field at `offset` the format does not allow.
126    pub fn malformed(offset: u64, reason: &'static str) -> Self {
127        Self::invalid(reason).at(offset)
128    }
129
130    /// A structure or field at `offset` outside the supported profile.
131    pub fn unsupported(offset: u64, reason: &'static str) -> Self {
132        Self::from(ErrorKind::UnsupportedFormat)
133            .at(offset)
134            .because(reason)
135    }
136
137    pub fn cancelled() -> Self {
138        ErrorKind::Cancelled.into()
139    }
140
141    /// The source ended at `offset` with `available` of `expected` bytes.
142    pub fn truncated(offset: u64, expected: u64, available: u64) -> Self {
143        Self::from(ErrorKind::Truncated {
144            expected,
145            available,
146        })
147        .at(offset)
148    }
149
150    /// A configured resource bound would be exceeded.
151    pub fn limit(resource: &'static str, limit: u64, attempted: u64) -> Self {
152        Self::from(ErrorKind::LimitExceeded {
153            resource,
154            limit,
155            attempted,
156        })
157    }
158
159    /// Set the absolute input offset.
160    #[must_use]
161    pub fn at(self, offset: u64) -> Self {
162        Self {
163            offset: Some(offset),
164            ..self
165        }
166    }
167
168    /// Set the description.
169    #[must_use]
170    pub fn because(self, reason: &'static str) -> Self {
171        Self { reason, ..self }
172    }
173
174    /// Set the structure the failure is located in.
175    #[must_use]
176    pub fn within(self, context: Context) -> Self {
177        Self { context, ..self }
178    }
179
180    #[must_use]
181    pub fn in_caj(self, record: Option<u32>) -> Self {
182        self.within(Context::Caj { record })
183    }
184
185    #[must_use]
186    pub fn in_pdf(self, object: Option<(u32, u16)>) -> Self {
187        self.within(Context::Pdf {
188            object,
189            repair: false,
190        })
191    }
192
193    #[must_use]
194    pub fn in_jbig2(self, segment: Option<u32>) -> Self {
195        self.within(Context::Jbig2 { segment })
196    }
197
198    /// Give an unlocated error this location; a located one keeps its own.
199    #[must_use]
200    pub fn or_at(self, offset: u64, context: Context) -> Self {
201        if self.context != Context::None {
202            return self;
203        }
204        Self {
205            offset: self.offset.or(Some(offset)),
206            context,
207            ..self
208        }
209    }
210}
211
212impl Context {
213    /// An HN/C8 structure not yet located in a page or stage.
214    pub(crate) const HNC8: Self = Self::Hnc8 {
215        variant: None,
216        page: None,
217        image: None,
218        segment: None,
219        stage: None,
220    };
221
222    fn write_name(self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        match self {
224            Self::None => Ok(()),
225            Self::Caj { .. } => f.write_str("CAJ"),
226            Self::Kdh => f.write_str("KDH"),
227            Self::Pdf { .. } => f.write_str("PDF"),
228            Self::Hnc8 { variant, .. } => {
229                f.write_str("HN/C8")?;
230                match variant {
231                    Some(variant) => write!(f, " {}", variant.as_str()),
232                    None => Ok(()),
233                }
234            }
235            Self::Jbig2 { .. } => f.write_str("JBIG2"),
236        }
237    }
238
239    fn write_location(self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        let mut item = |name: &str, value: Option<u32>| match value {
241            Some(value) => write!(f, ", {name} {value}"),
242            None => Ok(()),
243        };
244        match self {
245            Self::None | Self::Kdh => Ok(()),
246            Self::Caj { record } => item("record", record),
247            Self::Pdf { object, .. } => match object {
248                Some((number, generation)) => write!(f, ", object {number} {generation}"),
249                None => Ok(()),
250            },
251            Self::Hnc8 {
252                page,
253                image,
254                segment,
255                ..
256            } => {
257                item("page", page)?;
258                item("image", image)?;
259                item("segment", segment)
260            }
261            Self::Jbig2 { segment } => item("segment", segment),
262        }
263    }
264}
265
266struct Name(Context);
267
268impl fmt::Display for Name {
269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270        self.0.write_name(f)
271    }
272}
273
274/// `"{kind} {context} at byte {offset}, {location}: {reason}: {details}"`
275/// with the pieces present.
276impl fmt::Display for Error {
277    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278        let name = Name(self.context);
279        let named = self.context != Context::None;
280        let input = |f: &mut fmt::Formatter<'_>, adjective: &str, fallback: &str| {
281            if named {
282                write!(f, "{adjective} {name}")
283            } else {
284                write!(f, "{adjective} {fallback}")
285            }
286        };
287        let event = |f: &mut fmt::Formatter<'_>, event: &str| {
288            f.write_str(event)?;
289            if named {
290                write!(f, " in {name}")?;
291            }
292            Ok(())
293        };
294        match &self.kind {
295            ErrorKind::Malformed if matches!(self.context, Context::Pdf { repair: true, .. }) => {
296                f.write_str("ambiguous repair PDF")?
297            }
298            ErrorKind::UnsupportedFormat if matches!(self.context, Context::Pdf { .. }) => {
299                f.write_str("unsupported feature PDF")?
300            }
301            ErrorKind::Malformed if !named => f.write_str("invalid input")?,
302            ErrorKind::Malformed => input(f, "malformed", "")?,
303            ErrorKind::UnsupportedFormat => input(f, "unsupported", "input format")?,
304            ErrorKind::Encrypted => input(f, "encrypted", "input")?,
305            ErrorKind::Truncated { .. } => input(f, "truncated", "input")?,
306            ErrorKind::LimitExceeded { resource, .. } if named => {
307                write!(f, "{name} {resource} limit exceeded")?
308            }
309            ErrorKind::LimitExceeded { resource, .. } => write!(f, "{resource} limit exceeded")?,
310            ErrorKind::Io(_) => event(f, "I/O error")?,
311            ErrorKind::Cancelled => event(f, "operation cancelled")?,
312        }
313        if let Some(offset) = self.offset {
314            write!(f, " at byte {offset}")?;
315        }
316        self.context.write_location(f)?;
317        if !self.reason.is_empty() {
318            write!(f, ": {}", self.reason)?;
319        }
320        match &self.kind {
321            ErrorKind::Truncated {
322                expected,
323                available,
324            } => write!(f, ": expected {expected} bytes, available {available}"),
325            ErrorKind::LimitExceeded {
326                limit, attempted, ..
327            } => write!(f, ": maximum {limit}, attempted {attempted}"),
328            ErrorKind::Io(error) => write!(f, ": {error}"),
329            _ => Ok(()),
330        }
331    }
332}
333
334impl std::error::Error for Error {
335    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
336        match &self.kind {
337            ErrorKind::Io(error) => Some(error),
338            _ => None,
339        }
340    }
341}
342
343/// An I/O error that carries a core error, as an [`io::Write`] adapter in
344/// this crate reports one, converts back to that error.
345impl From<io::Error> for Error {
346    fn from(error: io::Error) -> Self {
347        match error.downcast::<Self>() {
348            Ok(error) => error,
349            Err(error) => ErrorKind::Io(error).into(),
350        }
351    }
352}
353
354/// Carry a core error through an [`io::Write`] adapter; an unlocated I/O
355/// error is passed on as it is, and a located one keeps its I/O kind.
356impl From<Error> for io::Error {
357    fn from(error: Error) -> Self {
358        let kind = match &error.kind {
359            ErrorKind::Io(inner) => inner.kind(),
360            _ => io::ErrorKind::Other,
361        };
362        match error {
363            Error {
364                kind: ErrorKind::Io(error),
365                offset: None,
366                context: Context::None,
367                reason: "",
368            } => error,
369            located => io::Error::new(kind, located),
370        }
371    }
372}
373
374#[cfg(test)]
375mod tests;