use std::borrow::Cow;
use memchr::memchr;
use crate::error::Result;
use crate::options::ParseOptions;
use crate::xml_bytes_reader::{
BytesAttrs, BytesCData, BytesComment, BytesEndTag, BytesEvent, BytesPi, BytesStartTag,
BytesText, XmlBytesReader, XmlDeclInfo,
};
#[derive(Debug)]
pub struct Attr<'src> {
pub name: &'src str,
pub value: Cow<'src, str>,
}
impl<'src> Attr<'src> {
pub fn name(&self) -> &'src str { self.name }
pub fn value(&self) -> &str { &self.value }
}
pub struct Attrs<'r, 'src> {
inner: BytesAttrs<'r, 'src>,
}
impl<'src> Iterator for Attrs<'_, 'src> {
type Item = Result<Attr<'src>>;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|res| res.map(|ba| Attr {
name: unsafe { std::str::from_utf8_unchecked(ba.name) },
value: cow_bytes_to_str(ba.value),
}))
}
}
pub struct StartTag<'r, 'src> {
inner: BytesStartTag<'r, 'src>,
}
impl<'r, 'src> StartTag<'r, 'src> {
#[inline]
pub fn name(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(self.inner.name()) }
}
pub fn name_cow(&self) -> Cow<'src, str> {
match self.inner.name_cow() {
Cow::Borrowed(b) => Cow::Borrowed(unsafe { std::str::from_utf8_unchecked(b) }),
Cow::Owned(v) => Cow::Owned(unsafe { String::from_utf8_unchecked(v) }),
}
}
pub fn attrs(self) -> Attrs<'r, 'src> {
Attrs { inner: self.inner.attrs() }
}
#[inline]
pub fn attrs_str(&self) -> &'src str {
unsafe { std::str::from_utf8_unchecked(self.inner.attrs_bytes()) }
}
}
impl std::fmt::Debug for StartTag<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StartTag")
.field("name", &self.name())
.field("attrs", &self.attrs_str())
.finish()
}
}
pub struct EndTag<'src> {
inner: BytesEndTag<'src>,
}
impl<'src> EndTag<'src> {
#[inline]
pub fn name(&self) -> &'src str {
unsafe { std::str::from_utf8_unchecked(self.inner.name()) }
}
}
impl std::fmt::Debug for EndTag<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EndTag").field("name", &self.name()).finish()
}
}
pub struct Text<'src> { inner: BytesText<'src> }
impl<'src> Text<'src> {
#[inline] pub fn as_str(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(self.inner.as_bytes()) }
}
pub fn into_str(self) -> Cow<'src, str> { cow_bytes_to_str(self.inner.into_bytes()) }
}
impl std::fmt::Debug for Text<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Text({:?})", self.as_str())
}
}
pub struct CData<'src> { inner: BytesCData<'src> }
impl<'src> CData<'src> {
#[inline] pub fn as_str(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(self.inner.as_bytes()) }
}
pub fn into_str(self) -> Cow<'src, str> { cow_bytes_to_str(self.inner.into_bytes()) }
}
impl std::fmt::Debug for CData<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CData({:?})", self.as_str())
}
}
pub struct Comment<'src> { inner: BytesComment<'src> }
impl<'src> Comment<'src> {
#[inline] pub fn as_str(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(self.inner.as_bytes()) }
}
pub fn into_str(self) -> Cow<'src, str> { cow_bytes_to_str(self.inner.into_bytes()) }
}
impl std::fmt::Debug for Comment<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Comment({:?})", self.as_str())
}
}
pub struct Pi<'src> { inner: BytesPi<'src> }
impl<'src> Pi<'src> {
#[inline] pub fn target(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(self.inner.target()) }
}
#[inline] pub fn content(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(self.inner.content()) }
}
pub fn into_parts(self) -> (Cow<'src, str>, Cow<'src, str>) {
let (t, c) = self.inner.into_parts();
(cow_bytes_to_str(t), cow_bytes_to_str(c))
}
}
impl std::fmt::Debug for Pi<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Pi")
.field("target", &self.target())
.field("content", &self.content())
.finish()
}
}
#[derive(Debug)]
pub enum Event<'r, 'src> {
StartElement(StartTag<'r, 'src>),
EndElement(EndTag<'src>),
Text(Text<'src>),
CData(CData<'src>),
Comment(Comment<'src>),
Pi(Pi<'src>),
EntityRef(EntityRef<'src>),
Eof,
}
pub struct EntityRef<'src> {
inner: crate::xml_bytes_reader::BytesEntityRef<'src>,
}
impl<'src> EntityRef<'src> {
#[inline]
pub fn name(&self) -> &'src str {
unsafe { std::str::from_utf8_unchecked(self.inner.name()) }
}
}
impl std::fmt::Debug for EntityRef<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EntityRef")
.field("name", &self.name())
.finish()
}
}
#[derive(Debug)]
pub enum EventInto<'src> {
StartElement { name: Cow<'src, str> },
EndElement { name: Cow<'src, str> },
Text(Cow<'src, str>),
CData(Cow<'src, str>),
Comment(Cow<'src, str>),
Pi { target: Cow<'src, str>, content: Cow<'src, str> },
EntityRef { name: Cow<'src, str> },
Eof,
}
pub struct XmlReader<'src> {
inner: XmlBytesReader<'src>,
}
impl<'src> XmlReader<'src> {
#[allow(clippy::should_implement_trait)] pub fn from_str(input: &'src str) -> Self {
Self { inner: XmlBytesReader::from_str(input) }
}
pub fn from_bytes(src: &'src [u8]) -> Result<Self> {
XmlBytesReader::from_bytes(src).map(|inner| Self { inner })
}
pub unsafe fn from_bytes_unchecked(src: &'src [u8]) -> Self {
Self { inner: unsafe { XmlBytesReader::from_bytes_unchecked(src) } }
}
pub unsafe fn from_bytes_in_place_unchecked(src: &'src mut [u8]) -> Self {
Self { inner: unsafe { XmlBytesReader::from_bytes_in_place_unchecked(src) } }
}
pub fn with_options(self, opts: ParseOptions) -> Self {
Self { inner: self.inner.with_options(opts) }
}
pub fn xml_decl(&self) -> Option<&XmlDeclInfo> {
self.inner.xml_decl()
}
pub fn recovered_errors(&self) -> &[crate::error::XmlError] {
self.inner.recovered_errors()
}
#[inline]
pub fn src_offset(&self) -> usize {
self.inner.src_offset()
}
#[inline]
pub fn last_start_offset(&self) -> Option<usize> {
self.inner.last_start_offset()
}
#[inline]
pub fn src_bytes(&self) -> &'src [u8] {
self.inner.src_bytes()
}
#[inline]
pub fn line_col(&self) -> (u32, u32) {
crate::scanner::compute_line_col(self.src_bytes(), self.src_offset())
}
#[inline]
pub fn line_col_at(&self, offset: usize) -> (u32, u32) {
crate::scanner::compute_line_col(self.src_bytes(), offset)
}
#[allow(clippy::should_implement_trait)] pub fn next(&mut self) -> Result<Event<'_, 'src>> {
match self.inner.next()? {
BytesEvent::StartElement(t) => Ok(Event::StartElement(StartTag { inner: t })),
BytesEvent::EndElement(t) => Ok(Event::EndElement(EndTag { inner: t })),
BytesEvent::Text(t) => Ok(Event::Text(Text { inner: t })),
BytesEvent::CData(s) => Ok(Event::CData(CData { inner: s })),
BytesEvent::Comment(s) => Ok(Event::Comment(Comment { inner: s })),
BytesEvent::Pi(p) => Ok(Event::Pi(Pi { inner: p })),
BytesEvent::EntityRef(e) => Ok(Event::EntityRef(EntityRef { inner: e })),
BytesEvent::Eof => Ok(Event::Eof),
}
}
pub fn next_into(&mut self, buf: &mut Vec<Attr<'src>>) -> Result<EventInto<'src>> {
buf.clear();
match self.next()? {
Event::StartElement(tag) => {
let name = tag.name_cow();
for attr in tag.attrs() { buf.push(attr?); }
Ok(EventInto::StartElement { name })
}
Event::EndElement(tag) => Ok(EventInto::EndElement {
name: Cow::Borrowed(tag.name()),
}),
Event::Text(t) => Ok(EventInto::Text(t.into_str())),
Event::CData(s) => Ok(EventInto::CData(s.into_str())),
Event::Comment(s) => Ok(EventInto::Comment(s.into_str())),
Event::Pi(p) => {
let (target, content) = p.into_parts();
Ok(EventInto::Pi { target, content })
}
Event::EntityRef(e) => Ok(EventInto::EntityRef {
name: Cow::Borrowed(e.name()),
}),
Event::Eof => Ok(EventInto::Eof),
}
}
}
#[inline]
fn cow_bytes_to_str(c: Cow<'_, [u8]>) -> Cow<'_, str> {
match c {
Cow::Borrowed(b) => Cow::Borrowed(unsafe { std::str::from_utf8_unchecked(b) }),
Cow::Owned(v) => Cow::Owned(unsafe { String::from_utf8_unchecked(v) }),
}
}
pub fn unescape(s: &str) -> Cow<'_, str> {
if memchr(b'&', s.as_bytes()).is_none() {
return Cow::Borrowed(s);
}
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] != b'&' {
out.push(bytes[i] as char);
i += 1;
continue;
}
let rest = &bytes[i + 1..];
let semi = match memchr(b';', rest) {
Some(n) if n <= 16 => n,
_ => { out.push('&'); i += 1; continue; }
};
let name = &rest[..semi];
match name {
b"amp" => { out.push('&'); }
b"lt" => { out.push('<'); }
b"gt" => { out.push('>'); }
b"quot" => { out.push('"'); }
b"apos" => { out.push('\''); }
_ if name.starts_with(b"#") => {
let cp: Option<u32> = if name.len() >= 2 && (name[1] == b'x' || name[1] == b'X') {
std::str::from_utf8(&name[2..]).ok()
.and_then(|h| u32::from_str_radix(h, 16).ok())
} else {
std::str::from_utf8(&name[1..]).ok()
.and_then(|d| d.parse::<u32>().ok())
};
match cp.and_then(char::from_u32) {
Some(c) => out.push(c),
None => {
out.push('&');
out.push_str(unsafe { std::str::from_utf8_unchecked(name) });
out.push(';');
}
}
}
_ => {
out.push('&');
out.push_str(unsafe { std::str::from_utf8_unchecked(name) });
out.push(';');
}
}
i += 1 + semi + 1;
}
Cow::Owned(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn events(src: &str) -> Vec<String> {
let mut r = XmlReader::from_str(src);
let mut out = Vec::new();
let mut buf = Vec::new();
loop {
match r.next_into(&mut buf).unwrap() {
EventInto::StartElement { name } => {
let a: Vec<_> = buf.iter().map(|a| format!("{}={}", a.name, a.value)).collect();
if a.is_empty() { out.push(format!("<{name}>")); }
else { out.push(format!("<{name} {}>", a.join(" "))); }
}
EventInto::EndElement { name } => out.push(format!("</{name}>")),
EventInto::Text(t) => out.push(format!("T:{t}")),
EventInto::CData(s) => out.push(format!("CD:{s}")),
EventInto::Comment(s) => out.push(format!("C:{s}")),
EventInto::Pi { target, .. } => out.push(format!("PI:{target}")),
EventInto::EntityRef { name } => out.push(format!("E:{name}")),
EventInto::Eof => break,
}
}
out
}
#[test]
fn minimal() {
assert_eq!(events("<r/>"), vec!["<r>", "</r>"]);
}
#[test]
fn nested() {
assert_eq!(
events("<a><b>hello</b></a>"),
vec!["<a>", "<b>", "T:hello", "</b>", "</a>"],
);
}
#[test]
fn attributes_borrowed() {
let src = r#"<el id="1" class="x"/>"#;
let mut r = XmlReader::from_str(src);
let mut buf = Vec::new();
let ev = r.next_into(&mut buf).unwrap();
assert!(matches!(&ev, EventInto::StartElement { name } if name == "el"));
assert_eq!(buf.len(), 2);
assert!(matches!(buf[0].value, Cow::Borrowed(_)), "no entity → should borrow");
}
#[test]
fn attribute_entity_owned() {
let src = r#"<el v="a&b"/>"#;
let mut r = XmlReader::from_str(src);
let mut buf = Vec::new();
r.next_into(&mut buf).unwrap();
assert_eq!(buf[0].value.as_ref(), "a&b");
assert!(matches!(buf[0].value, Cow::Owned(_)), "entity → must allocate");
}
#[test]
fn text_borrowed() {
let src = "<r>hello world</r>";
let mut r = XmlReader::from_str(src);
let mut buf = Vec::new();
r.next_into(&mut buf).unwrap(); let ev = r.next_into(&mut buf).unwrap();
assert!(matches!(ev, EventInto::Text(Cow::Borrowed("hello world"))));
}
#[test]
fn cdata_borrowed() {
let src = "<r><![CDATA[raw <data>]]></r>";
assert_eq!(events(src), vec!["<r>", "CD:raw <data>", "</r>"]);
}
#[test]
fn empty_element_emits_both_events() {
assert_eq!(events("<root><br/></root>"), vec!["<root>", "<br>", "</br>", "</root>"]);
}
#[test]
fn buffer_reuse() {
let src = "<a x='1'/><b y='2'/>";
let src = format!("<root>{src}</root>");
let mut r = XmlReader::from_str(&src);
let mut buf: Vec<Attr> = Vec::new();
let cap_before;
loop {
match r.next_into(&mut buf).unwrap() {
EventInto::StartElement { name } if name == "a" => {
cap_before = buf.capacity();
break;
}
EventInto::Eof => panic!("unexpected EOF"),
_ => {}
}
}
loop {
match r.next_into(&mut buf).unwrap() {
EventInto::StartElement { name } if name == "b" => {
assert_eq!(buf.capacity(), cap_before, "capacity should not grow for same-size attrs");
break;
}
EventInto::Eof => panic!("unexpected EOF"),
_ => {}
}
}
}
#[test]
fn lazy_attrs_iter() {
let src = r#"<el id="1" class="x"/>"#;
let mut r = XmlReader::from_str(src);
match r.next().unwrap() {
Event::StartElement(tag) => {
assert_eq!(tag.name(), "el");
let pairs: Vec<(String, String)> = tag.attrs()
.map(|a| a.map(|a| (a.name.to_owned(), a.value.into_owned())).unwrap())
.collect();
assert_eq!(pairs, vec![("id".into(), "1".into()), ("class".into(), "x".into())]);
}
_ => panic!("expected StartElement"),
}
}
#[test]
fn lazy_attrs_skipped_costs_nothing() {
let src = r#"<el id="1" class="x"/>"#;
let mut r = XmlReader::from_str(src);
match r.next().unwrap() {
Event::StartElement(tag) => assert_eq!(tag.name(), "el"),
_ => panic!(),
}
match r.next().unwrap() {
Event::EndElement(tag) => assert_eq!(tag.name(), "el"),
_ => panic!(),
}
match r.next().unwrap() {
Event::Eof => {}
_ => panic!(),
}
}
#[test]
fn debug_impl_shows_contents() {
let mut r = XmlReader::from_str("<a x='1'>hi<!-- c --><![CDATA[cd]]><?p y?></a>");
let mut seen: Vec<String> = Vec::new();
loop {
match r.next().unwrap() {
Event::Eof => break,
ev => seen.push(format!("{ev:?}")),
}
}
let combined = seen.join("\n");
assert!(combined.contains("StartTag"), "Debug missing StartTag — {combined}");
assert!(combined.contains("\"a\""), "Debug should show element name — {combined}");
assert!(combined.contains("Comment(\" c \")"), "Debug for Comment — {combined}");
assert!(combined.contains("CData(\"cd\")"), "Debug for CData — {combined}");
assert!(combined.contains("EndTag"), "Debug missing EndTag — {combined}");
}
#[test]
fn text_method_access() {
let src = "<r>hello</r>";
let mut r = XmlReader::from_str(src);
let _ = r.next().unwrap(); match r.next().unwrap() {
Event::Text(t) => {
assert_eq!(t.as_str(), "hello");
let owned = t.into_str();
assert_eq!(owned, "hello");
assert!(matches!(owned, Cow::Borrowed("hello")));
}
_ => panic!(),
}
}
#[test]
fn attr_name_and_value_accessors() {
let src = r#"<el id="1" class="x"/>"#;
let mut r = XmlReader::from_str(src);
match r.next().unwrap() {
Event::StartElement(tag) => {
let attrs: Vec<_> = tag.attrs().map(|a| a.unwrap()).collect();
assert_eq!(attrs[0].name(), "id");
assert_eq!(attrs[0].value(), "1");
assert_eq!(attrs[1].name(), "class");
assert_eq!(attrs[1].value(), "x");
}
_ => panic!(),
}
}
#[test]
fn from_bytes_valid_utf8() {
let src = b"<r/>";
let mut r = XmlReader::from_bytes(src).expect("valid utf-8");
match r.next().unwrap() {
Event::StartElement(tag) => assert_eq!(tag.name(), "r"),
_ => panic!(),
}
}
#[test]
fn from_bytes_invalid_utf8_errors() {
let bad = &[b'<', 0xFF, 0xFE, b'>'];
assert!(XmlReader::from_bytes(bad).is_err());
}
#[test]
fn from_bytes_unchecked_skips_validation() {
let src = b"<r/>";
let mut r = unsafe { XmlReader::from_bytes_unchecked(src) };
match r.next().unwrap() {
Event::StartElement(tag) => assert_eq!(tag.name(), "r"),
_ => panic!(),
}
}
#[test]
fn from_bytes_in_place_unchecked() {
let mut src = b"<r/>".to_vec();
let mut r = unsafe { XmlReader::from_bytes_in_place_unchecked(&mut src) };
match r.next().unwrap() {
Event::StartElement(tag) => assert_eq!(tag.name(), "r"),
_ => panic!(),
}
drop(r);
}
#[test]
fn with_options_changes_behavior() {
let opts = ParseOptions { skip_entity_expansion: true, ..ParseOptions::default() };
let mut r = XmlReader::from_str("<r>&</r>").with_options(opts);
let _ = r.next().unwrap(); match r.next().unwrap() {
Event::Text(t) => assert_eq!(t.as_str(), "&"),
ev => panic!("expected Text, got {ev:?}"),
}
}
#[test]
fn xml_decl_returns_some_after_reading_prolog() {
let mut r = XmlReader::from_str(r#"<?xml version="1.0" encoding="UTF-8"?><r/>"#);
let _ = r.next().unwrap(); let decl = r.xml_decl().expect("xml decl");
assert_eq!(decl.version, "1.0");
}
#[test]
fn xml_decl_returns_none_when_absent() {
let mut r = XmlReader::from_str("<r/>");
let _ = r.next().unwrap();
assert!(r.xml_decl().is_none());
}
#[test]
fn recovered_errors_empty_for_well_formed_input() {
let mut r = XmlReader::from_str("<r/>");
let _ = r.next().unwrap();
let _ = r.next().unwrap();
assert!(r.recovered_errors().is_empty());
}
#[test]
fn entity_ref_event_via_next() {
let src = r#"<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY foo "bar">]>
<r>&foo;</r>"#;
let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
let mut r = XmlReader::from_str(src).with_options(opts);
let mut found = false;
loop {
match r.next().unwrap() {
Event::EntityRef(e) => {
assert_eq!(e.name(), "foo");
let s = format!("{e:?}");
assert!(s.contains("foo"), "got {s}");
found = true;
break;
}
Event::Eof => break,
_ => continue,
}
}
assert!(found, "EntityRef event was not emitted");
}
#[test]
fn entity_ref_event_via_next_into() {
let src = r#"<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY bar "x">]>
<r>&bar;</r>"#;
let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
let mut r = XmlReader::from_str(src).with_options(opts);
let mut buf = Vec::new();
let mut found = false;
loop {
match r.next_into(&mut buf).unwrap() {
EventInto::EntityRef { name } => {
assert_eq!(name, "bar");
found = true;
break;
}
EventInto::Eof => break,
_ => continue,
}
}
assert!(found, "EntityRef event was not emitted");
}
#[test]
fn unescape_no_amp_returns_borrowed() {
let s = "no entities here";
let out = unescape(s);
assert!(matches!(out, Cow::Borrowed(_)));
assert_eq!(out, "no entities here");
}
#[test]
fn unescape_predefined_entities() {
assert_eq!(unescape("&").as_ref(), "&");
assert_eq!(unescape("<").as_ref(), "<");
assert_eq!(unescape(">").as_ref(), ">");
assert_eq!(unescape(""").as_ref(), "\"");
assert_eq!(unescape("'").as_ref(), "'");
}
#[test]
fn unescape_mixed_text() {
assert_eq!(
unescape("a & b < c > d").as_ref(),
"a & b < c > d",
);
}
#[test]
fn unescape_numeric_decimal_char_ref() {
assert_eq!(unescape("A").as_ref(), "A");
assert_eq!(unescape("€").as_ref(), "€");
}
#[test]
fn unescape_numeric_hex_char_ref() {
assert_eq!(unescape("A").as_ref(), "A");
assert_eq!(unescape("&#xX41;").as_ref(), "&#xX41;"); assert_eq!(unescape("A").as_ref(), "A");
}
#[test]
fn unescape_invalid_char_ref_passes_through() {
assert_eq!(unescape("&#abc;").as_ref(), "&#abc;");
assert_eq!(unescape("�").as_ref(), "�");
}
#[test]
fn unescape_unknown_named_entity_passes_through() {
assert_eq!(unescape("&bogus;").as_ref(), "&bogus;");
}
#[test]
fn unescape_ampersand_without_semicolon() {
assert_eq!(unescape("&just an ampersand").as_ref(), "&just an ampersand");
assert_eq!(unescape("&very_long_name_that_exceeds_sixteen_chars_threshold;").as_ref(),
"&very_long_name_that_exceeds_sixteen_chars_threshold;");
}
}