Skip to main content

sheetkit_core/
error.rs

1//! Error types for the SheetKit core library.
2//!
3//! Provides a comprehensive [`Error`] enum covering all failure modes
4//! encountered when reading, writing, and manipulating Excel workbooks.
5
6use thiserror::Error;
7
8/// The top-level error type for SheetKit.
9#[derive(Error, Debug)]
10pub enum Error {
11    /// The given string is not a valid A1-style cell reference.
12    #[error("invalid cell reference: {0}")]
13    InvalidCellReference(String),
14
15    /// The row number is out of the allowed range (1..=1_048_576).
16    #[error("invalid row number: {0}")]
17    InvalidRowNumber(u32),
18
19    /// The column number is out of the allowed range (1..=16_384).
20    #[error("invalid column number: {0}")]
21    InvalidColumnNumber(u32),
22
23    /// No sheet with the given name exists in the workbook.
24    #[error("sheet '{name}' does not exist")]
25    SheetNotFound { name: String },
26
27    /// A sheet with the given name already exists.
28    #[error("sheet '{name}' already exists")]
29    SheetAlreadyExists { name: String },
30
31    /// The sheet name violates Excel naming rules.
32    #[error("invalid sheet name: {0}")]
33    InvalidSheetName(String),
34
35    /// An underlying I/O error.
36    #[error("I/O error: {0}")]
37    Io(#[from] std::io::Error),
38
39    /// An error originating from the ZIP layer.
40    #[error("ZIP error: {0}")]
41    Zip(String),
42
43    /// An error encountered while parsing XML.
44    #[error("XML parse error: {0}")]
45    XmlParse(String),
46
47    /// An error encountered while deserializing XML into typed structures.
48    #[error("XML deserialization error: {0}")]
49    XmlDeserialize(String),
50
51    /// Column width exceeds the allowed maximum (255).
52    #[error("column width {width} exceeds maximum {max}")]
53    ColumnWidthExceeded { width: f64, max: f64 },
54
55    /// Row height exceeds the allowed maximum (409).
56    #[error("row height {height} exceeds maximum {max}")]
57    RowHeightExceeded { height: f64, max: f64 },
58
59    /// A cell value exceeds the maximum character limit.
60    #[error("cell value too long: {length} characters (max {max})")]
61    CellValueTooLong { length: usize, max: usize },
62
63    /// The style ID was not found in the stylesheet.
64    #[error("style not found: {id}")]
65    StyleNotFound { id: u32 },
66
67    /// Too many cell styles have been registered.
68    #[error("cell styles exceeded maximum ({max})")]
69    CellStylesExceeded { max: usize },
70
71    /// A row has already been written; rows must be written in ascending order.
72    #[error("row {row} has already been written (must write rows in ascending order)")]
73    StreamRowAlreadyWritten { row: u32 },
74
75    /// The stream writer has already been finished.
76    #[error("stream writer already finished")]
77    StreamAlreadyFinished,
78
79    /// Column widths cannot be set after rows have been written.
80    #[error("cannot set column width after rows have been written")]
81    StreamColumnsAfterRows,
82
83    /// Merge cell ranges overlap.
84    #[error("merge cell range '{new}' overlaps with existing range '{existing}'")]
85    MergeCellOverlap { new: String, existing: String },
86
87    /// The specified merge cell range was not found.
88    #[error("merge cell range '{0}' not found")]
89    MergeCellNotFound(String),
90
91    /// The defined name is invalid.
92    #[error("invalid defined name: {0}")]
93    InvalidDefinedName(String),
94
95    /// The specified defined name was not found.
96    #[error("defined name '{name}' not found")]
97    DefinedNameNotFound { name: String },
98
99    /// A circular reference was detected during formula evaluation.
100    #[error("circular reference detected at {cell}")]
101    CircularReference { cell: String },
102
103    /// The formula references an unknown function.
104    #[error("unknown function: {name}")]
105    UnknownFunction { name: String },
106
107    /// A function received the wrong number of arguments.
108    #[error("function {name} expects {expected} arguments, got {got}")]
109    WrongArgCount {
110        name: String,
111        expected: String,
112        got: usize,
113    },
114
115    /// A general formula evaluation error.
116    #[error("formula evaluation error: {0}")]
117    FormulaError(String),
118
119    /// The specified pivot table was not found.
120    #[error("pivot table '{name}' not found")]
121    PivotTableNotFound { name: String },
122
123    /// A pivot table with the given name already exists.
124    #[error("pivot table '{name}' already exists")]
125    PivotTableAlreadyExists { name: String },
126
127    /// The specified table was not found.
128    #[error("table '{name}' not found")]
129    TableNotFound { name: String },
130
131    /// A table with the given name already exists.
132    #[error("table '{name}' already exists")]
133    TableAlreadyExists { name: String },
134
135    /// The source data range for a pivot table is invalid.
136    #[error("invalid source range: {0}")]
137    InvalidSourceRange(String),
138
139    /// The specified slicer was not found.
140    #[error("slicer '{name}' not found")]
141    SlicerNotFound { name: String },
142
143    /// A slicer with the given name already exists.
144    #[error("slicer '{name}' already exists")]
145    SlicerAlreadyExists { name: String },
146
147    /// The specified column was not found in the table.
148    #[error("column '{column}' not found in table '{table}'")]
149    TableColumnNotFound { table: String, column: String },
150
151    /// The image format is not supported.
152    #[error("unsupported image format: {format}")]
153    UnsupportedImageFormat { format: String },
154
155    /// The file is encrypted and requires a password to open.
156    #[error("file is encrypted, password required")]
157    FileEncrypted,
158
159    /// The provided password is incorrect.
160    #[error("incorrect password")]
161    IncorrectPassword,
162
163    /// The encryption method is not supported.
164    #[error("unsupported encryption method: {0}")]
165    UnsupportedEncryption(String),
166
167    /// The outline level exceeds the allowed maximum (7).
168    #[error("outline level {level} exceeds maximum {max}")]
169    OutlineLevelExceeded { level: u8, max: u8 },
170
171    /// A merge cell reference format is invalid.
172    #[error("invalid merge cell reference: {0}")]
173    InvalidMergeCellReference(String),
174
175    /// A cell range reference (sqref) is invalid.
176    #[error("invalid reference: {reference}")]
177    InvalidReference { reference: String },
178
179    /// A function argument or configuration value is invalid.
180    #[error("invalid argument: {0}")]
181    InvalidArgument(String),
182
183    /// The file extension is not a supported OOXML spreadsheet format.
184    #[error("unsupported file extension: {0}")]
185    UnsupportedFileExtension(String),
186
187    /// The total decompressed size of the ZIP archive exceeds the safety limit.
188    #[error("ZIP decompressed size {size} bytes exceeds limit of {limit} bytes")]
189    ZipSizeExceeded { size: u64, limit: u64 },
190
191    /// The number of entries in the ZIP archive exceeds the safety limit.
192    #[error("ZIP entry count {count} exceeds limit of {limit}")]
193    ZipEntryCountExceeded { count: usize, limit: usize },
194
195    /// The specified threaded comment was not found.
196    #[error("threaded comment '{id}' not found")]
197    ThreadedCommentNotFound { id: String },
198
199    /// No chart was found at the specified cell.
200    #[error("no chart found at cell '{cell}' on sheet '{sheet}'")]
201    ChartNotFound { sheet: String, cell: String },
202
203    /// No picture was found at the specified cell.
204    #[error("no picture found at cell '{cell}' on sheet '{sheet}'")]
205    PictureNotFound { sheet: String, cell: String },
206
207    /// An internal or otherwise unclassified error.
208    #[error("internal error: {0}")]
209    Internal(String),
210}
211
212/// A convenience alias used throughout the crate.
213pub type Result<T> = std::result::Result<T, Error>;
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_error_display_invalid_cell_reference() {
221        let err = Error::InvalidCellReference("XYZ0".to_string());
222        assert_eq!(err.to_string(), "invalid cell reference: XYZ0");
223    }
224
225    #[test]
226    fn test_error_display_sheet_not_found() {
227        let err = Error::SheetNotFound {
228            name: "Missing".to_string(),
229        };
230        assert_eq!(err.to_string(), "sheet 'Missing' does not exist");
231    }
232
233    #[test]
234    fn test_error_display_sheet_already_exists() {
235        let err = Error::SheetAlreadyExists {
236            name: "Sheet1".to_string(),
237        };
238        assert_eq!(err.to_string(), "sheet 'Sheet1' already exists");
239    }
240
241    #[test]
242    fn test_error_display_invalid_sheet_name() {
243        let err = Error::InvalidSheetName("bad[name".to_string());
244        assert_eq!(err.to_string(), "invalid sheet name: bad[name");
245    }
246
247    #[test]
248    fn test_error_display_invalid_row_number() {
249        let err = Error::InvalidRowNumber(0);
250        assert_eq!(err.to_string(), "invalid row number: 0");
251    }
252
253    #[test]
254    fn test_error_display_invalid_column_number() {
255        let err = Error::InvalidColumnNumber(99999);
256        assert_eq!(err.to_string(), "invalid column number: 99999");
257    }
258
259    #[test]
260    fn test_error_display_io() {
261        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
262        let err = Error::Io(io_err);
263        assert_eq!(err.to_string(), "I/O error: gone");
264    }
265
266    #[test]
267    fn test_error_display_zip() {
268        let err = Error::Zip("corrupted archive".to_string());
269        assert_eq!(err.to_string(), "ZIP error: corrupted archive");
270    }
271
272    #[test]
273    fn test_error_display_xml_parse() {
274        let err = Error::XmlParse("unexpected EOF".to_string());
275        assert_eq!(err.to_string(), "XML parse error: unexpected EOF");
276    }
277
278    #[test]
279    fn test_error_display_xml_deserialize() {
280        let err = Error::XmlDeserialize("missing attribute".to_string());
281        assert_eq!(
282            err.to_string(),
283            "XML deserialization error: missing attribute"
284        );
285    }
286
287    #[test]
288    fn test_error_display_cell_value_too_long() {
289        let err = Error::CellValueTooLong {
290            length: 40000,
291            max: 32767,
292        };
293        assert_eq!(
294            err.to_string(),
295            "cell value too long: 40000 characters (max 32767)"
296        );
297    }
298
299    #[test]
300    fn test_error_display_outline_level_exceeded() {
301        let err = Error::OutlineLevelExceeded { level: 8, max: 7 };
302        assert_eq!(err.to_string(), "outline level 8 exceeds maximum 7");
303    }
304
305    #[test]
306    fn test_error_display_invalid_merge_cell_reference() {
307        let err = Error::InvalidMergeCellReference("bad ref".to_string());
308        assert_eq!(err.to_string(), "invalid merge cell reference: bad ref");
309    }
310
311    #[test]
312    fn test_error_display_unsupported_file_extension() {
313        let err = Error::UnsupportedFileExtension("csv".to_string());
314        assert_eq!(err.to_string(), "unsupported file extension: csv");
315    }
316
317    #[test]
318    fn test_error_display_internal() {
319        let err = Error::Internal("something went wrong".to_string());
320        assert_eq!(err.to_string(), "internal error: something went wrong");
321    }
322
323    #[test]
324    fn test_from_io_error() {
325        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
326        let err: Error = io_err.into();
327        assert!(matches!(err, Error::Io(_)));
328    }
329
330    #[test]
331    fn test_error_is_send_and_sync() {
332        fn assert_send<T: Send>() {}
333        fn assert_sync<T: Sync>() {}
334        assert_send::<Error>();
335        assert_sync::<Error>();
336    }
337}