Skip to main content

pdfboss_aio/
error.rs

1//! Error type for pdfboss-aio: wraps core parse errors and transport
2//! failures, with a dedicated variant for short reads. Messages are
3//! prefixed by layer ("parse:", "io:", "http:") so downstream consumers
4//! can present them uniformly.
5
6/// Convenience alias used throughout pdfboss-aio.
7pub type Result<T> = std::result::Result<T, Error>;
8
9/// All errors surfaced by pdfboss-aio.
10///
11/// Every fetch failure carries the offset/range it was fetching, for
12/// diagnosability; parse errors wrap the core error unchanged.
13#[derive(Debug, thiserror::Error)]
14pub enum Error {
15    /// A parse-layer error from the sync core machinery.
16    #[error("parse: {0}")]
17    Core(#[from] pdfboss_core::Error),
18    /// A transport-layer I/O error.
19    #[error("io: {0}")]
20    Io(std::io::Error),
21    /// An HTTP transport error (connection, status, malformed response).
22    /// The status is rendered when known (`http 404: msg`); a connection-
23    /// level failure with no response at all keeps the bare `http:` prefix
24    /// (asserted by the CLI's own tests, which look for that exact
25    /// substring on a refused connection).
26    #[cfg(feature = "http")]
27    #[error("http{}: {msg}", status.map(|code| format!(" {code}")).unwrap_or_default())]
28    Http { status: Option<u16>, msg: String },
29    /// A read stopped short of the requested range while more bytes were
30    /// expected (the source is shorter than its declared length).
31    #[error("truncated read at offset {offset}: wanted {wanted} bytes, got {got}")]
32    TruncatedRead {
33        offset: u64,
34        wanted: usize,
35        got: usize,
36    },
37}
38
39impl From<std::io::Error> for Error {
40    fn from(inner: std::io::Error) -> Error {
41        #[cfg(feature = "http")]
42        if let Some(marker) = inner
43            .get_ref()
44            .and_then(|source| source.downcast_ref::<TransportMarker>())
45        {
46            return Error::Http {
47                status: marker.status,
48                msg: marker.msg.clone(),
49            };
50        }
51        Error::Io(inner)
52    }
53}
54
55/// The adapter that lets [`crate::AsyncDocument`] speak a shared algorithm's
56/// `pdfboss_core::Result`: parse-layer errors unwrap back to the original
57/// core variant, and the genuinely transport-level remainder renders into
58/// [`pdfboss_core::Error::Transport`], message intact. Nothing is lost that a
59/// shared algorithm could act on — leniency decisions key off the parse
60/// variants, which survive unwrapped.
61impl From<Error> for pdfboss_core::Error {
62    fn from(e: Error) -> pdfboss_core::Error {
63        match e {
64            Error::Core(inner) => inner,
65            transport => pdfboss_core::Error::Transport(transport.to_string()),
66        }
67    }
68}
69
70/// Marker payload smuggled through `std::io::Error` by backends whose
71/// trait methods can only return `io::Result`; recovered by
72/// [`From<std::io::Error>`] above. Only the HTTP backend produces these.
73#[cfg(feature = "http")]
74#[derive(Debug)]
75pub(crate) struct TransportMarker {
76    pub(crate) status: Option<u16>,
77    pub(crate) msg: String,
78}
79
80#[cfg(feature = "http")]
81impl std::fmt::Display for TransportMarker {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        write!(f, "http {:?}: {}", self.status, self.msg)
84    }
85}
86
87#[cfg(feature = "http")]
88impl std::error::Error for TransportMarker {}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn wraps_core_and_io_errors_with_layer_prefixes() {
96        let core = Error::from(pdfboss_core::Error::InvalidXref);
97        assert!(matches!(
98            core,
99            Error::Core(pdfboss_core::Error::InvalidXref)
100        ));
101        assert_eq!(
102            core.to_string(),
103            "parse: invalid or unrecoverable cross-reference data"
104        );
105        let io = Error::from(std::io::Error::other("boom"));
106        assert!(matches!(io, Error::Io(_)));
107        assert_eq!(io.to_string(), "io: boom");
108    }
109
110    #[test]
111    fn transport_variants_render_their_context() {
112        let err = Error::TruncatedRead {
113            offset: 512,
114            wanted: 100,
115            got: 3,
116        };
117        assert_eq!(
118            err.to_string(),
119            "truncated read at offset 512: wanted 100 bytes, got 3"
120        );
121    }
122
123    #[cfg(feature = "http")]
124    #[test]
125    fn http_error_renders_status_when_known_and_stays_prefixed_without_it() {
126        let with_status = Error::Http {
127            status: Some(404),
128            msg: "not found".to_string(),
129        };
130        assert_eq!(with_status.to_string(), "http 404: not found");
131        let without_status = Error::Http {
132            status: None,
133            msg: "connection refused".to_string(),
134        };
135        assert_eq!(without_status.to_string(), "http: connection refused");
136    }
137}