use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;
use crate::bindings::*;
use crate::readonly::RoNode;
use crate::tree::{Document, NodeType};
pub struct TextReader {
ptr: xmlTextReaderPtr,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReaderEvent {
None,
Element,
Attribute,
Text,
CData,
EntityReference,
Entity,
ProcessingInstruction,
Comment,
Document,
DocumentType,
DocumentFragment,
Notation,
Whitespace,
SignificantWhitespace,
EndElement,
EndEntity,
XmlDeclaration,
}
impl ReaderEvent {
fn from_int(t: i32) -> Self {
match t {
1 => ReaderEvent::Element,
2 => ReaderEvent::Attribute,
3 => ReaderEvent::Text,
4 => ReaderEvent::CData,
5 => ReaderEvent::EntityReference,
6 => ReaderEvent::Entity,
7 => ReaderEvent::ProcessingInstruction,
8 => ReaderEvent::Comment,
9 => ReaderEvent::Document,
10 => ReaderEvent::DocumentType,
11 => ReaderEvent::DocumentFragment,
12 => ReaderEvent::Notation,
13 => ReaderEvent::Whitespace,
14 => ReaderEvent::SignificantWhitespace,
15 => ReaderEvent::EndElement,
16 => ReaderEvent::EndEntity,
17 => ReaderEvent::XmlDeclaration,
_ => ReaderEvent::None,
}
}
}
impl Drop for TextReader {
fn drop(&mut self) {
unsafe { xmlFreeTextReader(self.ptr) };
}
}
fn const_xmlchar_to_string(ptr: *const xmlChar) -> Option<String> {
if ptr.is_null() {
return None;
}
Some(
unsafe { CStr::from_ptr(ptr as *const c_char) }
.to_string_lossy()
.into_owned(),
)
}
fn read_status(rc: i32) -> Result<bool, ()> {
match rc {
1 => Ok(true),
0 => Ok(false),
_ => Err(()),
}
}
impl TextReader {
pub fn from_file(path: &str, options: i32) -> Result<Self, ()> {
let c_path = CString::new(path).map_err(|_| ())?;
let ptr = unsafe { xmlReaderForFile(c_path.as_ptr(), ptr::null(), options) };
if ptr.is_null() {
Err(())
} else {
Ok(TextReader { ptr })
}
}
pub fn read(&mut self) -> Result<bool, ()> {
read_status(unsafe { xmlTextReaderRead(self.ptr) })
}
pub fn read_next(&mut self) -> Result<bool, ()> {
read_status(unsafe { xmlTextReaderNext(self.ptr) })
}
pub fn node_type(&self) -> Option<NodeType> {
let t = unsafe { xmlTextReaderNodeType(self.ptr) };
if (1..=12).contains(&t) {
NodeType::from_int(t as xmlElementType)
} else {
None
}
}
pub fn is_element(&self) -> bool {
self.node_type() == Some(NodeType::ElementNode)
}
pub fn event(&self) -> ReaderEvent {
ReaderEvent::from_int(unsafe { xmlTextReaderNodeType(self.ptr) })
}
pub fn depth(&self) -> i32 {
unsafe { xmlTextReaderDepth(self.ptr) }
}
pub fn local_name(&self) -> Option<String> {
const_xmlchar_to_string(unsafe { xmlTextReaderConstLocalName(self.ptr) })
}
pub fn namespace_uri(&self) -> Option<String> {
const_xmlchar_to_string(unsafe { xmlTextReaderConstNamespaceUri(self.ptr) })
}
pub fn expand(&self) -> Option<RoNode> {
self.current_subtree().map(RoNode)
}
fn current_subtree(&self) -> Option<xmlNodePtr> {
let node = unsafe { xmlTextReaderExpand(self.ptr) };
(!node.is_null()).then_some(node)
}
pub fn expand_to_document(&self) -> Option<Document> {
let node = self.current_subtree()?;
unsafe {
let newdoc = xmlNewDoc(c"1.0".as_ptr() as *const xmlChar);
if newdoc.is_null() {
return None;
}
let ctxt = xmlDOMWrapNewCtxt();
let mut cloned: xmlNodePtr = ptr::null_mut();
let src_doc = (*node).doc;
let rc = xmlDOMWrapCloneNode(
ctxt,
src_doc,
node,
&mut cloned,
newdoc,
ptr::null_mut(), 1, 0, );
xmlDOMWrapFreeCtxt(ctxt);
if rc != 0 || cloned.is_null() {
xmlFreeDoc(newdoc);
return None;
}
xmlDocSetRootElement(newdoc, cloned);
xmlReconciliateNs(newdoc, cloned);
let mut decl = (*cloned).nsDef;
while !decl.is_null() {
let prefix = (*decl).prefix;
if !prefix.is_null()
&& xmlStrncmp(prefix, c"default".as_ptr() as *const xmlChar, 7) == 0
{
let src_ns = xmlSearchNsByHref(src_doc, node, (*decl).href);
if !src_ns.is_null() {
let want = (*src_ns).prefix;
let mut clash = false;
let mut other = (*cloned).nsDef;
while !other.is_null() {
if other != decl && xmlStrEqual((*other).prefix, want) == 1 {
clash = true;
break;
}
other = (*other).next;
}
if !clash && xmlStrEqual(prefix, want) == 0 {
let old = (*decl).prefix as *mut ::std::os::raw::c_void;
(*decl).prefix = if want.is_null() {
ptr::null()
} else {
xmlStrdup(want)
};
crate::c_helpers::bindgenFree(old);
}
}
}
decl = (*decl).next;
}
Some(Document::new_ptr(newdoc))
}
}
pub fn attributes_qname(&mut self) -> Vec<(String, String)> {
let mut out = Vec::new();
if unsafe { xmlTextReaderMoveToFirstAttribute(self.ptr) } != 1 {
return out;
}
loop {
let name = const_xmlchar_to_string(unsafe { xmlTextReaderConstName(self.ptr) });
let value = const_xmlchar_to_string(unsafe { xmlTextReaderConstValue(self.ptr) });
if let (Some(n), Some(v)) = (name, value) {
out.push((n, v));
}
if unsafe { xmlTextReaderMoveToNextAttribute(self.ptr) } != 1 {
break;
}
}
unsafe { xmlTextReaderMoveToElement(self.ptr) };
out
}
pub fn value(&self) -> Option<String> {
const_xmlchar_to_string(unsafe { xmlTextReaderConstValue(self.ptr) })
}
pub fn is_empty_element(&self) -> bool {
(unsafe { xmlTextReaderIsEmptyElement(self.ptr) }) == 1
}
pub fn outer_xml(&self) -> Option<String> {
let node = self.current_subtree()?;
unsafe {
let buf = xmlBufferCreate();
if buf.is_null() {
return None;
}
let rc = xmlNodeDump(buf, (*node).doc, node, 0, 0);
let content = xmlBufferContent(buf);
let result = if rc < 0 || content.is_null() {
None
} else {
Some(
CStr::from_ptr(content as *const c_char)
.to_string_lossy()
.into_owned(),
)
};
xmlBufferFree(buf);
result
}
}
pub fn read_to_next<F>(&mut self, want: F) -> Result<bool, ()>
where
F: Fn(Option<&str>, &str) -> bool,
{
while self.read()? {
if self.is_element()
&& let Some(name) = self.local_name()
&& want(self.namespace_uri().as_deref(), &name)
{
return Ok(true);
}
}
Ok(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
const NS: &str = "http://example.org/ns";
fn write_temp(name: &str, xml: &str) -> String {
let path = std::env::temp_dir().join(format!(
"rust-libxml-reader-{}-{name}.xml",
std::process::id()
));
std::fs::write(&path, xml).unwrap();
path.to_string_lossy().into_owned()
}
#[test]
fn stream_sections_owned_and_namespace_reconciled() {
let xml = r#"<?xml version="1.0"?>
<doc xmlns="http://example.org/ns" xmlns:x="http://example.org/x">
<meta>skip me</meta>
<section id="a"><title>Alpha</title><p>one</p></section>
<section id="b"><title>Beta</title><x:note>hi</x:note></section>
</doc>"#;
let path = write_temp("sections", xml);
let mut sections = Vec::new();
{
let mut reader = TextReader::from_file(&path, 0).unwrap();
while reader
.read_to_next(|ns, name| ns == Some(NS) && name == "section")
.unwrap()
{
sections.push(reader.expand_to_document().unwrap());
}
}
assert_eq!(sections.len(), 2, "should stream exactly two <section>s");
let root0 = sections[0].get_root_element().unwrap();
assert_eq!(root0.get_name(), "section");
assert_eq!(root0.get_attribute("id").as_deref(), Some("a"));
assert_eq!(
root0.get_namespace().map(|n| n.get_href()),
Some(NS.to_string()),
"default namespace must be reconciled onto the detached copy"
);
let s0 = sections[0].to_string();
assert!(
s0.contains("http://example.org/ns"),
"ns decl missing: {s0}"
);
assert!(
!s0.contains("default:"),
"default-namespace content must keep a NULL prefix, not a minted default: — {s0}"
);
assert!(
s0.contains(r#"<section xmlns="http://example.org/ns""#),
"the default declaration must materialize on the copy root: {s0}"
);
assert!(
s0.contains("Alpha") && s0.contains("one"),
"content lost: {s0}"
);
let s1 = sections[1].to_string();
assert!(s1.contains("Beta"), "content lost: {s1}");
assert!(
s1.contains("http://example.org/x"),
"prefixed ns lost: {s1}"
);
std::fs::remove_file(&path).ok();
}
#[test]
fn read_and_next_skip_subtree() {
let xml = r#"<r><a><deep/></a><b/></r>"#;
let path = write_temp("skip", xml);
let mut reader = TextReader::from_file(&path, 0).unwrap();
assert!(reader.read().unwrap()); assert!(reader.is_element());
assert_eq!(reader.local_name().as_deref(), Some("r"));
assert!(reader.read().unwrap()); assert_eq!(reader.local_name().as_deref(), Some("a"));
assert!(reader.read_next().unwrap());
assert_eq!(reader.local_name().as_deref(), Some("b"));
std::fs::remove_file(&path).ok();
}
#[test]
fn from_file_on_missing_path_is_err() {
assert!(TextReader::from_file("/no/such/rust-libxml-reader-missing.xml", 0).is_err());
}
#[test]
fn read_surfaces_parse_error_on_malformed_xml() {
let path = write_temp("malformed", "<a><b></a>");
let mut reader = TextReader::from_file(&path, 0).unwrap();
let mut saw_err = false;
loop {
match reader.read() {
Ok(true) => continue,
Ok(false) => break,
Err(()) => {
saw_err = true;
break;
}
}
}
assert!(
saw_err,
"malformed XML must surface a read error, not Ok(false)"
);
std::fs::remove_file(&path).ok();
}
#[test]
fn read_to_next_returns_false_when_pattern_absent() {
let path = write_temp("nomatch", r#"<doc><a/><b/></doc>"#);
let mut reader = TextReader::from_file(&path, 0).unwrap();
let found = reader.read_to_next(|_ns, name| name == "zzz").unwrap();
assert!(
!found,
"no <zzz> exists → read_to_next must reach EOF and return false"
);
std::fs::remove_file(&path).ok();
}
#[test]
fn attributes_qname_in_order_without_expand() {
let xml = r#"<doc xmlns="http://example.org/ns" xmlns:x="http://example.org/x">
<section xml:id="s1" class="c" x:extra="e"><p>body</p></section>
</doc>"#;
let path = write_temp("attrs", xml);
let mut reader = TextReader::from_file(&path, 0).unwrap();
assert!(reader.read().unwrap()); let root_attrs = reader.attributes_qname();
assert_eq!(
root_attrs,
vec![
("xmlns".to_string(), "http://example.org/ns".to_string()),
("xmlns:x".to_string(), "http://example.org/x".to_string()),
],
"namespace declarations must be reported as ordinary attributes"
);
assert_eq!(reader.local_name().as_deref(), Some("doc"));
assert!(
reader
.read_to_next(|_, name| name == "section")
.unwrap()
);
assert_eq!(
reader.attributes_qname(),
vec![
("xml:id".to_string(), "s1".to_string()),
("class".to_string(), "c".to_string()),
("x:extra".to_string(), "e".to_string()),
],
"attribute order must be document order, names fully qualified"
);
std::fs::remove_file(&path).ok();
}
#[test]
fn outer_xml_preserves_default_namespace_content() {
let xml = r#"<doc xmlns="http://example.org/ns" xmlns:x="http://example.org/x">
<section a="1" b="<2>"><p>t&t</p><x:note>hi</x:note></section>
</doc>"#;
let path = write_temp("outerxml", xml);
let mut reader = TextReader::from_file(&path, 0).unwrap();
assert!(
reader
.read_to_next(|_, name| name == "section")
.unwrap()
);
let outer = reader.outer_xml().unwrap();
assert_eq!(
outer,
r#"<section a="1" b="<2>"><p>t&t</p><x:note>hi</x:note></section>"#,
"no default: prefix, no added xmlns decls, escaping and attr order intact"
);
assert_eq!(reader.local_name().as_deref(), Some("section"));
assert!(reader.read_next().unwrap()); std::fs::remove_file(&path).ok();
}
#[test]
fn value_and_is_empty_element() {
let xml = r#"<r><?pi data?><!--note--><a/><b></b>text</r>"#;
let path = write_temp("value", xml);
let mut reader = TextReader::from_file(&path, 0).unwrap();
assert!(reader.read().unwrap()); assert!(!reader.is_empty_element());
assert!(reader.read().unwrap()); assert_eq!(reader.node_type(), Some(NodeType::PiNode));
assert_eq!(reader.local_name().as_deref(), Some("pi"));
assert_eq!(reader.value().as_deref(), Some("data"));
assert!(reader.read().unwrap()); assert_eq!(reader.node_type(), Some(NodeType::CommentNode));
assert_eq!(reader.value().as_deref(), Some("note"));
assert!(reader.read().unwrap()); assert!(reader.is_empty_element());
assert!(reader.read().unwrap()); assert!(!reader.is_empty_element());
assert!(reader.read().unwrap());
assert!(reader.read().unwrap()); assert_eq!(reader.node_type(), Some(NodeType::TextNode));
assert_eq!(reader.value().as_deref(), Some("text"));
std::fs::remove_file(&path).ok();
}
#[test]
fn node_type_distinguishes_open_from_close_tag() {
let path = write_temp("openclose", r#"<r><a>x</a></r>"#);
let mut reader = TextReader::from_file(&path, 0).unwrap();
assert!(reader.read().unwrap()); assert_eq!(reader.node_type(), Some(NodeType::ElementNode));
assert!(reader.is_element());
assert!(reader.read().unwrap()); assert_eq!(reader.node_type(), Some(NodeType::ElementNode));
assert!(reader.read().unwrap()); assert_eq!(reader.node_type(), Some(NodeType::TextNode));
assert!(!reader.is_element());
assert!(reader.read().unwrap()); assert_eq!(reader.local_name().as_deref(), Some("a"));
assert_eq!(
reader.node_type(),
None,
"a closing tag has no NodeType equivalent — must be None, not ElementDecl"
);
assert!(!reader.is_element());
std::fs::remove_file(&path).ok();
}
}