use crate::error::{Error, Result};
use quick_xml::escape::resolve_predefined_entity;
use quick_xml::events::BytesStart;
use quick_xml::events::Event as QxEvent;
use quick_xml::Reader as QxReader;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum TokenType {
Invalid,
StartElement,
EndElement,
Characters,
EndDocument,
}
#[derive(Clone, Debug)]
struct Attribute {
name: String,
value: String,
}
#[derive(Clone, Debug)]
struct Event {
token_type: TokenType,
name: String,
text: String,
attributes: Vec<Attribute>,
}
pub struct XmlReader {
events: Vec<Event>,
pos: usize,
token: TokenType,
name: String,
text: String,
attributes: Vec<Attribute>,
has_error: bool,
}
impl XmlReader {
pub fn new(data: &str) -> Result<Self> {
let (events, has_error) = parse_document(data);
Ok(Self {
events,
pos: 0,
token: TokenType::Invalid,
name: String::new(),
text: String::new(),
attributes: Vec::new(),
has_error,
})
}
fn read_next(&mut self) -> TokenType {
self.attributes.clear();
self.text.clear();
self.name.clear();
if self.has_error {
self.token = TokenType::Invalid;
return self.token;
}
if self.pos >= self.events.len() {
self.token = TokenType::EndDocument;
return self.token;
}
let ev = &self.events[self.pos];
self.pos += 1;
self.token = ev.token_type;
self.name = ev.name.clone();
self.text = ev.text.clone();
self.attributes = ev.attributes.clone();
self.token
}
fn is_start_element(&self) -> bool {
self.token == TokenType::StartElement
}
fn is_end_element(&self) -> bool {
self.token == TokenType::EndElement
}
pub fn read_next_start_element(&mut self) -> Result<bool> {
loop {
let token = self.read_next();
if token == TokenType::Invalid || token == TokenType::EndDocument {
return Ok(false);
}
if self.is_end_element() {
return Ok(false);
}
if self.is_start_element() {
return Ok(true);
}
}
}
pub fn name(&self) -> Result<String> {
Ok(self.name.clone())
}
pub fn read_element_text(&mut self) -> Result<String> {
if !self.is_start_element() {
return Ok(String::new());
}
let mut result = String::new();
let mut depth = 1;
loop {
let token = self.read_next();
if token == TokenType::Invalid || token == TokenType::EndDocument {
break;
}
if token == TokenType::StartElement {
depth += 1;
} else if token == TokenType::EndElement {
depth -= 1;
if depth == 0 {
break;
}
} else if token == TokenType::Characters && depth == 1 {
result.push_str(&self.text);
}
}
Ok(result)
}
pub fn skip_current_element(&mut self) -> Result<()> {
if !self.is_start_element() {
return Ok(());
}
let mut depth = 1;
loop {
let token = self.read_next();
if token == TokenType::Invalid || token == TokenType::EndDocument {
break;
}
if token == TokenType::StartElement {
depth += 1;
} else if token == TokenType::EndElement {
depth -= 1;
if depth == 0 {
break;
}
}
}
Ok(())
}
pub fn attribute_count(&self) -> Result<i32> {
Ok(self.attributes.len() as i32)
}
pub fn attribute_name(&self, index: i32) -> Result<String> {
if index < 0 || index as usize >= self.attributes.len() {
return Err(Error::NotFound);
}
Ok(self.attributes[index as usize].name.clone())
}
pub fn attribute_value(&self, index: i32) -> Result<String> {
if index < 0 || index as usize >= self.attributes.len() {
return Err(Error::NotFound);
}
Ok(self.attributes[index as usize].value.clone())
}
pub fn has_error(&self) -> Result<bool> {
Ok(self.has_error)
}
}
pub struct XmlWriter {
output: String,
stack: Vec<String>,
open_start_tag: bool,
}
impl XmlWriter {
pub fn new() -> Self {
Self {
output: String::new(),
stack: Vec::new(),
open_start_tag: false,
}
}
pub fn write_start_element(&mut self, name: &str) -> Result<()> {
if self.open_start_tag {
self.output.push('>');
self.open_start_tag = false;
}
self.output.push('<');
self.output.push_str(name);
self.stack.push(name.to_string());
self.open_start_tag = true;
Ok(())
}
pub fn write_attribute(&mut self, name: &str, value: &str) -> Result<()> {
if !self.open_start_tag {
return Ok(());
}
self.output.push(' ');
self.output.push_str(name);
self.output.push_str("=\"");
self.output.push_str(&escape_attribute(value));
self.output.push('"');
Ok(())
}
pub fn write_characters(&mut self, text: &str) -> Result<()> {
if self.open_start_tag {
self.output.push('>');
self.open_start_tag = false;
}
self.output.push_str(&escape_text(text));
Ok(())
}
pub fn write_text_element(&mut self, name: &str, text: &str) -> Result<()> {
self.write_start_element(name)?;
self.write_characters(text)?;
self.write_end_element()?;
Ok(())
}
pub fn write_end_element(&mut self) -> Result<()> {
let name = match self.stack.pop() {
Some(n) => n,
None => return Ok(()),
};
if self.open_start_tag {
self.output.push_str("/>");
self.open_start_tag = false;
} else {
self.output.push_str("</");
self.output.push_str(&name);
self.output.push('>');
}
Ok(())
}
pub fn write_end_document(&mut self) -> Result<()> {
while !self.stack.is_empty() {
self.write_end_element()?;
}
Ok(())
}
pub fn output(&self) -> Result<String> {
Ok(self.output.clone())
}
}
fn escape_text(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
_ => out.push(c),
}
}
out
}
fn escape_attribute(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
_ => out.push(c),
}
}
out
}
fn is_ws(c: char) -> bool {
c == ' ' || c == '\t' || c == '\r' || c == '\n'
}
fn qname_string(q: impl AsRef<[u8]>) -> String {
std::str::from_utf8(q.as_ref())
.map(str::to_owned)
.unwrap_or_default()
}
fn push_characters(events: &mut Vec<Event>, text: &str) {
if text.is_empty() {
return;
}
if let Some(last) = events.last_mut() {
if last.token_type == TokenType::Characters {
last.text.push_str(text);
return;
}
}
events.push(Event {
token_type: TokenType::Characters,
name: String::new(),
text: text.to_string(),
attributes: Vec::new(),
});
}
fn push_start_element(
e: &BytesStart,
events: &mut Vec<Event>,
stack: &mut Vec<String>,
saw_start: &mut bool,
) -> Option<String> {
let name = qname_string(e.name());
let mut attributes: Vec<Attribute> = Vec::new();
for attr in e.attributes() {
let attr = match attr {
Ok(a) => a,
Err(_) => return None,
};
if attr.value.contains(&b'<') {
return None;
}
let value = match attr.normalized_value(quick_xml::XmlVersion::Implicit1_0) {
Ok(v) => v.into_owned(),
Err(_) => return None,
};
attributes.push(Attribute {
name: qname_string(attr.key),
value,
});
}
if stack.is_empty() && *saw_start {
return None;
}
*saw_start = true;
stack.push(name.clone());
events.push(Event {
token_type: TokenType::StartElement,
name: name.clone(),
text: String::new(),
attributes,
});
Some(name)
}
fn parse_document(data: &str) -> (Vec<Event>, bool) {
let mut events: Vec<Event> = Vec::new();
let mut stack: Vec<String> = Vec::new();
let mut saw_start = false;
let mut has_error = false;
let mut reader = QxReader::from_str(data);
loop {
let ev = match reader.read_event() {
Ok(e) => e,
Err(_) => {
has_error = true;
break;
}
};
match ev {
QxEvent::Start(e) => {
if push_start_element(&e, &mut events, &mut stack, &mut saw_start).is_none() {
has_error = true;
break;
}
}
QxEvent::Empty(e) => {
let name = match push_start_element(&e, &mut events, &mut stack, &mut saw_start) {
Some(n) => n,
None => {
has_error = true;
break;
}
};
stack.pop();
events.push(Event {
token_type: TokenType::EndElement,
name,
text: String::new(),
attributes: Vec::new(),
});
}
QxEvent::End(e) => {
let name = qname_string(e.name());
match stack.last() {
Some(top) if *top == name => {
stack.pop();
events.push(Event {
token_type: TokenType::EndElement,
name,
text: String::new(),
attributes: Vec::new(),
});
}
_ => {
has_error = true;
break;
}
}
}
QxEvent::Text(t) => {
let text = t.decode().map(|c| c.into_owned()).unwrap_or_default();
if stack.is_empty() {
if !text.chars().all(is_ws) {
has_error = true;
break;
}
} else {
push_characters(&mut events, &text);
}
}
QxEvent::CData(c) => {
if stack.is_empty() {
has_error = true;
break;
}
let text = c.decode().map(|c| c.into_owned()).unwrap_or_default();
push_characters(&mut events, &text);
}
QxEvent::GeneralRef(r) => {
if stack.is_empty() {
has_error = true;
break;
}
if r.is_char_ref() {
match r.resolve_char_ref() {
Ok(Some(ch)) => {
let mut s = String::new();
s.push(ch);
push_characters(&mut events, &s);
}
_ => {
has_error = true;
break;
}
}
} else {
let name = r.decode().map(|c| c.into_owned()).unwrap_or_default();
match resolve_predefined_entity(&name) {
Some(s) => push_characters(&mut events, s),
None => {
has_error = true;
break;
}
}
}
}
QxEvent::Eof => break,
_ => {}
}
}
if !has_error && (!saw_start || !stack.is_empty()) {
has_error = true;
}
(events, has_error)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn writer_basic_document() {
let mut w = XmlWriter::new();
w.write_start_element("root").unwrap();
w.write_attribute("a", "1").unwrap();
w.write_attribute("b", "x\"y").unwrap();
w.write_text_element("child", "hi & bye <there>").unwrap();
w.write_end_element().unwrap();
w.write_end_document().unwrap();
assert_eq!(
w.output().unwrap(),
"<root a=\"1\" b=\"x"y\"><child>hi & bye <there></child></root>"
);
}
#[test]
fn writer_empty_and_noop() {
let mut w = XmlWriter::new();
w.write_attribute("a", "b").unwrap();
w.write_end_element().unwrap();
w.write_characters("x").unwrap();
w.write_end_document().unwrap();
assert_eq!(w.output().unwrap(), "x");
}
#[test]
fn writer_self_closing_empty_element() {
let mut w = XmlWriter::new();
w.write_start_element("a").unwrap();
w.write_attribute("k", "v").unwrap();
w.write_end_element().unwrap();
assert_eq!(w.output().unwrap(), "<a k=\"v\"/>");
}
#[test]
fn reader_basic_attributes_and_text() {
let doc = "<root a=\"1\" b=\"2\"><child>x</child><empty/></root>";
let mut r = XmlReader::new(doc).unwrap();
assert!(!r.has_error().unwrap());
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "root");
assert_eq!(r.attribute_count().unwrap(), 2);
assert_eq!(r.attribute_name(0).unwrap(), "a");
assert_eq!(r.attribute_value(0).unwrap(), "1");
assert_eq!(r.attribute_name(1).unwrap(), "b");
assert_eq!(r.attribute_value(1).unwrap(), "2");
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "child");
assert_eq!(r.read_element_text().unwrap(), "x");
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "empty");
assert_eq!(r.attribute_count().unwrap(), 0);
assert!(!r.read_next_start_element().unwrap());
}
#[test]
fn reader_nested_text_only_direct_children() {
let doc = "<a><b>inner</b>outer</a>";
let mut r = XmlReader::new(doc).unwrap();
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "a");
assert_eq!(r.read_element_text().unwrap(), "outer");
}
#[test]
fn reader_skip_current_element() {
let doc = "<root><skip><deep>d</deep></skip><keep>v</keep></root>";
let mut r = XmlReader::new(doc).unwrap();
assert!(r.read_next_start_element().unwrap());
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "skip");
r.skip_current_element().unwrap();
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "keep");
assert_eq!(r.read_element_text().unwrap(), "v");
}
#[test]
fn reader_comments_pi_and_entities() {
let doc = "<!-- c --><root a=\"x"y\">& < ></root>";
let mut r = XmlReader::new(doc).unwrap();
assert!(!r.has_error().unwrap());
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "root");
assert_eq!(r.attribute_value(0).unwrap(), "x\"y");
assert_eq!(r.read_element_text().unwrap(), "& < >");
}
#[test]
fn reader_cdata() {
let doc = "<a><![CDATA[x < y & z]]></a>";
let mut r = XmlReader::new(doc).unwrap();
assert!(!r.has_error().unwrap());
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.read_element_text().unwrap(), "x < y & z");
}
#[test]
fn reader_whitespace_between_elements() {
let doc = "<a>\n <b/>\n</a>";
let mut r = XmlReader::new(doc).unwrap();
assert!(!r.has_error().unwrap());
assert!(r.read_next_start_element().unwrap());
let text = r.read_element_text().unwrap();
assert!(text.contains("\n ") || text.trim().is_empty());
}
#[test]
fn reader_mismatched_tag_is_error() {
let mut r = XmlReader::new("<a></b>").unwrap();
assert!(r.has_error().unwrap());
assert!(!r.read_next_start_element().unwrap());
}
#[test]
fn reader_unclosed_element_is_error() {
let r = XmlReader::new("<a>").unwrap();
assert!(r.has_error().unwrap());
}
#[test]
fn reader_empty_document_is_error() {
let r = XmlReader::new("").unwrap();
assert!(r.has_error().unwrap());
}
#[test]
fn reader_undefined_entity_is_error() {
let r = XmlReader::new("<a>&foo;</a>").unwrap();
assert!(r.has_error().unwrap());
}
#[test]
fn reader_attribute_out_of_range_is_not_found() {
let mut r = XmlReader::new("<a x=\"1\"></a>").unwrap();
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.attribute_count().unwrap(), 1);
assert!(r.attribute_name(5).is_err());
assert!(r.attribute_name(-1).is_err());
assert!(r.attribute_value(1).is_err());
}
#[test]
fn reader_name_before_read_is_empty() {
let r = XmlReader::new("<a></a>").unwrap();
assert_eq!(r.name().unwrap(), "");
assert_eq!(r.attribute_count().unwrap(), 0);
}
#[test]
fn round_trip_writer_then_reader() {
let mut w = XmlWriter::new();
w.write_start_element("root").unwrap();
w.write_text_element("a", "1").unwrap();
w.write_text_element("b", "2").unwrap();
w.write_end_element().unwrap();
let xml = w.output().unwrap();
assert_eq!(xml, "<root><a>1</a><b>2</b></root>");
let mut r = XmlReader::new(&xml).unwrap();
assert!(!r.has_error().unwrap());
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "root");
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "a");
assert_eq!(r.read_element_text().unwrap(), "1");
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "b");
assert_eq!(r.read_element_text().unwrap(), "2");
assert!(!r.read_next_start_element().unwrap());
}
#[test]
fn reader_attribute_order_preserved() {
let doc = "<e zeta=\"1\" alpha=\"2\" mid=\"3\" omega=\"4\"/>";
let mut r = XmlReader::new(doc).unwrap();
assert!(!r.has_error().unwrap());
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.attribute_count().unwrap(), 4);
for (i, name) in ["zeta", "alpha", "mid", "omega"].iter().enumerate() {
assert_eq!(r.attribute_name(i as i32).unwrap(), *name);
assert_eq!(r.attribute_value(i as i32).unwrap(), format!("{}", i + 1));
}
}
#[test]
fn reader_single_quoted_attribute() {
let doc = "<e a='v1' b='x"y'/>";
let mut r = XmlReader::new(doc).unwrap();
assert!(!r.has_error().unwrap());
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.attribute_value(0).unwrap(), "v1");
assert_eq!(r.attribute_value(1).unwrap(), "x\"y");
}
#[test]
fn reader_numeric_character_references() {
let doc = "<e a=\"AB\">CD</e>";
let mut r = XmlReader::new(doc).unwrap();
assert!(!r.has_error().unwrap());
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.attribute_value(0).unwrap(), "AB");
assert_eq!(r.read_element_text().unwrap(), "CD");
}
#[test]
fn reader_predefined_entities() {
let doc = "<e>&<>"'</e>";
let mut r = XmlReader::new(doc).unwrap();
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.read_element_text().unwrap(), "&<>\"'");
}
#[test]
fn reader_duplicate_attribute_is_error() {
let r = XmlReader::new("<e a=\"1\" a=\"2\"/>").unwrap();
assert!(r.has_error().unwrap());
}
#[test]
fn reader_lt_in_attribute_value_is_error() {
let r = XmlReader::new("<e a=\"x<y\"/>").unwrap();
assert!(r.has_error().unwrap());
}
#[test]
fn reader_text_merges_across_entity_references() {
let doc = "<e>one & two < three</e>";
let mut r = XmlReader::new(doc).unwrap();
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.read_element_text().unwrap(), "one & two < three");
}
#[test]
fn reader_cdata_merges_with_adjacent_text() {
let doc = "<e>plain<![CDATA[<raw&>]]>tail</e>";
let mut r = XmlReader::new(doc).unwrap();
assert!(!r.has_error().unwrap());
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.read_element_text().unwrap(), "plain<raw&>tail");
}
#[test]
fn reader_comment_inside_text() {
let doc = "<e>ab<!-- hidden -->cd</e>";
let mut r = XmlReader::new(doc).unwrap();
assert!(!r.has_error().unwrap());
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.read_element_text().unwrap(), "abcd");
}
#[test]
fn reader_whitespace_outside_root_is_ignored() {
let doc = " \n\t<a>x</a>\n ";
let mut r = XmlReader::new(doc).unwrap();
assert!(!r.has_error().unwrap());
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "a");
assert_eq!(r.read_element_text().unwrap(), "x");
}
#[test]
fn reader_text_before_root_is_error() {
let r = XmlReader::new("junk<a/>").unwrap();
assert!(r.has_error().unwrap());
}
#[test]
fn reader_text_after_root_is_error() {
let r = XmlReader::new("<a/>junk").unwrap();
assert!(r.has_error().unwrap());
}
#[test]
fn reader_second_root_element_is_error() {
let r = XmlReader::new("<a/><b/>").unwrap();
assert!(r.has_error().unwrap());
let r = XmlReader::new("<a></a><b></b>").unwrap();
assert!(r.has_error().unwrap());
}
#[test]
fn reader_comment_and_pi_after_root_ok() {
let doc = "<a/><!-- tail --><?pi data?>";
let mut r = XmlReader::new(doc).unwrap();
assert!(!r.has_error().unwrap());
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "a");
assert!(!r.read_next_start_element().unwrap());
}
#[test]
fn reader_xml_declaration_and_doctype_skipped() {
let doc = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE root><root>v</root>";
let mut r = XmlReader::new(doc).unwrap();
assert!(!r.has_error().unwrap());
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "root");
assert_eq!(r.read_element_text().unwrap(), "v");
}
#[test]
fn reader_read_element_text_off_start_element_is_empty() {
let mut r = XmlReader::new("<a>x</a>").unwrap();
assert_eq!(r.read_element_text().unwrap(), "");
let mut r = XmlReader::new("<a><b/></a>").unwrap();
assert!(r.read_next_start_element().unwrap()); assert!(r.read_next_start_element().unwrap()); assert!(!r.read_next_start_element().unwrap()); assert_eq!(r.read_element_text().unwrap(), "");
}
#[test]
fn reader_skip_off_start_element_is_noop() {
let mut r = XmlReader::new("<a><b/></a>").unwrap();
r.skip_current_element().unwrap();
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "a");
}
#[test]
fn reader_skip_nested_same_name() {
let doc = "<root><a><a><a>deep</a></a></a><b>v</b></root>";
let mut r = XmlReader::new(doc).unwrap();
assert!(r.read_next_start_element().unwrap()); assert!(r.read_next_start_element().unwrap()); r.skip_current_element().unwrap();
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "b");
}
#[test]
fn reader_read_element_text_on_self_closing() {
let doc = "<a><b/></a>";
let mut r = XmlReader::new(doc).unwrap();
assert!(r.read_next_start_element().unwrap());
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "b");
assert_eq!(r.read_element_text().unwrap(), "");
assert!(!r.read_next_start_element().unwrap());
}
#[test]
fn reader_read_next_start_element_stops_at_end_element() {
let doc = "<a><b><c/></b></a>";
let mut r = XmlReader::new(doc).unwrap();
assert!(r.read_next_start_element().unwrap()); assert!(r.read_next_start_element().unwrap()); assert!(r.read_next_start_element().unwrap()); assert!(!r.read_next_start_element().unwrap());
}
#[test]
fn reader_mixed_content_direct_text_in_order() {
let doc = "<p>t1<b>x</b>t2<i>y</i>t3</p>";
let mut r = XmlReader::new(doc).unwrap();
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.read_element_text().unwrap(), "t1t2t3");
}
#[test]
fn reader_error_state_is_sticky() {
let mut r = XmlReader::new("<a></b>").unwrap();
assert!(r.has_error().unwrap());
assert!(!r.read_next_start_element().unwrap());
assert!(!r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "");
assert_eq!(r.attribute_count().unwrap(), 0);
}
#[test]
fn reader_malformed_matrix() {
let cases = [
"", " \n ", "<a>", "<a><b></a></b>", "<a></a></a>", "<a></>", "<a>< /a>", "<a", "<a b>", "<a b=1>", "<a b=\"v>", "<a></a", "<a><!-- c</a>", "<a><![CDATA[x</a>", "<?pi<a></a>", "<a>&undefined;</a>", "<a>&#xZZ;</a>", "<a>�</a>", "<a b=\"&foo;\"/>", "<a/>tail", "<a/><b/>", "<a a=\"1\" a=\"2\"/>", "<a b=\"<\"/>", "<a/ ><b/>", ];
for doc in cases {
let r = XmlReader::new(doc).unwrap();
assert!(r.has_error().unwrap(), "expected error for {:?}", doc);
}
}
#[test]
fn reader_wellformed_matrix() {
let cases = [
"<a/>",
"<a></a>",
"<a>text</a>",
"<a b=\"v\" c='w'/>",
"<a><b><c>deep</c></b></a>",
"<?xml version=\"1.0\"?><a/>",
"<!-- c --><a/><!-- c -->",
"<a><![CDATA[x]]></a>",
"<a>&AA</a>",
"<a\n\tb = \"v\"\n/>", "<a></a >", "<a>x</a >",
" \n<a> </a>\n ",
"<ns:a xmlns:ns=\"urn:x\" ns:b=\"v\"/>", "<a>]]</a>", ];
for doc in cases {
let r = XmlReader::new(doc).unwrap();
assert!(!r.has_error().unwrap(), "unexpected error for {:?}", doc);
}
}
#[test]
fn writer_end_document_auto_closes() {
let mut w = XmlWriter::new();
w.write_start_element("a").unwrap();
w.write_start_element("b").unwrap();
w.write_characters("t").unwrap();
w.write_start_element("c").unwrap();
w.write_end_document().unwrap();
assert_eq!(w.output().unwrap(), "<a><b>t<c/></b></a>");
}
#[test]
fn writer_attribute_after_text_is_dropped() {
let mut w = XmlWriter::new();
w.write_start_element("a").unwrap();
w.write_characters("x").unwrap();
w.write_attribute("late", "v").unwrap();
w.write_end_element().unwrap();
assert_eq!(w.output().unwrap(), "<a>x</a>");
}
#[test]
fn writer_escaping_matrix() {
let mut w = XmlWriter::new();
w.write_start_element("a").unwrap();
w.write_attribute("k", "&<>\"'\n").unwrap();
w.write_characters("&<>\"'\t").unwrap();
w.write_end_element().unwrap();
assert_eq!(
w.output().unwrap(),
"<a k=\"&<>"'\n\">&<>\"'\t</a>"
);
}
#[test]
fn writer_extra_end_element_noop_then_continue() {
let mut w = XmlWriter::new();
w.write_start_element("a").unwrap();
w.write_end_element().unwrap();
w.write_end_element().unwrap(); w.write_start_element("b").unwrap();
w.write_end_document().unwrap();
assert_eq!(w.output().unwrap(), "<a/><b/>");
}
#[test]
fn writer_text_element_empty_text() {
let mut w = XmlWriter::new();
w.write_text_element("a", "").unwrap();
assert_eq!(w.output().unwrap(), "<a></a>");
}
#[test]
fn writer_text_element_inside_open_tag() {
let mut w = XmlWriter::new();
w.write_start_element("root").unwrap();
w.write_attribute("v", "1").unwrap();
w.write_text_element("child", "x").unwrap();
w.write_end_document().unwrap();
assert_eq!(w.output().unwrap(), "<root v=\"1\"><child>x</child></root>");
}
#[test]
fn writer_characters_outside_any_element() {
let mut w = XmlWriter::new();
w.write_characters("a & b").unwrap();
assert_eq!(w.output().unwrap(), "a & b");
}
#[test]
fn round_trip_special_characters() {
let tricky = "a & b < c > d \"e\" 'f'\n\tg";
let mut w = XmlWriter::new();
w.write_start_element("root").unwrap();
w.write_attribute("attr", tricky).unwrap();
w.write_text_element("child", tricky).unwrap();
w.write_end_document().unwrap();
let xml = w.output().unwrap();
let mut r = XmlReader::new(&xml).unwrap();
assert!(!r.has_error().unwrap());
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.name().unwrap(), "root");
assert_eq!(r.attribute_count().unwrap(), 1);
assert_eq!(r.attribute_name(0).unwrap(), "attr");
let normalized_attr = "a & b < c > d \"e\" 'f' g";
assert_eq!(r.attribute_value(0).unwrap(), normalized_attr);
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.read_element_text().unwrap(), tricky);
}
#[test]
fn round_trip_utf8() {
let text = "héllo wörld — 中文";
let mut w = XmlWriter::new();
w.write_text_element("t", text).unwrap();
let xml = w.output().unwrap();
let mut r = XmlReader::new(&xml).unwrap();
assert!(!r.has_error().unwrap());
assert!(r.read_next_start_element().unwrap());
assert_eq!(r.read_element_text().unwrap(), text);
}
}