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,
}
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 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);
Some(Document::new_ptr(newdoc))
}
}
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("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 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();
}
}