use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Cursor, Read, Seek};
use std::path::{Path, PathBuf};
use std::sync::mpsc::{self, Receiver, SyncSender};
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use std::thread::{self, JoinHandle};
use calamine::{CellErrorType, Data, DataType, ExcelDateTime, ExcelDateTimeType};
use quick_xml::Reader;
use quick_xml::encoding::Decoder;
use quick_xml::events::{BytesRef, BytesStart, Event};
use zip::ZipArchive;
use zip::result::ZipError;
use crate::cell::MAX_EXCEL_COLUMN;
use crate::reader::{SelectedCellMetadata, SelectedRow};
use crate::{
CellReference, Error, ExcelRange, ReadOptions, Result, SheetInfo as PublicSheetInfo, SheetType,
SheetVisibility,
};
const ROW_BUFFER_SIZE: usize = 8;
pub(super) struct StreamingRawRows {
receiver: Option<Receiver<Result<SelectedRow>>>,
worker: Option<JoinHandle<()>>,
sheet_name: String,
cancelled: Arc<AtomicBool>,
}
pub(super) fn visit_raw_rows<F>(
bytes: &[u8],
options: &ReadOptions,
preserve_structure: bool,
mut visitor: F,
) -> Result<String>
where
F: FnMut(&str, SelectedRow) -> Result<bool>,
{
validate_range(options)?;
let mut archive = ZipArchive::new(Cursor::new(bytes))
.map_err(|error| stream_error("cannot open in-memory XLSX data:", error))?;
let context = prepare_workbook(&mut archive, options, preserve_structure)?;
let sheet_name = context.sheet_name.clone();
let cancelled = AtomicBool::new(false);
let extent = scan_worksheet_extent(&mut archive, &context.sheet_path, &cancelled)?;
let mut visitor_error = None;
stream_worksheet(
&mut archive,
context,
extent,
options,
&cancelled,
&mut |row| match visitor(&sheet_name, row) {
Ok(should_continue) => should_continue,
Err(error) => {
visitor_error = Some(error);
false
}
},
)?;
if let Some(error) = visitor_error {
return Err(error);
}
Ok(sheet_name)
}
pub(super) fn sheet_names_from_bytes(bytes: &[u8]) -> Result<Vec<String>> {
let mut archive = ZipArchive::new(Cursor::new(bytes))
.map_err(|error| stream_error("cannot open in-memory XLSX data:", error))?;
Ok(read_workbook_info(&mut archive)?.sheets.into_iter().map(|sheet| sheet.name).collect())
}
pub(super) fn sheet_names(path: impl AsRef<Path>) -> Result<Vec<String>> {
let file = File::open(path)?;
let mut archive = ZipArchive::new(BufReader::new(file))
.map_err(|error| stream_error("cannot open XLSX workbook:", error))?;
Ok(read_workbook_info(&mut archive)?.sheets.into_iter().map(|sheet| sheet.name).collect())
}
pub(super) fn sheet_info(path: impl AsRef<Path>) -> Result<Vec<PublicSheetInfo>> {
let file = File::open(path)?;
let mut archive = ZipArchive::new(BufReader::new(file))
.map_err(|error| stream_error("cannot open XLSX workbook:", error))?;
read_sheet_info(&mut archive)
}
pub(super) fn sheet_info_from_bytes(bytes: &[u8]) -> Result<Vec<PublicSheetInfo>> {
let mut archive = ZipArchive::new(Cursor::new(bytes))
.map_err(|error| stream_error("cannot open in-memory XLSX data:", error))?;
read_sheet_info(&mut archive)
}
fn read_sheet_info<R>(archive: &mut ZipArchive<R>) -> Result<Vec<PublicSheetInfo>>
where
R: Read + Seek,
{
let workbook = read_workbook_info(archive)?;
let relationships = read_workbook_relationships(archive)?;
workbook
.sheets
.into_iter()
.enumerate()
.map(|(index, sheet)| {
let relationship = relationships.get(&sheet.relationship_id).ok_or_else(|| {
Error::stream(format!(
"worksheet relationship '{}' was not found",
sheet.relationship_id
))
})?;
let sheet_type = sheet_type_from_relationship(&relationship.relationship_type)?;
Ok(PublicSheetInfo::new(
sheet.id,
index,
sheet.name,
sheet_type,
sheet.visibility,
index == workbook.active_sheet_index,
))
})
.collect()
}
pub(super) fn sheet_dimensions(path: impl AsRef<Path>) -> Result<Vec<ExcelRange>> {
let file = File::open(path)?;
let mut archive = ZipArchive::new(BufReader::new(file))
.map_err(|error| stream_error("cannot open XLSX workbook:", error))?;
read_sheet_dimensions(&mut archive)
}
pub(super) fn sheet_dimensions_from_bytes(bytes: &[u8]) -> Result<Vec<ExcelRange>> {
let mut archive = ZipArchive::new(Cursor::new(bytes))
.map_err(|error| stream_error("cannot open in-memory XLSX data:", error))?;
read_sheet_dimensions(&mut archive)
}
fn read_sheet_dimensions<R>(archive: &mut ZipArchive<R>) -> Result<Vec<ExcelRange>>
where
R: Read + Seek,
{
let workbook = read_workbook_info(archive)?;
let relationships = read_workbook_relationships(archive)?;
let cancelled = AtomicBool::new(false);
let mut dimensions = Vec::with_capacity(workbook.sheets.len());
for sheet in workbook.sheets {
let sheet_path = &relationships
.get(&sheet.relationship_id)
.ok_or_else(|| {
Error::stream(format!(
"worksheet relationship '{}' was not found",
sheet.relationship_id
))
})?
.target;
let extent = match read_declared_worksheet_extent(archive, sheet_path)? {
Some(extent) => extent,
None => scan_worksheet_extent(archive, sheet_path, &cancelled)?,
};
dimensions.push(ExcelRange::from_bounds(
extent.start_row,
extent.start_column,
extent.end_row,
extent.end_column,
));
}
Ok(dimensions)
}
impl StreamingRawRows {
pub(super) fn open(
path: impl AsRef<Path>,
options: &ReadOptions,
preserve_structure: bool,
) -> Result<Self> {
validate_range(options)?;
let path = path.as_ref().to_owned();
let file = File::open(&path)?;
let (ready_sender, ready_receiver) = mpsc::sync_channel(0);
let (row_sender, row_receiver) = mpsc::sync_channel(ROW_BUFFER_SIZE);
let cancelled = Arc::new(AtomicBool::new(false));
let worker_cancelled = Arc::clone(&cancelled);
let options = options.clone();
let worker =
thread::Builder::new().name("miniexcel-xlsx-stream".to_owned()).spawn(move || {
worker_main(
path,
BufReader::new(file),
options,
preserve_structure,
worker_cancelled,
ready_sender,
row_sender,
)
})?;
let ready = match ready_receiver.recv() {
Ok(ready) => ready,
Err(_) => Err(Error::stream(
"the XLSX streaming worker stopped during initialization".to_owned(),
)),
};
match ready {
Ok(sheet_name) => Ok(Self {
receiver: Some(row_receiver),
worker: Some(worker),
sheet_name,
cancelled,
}),
Err(error) => {
drop(row_receiver);
let _ = worker.join();
Err(error)
}
}
}
pub(super) fn sheet_name(&self) -> &str {
&self.sheet_name
}
}
impl Iterator for StreamingRawRows {
type Item = Result<SelectedRow>;
fn next(&mut self) -> Option<Self::Item> {
self.receiver.as_ref()?.recv().ok()
}
}
impl Drop for StreamingRawRows {
fn drop(&mut self) {
self.cancelled.store(true, Ordering::Relaxed);
self.receiver.take();
if let Some(worker) = self.worker.take() {
let _ = worker.join();
}
}
}
struct WorkbookContext {
sheet_name: String,
sheet_path: String,
shared_strings: Vec<String>,
styles: Vec<CellStyle>,
is_1904: bool,
preserve_structure: bool,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
enum CellFormat {
#[default]
Other,
DateTime,
TimeDelta,
}
struct CellStyle {
format: CellFormat,
number_format: Option<Arc<str>>,
}
#[derive(Default)]
struct WorkbookInfo {
sheets: Vec<WorkbookSheet>,
is_1904: bool,
active_sheet_index: usize,
}
struct WorkbookSheet {
id: u32,
name: String,
relationship_id: String,
visibility: SheetVisibility,
}
struct WorkbookRelationship {
target: String,
relationship_type: String,
}
fn worker_main<R>(
path: PathBuf,
reader: R,
options: ReadOptions,
preserve_structure: bool,
cancelled: Arc<AtomicBool>,
ready_sender: SyncSender<Result<String>>,
row_sender: SyncSender<Result<SelectedRow>>,
) where
R: Read + Seek,
{
let mut archive = match ZipArchive::new(reader) {
Ok(archive) => archive,
Err(error) => {
let _ = ready_sender
.send(Err(stream_error(format!("cannot open '{}':", path.display()), error)));
return;
}
};
let context = match prepare_workbook(&mut archive, &options, preserve_structure) {
Ok(context) => context,
Err(error) => {
let _ = ready_sender.send(Err(error));
return;
}
};
if ready_sender.send(Ok(context.sheet_name.clone())).is_err() {
return;
}
let extent = match scan_worksheet_extent(&mut archive, &context.sheet_path, &cancelled) {
Ok(extent) => extent,
Err(error) => {
let _ = row_sender.send(Err(error));
return;
}
};
if cancelled.load(Ordering::Relaxed) {
return;
}
let mut emit = |row| row_sender.send(Ok(row)).is_ok();
if let Err(error) =
stream_worksheet(&mut archive, context, extent, &options, &cancelled, &mut emit)
{
let _ = row_sender.send(Err(error));
}
}
fn prepare_workbook<R>(
archive: &mut ZipArchive<R>,
options: &ReadOptions,
preserve_structure: bool,
) -> Result<WorkbookContext>
where
R: Read + Seek,
{
let workbook = read_workbook_info(archive)?;
let sheet = match options.sheet_name() {
Some(sheet_name) => workbook
.sheets
.iter()
.find(|sheet| sheet.name == sheet_name)
.ok_or_else(|| Error::sheet_not_found(sheet_name))?,
None => workbook.sheets.first().ok_or_else(Error::no_worksheets)?,
};
let sheet_path = read_relationship_target(archive, &sheet.relationship_id)?;
let shared_strings = read_shared_strings(archive)?;
let styles = read_styles(archive, preserve_structure)?;
Ok(WorkbookContext {
sheet_name: sheet.name.clone(),
sheet_path,
shared_strings,
styles,
is_1904: workbook.is_1904,
preserve_structure,
})
}
fn read_workbook_info<R>(archive: &mut ZipArchive<R>) -> Result<WorkbookInfo>
where
R: Read + Seek,
{
let file = archive
.by_name("xl/workbook.xml")
.map_err(|error| stream_error("cannot read xl/workbook.xml:", error))?;
let mut xml = Reader::from_reader(BufReader::new(file));
let mut buffer = Vec::new();
let mut workbook = WorkbookInfo::default();
loop {
match xml
.read_event_into(&mut buffer)
.map_err(|error| stream_error("invalid xl/workbook.xml:", error))?
{
Event::Start(event) | Event::Empty(event)
if is_name(event.name().as_ref(), b"workbookPr") =>
{
workbook.is_1904 = attribute(&event, xml.decoder(), b"date1904")?
.is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true"));
}
Event::Start(event) | Event::Empty(event)
if is_name(event.name().as_ref(), b"workbookView") =>
{
if let Some(index) = attribute(&event, xml.decoder(), b"activeTab")?
.and_then(|value| value.parse().ok())
{
workbook.active_sheet_index = index;
}
}
Event::Start(event) | Event::Empty(event)
if is_name(event.name().as_ref(), b"sheet") =>
{
let Some(name) =
attribute(&event, xml.decoder(), b"name")?.filter(|name| !name.is_empty())
else {
buffer.clear();
continue;
};
let Some(relationship_id) = attribute(&event, xml.decoder(), b"id")? else {
buffer.clear();
continue;
};
let id = attribute(&event, xml.decoder(), b"sheetId")?
.and_then(|value| value.parse().ok())
.unwrap_or(0);
let visibility = match attribute(&event, xml.decoder(), b"state")? {
None => SheetVisibility::Visible,
Some(state) if state.eq_ignore_ascii_case("visible") => {
SheetVisibility::Visible
}
Some(state) if state.eq_ignore_ascii_case("hidden") => SheetVisibility::Hidden,
Some(state) if state.eq_ignore_ascii_case("veryHidden") => {
SheetVisibility::VeryHidden
}
Some(state) => {
return Err(Error::stream(format!(
"unable to parse state '{state}' for worksheet '{name}'"
)));
}
};
workbook.sheets.push(WorkbookSheet { id, name, relationship_id, visibility });
}
Event::Eof => break,
_ => {}
}
buffer.clear();
}
Ok(workbook)
}
fn read_relationship_target<R>(archive: &mut ZipArchive<R>, relationship_id: &str) -> Result<String>
where
R: Read + Seek,
{
let file = archive
.by_name("xl/_rels/workbook.xml.rels")
.map_err(|error| stream_error("cannot read workbook relationships:", error))?;
let mut xml = Reader::from_reader(BufReader::new(file));
let mut buffer = Vec::new();
loop {
match xml
.read_event_into(&mut buffer)
.map_err(|error| stream_error("invalid workbook relationships:", error))?
{
Event::Start(event) | Event::Empty(event)
if is_name(event.name().as_ref(), b"Relationship") =>
{
if attribute(&event, xml.decoder(), b"Id")?.as_deref() == Some(relationship_id) {
let target = attribute(&event, xml.decoder(), b"Target")?.ok_or_else(|| {
Error::stream(format!(
"worksheet relationship '{relationship_id}' has no target"
))
})?;
return Ok(normalize_zip_path(&target));
}
}
Event::Eof => break,
_ => {}
}
buffer.clear();
}
Err(Error::stream(format!("worksheet relationship '{relationship_id}' was not found")))
}
fn read_workbook_relationships<R>(
archive: &mut ZipArchive<R>,
) -> Result<HashMap<String, WorkbookRelationship>>
where
R: Read + Seek,
{
let file = archive
.by_name("xl/_rels/workbook.xml.rels")
.map_err(|error| stream_error("cannot read workbook relationships:", error))?;
let mut xml = Reader::from_reader(BufReader::new(file));
let mut buffer = Vec::new();
let mut relationships = HashMap::new();
loop {
match xml
.read_event_into(&mut buffer)
.map_err(|error| stream_error("invalid workbook relationships:", error))?
{
Event::Start(event) | Event::Empty(event)
if is_name(event.name().as_ref(), b"Relationship") =>
{
if let (Some(id), Some(target), Some(relationship_type)) = (
attribute(&event, xml.decoder(), b"Id")?,
attribute(&event, xml.decoder(), b"Target")?,
attribute(&event, xml.decoder(), b"Type")?,
) {
relationships.insert(
id,
WorkbookRelationship {
target: normalize_zip_path(&target),
relationship_type,
},
);
}
}
Event::Eof => break,
_ => {}
}
buffer.clear();
}
Ok(relationships)
}
fn sheet_type_from_relationship(relationship_type: &str) -> Result<SheetType> {
match relationship_type.rsplit('/').next() {
Some("worksheet") => Ok(SheetType::Worksheet),
Some("dialogsheet") => Ok(SheetType::DialogSheet),
Some("macrosheet") => Ok(SheetType::MacroSheet),
Some("chartsheet") => Ok(SheetType::ChartSheet),
Some("vbaProject") => Ok(SheetType::Vba),
_ => Err(Error::stream(format!(
"unsupported worksheet relationship type '{relationship_type}'"
))),
}
}
fn read_shared_strings<R>(archive: &mut ZipArchive<R>) -> Result<Vec<String>>
where
R: Read + Seek,
{
let file = match archive.by_name("xl/sharedStrings.xml") {
Ok(file) => file,
Err(ZipError::FileNotFound) => return Ok(Vec::new()),
Err(error) => return Err(stream_error("cannot read shared strings:", error)),
};
let mut xml = Reader::from_reader(BufReader::new(file));
let mut buffer = Vec::new();
let mut strings = Vec::new();
let mut current = String::new();
let mut in_item = false;
let mut in_text = false;
loop {
match xml
.read_event_into(&mut buffer)
.map_err(|error| stream_error("invalid shared strings XML:", error))?
{
Event::Start(event) if is_name(event.name().as_ref(), b"si") => {
current.clear();
in_item = true;
}
Event::Empty(event) if is_name(event.name().as_ref(), b"si") => {
strings.push(String::new());
}
Event::Start(event) if in_item && is_name(event.name().as_ref(), b"t") => {
in_text = true;
}
Event::Text(text) if in_text => append_text(&text, &mut current)?,
Event::GeneralRef(reference) if in_text => append_reference(&reference, &mut current)?,
Event::End(event) if is_name(event.name().as_ref(), b"t") => in_text = false,
Event::End(event) if is_name(event.name().as_ref(), b"si") => {
strings.push(decode_excel_escapes(¤t));
in_item = false;
}
Event::Eof => break,
_ => {}
}
buffer.clear();
}
Ok(strings)
}
fn read_styles<R>(archive: &mut ZipArchive<R>, preserve_structure: bool) -> Result<Vec<CellStyle>>
where
R: Read + Seek,
{
let file = match archive.by_name("xl/styles.xml") {
Ok(file) => file,
Err(ZipError::FileNotFound) => return Ok(Vec::new()),
Err(error) => return Err(stream_error("cannot read styles:", error)),
};
let mut xml = Reader::from_reader(BufReader::new(file));
let mut buffer = Vec::new();
let mut custom_formats = HashMap::new();
let mut styles = Vec::new();
let mut in_cell_formats = false;
loop {
match xml
.read_event_into(&mut buffer)
.map_err(|error| stream_error("invalid styles XML:", error))?
{
Event::Start(event) if is_name(event.name().as_ref(), b"cellXfs") => {
in_cell_formats = true;
}
Event::End(event) if is_name(event.name().as_ref(), b"cellXfs") => {
in_cell_formats = false;
}
Event::Start(event) | Event::Empty(event)
if is_name(event.name().as_ref(), b"numFmt") =>
{
let id = attribute(&event, xml.decoder(), b"numFmtId")?
.and_then(|value| value.parse::<u32>().ok());
let format = attribute(&event, xml.decoder(), b"formatCode")?;
if let (Some(id), Some(format)) = (id, format) {
custom_formats.insert(
id,
(
classify_custom_format(&format),
preserve_structure.then(|| Arc::from(format)),
),
);
}
}
Event::Start(event) | Event::Empty(event)
if in_cell_formats && is_name(event.name().as_ref(), b"xf") =>
{
let id = attribute(&event, xml.decoder(), b"numFmtId")?
.and_then(|value| value.parse::<u32>().ok())
.unwrap_or(0);
let (format, number_format) = custom_formats.get(&id).map_or_else(
|| {
(
builtin_format(id),
preserve_structure
.then(|| builtin_number_format(id).map(Arc::from))
.flatten(),
)
},
|(format, number_format)| (*format, number_format.clone()),
);
styles.push(CellStyle { format, number_format });
}
Event::Eof => break,
_ => {}
}
buffer.clear();
}
Ok(styles)
}
#[derive(Clone, Copy, Default)]
enum CellKind {
#[default]
Number,
SharedString,
InlineString,
Boolean,
Error,
String,
IsoDate,
}
#[derive(Clone, Copy)]
enum Capture {
Value,
InlineText,
Formula,
}
struct CellState {
column: usize,
style: u32,
kind: CellKind,
value: String,
inline_text: String,
formula: Option<String>,
capture: Option<Capture>,
}
struct RowState {
excel_row: usize,
cells: Vec<ParsedCell>,
next_column: usize,
}
struct ParsedCell {
column: usize,
value: Data,
metadata: Option<ParsedCellMetadata>,
}
struct ParsedCellMetadata {
formula: Option<String>,
style_id: u32,
number_format: Option<Arc<str>>,
}
#[derive(Clone, Copy, Default)]
struct WorksheetExtent {
start_row: Option<usize>,
start_column: Option<usize>,
end_row: Option<usize>,
end_column: Option<usize>,
}
fn read_declared_worksheet_extent<R>(
archive: &mut ZipArchive<R>,
sheet_path: &str,
) -> Result<Option<WorksheetExtent>>
where
R: Read + Seek,
{
let file = archive
.by_name(sheet_path)
.map_err(|error| stream_error("cannot read worksheet dimensions:", error))?;
let mut xml = Reader::from_reader(BufReader::new(file));
let mut buffer = Vec::new();
loop {
match xml
.read_event_into(&mut buffer)
.map_err(|error| stream_error("invalid worksheet XML dimensions:", error))?
{
Event::Start(event) | Event::Empty(event)
if is_name(event.name().as_ref(), b"dimension") =>
{
let reference = attribute(&event, xml.decoder(), b"ref")?
.ok_or_else(|| Error::stream("worksheet dimension has no reference"))?;
let mut cells = reference.split(':');
let start = cells
.next()
.ok_or_else(|| Error::stream("worksheet dimension is empty"))?
.parse::<CellReference>()?;
let end = cells.next().unwrap_or(&reference).parse::<CellReference>()?;
return Ok(Some(WorksheetExtent {
start_row: Some(start.row()),
start_column: Some(start.column()),
end_row: Some(end.row()),
end_column: Some(end.column()),
}));
}
Event::Start(event) if is_name(event.name().as_ref(), b"sheetData") => return Ok(None),
Event::Eof => return Ok(None),
_ => {}
}
buffer.clear();
}
}
fn scan_worksheet_extent<R>(
archive: &mut ZipArchive<R>,
sheet_path: &str,
cancelled: &AtomicBool,
) -> Result<WorksheetExtent>
where
R: Read + Seek,
{
let file = archive
.by_name(sheet_path)
.map_err(|error| stream_error("cannot scan worksheet XML:", error))?;
let mut xml = Reader::from_reader(BufReader::new(file));
let mut buffer = Vec::new();
let mut extent = WorksheetExtent::default();
let mut current_row = None;
let mut last_declared_row = None;
let mut next_column = 0;
let mut in_sheet_data = false;
loop {
if cancelled.load(Ordering::Relaxed) {
return Ok(extent);
}
match xml
.read_event_into(&mut buffer)
.map_err(|error| stream_error("invalid worksheet XML during scan:", error))?
{
Event::Start(event) if is_name(event.name().as_ref(), b"sheetData") => {
in_sheet_data = true;
}
Event::End(event) if is_name(event.name().as_ref(), b"sheetData") => break,
Event::Start(event) | Event::Empty(event)
if in_sheet_data && is_name(event.name().as_ref(), b"row") =>
{
let row = row_index(&event, xml.decoder(), last_declared_row)?;
current_row = Some(row);
last_declared_row = Some(row);
extent.start_row = Some(extent.start_row.map_or(row, |start| start.min(row)));
extent.end_row = Some(extent.end_row.map_or(row, |end| end.max(row)));
next_column = 0;
}
Event::Start(event) | Event::Empty(event)
if current_row.is_some() && is_name(event.name().as_ref(), b"c") =>
{
let column = attribute(&event, xml.decoder(), b"r")?
.and_then(|reference| parse_column(&reference))
.unwrap_or(next_column);
next_column = column.saturating_add(1);
extent.start_column =
Some(extent.start_column.map_or(column, |start| start.min(column)));
extent.end_column = Some(extent.end_column.map_or(column, |end| end.max(column)));
}
Event::End(event) if is_name(event.name().as_ref(), b"row") => {
current_row = None;
}
Event::Eof => break,
_ => {}
}
buffer.clear();
}
Ok(extent)
}
fn stream_worksheet<R, F>(
archive: &mut ZipArchive<R>,
context: WorkbookContext,
extent: WorksheetExtent,
options: &ReadOptions,
cancelled: &AtomicBool,
emit: &mut F,
) -> Result<()>
where
R: Read + Seek,
F: FnMut(SelectedRow) -> bool,
{
let file = archive
.by_name(&context.sheet_path)
.map_err(|error| stream_error("cannot read worksheet XML:", error))?;
let mut xml = Reader::from_reader(BufReader::new(file));
let mut buffer = Vec::new();
let mut current_row = None;
let mut current_cell = None;
let mut last_declared_row = None;
let mut next_output_row = options.start_cell().row();
let mut in_sheet_data = false;
let end_row = options.end_cell().map_or(extent.end_row, |cell| Some(cell.row()));
let end_column = options.end_cell().map_or(extent.end_column, |cell| Some(cell.column()));
loop {
if cancelled.load(Ordering::Relaxed) {
return Ok(());
}
match xml
.read_event_into(&mut buffer)
.map_err(|error| stream_error("invalid worksheet XML:", error))?
{
Event::Start(event) if is_name(event.name().as_ref(), b"sheetData") => {
in_sheet_data = true;
}
Event::End(event) if is_name(event.name().as_ref(), b"sheetData") => break,
Event::Start(event) if in_sheet_data && is_name(event.name().as_ref(), b"row") => {
let excel_row = row_index(&event, xml.decoder(), last_declared_row)?;
if end_row.is_none_or(|end_row| excel_row > end_row) {
break;
}
if !emit_missing_rows(&mut next_output_row, excel_row, end_column, options, emit) {
return Ok(());
}
last_declared_row = Some(excel_row);
current_row = Some(RowState { excel_row, cells: Vec::new(), next_column: 0 });
}
Event::Empty(event) if in_sheet_data && is_name(event.name().as_ref(), b"row") => {
let excel_row = row_index(&event, xml.decoder(), last_declared_row)?;
if end_row.is_none_or(|end_row| excel_row > end_row) {
break;
}
if !emit_missing_rows(&mut next_output_row, excel_row, end_column, options, emit) {
return Ok(());
}
last_declared_row = Some(excel_row);
let row = RowState { excel_row, cells: Vec::new(), next_column: 0 };
if !emit_row(row, &mut next_output_row, end_column, options, emit) {
return Ok(());
}
}
Event::Start(event)
if current_row.is_some() && is_name(event.name().as_ref(), b"c") =>
{
current_cell = Some(start_cell(
&event,
xml.decoder(),
current_row.as_mut().expect("row checked above"),
)?);
}
Event::Empty(event)
if current_row.is_some() && is_name(event.name().as_ref(), b"c") =>
{
let cell = start_cell(
&event,
xml.decoder(),
current_row.as_mut().expect("row checked above"),
)?;
finish_cell(cell, current_row.as_mut().expect("row checked above"), &context)?;
}
Event::Start(event)
if current_cell.is_some() && is_name(event.name().as_ref(), b"v") =>
{
current_cell.as_mut().expect("cell checked above").capture = Some(Capture::Value);
}
Event::Start(event)
if current_cell.is_some() && is_name(event.name().as_ref(), b"t") =>
{
current_cell.as_mut().expect("cell checked above").capture =
Some(Capture::InlineText);
}
Event::Start(event)
if current_cell.is_some() && is_name(event.name().as_ref(), b"f") =>
{
if context.preserve_structure {
let cell = current_cell.as_mut().expect("cell checked above");
cell.formula = Some(String::new());
cell.capture = Some(Capture::Formula);
}
}
Event::Empty(event)
if current_cell.is_some() && is_name(event.name().as_ref(), b"f") =>
{
if context.preserve_structure {
current_cell.as_mut().expect("cell checked above").formula =
Some(String::new());
}
}
Event::Text(text) if current_cell.is_some() => {
append_cell_text(&text, current_cell.as_mut().expect("cell checked above"))?;
}
Event::GeneralRef(reference) if current_cell.is_some() => {
append_cell_reference(
&reference,
current_cell.as_mut().expect("cell checked above"),
)?;
}
Event::End(event)
if current_cell.is_some()
&& (is_name(event.name().as_ref(), b"v")
|| is_name(event.name().as_ref(), b"t")
|| is_name(event.name().as_ref(), b"f")) =>
{
current_cell.as_mut().expect("cell checked above").capture = None;
}
Event::End(event) if is_name(event.name().as_ref(), b"c") => {
if let (Some(cell), Some(row)) = (current_cell.take(), current_row.as_mut()) {
finish_cell(cell, row, &context)?;
}
}
Event::End(event) if is_name(event.name().as_ref(), b"row") => {
if let Some(row) = current_row.take() {
if !emit_row(row, &mut next_output_row, end_column, options, emit) {
return Ok(());
}
}
}
Event::Eof => break,
_ => {}
}
buffer.clear();
}
Ok(())
}
fn start_cell(event: &BytesStart<'_>, decoder: Decoder, row: &mut RowState) -> Result<CellState> {
let column = attribute(event, decoder, b"r")?
.and_then(|reference| parse_column(&reference))
.unwrap_or(row.next_column);
row.next_column = column.saturating_add(1);
let style =
attribute(event, decoder, b"s")?.and_then(|value| value.parse::<u32>().ok()).unwrap_or(0);
let kind = match attribute(event, decoder, b"t")?.as_deref() {
Some("s") => CellKind::SharedString,
Some("inlineStr") => CellKind::InlineString,
Some("b") => CellKind::Boolean,
Some("e") => CellKind::Error,
Some("str") => CellKind::String,
Some("d") => CellKind::IsoDate,
_ => CellKind::Number,
};
Ok(CellState {
column,
style,
kind,
value: String::new(),
inline_text: String::new(),
formula: None,
capture: None,
})
}
fn finish_cell(cell: CellState, row: &mut RowState, context: &WorkbookContext) -> Result<()> {
let CellState { column, style, kind, value, inline_text, formula, capture: _ } = cell;
let cell_style = context.styles.get(style as usize);
let format = cell_style.map_or(CellFormat::Other, |style| style.format);
let number_format = cell_style.and_then(|style| style.number_format.clone());
let data = match kind {
CellKind::SharedString => {
if value.is_empty() {
Data::Empty
} else {
let index = value
.parse::<usize>()
.map_err(|error| stream_error("invalid shared string index:", error))?;
Data::String(context.shared_strings.get(index).cloned().ok_or_else(|| {
Error::stream(format!("shared string index {index} is out of range"))
})?)
}
}
CellKind::InlineString => Data::String(decode_excel_escapes(&inline_text)),
CellKind::Boolean => Data::Bool(value == "1" || value.eq_ignore_ascii_case("true")),
CellKind::Error => Data::Error(parse_cell_error(&value)?),
CellKind::String => Data::String(decode_excel_escapes(&value)),
CellKind::IsoDate => Data::DateTimeIso(value),
CellKind::Number if value.is_empty() => Data::Empty,
CellKind::Number => {
let value = value
.parse::<f64>()
.map_err(|error| stream_error("invalid numeric cell value:", error))?;
match format {
CellFormat::DateTime => Data::DateTime(ExcelDateTime::new(
value,
ExcelDateTimeType::DateTime,
context.is_1904,
)),
CellFormat::TimeDelta => Data::DateTime(ExcelDateTime::new(
value,
ExcelDateTimeType::TimeDelta,
context.is_1904,
)),
CellFormat::Other => Data::Float(value),
}
}
};
let metadata = context.preserve_structure.then_some(ParsedCellMetadata {
formula,
style_id: style,
number_format,
});
row.cells.push(ParsedCell { column, value: data, metadata });
Ok(())
}
fn emit_missing_rows<F>(
next_output_row: &mut usize,
target_row: usize,
end_column: Option<usize>,
options: &ReadOptions,
emit: &mut F,
) -> bool
where
F: FnMut(SelectedRow) -> bool,
{
while *next_output_row < target_row {
let row = RowState { excel_row: *next_output_row, cells: Vec::new(), next_column: 0 };
if !emit_row(row, next_output_row, end_column, options, emit) {
return false;
}
}
true
}
fn emit_row<F>(
row: RowState,
next_output_row: &mut usize,
end_column: Option<usize>,
options: &ReadOptions,
emit: &mut F,
) -> bool
where
F: FnMut(SelectedRow) -> bool,
{
*next_output_row = (*next_output_row).max(row.excel_row.saturating_add(1));
if row.excel_row < options.start_cell().row() {
return true;
}
let start_column = options.start_cell().column();
let width = end_column
.filter(|column| *column >= start_column)
.map_or(0, |column| column - start_column + 1);
let mut values = vec![Data::Empty; width];
let mut cells = Vec::new();
for cell in row.cells {
if cell.column >= start_column && end_column.is_some_and(|end| cell.column <= end) {
values[cell.column - start_column] = cell.value;
if let Some(metadata) = cell.metadata {
cells.push(SelectedCellMetadata {
excel_column: cell.column,
formula: metadata.formula,
style_id: metadata.style_id,
number_format: metadata.number_format,
});
}
}
}
if options.ignore_empty_rows() && values.iter().all(DataType::is_empty) {
return true;
}
emit(SelectedRow { excel_row: row.excel_row, start_column, values, cells })
}
fn validate_range(options: &ReadOptions) -> Result<()> {
let Some(end_cell) = options.end_cell() else {
return Ok(());
};
let start_cell = options.start_cell();
if end_cell.row() < start_cell.row() || end_cell.column() < start_cell.column() {
return Err(Error::invalid_cell_range(start_cell.to_string(), end_cell.to_string()));
}
Ok(())
}
fn row_index(event: &BytesStart<'_>, decoder: Decoder, previous: Option<usize>) -> Result<usize> {
Ok(attribute(event, decoder, b"r")?
.and_then(|value| value.parse::<usize>().ok())
.and_then(|value| value.checked_sub(1))
.unwrap_or_else(|| previous.map_or(0, |row| row.saturating_add(1))))
}
fn append_cell_text(text: &quick_xml::events::BytesText<'_>, cell: &mut CellState) -> Result<()> {
match cell.capture {
Some(Capture::Value) => append_text(text, &mut cell.value),
Some(Capture::InlineText) => append_text(text, &mut cell.inline_text),
Some(Capture::Formula) => {
append_text(text, cell.formula.as_mut().expect("formula capture initializes storage"))
}
None => Ok(()),
}
}
fn append_cell_reference(reference: &BytesRef<'_>, cell: &mut CellState) -> Result<()> {
match cell.capture {
Some(Capture::Value) => append_reference(reference, &mut cell.value),
Some(Capture::InlineText) => append_reference(reference, &mut cell.inline_text),
Some(Capture::Formula) => append_reference(
reference,
cell.formula.as_mut().expect("formula capture initializes storage"),
),
None => Ok(()),
}
}
fn append_text(text: &quick_xml::events::BytesText<'_>, target: &mut String) -> Result<()> {
let text = text.xml10_content().map_err(|error| stream_error("invalid XML text:", error))?;
target.push_str(&text);
Ok(())
}
fn append_reference(reference: &BytesRef<'_>, target: &mut String) -> Result<()> {
let decoded =
reference.decode().map_err(|error| stream_error("invalid XML reference:", error))?;
match decoded.as_ref() {
"lt" => target.push('<'),
"gt" => target.push('>'),
"amp" => target.push('&'),
"quot" => target.push('"'),
"apos" => target.push('\''),
_ => {
if let Some(value) = reference
.resolve_char_ref()
.map_err(|error| stream_error("invalid XML character reference:", error))?
{
target.push(value);
} else {
return Err(Error::stream(format!("unrecognized XML entity '&{decoded};'")));
}
}
}
Ok(())
}
fn attribute(event: &BytesStart<'_>, decoder: Decoder, name: &[u8]) -> Result<Option<String>> {
for attribute in event.attributes().with_checks(false) {
let attribute = attribute.map_err(|error| stream_error("invalid XML attribute:", error))?;
if is_name(attribute.key.as_ref(), name) {
return attribute
.decode_and_unescape_value(decoder)
.map(|value| Some(value.into_owned()))
.map_err(|error| stream_error("invalid XML attribute value:", error));
}
}
Ok(None)
}
fn is_name(actual: &[u8], expected: &[u8]) -> bool {
actual.rsplit(|byte| *byte == b':').next() == Some(expected)
}
fn normalize_zip_path(target: &str) -> String {
let target = target.replace('\\', "/");
let path = if target.starts_with('/') {
target.trim_start_matches('/').to_owned()
} else if target.starts_with("xl/") {
target
} else {
format!("xl/{target}")
};
let mut parts = Vec::new();
for part in path.split('/') {
match part {
"" | "." => {}
".." => {
parts.pop();
}
_ => parts.push(part),
}
}
parts.join("/")
}
fn parse_column(reference: &str) -> Option<usize> {
let mut column = 0usize;
let mut found = false;
for byte in reference.bytes() {
if byte == b'$' && !found {
continue;
}
if !byte.is_ascii_alphabetic() {
break;
}
found = true;
column = column
.checked_mul(26)?
.checked_add(usize::from(byte.to_ascii_uppercase() - b'A' + 1))?;
}
found.then(|| column - 1).filter(|column| *column <= MAX_EXCEL_COLUMN)
}
fn parse_cell_error(value: &str) -> Result<CellErrorType> {
match value {
"#DIV/0!" => Ok(CellErrorType::Div0),
"#N/A" => Ok(CellErrorType::NA),
"#NAME?" => Ok(CellErrorType::Name),
"#NULL!" => Ok(CellErrorType::Null),
"#NUM!" => Ok(CellErrorType::Num),
"#REF!" => Ok(CellErrorType::Ref),
"#VALUE!" => Ok(CellErrorType::Value),
"#DATA!" | "#GETTING_DATA" => Ok(CellErrorType::GettingData),
_ => Err(Error::stream(format!("unknown Excel cell error '{value}'"))),
}
}
fn builtin_format(id: u32) -> CellFormat {
match id {
14..=22 | 45 | 47 => CellFormat::DateTime,
46 => CellFormat::TimeDelta,
_ => CellFormat::Other,
}
}
fn builtin_number_format(id: u32) -> Option<&'static str> {
match id {
0 => Some("General"),
1 => Some("0"),
2 => Some("0.00"),
3 => Some("#,##0"),
4 => Some("#,##0.00"),
9 => Some("0%"),
10 => Some("0.00%"),
11 => Some("0.00E+00"),
12 => Some("# ?/?"),
13 => Some("# ??/??"),
14 => Some("mm-dd-yy"),
15 => Some("d-mmm-yy"),
16 => Some("d-mmm"),
17 => Some("mmm-yy"),
18 => Some("h:mm AM/PM"),
19 => Some("h:mm:ss AM/PM"),
20 => Some("h:mm"),
21 => Some("h:mm:ss"),
22 => Some("m/d/yy h:mm"),
37 => Some("#,##0 ;(#,##0)"),
38 => Some("#,##0 ;[Red](#,##0)"),
39 => Some("#,##0.00;(#,##0.00)"),
40 => Some("#,##0.00;[Red](#,##0.00)"),
45 => Some("mm:ss"),
46 => Some("[h]:mm:ss"),
47 => Some("mmss.0"),
48 => Some("##0.0E+0"),
49 => Some("@"),
_ => None,
}
}
fn classify_custom_format(format: &str) -> CellFormat {
let characters = format.as_bytes();
let mut index = 0;
while index < characters.len() {
match characters[index] {
b';' => break,
b'\\' | b'_' | b'*' => index = index.saturating_add(2),
b'"' => {
index += 1;
while index < characters.len() && characters[index] != b'"' {
index += 1;
}
index = index.saturating_add(1);
}
b'[' => {
let start = index + 1;
index = start;
while index < characters.len() && characters[index] != b']' {
index += 1;
}
let token = &format[start..index];
if !token.is_empty()
&& token
.bytes()
.all(|byte| matches!(byte.to_ascii_lowercase(), b'h' | b'm' | b's'))
{
return CellFormat::TimeDelta;
}
index = index.saturating_add(1);
}
byte if matches!(byte.to_ascii_lowercase(), b'd' | b'm' | b'y' | b'h' | b's') => {
return CellFormat::DateTime;
}
_ => index += 1,
}
}
CellFormat::Other
}
fn decode_excel_escapes(value: &str) -> String {
let bytes = value.as_bytes();
let mut result = String::with_capacity(value.len());
let mut index = 0;
while index < bytes.len() {
if index + 7 <= bytes.len()
&& bytes[index] == b'_'
&& matches!(bytes[index + 1], b'x' | b'X')
&& bytes[index + 6] == b'_'
{
if let Ok(code) = u16::from_str_radix(&value[index + 2..index + 6], 16) {
if let Some(character) = char::from_u32(u32::from(code)) {
result.push(character);
index += 7;
continue;
}
}
}
let character = value[index..].chars().next().expect("index is in bounds");
result.push(character);
index += character.len_utf8();
}
result
}
fn stream_error(context: impl std::fmt::Display, error: impl std::fmt::Display) -> Error {
Error::stream(format!("{context} {error}"))
}
#[cfg(test)]
mod tests {
use super::{CellFormat, classify_custom_format, parse_column};
#[test]
fn classifies_custom_excel_number_formats() {
assert_eq!(classify_custom_format("yyyy-mm-dd"), CellFormat::DateTime);
assert_eq!(classify_custom_format("h:mm AM/PM"), CellFormat::DateTime);
assert_eq!(classify_custom_format("[h]:mm:ss"), CellFormat::TimeDelta);
assert_eq!(classify_custom_format("0.00"), CellFormat::Other);
assert_eq!(classify_custom_format("[Red][>=100]0.00"), CellFormat::Other);
assert_eq!(classify_custom_format("\"days\" 0"), CellFormat::Other);
}
#[test]
fn rejects_columns_beyond_excel_limits() {
assert_eq!(parse_column("XFD1048576"), Some(16_383));
assert_eq!(parse_column("XFE1"), None);
assert_eq!(parse_column("ZZZZ1"), None);
}
}