#[cfg(any(feature = "xml", feature = "office", feature = "hwpx"))]
use std::borrow::Cow;
#[cfg(any(feature = "xml", feature = "office", feature = "hwpx"))]
pub(crate) fn decode_xml_to_utf8(content: &[u8]) -> (String, bool) {
let prolog_len = content.len().min(256);
let prolog = String::from_utf8_lossy(&content[..prolog_len]);
let declared = prolog
.split_once("encoding")
.and_then(|(_, rest)| rest.split_once(['"', '\'']))
.and_then(|(_, rest)| rest.split(['"', '\'']).next())
.map(str::trim)
.filter(|s| !s.is_empty());
let outcome = crate::utils::decode_with_provenance(content, declared);
(
crate::utils::strip_bom(&outcome.text).to_string(),
outcome.replaced_characters,
)
}
#[cfg(any(feature = "xml", feature = "office", feature = "hwpx"))]
#[inline]
pub(crate) fn xml_tag_name(name: &str) -> Cow<'_, str> {
Cow::Borrowed(name)
}
#[cfg(any(feature = "xml", feature = "office", feature = "hwpx"))]
pub(crate) struct EntityReader<'x> {
reader: quick_xml::Reader<&'x [u8]>,
pending: Option<quick_xml::events::Event<'x>>,
}
#[cfg(any(feature = "xml", feature = "office", feature = "hwpx"))]
#[allow(dead_code)]
impl<'x> EntityReader<'x> {
pub(crate) fn from_str(content: &'x str) -> Self {
Self {
reader: quick_xml::Reader::from_str(content),
pending: None,
}
}
pub(crate) fn from_bytes(content: &'x [u8]) -> Self {
Self {
reader: quick_xml::Reader::from_reader(content),
pending: None,
}
}
pub(crate) fn config_mut(&mut self) -> &mut quick_xml::reader::Config {
self.reader.config_mut()
}
pub(crate) fn buffer_position(&self) -> u64 {
self.reader.buffer_position()
}
pub(crate) fn read_event(&mut self) -> quick_xml::Result<quick_xml::events::Event<'x>> {
use quick_xml::events::{BytesText, Event};
let first = match self.pending.take() {
Some(event) => event,
None => self.reader.read_event()?,
};
let mut text = match first {
Event::Text(t) => std::borrow::Cow::Borrowed(t.as_ref()).into_owned(),
Event::GeneralRef(r) => resolve_general_ref(&r),
other => return Ok(other),
};
loop {
match self.reader.read_event()? {
Event::Text(t) => text.push_str(&std::borrow::Cow::Borrowed(t.as_ref())),
Event::GeneralRef(r) => text.push_str(&resolve_general_ref(&r)),
other => {
self.pending = Some(other);
break;
}
}
}
Ok(Event::Text(BytesText::from_escaped(text)))
}
}
#[cfg(any(feature = "xml", feature = "office", feature = "hwpx"))]
pub(crate) fn resolve_general_ref(reference: &quick_xml::events::BytesRef<'_>) -> String {
if let Ok(Some(ch)) = reference.resolve_char_ref() {
return ch.to_string();
}
let name: Cow<str> = Cow::Borrowed(reference.as_ref());
if let Some(resolved) = quick_xml::escape::resolve_predefined_entity(&name) {
return resolved.to_string();
}
if name.as_ref() == "nbsp" {
return "\u{00A0}".to_string();
}
String::new()
}
#[cfg(all(test, any(feature = "xml", feature = "office")))]
mod tests {
use super::*;
use quick_xml::events::{BytesRef, Event};
fn resolve(name: &str) -> String {
resolve_general_ref(&BytesRef::new(name))
}
#[test]
fn test_resolve_general_ref_predefined_entities() {
assert_eq!(resolve("amp"), "&");
assert_eq!(resolve("lt"), "<");
assert_eq!(resolve("gt"), ">");
assert_eq!(resolve("quot"), "\"");
assert_eq!(resolve("apos"), "'");
assert_eq!(resolve("nbsp"), "\u{00A0}");
}
#[test]
fn test_resolve_general_ref_character_references() {
assert_eq!(resolve("#8212"), "\u{2014}");
assert_eq!(resolve("#x2014"), "\u{2014}");
assert_eq!(resolve("#65"), "A");
}
#[test]
fn test_resolve_general_ref_unknown_entity_is_empty() {
assert_eq!(resolve("unknownentity"), "");
assert_eq!(resolve("#xZZ"), "");
assert_eq!(resolve("#1114112"), ""); }
#[test]
fn test_entity_reader_coalesces_text_and_references() {
let xml = "<root><a>Profits & losses</a><b>5>3</b><c/></root>";
let mut reader = EntityReader::from_str(xml);
let mut texts = Vec::new();
loop {
match reader.read_event().expect("valid XML") {
Event::Text(t) => texts.push(t.as_ref().to_string()),
Event::Eof => break,
_ => {}
}
}
assert_eq!(texts, vec!["Profits & losses", "5>3"]);
}
#[test]
fn test_entity_reader_preserves_non_text_events() {
let xml = "<root attr=\"v\">xAy<child/></root>";
let mut reader = EntityReader::from_str(xml);
let mut summary = Vec::new();
loop {
match reader.read_event().expect("valid XML") {
Event::Start(e) => summary.push(format!("start:{}", e.name().as_ref())),
Event::Empty(e) => summary.push(format!("empty:{}", e.name().as_ref())),
Event::End(e) => summary.push(format!("end:{}", e.name().as_ref())),
Event::Text(t) => summary.push(format!("text:{}", t.as_ref())),
Event::Eof => break,
_ => {}
}
}
assert_eq!(summary, vec!["start:root", "text:xAy", "empty:child", "end:root"]);
}
}
pub(crate) fn doctype_end(tail: &str) -> Option<usize> {
let mut bracket_depth: usize = 0;
for (idx, ch) in tail.char_indices() {
match ch {
'[' => bracket_depth += 1,
']' => bracket_depth = bracket_depth.saturating_sub(1),
'>' if bracket_depth == 0 => return Some(idx),
_ => {}
}
}
None
}
#[cfg(any(feature = "xml", feature = "office", feature = "hwpx"))]
pub(crate) fn strip_doctype(xml: &str) -> std::borrow::Cow<'_, str> {
let Some(start) = xml.find("<!DOCTYPE") else {
return std::borrow::Cow::Borrowed(xml);
};
let tail = &xml[start + "<!DOCTYPE".len()..];
let Some(end) = doctype_end(tail) else {
return std::borrow::Cow::Borrowed(xml);
};
let mut out = String::with_capacity(xml.len());
out.push_str(&xml[..start]);
out.push_str(&tail[end + 1..]);
std::borrow::Cow::Owned(out)
}