use std::collections::{BTreeMap, BTreeSet};
use std::io::Read;
use std::str;
use quick_xml::events::{BytesStart, Event};
use quick_xml::reader::Reader;
use quick_xml::XmlVersion;
use crate::core::{
validate_namespace_binding, Attribute, Document, ErrorKind, NamespaceDeclaration, QName, Span,
XmlError, XmlResult, XML_NAMESPACE_URI,
};
use crate::security::{EntityPolicy, ParserSecurityConfig, SecurityLimits};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParserConfig {
preserve_comments: bool,
preserve_cdata: bool,
security: ParserSecurityConfig,
}
impl ParserConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_preserve_comments(mut self, preserve: bool) -> Self {
self.preserve_comments = preserve;
self
}
pub fn with_preserve_cdata(mut self, preserve: bool) -> Self {
self.preserve_cdata = preserve;
self
}
pub fn with_max_document_bytes(mut self, limit: usize) -> Self {
let limits = self
.security
.limits()
.clone()
.with_max_document_bytes(limit);
self.security = self.security.with_limits(limits);
self
}
pub fn with_max_text_bytes(mut self, limit: usize) -> Self {
let limits = self.security.limits().clone().with_max_text_bytes(limit);
self.security = self.security.with_limits(limits);
self
}
pub fn with_max_depth(mut self, limit: usize) -> Self {
let limits = self.security.limits().clone().with_max_depth(limit);
self.security = self.security.with_limits(limits);
self
}
pub fn with_max_nodes(mut self, limit: usize) -> Self {
let limits = self.security.limits().clone().with_max_nodes(limit);
self.security = self.security.with_limits(limits);
self
}
pub fn with_security(mut self, security: ParserSecurityConfig) -> Self {
self.security = security;
self
}
pub fn preserve_comments(&self) -> bool {
self.preserve_comments
}
pub fn preserve_cdata(&self) -> bool {
self.preserve_cdata
}
pub fn security(&self) -> &ParserSecurityConfig {
&self.security
}
fn limits(&self) -> &SecurityLimits {
self.security.limits()
}
}
impl Default for ParserConfig {
fn default() -> Self {
Self {
preserve_comments: true,
preserve_cdata: true,
security: ParserSecurityConfig::default(),
}
}
}
pub fn parse_str(xml: &str) -> XmlResult<Document> {
parse_str_with_config(xml, &ParserConfig::default())
}
pub fn parse_str_with_config(xml: &str, config: &ParserConfig) -> XmlResult<Document> {
config.limits().check_document_size(xml.len())?;
let mut reader = Reader::from_str(xml);
reader.config_mut().trim_text(false);
reader.config_mut().expand_empty_elements = false;
reader.config_mut().check_end_names = true;
parse_events(xml, &mut reader, config)
}
pub fn parse_reader(reader: impl Read) -> XmlResult<Document> {
parse_reader_with_config(reader, &ParserConfig::default())
}
pub fn parse_reader_with_config(
mut reader: impl Read,
config: &ParserConfig,
) -> XmlResult<Document> {
let mut bytes = Vec::new();
let limit = config.limits().max_document_bytes() as u64 + 1;
reader
.by_ref()
.take(limit)
.read_to_end(&mut bytes)
.map_err(|error| XmlError::new(ErrorKind::Io, error.to_string()))?;
config.limits().check_document_size(bytes.len())?;
let xml = String::from_utf8(bytes).map_err(|error| {
XmlError::new(
ErrorKind::Parse,
format!("XML input must be valid UTF-8: {error}"),
)
})?;
parse_str_with_config(&xml, config)
}
fn parse_events(
xml: &str,
reader: &mut Reader<&[u8]>,
config: &ParserConfig,
) -> XmlResult<Document> {
let mut state = ParserState::new(config);
loop {
let event = reader.read_event().map_err(|error| {
parse_error_with_position(xml, reader.error_position() as usize, error.to_string())
})?;
match event {
Event::Start(start) => state.start_element(start, reader, xml)?,
Event::Empty(start) => {
state.start_element(start, reader, xml)?;
state.end_element();
}
Event::End(_) => state.end_element(),
Event::Text(text) => {
let value = text.xml10_content().map_err(|error| {
parse_error_with_position(
xml,
reader.error_position() as usize,
error.to_string(),
)
})?;
state.text(value.as_ref())?;
}
Event::CData(cdata) => {
let value = cdata.decode().map_err(|error| {
parse_error_with_position(
xml,
reader.error_position() as usize,
error.to_string(),
)
})?;
state.cdata(value.as_ref())?;
}
Event::Comment(comment) => {
if config.preserve_comments {
let value = comment.xml10_content().map_err(|error| {
parse_error_with_position(
xml,
reader.error_position() as usize,
error.to_string(),
)
})?;
state.comment(value.as_ref())?;
}
}
Event::PI(pi) => {
let content = str::from_utf8(pi.content()).map_err(|error| {
parse_error_with_position(
xml,
reader.error_position() as usize,
error.to_string(),
)
})?;
let target = str::from_utf8(pi.target()).map_err(|error| {
parse_error_with_position(
xml,
reader.error_position() as usize,
error.to_string(),
)
})?;
state.processing_instruction(target, processing_instruction_data(content))?;
}
Event::Decl(_) => {}
Event::DocType(_) => {
config
.security()
.entity_policy()
.reject_doctype()
.map_err(|error| {
error.with_span(span_for_byte(xml, reader.error_position() as usize))
})?;
}
Event::GeneralRef(reference) => {
if let Some(ch) = reference.resolve_char_ref().map_err(|error| {
parse_error_with_position(
xml,
reader.error_position() as usize,
error.to_string(),
)
})? {
state.text(&ch.to_string())?;
} else {
let name = reference.decode().map_err(|error| {
parse_error_with_position(
xml,
reader.error_position() as usize,
error.to_string(),
)
})?;
let value = match predefined_entity(name.as_ref()) {
Some(value) => value,
None => {
return Err(unresolved_entity_error(
config.security().entity_policy(),
name.as_ref(),
span_for_byte(xml, reader.error_position() as usize),
));
}
};
state.text(value)?;
}
}
Event::Eof => break,
}
}
state.finish()
}
struct ParserState<'a> {
config: &'a ParserConfig,
document: Document,
stack: Vec<crate::core::NodeId>,
namespace_stack: Vec<NamespaceScope>,
node_count: usize,
}
impl<'a> ParserState<'a> {
fn new(config: &'a ParserConfig) -> Self {
Self {
config,
document: Document::new(),
stack: Vec::new(),
namespace_stack: vec![NamespaceScope::default()],
node_count: 0,
}
}
fn start_element(
&mut self,
start: BytesStart<'_>,
reader: &Reader<&[u8]>,
xml: &str,
) -> XmlResult<()> {
let declarations = namespace_declarations(&start, reader, xml)?;
let scope = self
.namespace_stack
.last()
.expect("root namespace scope exists")
.with_declarations(&declarations);
let name = qname_from_raw(start.name().as_ref(), &scope, true)?;
let id = match self.stack.last().copied() {
Some(parent) => self.document.add_element(parent, name)?,
None => self.document.add_root_element(name)?,
};
self.count_node()?;
self.config.limits().check_depth(self.stack.len() + 1)?;
for declaration in declarations {
let core_declaration = match declaration.prefix {
Some(prefix) => NamespaceDeclaration::prefixed(prefix, declaration.uri)?,
None => NamespaceDeclaration::default(declaration.uri)?,
};
self.document
.add_namespace_declaration(id, core_declaration)?;
}
let mut attribute_names = BTreeSet::new();
for attribute in start.attributes() {
let attribute =
attribute.map_err(|error| parse_error_with_position(xml, 0, error.to_string()))?;
if is_namespace_declaration(attribute.key.as_ref()) {
continue;
}
let name = qname_from_raw(attribute.key.as_ref(), &scope, false)?;
if !attribute_names.insert(expanded_attribute_name(&name)) {
return Err(XmlError::new(
ErrorKind::Parse,
format!(
"duplicate attribute `{}` by expanded name",
name.lexical_name()
),
));
}
let value = attribute
.decoded_and_normalized_value(XmlVersion::Explicit1_0, reader.decoder())
.map_err(|error| parse_error_with_position(xml, 0, error.to_string()))?;
self.document
.add_attribute(id, Attribute::new(name, value.as_ref()))?;
}
self.stack.push(id);
self.namespace_stack.push(scope);
Ok(())
}
fn end_element(&mut self) {
self.stack.pop();
self.namespace_stack.pop();
}
fn text(&mut self, value: &str) -> XmlResult<()> {
if value.is_empty() {
return Ok(());
}
if self.stack.is_empty() {
if value.trim().is_empty() {
return Ok(());
}
return Err(XmlError::new(
ErrorKind::Parse,
"non-whitespace text outside the document root is not allowed",
));
}
self.check_text_limit(value)?;
let parent = self.current_parent()?;
self.document.add_text(parent, value)?;
self.count_node()
}
fn cdata(&mut self, value: &str) -> XmlResult<()> {
if value.is_empty() {
return Ok(());
}
if self.stack.is_empty() {
return Err(XmlError::new(
ErrorKind::Parse,
"CDATA outside the document root is not allowed",
));
}
self.check_text_limit(value)?;
let parent = self.current_parent()?;
if self.config.preserve_cdata {
self.document.add_cdata(parent, value)?;
} else {
self.document.add_text(parent, value)?;
}
self.count_node()
}
fn comment(&mut self, value: &str) -> XmlResult<()> {
if self.stack.is_empty() {
return Ok(());
}
self.check_text_limit(value)?;
let parent = self.current_parent()?;
self.document.add_comment(parent, value)?;
self.count_node()
}
fn processing_instruction(&mut self, target: &str, data: Option<&str>) -> XmlResult<()> {
if self.stack.is_empty() {
return Ok(());
}
let parent = self.current_parent()?;
self.document
.add_processing_instruction(parent, target, data)?;
self.count_node()
}
fn finish(self) -> XmlResult<Document> {
if !self.stack.is_empty() {
return Err(XmlError::new(
ErrorKind::Parse,
"XML document ended before closing all elements",
));
}
if self.document.root().is_none() {
return Err(XmlError::new(
ErrorKind::Parse,
"XML document must contain one root element",
));
}
Ok(self.document)
}
fn current_parent(&self) -> XmlResult<crate::core::NodeId> {
self.stack.last().copied().ok_or_else(|| {
XmlError::new(
ErrorKind::Parse,
"XML content outside the document root is not supported",
)
})
}
fn count_node(&mut self) -> XmlResult<()> {
self.node_count += 1;
self.config.limits().check_nodes(self.node_count)
}
fn check_text_limit(&self, value: &str) -> XmlResult<()> {
self.config.limits().check_text_size(value.len())
}
}
#[derive(Debug, Clone, Default)]
struct NamespaceScope {
default_namespace: Option<String>,
prefixed: BTreeMap<String, String>,
}
impl NamespaceScope {
fn with_declarations(&self, declarations: &[ParsedNamespaceDeclaration]) -> Self {
let mut next = self.clone();
for declaration in declarations {
match &declaration.prefix {
Some(prefix) => {
next.prefixed
.insert(prefix.clone(), declaration.uri.clone());
}
None => {
next.default_namespace = Some(declaration.uri.clone());
}
}
}
next
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ParsedNamespaceDeclaration {
prefix: Option<String>,
uri: String,
}
fn namespace_declarations(
start: &BytesStart<'_>,
reader: &Reader<&[u8]>,
xml: &str,
) -> XmlResult<Vec<ParsedNamespaceDeclaration>> {
let mut declarations = Vec::new();
for attribute in start.attributes() {
let attribute =
attribute.map_err(|error| parse_error_with_position(xml, 0, error.to_string()))?;
let raw_name = attribute.key.as_ref();
if !is_namespace_declaration(raw_name) {
continue;
}
let uri = attribute
.decoded_and_normalized_value(XmlVersion::Explicit1_0, reader.decoder())
.map_err(|error| parse_error_with_position(xml, 0, error.to_string()))?;
let prefix = raw_name
.strip_prefix(b"xmlns:")
.map(bytes_to_string)
.transpose()?;
validate_namespace_binding(prefix.as_deref(), uri.as_ref())?;
declarations.push(ParsedNamespaceDeclaration {
prefix,
uri: uri.into_owned(),
});
}
Ok(declarations)
}
fn qname_from_raw(raw: &[u8], scope: &NamespaceScope, default_applies: bool) -> XmlResult<QName> {
let raw = bytes_to_string(raw)?;
match raw.split_once(':') {
Some((prefix, local)) => {
let uri = if prefix == "xml" {
XML_NAMESPACE_URI
} else {
scope.prefixed.get(prefix).ok_or_else(|| {
XmlError::new(
ErrorKind::UnknownNamespacePrefix,
format!("namespace prefix `{prefix}` is not declared"),
)
})?
};
QName::qualified(prefix, local, uri)
}
None if default_applies => match &scope.default_namespace {
Some(uri) => QName::namespaced(raw, uri),
None => QName::new(raw),
},
None => QName::new(raw),
}
}
fn expanded_attribute_name(name: &QName) -> (Option<String>, String) {
(
name.namespace_uri().map(|uri| uri.as_str().to_owned()),
name.local().to_owned(),
)
}
fn is_namespace_declaration(raw_name: &[u8]) -> bool {
raw_name == b"xmlns" || raw_name.starts_with(b"xmlns:")
}
fn bytes_to_string(bytes: &[u8]) -> XmlResult<String> {
str::from_utf8(bytes)
.map(str::to_owned)
.map_err(|error| XmlError::new(ErrorKind::Parse, error.to_string()))
}
fn empty_to_none(value: &str) -> Option<&str> {
if value.is_empty() {
None
} else {
Some(value)
}
}
fn processing_instruction_data(value: &str) -> Option<&str> {
let value = value
.strip_prefix(' ')
.or_else(|| value.strip_prefix('\t'))
.or_else(|| value.strip_prefix('\r'))
.or_else(|| value.strip_prefix('\n'))
.unwrap_or(value);
empty_to_none(value)
}
fn predefined_entity(name: &str) -> Option<&'static str> {
match name {
"lt" => Some("<"),
"gt" => Some(">"),
"amp" => Some("&"),
"apos" => Some("'"),
"quot" => Some("\""),
_ => None,
}
}
fn unresolved_entity_error(policy: &EntityPolicy, name: &str, span: Span) -> XmlError {
match policy.reject_external_entity(name) {
Err(error) => error.with_span(span),
Ok(()) => XmlError::new(
ErrorKind::Parse,
format!("external entity resolution is not implemented for `&{name};`"),
)
.with_span(span),
}
}
fn parse_error_with_position(
xml: &str,
byte_position: usize,
message: impl Into<String>,
) -> XmlError {
XmlError::new(ErrorKind::Parse, message).with_span(span_for_byte(xml, byte_position))
}
fn span_for_byte(xml: &str, byte_position: usize) -> Span {
let mut line = 1;
let mut column = 1;
for (index, ch) in xml.char_indices() {
if index >= byte_position {
break;
}
if ch == '\n' {
line += 1;
column = 1;
} else {
column += 1;
}
}
Span::new(line, column)
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use super::*;
use crate::core::{Attribute, NamespaceDeclaration, NodeKind};
use crate::writer::to_string_compact;
#[test]
fn parser_parse_str_reads_simple_xml() -> XmlResult<()> {
let document = parse_str("<Root><Child>value</Child></Root>")?;
let root = document.root().expect("root");
let [child] = document.children(root)? else {
panic!("expected one child");
};
let [text] = document.children(*child)? else {
panic!("expected one text child");
};
assert!(matches!(document.node(*text)?.kind(), NodeKind::Text(value) if value == "value"));
Ok(())
}
#[test]
fn parser_parse_reader_reads_xml() -> XmlResult<()> {
let document = parse_reader(Cursor::new("<Root><Child/></Root>"))?;
assert_eq!(to_string_compact(&document)?, "<Root><Child/></Root>");
Ok(())
}
#[test]
fn parser_whitespace_around_root_is_allowed() -> XmlResult<()> {
let document = parse_str(" \n\t<Root/> \n")?;
assert_eq!(to_string_compact(&document)?, "<Root/>");
Ok(())
}
#[test]
fn parser_xml_declaration_and_boundary_misc_are_allowed() -> XmlResult<()> {
let document = parse_str(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- before -->\n<?before ok?>\n<Root/>\n<!-- after -->\n<?after ok?>",
)?;
assert_eq!(to_string_compact(&document)?, "<Root/>");
Ok(())
}
#[test]
fn parser_processing_instruction_roundtrips_without_accumulating_separator_space(
) -> XmlResult<()> {
let document = parse_str("<Root><?format keep?></Root>")?;
assert_eq!(
to_string_compact(&document)?,
"<Root><?format keep?></Root>"
);
Ok(())
}
#[test]
fn parser_rejects_non_whitespace_text_outside_root() {
let before = parse_str("text<Root/>").expect_err("text before root must fail");
let after = parse_str("<Root/>text").expect_err("text after root must fail");
assert_eq!(before.kind(), &ErrorKind::Parse);
assert!(before.message().contains("outside the document root"));
assert_eq!(after.kind(), &ErrorKind::Parse);
assert!(after.message().contains("outside the document root"));
}
#[test]
fn parser_empty_document_requires_root() {
let empty = parse_str("").expect_err("empty document must fail");
let whitespace = parse_str(" \n\t ").expect_err("whitespace-only document must fail");
let comment =
parse_str("<!-- only comment -->").expect_err("comment-only document must fail");
let pi = parse_str("<?xml-stylesheet href=\"style.xsl\"?>")
.expect_err("PI-only document must fail");
for error in [empty, whitespace, comment, pi] {
assert_eq!(error.kind(), &ErrorKind::Parse);
assert!(error.message().contains("root element"));
}
}
#[test]
fn parser_namespaces_preserves_qnames_and_attributes() -> XmlResult<()> {
let document = parse_str(
r#"<doc:Root xmlns="urn:default" xmlns:doc="urn:doc" doc:id="A1"><Child plain="yes"/></doc:Root>"#,
)?;
let root = document.root().expect("root");
let root_node = document.node(root)?;
let NodeKind::Element(root_element) = root_node.kind() else {
panic!("expected root element");
};
assert_eq!(
root_element.name().prefix().map(|prefix| prefix.as_str()),
Some("doc")
);
assert_eq!(
root_element.name().namespace_uri().map(|uri| uri.as_str()),
Some("urn:doc")
);
assert_eq!(root_element.namespace_declarations().len(), 2);
assert_eq!(root_element.attributes()[0].name().lexical_name(), "doc:id");
let child = document.children(root)?[0];
let NodeKind::Element(child_element) = document.node(child)?.kind() else {
panic!("expected child element");
};
assert_eq!(
child_element.name().namespace_uri().map(|uri| uri.as_str()),
Some("urn:default")
);
assert_eq!(child_element.attributes()[0].name().namespace_uri(), None);
Ok(())
}
#[test]
fn parser_namespace_reserved_xml_prefix_is_implicit() -> XmlResult<()> {
let document = parse_str(r#"<Root xml:lang="en" xml:space="preserve"/>"#)?;
let root = document.root().expect("root");
let NodeKind::Element(element) = document.node(root)?.kind() else {
panic!("expected root element");
};
assert_eq!(element.attributes().len(), 2);
assert_eq!(element.attributes()[0].name().lexical_name(), "xml:lang");
assert_eq!(
element.attributes()[0]
.name()
.namespace_uri()
.map(|uri| uri.as_str()),
Some(XML_NAMESPACE_URI)
);
assert_eq!(element.attributes()[1].name().lexical_name(), "xml:space");
Ok(())
}
#[test]
fn parser_namespace_rejects_reserved_declaration_misuse() {
let cases = [
r#"<Root xmlns:xml="urn:wrong"/>"#,
r#"<Root xmlns:doc="http://www.w3.org/XML/1998/namespace"/>"#,
r#"<Root xmlns:xmlns="urn:any"/>"#,
r#"<Root xmlns="http://www.w3.org/2000/xmlns/"/>"#,
];
for xml in cases {
let error = parse_str(xml).expect_err("reserved namespace misuse must fail");
assert_eq!(error.kind(), &ErrorKind::InvalidNamespace, "{xml}");
}
}
#[test]
fn parser_namespace_rejects_duplicate_attributes_by_expanded_name() {
let direct = parse_str(r#"<Root id="1" id="2"/>"#)
.expect_err("duplicate unqualified attributes must fail");
let expanded = parse_str(r#"<Root xmlns:a="urn:x" xmlns:b="urn:x" a:id="1" b:id="2"/>"#)
.expect_err("duplicate expanded attributes must fail");
assert_eq!(direct.kind(), &ErrorKind::Parse);
assert!(direct.message().contains("duplicate"));
assert_eq!(expanded.kind(), &ErrorKind::Parse);
assert!(expanded.message().contains("duplicate"));
}
#[test]
fn parser_namespace_default_does_not_apply_to_attributes() -> XmlResult<()> {
let document = parse_str(r#"<Root xmlns="urn:root" id="A1"/>"#)?;
let root = document.root().expect("root");
let NodeKind::Element(element) = document.node(root)?.kind() else {
panic!("expected root element");
};
assert_eq!(
element.name().namespace_uri().map(|uri| uri.as_str()),
Some("urn:root")
);
assert_eq!(element.attributes()[0].name().namespace_uri(), None);
Ok(())
}
#[test]
fn parser_comments_can_be_preserved_or_discarded() -> XmlResult<()> {
let preserved = parse_str("<Root><!-- note --><Child/></Root>")?;
assert!(matches!(
preserved.node(preserved.children(preserved.root().unwrap())?[0])?.kind(),
NodeKind::Comment(comment) if comment == " note "
));
let discarded = parse_str_with_config(
"<Root><!-- note --><Child/></Root>",
&ParserConfig::default().with_preserve_comments(false),
)?;
assert_eq!(discarded.children(discarded.root().unwrap())?.len(), 1);
Ok(())
}
#[test]
fn parser_preserves_cdata() -> XmlResult<()> {
let document = parse_str("<Root><![CDATA[a < b]]></Root>")?;
let root = document.root().expect("root");
let child = document.children(root)?[0];
assert!(matches!(document.node(child)?.kind(), NodeKind::CData(value) if value == "a < b"));
Ok(())
}
#[test]
fn parser_security_rejects_external_entities_by_default() {
let error = parse_str(r#"<!DOCTYPE Root SYSTEM "file:///tmp/x"><Root/>"#)
.expect_err("doctype must be blocked");
assert_eq!(error.kind(), &ErrorKind::Parse);
assert!(error.message().contains("DOCTYPE"));
}
#[test]
fn parser_entity_predefined_references_are_resolved() -> XmlResult<()> {
let document = parse_str("<Root><&>'"</Root>")?;
assert_eq!(
to_string_compact(&document)?,
"<Root><&>'\"</Root>"
);
Ok(())
}
#[test]
fn parser_entity_unknown_reference_is_rejected_by_default() {
let error = parse_str("<Root>&xxe;</Root>").expect_err("unknown entity must fail");
assert_eq!(error.kind(), &ErrorKind::Parse);
assert!(error.message().contains("disabled by default"));
assert!(error.span().is_some());
}
#[test]
fn parser_entity_permissive_policy_still_rejects_unimplemented_resolution() {
let security = ParserSecurityConfig::default()
.with_entity_policy(EntityPolicy::secure().with_external_entities(true));
let config = ParserConfig::default().with_security(security);
let error = parse_str_with_config("<Root>&xxe;</Root>", &config)
.expect_err("unknown entity must fail without panic");
assert_eq!(error.kind(), &ErrorKind::Parse);
assert!(error.message().contains("not implemented"));
assert!(error.span().is_some());
}
#[test]
fn parser_respects_max_depth() {
let config = ParserConfig::default().with_max_depth(1);
let error =
parse_str_with_config("<Root><Child/></Root>", &config).expect_err("depth must fail");
assert_eq!(error.kind(), &ErrorKind::Parse);
assert!(error.message().contains("depth"));
}
#[test]
fn parser_consumes_shared_security_config() {
let security = ParserSecurityConfig::default()
.with_limits(SecurityLimits::default().with_max_document_bytes(6));
let config = ParserConfig::default().with_security(security);
let error = parse_str_with_config("<Root/>", &config).expect_err("size must fail");
assert_eq!(error.kind(), &ErrorKind::Parse);
assert!(error.message().contains("maximum size"));
}
#[test]
fn parser_reports_span_for_malformed_xml() {
let error = parse_str("<Root>\n <Child></Root>").expect_err("malformed XML must fail");
assert_eq!(error.kind(), &ErrorKind::Parse);
assert!(error.span().is_some());
}
#[test]
fn parser_roundtrip_reads_writer_output() -> XmlResult<()> {
let mut document = Document::new();
let root = document.add_root_element(QName::qualified("doc", "Root", "urn:doc")?)?;
document
.add_namespace_declaration(root, NamespaceDeclaration::prefixed("doc", "urn:doc")?)?;
document.add_attribute(root, Attribute::new(QName::new("id")?, "A1"))?;
document.add_text(root, "value")?;
let xml = to_string_compact(&document)?;
let parsed = parse_str(&xml)?;
assert_eq!(to_string_compact(&parsed)?, xml);
Ok(())
}
}