mod relationships;
mod shared_strings;
mod styles;
mod workbook;
mod worksheet;
pub(crate) use relationships::parse_relationships;
pub(crate) use shared_strings::{parse_shared_strings, SharedStringTable};
pub(crate) use styles::parse_styles;
pub(crate) use workbook::parse_workbook_xml;
pub(crate) use worksheet::{parse_worksheet, PendingSharedString, PendingStyle};
use crate::error::Error;
use quick_xml::events::{BytesStart, Event};
use quick_xml::Reader;
use std::io::BufRead;
pub(crate) fn create_secure_reader<R: BufRead>(inner: R) -> Reader<R> {
let mut reader = Reader::from_reader(inner);
reader.config_mut().trim_text(false);
reader
}
pub(crate) fn convert_xml_error(path: &str, err: quick_xml::Error) -> Error {
if let quick_xml::Error::Io(io_err) = &err {
if let Some(limit) = io_err
.get_ref()
.and_then(|e| e.downcast_ref::<crate::container::sanitize::LimitExceeded>())
{
return Error::ZipBombDetected {
limit: limit.limit,
actual: limit.actual,
};
}
}
Error::XmlParse {
path: path.to_string(),
source: Box::new(err),
}
}
pub(crate) fn read_event<'a>(
reader: &mut Reader<impl BufRead>,
buf: &'a mut Vec<u8>,
path: &str,
) -> Result<Event<'a>, Error> {
let event = reader
.read_event_into(buf)
.map_err(|err| convert_xml_error(path, err))?;
if matches!(event, Event::DocType(_)) {
return Err(Error::DoctypeRejected {
path: path.to_string(),
});
}
Ok(event)
}
pub(crate) fn required_attr(
start: &BytesStart<'_>,
path: &str,
name: &'static str,
) -> Result<String, Error> {
for attr in start.attributes() {
let attr = attr.map_err(|err| Error::XmlParse {
path: path.to_string(),
source: Box::new(err),
})?;
if attr.key.as_ref() == name.as_bytes() {
let value = attr
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.map_err(|err| Error::XmlParse {
path: path.to_string(),
source: Box::new(err),
})?;
return Ok(value.into_owned());
}
}
Err(Error::MissingRequiredElement {
path: path.to_string(),
name,
})
}
pub(crate) fn optional_attr(
start: &BytesStart<'_>,
path: &str,
name: &str,
) -> Result<Option<String>, Error> {
for attr in start.attributes() {
let attr = attr.map_err(|err| Error::XmlParse {
path: path.to_string(),
source: Box::new(err),
})?;
if attr.key.as_ref() == name.as_bytes() {
let value = attr
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.map_err(|err| Error::XmlParse {
path: path.to_string(),
source: Box::new(err),
})?;
return Ok(Some(value.into_owned()));
}
}
Ok(None)
}
pub(crate) fn push_general_ref(
text: &mut String,
r: &quick_xml::events::BytesRef<'_>,
path: &str,
) -> Result<(), Error> {
match r.resolve_char_ref().map_err(|err| Error::XmlParse {
path: path.to_string(),
source: Box::new(err),
})? {
Some(ch) => text.push(ch),
None => {
let decoded = r.decode().map_err(|err| Error::XmlParse {
path: path.to_string(),
source: Box::new(err),
})?;
let resolved =
quick_xml::escape::resolve_predefined_entity(&decoded).ok_or_else(|| {
Error::XmlParse {
path: path.to_string(),
source: format!("unknown XML entity reference: &{decoded};").into(),
}
})?;
text.push_str(resolved);
}
}
Ok(())
}
pub(crate) fn concat_rich_text<R: BufRead>(
reader: &mut Reader<R>,
path: &str,
) -> Result<String, Error> {
let mut text = String::new();
let mut buf = Vec::new();
let mut skip_depth: u32 = 0;
let mut t_start: Option<usize> = None;
let mut t_preserve = false;
loop {
match read_event(reader, &mut buf, path)? {
Event::Start(e) if e.local_name().as_ref() == b"rPr" => skip_depth += 1,
Event::Start(e) if e.local_name().as_ref() == b"rPh" => skip_depth += 1,
Event::End(e) if e.local_name().as_ref() == b"rPr" => skip_depth -= 1,
Event::End(e) if e.local_name().as_ref() == b"rPh" => skip_depth -= 1,
Event::Start(e) if skip_depth == 0 && e.local_name().as_ref() == b"t" => {
t_preserve = optional_attr(&e, path, "xml:space")?.as_deref() == Some("preserve");
t_start = Some(text.len());
}
Event::End(e) if skip_depth == 0 && e.local_name().as_ref() == b"t" => {
if let Some(start) = t_start.take() {
if !t_preserve {
trim_tail_in_place(&mut text, start);
}
}
}
Event::Text(e) if skip_depth == 0 && t_start.is_some() => {
let decoded = e.decode().map_err(|err| Error::XmlParse {
path: path.to_string(),
source: Box::new(err),
})?;
text.push_str(&normalize_line_endings(&decoded));
}
Event::CData(e) if skip_depth == 0 && t_start.is_some() => {
let decoded = e.decode().map_err(|err| Error::XmlParse {
path: path.to_string(),
source: Box::new(err),
})?;
text.push_str(&normalize_line_endings(&decoded));
}
Event::GeneralRef(e) if skip_depth == 0 && t_start.is_some() => {
push_general_ref(&mut text, &e, path)?
}
Event::End(e)
if e.local_name().as_ref() == b"si" || e.local_name().as_ref() == b"is" =>
{
break;
}
Event::Eof => {
return Err(Error::MissingRequiredElement {
path: path.to_string(),
name: "si/is closing tag",
})
}
_ => {}
}
buf.clear();
}
if text.contains("_x000D_") {
text = text.replace("_x000D_", "\r");
}
Ok(text)
}
fn trim_tail_in_place(text: &mut String, start: usize) {
let trailing_len = text.len() - start - text[start..].trim_end().len();
text.truncate(text.len() - trailing_len);
let leading_len = text[start..].len() - text[start..].trim_start().len();
if leading_len > 0 {
text.drain(start..start + leading_len);
}
}
fn normalize_line_endings(s: &str) -> std::borrow::Cow<'_, str> {
if !s.contains('\r') {
return std::borrow::Cow::Borrowed(s);
}
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\r' {
if chars.peek() == Some(&'\n') {
chars.next();
}
out.push('\n');
} else {
out.push(c);
}
}
std::borrow::Cow::Owned(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::container::sanitize::BoundedReader;
use std::io::{self, Read};
#[test]
fn create_secure_reader_disables_text_trimming() {
let xml = b"<root> padded </root>".as_slice();
let mut reader = create_secure_reader(xml);
let mut buf = Vec::new();
assert!(matches!(
reader.read_event_into(&mut buf).unwrap(),
Event::Start(_)
));
buf.clear();
match reader.read_event_into(&mut buf).unwrap() {
Event::Text(t) => assert_eq!(t.decode().unwrap(), " padded "),
other => panic!("expected Text event, got {other:?}"),
}
}
#[test]
fn convert_xml_error_maps_limit_exceeded_to_zip_bomb_detected() {
let mut cumulative = 0u64;
let data = [0u8; 11];
let mut bounded = BoundedReader::new(&data[..], 10, &mut cumulative, 1000);
let mut out = Vec::new();
let io_err = bounded.read_to_end(&mut out).unwrap_err();
let quick_xml_err = quick_xml::Error::Io(std::sync::Arc::new(io_err));
let err = convert_xml_error("xl/worksheets/sheet1.xml", quick_xml_err);
match err {
Error::ZipBombDetected { limit, actual } => {
assert_eq!(limit, 10);
assert_eq!(actual, 11);
}
other => panic!("expected ZipBombDetected, got {other:?}"),
}
}
#[test]
fn convert_xml_error_falls_back_to_xml_parse() {
let io_err = io::Error::other("plain io failure");
let quick_xml_err = quick_xml::Error::Io(std::sync::Arc::new(io_err));
let err = convert_xml_error("xl/worksheets/sheet1.xml", quick_xml_err);
match err {
Error::XmlParse { path, .. } => assert_eq!(path, "xl/worksheets/sheet1.xml"),
other => panic!("expected XmlParse, got {other:?}"),
}
}
#[test]
fn required_attr_returns_value_when_present() {
let xml = br#"<c r="A1" t="s"></c>"#.as_slice();
let mut reader = create_secure_reader(xml);
let mut buf = Vec::new();
let event = reader.read_event_into(&mut buf).unwrap();
let Event::Start(start) = event else {
panic!("expected Start event");
};
assert_eq!(required_attr(&start, "sheet1.xml", "r").unwrap(), "A1");
assert_eq!(required_attr(&start, "sheet1.xml", "t").unwrap(), "s");
}
#[test]
fn required_attr_errors_when_absent() {
let xml = br#"<c r="A1"></c>"#.as_slice();
let mut reader = create_secure_reader(xml);
let mut buf = Vec::new();
let event = reader.read_event_into(&mut buf).unwrap();
let Event::Start(start) = event else {
panic!("expected Start event");
};
let err = required_attr(&start, "sheet1.xml", "t").unwrap_err();
assert!(matches!(
err,
Error::MissingRequiredElement { name: "t", .. }
));
}
fn parse_si_body_result(xml: &[u8]) -> Result<String, Error> {
let mut reader = create_secure_reader(xml);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf).unwrap() {
Event::Start(e)
if e.local_name().as_ref() == b"si" || e.local_name().as_ref() == b"is" =>
{
break
}
Event::Eof => panic!("no <si>/<is> start tag found"),
_ => {}
}
buf.clear();
}
buf.clear();
concat_rich_text(&mut reader, "xl/sharedStrings.xml")
}
fn parse_si_body(xml: &[u8]) -> String {
parse_si_body_result(xml).unwrap()
}
#[test]
fn concat_rich_text_single_bare_t() {
let xml = b"<si><t>hello</t></si>";
assert_eq!(parse_si_body(xml), "hello");
}
#[test]
fn concat_rich_text_multiple_runs() {
let xml = br#"<si><r><t xml:space="preserve">hello </t></r><r><t>world</t></r></si>"#;
assert_eq!(parse_si_body(xml), "hello world");
}
#[test]
fn concat_rich_text_excludes_rpr_and_rph() {
let xml = b"<si><r><rPr><b/></rPr><t>bold</t></r><rPh><t>phonetic</t></rPh></si>";
assert_eq!(parse_si_body(xml), "bold");
}
#[test]
fn concat_rich_text_ignores_whitespace_between_sibling_tags_in_pretty_printed_xml() {
let xml = b"<si>\n <r>\n <rPr>\n <b/>\n </rPr>\n <t>NN</t>\n </r>\n</si>";
assert_eq!(parse_si_body(xml), "NN");
}
#[test]
fn concat_rich_text_trims_whitespace_unless_xml_space_preserve() {
let untagged = b"<si><t> trimmed value </t></si>";
assert_eq!(parse_si_body(untagged), "trimmed value");
let preserved = br#"<si><t xml:space="preserve"> preserved value </t></si>"#;
assert_eq!(parse_si_body(preserved), " preserved value ");
let whitespace_only = b"<si><t> \t\n</t></si>";
assert_eq!(parse_si_body(whitespace_only), "");
}
#[test]
fn concat_rich_text_trims_each_run_independently() {
let xml = br#"<si><r><t xml:space="preserve"> a </t></r><r><t> b </t></r></si>"#;
assert_eq!(parse_si_body(xml), " a b");
}
#[test]
fn concat_rich_text_restores_x000d_escape() {
let xml = b"<si><t>ABC_x000D_\nDEF</t></si>";
assert_eq!(parse_si_body(xml), "ABC\r\nDEF");
}
#[test]
fn concat_rich_text_x000d_escape_followed_by_raw_crlf_source_line_ending_is_not_doubled() {
let xml = b"<si><t>ABC_x000D_\r\nDEF</t></si>";
assert_eq!(parse_si_body(xml), "ABC\r\nDEF");
}
#[test]
fn concat_rich_text_normalizes_raw_crlf_and_lone_cr_to_lf() {
let crlf = b"<si><t xml:space=\"preserve\">a\r\nb</t></si>";
assert_eq!(parse_si_body(crlf), "a\nb");
let lone_cr = b"<si><t xml:space=\"preserve\">a\rb</t></si>";
assert_eq!(parse_si_body(lone_cr), "a\nb");
}
#[test]
fn concat_rich_text_does_not_normalize_an_explicit_cr_character_reference() {
let xml = b"<si><t xml:space=\"preserve\">a b</t></si>";
assert_eq!(parse_si_body(xml), "a\rb");
}
#[test]
fn concat_rich_text_reads_cdata_content() {
let xml = b"<si><t><![CDATA[Hello CDATA]]></t></si>";
assert_eq!(parse_si_body(xml), "Hello CDATA");
}
#[test]
fn concat_rich_text_invalid_utf8_in_plain_text_is_xml_parse_error() {
let mut xml = b"<si><t>".to_vec();
xml.push(0xFF);
xml.extend_from_slice(b"</t></si>");
let err = parse_si_body_result(&xml).unwrap_err();
assert!(matches!(err, Error::XmlParse { .. }));
}
#[test]
fn concat_rich_text_invalid_utf8_in_cdata_is_xml_parse_error() {
let mut xml = b"<si><t><![CDATA[".to_vec();
xml.push(0xFF); xml.extend_from_slice(b"]]></t></si>");
let err = parse_si_body_result(&xml).unwrap_err();
assert!(matches!(err, Error::XmlParse { .. }));
}
#[test]
fn concat_rich_text_eof_before_closing_tag_is_missing_required_element() {
let xml = b"<si><t>unterminated";
let err = parse_si_body_result(xml).unwrap_err();
assert!(matches!(
err,
Error::MissingRequiredElement {
name: "si/is closing tag",
..
}
));
}
#[test]
fn read_event_rejects_doctype_and_stops_reading() {
let xml =
br#"<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]><root/>"#.as_slice();
let mut reader = create_secure_reader(xml);
let mut buf = Vec::new();
let err = read_event(&mut reader, &mut buf, "xl/sharedStrings.xml").unwrap_err();
assert!(matches!(err, Error::DoctypeRejected { .. }));
}
#[test]
fn read_event_passes_through_legitimate_xml_without_false_positives() {
let xml = br#"<?xml version="1.0"?><sst><si><t>ok</t></si></sst>"#.as_slice();
let mut reader = create_secure_reader(xml);
let mut buf = Vec::new();
loop {
if read_event(&mut reader, &mut buf, "xl/sharedStrings.xml").unwrap() == Event::Eof {
break;
}
buf.clear();
}
}
#[test]
fn read_event_converts_syntax_error_via_convert_xml_error() {
let xml = b"<root><unclosed></root>".as_slice();
let mut reader = create_secure_reader(xml);
let mut buf = Vec::new();
let mut last_err = None;
loop {
buf.clear();
match read_event(&mut reader, &mut buf, "sheet1.xml") {
Ok(Event::Eof) => break,
Ok(_) => continue,
Err(err) => {
last_err = Some(err);
break;
}
}
}
assert!(matches!(last_err, Some(Error::XmlParse { .. })));
}
#[test]
fn read_event_converts_bounded_reader_limit_via_convert_xml_error() {
let mut cumulative = 0u64;
let data = b"<root>this is too much data</root>".to_vec();
let bounded = BoundedReader::new(&data[..], 5, &mut cumulative, 1000);
let mut reader = create_secure_reader(io::BufReader::new(bounded));
let mut buf = Vec::new();
let err = loop {
buf.clear();
match read_event(&mut reader, &mut buf, "sheet1.xml") {
Ok(Event::Eof) => panic!("expected an error before EOF"),
Ok(_) => continue,
Err(err) => break err,
}
};
assert!(matches!(err, Error::ZipBombDetected { limit: 5, .. }));
}
}