Skip to main content

excelx/
read.rs

1use std::collections::HashMap;
2use std::io::{Cursor, Read, Seek};
3
4use calamine::{Data, Range, Reader, Xlsx};
5
6use crate::{CellValue, ColumnDef, ExcelError, ExcelRow, RowView, validate_columns};
7
8/// Maximum number of rows in an XLSX worksheet, including the header row.
9pub const XLSX_MAX_ROWS: usize = 1_048_576;
10
11/// Maximum number of columns in an XLSX worksheet.
12pub const XLSX_MAX_COLUMNS: usize = 16_384;
13
14/// Selects a worksheet by workbook order or visible worksheet name.
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum SheetRef<'a> {
17    Index(usize),
18    Name(&'a str),
19}
20
21/// Options for parsing an XLSX workbook.
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub struct ReadOptions<'a> {
24    pub sheet: SheetRef<'a>,
25}
26
27impl<'a> Default for ReadOptions<'a> {
28    fn default() -> Self {
29        Self {
30            sheet: SheetRef::Index(0),
31        }
32    }
33}
34
35/// Optional safety limits for parsing untrusted or user-uploaded workbooks.
36#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
37pub struct ReadLimits {
38    /// Maximum number of non-empty data rows to parse per worksheet.
39    pub max_rows: Option<usize>,
40    /// Maximum number of schema columns accepted for the row type.
41    pub max_columns: Option<usize>,
42}
43
44impl ReadLimits {
45    pub const fn new() -> Self {
46        Self {
47            max_rows: None,
48            max_columns: None,
49        }
50    }
51
52    pub const fn max_rows(mut self, max_rows: usize) -> Self {
53        self.max_rows = Some(max_rows);
54        self
55    }
56
57    pub const fn max_columns(mut self, max_columns: usize) -> Self {
58        self.max_columns = Some(max_columns);
59        self
60    }
61}
62
63/// Optional safety limits for parsing homogeneous multi-sheet workbooks.
64#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
65pub struct MultiSheetReadLimits {
66    /// Maximum number of worksheets to parse.
67    pub max_sheets: Option<usize>,
68    /// Maximum number of non-empty data rows to parse per worksheet.
69    pub max_rows_per_sheet: Option<usize>,
70    /// Maximum number of schema columns accepted for the row type.
71    pub max_columns_per_sheet: Option<usize>,
72}
73
74impl MultiSheetReadLimits {
75    pub const fn new() -> Self {
76        Self {
77            max_sheets: None,
78            max_rows_per_sheet: None,
79            max_columns_per_sheet: None,
80        }
81    }
82
83    pub const fn max_sheets(mut self, max_sheets: usize) -> Self {
84        self.max_sheets = Some(max_sheets);
85        self
86    }
87
88    pub const fn max_rows_per_sheet(mut self, max_rows_per_sheet: usize) -> Self {
89        self.max_rows_per_sheet = Some(max_rows_per_sheet);
90        self
91    }
92
93    pub const fn max_columns_per_sheet(mut self, max_columns_per_sheet: usize) -> Self {
94        self.max_columns_per_sheet = Some(max_columns_per_sheet);
95        self
96    }
97}
98
99/// Parsed rows for one worksheet in a homogeneous multi-sheet workbook.
100#[derive(Clone, Debug, Eq, PartialEq)]
101pub struct ParsedSheet<T> {
102    pub name: String,
103    pub rows: Vec<T>,
104}
105
106impl<T> ParsedSheet<T> {
107    pub fn new(name: impl Into<String>, rows: Vec<T>) -> Self {
108        Self {
109            name: name.into(),
110            rows,
111        }
112    }
113}
114
115/// Parse the first worksheet in an XLSX byte slice into typed rows.
116pub fn from_xlsx<T: ExcelRow>(bytes: &[u8]) -> Result<Vec<T>, ExcelError> {
117    from_reader(Cursor::new(bytes))
118}
119
120/// Parse the first worksheet in an XLSX reader into typed rows.
121pub fn from_reader<T: ExcelRow, R: Read + Seek>(reader: R) -> Result<Vec<T>, ExcelError> {
122    from_reader_with_options(reader, ReadOptions::default())
123}
124
125/// Parse one selected worksheet in an XLSX byte slice into typed rows.
126pub fn from_xlsx_sheet<T: ExcelRow>(
127    bytes: &[u8],
128    sheet: SheetRef<'_>,
129) -> Result<Vec<T>, ExcelError> {
130    from_xlsx_with_options(bytes, ReadOptions { sheet })
131}
132
133/// Parse one selected worksheet in an XLSX byte slice with explicit safety limits.
134pub fn from_xlsx_with_limits<T: ExcelRow>(
135    bytes: &[u8],
136    sheet: SheetRef<'_>,
137    limits: ReadLimits,
138) -> Result<Vec<T>, ExcelError> {
139    from_reader_with_limits(Cursor::new(bytes), sheet, limits)
140}
141
142/// Parse one selected worksheet in an XLSX byte slice with explicit options.
143pub fn from_xlsx_with_options<T: ExcelRow>(
144    bytes: &[u8],
145    options: ReadOptions<'_>,
146) -> Result<Vec<T>, ExcelError> {
147    from_reader_with_options(Cursor::new(bytes), options)
148}
149
150/// Parse one selected worksheet in an XLSX reader with explicit options.
151pub fn from_reader_with_options<T: ExcelRow, R: Read + Seek>(
152    reader: R,
153    options: ReadOptions<'_>,
154) -> Result<Vec<T>, ExcelError> {
155    from_reader_with_limits(reader, options.sheet, ReadLimits::default())
156}
157
158/// Parse one selected worksheet in an XLSX reader with explicit safety limits.
159pub fn from_reader_with_limits<T: ExcelRow, R: Read + Seek>(
160    reader: R,
161    sheet: SheetRef<'_>,
162    limits: ReadLimits,
163) -> Result<Vec<T>, ExcelError> {
164    let columns = T::columns();
165    let sorted_columns = validate_columns(&columns)?;
166    validate_column_limit("schema", sorted_columns.len(), limits.max_columns)?;
167    let mut workbook: Xlsx<R> = Xlsx::new(reader)?;
168    let range = worksheet_range(&mut workbook, sheet)?;
169
170    parse_range(range, &columns, &sorted_columns, limits)
171}
172
173/// Parse all worksheets in an XLSX byte slice as the same row type.
174pub fn from_xlsx_multi<T: ExcelRow>(bytes: &[u8]) -> Result<Vec<ParsedSheet<T>>, ExcelError> {
175    from_reader_multi(Cursor::new(bytes))
176}
177
178/// Parse all worksheets in an XLSX byte slice with explicit safety limits.
179pub fn from_xlsx_multi_with_limits<T: ExcelRow>(
180    bytes: &[u8],
181    limits: MultiSheetReadLimits,
182) -> Result<Vec<ParsedSheet<T>>, ExcelError> {
183    from_reader_multi_with_limits(Cursor::new(bytes), limits)
184}
185
186/// Parse all worksheets in an XLSX reader as the same row type.
187pub fn from_reader_multi<T: ExcelRow, R: Read + Seek>(
188    reader: R,
189) -> Result<Vec<ParsedSheet<T>>, ExcelError> {
190    from_reader_multi_with_limits(reader, MultiSheetReadLimits::default())
191}
192
193/// Parse all worksheets in an XLSX reader with explicit safety limits.
194pub fn from_reader_multi_with_limits<T: ExcelRow, R: Read + Seek>(
195    reader: R,
196    limits: MultiSheetReadLimits,
197) -> Result<Vec<ParsedSheet<T>>, ExcelError> {
198    let columns = T::columns();
199    let sorted_columns = validate_columns(&columns)?;
200    validate_column_limit("schema", sorted_columns.len(), limits.max_columns_per_sheet)?;
201    let mut workbook: Xlsx<R> = Xlsx::new(reader)?;
202    let sheet_names = workbook.sheet_names();
203
204    if sheet_names.is_empty() {
205        return Err(ExcelError::Parse(
206            "workbook does not contain any worksheets".to_owned(),
207        ));
208    }
209    validate_sheet_limit(sheet_names.len(), limits.max_sheets)?;
210
211    let mut parsed_sheets = Vec::with_capacity(sheet_names.len());
212    for name in sheet_names {
213        let range = workbook.worksheet_range(&name)?;
214        let rows = parse_range(
215            range,
216            &columns,
217            &sorted_columns,
218            ReadLimits {
219                max_rows: limits.max_rows_per_sheet,
220                max_columns: limits.max_columns_per_sheet,
221            },
222        )?;
223        parsed_sheets.push(ParsedSheet::new(name, rows));
224    }
225
226    Ok(parsed_sheets)
227}
228
229fn parse_range<T: ExcelRow>(
230    range: Range<Data>,
231    columns: &[ColumnDef],
232    sorted_columns: &[ColumnDef],
233    limits: ReadLimits,
234) -> Result<Vec<T>, ExcelError> {
235    let mut rows = range.rows();
236    let header_row = rows
237        .next()
238        .ok_or_else(|| ExcelError::Parse("worksheet does not contain a header row".to_owned()))?;
239    let header_map = build_header_map(header_row)?;
240    validate_column_limit("worksheet", header_map.len(), limits.max_columns)?;
241    ensure_required_headers(sorted_columns, &header_map)?;
242
243    let mut parsed_rows = Vec::new();
244    for (relative_index, row) in rows.enumerate() {
245        if is_empty_row(row) {
246            continue;
247        }
248
249        let excel_row_number = relative_index + 2;
250        validate_row_limit(parsed_rows.len() + 1, limits.max_rows)?;
251        let values = values_for_schema(row, columns, &header_map)?;
252        let row_view = RowView::new(excel_row_number, columns, values);
253        parsed_rows.push(T::from_row(&row_view)?);
254    }
255
256    Ok(parsed_rows)
257}
258
259fn worksheet_range<R: Read + Seek>(
260    workbook: &mut Xlsx<R>,
261    sheet: SheetRef<'_>,
262) -> Result<Range<Data>, ExcelError> {
263    match sheet {
264        SheetRef::Index(index) => workbook
265            .worksheet_range_at(index)
266            .ok_or_else(|| missing_sheet_by_index(index))?
267            .map_err(ExcelError::from),
268        SheetRef::Name(name) => {
269            if !workbook
270                .sheet_names()
271                .iter()
272                .any(|sheet_name| sheet_name == name)
273            {
274                return Err(missing_sheet_by_name(name));
275            }
276
277            workbook.worksheet_range(name).map_err(ExcelError::from)
278        }
279    }
280}
281
282fn missing_sheet_by_index(index: usize) -> ExcelError {
283    if index == 0 {
284        ExcelError::Parse("workbook does not contain any worksheets".to_owned())
285    } else {
286        ExcelError::Parse(format!("worksheet index {index} does not exist"))
287    }
288}
289
290fn missing_sheet_by_name(name: &str) -> ExcelError {
291    ExcelError::Parse(format!("worksheet named `{name}` does not exist"))
292}
293
294fn validate_column_limit(
295    label: &str,
296    count: usize,
297    limit: Option<usize>,
298) -> Result<(), ExcelError> {
299    if count > XLSX_MAX_COLUMNS {
300        return Err(ExcelError::LimitExceeded(format!(
301            "{label} defines {count} columns but XLSX supports at most {XLSX_MAX_COLUMNS}"
302        )));
303    }
304
305    if let Some(limit) = limit {
306        if count > limit {
307            return Err(ExcelError::LimitExceeded(format!(
308                "{label} defines {count} columns but configured max is {limit}"
309            )));
310        }
311    }
312
313    Ok(())
314}
315
316fn validate_row_limit(count: usize, limit: Option<usize>) -> Result<(), ExcelError> {
317    let max_data_rows = XLSX_MAX_ROWS - 1;
318    if count > max_data_rows {
319        return Err(ExcelError::LimitExceeded(format!(
320            "worksheet contains {count} data rows but XLSX supports at most {max_data_rows}"
321        )));
322    }
323
324    if let Some(limit) = limit {
325        if count > limit {
326            return Err(ExcelError::LimitExceeded(format!(
327                "worksheet contains {count} data rows but configured max is {limit}"
328            )));
329        }
330    }
331
332    Ok(())
333}
334
335fn validate_sheet_limit(count: usize, limit: Option<usize>) -> Result<(), ExcelError> {
336    if let Some(limit) = limit {
337        if count > limit {
338            return Err(ExcelError::LimitExceeded(format!(
339                "workbook contains {count} worksheets but configured max is {limit}"
340            )));
341        }
342    }
343
344    Ok(())
345}
346
347fn build_header_map(row: &[Data]) -> Result<HashMap<String, usize>, ExcelError> {
348    let mut headers = HashMap::with_capacity(row.len());
349
350    for (index, cell) in row.iter().enumerate() {
351        if matches!(cell, Data::Empty) {
352            continue;
353        }
354
355        let header = match cell {
356            Data::String(value) => value.trim().to_owned(),
357            Data::Int(value) => value.to_string(),
358            Data::Float(value) => value.to_string(),
359            Data::Bool(value) => value.to_string(),
360            other => {
361                return Err(ExcelError::Parse(format!(
362                    "unsupported header cell type at column {}: {other}",
363                    index + 1
364                )));
365            }
366        };
367
368        if header.is_empty() {
369            continue;
370        }
371
372        if headers.insert(header.clone(), index).is_some() {
373            return Err(ExcelError::DuplicateHeader(header));
374        }
375    }
376
377    Ok(headers)
378}
379
380fn ensure_required_headers(
381    columns: &[ColumnDef],
382    header_map: &HashMap<String, usize>,
383) -> Result<(), ExcelError> {
384    for column in columns {
385        if !header_map.contains_key(column.header) {
386            return Err(ExcelError::MissingHeader(column.header.to_owned()));
387        }
388    }
389
390    Ok(())
391}
392
393fn values_for_schema(
394    row: &[Data],
395    columns: &[ColumnDef],
396    header_map: &HashMap<String, usize>,
397) -> Result<Vec<CellValue>, ExcelError> {
398    columns
399        .iter()
400        .map(|column| {
401            let index = header_map
402                .get(column.header)
403                .ok_or_else(|| ExcelError::MissingHeader(column.header.to_owned()))?;
404
405            row.get(*index)
406                .map(CellValue::try_from)
407                .unwrap_or(Ok(CellValue::Empty))
408        })
409        .collect()
410}
411
412fn is_empty_row(row: &[Data]) -> bool {
413    row.iter().all(|cell| matches!(cell, Data::Empty))
414}