Skip to main content

sheets_diff/
error.rs

1//! Fatal error type for all fallible v2 entry points (RFC-005, RFC-033 §9).
2
3use std::fmt;
4
5use crate::model::{SheetRef, Side, SourceDescription};
6
7// ---------------------------------------------------------------------------
8// Open / read error kinds
9// ---------------------------------------------------------------------------
10
11/// Why a workbook could not be opened.
12#[non_exhaustive]
13#[derive(Debug)]
14pub enum OpenErrorKind {
15    NotFound,
16    PermissionDenied,
17    /// The bytes are not a valid ZIP / xlsx container.
18    NotXlsx,
19    /// Structurally valid ZIP, but xlsx internals are corrupt.
20    Corrupt,
21    /// File is locked or busy (OS-level).
22    Locked,
23    Other,
24}
25
26impl fmt::Display for OpenErrorKind {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            OpenErrorKind::NotFound => f.write_str("file not found"),
30            OpenErrorKind::PermissionDenied => f.write_str("permission denied"),
31            OpenErrorKind::NotXlsx => f.write_str("not an xlsx file"),
32            OpenErrorKind::Corrupt => f.write_str("file is corrupt"),
33            OpenErrorKind::Locked => f.write_str("file is locked"),
34            OpenErrorKind::Other => f.write_str("open failed"),
35        }
36    }
37}
38
39/// Why a sheet could not be read.
40#[non_exhaustive]
41#[derive(Debug)]
42pub enum ReadErrorKind {
43    SheetNotFound,
44    MalformedSheet,
45    Other,
46}
47
48impl fmt::Display for ReadErrorKind {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            ReadErrorKind::SheetNotFound => f.write_str("sheet not found"),
52            ReadErrorKind::MalformedSheet => f.write_str("sheet is malformed"),
53            ReadErrorKind::Other => f.write_str("read failed"),
54        }
55    }
56}
57
58// ---------------------------------------------------------------------------
59// Limit kind (RFC-012 / RFC-033 §10)
60// ---------------------------------------------------------------------------
61
62/// Which resource limit was exceeded.
63#[derive(Clone, Copy, PartialEq, Eq, Debug)]
64pub enum LimitKind {
65    Sheets,
66    CellsRead,
67    CellsCompared,
68    DiffsReturned,
69}
70
71impl fmt::Display for LimitKind {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        match self {
74            LimitKind::Sheets => f.write_str("max_sheets"),
75            LimitKind::CellsRead => f.write_str("max_cells_read"),
76            LimitKind::CellsCompared => f.write_str("max_cells_compared"),
77            LimitKind::DiffsReturned => f.write_str("max_diffs_returned"),
78        }
79    }
80}
81
82// ---------------------------------------------------------------------------
83// Boxed calamine error carrier
84// ---------------------------------------------------------------------------
85
86/// Opaque wrapper that owns the original `calamine::XlsxError` so that
87/// `SheetsDiffError::source()` can return it without naming calamine in any
88/// public signature (RFC-026).
89pub struct CalamiLineError(pub(crate) calamine::XlsxError);
90
91impl fmt::Debug for CalamiLineError {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        write!(f, "calamine error: {}", self.0)
94    }
95}
96impl fmt::Display for CalamiLineError {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        write!(f, "{}", self.0)
99    }
100}
101impl std::error::Error for CalamiLineError {}
102
103// ---------------------------------------------------------------------------
104// SheetsDiffError (RFC-033 §9)
105// ---------------------------------------------------------------------------
106
107/// Fatal error returned by every v2 entry point.
108///
109/// The `calamine` source error is preserved behind `std::error::Error::source()`
110/// — it never appears in any public variant type.
111#[non_exhaustive]
112#[derive(Debug)]
113pub enum SheetsDiffError {
114    /// A workbook could not be opened or parsed.
115    OpenWorkbook {
116        side: Side,
117        source: SourceDescription,
118        kind: OpenErrorKind,
119        /// Boxed calamine error; accessible via `Error::source()`.
120        inner: Option<Box<CalamiLineError>>,
121    },
122    /// A specific sheet inside an opened workbook could not be read.
123    ReadSheet {
124        side: Side,
125        sheet: SheetRef,
126        kind: ReadErrorKind,
127        inner: Option<Box<CalamiLineError>>,
128    },
129    /// The bytes/reader are a valid ZIP but not a recognised xlsx workbook.
130    UnsupportedFormat { side: Side, detail: String },
131    /// The workbook is password-protected (calamine `XlsxError::Password`).
132    EncryptedWorkbook { side: Side },
133    /// A `DiffOptions` combination is invalid; detected before any I/O.
134    InvalidOptions { detail: String },
135    /// The caller's cancellation predicate returned `true`.
136    Cancelled,
137    /// A configured `Limits` bound was reached.
138    LimitExceeded { limit: LimitKind, observed: u64 },
139    /// An internal programming error; indicates a bug in `sheets-diff`.
140    Internal { detail: String },
141}
142
143impl fmt::Display for SheetsDiffError {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self {
146            SheetsDiffError::OpenWorkbook { side, source, kind, .. } => {
147                let name = source
148                    .display_name
149                    .as_deref()
150                    .unwrap_or("<unknown>");
151                write!(f, "cannot open {side} workbook '{name}': {kind}")
152            }
153            SheetsDiffError::ReadSheet { side, sheet, kind, .. } => {
154                write!(f, "cannot read sheet '{}' from {side} workbook: {kind}", sheet.name)
155            }
156            SheetsDiffError::UnsupportedFormat { side, detail } => {
157                write!(f, "{side} workbook is not a supported xlsx format: {detail}")
158            }
159            SheetsDiffError::EncryptedWorkbook { side } => {
160                write!(f, "{side} workbook is password-protected")
161            }
162            SheetsDiffError::InvalidOptions { detail } => {
163                write!(f, "invalid options: {detail}")
164            }
165            SheetsDiffError::Cancelled => f.write_str("comparison was cancelled"),
166            SheetsDiffError::LimitExceeded { limit, observed } => {
167                write!(f, "limit '{limit}' exceeded (observed {observed})")
168            }
169            SheetsDiffError::Internal { detail } => {
170                write!(f, "internal error: {detail}")
171            }
172        }
173    }
174}
175
176impl std::error::Error for SheetsDiffError {
177    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
178        match self {
179            SheetsDiffError::OpenWorkbook { inner, .. } => {
180                inner.as_deref().map(|e| e as &dyn std::error::Error)
181            }
182            SheetsDiffError::ReadSheet { inner, .. } => {
183                inner.as_deref().map(|e| e as &dyn std::error::Error)
184            }
185            _ => None,
186        }
187    }
188}
189
190// ---------------------------------------------------------------------------
191// Conversion helpers (crate-internal)
192// ---------------------------------------------------------------------------
193
194impl SheetsDiffError {
195    pub(crate) fn open_workbook(
196        side: Side,
197        source: SourceDescription,
198        calamine_err: calamine::XlsxError,
199    ) -> Self {
200        let kind = classify_open_error(&calamine_err);
201        SheetsDiffError::OpenWorkbook {
202            side,
203            source,
204            kind,
205            inner: Some(Box::new(CalamiLineError(calamine_err))),
206        }
207    }
208
209    pub(crate) fn read_sheet(
210        side: Side,
211        sheet: SheetRef,
212        calamine_err: calamine::XlsxError,
213    ) -> Self {
214        let kind = classify_read_error(&calamine_err);
215        SheetsDiffError::ReadSheet {
216            side,
217            sheet,
218            kind,
219            inner: Some(Box::new(CalamiLineError(calamine_err))),
220        }
221    }
222}
223
224fn classify_open_error(e: &calamine::XlsxError) -> OpenErrorKind {
225    use calamine::XlsxError;
226    match e {
227        XlsxError::Password => OpenErrorKind::NotXlsx, // reclassified below via EncryptedWorkbook
228        XlsxError::FileNotFound(_) => OpenErrorKind::NotFound,
229        XlsxError::Io(io) => match io.kind() {
230            std::io::ErrorKind::NotFound => OpenErrorKind::NotFound,
231            std::io::ErrorKind::PermissionDenied => OpenErrorKind::PermissionDenied,
232            _ => OpenErrorKind::Other,
233        },
234        XlsxError::Zip(_) => OpenErrorKind::NotXlsx,
235        _ => OpenErrorKind::Corrupt,
236    }
237}
238
239fn classify_read_error(e: &calamine::XlsxError) -> ReadErrorKind {
240    use calamine::XlsxError;
241    match e {
242        XlsxError::WorksheetNotFound(_) => ReadErrorKind::SheetNotFound,
243        _ => ReadErrorKind::MalformedSheet,
244    }
245}
246
247/// Convert a calamine open error, detecting `Password` to produce the
248/// dedicated `EncryptedWorkbook` variant.
249pub(crate) fn from_open_error(
250    side: Side,
251    source: SourceDescription,
252    e: calamine::XlsxError,
253) -> SheetsDiffError {
254    if matches!(e, calamine::XlsxError::Password) {
255        SheetsDiffError::EncryptedWorkbook { side }
256    } else {
257        SheetsDiffError::open_workbook(side, source, e)
258    }
259}