fn parse_workbook(xml: &str) -> Vec<String> {
let mut reader = xml_reader(xml);
let mut buffer = Vec::new();
let mut names = Vec::new();
loop {
match reader.read_event_into(&mut buffer) {
Ok(Event::Start(e)) | Ok(Event::Empty(e)) if local_name(e.name()) == "sheet" => {
if let Some(name) = attr_value(&e, b"name") {
names.push(name);
}
}
Ok(Event::Eof) | Err(_) => break,
_ => {}
}
buffer.clear();
}
names
}
fn parse_shared_strings(xml: &str) -> Vec<String> {
let mut reader = xml_reader(xml);
let mut buffer = Vec::new();
let mut strings = Vec::new();
let mut inside_si = false;
let mut inside_t = false;
let mut current = String::new();
loop {
match reader.read_event_into(&mut buffer) {
Ok(Event::Start(e)) => match local_name(e.name()).as_str() {
"si" => {
inside_si = true;
current.clear();
}
"t" if inside_si => inside_t = true,
_ => {}
},
Ok(Event::Text(t)) if inside_t => {
if let Some(text) = text_of(&Event::Text(t)) {
current.push_str(&text);
}
}
Ok(Event::End(e)) => match local_name(e.name()).as_str() {
"t" => inside_t = false,
"si" => {
inside_si = false;
strings.push(std::mem::take(&mut current));
}
_ => {}
},
Ok(Event::Eof) | Err(_) => break,
_ => {}
}
buffer.clear();
}
strings
}
struct SheetScan<'a> {
max_part: u64,
max_sample_rows: usize,
truncated_parts: &'a mut u64,
}
fn read_sheets(
archive: &mut ZipArchive<BufReader<File>>,
names: &[String],
path: &std::path::Path,
shared: &[String],
sheet_names: &[String],
scan: &mut SheetScan<'_>,
) -> Vec<SheetSignal> {
let mut parts: Vec<String> = names
.iter()
.filter(|n| is_worksheet_part(n))
.cloned()
.collect();
parts.sort();
parts.truncate(crate::config::tuning().extract_max_sheets);
let mut sheets = Vec::with_capacity(parts.len());
for (index, part) in parts.iter().enumerate() {
let Ok((xml, cut)) = read_part(archive, part, path, scan.max_part) else {
sheets.push(SheetSignal {
name: sheet_label(sheet_names, index),
rows: 0,
columns: 0,
header: Vec::new(),
types: Vec::new(),
sample: Vec::new(),
});
continue;
};
*scan.truncated_parts += u64::from(cut);
let parsed = parse_sheet(&xml, shared, scan.max_sample_rows);
sheets.push(SheetSignal {
name: sheet_label(sheet_names, index),
rows: parsed.rows,
columns: parsed.header.len() as u64,
header: parsed.header,
types: parsed.types,
sample: parsed.sample,
});
}
sheets
}
fn is_worksheet_part(name: &str) -> bool {
name.starts_with("xl/worksheets/sheet") && name.ends_with(".xml") && !name.contains("/_rels/")
}
fn sheet_label(names: &[String], index: usize) -> String {
names
.get(index)
.cloned()
.unwrap_or_else(|| format!("sheet{}", index + 1))
}
struct SheetParse {
rows: u64,
header: Vec<String>,
types: Vec<String>,
sample: Vec<Vec<String>>,
}
fn parse_sheet(xml: &str, shared: &[String], max_sample_rows: usize) -> SheetParse {
let mut reader = xml_reader(xml);
let mut buffer = Vec::new();
let mut rows = 0u64;
let mut header: Vec<String> = Vec::new();
let mut sample: Vec<Vec<String>> = Vec::new();
let mut types: Vec<String> = Vec::new();
let mut row_cells = 0u64;
let mut row_values: Vec<(usize, String)> = Vec::new();
let mut cell_column = 0usize;
let mut cell_kind = CellKind::Number;
let mut inside_value = false;
let mut inside_inline = false;
let mut cell_text = String::new();
loop {
match reader.read_event_into(&mut buffer) {
Ok(Event::Start(e)) => match local_name(e.name()).as_str() {
"row" => {
row_cells = 0;
row_values.clear();
}
"c" => {
row_cells += 1;
cell_column = attr_value(&e, b"r")
.as_deref()
.and_then(column_index)
.unwrap_or(row_cells as usize - 1);
cell_kind = match attr_value(&e, b"t").as_deref() {
Some("s") => CellKind::SharedString,
Some("inlineStr") => CellKind::InlineString,
Some("b") => CellKind::Boolean,
_ => CellKind::Number,
};
cell_text.clear();
}
"v" => inside_value = true,
"t" if cell_kind == CellKind::InlineString => inside_inline = true,
_ => {}
},
Ok(Event::Text(t)) => {
if inside_value || inside_inline {
if let Some(text) = text_of(&Event::Text(t)) {
cell_text.push_str(&text);
}
}
}
Ok(Event::End(e)) => match local_name(e.name()).as_str() {
"v" => {
inside_value = false;
row_values.push((cell_column, resolve_cell(cell_kind, &cell_text, shared)));
}
"t" => inside_inline = false,
"c" => {
if cell_kind == CellKind::InlineString && !inside_value {
row_values.push((cell_column, resolve_cell(cell_kind, &cell_text, shared)));
}
}
"row" => {
rows += 1;
finish_row(
&mut row_values,
&mut header,
&mut sample,
&mut types,
max_sample_rows,
);
}
_ => {}
},
Ok(Event::Eof) | Err(_) => break,
_ => {}
}
buffer.clear();
}
SheetParse {
rows,
header,
types,
sample,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CellKind {
Number,
SharedString,
InlineString,
Boolean,
}
fn resolve_cell(kind: CellKind, raw: &str, shared: &[String]) -> String {
let value = match kind {
CellKind::SharedString => raw
.trim()
.parse::<usize>()
.ok()
.and_then(|i| shared.get(i).cloned())
.unwrap_or_default(),
CellKind::Boolean => match raw.trim() {
"1" => "true".to_string(),
"0" => "false".to_string(),
other => other.to_string(),
},
CellKind::Number | CellKind::InlineString => raw.trim().to_string(),
};
truncate_chars(&value, CELL_CHAR_CAP)
}
fn column_index(reference: &str) -> Option<usize> {
let mut index: usize = 0;
let mut seen = false;
for ch in reference.chars() {
match ch {
'$' if !seen => continue,
'A'..='Z' | 'a'..='z' => {
seen = true;
index = index * crate::constants::LATIN_ALPHABET_LEN
+ (ch.to_ascii_uppercase() as usize - 'A' as usize + 1);
}
_ => break,
}
}
seen.then(|| index - 1)
}
fn finish_row(
values: &mut Vec<(usize, String)>,
header: &mut Vec<String>,
sample: &mut Vec<Vec<String>>,
types: &mut Vec<String>,
max_sample_rows: usize,
) {
if values.is_empty() {
return;
}
values.sort_by_key(|(column, _)| *column);
let width = values.last().map_or(0, |(column, _)| column + 1);
let mut row = vec![String::new(); width];
for (column, value) in values.drain(..) {
if let Some(slot) = row.get_mut(column) {
*slot = value;
}
}
if header.is_empty() && row.iter().any(|v| !v.is_empty()) {
header.clone_from(&row);
types.clone_from(&vec![String::new(); row.len()]);
return;
}
if sample.len() >= max_sample_rows {
return;
}
for (index, value) in row.iter().enumerate() {
let Some(slot) = types.get_mut(index) else {
continue;
};
let observed = scalar_type(value);
if slot.is_empty() || (*slot != "string" && *slot != observed) {
*slot = if slot.is_empty() {
observed.to_string()
} else {
"string".to_string()
};
}
}
sample.push(row);
}
fn scalar_type(value: &str) -> &'static str {
if value.is_empty() {
return "empty";
}
if value == "true" || value == "false" {
return "boolean";
}
if value.parse::<f64>().is_ok() {
return "number";
}
"string"
}