diff --git a/src/spreadsheet/mod.rs b/src/spreadsheet/mod.rs index 6ab6f4d..ae71930 100644 --- a/src/spreadsheet/mod.rs +++ b/src/spreadsheet/mod.rs @@ -27,6 +27,25 @@ pub(crate) mod xlsx; pub(crate) mod criteria; pub(crate) mod sheet; +pub(crate) fn resolve_number_format( + number_formats: &[CellType], + file_name: &str, + sheet_name: &str, + row: usize, + col: usize, + style_index: usize, +) -> Result { + number_formats.get(style_index).copied().ok_or_else(|| { + SpreadsheetError::CellStyleIndexError( + file_name.to_owned(), + sheet_name.to_owned(), + crate::spreadsheet::reference::index_to_reference(row, col), + style_index, + number_formats.len(), + ) + }) +} + #[derive(Error, Debug)] pub(crate) enum SpreadsheetError { /// Error indicating the spreadsheet format is not supported @@ -48,6 +67,10 @@ pub(crate) enum SpreadsheetError { /// Error indicating a specific cell value is invalid #[error("Cell '[{0}]{1}!{2}': {3}")] CellValueError(String, String, String, String), + + /// Error indicating a cell style index is outside the workbook's style table. + #[error("Cell '[{0}]{1}!{2}': invalid style index {3}; workbook defines {4} styles")] + CellStyleIndexError(String, String, String, usize, usize), } pub(crate) trait Spreadsheet { diff --git a/src/spreadsheet/xls.rs b/src/spreadsheet/xls.rs index 0e68ef1..02119c4 100644 --- a/src/spreadsheet/xls.rs +++ b/src/spreadsheet/xls.rs @@ -10,6 +10,7 @@ use crate::spreadsheet::cell::CellType; use crate::spreadsheet::criteria::Criteria; use crate::spreadsheet::excel::load_number_formats; use crate::spreadsheet::reference::index_to_reference; +use crate::spreadsheet::resolve_number_format; use crate::spreadsheet::sheet::Sheet; use crate::spreadsheet::Spreadsheet; use crate::spreadsheet::SpreadsheetError; @@ -201,7 +202,14 @@ impl Spreadsheet for XlsSpreadsheet { } last_row = Some(row); let index = self.reader.read_u16()? as usize; - let kind = self.number_formats[index]; + let kind = resolve_number_format( + &self.number_formats, + &sheet.file_name, + &sheet.name, + row, + col, + index, + )?; let value = self.reader.read_rk_number()?; sheet.push(Cell { row, @@ -234,7 +242,14 @@ impl Spreadsheet for XlsSpreadsheet { }; let kind = match either { Either::Left(kind) => kind, - Either::Right(index) => self.number_formats[index], + Either::Right(index) => resolve_number_format( + &self.number_formats, + &sheet.file_name, + &sheet.name, + row, + col, + index, + )?, }; if kind != CellType::Error { if !criteria.nulls.contains(&value) { diff --git a/src/spreadsheet/xlsb.rs b/src/spreadsheet/xlsb.rs index bb1231e..12dafe7 100644 --- a/src/spreadsheet/xlsb.rs +++ b/src/spreadsheet/xlsb.rs @@ -10,6 +10,7 @@ use crate::spreadsheet::criteria::Criteria; use crate::spreadsheet::excel; use crate::spreadsheet::excel::load_relationships; use crate::spreadsheet::reference::index_to_reference; +use crate::spreadsheet::resolve_number_format; use crate::spreadsheet::sheet::Sheet; use crate::spreadsheet::Spreadsheet; use crate::spreadsheet::SpreadsheetError; @@ -221,7 +222,14 @@ impl Spreadsheet for XlsbSpreadsheet { }; let kind = match either { Either::Left(kind) => kind, - Either::Right(index) => (self.number_formats)[index], + Either::Right(index) => resolve_number_format( + &self.number_formats, + &sheet.file_name, + &sheet.name, + row, + col, + index, + )?, }; if kind != CellType::Error { if !criteria.nulls.contains(&value) { @@ -451,4 +459,4 @@ fn read_rk_cell(reader: &mut Biff12Reader>>) -> }; (Either::Right(index), value) - } \ No newline at end of file + } diff --git a/src/spreadsheet/xlsx.rs b/src/spreadsheet/xlsx.rs index 026954b..1cda225 100644 --- a/src/spreadsheet/xlsx.rs +++ b/src/spreadsheet/xlsx.rs @@ -15,6 +15,7 @@ use crate::spreadsheet::excel; use crate::spreadsheet::excel::load_relationships; use crate::spreadsheet::reference::index_to_reference; use crate::spreadsheet::reference::reference_to_index; +use crate::spreadsheet::resolve_number_format; use crate::spreadsheet::sheet::Sheet; use quick_xml::events::Event; use quick_xml::name::QName; @@ -231,7 +232,14 @@ impl Spreadsheet for XlsxSpreadsheet { if let Some(format_id) = event.get_attribute_value("s")? { if kind == CellType::Number && !format_id.is_empty() { let index = format_id.parse::()?; - kind = self.number_formats[index]; + kind = resolve_number_format( + &self.number_formats, + &sheet.file_name, + &sheet.name, + row, + col, + index, + )?; } } } else { @@ -519,3 +527,117 @@ fn read_string_value( }); Ok(text) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::spreadsheet::criteria::Criteria; + use std::collections::HashSet; + use std::fs::File; + use std::io::Write; + use std::path::Path; + use std::path::PathBuf; + use std::time::SystemTime; + use std::time::UNIX_EPOCH; + use zip::write::SimpleFileOptions; + use zip::ZipWriter; + + #[test] + fn invalid_style_index_returns_error() { + let path = invalid_style_workbook_path(); + write_invalid_style_workbook(&path); + + let mut spreadsheet = XlsxSpreadsheet::open(path.to_str().unwrap()).unwrap(); + let result = spreadsheet.read_sheets(&Criteria { + sheet_name_patterns: None, + sheet_limit: None, + range: None, + rows_limit: None, + nulls: HashSet::from(["".to_string()]), + error_as_null: false, + skip_empty_rows: false, + end_at_empty_row: false, + spread_merged_cells: false, + }); + + std::fs::remove_file(path).unwrap(); + let error = match result { + Ok(_) => panic!("expected invalid style index error"), + Err(error) => error.to_string(), + }; + assert!(error.contains("Sheet1!A11")); + assert!(error.contains("invalid style index 999")); + assert!(error.contains("workbook defines 1 styles")); + } + + fn invalid_style_workbook_path() -> PathBuf { + let id = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + std::env::temp_dir().join(format!("rusty-sheet-invalid-style-{id}.xlsx")) + } + + fn write_invalid_style_workbook(path: &Path) { + let file = File::create(path).unwrap(); + let mut zip = ZipWriter::new(file); + let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated); + let rows = (2..11) + .map(|row| format!("{row}")) + .collect::(); + let sheet = format!( + r#" + + value{rows}11 +"# + ); + + for (name, content) in [ + ( + "[Content_Types].xml", + r#" + + + + + + +"#.to_string(), + ), + ( + "_rels/.rels", + r#" + + +"#.to_string(), + ), + ( + "xl/workbook.xml", + r#" + + +"#.to_string(), + ), + ( + "xl/_rels/workbook.xml.rels", + r#" + + + +"#.to_string(), + ), + ( + "xl/styles.xml", + r#" + + +"#.to_string(), + ), + ("xl/worksheets/sheet1.xml", sheet), + ] { + zip.start_file(name, options).unwrap(); + zip.write_all(content.as_bytes()).unwrap(); + } + zip.finish().unwrap(); + } +}