use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Cursor, Read, Seek};
use std::path::Path;
use oxiarc_archive::zip::ZipReader;
use quick_xml::escape::unescape;
use quick_xml::events::attributes::Attribute;
use quick_xml::events::{BytesText, Event};
use quick_xml::Reader;
use quick_xml::XmlVersion;
use crate::error::Result;
use super::cell::{parse_ref, XlsxCellValue, MAX_XLSX_COLS, MAX_XLSX_ROWS};
use super::error::{invalid, io_err, std_io, xml_err, zip_err};
use super::styles::{self, DateKind, StylesInfo};
#[derive(Debug, Clone)]
struct WorkbookManifest {
sheet_names: Vec<String>,
sheet_rids: Vec<String>,
date1904: bool,
}
pub(super) struct WorkbookHandle {
pub(super) sheet_names: Vec<String>,
shared_strings: Vec<String>,
styles: StylesInfo,
date1904: bool,
raw_sheet_bytes: Vec<Vec<u8>>,
}
impl WorkbookHandle {
pub(super) fn sheet_index(&self, name: Option<&str>) -> Result<usize> {
match name {
Some(n) => self
.sheet_names
.iter()
.position(|s| s == n)
.ok_or_else(|| io_err(format!("xlsx: sheet '{n}' not found"))),
None => {
if self.sheet_names.is_empty() {
Err(io_err("xlsx: workbook contains no sheets"))
} else {
Ok(0)
}
}
}
}
pub(super) fn load_sheet(&self, index: usize) -> Result<LoadedSheet> {
let bytes = self
.raw_sheet_bytes
.get(index)
.ok_or_else(|| io_err(format!("xlsx: sheet index {index} out of range")))?;
let name = self
.sheet_names
.get(index)
.ok_or_else(|| io_err(format!("xlsx: sheet index {index} out of range")))?
.clone();
let (cells, row_count, col_count) =
parse_worksheet(bytes, &self.shared_strings, &self.styles, self.date1904)?;
Ok(LoadedSheet {
name,
cells,
row_count,
col_count,
})
}
pub(super) fn load_all(&self) -> Result<Vec<LoadedSheet>> {
(0..self.sheet_names.len())
.map(|i| self.load_sheet(i))
.collect()
}
}
pub(super) struct LoadedSheet {
pub(super) name: String,
cells: HashMap<(usize, usize), XlsxCellValue>,
pub(super) row_count: usize,
pub(super) col_count: usize,
}
impl LoadedSheet {
pub(super) fn get(&self, row: usize, col: usize) -> &XlsxCellValue {
const EMPTY: XlsxCellValue = XlsxCellValue::Empty;
self.cells.get(&(row, col)).unwrap_or(&EMPTY)
}
}
struct RawSheet {
path: String,
rid: String,
bytes: Vec<u8>,
}
pub(super) fn open_workbook<P: AsRef<Path>>(path: P) -> Result<WorkbookHandle> {
let file = File::open(path.as_ref()).map_err(std_io)?;
let buf = BufReader::new(file);
open_workbook_from_reader(buf)
}
pub(super) fn open_workbook_from_reader<R: Read + Seek>(reader: R) -> Result<WorkbookHandle> {
let mut zr = ZipReader::new(reader).map_err(zip_err)?;
let entries: Vec<_> = zr.entries().to_vec();
let mut workbook_bytes: Option<Vec<u8>> = None;
let mut workbook_rels_bytes: Option<Vec<u8>> = None;
let mut shared_bytes: Option<Vec<u8>> = None;
let mut styles_bytes: Option<Vec<u8>> = None;
let mut raw_sheets: Vec<RawSheet> = Vec::new();
for entry in &entries {
let name = entry.name.as_str();
if name == "xl/workbook.xml" {
workbook_bytes = Some(zr.extract(entry).map_err(zip_err)?);
} else if name == "xl/_rels/workbook.xml.rels" {
workbook_rels_bytes = Some(zr.extract(entry).map_err(zip_err)?);
} else if name == "xl/sharedStrings.xml" {
shared_bytes = Some(zr.extract(entry).map_err(zip_err)?);
} else if name == "xl/styles.xml" {
styles_bytes = Some(zr.extract(entry).map_err(zip_err)?);
} else if name.starts_with("xl/worksheets/") && name.ends_with(".xml") {
let bytes = zr.extract(entry).map_err(zip_err)?;
raw_sheets.push(RawSheet {
path: name.to_string(),
rid: String::new(), bytes,
});
}
}
let workbook_bytes =
workbook_bytes.ok_or_else(|| io_err("xlsx: workbook.xml missing from archive"))?;
let manifest = parse_workbook_manifest(&workbook_bytes)?;
if let Some(rels) = workbook_rels_bytes.as_deref() {
let rid_to_target = parse_rels(rels)?;
for raw in &mut raw_sheets {
for (rid, target) in &rid_to_target {
let normalized = if target.starts_with('/') {
target[1..].to_string()
} else {
format!("xl/{target}")
};
if normalized == raw.path || target == &raw.path {
raw.rid = rid.clone();
break;
}
}
}
}
let shared_strings = match shared_bytes.as_deref() {
Some(b) => parse_shared_strings(b)?,
None => Vec::new(),
};
let styles = match styles_bytes.as_deref() {
Some(b) => styles::parse_styles(b)?,
None => StylesInfo::default(),
};
let mut sheet_names: Vec<String> = Vec::with_capacity(manifest.sheet_names.len());
let mut raw_sheet_bytes: Vec<Vec<u8>> = Vec::with_capacity(manifest.sheet_names.len());
for (i, (name, rid)) in manifest
.sheet_names
.iter()
.zip(manifest.sheet_rids.iter())
.enumerate()
{
let fallback_path = format!("xl/worksheets/sheet{}.xml", i + 1);
let pos = raw_sheets
.iter()
.position(|r| !r.rid.is_empty() && r.rid == *rid)
.or_else(|| raw_sheets.iter().position(|r| r.path == fallback_path))
.ok_or_else(|| io_err(format!("xlsx: sheet '{name}' not found in archive")))?;
let raw = raw_sheets.remove(pos);
sheet_names.push(name.clone());
raw_sheet_bytes.push(raw.bytes);
}
Ok(WorkbookHandle {
sheet_names,
shared_strings,
styles,
date1904: manifest.date1904,
raw_sheet_bytes,
})
}
fn parse_workbook_manifest(bytes: &[u8]) -> Result<WorkbookManifest> {
let mut names = Vec::new();
let mut rids = Vec::new();
let mut date1904 = false;
let mut reader = Reader::from_reader(Cursor::new(bytes));
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf).map_err(xml_err)? {
Event::Eof => break,
Event::Empty(e) | Event::Start(e) if e.name().as_ref() == b"sheet" => {
let mut name = String::new();
let mut rid = String::new();
for a in e.attributes().with_checks(false).flatten() {
let key = a.key.as_ref();
if key == b"name" {
name = attr_to_string(&a)?;
} else if key == b"r:id" || key.ends_with(b":id") || key == b"id" {
rid = attr_to_string(&a)?;
}
}
if !name.is_empty() {
names.push(name);
rids.push(rid);
}
}
Event::Empty(e) | Event::Start(e) if e.name().as_ref() == b"workbookPr" => {
for a in e.attributes().with_checks(false).flatten() {
if a.key.as_ref() == b"date1904" {
let v = attr_to_string(&a)?;
date1904 = matches!(v.as_str(), "1" | "true" | "TRUE");
}
}
}
_ => {}
}
buf.clear();
}
if names.is_empty() {
return Err(invalid("xlsx: workbook contains no sheets"));
}
Ok(WorkbookManifest {
sheet_names: names,
sheet_rids: rids,
date1904,
})
}
fn parse_rels(bytes: &[u8]) -> Result<Vec<(String, String)>> {
let mut out = Vec::new();
let mut reader = Reader::from_reader(Cursor::new(bytes));
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf).map_err(xml_err)? {
Event::Eof => break,
Event::Empty(e) | Event::Start(e) if e.name().as_ref() == b"Relationship" => {
let mut id = String::new();
let mut target = String::new();
for a in e.attributes().with_checks(false).flatten() {
let key = a.key.as_ref();
if key == b"Id" {
id = attr_to_string(&a)?;
} else if key == b"Target" {
target = attr_to_string(&a)?;
}
}
if !id.is_empty() && !target.is_empty() {
out.push((id, target));
}
}
_ => {}
}
buf.clear();
}
Ok(out)
}
fn parse_shared_strings(bytes: &[u8]) -> Result<Vec<String>> {
let mut out: Vec<String> = Vec::new();
let mut reader = Reader::from_reader(Cursor::new(bytes));
reader.config_mut().trim_text(false);
let mut buf = Vec::new();
let mut in_si = false;
let mut in_rph = false;
let mut in_t = false;
let mut accum = String::new();
loop {
match reader.read_event_into(&mut buf).map_err(xml_err)? {
Event::Eof => break,
Event::Start(e) => match e.name().as_ref() {
b"si" => {
in_si = true;
in_rph = false;
accum.clear();
}
b"rPh" => {
in_rph = true;
}
b"t" => {
if in_si && !in_rph {
in_t = true;
}
}
_ => {}
},
Event::End(e) => match e.name().as_ref() {
b"si" => {
if in_si {
out.push(std::mem::take(&mut accum));
in_si = false;
}
}
b"rPh" => {
in_rph = false;
}
b"t" => {
in_t = false;
}
_ => {}
},
Event::Text(t) => {
if in_si && in_t {
let s = decode_and_unescape(&t)?;
accum.push_str(&s);
}
}
Event::CData(c) => {
if in_si && in_t {
let s = std::str::from_utf8(c.as_ref())
.map_err(|e| io_err(format!("xlsx: invalid UTF-8 in CDATA: {e}")))?;
accum.push_str(s);
}
}
_ => {}
}
buf.clear();
}
Ok(out)
}
fn check_bounds(r: usize, c: usize) -> Result<()> {
if r >= MAX_XLSX_ROWS || c >= MAX_XLSX_COLS {
return Err(invalid(format!(
"xlsx: cell position (row {}, col {}) exceeds the {MAX_XLSX_ROWS}x{MAX_XLSX_COLS} worksheet limit",
r + 1,
c + 1
)));
}
Ok(())
}
fn parse_worksheet(
bytes: &[u8],
shared: &[String],
styles: &StylesInfo,
date1904: bool,
) -> Result<(HashMap<(usize, usize), XlsxCellValue>, usize, usize)> {
let mut reader = Reader::from_reader(Cursor::new(bytes));
reader.config_mut().trim_text(false);
let mut buf = Vec::new();
let mut cells: HashMap<(usize, usize), XlsxCellValue> = HashMap::new();
let mut max_row = 0usize;
let mut max_col = 0usize;
let mut cur_row: usize = 0;
let mut next_implicit_row: usize = 0;
let mut col_cursor: usize = 0;
let mut cur_ref: Option<(usize, usize)> = None;
let mut cur_type: CellType = CellType::Number;
let mut cur_style: Option<u32> = None;
let mut in_value = false;
let mut value_text = String::new();
let mut in_inline_text = false;
let mut inline_text = String::new();
loop {
match reader.read_event_into(&mut buf).map_err(xml_err)? {
Event::Eof => break,
Event::Start(e) => {
let name = e.name();
let tag = name.as_ref();
if tag == b"row" {
let row = resolve_row_index(&e, next_implicit_row)?;
check_bounds(row, 0)?;
cur_row = row;
next_implicit_row = row + 1;
col_cursor = 0;
} else if tag == b"c" {
cur_ref = None;
cur_type = CellType::Number;
cur_style = None;
value_text.clear();
inline_text.clear();
for a in e.attributes().with_checks(false).flatten() {
let key = a.key.as_ref();
if key == b"r" {
let s = attr_to_string(&a)?;
cur_ref = Some(parse_ref(&s)?);
} else if key == b"t" {
cur_type = CellType::from_attr(a.value.as_ref());
} else if key == b"s" {
let s = attr_to_string(&a)?;
cur_style = s.parse::<u32>().ok();
}
}
let (r, c) = cur_ref.unwrap_or((cur_row, col_cursor));
check_bounds(r, c)?;
cur_ref = Some((r, c));
} else if tag == b"v" {
in_value = true;
value_text.clear();
} else if tag == b"t" {
in_inline_text = true;
}
}
Event::Empty(e) => {
let name = e.name();
let tag = name.as_ref();
if tag == b"row" {
let row = resolve_row_index(&e, next_implicit_row)?;
check_bounds(row, 0)?;
next_implicit_row = row + 1;
} else if tag == b"c" {
let mut coord: Option<(usize, usize)> = None;
for a in e.attributes().with_checks(false).flatten() {
if a.key.as_ref() == b"r" {
let s = attr_to_string(&a)?;
coord = Some(parse_ref(&s)?);
}
}
let (r, c) = coord.unwrap_or((cur_row, col_cursor));
check_bounds(r, c)?;
cells.insert((r, c), XlsxCellValue::Empty);
max_row = max_row.max(r + 1);
max_col = max_col.max(c + 1);
col_cursor = c + 1;
}
}
Event::End(e) => {
let name = e.name();
let tag = name.as_ref();
if tag == b"v" {
in_value = false;
} else if tag == b"t" {
in_inline_text = false;
} else if tag == b"c" {
let Some((r, c)) = cur_ref else { continue };
let mut val = finalise_cell(&cur_type, &value_text, &inline_text, shared)?;
if cur_type == CellType::Number {
if let Some(serial) = val.as_number() {
let kind = styles.date_kind(cur_style);
if kind != DateKind::None {
if let Some(iso) =
styles::excel_serial_to_iso(serial, date1904, kind)
{
val = XlsxCellValue::String(iso);
}
}
}
}
cells.insert((r, c), val);
max_row = max_row.max(r + 1);
max_col = max_col.max(c + 1);
col_cursor = c + 1;
cur_ref = None;
}
}
Event::Text(t) => {
if in_value {
let s = decode_and_unescape(&t)?;
value_text.push_str(&s);
} else if in_inline_text {
let s = decode_and_unescape(&t)?;
inline_text.push_str(&s);
}
}
Event::CData(c) => {
let s = std::str::from_utf8(c.as_ref())
.map_err(|e| io_err(format!("xlsx: invalid UTF-8 in CDATA: {e}")))?;
if in_value {
value_text.push_str(s);
} else if in_inline_text {
inline_text.push_str(s);
}
}
_ => {}
}
buf.clear();
}
const DENSITY_GUARD_FLOOR: u64 = 100_000;
const DENSITY_GUARD_FACTOR: u64 = 128;
let populated = cells.len() as u64;
let claimed = (max_row as u64).saturating_mul(max_col as u64);
if claimed >= DENSITY_GUARD_FLOOR && claimed > populated.saturating_mul(DENSITY_GUARD_FACTOR) {
return Err(invalid(format!(
"xlsx: worksheet declares {max_row} rows x {max_col} cols ({claimed} cells) but only \
{populated} cells are actually populated (ratio exceeds the {DENSITY_GUARD_FACTOR}x \
safety margin); refusing to allocate a dense matrix for what looks like a maliciously \
crafted or corrupt file"
)));
}
Ok((cells, max_row, max_col))
}
fn resolve_row_index(e: &quick_xml::events::BytesStart<'_>, next_implicit: usize) -> Result<usize> {
for a in e.attributes().with_checks(false).flatten() {
if a.key.as_ref() == b"r" {
let s = attr_to_string(&a)?;
let n: usize = s
.parse()
.map_err(|_| invalid(format!("xlsx: invalid row index '{s}'")))?;
if n == 0 {
return Err(invalid("xlsx: invalid row index '0'"));
}
return Ok(n - 1);
}
}
Ok(next_implicit)
}
fn finalise_cell(
kind: &CellType,
v: &str,
inline: &str,
shared: &[String],
) -> Result<XlsxCellValue> {
match kind {
CellType::Number => {
if v.is_empty() {
Ok(XlsxCellValue::Empty)
} else {
match v.parse::<f64>() {
Ok(n) => Ok(XlsxCellValue::Number {
value: n,
raw: v.to_string(),
}),
Err(_) => Ok(XlsxCellValue::String(v.to_string())),
}
}
}
CellType::SharedString => {
if v.is_empty() {
Ok(XlsxCellValue::Empty)
} else {
let idx: usize = v
.parse()
.map_err(|_| io_err(format!("xlsx: invalid shared-string index '{v}'")))?;
match shared.get(idx) {
Some(s) => Ok(XlsxCellValue::String(s.clone())),
None => Err(io_err(format!(
"xlsx: shared-string index {idx} out of bounds ({} strings)",
shared.len()
))),
}
}
}
CellType::InlineStr => Ok(XlsxCellValue::String(inline.to_string())),
CellType::Str => Ok(XlsxCellValue::String(v.to_string())),
CellType::Boolean => {
let b = matches!(v, "1" | "TRUE" | "true");
Ok(XlsxCellValue::Boolean(b))
}
CellType::Error => Ok(XlsxCellValue::Error(v.to_string())),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CellType {
Number,
SharedString,
InlineStr,
Str,
Boolean,
Error,
}
impl CellType {
fn from_attr(v: &[u8]) -> Self {
match v {
b"s" => CellType::SharedString,
b"inlineStr" => CellType::InlineStr,
b"str" => CellType::Str,
b"b" => CellType::Boolean,
b"e" => CellType::Error,
_ => CellType::Number,
}
}
}
fn attr_to_string(a: &Attribute<'_>) -> Result<String> {
let cow = a
.normalized_value(XmlVersion::Implicit1_0)
.map_err(xml_err)?;
Ok(cow.into_owned())
}
fn decode_and_unescape(t: &BytesText<'_>) -> Result<String> {
let decoded = t
.decode()
.map_err(|e| io_err(format!("xlsx: text decode failure: {e}")))?;
let unescaped =
unescape(&decoded).map_err(|e| io_err(format!("xlsx: text unescape failure: {e}")))?;
Ok(unescaped.into_owned())
}