use quick_xml::XmlVersion;
use quick_xml::events::{BytesCData, BytesDecl, BytesPI, BytesRef, BytesStart, BytesText};
use quick_xml::name::{QName, ResolveResult};
use quick_xml::reader::NsReader;
use super::{ReadLimits, XlsxErrorCode, XlsxReadError};
use crate::SourceId;
pub(super) const SPREADSHEETML_TRANSITIONAL: &[u8] =
b"http://schemas.openxmlformats.org/spreadsheetml/2006/main";
pub(super) const SPREADSHEETML_STRICT: &[u8] = b"http://purl.oclc.org/ooxml/spreadsheetml/main";
pub(super) const DOCUMENT_RELATIONSHIPS_TRANSITIONAL: &[u8] =
b"http://schemas.openxmlformats.org/officeDocument/2006/relationships";
pub(super) const DOCUMENT_RELATIONSHIPS_STRICT: &[u8] =
b"http://purl.oclc.org/ooxml/officeDocument/relationships";
#[derive(Debug)]
pub(super) struct XmlAttribute {
local_name: Box<str>,
namespace: Option<Box<[u8]>>,
value: Box<str>,
}
#[derive(Debug)]
pub(super) struct XmlAttributes(Vec<XmlAttribute>);
impl XmlAttributes {
pub(super) fn unqualified(&self, local_name: &str) -> Option<&str> {
self.0
.iter()
.find(|attribute| {
attribute.namespace.is_none() && attribute.local_name.as_ref() == local_name
})
.map(|attribute| attribute.value.as_ref())
}
pub(super) fn namespaced(&self, namespace: &[u8], local_name: &str) -> Option<&str> {
self.0
.iter()
.find(|attribute| {
attribute.namespace.as_deref() == Some(namespace)
&& attribute.local_name.as_ref() == local_name
})
.map(|attribute| attribute.value.as_ref())
}
pub(super) fn iter(&self) -> impl ExactSizeIterator<Item = (&str, Option<&[u8]>, &str)> {
self.0.iter().map(|attribute| {
(
attribute.local_name.as_ref(),
attribute.namespace.as_deref(),
attribute.value.as_ref(),
)
})
}
}
#[derive(Debug)]
pub(super) struct XmlBudget {
depth: u64,
limits: ReadLimits,
source: SourceId,
invalid_code: XlsxErrorCode,
}
impl XmlBudget {
pub(super) fn new(limits: ReadLimits, source: SourceId, invalid_code: XlsxErrorCode) -> Self {
Self {
depth: 0,
limits,
source,
invalid_code,
}
}
pub(super) fn start(&mut self) -> Result<u64, XlsxReadError> {
let next = self.depth.saturating_add(1);
if next > self.limits.max_xml_depth() {
return Err(self.error(XlsxErrorCode::XmlDepthExceeded));
}
self.depth = next;
Ok(next)
}
pub(super) fn empty(&self) -> Result<u64, XlsxReadError> {
let next = self.depth.saturating_add(1);
if next > self.limits.max_xml_depth() {
return Err(self.error(XlsxErrorCode::XmlDepthExceeded));
}
Ok(next)
}
pub(super) fn end(&mut self) -> Result<u64, XlsxReadError> {
let current = self.depth;
self.depth = self
.depth
.checked_sub(1)
.ok_or_else(|| self.error(self.invalid_code))?;
Ok(current)
}
pub(super) fn finish(&self, saw_root: bool) -> Result<(), XlsxReadError> {
if !saw_root || self.depth != 0 {
return Err(self.error(self.invalid_code));
}
Ok(())
}
pub(super) const fn limits(&self) -> ReadLimits {
self.limits
}
pub(super) const fn current_depth(&self) -> u64 {
self.depth
}
pub(super) const fn source_id(&self) -> &SourceId {
&self.source
}
pub(super) fn error(&self, code: XlsxErrorCode) -> XlsxReadError {
XlsxReadError::new(code).at_source(self.source.clone())
}
}
pub(super) fn reader(bytes: &[u8]) -> NsReader<&[u8]> {
let mut reader = NsReader::from_reader(bytes);
let config = reader.config_mut();
config.check_end_names = true;
config.allow_unmatched_ends = false;
config.check_comments = true;
config.expand_empty_elements = false;
config.trim_text(false);
reader
}
pub(super) fn validate_xml_declaration(
declaration: &BytesDecl<'_>,
budget: &XmlBudget,
) -> Result<(), XlsxReadError> {
let version = declaration
.xml_version()
.map_err(|error| budget.error(budget.invalid_code).with_cause(error))?;
if version != XmlVersion::Explicit1_0 {
return Err(budget.error(budget.invalid_code));
}
let declaration_text = std::str::from_utf8(&declaration[..])
.map_err(|error| budget.error(budget.invalid_code).with_cause(error))?;
let declaration_start = BytesStart::from_content(declaration_text, 3);
let mut saw_encoding = false;
let mut saw_standalone = false;
for (index, attribute) in declaration_start.attributes().with_checks(true).enumerate() {
let attribute =
attribute.map_err(|error| budget.error(budget.invalid_code).with_cause(error))?;
let key = attribute.key.as_ref();
let value = attribute.value.as_ref();
let valid = match (index, key) {
(0, b"version") => value == b"1.0",
(_, b"encoding") if !saw_encoding && !saw_standalone => {
saw_encoding = true;
is_xml_encoding_name(value)
}
(_, b"standalone") if !saw_standalone => {
saw_standalone = true;
matches!(value, b"yes" | b"no")
}
_ => false,
};
if !valid {
return Err(budget.error(budget.invalid_code));
}
}
Ok(())
}
pub(super) fn validate_processing_instruction(
instruction: &BytesPI<'_>,
budget: &XmlBudget,
) -> Result<(), XlsxReadError> {
let target = std::str::from_utf8(instruction.target())
.map_err(|error| budget.error(budget.invalid_code).with_cause(error))?;
let mut characters = target.chars();
let valid_name = characters.next().is_some_and(is_xml_name_start_character)
&& characters.all(is_xml_name_character);
if !valid_name || target.eq_ignore_ascii_case("xml") {
return Err(budget.error(budget.invalid_code));
}
Ok(())
}
fn is_xml_name_start_character(character: char) -> bool {
matches!(
character,
':'
| 'A'..='Z'
| '_'
| 'a'..='z'
| '\u{00C0}'..='\u{00D6}'
| '\u{00D8}'..='\u{00F6}'
| '\u{00F8}'..='\u{02FF}'
| '\u{0370}'..='\u{037D}'
| '\u{037F}'..='\u{1FFF}'
| '\u{200C}'..='\u{200D}'
| '\u{2070}'..='\u{218F}'
| '\u{2C00}'..='\u{2FEF}'
| '\u{3001}'..='\u{D7FF}'
| '\u{F900}'..='\u{FDCF}'
| '\u{FDF0}'..='\u{FFFD}'
| '\u{10000}'..='\u{EFFFF}'
)
}
fn is_xml_name_character(character: char) -> bool {
is_xml_name_start_character(character)
|| matches!(
character,
'-' | '.' | '0'..='9' | '\u{00B7}' | '\u{0300}'..='\u{036F}' | '\u{203F}'..='\u{2040}'
)
}
fn is_xml_encoding_name(value: &[u8]) -> bool {
value.first().is_some_and(u8::is_ascii_alphabetic)
&& value[1..]
.iter()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
}
pub(super) fn read_attributes(
element: &BytesStart<'_>,
reader: &NsReader<&[u8]>,
budget: &XmlBudget,
) -> Result<XmlAttributes, XlsxReadError> {
let mut values = Vec::new();
for (index, attribute) in element.attributes().with_checks(true).enumerate() {
if index as u64 >= budget.limits().max_xml_attributes() {
return Err(budget.error(XlsxErrorCode::XmlAttributesExceeded));
}
let attribute =
attribute.map_err(|error| budget.error(budget.invalid_code).with_cause(error))?;
if attribute.key.as_ref() == b"xmlns" || attribute.key.as_ref().starts_with(b"xmlns:") {
continue;
}
let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
let namespace = match namespace {
ResolveResult::Unbound => None,
ResolveResult::Bound(namespace) => Some(Box::<[u8]>::from(namespace.as_ref())),
ResolveResult::Unknown(prefix) => {
return Err(budget
.error(budget.invalid_code)
.with_detail(String::from_utf8_lossy(&prefix).into_owned()));
}
};
let local_name = std::str::from_utf8(local_name.as_ref())
.map_err(|error| budget.error(budget.invalid_code).with_cause(error))?;
let value = attribute
.decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
.map_err(|error| budget.error(budget.invalid_code).with_cause(error))?;
values.push(XmlAttribute {
local_name: local_name.to_owned().into_boxed_str(),
namespace,
value: value.into_owned().into_boxed_str(),
});
}
Ok(XmlAttributes(values))
}
pub(super) fn require_spreadsheet_element(
is_spreadsheet: bool,
local_name: &[u8],
expected_local_name: &[u8],
budget: &XmlBudget,
) -> Result<(), XlsxReadError> {
if local_name != expected_local_name || !is_spreadsheet {
return Err(budget.error(budget.invalid_code));
}
Ok(())
}
pub(super) fn is_spreadsheet_element(
reader: &NsReader<&[u8]>,
name: QName<'_>,
budget: &XmlBudget,
) -> Result<bool, XlsxReadError> {
is_element_in_namespace(reader, name, SPREADSHEETML_TRANSITIONAL, budget)
}
pub(super) fn is_element_in_namespace(
reader: &NsReader<&[u8]>,
name: QName<'_>,
expected_namespace: &[u8],
budget: &XmlBudget,
) -> Result<bool, XlsxReadError> {
match reader.resolver().resolve_element(name).0 {
ResolveResult::Bound(namespace) => {
let namespace = namespace.as_ref();
Ok(namespace == expected_namespace
|| (expected_namespace == SPREADSHEETML_TRANSITIONAL
&& namespace == SPREADSHEETML_STRICT))
}
ResolveResult::Unbound => Ok(false),
ResolveResult::Unknown(prefix) => Err(budget
.error(budget.invalid_code)
.with_detail(String::from_utf8_lossy(&prefix).into_owned())),
}
}
pub(super) fn decode_text(
text: &BytesText<'_>,
budget: &XmlBudget,
) -> Result<String, XlsxReadError> {
text.xml_content(XmlVersion::Implicit1_0)
.map(|value| value.into_owned())
.map_err(|error| budget.error(budget.invalid_code).with_cause(error))
}
pub(super) fn decode_cdata(
text: &BytesCData<'_>,
budget: &XmlBudget,
) -> Result<String, XlsxReadError> {
text.xml_content(XmlVersion::Implicit1_0)
.map(|value| value.into_owned())
.map_err(|error| budget.error(budget.invalid_code).with_cause(error))
}
pub(super) fn decode_reference(
reference: &BytesRef<'_>,
budget: &XmlBudget,
) -> Result<String, XlsxReadError> {
if let Some(character) = reference
.resolve_char_ref()
.map_err(|error| budget.error(budget.invalid_code).with_cause(error))?
{
return Ok(character.to_string());
}
let name = reference
.decode()
.map_err(|error| budget.error(budget.invalid_code).with_cause(error))?;
match name.as_ref() {
"lt" => Ok("<".to_owned()),
"gt" => Ok(">".to_owned()),
"amp" => Ok("&".to_owned()),
"apos" => Ok("'".to_owned()),
"quot" => Ok("\"".to_owned()),
_ => Err(budget
.error(budget.invalid_code)
.with_detail(name.into_owned())),
}
}