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
8pub const XLSX_MAX_ROWS: usize = 1_048_576;
10
11pub const XLSX_MAX_COLUMNS: usize = 16_384;
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum SheetRef<'a> {
17 Index(usize),
18 Name(&'a str),
19}
20
21#[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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
37pub struct ReadLimits {
38 pub max_rows: Option<usize>,
40 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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
65pub struct MultiSheetReadLimits {
66 pub max_sheets: Option<usize>,
68 pub max_rows_per_sheet: Option<usize>,
70 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#[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
115pub fn from_xlsx<T: ExcelRow>(bytes: &[u8]) -> Result<Vec<T>, ExcelError> {
117 from_reader(Cursor::new(bytes))
118}
119
120pub fn from_reader<T: ExcelRow, R: Read + Seek>(reader: R) -> Result<Vec<T>, ExcelError> {
122 from_reader_with_options(reader, ReadOptions::default())
123}
124
125pub 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
133pub 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
142pub 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
150pub 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
158pub 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
173pub fn from_xlsx_multi<T: ExcelRow>(bytes: &[u8]) -> Result<Vec<ParsedSheet<T>>, ExcelError> {
175 from_reader_multi(Cursor::new(bytes))
176}
177
178pub 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
186pub 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
193pub 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}