use std::fmt::Write as _;
use std::io::{BufReader, Read};
use oxml::stream::{Event, Reader};
struct Trickle {
data: Vec<u8>,
at: usize,
n: usize,
}
impl Read for Trickle {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let take = self.n.min(buf.len()).min(self.data.len() - self.at);
buf[..take].copy_from_slice(&self.data[self.at..self.at + take]);
self.at += take;
Ok(take)
}
}
fn trickle(data: &str, n: usize) -> BufReader<Trickle> {
BufReader::with_capacity(
1,
Trickle {
data: data.as_bytes().to_vec(),
at: 0,
n,
},
)
}
fn described(
mut next: impl FnMut() -> Result<Option<Event>, oxml::Error>,
) -> Result<Vec<String>, oxml::Error> {
let mut out = Vec::new();
while let Some(event) = next()? {
out.push(match event {
Event::StartElement { name, attributes } => {
let mut s = format!("<{}", name.local);
for (n, v) in &attributes {
let _ = write!(s, " {}={v:?}", n.local);
}
s.push('>');
s
}
Event::EndElement { name } => format!("</{}>", name.local),
Event::Text(t) => format!("text {t:?}"),
Event::Comment(c) => format!("comment {c:?}"),
Event::ProcessingInstruction { target, data } => {
format!("pi {target} {data:?}")
}
_ => "?".to_owned(),
});
}
Ok(out)
}
fn in_memory(doc: &str) -> Result<Vec<String>, oxml::Error> {
let mut r = Reader::new(doc)?;
described(move || r.next_event())
}
fn from_bytes(doc: &str, chunk: usize) -> Result<Vec<String>, oxml::Error> {
let mut r = Reader::from_reader(trickle(doc, chunk))?;
described(move || r.next_event())
}
const DOCS: &[&str] = &[
"<a/>",
"<a><b>text</b></a>",
"<a><b/><c/></a>",
r#"<r xmlns:m="urn:u"><m:a k="1">x</m:a></r>"#,
"<a>one & two</a>",
"<a><![CDATA[<not markup>]]></a>",
"<!-- before --><a/><!-- after -->",
"<?xml version='1.0'?><a><b>1</b></a>",
"<!DOCTYPE a [<!ENTITY e \"expanded\">]><a>&e;</a>",
"<a>line one\nline two\r\nline three\rline four</a>",
"<a x=' spaced ' y=\"tab\there\"/>",
"<a>\u{1F600} unicode \u{4e2d}\u{6587}</a>",
];
#[test]
fn a_streamed_read_matches_an_in_memory_one() {
for doc in DOCS {
let expected = in_memory(doc).unwrap_or_else(|e| {
panic!("in-memory reader refused {doc:?}: {e}")
});
for chunk in [1, 2, 3, 7, 64, 8192] {
let got = from_bytes(doc, chunk).unwrap_or_else(|e| {
panic!("streamed reader refused {doc:?} at chunk {chunk}: {e}")
});
assert_eq!(got, expected, "{doc:?} at chunk size {chunk}");
}
}
}
#[test]
fn a_streamed_read_fails_where_an_in_memory_one_does() {
for doc in [
"<a>",
"<a></b>",
"<a x='1' x='2'/>",
"</a>",
"<a/><b/>",
"<a>&undefined;</a>",
"<a x=1/>",
] {
let memory = in_memory(doc).expect_err("invalid");
for chunk in [1, 3, 8192] {
let streamed = from_bytes(doc, chunk).expect_err("invalid");
assert_eq!(
(&streamed.kind, streamed.offset),
(&memory.kind, memory.offset),
"{doc:?} at chunk {chunk}: offsets are the document's, \
not the buffer's"
);
}
}
}
#[test]
fn a_document_larger_than_the_buffer_is_read() {
use std::fmt::Write as _;
let mut doc = String::from("<catalogue>");
for i in 0..20_000 {
let _ = write!(doc, "<item id=\"i{i}\">value {i}</item>");
}
doc.push_str("</catalogue>");
let mut reader = Reader::from_reader(trickle(&doc, 64)).expect("valid");
let mut items = 0usize;
while let Some(event) = reader.next_event().expect("valid") {
if let Event::StartElement { name, .. } = event {
if name.local == "item" {
items += 1;
}
}
}
assert_eq!(
items,
20_000,
"{} KB read 64 bytes at a time",
doc.len() / 1024
);
}
#[test]
fn a_line_ending_split_across_a_read_is_one_ending() {
let doc = "<a>one\r\ntwo\r\nthree</a>";
let expected = in_memory(doc).expect("valid");
for chunk in 1..12 {
assert_eq!(
from_bytes(doc, chunk).expect("valid"),
expected,
"a CRLF pair split at chunk size {chunk}"
);
}
}
#[test]
fn a_character_split_across_a_read_survives() {
let doc = "<a>\u{1F600}</a>";
let expected = in_memory(doc).expect("valid");
for chunk in 1..8 {
assert_eq!(
from_bytes(doc, chunk).expect("valid"),
expected,
"an emoji split at chunk size {chunk}"
);
}
}
#[test]
fn standalone_is_read_after_line_endings_are_normalised() {
for separator in ['\u{85}', '\u{2028}', '\n'] {
let doc = format!(
"<?xml version=\"1.1\" standalone{separator}=\"yes\"?>\
<!DOCTYPE a SYSTEM \"x.dtd\"><a>&e;</a>"
);
let streamed = in_memory(&doc).expect_err("undeclared entity");
assert!(
matches!(streamed.kind, oxml::ErrorKind::UnknownEntity(ref n) if n == "e"),
"separator {separator:?} gave {streamed:?}"
);
let tree = oxml::parse(&doc).expect_err("undeclared entity");
assert_eq!(tree.kind, streamed.kind, "separator {separator:?}");
assert_eq!(
from_bytes(&doc, 3).expect_err("undeclared entity").kind,
streamed.kind,
"separator {separator:?}"
);
}
}