use crate::encoding;
use crate::error::{ErrorDomain, ErrorLevel, Result, XmlError};
use crate::ns_helpers::{
ns_err, validate_qname, validate_xmlns_decl, XML_NS_URI, XMLNS_NS_URI,
};
use crate::options::ParseOptions;
use crate::xml_bytes_reader::{BytesAttr, BytesEvent, XmlBytesReader};
use rustc_hash::FxHashSet;
use sup_xml_tree::dom::{Attribute, Document, DocumentBuilder, Namespace, Node};
pub fn parse_str(input: &str, opts: &ParseOptions) -> Result<Document> {
let source: Box<[u8]> = input.as_bytes().to_vec().into_boxed_slice();
parse_owned_bytes(source, opts)
}
pub fn parse_str_with_recovered(
input: &str,
opts: &ParseOptions,
) -> (Result<Document>, Vec<XmlError>) {
let source: Box<[u8]> = input.as_bytes().to_vec().into_boxed_slice();
parse_owned_bytes_with_recovered(source, opts)
}
pub fn parse_bytes(input: &[u8], opts: &ParseOptions) -> Result<Document> {
let source = transcode_and_validate(input, opts)?;
parse_owned_bytes(source, opts)
}
pub fn parse_bytes_with_dtd(
input: &[u8],
opts: &ParseOptions,
) -> Result<(Document, crate::dtd::Dtd)> {
let source = transcode_and_validate(input, opts)?;
let b = DocumentBuilder::new();
b.set_source(source);
let src_bytes: &[u8] = b.source().expect("source just set");
let mut reader = unsafe { XmlBytesReader::from_bytes_unchecked(src_bytes) }
.with_options(opts.clone());
drive(&b, &mut reader, opts)?;
let dtd = reader.take_dtd();
Ok((b.build(), dtd))
}
pub fn parse_external_subset(
input: &[u8],
opts: &ParseOptions,
) -> Result<crate::dtd::Dtd> {
let bytes = transcode_and_validate(input, opts)?;
let text = unsafe { String::from_utf8_unchecked(bytes.into_vec()) };
let mut reader = unsafe { XmlBytesReader::from_bytes_unchecked(&[]) }
.with_options(opts.clone());
reader.parse_standalone_external_subset(text)?;
Ok(reader.take_dtd())
}
#[cfg(feature = "c-abi")]
pub unsafe fn parse_bytes_with_dtd_and_dict(
input: &[u8],
opts: &ParseOptions,
dict: *mut sup_xml_tree::dict::Dict,
) -> Result<(Document, crate::dtd::Dtd)> {
let source = transcode_and_validate(input, opts)?;
let b = unsafe { DocumentBuilder::new_with_dict(dict) };
b.set_source(source);
let src_bytes: &[u8] = b.source().expect("source just set");
let mut reader = unsafe { XmlBytesReader::from_bytes_unchecked(src_bytes) }
.with_options(opts.clone());
drive(&b, &mut reader, opts)?;
let dtd = reader.take_dtd();
Ok((b.build(), dtd))
}
#[cfg(feature = "c-abi")]
pub unsafe fn parse_bytes_with_dtd_dict_arena(
input: &[u8],
opts: &ParseOptions,
dict: *mut sup_xml_tree::dict::Dict,
arena: std::sync::Arc<bumpalo::Bump>,
) -> Result<(Document, crate::dtd::Dtd)> {
let source = transcode_and_validate(input, opts)?;
let b = unsafe { DocumentBuilder::new_with_dict_and_arena(dict, arena) };
b.set_source(source);
let src_bytes: &[u8] = b.source().expect("source just set");
let mut reader = unsafe { XmlBytesReader::from_bytes_unchecked(src_bytes) }
.with_options(opts.clone());
drive(&b, &mut reader, opts)?;
let dtd = reader.take_dtd();
Ok((b.build(), dtd))
}
pub fn parse_bytes_with_recovered(
input: &[u8],
opts: &ParseOptions,
) -> (Result<Document>, Vec<XmlError>) {
let source = match transcode_and_validate(input, opts) {
Ok(s) => s,
Err(e) => return (Err(e), Vec::new()),
};
parse_owned_bytes_with_recovered(source, opts)
}
pub unsafe fn parse_bytes_unchecked(input: &[u8], opts: &ParseOptions) -> Result<Document> {
let source: Box<[u8]> = input.to_vec().into_boxed_slice();
parse_owned_bytes(source, opts)
}
pub fn parse_bytes_in_place(buf: Vec<u8>, opts: &ParseOptions) -> Result<Document> {
if opts.recovery_mode {
return Err(XmlError::new(
ErrorDomain::Parser,
ErrorLevel::Fatal,
"parse_bytes_in_place does not support ParseOptions::recovery_mode \
— destructive parsing can't unwind after a partial mutation. \
Use parse_bytes if you need recovery."
.to_string(),
));
}
let source: Box<[u8]> = if let Some(enc) = opts.forced_encoding.clone() {
encoding::transcode_to_utf8_as(&buf, enc)?
.into_owned()
.into_boxed_slice()
} else if opts.auto_transcode {
encoding::transcode_to_utf8(&buf)?
.into_owned()
.into_boxed_slice()
} else {
simdutf8::compat::from_utf8(&buf).map_err(|e| {
XmlError::new(
ErrorDomain::Encoding,
ErrorLevel::Fatal,
format!("invalid UTF-8: {e}"),
)
})?;
buf.into_boxed_slice()
};
parse_owned_bytes_inplace(source, opts)
}
fn parse_owned_bytes_inplace(source: Box<[u8]>, opts: &ParseOptions) -> Result<Document> {
let b = DocumentBuilder::new();
b.set_source(source);
let src_bytes: &mut [u8] = {
let (ptr, len) = (b.source_ptr_for_inplace(), b.source_len_for_inplace());
unsafe { std::slice::from_raw_parts_mut(ptr, len) }
};
let mut reader = unsafe { XmlBytesReader::from_bytes_in_place_unchecked(src_bytes) }
.with_options(opts.clone());
drive(&b, &mut reader, opts)?;
Ok(b.build())
}
fn transcode_and_validate(input: &[u8], opts: &ParseOptions) -> Result<Box<[u8]>> {
let owned: Vec<u8> = if let Some(enc) = opts.forced_encoding.clone() {
encoding::transcode_to_utf8_as(input, enc)?.into_owned()
} else if opts.auto_transcode {
encoding::transcode_to_utf8(input)?.into_owned()
} else {
input.to_vec()
};
simdutf8::compat::from_utf8(&owned).map_err(|e| {
let off = e.valid_up_to();
let (line, col) = crate::scanner::compute_line_col(&owned, off);
XmlError::new(ErrorDomain::Encoding, ErrorLevel::Fatal, format!("invalid UTF-8: {e}"))
.at("<input>", line, col, off as u64)
})?;
Ok(owned.into_boxed_slice())
}
fn parse_owned_bytes(source: Box<[u8]>, opts: &ParseOptions) -> Result<Document> {
let b = DocumentBuilder::new();
b.set_source(source);
let src_bytes: &[u8] = b.source().expect("source just set");
let mut reader = unsafe { XmlBytesReader::from_bytes_unchecked(src_bytes) }
.with_options(opts.clone());
drive(&b, &mut reader, opts)?;
let dtd = reader.take_dtd();
let mut doc = b.build();
if !dtd.unparsed_entities.is_empty() {
doc.set_unparsed_entities(dtd.unparsed_entities.clone());
}
if !dtd.is_empty() {
let _ = crate::dtd::inject::inject_defaults(&doc, &dtd);
let id_map = crate::dtd::collect_id_attrs(&dtd);
if !id_map.is_empty() {
doc.set_id_attributes(id_map);
}
let idref_map = crate::dtd::collect_idref_attrs(&dtd);
if !idref_map.is_empty() {
doc.set_idref_attributes(idref_map);
}
}
Ok(doc)
}
fn parse_owned_bytes_with_recovered(
source: Box<[u8]>,
opts: &ParseOptions,
) -> (Result<Document>, Vec<XmlError>) {
let b = DocumentBuilder::new();
b.set_source(source);
let src_bytes: &[u8] = b.source().expect("source just set");
let mut reader = unsafe { XmlBytesReader::from_bytes_unchecked(src_bytes) }
.with_options(opts.clone());
let drive_result = drive(&b, &mut reader, opts);
let recovered = reader.recovered_errors().to_vec();
let unparsed = reader.take_dtd().unparsed_entities;
let result = drive_result.map(|()| {
let mut d = b.build();
if !unparsed.is_empty() { d.set_unparsed_entities(unparsed); }
d
});
(result, recovered)
}
pub fn parse_ns_str(input: &str) -> Result<Document> {
let opts = ParseOptions { namespace_aware: true, ..ParseOptions::default() };
parse_str(input, &opts)
}
pub fn parse_ns_bytes(input: &[u8]) -> Result<Document> {
let opts = ParseOptions { namespace_aware: true, ..ParseOptions::default() };
parse_bytes(input, &opts)
}
type ErasedNodePtr = *const ();
type ErasedAttrPtr = *const ();
#[inline] fn erase(node: &Node<'_>) -> ErasedNodePtr {
node as *const Node<'_> as *const ()
}
#[inline] unsafe fn unerase<'a>(p: ErasedNodePtr) -> &'a Node<'a> {
unsafe { &*(p as *const Node<'a>) }
}
fn drive(
b: &DocumentBuilder,
reader: &mut XmlBytesReader<'_>,
opts: &ParseOptions,
) -> Result<()> {
crate::license_gate::ensure_licensed()?;
let mut stack: Vec<ErasedNodePtr> = Vec::with_capacity(16);
let mut attr_buf: Vec<BytesAttr<'_>> = Vec::with_capacity(16);
let mut root: Option<ErasedNodePtr> = None;
let mut line_cursor: (usize, u32) = (0, 1);
if opts.base_url.is_some() {
b.set_base_url(opts.base_url.clone());
}
let ns_aware = opts.namespace_aware;
let mut ns_bindings: Vec<(Option<&str>, *const ())> = Vec::new();
let mut ns_frames: Vec<usize> = Vec::new();
let mut ns_frame_count_stack: Vec<u32> = Vec::new();
let mut active_user_bindings: u32 = 0;
if ns_aware {
let xml_ns = b.new_namespace(Some("xml"), XML_NS_URI);
let xmlns_ns = b.new_namespace(Some("xmlns"), XMLNS_NS_URI);
ns_bindings.push((Some("xml"), erase_ns(xml_ns)));
ns_bindings.push((Some("xmlns"), erase_ns(xmlns_ns)));
}
#[inline]
fn alloc_cow_bytes_as_str<'b>(
b: &'b DocumentBuilder,
c: std::borrow::Cow<'_, [u8]>,
) -> &'b str {
match c {
std::borrow::Cow::Borrowed(bytes) => {
let s: &str = unsafe { std::str::from_utf8_unchecked(bytes) };
let extended: &'b str = unsafe { &*(s as *const str) };
unsafe { b.alloc_str_borrow(extended) }
}
std::borrow::Cow::Owned(v) => {
let s: &str = unsafe { std::str::from_utf8_unchecked(&v) };
b.alloc_str(s)
}
}
}
#[inline]
fn alloc_name_bytes_as_str<'b>(b: &'b DocumentBuilder, bytes: &[u8]) -> &'b str {
let s: &str = unsafe { std::str::from_utf8_unchecked(bytes) };
let extended: &'b str = unsafe { &*(s as *const str) };
unsafe { b.alloc_str_borrow(extended) }
}
fn dtd_att_type<'d>(
dtd: &'d crate::dtd::Dtd,
elem_name: &str,
attr_name: &str,
) -> Option<&'d crate::dtd::AttType> {
let attlist = dtd.attlists.get(elem_name)?;
attlist.iter().find(|d| d.name == attr_name).map(|d| &d.att_type)
}
fn dtd_normalize_attr_value<'b>(
b: &'b DocumentBuilder,
dtd: &crate::dtd::Dtd,
elem_name: &str,
attr_name: &str,
raw_value: &'b str,
) -> &'b str {
use crate::dtd::AttType;
let is_xml_id = attr_name == "xml:id";
let needs_non_cdata = is_xml_id || {
let att_type = dtd_att_type(dtd, elem_name, attr_name);
matches!(
att_type,
Some(AttType::Id | AttType::IdRef | AttType::IdRefs
| AttType::Entity | AttType::Entities
| AttType::Nmtoken | AttType::Nmtokens
| AttType::Notation(_) | AttType::Enumeration(_))
)
};
if !needs_non_cdata { return raw_value; }
let normalized = normalize_non_cdata(raw_value);
b.alloc_str(&normalized)
}
let src_bytes: &[u8] = reader.src_bytes();
fn normalize_non_cdata(value: &str) -> String {
let bytes = value.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
let mut in_run = true; for &b in bytes {
if b == b' ' {
if !in_run {
out.push(b' ');
in_run = true;
}
} else {
out.push(b);
in_run = false;
}
}
if out.last() == Some(&b' ') { out.pop(); }
unsafe { String::from_utf8_unchecked(out) }
}
loop {
debug_assert!(attr_buf.is_empty());
match reader.next()? {
BytesEvent::StartElement(tag) => {
let name: &str = match tag.name_cow() {
std::borrow::Cow::Borrowed(bytes) => {
alloc_name_bytes_as_str(b, bytes)
}
std::borrow::Cow::Owned(bytes) => {
b.alloc_str(unsafe { std::str::from_utf8_unchecked(&bytes) })
}
};
#[cfg(feature = "c-abi")]
let elem_name = if ns_aware {
match memchr::memchr(b':', name.as_bytes()) {
Some(idx) => &name[idx + 1..],
None => name,
}
} else {
name
};
#[cfg(not(feature = "c-abi"))]
let elem_name = name;
let el = b.new_element(elem_name);
{
let name_offset = (tag.name_offset() as usize).min(src_bytes.len());
if name_offset > line_cursor.0 {
let slice = &src_bytes[line_cursor.0..name_offset];
line_cursor.1 += memchr::memchr_iter(b'\n', slice).count() as u32;
line_cursor.0 = name_offset;
}
let raw_line = line_cursor.1;
#[cfg(feature = "c-abi")]
el.line.set(raw_line.min(u16::MAX as u32) as u16);
#[cfg(not(feature = "c-abi"))]
{
el.line = raw_line;
}
#[cfg(feature = "c-abi")]
{
el.full_line.set(raw_line);
}
el.source_offset = name_offset.min(u32::MAX as usize) as u32;
}
let entity_attr_pairs: Option<Vec<(Vec<u8>, Vec<u8>)>> =
tag.entity_attrs().map(|v| v.to_vec());
if !tag.attrs_bytes().is_empty() {
for a in tag.attrs() { attr_buf.push(a?); }
}
if let Some(pairs) = entity_attr_pairs {
for (n_bytes, v_bytes) in pairs {
let n_arena: &str = b.alloc_str(unsafe {
std::str::from_utf8_unchecked(&n_bytes)
});
let v_arena: &str = b.alloc_str(unsafe {
std::str::from_utf8_unchecked(&v_bytes)
});
let n_slice: &[u8] = n_arena.as_bytes();
let v_slice: &[u8] = v_arena.as_bytes();
let n_ext: &[u8] = unsafe { &*(n_slice as *const [u8]) };
let v_ext: &[u8] = unsafe { &*(v_slice as *const [u8]) };
attr_buf.push(BytesAttr {
name: n_ext,
value: std::borrow::Cow::Borrowed(v_ext),
});
}
}
if !ns_aware {
for a in attr_buf.drain(..) {
let aname = alloc_name_bytes_as_str(b, a.name);
let raw_avalue = alloc_cow_bytes_as_str(b, a.value);
let avalue: &str =
dtd_normalize_attr_value(b, reader.dtd(), name, aname, raw_avalue);
let attr = b.new_attribute(aname, avalue);
b.append_attribute(el, attr);
}
} else {
validate_qname(name, "element")?;
ns_frames.push(ns_bindings.len());
let mut new_bindings_this_frame = 0u32;
let mut any_attr_prefixed = false;
let mut prefixed_attrs: Vec<(&Attribute<'_>, &str)> = Vec::new();
for a in attr_buf.drain(..) {
let aname = alloc_name_bytes_as_str(b, a.name);
let raw_avalue = alloc_cow_bytes_as_str(b, a.value);
let is_xmlns_decl =
aname == "xmlns" || aname.starts_with("xmlns:");
let avalue: &str =
dtd_normalize_attr_value(b, reader.dtd(), name, aname, raw_avalue);
#[cfg(not(feature = "c-abi"))]
let always_attach = true;
#[cfg(feature = "c-abi")]
let always_attach = false;
let is_prefixed = !is_xmlns_decl
&& memchr::memchr(b':', aname.as_bytes()).is_some();
if always_attach || !is_xmlns_decl {
#[cfg(feature = "c-abi")]
let stored_name = if ns_aware && is_prefixed {
let i = memchr::memchr(b':', aname.as_bytes()).unwrap();
&aname[i + 1..]
} else {
aname
};
#[cfg(not(feature = "c-abi"))]
let stored_name = aname;
let attr: &Attribute<'_> = b.new_attribute(stored_name, avalue);
b.append_attribute(el, attr);
if ns_aware && is_prefixed {
prefixed_attrs.push((attr, aname));
}
}
if aname == "xmlns" {
let ns = b.new_namespace(None, avalue);
ns_bindings.push((None, erase_ns(ns)));
#[cfg(feature = "c-abi")]
{ b.append_ns_def(el, ns); }
new_bindings_this_frame += 1;
} else if let Some(local) = aname.strip_prefix("xmlns:") {
validate_xmlns_decl(local, avalue)?;
if local == "xml" { continue; }
let ns = b.new_namespace(Some(local), avalue);
ns_bindings.push((Some(local), erase_ns(ns)));
#[cfg(feature = "c-abi")]
{ b.append_ns_def(el, ns); }
new_bindings_this_frame += 1;
} else if memchr::memchr(b':', aname.as_bytes()).is_some() {
any_attr_prefixed = true;
}
}
active_user_bindings += new_bindings_this_frame;
let elem_has_colon = memchr::memchr(b':', name.as_bytes()).is_some();
if elem_has_colon || active_user_bindings > 0 {
let el_ns = resolve_qname(name, &ns_bindings, false)?;
el.namespace.set(el_ns);
}
if any_attr_prefixed {
for &(attr, qname) in &prefixed_attrs {
validate_qname(qname, "attribute")?;
let ns = resolve_qname(qname, &ns_bindings, true)?;
attr.namespace.set(ns);
}
let mut seen: FxHashSet<(&str, &str)> = FxHashSet::default();
let mut attr_cur = el.first_attribute.get();
while let Some(attr) = attr_cur {
if attr.name() != "xmlns" && !attr.name().starts_with("xmlns:") {
let ns_uri = attr.namespace.get().map(|n| n.href()).unwrap_or("");
let local = attr.name().rfind(':')
.map(|i| &attr.name()[i + 1..])
.unwrap_or(attr.name());
if !seen.insert((ns_uri, local)) {
return Err(ns_err(if ns_uri.is_empty() {
format!("duplicate attribute '{local}' after namespace expansion")
} else {
format!("duplicate attribute '{local}' in namespace '{ns_uri}' after namespace expansion")
}));
}
}
attr_cur = attr.next.get();
}
}
ns_frame_count_stack.push(new_bindings_this_frame);
}
if let Some(&parent_ptr) = stack.last() {
let parent: &Node<'_> = unsafe { unerase(parent_ptr) };
b.append_child(parent, el);
} else {
root = Some(erase(el));
}
stack.push(erase(el));
}
BytesEvent::EndElement(_) => {
stack.pop().expect("EndElement without StartElement — XmlBytesReader invariant");
if ns_aware {
if let Some(frame_start) = ns_frames.pop() {
ns_bindings.truncate(frame_start);
}
if let Some(count) = ns_frame_count_stack.pop() {
active_user_bindings -= count;
}
}
}
BytesEvent::Text(t) => attach_leaf(b, &stack, b.new_text (alloc_cow_bytes_as_str(b, t.into_bytes())), root.is_some()),
BytesEvent::CData(s) => {
let content = alloc_cow_bytes_as_str(b, s.into_bytes());
let leaf = if opts.cdata_as_text { b.new_text(content) } else { b.new_cdata(content) };
attach_leaf(b, &stack, leaf, root.is_some());
}
BytesEvent::Comment(s) => {
if !opts.remove_comments {
attach_leaf(b, &stack, b.new_comment(alloc_cow_bytes_as_str(b, s.into_bytes())), root.is_some());
}
}
BytesEvent::Pi(p) => {
if !opts.remove_pis {
let (t, c) = p.into_parts();
let target = alloc_cow_bytes_as_str(b, t);
let content = if c.is_empty() { None } else { Some(alloc_cow_bytes_as_str(b, c)) };
attach_leaf(b, &stack, b.new_pi(target, content), root.is_some());
}
}
BytesEvent::EntityRef(e) => {
let name_bytes = e.name();
let name: &str = unsafe { std::str::from_utf8_unchecked(name_bytes) };
let name = b.alloc_str(name);
let lit = format!("&{name};");
let content = b.alloc_str(&lit);
attach_leaf(b, &stack, b.new_entity_ref(name, content), root.is_some());
}
BytesEvent::Eof => break,
}
}
let root_ptr = root.ok_or_else(|| XmlError::new(
ErrorDomain::Parser, ErrorLevel::Fatal,
"document has no root element",
))?;
let root_ref: &Node<'_> = unsafe { unerase(root_ptr) };
b.set_root(root_ref);
if let Some(decl) = reader.xml_decl() {
b.set_version(decl.version.clone());
if let Some(enc) = &decl.encoding {
b.set_encoding(enc.clone());
}
b.set_standalone(decl.standalone);
}
Ok(())
}
#[inline] fn erase_ns(ns: &Namespace<'_>) -> *const () {
ns as *const Namespace<'_> as *const ()
}
#[inline] unsafe fn unerase_ns<'a>(p: *const ()) -> &'a Namespace<'a> {
unsafe { &*(p as *const Namespace<'a>) }
}
fn lookup_ns<'a>(
prefix: Option<&str>,
bindings: &[(Option<&'a str>, *const ())],
) -> Option<&'a Namespace<'a>> {
for &(p, ns_ptr) in bindings.iter().rev() {
if p == prefix {
return Some(unsafe { unerase_ns(ns_ptr) });
}
}
None
}
fn resolve_qname<'a>(
qname: &'a str,
bindings: &[(Option<&'a str>, *const ())],
is_attribute: bool,
) -> Result<Option<&'a Namespace<'a>>> {
if let Some(colon) = qname.find(':') {
let prefix = &qname[..colon];
match lookup_ns(Some(prefix), bindings) {
Some(ns) => Ok(Some(ns)),
None => Err(ns_err(format!("undeclared namespace prefix '{prefix}' in '{qname}'"))),
}
} else if is_attribute {
Ok(None)
} else {
match lookup_ns(None, bindings) {
Some(ns) if !ns.href().is_empty() => Ok(Some(ns)),
_ => Ok(None),
}
}
}
fn attach_leaf<'a>(
b: &'a DocumentBuilder,
stack: &[ErasedNodePtr],
node: &'a Node<'a>,
after_root: bool,
) {
if let Some(&parent_ptr) = stack.last() {
let parent: &'a Node<'a> = unsafe { unerase(parent_ptr) };
b.append_child(parent, node);
} else if after_root {
b.attach_epilogue_orphan(node);
} else {
b.attach_prolog_orphan(node);
}
}
#[allow(dead_code)] type _AttrAlias = ErasedAttrPtr;
#[cfg(test)]
mod tests {
use super::*;
use sup_xml_tree::dom::NodeKind;
fn parse(xml: &str) -> Document {
parse_str(xml, &ParseOptions::default()).expect("parse")
}
fn parse_ns(xml: &str) -> Document {
let opts = ParseOptions { namespace_aware: true, ..ParseOptions::default() };
parse_str(xml, &opts).expect("parse")
}
#[test]
fn empty_element() {
let doc = parse("<r/>");
assert_eq!(doc.root().name(), "r");
assert!(doc.root().children().next().is_none());
}
#[test]
fn element_records_source_offset_for_line_col_derivation() {
let xml = "<a>\n <b/></a>";
let doc = parse(xml);
let b = doc.root().children()
.find(|n| n.is_element() && n.name() == "b")
.expect("found <b>");
assert_eq!(b.source_offset, 7, "byte offset of `b` name");
let (line, col) = crate::compute_line_col(xml.as_bytes(), b.source_offset as usize);
assert_eq!((line, col), (2, 4), "derived line/col");
}
#[test]
fn xmlns_nmtoken_value_is_stripped_before_binding() {
let xml = r#"<?xml version="1.0"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY>
<!ATTLIST foo xmlns:a CDATA #IMPLIED
xmlns:b NMTOKEN #IMPLIED>
]>
<foo xmlns:a="urn:x" xmlns:b=" urn:x "/>"#;
let doc = parse_ns(xml);
let root = doc.root();
let nsdefs: Vec<(Option<&str>, &str)> = {
#[cfg(feature = "c-abi")]
{ root.ns_declarations().collect() }
#[cfg(not(feature = "c-abi"))]
{
let mut out = Vec::new();
let mut a = root.first_attribute.get();
while let Some(attr) = a {
let name = attr.name();
if name == "xmlns" {
out.push((None, attr.value()));
} else if let Some(p) = name.strip_prefix("xmlns:") {
out.push((Some(p), attr.value()));
}
a = attr.next.get();
}
out
}
};
let a_uri = nsdefs.iter().find(|(p, _)| *p == Some("a")).map(|(_, u)| *u);
let b_uri = nsdefs.iter().find(|(p, _)| *p == Some("b")).map(|(_, u)| *u);
assert_eq!(a_uri, Some("urn:x"));
assert_eq!(b_uri, Some("urn:x"),
"NMTOKEN normalization should strip whitespace; got {b_uri:?}");
}
#[test]
fn xmlns_cdata_value_is_not_stripped() {
let xml = r#"<r xmlns:b=" urn:x "/>"#;
let doc = parse_ns(xml);
let root = doc.root();
let b_uri = {
#[cfg(feature = "c-abi")]
{ root.ns_declarations().find(|(p, _)| *p == Some("b")).map(|(_, u)| u) }
#[cfg(not(feature = "c-abi"))]
{
let mut a = root.first_attribute.get();
let mut found = None;
while let Some(attr) = a {
if attr.name() == "xmlns:b" { found = Some(attr.value()); break; }
a = attr.next.get();
}
found
}
};
assert_eq!(b_uri, Some(" urn:x "),
"no ATTLIST → no non-CDATA normalization, URI must stay verbatim");
}
#[test]
fn duplicate_expanded_attribute_name_rejected() {
let xml = r#"<r xmlns:a="urn:x" xmlns:b="urn:x" a:attr="1" b:attr="2"/>"#;
let err = parse_str(xml, &ParseOptions { namespace_aware: true, ..ParseOptions::default() })
.expect_err("two prefixes binding the same URI with same local must be rejected");
let msg = err.to_string().to_lowercase();
assert!(msg.contains("duplicate") && (msg.contains("namespace") || msg.contains("attribute")),
"expected duplicate-attribute message, got: {err}");
}
#[test]
fn duplicate_expanded_attribute_name_rejected_across_elements() {
let xml = r#"<f xmlns:a="urn:x" xmlns:b="urn:x"><g a:attr="1" b:attr="2"/></f>"#;
let err = parse_str(xml, &ParseOptions { namespace_aware: true, ..ParseOptions::default() })
.expect_err("inner-element collision must be caught through inherited bindings");
let msg = err.to_string().to_lowercase();
assert!(msg.contains("duplicate"),
"expected duplicate-attribute message, got: {err}");
}
#[test]
fn nmtoken_normalization_unlocks_ns_uniqueness_collision() {
let xml = r#"<?xml version="1.0"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY>
<!ATTLIST foo xmlns:a CDATA #IMPLIED
xmlns:b NMTOKEN #IMPLIED>
<!ELEMENT bar ANY>
<!ATTLIST bar a:attr CDATA #IMPLIED
b:attr CDATA #IMPLIED>
]>
<foo xmlns:a="urn:x" xmlns:b=" urn:x ">
<bar a:attr="1" b:attr="2"/>
</foo>"#;
let err = parse_str(xml, &ParseOptions { namespace_aware: true, ..ParseOptions::default() })
.expect_err("rmt-ns10-012 minimum repro must be rejected once NMTOKEN normalization \
strips the whitespace from xmlns:b's value");
let msg = err.to_string().to_lowercase();
assert!(msg.contains("duplicate"),
"expected duplicate-after-expansion error, got: {err}");
}
#[test]
fn nmtoken_attribute_value_is_normalized() {
let xml = r#"<?xml version="1.0"?>
<!DOCTYPE r [
<!ELEMENT r EMPTY>
<!ATTLIST r kind NMTOKEN #IMPLIED>
]>
<r kind=" alpha "/>"#;
let doc = parse_str(xml, &ParseOptions::default()).expect("parse");
let kind = doc.root().attributes()
.find(|a| a.name() == "kind")
.map(|a| a.value());
assert_eq!(kind, Some("alpha"),
"NMTOKEN value should be stripped; got {kind:?}");
}
#[test]
fn id_attribute_value_is_normalized_collapsed() {
let xml = r#"<?xml version="1.0"?>
<!DOCTYPE r [
<!ELEMENT r EMPTY>
<!ATTLIST r tag ID #IMPLIED>
]>
<r tag=" a b c "/>"#;
let doc = parse_str(xml, &ParseOptions::default()).expect("parse");
let tag = doc.root().attributes()
.find(|a| a.name() == "tag")
.map(|a| a.value());
assert_eq!(tag, Some("a b c"),
"ID value should strip + collapse; got {tag:?}");
}
#[test]
fn cdata_attribute_value_is_not_stripped() {
let xml = r#"<r tag=" a b "/>"#;
let doc = parse_str(xml, &ParseOptions::default()).expect("parse");
let tag = doc.root().attributes()
.find(|a| a.name() == "tag")
.map(|a| a.value());
assert_eq!(tag, Some(" a b "),
"CDATA-defaulted value must stay verbatim; got {tag:?}");
}
#[test]
fn enumeration_attribute_value_is_normalized() {
let xml = r#"<?xml version="1.0"?>
<!DOCTYPE r [
<!ELEMENT r EMPTY>
<!ATTLIST r kind (yes|no) #IMPLIED>
]>
<r kind=" yes "/>"#;
let doc = parse_str(xml, &ParseOptions::default()).expect("parse");
let kind = doc.root().attributes()
.find(|a| a.name() == "kind")
.map(|a| a.value());
assert_eq!(kind, Some("yes"));
}
#[test]
fn resolve_entities_default_expands_inline() {
let xml = r#"<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY hi "hello">]>
<r>&hi;</r>"#;
let doc = parse_str(xml, &ParseOptions::default()).expect("parse");
let root = doc.root();
let kinds: Vec<NodeKind> = root.children().map(|c| c.kind).collect();
assert_eq!(kinds, vec![NodeKind::Text]);
let text = root.children().next().unwrap().content();
assert_eq!(text, "hello");
}
#[test]
fn resolve_entities_false_emits_entity_ref_node() {
let xml = r#"<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY hi "hello">]>
<r>before &hi; after</r>"#;
let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
let doc = parse_str(xml, &opts).expect("parse");
let root = doc.root();
let kinds: Vec<NodeKind> = root.children().map(|c| c.kind).collect();
assert_eq!(
kinds,
vec![NodeKind::Text, NodeKind::EntityRef, NodeKind::Text],
"expected Text-EntityRef-Text triple, got {kinds:?}"
);
let ref_node = root.children().nth(1).unwrap();
assert_eq!(ref_node.name(), "hi");
assert_eq!(ref_node.content(), "&hi;");
}
#[test]
fn resolve_entities_false_still_expands_predefined() {
let xml = r#"<r>a & b < c</r>"#;
let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
let doc = parse_str(xml, &opts).expect("parse");
let root = doc.root();
let kinds: Vec<NodeKind> = root.children().map(|c| c.kind).collect();
assert_eq!(kinds, vec![NodeKind::Text],
"predefined entities must expand inline, got {kinds:?}");
assert_eq!(root.children().next().unwrap().content(), "a & b < c");
}
#[test]
fn resolve_entities_false_still_expands_numeric() {
let xml = r#"<r>before A after</r>"#;
let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
let doc = parse_str(xml, &opts).expect("parse");
let root = doc.root();
let kinds: Vec<NodeKind> = root.children().map(|c| c.kind).collect();
assert_eq!(kinds, vec![NodeKind::Text]);
assert_eq!(root.children().next().unwrap().content(), "before A after");
}
#[test]
fn resolve_entities_false_round_trips_through_serializer() {
let xml = r#"<r>x &hi; y</r>"#;
let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
let doc = parse_str(xml, &opts).expect("parse");
let out = crate::serialize_to_string(&doc);
assert!(out.contains("&hi;"),
"EntityRef should serialize as `&hi;`, got: {out}");
}
#[test]
fn duplicate_expanded_name_accepted_when_namespace_blind() {
let xml = r#"<r xmlns:a="urn:x" xmlns:b="urn:x" a:attr="1" b:attr="2"/>"#;
parse_str(xml, &ParseOptions::default())
.expect("namespace-blind parse must accept lexically-distinct attribute names");
}
#[test]
fn line_numbers_basic() {
let doc = parse("<r>\n <a/>\n <b/>\n</r>");
let root = doc.root();
let a = root.children().find(|n| n.is_element()).unwrap();
let b = a.next_sibling.get().and_then(|n|
if n.is_element() { Some(n) } else { n.next_sibling.get() }
).unwrap();
assert_eq!(root.line_no(), 1, "root <r> on line 1");
assert_eq!(a.line_no(), 2, "<a/> on line 2");
assert_eq!(b.line_no(), 3, "<b/> on line 3");
}
#[test]
fn line_numbers_many_elements() {
let mut src = String::from("<root>\n");
let n = 300u32;
for i in 0..n {
src.push_str(&format!(" <e i=\"{i}\"/>\n"));
}
src.push_str("</root>\n");
let doc = parse(&src);
assert_eq!(doc.root().line_no(), 1);
let mut expected: u32 = 2;
let mut walked = 0u32;
for child in doc.root().children().filter(|n| n.is_element()) {
assert_eq!(child.line_no(), expected,
"child #{walked}: expected line {expected}, got {}", child.line_no());
walked += 1;
expected += 1;
}
assert_eq!(walked, n);
}
#[test]
fn nested_elements() {
let doc = parse("<a><b><c/></b></a>");
let a = doc.root();
assert_eq!(a.name(), "a");
let b = a.children().next().unwrap();
assert_eq!(b.name(), "b");
let c = b.children().next().unwrap();
assert_eq!(c.name(), "c");
assert!(std::ptr::eq(c.parent.get().unwrap(), b));
assert!(std::ptr::eq(b.parent.get().unwrap(), a));
}
#[test]
fn attributes_in_order() {
let doc = parse(r#"<el id="1" class="x" data-y="42"/>"#);
let pairs: Vec<(&str, &str)> = doc.root().attributes()
.map(|a| (a.name(), a.value()))
.collect();
assert_eq!(pairs, vec![("id", "1"), ("class", "x"), ("data-y", "42")]);
}
#[test]
fn mixed_content() {
let doc = parse("<r>before<!-- c --><b>x</b><![CDATA[<raw>]]>after</r>");
let kinds: Vec<NodeKind> = doc.root().children().map(|c| c.kind).collect();
assert_eq!(kinds, vec![
NodeKind::Text, NodeKind::Comment, NodeKind::Element,
NodeKind::CData, NodeKind::Text,
]);
}
#[test]
fn pi_inside_element() {
let doc = parse(r#"<r><?xml-stylesheet href="s.xsl"?></r>"#);
let pi = doc.root().children().next().unwrap();
assert_eq!(pi.kind, NodeKind::Pi);
assert_eq!(pi.name(), "xml-stylesheet");
assert_eq!(pi.content(), r#"href="s.xsl""#);
}
#[test]
fn entity_in_text_is_expanded() {
let doc = parse("<r>a&b<c</r>");
let t = doc.root().children().next().unwrap();
assert_eq!(t.kind, NodeKind::Text);
assert_eq!(t.content(), "a&b<c");
}
#[test]
fn entity_in_attr_is_expanded() {
let doc = parse(r#"<r v="a&b"/>"#);
let v = doc.root().attributes().next().unwrap();
assert_eq!(v.value(), "a&b");
}
#[test]
fn deeply_nested_doc() {
let mut xml = String::new();
for _ in 0..50 { xml.push_str("<n>"); }
xml.push_str("hello");
for _ in 0..50 { xml.push_str("</n>"); }
let doc = parse(&xml);
let mut cur = doc.root();
let mut depth = 1;
while let Some(c) = cur.children().next() {
if c.kind == NodeKind::Element { cur = c; depth += 1; } else { break; }
}
assert_eq!(depth, 50);
assert_eq!(cur.children().next().unwrap().content(), "hello");
}
#[test]
fn root_lifetime_keeps_tree_alive() {
let doc = parse("<r><a><b>x</b></a></r>");
let a = doc.root().children().next().unwrap();
let b = a.children().next().unwrap();
assert_eq!(b.text_content(), Some("x"));
}
#[test]
fn errors_propagate_from_reader() {
let err = parse_str("<r><a></b></r>", &ParseOptions::default());
assert!(err.is_err());
}
#[test]
fn missing_root_returns_error() {
let r = parse_str(" ", &ParseOptions::default());
assert!(r.is_err());
}
#[test]
fn no_namespaces_unchanged() {
let doc = parse_ns("<root><child/></root>");
assert!(doc.root().namespace.get().is_none());
}
#[test]
fn default_namespace_applied_to_element() {
let doc = parse_ns(r#"<root xmlns="http://example.com/"/>"#);
let ns = doc.root().namespace.get().unwrap();
assert!(ns.prefix.is_none());
assert_eq!(ns.href(), "http://example.com/");
}
#[test]
fn default_namespace_not_applied_to_attr() {
let doc = parse_ns(r#"<root xmlns="http://example.com/" id="1"/>"#);
let id_attr = doc.root().attributes().find(|a| a.name() == "id").unwrap();
assert!(id_attr.namespace.get().is_none(), "default ns must not apply to unprefixed attrs");
}
#[test]
fn prefixed_element_resolved() {
let doc = parse_ns(r#"<dc:title xmlns:dc="http://purl.org/dc/elements/1.1/">X</dc:title>"#);
let ns = doc.root().namespace.get().unwrap();
assert_eq!(ns.prefix(), Some("dc"));
assert_eq!(ns.href(), "http://purl.org/dc/elements/1.1/");
}
#[test]
fn prefixed_attribute_resolved() {
let doc = parse_ns(
r#"<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>"#
);
let nil = doc.root().attributes()
.find(|a| a.local_name() == "nil"
&& a.namespace.get().and_then(|n| n.prefix()) == Some("xsi"))
.unwrap();
assert_eq!(nil.namespace.get().unwrap().href(),
"http://www.w3.org/2001/XMLSchema-instance");
}
#[test]
fn xml_prefix_builtin() {
let doc = parse_ns(r#"<root xml:lang="en"/>"#);
let lang = doc.root().attributes()
.find(|a| a.local_name() == "lang"
&& a.namespace.get().and_then(|n| n.prefix()) == Some("xml"))
.unwrap();
assert_eq!(lang.namespace.get().unwrap().href(),
"http://www.w3.org/XML/1998/namespace");
}
#[test]
fn nested_prefix_scope_inherits_outer() {
let doc = parse_ns(r#"
<root xmlns:a="http://a.com/">
<a:child xmlns:b="http://b.com/">
<b:leaf/>
</a:child>
</root>
"#);
let root = doc.root();
assert!(root.namespace.get().is_none());
#[cfg(feature = "c-abi")]
let child_local = "child";
#[cfg(not(feature = "c-abi"))]
let child_local = "a:child";
let child = root.children().find(|c| c.is_element() && c.name() == child_local).unwrap();
assert_eq!(child.namespace.get().unwrap().href(), "http://a.com/");
let leaf = child.children().find(|c| c.is_element()).unwrap();
assert_eq!(leaf.namespace.get().unwrap().href(), "http://b.com/");
}
#[test]
fn undeclared_prefix_is_error() {
let r = parse_str("<dc:title>X</dc:title>", &ParseOptions { namespace_aware: true, ..ParseOptions::default() });
let msg = r.unwrap_err().to_string();
assert!(msg.contains("undeclared") || msg.contains("prefix"), "{msg}");
}
#[test]
fn default_namespace_override_in_child() {
let doc = parse_ns(r#"
<root xmlns="http://outer.com/">
<inner xmlns="http://inner.com/"/>
</root>
"#);
assert_eq!(doc.root().namespace.get().unwrap().href(), "http://outer.com/");
let inner = doc.root().children().find(|c| c.is_element()).unwrap();
assert_eq!(inner.namespace.get().unwrap().href(), "http://inner.com/");
}
#[test]
fn undeclare_default_namespace() {
let doc = parse_ns(r#"
<root xmlns="http://example.com/">
<child xmlns=""/>
</root>
"#);
assert!(doc.root().namespace.get().is_some());
let child = doc.root().children().find(|c| c.is_element()).unwrap();
assert!(child.namespace.get().is_none(), "xmlns='' should clear the default namespace");
}
#[test]
fn xmlns_prefix_cannot_be_declared() {
let r = parse_str(
r#"<r xmlns:xmlns="http://x.com/"/>"#,
&ParseOptions { namespace_aware: true, ..ParseOptions::default() },
);
assert!(r.is_err());
}
#[test]
fn duplicate_attribute_after_ns_expansion() {
let r = parse_str(
r#"<r xmlns:a="http://x.com/" xmlns:b="http://x.com/" a:id="1" b:id="2"/>"#,
&ParseOptions { namespace_aware: true, ..ParseOptions::default() },
);
assert!(r.is_err(), "duplicate expanded attribute name must be rejected");
}
#[test]
fn deep_nesting_pops_scope_on_close() {
let r = parse_str(r#"
<root>
<a xmlns:p="http://x.com/"><p:leaf/></a>
<p:bad/>
</root>
"#, &ParseOptions { namespace_aware: true, ..ParseOptions::default() });
assert!(r.is_err());
}
#[test]
fn xml_decl_fields_are_captured() {
let doc = parse(r#"<?xml version="1.1" encoding="ISO-8859-1" standalone="yes"?><r/>"#);
assert_eq!(doc.version, "1.1");
assert_eq!(doc.encoding, "ISO-8859-1");
assert_eq!(doc.standalone, Some(true));
}
#[test]
fn xml_decl_defaults_when_absent() {
let doc = parse("<r/>");
assert_eq!(doc.version, "1.0");
assert_eq!(doc.encoding, "");
assert_eq!(doc.standalone, None);
}
#[test]
fn xml_decl_partial_keeps_encoding_default() {
let doc = parse(r#"<?xml version="1.0"?><r/>"#);
assert_eq!(doc.version, "1.0");
assert_eq!(doc.encoding, "");
assert_eq!(doc.standalone, None);
}
#[test]
fn xml_decl_standalone_no_is_captured() {
let doc = parse(r#"<?xml version="1.0" encoding="UTF-8" standalone="no"?><r/>"#);
assert_eq!(doc.standalone, Some(false));
}
#[test]
fn many_siblings_preserve_order() {
let mut xml = String::from("<r>");
for i in 0..100 {
xml.push_str(&format!("<i>{i}</i>"));
}
xml.push_str("</r>");
let doc = parse(&xml);
let texts: Vec<&str> = doc.root().children()
.filter(|c| c.kind == NodeKind::Element)
.map(|c| c.text_content().unwrap_or(""))
.collect();
assert_eq!(texts.len(), 100);
assert_eq!(texts[0], "0");
assert_eq!(texts[99], "99");
}
#[test]
fn in_place_parses_basic_document() {
let buf = b"<root><child id=\"1\">hello</child></root>".to_vec();
let doc = parse_bytes_in_place(buf, &ParseOptions::default())
.expect("in-place parse should succeed on well-formed input");
assert_eq!(doc.root().name(), "root");
let child = doc.root().children().next().expect("child element");
assert_eq!(child.name(), "child");
assert_eq!(child.attributes().next().unwrap().value(), "1");
}
#[test]
fn in_place_rejects_recovery_mode_at_call_time() {
let buf = b"<r/>".to_vec();
let opts = ParseOptions { recovery_mode: true, ..ParseOptions::default() };
let err = parse_bytes_in_place(buf, &opts)
.expect_err("recovery_mode=true must be rejected up front");
assert!(
err.message.contains("recovery_mode"),
"error should mention recovery_mode, got: {}", err.message,
);
}
#[test]
fn in_place_transcodes_non_utf8_when_auto_transcode_on() {
let buf: Vec<u8> =
b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><r>caf\xe9</r>".to_vec();
let opts = ParseOptions { auto_transcode: true, ..ParseOptions::default() };
let doc = parse_bytes_in_place(buf, &opts).expect("transcoded parse should succeed");
let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
assert_eq!(text, "café");
}
#[test]
fn in_place_rejects_invalid_utf8_when_auto_transcode_off() {
let buf: Vec<u8> = b"<r>\xff</r>".to_vec();
let opts = ParseOptions { auto_transcode: false, ..ParseOptions::default() };
let err = parse_bytes_in_place(buf, &opts).expect_err("invalid UTF-8 must be rejected");
assert_eq!(err.domain, crate::error::ErrorDomain::Encoding);
}
fn std_verdict(b: &[u8]) -> std::result::Result<(), (usize, Option<usize>)> {
std::str::from_utf8(b)
.map(|_| ())
.map_err(|e| (e.valid_up_to(), e.error_len()))
}
fn simd_verdict(b: &[u8]) -> std::result::Result<(), (usize, Option<usize>)> {
simdutf8::compat::from_utf8(b)
.map(|_| ())
.map_err(|e| (e.valid_up_to(), e.error_len()))
}
#[test]
fn simdutf8_matches_std_at_chunk_boundaries() {
for len in [0usize, 1, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129] {
let base = vec![b'a'; len];
assert_eq!(std_verdict(&base), simd_verdict(&base), "clean buffer len {len}");
for pos in 0..len {
let mut bad = base.clone();
bad[pos] = 0xFF;
assert_eq!(
std_verdict(&bad), simd_verdict(&bad),
"diverged: lone 0xFF at offset {pos} in len {len}",
);
}
}
}
#[test]
fn simdutf8_matches_std_on_adversarial_utf8() {
let cases: &[&[u8]] = &[
b"",
b"hello",
&[0xC3, 0xA9], &[0xC3], &[0xE2, 0x82, 0xAC], &[0xE2, 0x82], &[0xF0, 0x9F, 0x98, 0x80], &[0xF0, 0x9F, 0x98], &[0x80], &[0xBF], &[0xC0, 0x80], &[0xE0, 0x80, 0x80], &[0xF0, 0x80, 0x80, 0x80], &[0xED, 0xA0, 0x80], &[0xED, 0xBF, 0xBF], &[0xF4, 0x90, 0x80, 0x80], &[0xFF],
&[0xFE],
b"caf\xe9", &[b'o', b'k', 0xC3, 0xA9, 0xFF, b'x'], ];
for c in cases {
assert_eq!(std_verdict(c), simd_verdict(c), "diverged on {c:02x?}");
}
}
#[test]
fn simdutf8_matches_std_on_random_bytes() {
let mut state = 0x9E3779B97F4A7C15u64;
let mut next = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
for _ in 0..20_000 {
let len = (next() % 70) as usize;
let mut buf = Vec::with_capacity(len);
for _ in 0..len {
let r = next();
let byte = if r & 3 == 0 {
(r >> 8) as u8 | 0x80
} else {
(r >> 8) as u8 & 0x7F
};
buf.push(byte);
}
assert_eq!(std_verdict(&buf), simd_verdict(&buf), "diverged on {buf:02x?}");
}
}
#[test]
fn in_place_decodes_builtin_entities() {
let buf = b"<r>tom & jerry</r>".to_vec();
let doc = parse_bytes_in_place(buf, &ParseOptions::default()).expect("parse");
let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
assert_eq!(text, "tom & jerry");
}
#[test]
fn in_place_decodes_multiple_builtin_entities() {
let buf = b"<r><hello> & "hi"</r>".to_vec();
let doc = parse_bytes_in_place(buf, &ParseOptions::default()).expect("parse");
let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
assert_eq!(text, r#"<hello> & "hi""#);
}
#[test]
fn in_place_text_without_entities_works() {
let buf = b"<r>plain text with no entities</r>".to_vec();
let doc = parse_bytes_in_place(buf, &ParseOptions::default()).expect("parse");
let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
assert_eq!(text, "plain text with no entities");
}
#[test]
fn in_place_accepts_user_entity_that_shrinks() {
let buf = br#"<!DOCTYPE r [<!ENTITY x "AB">]><r>&x;</r>"#.to_vec();
let doc = parse_bytes_in_place(buf, &ParseOptions::default()).expect("parse");
let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
assert_eq!(text, "AB");
}
#[test]
fn in_place_rejects_user_entity_that_grows() {
let buf = br#"<!DOCTYPE r [<!ENTITY hi "Hello, World!">]><r>&hi;</r>"#.to_vec();
let err = parse_bytes_in_place(buf, &ParseOptions::default()).expect_err(
"expansion bigger than reference must be rejected in in-place mode",
);
assert!(
err.message.contains("exceeds source span")
|| err.message.contains("expansion"),
"error should explain the expansion mismatch, got: {}", err.message,
);
}
#[test]
fn in_place_numeric_char_refs_work() {
let buf = "<r>cafÉ and 𝐀</r>".as_bytes().to_vec();
let doc = parse_bytes_in_place(buf, &ParseOptions::default()).expect("parse");
let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
assert_eq!(text, "cafÉ and 𝐀");
}
#[test]
fn in_place_rejects_recursive_entity() {
let buf = br#"<!DOCTYPE r [<!ENTITY a "&a;">]><r>&a;</r>"#.to_vec();
let err = parse_bytes_in_place(buf, &ParseOptions::default())
.expect_err("recursive entity must be rejected");
let msg = err.message.to_lowercase();
assert!(
msg.contains("recurs") || msg.contains("cycle"),
"error should mention recursion/cycle, got: {}", err.message,
);
}
#[test]
fn in_place_default_opts_reject_bad_name_start() {
let buf = b"<1foo/>".to_vec();
let err = parse_bytes_in_place(buf, &ParseOptions::default())
.expect_err("default opts must reject name starting with digit");
assert!(
err.message.contains("name-start") || err.message.contains("name start"),
"error should mention name-start, got: {}", err.message,
);
assert_eq!(err.code, crate::error::ErrorCode::NameRequired);
assert_eq!(err.code as i32, 68);
}
#[test]
fn in_place_skip_name_validation_accepts_bad_name_start() {
let buf = b"<1foo/>".to_vec();
let opts = ParseOptions { skip_name_validation: true, ..ParseOptions::default() };
let doc = parse_bytes_in_place(buf, &opts)
.expect("skip_name_validation=true must accept malformed name");
assert_eq!(doc.root().name(), "1foo");
}
#[test]
fn in_place_default_opts_reject_duplicate_attribute() {
let buf = br#"<r a="1" a="2"/>"#.to_vec();
let err = parse_bytes_in_place(buf, &ParseOptions::default())
.expect_err("default opts must reject duplicate attribute");
assert!(
err.message.to_lowercase().contains("duplicate") || err.message.contains("Unique Att Spec"),
"error should mention duplicate-attribute, got: {}", err.message,
);
}
#[test]
fn in_place_skip_attr_validation_accepts_duplicate_attribute() {
let buf = br#"<r a="1" a="2"/>"#.to_vec();
let opts = ParseOptions { skip_attr_validation: true, ..ParseOptions::default() };
let doc = parse_bytes_in_place(buf, &opts)
.expect("skip_attr_validation=true must accept duplicate attr");
let n_attrs = doc.root().attributes().count();
assert_eq!(n_attrs, 2, "both duplicate attrs should be present");
}
#[test]
fn in_place_default_opts_reject_end_tag_mismatch() {
let buf = b"<a></b>".to_vec();
let err = parse_bytes_in_place(buf, &ParseOptions::default())
.expect_err("default opts must reject mismatched end tag");
let msg = err.message.to_lowercase();
assert!(
msg.contains("mismatched") || msg.contains("expected"),
"error should mention mismatched end tag, got: {}", err.message,
);
}
#[test]
fn in_place_skip_end_tag_check_accepts_end_tag_mismatch() {
let buf = b"<a></b>".to_vec();
let opts = ParseOptions { skip_end_tag_check: true, ..ParseOptions::default() };
let doc = parse_bytes_in_place(buf, &opts)
.expect("skip_end_tag_check=true must accept mismatched end tag");
assert_eq!(doc.root().name(), "a");
}
#[test]
fn in_place_default_opts_reject_invalid_xml_char() {
let buf = b"<r>hello\x01world</r>".to_vec();
let err = parse_bytes_in_place(buf, &ParseOptions::default())
.expect_err("default opts must reject invalid XML char");
let msg = err.message.to_lowercase();
assert!(
msg.contains("xml 1.0") || msg.contains("char") || msg.contains("invalid"),
"error should mention invalid XML char, got: {}", err.message,
);
}
#[test]
fn in_place_skip_xml_char_validation_accepts_invalid_xml_char() {
let buf = b"<r>hello\x01world</r>".to_vec();
let opts = ParseOptions { skip_xml_char_validation: true, ..ParseOptions::default() };
let doc = parse_bytes_in_place(buf, &opts)
.expect("skip_xml_char_validation=true must accept 0x01 in text");
let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
assert_eq!(text.as_bytes(), b"hello\x01world");
}
#[test]
fn in_place_with_all_skips_parses_well_formed_doc() {
let buf = b"<root><a id=\"1\">hello</a></root>".to_vec();
let opts = ParseOptions {
skip_xml_char_validation: true,
skip_name_validation: true,
skip_attr_validation: true,
skip_end_tag_check: true,
..ParseOptions::default()
};
let doc = parse_bytes_in_place(buf, &opts).expect("all-skips parse");
assert_eq!(doc.root().name(), "root");
let a = doc.root().children().next().expect("element child");
assert_eq!(a.name(), "a");
assert_eq!(a.attributes().next().unwrap().value(), "1");
}
}