Skip to main content

libxml_rs/xml/reader/
mod.rs

1//! XML Reader API (§30, §85 Phase 7).
2//!
3//! Cursor-based streaming reader with node type, depth, attribute traversal,
4//! namespace lookup, value retrieval, validation integration.
5//!
6//! Implements the `xmlTextReader` API from libxml2, which provides a
7//! cursor-based streaming interface for reading XML documents. The reader
8//! parses the entire document into a tree on the first `Read()` call, then
9//! walks the tree in document order (depth-first traversal) generating
10//! node events for elements, text, comments, PIs, etc.
11//!
12//! # UPSTREAM-PARITY
13//!
14//! The reader API is defined in `libxml/xmlreader.h` and `libxml/xmlreader.c`.
15//! Key differences from upstream:
16//!
17//! - The reader parses the full document on first Read rather than using
18//!   a true streaming/event-driven parser. This simplifies the implementation
19//!   while preserving the observable API surface.
20//! - Pattern-based reader operations (xmlTextReaderPreservePattern, etc.)
21//!   are not yet implemented.
22
23#![allow(
24    missing_docs,
25    non_snake_case,
26    non_camel_case_types,
27    non_upper_case_globals
28)]
29
30use core::ffi::c_void;
31use core::ptr;
32use std::os::raw::{c_char, c_int};
33
34use crate::abi::allocator::xmlFree;
35use crate::abi::callbacks::{xmlInputCloseCallback, xmlInputReadCallback};
36use crate::abi::structs::{_xmlAttr, _xmlDoc, _xmlNode, _xmlParserCtxt, _xmlParserInputBuffer};
37
38use crate::abi::types::xmlElementType::*;
39use crate::abi::types::*;
40use crate::xml::parser::helpers::{
41    create_parser_ctxt, free_parser_ctxt, input_from_file, input_from_io, input_from_memory,
42    parse_document, setup_parser_input,
43};
44use crate::xml::parser::input::InputBuffer;
45use crate::xml::string::{bytes_to_xmlstr, xml_strdup, xmlstr_to_bytes, xmlstr_to_string};
46use crate::xml::tree;
47
48// ═══════════════════════════════════════════════════════════════════════════════
49// Reader Types (xmlreader.h)
50// ═══════════════════════════════════════════════════════════════════════════════
51
52/// Reader node types (xmlReaderTypes enum).
53///
54/// # UPSTREAM-PARITY
55///
56/// ```c
57/// typedef enum {
58///     XML_TEXTREADER_NONE = 0,
59///     XML_TEXTREADER_ELEMENT = 1,
60///     XML_TEXTREADER_ATTRIBUTE = 2,
61///     XML_TEXTREADER_TEXT = 3,
62///     XML_TEXTREADER_CDATA = 4,
63///     XML_TEXTREADER_ENTITY_REFERENCE = 5,
64///     XML_TEXTREADER_ENTITY = 6,
65///     XML_TEXTREADER_PROCESSING_INSTRUCTION = 7,
66///     XML_TEXTREADER_COMMENT = 8,
67///     XML_TEXTREADER_DOCUMENT = 9,
68///     XML_TEXTREADER_DOCUMENT_TYPE = 10,
69///     XML_TEXTREADER_DOCUMENT_FRAGMENT = 11,
70///     XML_TEXTREADER_NOTATION = 12,
71///     XML_TEXTREADER_WHITESPACE = 13,
72///     XML_TEXTREADER_SIGNIFICANT_WHITESPACE = 14,
73///     XML_TEXTREADER_END_ELEMENT = 15,
74///     XML_TEXTREADER_END_ENTITY = 16,
75///     XML_TEXTREADER_XML_DECLARATION = 17
76/// } xmlReaderTypes;
77/// ```
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79#[repr(i32)]
80pub(crate) enum ReaderNodeType {
81    NONE = 0,
82    ELEMENT = 1,
83    ATTRIBUTE = 2,
84    TEXT = 3,
85    CDATA = 4,
86    ENTITY_REFERENCE = 5,
87    ENTITY = 6,
88    PROCESSING_INSTRUCTION = 7,
89    COMMENT = 8,
90    DOCUMENT = 9,
91    DOCUMENT_TYPE = 10,
92    DOCUMENT_FRAGMENT = 11,
93    NOTATION = 12,
94    WHITESPACE = 13,
95    SIGNIFICANT_WHITESPACE = 14,
96    END_ELEMENT = 15,
97    END_ENTITY = 16,
98    XML_DECLARATION = 17,
99}
100
101/// Reader read state (xmlTextReaderReadState enum).
102///
103/// # UPSTREAM-PARITY
104///
105/// ```c
106/// typedef enum {
107///     XML_TEXTREADER_NOT_INITIALIZED = 0,
108///     XML_TEXTREADER_INITIALIZED = 1,
109///     XML_TEXTREADER_READING = 2,
110///     XML_TEXTREADER_EOF = 3,
111///     XML_TEXTREADER_CLOSED = 4,
112///     XML_TEXTREADER_ERROR = 5
113/// } xmlTextReaderReadState;
114/// ```
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116#[repr(i32)]
117pub(crate) enum ReadState {
118    NOT_INITIALIZED = 0,
119    INITIALIZED = 1,
120    READING = 2,
121    EOF = 3,
122    CLOSED = 4,
123    ERROR = 5,
124}
125
126/// Parser properties for xmlTextReaderGetParserProp / SetParserProp.
127///
128/// # UPSTREAM-PARITY
129///
130/// ```c
131/// typedef enum {
132///     XML_PARSER_LOADDTD = 1,
133///     XML_PARSER_DEFAULTATTRS = 2,
134///     XML_PARSER_VALIDATE = 3,
135///     XML_PARSER_SUBST_ENTITIES = 4
136/// } xmlParserProperties;
137/// ```
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139#[repr(i32)]
140pub(crate) enum ParserProp {
141    LOADDTD = 1,
142    DEFAULTATTRS = 2,
143    VALIDATE = 3,
144    SUBST_ENTITIES = 4,
145}
146
147/// A traversal event in the document-order walk of the parsed tree.
148///
149/// Each event represents either entering a node (ELEMENT, TEXT, etc.) or
150/// exiting an element (END_ELEMENT). The `depth` is the element nesting
151/// depth at the time of the event.
152#[derive(Debug, Clone)]
153struct TraversalEvent {
154    /// The node this event refers to.
155    node: *mut _xmlNode,
156    /// Whether this is an "exit" event (END_ELEMENT).
157    is_end: bool,
158    /// The depth at this event (number of ancestor elements).
159    depth: i32,
160}
161
162/// Compute the element nesting depth of a node in the tree.
163///
164/// Counts the number of `XML_ELEMENT_NODE` ancestors.
165///
166/// # Safety
167///
168/// `node` must be a valid pointer to a node in a valid tree, or NULL.
169unsafe fn compute_depth(node: *mut _xmlNode) -> i32 {
170    if node.is_null() {
171        return 0;
172    }
173    let mut depth: i32 = 0;
174    // SAFETY: node is valid, and parent pointers form a tree.
175    let mut cur = unsafe { (*node).parent };
176    while !cur.is_null() {
177        // SAFETY: cur is valid.
178        if unsafe { (*cur).type_ } == XML_ELEMENT_NODE as c_int {
179            depth += 1;
180        }
181        // SAFETY: cur's parent is valid.
182        cur = unsafe { (*cur).parent };
183    }
184    depth
185}
186
187/// Convert an `xmlElementType` to the corresponding `ReaderNodeType`.
188fn element_type_to_reader_type(etype: c_int) -> ReaderNodeType {
189    match etype {
190        x if x == XML_ELEMENT_NODE as c_int => ReaderNodeType::ELEMENT,
191        x if x == XML_ATTRIBUTE_NODE as c_int => ReaderNodeType::ATTRIBUTE,
192        x if x == XML_TEXT_NODE as c_int => ReaderNodeType::TEXT,
193        x if x == XML_CDATA_SECTION_NODE as c_int => ReaderNodeType::CDATA,
194        x if x == XML_ENTITY_REF_NODE as c_int => ReaderNodeType::ENTITY_REFERENCE,
195        x if x == XML_ENTITY_NODE as c_int => ReaderNodeType::ENTITY,
196        x if x == XML_PI_NODE as c_int => ReaderNodeType::PROCESSING_INSTRUCTION,
197        x if x == XML_COMMENT_NODE as c_int => ReaderNodeType::COMMENT,
198        x if x == XML_DOCUMENT_NODE as c_int => ReaderNodeType::DOCUMENT,
199        x if x == XML_DOCUMENT_TYPE_NODE as c_int => ReaderNodeType::DOCUMENT_TYPE,
200        x if x == XML_DOCUMENT_FRAG_NODE as c_int => ReaderNodeType::DOCUMENT_FRAGMENT,
201        x if x == XML_NOTATION_NODE as c_int => ReaderNodeType::NOTATION,
202        x if x == XML_DTD_NODE as c_int => ReaderNodeType::DOCUMENT_TYPE,
203        x if x == XML_NAMESPACE_DECL as c_int => ReaderNodeType::NONE,
204        _ => ReaderNodeType::NONE,
205    }
206}
207
208/// Check whether a text node consists entirely of whitespace.
209fn is_whitespace_only(text: &[u8]) -> bool {
210    text.iter()
211        .all(|&b| b == b' ' || b == b'\t' || b == b'\n' || b == b'\r')
212}
213
214// ═══════════════════════════════════════════════════════════════════════════════
215// XmlTextReader — Internal Rust Type
216// ═══════════════════════════════════════════════════════════════════════════════
217
218/// The internal representation of an `xmlTextReader`.
219///
220/// This struct holds all state for the reader cursor: the parsed document,
221/// the current position in the traversal, attribute navigation state, and
222/// cached information about the current node.
223pub(crate) struct XmlTextReader {
224    /// The parsed XML document.
225    doc: *mut _xmlDoc,
226    /// The parser context used to parse the document (NULL after parsing).
227    ctxt: *mut _xmlParserCtxt,
228    /// The traversal events computed from the parsed tree.
229    events: Vec<TraversalEvent>,
230    /// Index into `events` for the current position.
231    event_index: usize,
232    /// Current read state.
233    state: ReadState,
234    /// The current node we're positioned on.
235    cur_node: *mut _xmlNode,
236    /// Current node type (reader node type).
237    node_type: ReaderNodeType,
238    /// Current depth.
239    depth: i32,
240    /// Cached name of the current node (xmlMalloc'd, NULL if none).
241    name: *mut xmlChar,
242    /// Cached value of the current node (xmlMalloc'd, NULL if none).
243    value: *mut xmlChar,
244    /// Number of attributes on the current element (-1 if not applicable).
245    attribute_count: i32,
246    /// Current attribute index (-1 = not on an attribute).
247    cur_attribute: i32,
248    /// Parser options bitmask.
249    options: c_int,
250    /// Document encoding string (xmlMalloc'd).
251    encoding: *mut xmlChar,
252    /// Document URL (xmlMalloc'd).
253    URL: *mut xmlChar,
254    /// Collected error messages.
255    errors: Vec<String>,
256    /// Whether the document has been parsed.
257    parsed: bool,
258}
259
260impl XmlTextReader {
261    /// Create a new reader with the given parser context and options.
262    ///
263    /// The reader takes ownership of the parser context. The document will be
264    /// parsed on the first call to `Read()`.
265    ///
266    /// # Safety
267    ///
268    /// `ctxt` must be a valid parser context created by `create_parser_ctxt`
269    /// and set up with input via `setup_parser_input`.
270    unsafe fn new(ctxt: *mut _xmlParserCtxt, URL: Option<&[u8]>, encoding: Option<&[u8]>) -> Self {
271        let url_ptr = URL
272            .map(|u| unsafe { bytes_to_xmlstr(u) })
273            .unwrap_or(ptr::null_mut());
274        let enc_ptr = encoding
275            .map(|e| unsafe { bytes_to_xmlstr(e) })
276            .unwrap_or(ptr::null_mut());
277
278        XmlTextReader {
279            doc: ptr::null_mut(),
280            ctxt,
281            events: Vec::new(),
282            event_index: 0,
283            state: ReadState::INITIALIZED,
284            cur_node: ptr::null_mut(),
285            node_type: ReaderNodeType::NONE,
286            depth: 0,
287            name: ptr::null_mut(),
288            value: ptr::null_mut(),
289            attribute_count: -1,
290            cur_attribute: -1,
291            options: 0,
292            encoding: enc_ptr,
293            URL: url_ptr,
294            errors: Vec::new(),
295            parsed: false,
296        }
297    }
298
299    /// Parse the document and build the event list.
300    ///
301    /// Returns 0 on success, -1 on error.
302    ///
303    /// # Safety
304    ///
305    /// `ctxt` must be a valid parser context with input set up.
306    unsafe fn parse_and_build_events(&mut self) -> c_int {
307        if self.ctxt.is_null() {
308            self.state = ReadState::ERROR;
309            self.errors.push("No parser context".to_string());
310            return -1;
311        }
312
313        // Set options on the context.
314        unsafe {
315            (*self.ctxt).options = self.options;
316        }
317
318        // Parse the document.
319        let result = unsafe { parse_document(self.ctxt) };
320
321        // Get the parsed document.
322        let doc = unsafe { (*self.ctxt).myDoc };
323        self.doc = doc;
324
325        // Free the parser context - we no longer need it.
326        if !self.ctxt.is_null() {
327            unsafe { free_parser_ctxt(self.ctxt) };
328        }
329        self.ctxt = ptr::null_mut();
330
331        if result != 0 || doc.is_null() {
332            self.state = ReadState::ERROR;
333            self.errors.push("Failed to parse document".to_string());
334            return -1;
335        }
336
337        // Set the encoding from the document if not already set.
338        if self.encoding.is_null() && !doc.is_null() {
339            // SAFETY: doc is valid.
340            let doc_enc = unsafe { (*doc).encoding };
341            if !doc_enc.is_null() {
342                self.encoding = unsafe { xml_strdup(doc_enc as *const xmlChar) };
343            }
344        }
345
346        // Build traversal events from the tree.
347        self.build_events();
348
349        self.parsed = true;
350        0
351    }
352
353    /// Walk the tree in document order and build traversal events.
354    ///
355    /// Generates events for all nodes (ELEMENT, TEXT, COMMENT, PI, etc.)
356    /// and END_ELEMENT events for elements.
357    fn build_events(&mut self) {
358        self.events.clear();
359
360        if self.doc.is_null() {
361            return;
362        }
363
364        // SAFETY: doc is valid.
365        let root = unsafe { (*self.doc).children };
366        if root.is_null() {
367            return;
368        }
369
370        // Walk all top-level children (PIs, comments, the root element, etc.)
371        // SAFETY: The tree is valid and all pointers are valid.
372        unsafe {
373            let mut n = root;
374            while !n.is_null() {
375                self.walk_tree(n, 0);
376                n = (*n).next;
377            }
378        }
379    }
380
381    /// Recursively walk a subtree and generate events.
382    ///
383    /// # Safety
384    ///
385    /// `node` must be a valid pointer to a node in the parsed tree.
386    unsafe fn walk_tree(&mut self, node: *mut _xmlNode, depth: i32) {
387        if node.is_null() {
388            return;
389        }
390
391        // SAFETY: node is valid.
392        let node_type = unsafe { (*node).type_ };
393
394        // For elements, generate an enter event and then recursively visit children,
395        // then generate an exit (END_ELEMENT) event.
396        if node_type == XML_ELEMENT_NODE as c_int {
397            self.events.push(TraversalEvent {
398                node,
399                is_end: false,
400                depth,
401            });
402
403            // Walk children.
404            // SAFETY: node's children are valid.
405            let mut child = unsafe { (*node).children };
406            while !child.is_null() {
407                let child_depth = depth + 1;
408                self.walk_tree(child, child_depth);
409                // SAFETY: child's next pointer is valid.
410                child = unsafe { (*child).next };
411            }
412
413            // Generate END_ELEMENT.
414            self.events.push(TraversalEvent {
415                node,
416                is_end: true,
417                depth,
418            });
419        } else if node_type == XML_TEXT_NODE as c_int
420            || node_type == XML_CDATA_SECTION_NODE as c_int
421            || node_type == XML_COMMENT_NODE as c_int
422            || node_type == XML_PI_NODE as c_int
423            || node_type == XML_ENTITY_REF_NODE as c_int
424        {
425            // Leaf nodes: text, CDATA, comment, PI, entity reference.
426            // Skip whitespace-only text nodes (libxml2 reader default behavior).
427            if node_type == XML_TEXT_NODE as c_int || node_type == XML_CDATA_SECTION_NODE as c_int {
428                let content = unsafe { (*node).content };
429                if !content.is_null() {
430                    let len = crate::xml::tree::xml_strlen(content);
431                    let slice = unsafe { core::slice::from_raw_parts(content, len as usize) };
432                    if slice
433                        .iter()
434                        .all(|&b| b == b' ' || b == b'\t' || b == b'\n' || b == b'\r')
435                    {
436                        // Skip whitespace-only text node
437                        return;
438                    }
439                }
440            }
441            self.events.push(TraversalEvent {
442                node,
443                is_end: false,
444                depth,
445            });
446        } else {
447            // Other node types (ENTITY, NOTATION, DTD, etc.) — skip or just enter.
448            self.events.push(TraversalEvent {
449                node,
450                is_end: false,
451                depth,
452            });
453        }
454    }
455
456    /// Position the reader on the event at the given index.
457    ///
458    /// Updates all cached fields (name, value, depth, node_type, etc.).
459    fn position_at(&mut self, index: usize) {
460        if index >= self.events.len() {
461            self.state = ReadState::EOF;
462            self.cur_node = ptr::null_mut();
463            self.node_type = ReaderNodeType::NONE;
464            self.depth = 0;
465            self.clear_cached_name();
466            self.clear_cached_value();
467            self.attribute_count = -1;
468            self.cur_attribute = -1;
469            return;
470        }
471
472        // Copy event data before any mutable self access to avoid borrow conflicts.
473        let ev_node: *mut _xmlNode;
474        let ev_is_end: bool;
475        let ev_depth: i32;
476        {
477            let event = &self.events[index];
478            ev_node = event.node;
479            ev_is_end = event.is_end;
480            ev_depth = event.depth;
481        }
482
483        self.event_index = index;
484        self.cur_node = ev_node;
485        self.depth = ev_depth;
486
487        // SAFETY: node is valid.
488        let etype = unsafe { (*ev_node).type_ };
489
490        if ev_is_end {
491            self.node_type = ReaderNodeType::END_ELEMENT;
492        } else {
493            self.node_type = element_type_to_reader_type(etype);
494        }
495
496        // Cache name and value.
497        // SAFETY: ev_node is a valid node pointer.
498        unsafe { self.cache_name_and_value(ev_node, ev_is_end) };
499
500        // Count attributes if this is an element.
501        if etype == XML_ELEMENT_NODE as c_int && !ev_is_end {
502            // SAFETY: ev_node is a valid element node.
503            self.attribute_count = unsafe { self.count_attributes(ev_node) };
504        } else {
505            self.attribute_count = -1;
506        }
507
508        // Reset attribute cursor.
509        self.cur_attribute = -1;
510    }
511
512    /// Cache the name of the current node.
513    ///
514    /// # Safety
515    ///
516    /// `node` must be a valid node pointer or NULL.
517    unsafe fn cache_name_and_value(&mut self, node: *mut _xmlNode, is_end: bool) {
518        self.clear_cached_name();
519        self.clear_cached_value();
520
521        if node.is_null() {
522            return;
523        }
524
525        // SAFETY: node is valid.
526        let etype = unsafe { (*node).type_ };
527
528        // Determine name.
529        let name: *mut xmlChar = if is_end {
530            // For END_ELEMENT, the name is the element name.
531            // SAFETY: node is valid.
532            unsafe { (*node).name as *mut xmlChar }
533        } else {
534            if etype == XML_ELEMENT_NODE as c_int
535                || etype == XML_PI_NODE as c_int
536                || etype == XML_ENTITY_REF_NODE as c_int
537                || etype == XML_ENTITY_NODE as c_int
538                || etype == XML_DOCUMENT_TYPE_NODE as c_int
539                || etype == XML_NOTATION_NODE as c_int
540            {
541                // SAFETY: node is valid.
542                unsafe { (*node).name as *mut xmlChar }
543            } else if etype == XML_ATTRIBUTE_NODE as c_int {
544                // For attribute nodes accessed via MoveToAttribute.
545                ptr::null_mut()
546            } else {
547                ptr::null_mut()
548            }
549        };
550
551        if !name.is_null() {
552            // SAFETY: name is a valid null-terminated xmlChar string.
553            self.name = unsafe { xml_strdup(name as *const xmlChar) };
554        }
555
556        // Determine value.
557        let value: *mut xmlChar = if etype == XML_TEXT_NODE as c_int
558            || etype == XML_CDATA_SECTION_NODE as c_int
559            || etype == XML_COMMENT_NODE as c_int
560        {
561            // SAFETY: node is valid.
562            unsafe { (*node).content }
563        } else if etype == XML_PI_NODE as c_int {
564            // PI nodes store content as the PI value (after the target).
565            // SAFETY: node is valid.
566            unsafe { (*node).content }
567        } else if etype == XML_ENTITY_REF_NODE as c_int {
568            // Entity references may have content.
569            // SAFETY: node is valid.
570            unsafe { (*node).content }
571        } else {
572            ptr::null_mut()
573        };
574
575        if !value.is_null() {
576            // SAFETY: value is a valid null-terminated xmlChar string.
577            self.value = unsafe { xml_strdup(value as *const xmlChar) };
578        }
579    }
580
581    /// Count the number of attributes on an element node.
582    ///
583    /// # Safety
584    ///
585    /// `node` must be a valid element node pointer.
586    unsafe fn count_attributes(&self, node: *mut _xmlNode) -> i32 {
587        let mut count: i32 = 0;
588        // SAFETY: node is a valid element.
589        let mut prop = unsafe { (*node).properties };
590        while !prop.is_null() {
591            count += 1;
592            // SAFETY: prop is valid.
593            prop = unsafe { (*prop).next };
594        }
595        count
596    }
597
598    /// Free the cached name.
599    fn clear_cached_name(&mut self) {
600        if !self.name.is_null() {
601            // SAFETY: name was allocated by xmlMalloc (via xml_strdup).
602            unsafe { xmlFree(self.name as *mut c_void) };
603            self.name = ptr::null_mut();
604        }
605    }
606
607    /// Free the cached value.
608    fn clear_cached_value(&mut self) {
609        if !self.value.is_null() {
610            // SAFETY: value was allocated by xmlMalloc (via xml_strdup).
611            unsafe { xmlFree(self.value as *mut c_void) };
612            self.value = ptr::null_mut();
613        }
614    }
615
616    // ─────────────────────────────────────────────────────────────────────────
617    // Navigation methods
618    // ─────────────────────────────────────────────────────────────────────────
619
620    /// Read the next node in document order.
621    ///
622    /// Returns 1 if a node was read, 0 if no more nodes (EOF), -1 on error.
623    pub unsafe fn Read(&mut self) -> c_int {
624        if self.state == ReadState::ERROR || self.state == ReadState::CLOSED {
625            return -1;
626        }
627
628        // On first call, parse the document and build events.
629        if !self.parsed {
630            if self.parse_and_build_events() != 0 {
631                self.state = ReadState::ERROR;
632                return -1;
633            }
634            self.state = ReadState::READING;
635        }
636
637        if self.state == ReadState::EOF {
638            return 0;
639        }
640
641        // If we're positioned on an attribute, return to the element first.
642        if self.cur_attribute >= 0 {
643            self.cur_attribute = -1;
644            // Re-cache the element info.
645            if !self.cur_node.is_null() {
646                // SAFETY: cur_node is valid.
647                unsafe { self.cache_name_and_value(self.cur_node, false) };
648            }
649        }
650
651        // Advance to the next event.
652        // If no events, we're at EOF.
653        if self.events.is_empty() {
654            self.state = ReadState::EOF;
655            return 0;
656        }
657
658        // Determine the next event index to position on.
659        // If cur_node is NULL, this is the first Read() after parsing —
660        // position at event 0. On subsequent calls, advance to the next event.
661        // We use cur_node.is_null() rather than event_index checks because
662        // after position_at(0), event_index == 0 and state == READING, which
663        // is indistinguishable from the pre-read state.
664        let next_index = if self.cur_node.is_null() {
665            // First Read() after parsing — position at event 0.
666            0
667        } else {
668            self.event_index + 1
669        };
670
671        if next_index < self.events.len() {
672            self.position_at(next_index);
673            1
674        } else {
675            self.state = ReadState::EOF;
676            self.cur_node = ptr::null_mut();
677            self.node_type = ReaderNodeType::NONE;
678            self.depth = 0;
679            self.clear_cached_name();
680            self.clear_cached_value();
681            self.attribute_count = -1;
682            self.cur_attribute = -1;
683            0
684        }
685    }
686
687    /// Skip to the next sibling of the current node.
688    ///
689    /// Returns 1 on success, 0 if no more siblings, -1 on error.
690    pub unsafe fn Next(&mut self) -> c_int {
691        if self.state != ReadState::READING || self.cur_node.is_null() {
692            return -1;
693        }
694
695        // Find the next sibling by scanning forward through events.
696        // We need to find the next event at depth <= current_depth that is not
697        // an END_ELEMENT. This skips:
698        // - All events in the current subtree (depth > current_depth)
699        // - END_ELEMENT events (which close the current element)
700        let current_depth = self.depth;
701        let mut i = self.event_index + 1;
702
703        while i < self.events.len() {
704            let event = &self.events[i];
705            if event.depth <= current_depth && !event.is_end {
706                self.position_at(i);
707                return 1;
708            }
709            i += 1;
710        }
711
712        0
713    }
714
715    /// Move to the parent element (if currently on an attribute).
716    ///
717    /// Returns 1 on success, 0 if not on an attribute, -1 on error.
718    pub unsafe fn MoveToElement(&mut self) -> c_int {
719        if self.cur_attribute < 0 {
720            return 0;
721        }
722        self.cur_attribute = -1;
723        if !self.cur_node.is_null() {
724            // SAFETY: cur_node is valid.
725            unsafe { self.cache_name_and_value(self.cur_node, false) };
726            self.node_type = ReaderNodeType::ELEMENT;
727        }
728        1
729    }
730
731    /// Move to an attribute by name.
732    ///
733    /// Returns 1 on success, 0 if attribute not found, -1 on error.
734    pub unsafe fn MoveToAttribute(&mut self, name: *const xmlChar) -> c_int {
735        if self.cur_node.is_null() {
736            return -1;
737        }
738
739        // SAFETY: cur_node is valid.
740        let etype = unsafe { (*self.cur_node).type_ };
741        if etype != XML_ELEMENT_NODE as c_int {
742            return -1;
743        }
744
745        // SAFETY: cur_node is an element.
746        let mut prop = unsafe { (*self.cur_node).properties };
747        let mut index: i32 = 0;
748        while !prop.is_null() {
749            // SAFETY: prop is a valid attribute.
750            let prop_name = unsafe { (*prop).name };
751            if !prop_name.is_null() {
752                // Compare names.
753                // SAFETY: Both strings are null-terminated.
754                if unsafe { crate::xml::string::xml_strcmp(prop_name as *const xmlChar, name) == 0 }
755                {
756                    self.cur_attribute = index;
757                    // Cache attribute info.
758                    self.cache_attribute_info(prop);
759                    return 1;
760                }
761            }
762            index += 1;
763            // SAFETY: prop's next pointer is valid.
764            prop = unsafe { (*prop).next };
765        }
766
767        0
768    }
769
770    /// Move to an attribute by index.
771    ///
772    /// Returns 1 on success, 0 if index out of range, -1 on error.
773    pub unsafe fn MoveToAttributeNo(&mut self, index: c_int) -> c_int {
774        if self.cur_node.is_null() || index < 0 {
775            return -1;
776        }
777
778        // SAFETY: cur_node is valid.
779        let etype = unsafe { (*self.cur_node).type_ };
780        if etype != XML_ELEMENT_NODE as c_int {
781            return -1;
782        }
783
784        // SAFETY: cur_node is an element.
785        let mut prop = unsafe { (*self.cur_node).properties };
786        let mut i: i32 = 0;
787        while !prop.is_null() {
788            if i == index {
789                self.cur_attribute = index;
790                // Cache attribute info.
791                self.cache_attribute_info(prop);
792                return 1;
793            }
794            i += 1;
795            // SAFETY: prop's next pointer is valid.
796            prop = unsafe { (*prop).next };
797        }
798
799        0
800    }
801
802    /// Move to the first attribute of the current element.
803    ///
804    /// Returns 1 on success, 0 if no attributes, -1 on error.
805    pub unsafe fn MoveToFirstAttribute(&mut self) -> c_int {
806        if self.cur_node.is_null() {
807            return -1;
808        }
809
810        // SAFETY: cur_node is valid.
811        let etype = unsafe { (*self.cur_node).type_ };
812        if etype != XML_ELEMENT_NODE as c_int {
813            return -1;
814        }
815
816        // SAFETY: cur_node is an element.
817        let first_prop = unsafe { (*self.cur_node).properties };
818        if first_prop.is_null() {
819            return 0;
820        }
821
822        self.cur_attribute = 0;
823        // Cache attribute info.
824        self.cache_attribute_info(first_prop);
825        1
826    }
827
828    /// Move to the next attribute.
829    ///
830    /// Returns 1 on success, 0 if no more attributes, -1 on error.
831    pub unsafe fn MoveToNextAttribute(&mut self) -> c_int {
832        if self.cur_attribute < 0 || self.cur_node.is_null() {
833            return -1;
834        }
835
836        // SAFETY: cur_node is valid.
837        let etype = unsafe { (*self.cur_node).type_ };
838        if etype != XML_ELEMENT_NODE as c_int {
839            return -1;
840        }
841
842        // Find the attribute at cur_attribute index.
843        // SAFETY: cur_node is an element.
844        let mut prop = unsafe { (*self.cur_node).properties };
845        let mut i: i32 = 0;
846        while !prop.is_null() && i <= self.cur_attribute {
847            if i == self.cur_attribute {
848                // Move to the next attribute.
849                // SAFETY: prop is a valid attribute.
850                let next_prop = unsafe { (*prop).next };
851                if next_prop.is_null() {
852                    return 0;
853                }
854                self.cur_attribute += 1;
855                self.cache_attribute_info(next_prop);
856                return 1;
857            }
858            i += 1;
859            // SAFETY: prop's next pointer is valid.
860            prop = unsafe { (*prop).next };
861        }
862
863        0
864    }
865
866    /// Cache the current position info for an attribute.
867    ///
868    /// # Safety
869    ///
870    /// `prop` must be a valid `_xmlAttr` pointer.
871    unsafe fn cache_attribute_info(&mut self, prop: *mut _xmlAttr) {
872        self.node_type = ReaderNodeType::ATTRIBUTE;
873        self.clear_cached_name();
874        self.clear_cached_value();
875
876        // SAFETY: prop is valid.
877        let attr = unsafe { &*prop };
878
879        // Name.
880        if !attr.name.is_null() {
881            // SAFETY: attr.name is null-terminated.
882            self.name = unsafe { xml_strdup(attr.name as *const xmlChar) };
883        }
884
885        // Value — the attribute's text content is in its child text node.
886        if !attr.children.is_null() {
887            // SAFETY: attr.children is a text node.
888            let val = unsafe { (*attr.children).content };
889            if !val.is_null() {
890                // SAFETY: val is null-terminated.
891                self.value = unsafe { xml_strdup(val as *const xmlChar) };
892            }
893        }
894    }
895
896    /// Move to the previous sibling.
897    ///
898    /// Returns 1 on success, 0 if no previous sibling, -1 on error.
899    pub unsafe fn Prev(&mut self) -> c_int {
900        if self.state != ReadState::READING || self.cur_node.is_null() {
901            return -1;
902        }
903
904        // Scan backward through events to find the previous sibling.
905        let current_depth = self.depth;
906        let mut i = if self.event_index > 0 {
907            self.event_index - 1
908        } else {
909            return 0;
910        };
911
912        loop {
913            let event = &self.events[i];
914            if event.depth == current_depth && !event.is_end {
915                self.position_at(i);
916                return 1;
917            }
918            if i == 0 {
919                break;
920            }
921            i -= 1;
922        }
923
924        0
925    }
926
927    // ─────────────────────────────────────────────────────────────────────────
928    // Information methods
929    // ─────────────────────────────────────────────────────────────────────────
930
931    /// Get the depth of the current node.
932    pub fn Depth(&self) -> c_int {
933        self.depth
934    }
935
936    /// Get the node type of the current node.
937    pub fn NodeType(&self) -> ReaderNodeType {
938        self.node_type
939    }
940
941    /// Get the name of the current node.
942    ///
943    /// Returns a pointer to a newly allocated string (caller must free with `xmlFree`),
944    /// or NULL if there is no name.
945    pub unsafe fn Name(&self) -> *mut xmlChar {
946        if self.name.is_null() {
947            return ptr::null_mut();
948        }
949        // SAFETY: name is a valid null-terminated xmlChar string.
950        unsafe { xml_strdup(self.name as *const xmlChar) }
951    }
952
953    /// Get the value of the current node.
954    ///
955    /// Returns a pointer to a newly allocated string (caller must free with `xmlFree`),
956    /// or NULL if there is no value.
957    pub unsafe fn Value(&self) -> *mut xmlChar {
958        if self.value.is_null() {
959            return ptr::null_mut();
960        }
961        // SAFETY: value is a valid null-terminated xmlChar string.
962        unsafe { xml_strdup(self.value as *const xmlChar) }
963    }
964
965    /// Get a constant pointer to the name (no copy).
966    ///
967    /// The returned pointer is valid only while the reader is alive and positioned
968    /// on the same node.
969    pub fn ConstName(&self) -> *const xmlChar {
970        self.name as *const xmlChar
971    }
972
973    /// Get a constant pointer to the value (no copy).
974    ///
975    /// The returned pointer is valid only while the reader is alive and positioned
976    /// on the same node.
977    pub fn ConstValue(&self) -> *const xmlChar {
978        self.value as *const xmlChar
979    }
980
981    /// Check if the current node has a value.
982    pub fn HasValue(&self) -> c_int {
983        if self.value.is_null() {
984            0
985        } else {
986            1
987        }
988    }
989
990    /// Check if the current node has attributes.
991    pub fn HasAttributes(&self) -> c_int {
992        if self.cur_node.is_null() {
993            return 0;
994        }
995        // SAFETY: cur_node is valid.
996        let etype = unsafe { (*self.cur_node).type_ };
997        if etype != XML_ELEMENT_NODE as c_int {
998            return 0;
999        }
1000        // SAFETY: cur_node is an element.
1001        let props = unsafe { (*self.cur_node).properties };
1002        if props.is_null() {
1003            0
1004        } else {
1005            1
1006        }
1007    }
1008
1009    /// Check if the current element is an empty element (no children).
1010    pub fn IsEmptyElement(&self) -> c_int {
1011        if self.cur_node.is_null() {
1012            return 0;
1013        }
1014        // SAFETY: cur_node is valid.
1015        let etype = unsafe { (*self.cur_node).type_ };
1016        if etype != XML_ELEMENT_NODE as c_int {
1017            return 0;
1018        }
1019        // SAFETY: cur_node is an element.
1020        let children = unsafe { (*self.cur_node).children };
1021        if children.is_null() {
1022            1
1023        } else {
1024            0
1025        }
1026    }
1027
1028    /// Get the base URI of the current node.
1029    ///
1030    /// Returns a newly allocated string (caller must free with `xmlFree`),
1031    /// or NULL if not available.
1032    pub unsafe fn BaseUri(&self) -> *mut xmlChar {
1033        // The base URI is typically the document URL.
1034        if self.doc.is_null() {
1035            return ptr::null_mut();
1036        }
1037        // SAFETY: doc is valid.
1038        let url = unsafe { (*self.doc).URL };
1039        if url.is_null() {
1040            return ptr::null_mut();
1041        }
1042        // SAFETY: url is null-terminated.
1043        unsafe { xml_strdup(url as *const xmlChar) }
1044    }
1045
1046    /// Get the local name of the current node.
1047    ///
1048    /// For namespaced names, this strips the prefix.
1049    /// Returns a newly allocated string, or NULL.
1050    pub unsafe fn LocalName(&self) -> *mut xmlChar {
1051        if self.name.is_null() {
1052            return ptr::null_mut();
1053        }
1054
1055        // SAFETY: name is a valid null-terminated string.
1056        let name_bytes = unsafe { xmlstr_to_bytes(self.name as *const xmlChar) };
1057
1058        // Find the colon separator.
1059        if let Some(pos) = name_bytes.iter().position(|&b| b == b':') {
1060            // Return everything after the colon.
1061            let local = &name_bytes[pos + 1..];
1062            if local.is_empty() {
1063                return ptr::null_mut();
1064            }
1065            // SAFETY: bytes_to_xmlstr allocates via xmlMalloc.
1066            unsafe { bytes_to_xmlstr(local) }
1067        } else {
1068            // No prefix, return the name as-is.
1069            // SAFETY: xml_strdup allocates via xmlMalloc.
1070            unsafe { xml_strdup(self.name as *const xmlChar) }
1071        }
1072    }
1073
1074    /// Get the namespace URI of the current node.
1075    ///
1076    /// Returns a newly allocated string, or NULL.
1077    pub unsafe fn NamespaceUri(&self) -> *mut xmlChar {
1078        if self.cur_node.is_null() {
1079            return ptr::null_mut();
1080        }
1081
1082        // SAFETY: cur_node is valid.
1083        let ns = unsafe { (*self.cur_node).ns };
1084        if ns.is_null() {
1085            return ptr::null_mut();
1086        }
1087
1088        // SAFETY: ns is valid.
1089        let href = unsafe { (*ns).href };
1090        if href.is_null() {
1091            return ptr::null_mut();
1092        }
1093
1094        // SAFETY: href is null-terminated.
1095        unsafe { xml_strdup(href as *const xmlChar) }
1096    }
1097
1098    /// Get the prefix of the current node.
1099    ///
1100    /// Returns a newly allocated string, or NULL.
1101    pub unsafe fn Prefix(&self) -> *mut xmlChar {
1102        if self.cur_node.is_null() {
1103            return ptr::null_mut();
1104        }
1105
1106        // SAFETY: cur_node is valid.
1107        let ns = unsafe { (*self.cur_node).ns };
1108        if ns.is_null() {
1109            return ptr::null_mut();
1110        }
1111
1112        // SAFETY: ns is valid.
1113        let prefix = unsafe { (*ns).prefix };
1114        if prefix.is_null() {
1115            return ptr::null_mut();
1116        }
1117
1118        // SAFETY: prefix is null-terminated.
1119        unsafe { xml_strdup(prefix as *const xmlChar) }
1120    }
1121
1122    /// Get the attribute count of the current element.
1123    pub fn AttributeCount(&self) -> c_int {
1124        self.attribute_count
1125    }
1126
1127    /// Get the read state.
1128    pub fn ReadState(&self) -> ReadState {
1129        self.state
1130    }
1131
1132    /// Get an attribute value by name.
1133    ///
1134    /// Returns a newly allocated string, or NULL.
1135    pub unsafe fn GetAttribute(&self, name: *const xmlChar) -> *mut xmlChar {
1136        if self.cur_node.is_null() {
1137            return ptr::null_mut();
1138        }
1139
1140        // SAFETY: cur_node is valid.
1141        let etype = unsafe { (*self.cur_node).type_ };
1142        if etype != XML_ELEMENT_NODE as c_int {
1143            return ptr::null_mut();
1144        }
1145
1146        // SAFETY: cur_node is an element.
1147        let mut prop = unsafe { (*self.cur_node).properties };
1148        while !prop.is_null() {
1149            // SAFETY: prop is valid.
1150            let prop_name = unsafe { (*prop).name };
1151            if !prop_name.is_null() {
1152                // SAFETY: Both strings are null-terminated.
1153                if unsafe { crate::xml::string::xml_strcmp(prop_name as *const xmlChar, name) == 0 }
1154                {
1155                    // Get the attribute value from its child text node.
1156                    // SAFETY: prop's children is valid.
1157                    let val = unsafe { (*prop).children };
1158                    if !val.is_null() {
1159                        // SAFETY: val's content is null-terminated.
1160                        let content = unsafe { (*val).content };
1161                        if !content.is_null() {
1162                            // SAFETY: content is null-terminated.
1163                            return unsafe { xml_strdup(content as *const xmlChar) };
1164                        }
1165                    }
1166                    return ptr::null_mut();
1167                }
1168            }
1169            // SAFETY: prop's next is valid.
1170            prop = unsafe { (*prop).next };
1171        }
1172
1173        ptr::null_mut()
1174    }
1175
1176    /// Get an attribute value by index.
1177    ///
1178    /// Returns a newly allocated string, or NULL.
1179    pub unsafe fn GetAttributeNo(&self, index: c_int) -> *mut xmlChar {
1180        if self.cur_node.is_null() || index < 0 {
1181            return ptr::null_mut();
1182        }
1183
1184        // SAFETY: cur_node is valid.
1185        let etype = unsafe { (*self.cur_node).type_ };
1186        if etype != XML_ELEMENT_NODE as c_int {
1187            return ptr::null_mut();
1188        }
1189
1190        // SAFETY: cur_node is an element.
1191        let mut prop = unsafe { (*self.cur_node).properties };
1192        let mut i: i32 = 0;
1193        while !prop.is_null() {
1194            if i == index {
1195                // SAFETY: prop is valid.
1196                let val = unsafe { (*prop).children };
1197                if !val.is_null() {
1198                    // SAFETY: val's content is null-terminated.
1199                    let content = unsafe { (*val).content };
1200                    if !content.is_null() {
1201                        // SAFETY: content is null-terminated.
1202                        return unsafe { xml_strdup(content as *const xmlChar) };
1203                    }
1204                }
1205                return ptr::null_mut();
1206            }
1207            i += 1;
1208            // SAFETY: prop's next is valid.
1209            prop = unsafe { (*prop).next };
1210        }
1211
1212        ptr::null_mut()
1213    }
1214
1215    /// Get an attribute value by local name and namespace URI.
1216    ///
1217    /// Returns a newly allocated string, or NULL.
1218    pub unsafe fn GetAttributeNs(
1219        &self,
1220        localName: *const xmlChar,
1221        namespaceURI: *const xmlChar,
1222    ) -> *mut xmlChar {
1223        if self.cur_node.is_null() {
1224            return ptr::null_mut();
1225        }
1226
1227        // SAFETY: cur_node is valid.
1228        let etype = unsafe { (*self.cur_node).type_ };
1229        if etype != XML_ELEMENT_NODE as c_int {
1230            return ptr::null_mut();
1231        }
1232
1233        // SAFETY: cur_node is an element.
1234        let mut prop = unsafe { (*self.cur_node).properties };
1235        while !prop.is_null() {
1236            // SAFETY: prop is valid.
1237            let prop_local = unsafe { (*prop).name };
1238            let prop_ns = unsafe { (*prop).ns };
1239
1240            // Check local name match.
1241            if prop_local.is_null() {
1242                // SAFETY: prop's next is valid.
1243                prop = unsafe { (*prop).next };
1244                continue;
1245            }
1246
1247            // SAFETY: prop_local is null-terminated.
1248            let name_match = unsafe {
1249                crate::xml::string::xml_strcmp(prop_local as *const xmlChar, localName) == 0
1250            };
1251
1252            if name_match {
1253                // Check namespace URI match.
1254                let ns_match = if namespaceURI.is_null() {
1255                    prop_ns.is_null()
1256                } else if prop_ns.is_null() {
1257                    false
1258                } else {
1259                    // SAFETY: Both hrefs are null-terminated.
1260                    unsafe {
1261                        crate::xml::string::xml_strcmp(
1262                            (*prop_ns).href as *const xmlChar,
1263                            namespaceURI,
1264                        ) == 0
1265                    }
1266                };
1267
1268                if ns_match {
1269                    // SAFETY: prop is valid.
1270                    let val = unsafe { (*prop).children };
1271                    if !val.is_null() {
1272                        // SAFETY: val's content is null-terminated.
1273                        let content = unsafe { (*val).content };
1274                        if !content.is_null() {
1275                            // SAFETY: content is null-terminated.
1276                            return unsafe { xml_strdup(content as *const xmlChar) };
1277                        }
1278                    }
1279                    return ptr::null_mut();
1280                }
1281            }
1282
1283            // SAFETY: prop's next is valid.
1284            prop = unsafe { (*prop).next };
1285        }
1286
1287        ptr::null_mut()
1288    }
1289
1290    /// Look up a namespace by prefix.
1291    ///
1292    /// Returns a newly allocated string with the namespace URI, or NULL.
1293    pub unsafe fn LookupNamespace(&self, prefix: *const xmlChar) -> *mut xmlChar {
1294        if self.cur_node.is_null() {
1295            return ptr::null_mut();
1296        }
1297
1298        // Walk up the tree looking for a namespace declaration matching the prefix.
1299        // SAFETY: cur_node is valid.
1300        let mut cur = self.cur_node;
1301        while !cur.is_null() {
1302            // SAFETY: cur is valid.
1303            let mut ns_def = unsafe { (*cur).nsDef };
1304            while !ns_def.is_null() {
1305                // SAFETY: ns_def is valid.
1306                let ns_prefix = unsafe { (*ns_def).prefix };
1307
1308                let match_prefix = if prefix.is_null() || *prefix == 0 {
1309                    // Looking for default namespace.
1310                    ns_prefix.is_null()
1311                } else if ns_prefix.is_null() {
1312                    false
1313                } else {
1314                    // SAFETY: Both are null-terminated.
1315                    unsafe {
1316                        crate::xml::string::xml_strcmp(ns_prefix as *const xmlChar, prefix) == 0
1317                    }
1318                };
1319
1320                if match_prefix {
1321                    // SAFETY: ns_def is valid.
1322                    let href = unsafe { (*ns_def).href };
1323                    if !href.is_null() {
1324                        // SAFETY: href is null-terminated.
1325                        return unsafe { xml_strdup(href as *const xmlChar) };
1326                    }
1327                    return ptr::null_mut();
1328                }
1329
1330                // SAFETY: ns_def's next is valid.
1331                ns_def = unsafe { (*ns_def).next };
1332            }
1333
1334            // SAFETY: cur's parent is valid.
1335            cur = unsafe { (*cur).parent };
1336        }
1337
1338        ptr::null_mut()
1339    }
1340
1341    /// Get a parser property.
1342    pub fn GetParserProp(&self, prop: c_int) -> c_int {
1343        match prop {
1344            1 /* XML_PARSER_LOADDTD */ => {
1345                if (self.options & XML_PARSE_DTDLOAD) != 0 { 1 } else { 0 }
1346            }
1347            2 /* XML_PARSER_DEFAULTATTRS */ => {
1348                if (self.options & XML_PARSE_DTDATTR) != 0 { 1 } else { 0 }
1349            }
1350            3 /* XML_PARSER_VALIDATE */ => {
1351                if (self.options & XML_PARSE_DTDVALID) != 0 { 1 } else { 0 }
1352            }
1353            4 /* XML_PARSER_SUBST_ENTITIES */ => {
1354                if (self.options & XML_PARSE_NOENT) != 0 { 1 } else { 0 }
1355            }
1356            _ => -1,
1357        }
1358    }
1359
1360    /// Set a parser property.
1361    pub fn SetParserProp(&mut self, prop: c_int, value: c_int) -> c_int {
1362        match prop {
1363            1 /* XML_PARSER_LOADDTD */ => {
1364                if value != 0 {
1365                    self.options |= XML_PARSE_DTDLOAD;
1366                } else {
1367                    self.options &= !XML_PARSE_DTDLOAD;
1368                }
1369                0
1370            }
1371            2 /* XML_PARSER_DEFAULTATTRS */ => {
1372                if value != 0 {
1373                    self.options |= XML_PARSE_DTDATTR;
1374                } else {
1375                    self.options &= !XML_PARSE_DTDATTR;
1376                }
1377                0
1378            }
1379            3 /* XML_PARSER_VALIDATE */ => {
1380                if value != 0 {
1381                    self.options |= XML_PARSE_DTDVALID;
1382                } else {
1383                    self.options &= !XML_PARSE_DTDVALID;
1384                }
1385                0
1386            }
1387            4 /* XML_PARSER_SUBST_ENTITIES */ => {
1388                if value != 0 {
1389                    self.options |= XML_PARSE_NOENT;
1390                } else {
1391                    self.options &= !XML_PARSE_NOENT;
1392                }
1393                0
1394            }
1395            _ => -1,
1396        }
1397    }
1398
1399    /// Get the current document.
1400    pub fn CurrentDoc(&self) -> *mut _xmlDoc {
1401        self.doc
1402    }
1403}
1404
1405impl Drop for XmlTextReader {
1406    fn drop(&mut self) {
1407        // Free cached strings.
1408        self.clear_cached_name();
1409        self.clear_cached_value();
1410
1411        // Free encoding and URL.
1412        if !self.encoding.is_null() {
1413            // SAFETY: encoding was allocated by xmlMalloc.
1414            unsafe { xmlFree(self.encoding as *mut c_void) };
1415            self.encoding = ptr::null_mut();
1416        }
1417        if !self.URL.is_null() {
1418            // SAFETY: URL was allocated by xmlMalloc.
1419            unsafe { xmlFree(self.URL as *mut c_void) };
1420            self.URL = ptr::null_mut();
1421        }
1422
1423        // Free the document if we own it.
1424        if !self.doc.is_null() {
1425            // SAFETY: doc was created by the parser, which allocates via xmlMalloc.
1426            // We own the doc since we created it.
1427            unsafe { tree::free_doc(self.doc) };
1428            self.doc = ptr::null_mut();
1429        }
1430
1431        // Free the parser context if still alive.
1432        if !self.ctxt.is_null() {
1433            // SAFETY: ctxt was created by create_parser_ctxt.
1434            unsafe { free_parser_ctxt(self.ctxt) };
1435            self.ctxt = ptr::null_mut();
1436        }
1437    }
1438}
1439
1440// ═══════════════════════════════════════════════════════════════════════════════
1441// Reader construction helpers
1442// ═══════════════════════════════════════════════════════════════════════════════
1443
1444/// Create a reader from a parser input buffer.
1445///
1446/// # Safety
1447///
1448/// `input` must be a valid `_xmlParserInputBuffer` pointer.
1449unsafe fn reader_from_input(
1450    input: *mut _xmlParserInputBuffer,
1451    URL: *const c_char,
1452    encoding: *const c_char,
1453    options: c_int,
1454) -> *mut XmlTextReader {
1455    if input.is_null() {
1456        return ptr::null_mut();
1457    }
1458
1459    // Create a parser context.
1460    let ctxt = create_parser_ctxt();
1461    if ctxt.is_null() {
1462        return ptr::null_mut();
1463    }
1464
1465    // Read all data from the input buffer using the read callback.
1466    let mut data = Vec::new();
1467    let mut tmp = [0u8; 4096];
1468
1469    // SAFETY: input is valid.
1470    let read_cb = unsafe { (*input).readcallback };
1471    let ioctx = unsafe { (*input).context };
1472
1473    if let Some(read) = read_cb {
1474        loop {
1475            // SAFETY: The read callback must be valid and ioctx must be a valid context.
1476            let n = unsafe { read(ioctx, tmp.as_mut_ptr() as *mut c_char, tmp.len() as c_int) };
1477            if n <= 0 {
1478                break;
1479            }
1480            data.extend_from_slice(&tmp[..n as usize]);
1481        }
1482    }
1483
1484    // Close the input if there's a close callback.
1485    // SAFETY: input is valid.
1486    let close_cb = unsafe { (*input).closecallback };
1487    if let Some(close) = close_cb {
1488        // SAFETY: The close callback must be valid.
1489        unsafe { close(ioctx) };
1490    }
1491
1492    // Create an InputBuffer from the data.
1493    let input_buf = InputBuffer::from_memory(&data, None);
1494
1495    // Set up the parser context with the input.
1496    setup_parser_input(ctxt, input_buf);
1497
1498    // Set options.
1499    unsafe {
1500        (*ctxt).options = options;
1501    }
1502
1503    // Build URL and encoding strings.
1504    let url_bytes = if URL.is_null() {
1505        None
1506    } else {
1507        // SAFETY: URL is a valid C string.
1508        unsafe {
1509            let cstr = std::ffi::CStr::from_ptr(URL);
1510            Some(cstr.to_bytes().to_vec())
1511        }
1512    };
1513
1514    let enc_bytes = if encoding.is_null() {
1515        None
1516    } else {
1517        // SAFETY: encoding is a valid C string.
1518        unsafe {
1519            let cstr = std::ffi::CStr::from_ptr(encoding);
1520            Some(cstr.to_bytes().to_vec())
1521        }
1522    };
1523
1524    // Create the reader.
1525    let mut reader = XmlTextReader::new(ctxt, url_bytes.as_deref(), enc_bytes.as_deref());
1526    reader.options = options;
1527
1528    // Box and leak the reader to return a raw pointer.
1529    Box::into_raw(Box::new(reader))
1530}
1531
1532// ═══════════════════════════════════════════════════════════════════════════════
1533// Public API functions
1534// ═══════════════════════════════════════════════════════════════════════════════
1535
1536/// Create a new text reader from an input buffer.
1537///
1538/// # UPSTREAM-PARITY
1539///
1540/// ```c
1541/// xmlTextReaderPtr xmlNewTextReader(xmlParserInputBufferPtr input, const char *URI);
1542/// ```
1543///
1544/// # Safety
1545///
1546/// - `input` must be a valid `_xmlParserInputBuffer` pointer or NULL.
1547/// - `URI` must be a valid C string or NULL.
1548#[no_mangle]
1549pub unsafe extern "C" fn xmlNewTextReader(
1550    input: *mut _xmlParserInputBuffer,
1551    URI: *const c_char,
1552) -> *mut XmlTextReader {
1553    // SAFETY: Forward to the helper.
1554    unsafe { reader_from_input(input, URI, ptr::null(), 0) }
1555}
1556
1557/// Create a text reader for a file.
1558///
1559/// # UPSTREAM-PARITY
1560///
1561/// ```c
1562/// xmlTextReaderPtr xmlReaderForFile(const char *filename, const char *encoding, int options);
1563/// ```
1564///
1565/// # Safety
1566///
1567/// - `filename` must be a valid C string or NULL.
1568/// - `encoding` must be a valid C string or NULL.
1569#[no_mangle]
1570pub unsafe extern "C" fn xmlReaderForFile(
1571    filename: *const c_char,
1572    encoding: *const c_char,
1573    options: c_int,
1574) -> *mut XmlTextReader {
1575    if filename.is_null() {
1576        return ptr::null_mut();
1577    }
1578
1579    // SAFETY: create_parser_ctxt returns a valid context or NULL.
1580    let ctxt = unsafe { create_parser_ctxt() };
1581    if ctxt.is_null() {
1582        return ptr::null_mut();
1583    }
1584
1585    // SAFETY: input_from_file reads the file; filename is a valid C string.
1586    let input = match unsafe { input_from_file(filename) } {
1587        Ok(input) => input,
1588        Err(_) => {
1589            // SAFETY: ctxt is valid.
1590            unsafe { free_parser_ctxt(ctxt) };
1591            return ptr::null_mut();
1592        }
1593    };
1594
1595    // SAFETY: ctxt and input are valid.
1596    unsafe { setup_parser_input(ctxt, input) };
1597    unsafe {
1598        (*ctxt).options = options;
1599    }
1600
1601    let enc_bytes = if encoding.is_null() {
1602        None
1603    } else {
1604        // SAFETY: encoding is a valid C string.
1605        unsafe {
1606            let cstr = std::ffi::CStr::from_ptr(encoding);
1607            Some(cstr.to_bytes().to_vec())
1608        }
1609    };
1610
1611    let mut reader = XmlTextReader::new(ctxt, None, enc_bytes.as_deref());
1612    reader.options = options;
1613    Box::into_raw(Box::new(reader))
1614}
1615
1616/// Create a text reader from memory.
1617///
1618/// # UPSTREAM-PARITY
1619///
1620/// ```c
1621/// xmlTextReaderPtr xmlReaderForMemory(const char *buffer, int size,
1622///                                     const char *URL, const char *encoding, int options);
1623/// ```
1624///
1625/// # Safety
1626///
1627/// - `buffer` must be a valid pointer with at least `size` readable bytes.
1628/// - `URL` and `encoding` must be valid C strings or NULL.
1629#[no_mangle]
1630pub unsafe extern "C" fn xmlReaderForMemory(
1631    buffer: *const c_char,
1632    size: c_int,
1633    URL: *const c_char,
1634    encoding: *const c_char,
1635    options: c_int,
1636) -> *mut XmlTextReader {
1637    if buffer.is_null() || size <= 0 {
1638        return ptr::null_mut();
1639    }
1640
1641    // SAFETY: create_parser_ctxt returns a valid context or NULL.
1642    let ctxt = unsafe { create_parser_ctxt() };
1643    if ctxt.is_null() {
1644        return ptr::null_mut();
1645    }
1646
1647    // SAFETY: input_from_memory copies the data; buffer and size are valid.
1648    let input = unsafe { input_from_memory(buffer, size) };
1649
1650    // SAFETY: ctxt and input are valid.
1651    unsafe { setup_parser_input(ctxt, input) };
1652    unsafe {
1653        (*ctxt).options = options;
1654    }
1655
1656    let url_bytes = if URL.is_null() {
1657        None
1658    } else {
1659        // SAFETY: URL is a valid C string.
1660        unsafe {
1661            let cstr = std::ffi::CStr::from_ptr(URL);
1662            Some(cstr.to_bytes().to_vec())
1663        }
1664    };
1665
1666    let enc_bytes = if encoding.is_null() {
1667        None
1668    } else {
1669        // SAFETY: encoding is a valid C string.
1670        unsafe {
1671            let cstr = std::ffi::CStr::from_ptr(encoding);
1672            Some(cstr.to_bytes().to_vec())
1673        }
1674    };
1675
1676    let mut reader = XmlTextReader::new(ctxt, url_bytes.as_deref(), enc_bytes.as_deref());
1677    reader.options = options;
1678    Box::into_raw(Box::new(reader))
1679}
1680
1681/// Create a text reader from a file descriptor.
1682///
1683/// # UPSTREAM-PARITY
1684///
1685/// ```c
1686/// xmlTextReaderPtr xmlReaderForFd(int fd, const char *URL,
1687///                                 const char *encoding, int options);
1688/// ```
1689///
1690/// # Safety
1691///
1692/// - `fd` must be a valid open file descriptor.
1693/// - `URL` and `encoding` must be valid C strings or NULL.
1694#[no_mangle]
1695pub unsafe extern "C" fn xmlReaderForFd(
1696    fd: c_int,
1697    URL: *const c_char,
1698    encoding: *const c_char,
1699    options: c_int,
1700) -> *mut XmlTextReader {
1701    // SAFETY: create_parser_ctxt returns a valid context or NULL.
1702    let ctxt = unsafe { create_parser_ctxt() };
1703    if ctxt.is_null() {
1704        return ptr::null_mut();
1705    }
1706
1707    // Read all data from the fd.
1708    let mut buf = Vec::new();
1709    let mut tmp = [0u8; 4096];
1710    loop {
1711        // SAFETY: fd must be a valid open file descriptor.
1712        let n = unsafe { libc::read(fd, tmp.as_mut_ptr() as *mut c_void, tmp.len()) };
1713        if n <= 0 {
1714            break;
1715        }
1716        buf.extend_from_slice(&tmp[..n as usize]);
1717    }
1718
1719    // SAFETY: input_from_memory copies the buffer contents.
1720    let input = unsafe { input_from_memory(buf.as_ptr() as *const c_char, buf.len() as c_int) };
1721
1722    // SAFETY: ctxt and input are valid.
1723    unsafe { setup_parser_input(ctxt, input) };
1724    unsafe {
1725        (*ctxt).options = options;
1726    }
1727
1728    let url_bytes = if URL.is_null() {
1729        None
1730    } else {
1731        // SAFETY: URL is a valid C string.
1732        unsafe {
1733            let cstr = std::ffi::CStr::from_ptr(URL);
1734            Some(cstr.to_bytes().to_vec())
1735        }
1736    };
1737
1738    let enc_bytes = if encoding.is_null() {
1739        None
1740    } else {
1741        // SAFETY: encoding is a valid C string.
1742        unsafe {
1743            let cstr = std::ffi::CStr::from_ptr(encoding);
1744            Some(cstr.to_bytes().to_vec())
1745        }
1746    };
1747
1748    let mut reader = XmlTextReader::new(ctxt, url_bytes.as_deref(), enc_bytes.as_deref());
1749    reader.options = options;
1750    Box::into_raw(Box::new(reader))
1751}
1752
1753/// Create a text reader from I/O callbacks.
1754///
1755/// # UPSTREAM-PARITY
1756///
1757/// ```c
1758/// xmlTextReaderPtr xmlReaderForIO(xmlInputReadCallback ioread, xmlInputCloseCallback ioclose,
1759///                                 void *ioctx, const char *URL,
1760///                                 const char *encoding, int options);
1761/// ```
1762///
1763/// # Safety
1764///
1765/// - `ioread` and `ioclose` must be valid function pointers or None.
1766/// - `ioctx` must be a valid context pointer for the callbacks.
1767/// - `URL` and `encoding` must be valid C strings or NULL.
1768#[no_mangle]
1769pub unsafe extern "C" fn xmlReaderForIO(
1770    ioread: Option<xmlInputReadCallback>,
1771    ioclose: Option<xmlInputCloseCallback>,
1772    ioctx: *mut c_void,
1773    URL: *const c_char,
1774    encoding: *const c_char,
1775    options: c_int,
1776) -> *mut XmlTextReader {
1777    // SAFETY: create_parser_ctxt returns a valid context or NULL.
1778    let ctxt = unsafe { create_parser_ctxt() };
1779    if ctxt.is_null() {
1780        return ptr::null_mut();
1781    }
1782
1783    // SAFETY: input_from_io reads all data via callbacks.
1784    let input = unsafe { input_from_io(ioread, ioclose, ioctx) };
1785
1786    // SAFETY: ctxt and input are valid.
1787    unsafe { setup_parser_input(ctxt, input) };
1788    unsafe {
1789        (*ctxt).options = options;
1790    }
1791
1792    let url_bytes = if URL.is_null() {
1793        None
1794    } else {
1795        // SAFETY: URL is a valid C string.
1796        unsafe {
1797            let cstr = std::ffi::CStr::from_ptr(URL);
1798            Some(cstr.to_bytes().to_vec())
1799        }
1800    };
1801
1802    let enc_bytes = if encoding.is_null() {
1803        None
1804    } else {
1805        // SAFETY: encoding is a valid C string.
1806        unsafe {
1807            let cstr = std::ffi::CStr::from_ptr(encoding);
1808            Some(cstr.to_bytes().to_vec())
1809        }
1810    };
1811
1812    let mut reader = XmlTextReader::new(ctxt, url_bytes.as_deref(), enc_bytes.as_deref());
1813    reader.options = options;
1814    Box::into_raw(Box::new(reader))
1815}
1816
1817// ─────────────────────────────────────────────────────────────────────────────
1818// Navigation functions
1819// ─────────────────────────────────────────────────────────────────────────────
1820
1821/// Advance the reader to the next node in document order.
1822///
1823/// Returns 1 on success, 0 if EOF, -1 on error.
1824///
1825/// # UPSTREAM-PARITY
1826///
1827/// ```c
1828/// int xmlTextReaderRead(xmlTextReaderPtr reader);
1829/// ```
1830///
1831/// # Safety
1832///
1833/// `reader` must be a valid pointer returned by `xmlNewTextReader` or one of the
1834/// `xmlReaderFor*` functions, or NULL (in which case -1 is returned).
1835#[no_mangle]
1836pub unsafe extern "C" fn xmlTextReaderRead(reader: *mut XmlTextReader) -> c_int {
1837    if reader.is_null() {
1838        return -1;
1839    }
1840    // SAFETY: reader is valid.
1841    unsafe { (*reader).Read() }
1842}
1843
1844/// Skip to the next sibling of the current node.
1845///
1846/// Returns 1 on success, 0 if no more siblings, -1 on error.
1847///
1848/// # UPSTREAM-PARITY
1849///
1850/// ```c
1851/// int xmlTextReaderNext(xmlTextReaderPtr reader);
1852/// ```
1853///
1854/// # Safety
1855///
1856/// `reader` must be a valid pointer or NULL.
1857#[no_mangle]
1858pub unsafe extern "C" fn xmlTextReaderNext(reader: *mut XmlTextReader) -> c_int {
1859    if reader.is_null() {
1860        return -1;
1861    }
1862    // SAFETY: reader is valid.
1863    unsafe { (*reader).Next() }
1864}
1865
1866/// Skip to the next sibling (same as xmlTextReaderNext).
1867///
1868/// # UPSTREAM-PARITY
1869///
1870/// ```c
1871/// int xmlTextReaderNextSibling(xmlTextReaderPtr reader);
1872/// ```
1873///
1874/// # Safety
1875///
1876/// `reader` must be a valid pointer or NULL.
1877#[no_mangle]
1878pub unsafe extern "C" fn xmlTextReaderNextSibling(reader: *mut XmlTextReader) -> c_int {
1879    if reader.is_null() {
1880        return -1;
1881    }
1882    // SAFETY: reader is valid.
1883    unsafe { (*reader).Next() }
1884}
1885
1886/// Skip to the previous sibling of the current node.
1887///
1888/// Returns 1 on success, 0 if no previous sibling, -1 on error.
1889///
1890/// # UPSTREAM-PARITY
1891///
1892/// ```c
1893/// int xmlTextReaderPrev(xmlTextReaderPtr reader);
1894/// ```
1895///
1896/// # Safety
1897///
1898/// `reader` must be a valid pointer or NULL.
1899#[no_mangle]
1900pub unsafe extern "C" fn xmlTextReaderPrev(reader: *mut XmlTextReader) -> c_int {
1901    if reader.is_null() {
1902        return -1;
1903    }
1904    // SAFETY: reader is valid.
1905    unsafe { (*reader).Prev() }
1906}
1907
1908/// Move the reader back to the parent element (from an attribute).
1909///
1910/// Returns 1 on success, 0 if not on an attribute, -1 on error.
1911///
1912/// # UPSTREAM-PARITY
1913///
1914/// ```c
1915/// int xmlTextReaderMoveToElement(xmlTextReaderPtr reader);
1916/// ```
1917///
1918/// # Safety
1919///
1920/// `reader` must be a valid pointer or NULL.
1921#[no_mangle]
1922pub unsafe extern "C" fn xmlTextReaderMoveToElement(reader: *mut XmlTextReader) -> c_int {
1923    if reader.is_null() {
1924        return -1;
1925    }
1926    // SAFETY: reader is valid.
1927    unsafe { (*reader).MoveToElement() }
1928}
1929
1930/// Move to an attribute by name.
1931///
1932/// Returns 1 on success, 0 if not found, -1 on error.
1933///
1934/// # UPSTREAM-PARITY
1935///
1936/// ```c
1937/// int xmlTextReaderMoveToAttribute(xmlTextReaderPtr reader, const xmlChar *name);
1938/// ```
1939///
1940/// # Safety
1941///
1942/// `reader` must be a valid pointer or NULL. `name` must be a valid C string or NULL.
1943#[no_mangle]
1944pub unsafe extern "C" fn xmlTextReaderMoveToAttribute(
1945    reader: *mut XmlTextReader,
1946    name: *const xmlChar,
1947) -> c_int {
1948    if reader.is_null() || name.is_null() {
1949        return -1;
1950    }
1951    // SAFETY: reader and name are valid.
1952    unsafe { (*reader).MoveToAttribute(name) }
1953}
1954
1955/// Move to an attribute by index.
1956///
1957/// Returns 1 on success, 0 if not found, -1 on error.
1958///
1959/// # UPSTREAM-PARITY
1960///
1961/// ```c
1962/// int xmlTextReaderMoveToAttributeNo(xmlTextReaderPtr reader, int index);
1963/// ```
1964///
1965/// # Safety
1966///
1967/// `reader` must be a valid pointer or NULL.
1968#[no_mangle]
1969pub unsafe extern "C" fn xmlTextReaderMoveToAttributeNo(
1970    reader: *mut XmlTextReader,
1971    index: c_int,
1972) -> c_int {
1973    if reader.is_null() {
1974        return -1;
1975    }
1976    // SAFETY: reader is valid.
1977    unsafe { (*reader).MoveToAttributeNo(index) }
1978}
1979
1980/// Move to the first attribute of the current element.
1981///
1982/// Returns 1 on success, 0 if no attributes, -1 on error.
1983///
1984/// # UPSTREAM-PARITY
1985///
1986/// ```c
1987/// int xmlTextReaderMoveToFirstAttribute(xmlTextReaderPtr reader);
1988/// ```
1989///
1990/// # Safety
1991///
1992/// `reader` must be a valid pointer or NULL.
1993#[no_mangle]
1994pub unsafe extern "C" fn xmlTextReaderMoveToFirstAttribute(reader: *mut XmlTextReader) -> c_int {
1995    if reader.is_null() {
1996        return -1;
1997    }
1998    // SAFETY: reader is valid.
1999    unsafe { (*reader).MoveToFirstAttribute() }
2000}
2001
2002/// Move to the next attribute.
2003///
2004/// Returns 1 on success, 0 if no more attributes, -1 on error.
2005///
2006/// # UPSTREAM-PARITY
2007///
2008/// ```c
2009/// int xmlTextReaderMoveToNextAttribute(xmlTextReaderPtr reader);
2010/// ```
2011///
2012/// # Safety
2013///
2014/// `reader` must be a valid pointer or NULL.
2015#[no_mangle]
2016pub unsafe extern "C" fn xmlTextReaderMoveToNextAttribute(reader: *mut XmlTextReader) -> c_int {
2017    if reader.is_null() {
2018        return -1;
2019    }
2020    // SAFETY: reader is valid.
2021    unsafe { (*reader).MoveToNextAttribute() }
2022}
2023
2024// ─────────────────────────────────────────────────────────────────────────────
2025// Information methods
2026// ─────────────────────────────────────────────────────────────────────────────
2027
2028/// Get the attribute count of the current element.
2029///
2030/// Returns the number of attributes, or -1 if not on an element.
2031///
2032/// # UPSTREAM-PARITY
2033///
2034/// ```c
2035/// int xmlTextReaderAttributeCount(xmlTextReaderPtr reader);
2036/// ```
2037///
2038/// # Safety
2039///
2040/// `reader` must be a valid pointer or NULL.
2041#[no_mangle]
2042pub unsafe extern "C" fn xmlTextReaderAttributeCount(reader: *mut XmlTextReader) -> c_int {
2043    if reader.is_null() {
2044        return -1;
2045    }
2046    // SAFETY: reader is valid.
2047    unsafe { (*reader).AttributeCount() }
2048}
2049
2050/// Get the depth of the current node.
2051///
2052/// Returns the depth (0 for root element), or -1 on error.
2053///
2054/// # UPSTREAM-PARITY
2055///
2056/// ```c
2057/// int xmlTextReaderDepth(xmlTextReaderPtr reader);
2058/// ```
2059///
2060/// # Safety
2061///
2062/// `reader` must be a valid pointer or NULL.
2063#[no_mangle]
2064pub unsafe extern "C" fn xmlTextReaderDepth(reader: *mut XmlTextReader) -> c_int {
2065    if reader.is_null() {
2066        return -1;
2067    }
2068    // SAFETY: reader is valid.
2069    unsafe { (*reader).Depth() }
2070}
2071
2072/// Get the node type of the current node.
2073///
2074/// Returns one of the `xmlReaderTypes` constants, or -1 on error.
2075///
2076/// # UPSTREAM-PARITY
2077///
2078/// ```c
2079/// int xmlTextReaderNodeType(xmlTextReaderPtr reader);
2080/// ```
2081///
2082/// # Safety
2083///
2084/// `reader` must be a valid pointer or NULL.
2085#[no_mangle]
2086pub unsafe extern "C" fn xmlTextReaderNodeType(reader: *mut XmlTextReader) -> c_int {
2087    if reader.is_null() {
2088        return -1;
2089    }
2090    // SAFETY: reader is valid.
2091    unsafe { (*reader).NodeType() as c_int }
2092}
2093
2094/// Get the name of the current node.
2095///
2096/// Returns a newly allocated string (caller must free with `xmlFree`),
2097/// or NULL if there is no name.
2098///
2099/// # UPSTREAM-PARITY
2100///
2101/// ```c
2102/// xmlChar *xmlTextReaderName(xmlTextReaderPtr reader);
2103/// ```
2104///
2105/// # Safety
2106///
2107/// `reader` must be a valid pointer or NULL.
2108#[no_mangle]
2109pub unsafe extern "C" fn xmlTextReaderName(reader: *mut XmlTextReader) -> *mut xmlChar {
2110    if reader.is_null() {
2111        return ptr::null_mut();
2112    }
2113    // SAFETY: reader is valid.
2114    unsafe { (*reader).Name() }
2115}
2116
2117/// Get the value of the current node.
2118///
2119/// Returns a newly allocated string (caller must free with `xmlFree`),
2120/// or NULL if there is no value.
2121///
2122/// # UPSTREAM-PARITY
2123///
2124/// ```c
2125/// xmlChar *xmlTextReaderValue(xmlTextReaderPtr reader);
2126/// ```
2127///
2128/// # Safety
2129///
2130/// `reader` must be a valid pointer or NULL.
2131#[no_mangle]
2132pub unsafe extern "C" fn xmlTextReaderValue(reader: *mut XmlTextReader) -> *mut xmlChar {
2133    if reader.is_null() {
2134        return ptr::null_mut();
2135    }
2136    // SAFETY: reader is valid.
2137    unsafe { (*reader).Value() }
2138}
2139
2140/// Get a constant pointer to the name (no copy).
2141///
2142/// The returned pointer is valid only while the reader is alive and positioned
2143/// on the same node.
2144///
2145/// # UPSTREAM-PARITY
2146///
2147/// ```c
2148/// const xmlChar *xmlTextReaderConstName(xmlTextReaderPtr reader);
2149/// ```
2150///
2151/// # Safety
2152///
2153/// `reader` must be a valid pointer or NULL.
2154#[no_mangle]
2155pub unsafe extern "C" fn xmlTextReaderConstName(reader: *mut XmlTextReader) -> *const xmlChar {
2156    if reader.is_null() {
2157        return ptr::null();
2158    }
2159    // SAFETY: reader is valid.
2160    unsafe { (*reader).ConstName() }
2161}
2162
2163/// Get a constant pointer to the value (no copy).
2164///
2165/// The returned pointer is valid only while the reader is alive and positioned
2166/// on the same node.
2167///
2168/// # UPSTREAM-PARITY
2169///
2170/// ```c
2171/// const xmlChar *xmlTextReaderConstValue(xmlTextReaderPtr reader);
2172/// ```
2173///
2174/// # Safety
2175///
2176/// `reader` must be a valid pointer or NULL.
2177#[no_mangle]
2178pub unsafe extern "C" fn xmlTextReaderConstValue(reader: *mut XmlTextReader) -> *const xmlChar {
2179    if reader.is_null() {
2180        return ptr::null();
2181    }
2182    // SAFETY: reader is valid.
2183    unsafe { (*reader).ConstValue() }
2184}
2185
2186/// Get the base URI of the current node.
2187///
2188/// Returns a newly allocated string (caller must free with `xmlFree`),
2189/// or NULL if not available.
2190///
2191/// # UPSTREAM-PARITY
2192///
2193/// ```c
2194/// xmlChar *xmlTextReaderBaseUri(xmlTextReaderPtr reader);
2195/// ```
2196///
2197/// # Safety
2198///
2199/// `reader` must be a valid pointer or NULL.
2200#[no_mangle]
2201pub unsafe extern "C" fn xmlTextReaderBaseUri(reader: *mut XmlTextReader) -> *mut xmlChar {
2202    if reader.is_null() {
2203        return ptr::null_mut();
2204    }
2205    // SAFETY: reader is valid.
2206    unsafe { (*reader).BaseUri() }
2207}
2208
2209/// Get the local name of the current node.
2210///
2211/// Returns a newly allocated string, or NULL.
2212///
2213/// # UPSTREAM-PARITY
2214///
2215/// ```c
2216/// xmlChar *xmlTextReaderLocalName(xmlTextReaderPtr reader);
2217/// ```
2218///
2219/// # Safety
2220///
2221/// `reader` must be a valid pointer or NULL.
2222#[no_mangle]
2223pub unsafe extern "C" fn xmlTextReaderLocalName(reader: *mut XmlTextReader) -> *mut xmlChar {
2224    if reader.is_null() {
2225        return ptr::null_mut();
2226    }
2227    // SAFETY: reader is valid.
2228    unsafe { (*reader).LocalName() }
2229}
2230
2231/// Get the namespace URI of the current node.
2232///
2233/// Returns a newly allocated string, or NULL.
2234///
2235/// # UPSTREAM-PARITY
2236///
2237/// ```c
2238/// xmlChar *xmlTextReaderNamespaceUri(xmlTextReaderPtr reader);
2239/// ```
2240///
2241/// # Safety
2242///
2243/// `reader` must be a valid pointer or NULL.
2244#[no_mangle]
2245pub unsafe extern "C" fn xmlTextReaderNamespaceUri(reader: *mut XmlTextReader) -> *mut xmlChar {
2246    if reader.is_null() {
2247        return ptr::null_mut();
2248    }
2249    // SAFETY: reader is valid.
2250    unsafe { (*reader).NamespaceUri() }
2251}
2252
2253/// Get the prefix of the current node.
2254///
2255/// Returns a newly allocated string, or NULL.
2256///
2257/// # UPSTREAM-PARITY
2258///
2259/// ```c
2260/// xmlChar *xmlTextReaderPrefix(xmlTextReaderPtr reader);
2261/// ```
2262///
2263/// # Safety
2264///
2265/// `reader` must be a valid pointer or NULL.
2266#[no_mangle]
2267pub unsafe extern "C" fn xmlTextReaderPrefix(reader: *mut XmlTextReader) -> *mut xmlChar {
2268    if reader.is_null() {
2269        return ptr::null_mut();
2270    }
2271    // SAFETY: reader is valid.
2272    unsafe { (*reader).Prefix() }
2273}
2274
2275/// Check if the current node has a value.
2276///
2277/// Returns 1 if the node has a value, 0 otherwise.
2278///
2279/// # UPSTREAM-PARITY
2280///
2281/// ```c
2282/// int xmlTextReaderHasValue(xmlTextReaderPtr reader);
2283/// ```
2284///
2285/// # Safety
2286///
2287/// `reader` must be a valid pointer or NULL.
2288#[no_mangle]
2289pub unsafe extern "C" fn xmlTextReaderHasValue(reader: *mut XmlTextReader) -> c_int {
2290    if reader.is_null() {
2291        return 0;
2292    }
2293    // SAFETY: reader is valid.
2294    unsafe { (*reader).HasValue() }
2295}
2296
2297/// Check if the current node has attributes.
2298///
2299/// Returns 1 if the node has attributes, 0 otherwise.
2300///
2301/// # UPSTREAM-PARITY
2302///
2303/// ```c
2304/// int xmlTextReaderHasAttributes(xmlTextReaderPtr reader);
2305/// ```
2306///
2307/// # Safety
2308///
2309/// `reader` must be a valid pointer or NULL.
2310#[no_mangle]
2311pub unsafe extern "C" fn xmlTextReaderHasAttributes(reader: *mut XmlTextReader) -> c_int {
2312    if reader.is_null() {
2313        return 0;
2314    }
2315    // SAFETY: reader is valid.
2316    unsafe { (*reader).HasAttributes() }
2317}
2318
2319/// Check if the current element is an empty element (no children).
2320///
2321/// Returns 1 if empty, 0 otherwise.
2322///
2323/// # UPSTREAM-PARITY
2324///
2325/// ```c
2326/// int xmlTextReaderIsEmptyElement(xmlTextReaderPtr reader);
2327/// ```
2328///
2329/// # Safety
2330///
2331/// `reader` must be a valid pointer or NULL.
2332#[no_mangle]
2333pub unsafe extern "C" fn xmlTextReaderIsEmptyElement(reader: *mut XmlTextReader) -> c_int {
2334    if reader.is_null() {
2335        return 0;
2336    }
2337    // SAFETY: reader is valid.
2338    unsafe { (*reader).IsEmptyElement() }
2339}
2340
2341/// Get the read state.
2342///
2343/// Returns one of the `xmlTextReaderReadState` constants.
2344///
2345/// # UPSTREAM-PARITY
2346///
2347/// ```c
2348/// int xmlTextReaderReadState(xmlTextReaderPtr reader);
2349/// ```
2350///
2351/// # Safety
2352///
2353/// `reader` must be a valid pointer or NULL.
2354#[no_mangle]
2355pub unsafe extern "C" fn xmlTextReaderReadState(reader: *mut XmlTextReader) -> c_int {
2356    if reader.is_null() {
2357        return ReadState::ERROR as c_int;
2358    }
2359    // SAFETY: reader is valid.
2360    unsafe { (*reader).ReadState() as c_int }
2361}
2362
2363// ─────────────────────────────────────────────────────────────────────────────
2364// Attribute access
2365// ─────────────────────────────────────────────────────────────────────────────
2366
2367/// Get an attribute value by name.
2368///
2369/// Returns a newly allocated string, or NULL.
2370///
2371/// # UPSTREAM-PARITY
2372///
2373/// ```c
2374/// xmlChar *xmlTextReaderGetAttribute(xmlTextReaderPtr reader, const xmlChar *name);
2375/// ```
2376///
2377/// # Safety
2378///
2379/// `reader` and `name` must be valid pointers or NULL.
2380#[no_mangle]
2381pub unsafe extern "C" fn xmlTextReaderGetAttribute(
2382    reader: *mut XmlTextReader,
2383    name: *const xmlChar,
2384) -> *mut xmlChar {
2385    if reader.is_null() || name.is_null() {
2386        return ptr::null_mut();
2387    }
2388    // SAFETY: reader and name are valid.
2389    unsafe { (*reader).GetAttribute(name) }
2390}
2391
2392/// Get an attribute value by index.
2393///
2394/// Returns a newly allocated string, or NULL.
2395///
2396/// # UPSTREAM-PARITY
2397///
2398/// ```c
2399/// xmlChar *xmlTextReaderGetAttributeNo(xmlTextReaderPtr reader, int index);
2400/// ```
2401///
2402/// # Safety
2403///
2404/// `reader` must be a valid pointer or NULL.
2405#[no_mangle]
2406pub unsafe extern "C" fn xmlTextReaderGetAttributeNo(
2407    reader: *mut XmlTextReader,
2408    index: c_int,
2409) -> *mut xmlChar {
2410    if reader.is_null() {
2411        return ptr::null_mut();
2412    }
2413    // SAFETY: reader is valid.
2414    unsafe { (*reader).GetAttributeNo(index) }
2415}
2416
2417/// Get an attribute value by local name and namespace URI.
2418///
2419/// Returns a newly allocated string, or NULL.
2420///
2421/// # UPSTREAM-PARITY
2422///
2423/// ```c
2424/// xmlChar *xmlTextReaderGetAttributeNs(xmlTextReaderPtr reader,
2425///                                      const xmlChar *localName,
2426///                                      const xmlChar *namespaceURI);
2427/// ```
2428///
2429/// # Safety
2430///
2431/// `reader`, `localName`, and `namespaceURI` must be valid pointers or NULL.
2432#[no_mangle]
2433pub unsafe extern "C" fn xmlTextReaderGetAttributeNs(
2434    reader: *mut XmlTextReader,
2435    localName: *const xmlChar,
2436    namespaceURI: *const xmlChar,
2437) -> *mut xmlChar {
2438    if reader.is_null() || localName.is_null() {
2439        return ptr::null_mut();
2440    }
2441    // SAFETY: reader, localName, and namespaceURI are valid.
2442    unsafe { (*reader).GetAttributeNs(localName, namespaceURI) }
2443}
2444
2445/// Look up a namespace by prefix.
2446///
2447/// Returns a newly allocated string with the namespace URI, or NULL.
2448///
2449/// # UPSTREAM-PARITY
2450///
2451/// ```c
2452/// xmlChar *xmlTextReaderLookupNamespace(xmlTextReaderPtr reader, const xmlChar *prefix);
2453/// ```
2454///
2455/// # Safety
2456///
2457/// `reader` and `prefix` must be valid pointers or NULL.
2458#[no_mangle]
2459pub unsafe extern "C" fn xmlTextReaderLookupNamespace(
2460    reader: *mut XmlTextReader,
2461    prefix: *const xmlChar,
2462) -> *mut xmlChar {
2463    if reader.is_null() {
2464        return ptr::null_mut();
2465    }
2466    // SAFETY: reader and prefix are valid.
2467    unsafe { (*reader).LookupNamespace(prefix) }
2468}
2469
2470// ─────────────────────────────────────────────────────────────────────────────
2471// Parser properties
2472// ─────────────────────────────────────────────────────────────────────────────
2473
2474/// Get a parser property.
2475///
2476/// Returns the property value (0 or 1), or -1 on error.
2477///
2478/// # UPSTREAM-PARITY
2479///
2480/// ```c
2481/// int xmlTextReaderGetParserProp(xmlTextReaderPtr reader, int prop);
2482/// ```
2483///
2484/// # Safety
2485///
2486/// `reader` must be a valid pointer or NULL.
2487#[no_mangle]
2488pub unsafe extern "C" fn xmlTextReaderGetParserProp(
2489    reader: *mut XmlTextReader,
2490    prop: c_int,
2491) -> c_int {
2492    if reader.is_null() {
2493        return -1;
2494    }
2495    // SAFETY: reader is valid.
2496    unsafe { (*reader).GetParserProp(prop) }
2497}
2498
2499/// Set a parser property.
2500///
2501/// Returns 0 on success, -1 on error.
2502///
2503/// # UPSTREAM-PARITY
2504///
2505/// ```c
2506/// int xmlTextReaderSetParserProp(xmlTextReaderPtr reader, int prop, int value);
2507/// ```
2508///
2509/// # Safety
2510///
2511/// `reader` must be a valid pointer or NULL.
2512#[no_mangle]
2513pub unsafe extern "C" fn xmlTextReaderSetParserProp(
2514    reader: *mut XmlTextReader,
2515    prop: c_int,
2516    value: c_int,
2517) -> c_int {
2518    if reader.is_null() {
2519        return -1;
2520    }
2521    // SAFETY: reader is valid.
2522    unsafe { (*reader).SetParserProp(prop, value) }
2523}
2524
2525// ─────────────────────────────────────────────────────────────────────────────
2526// Lifecycle
2527// ─────────────────────────────────────────────────────────────────────────────
2528
2529/// Free a text reader.
2530///
2531/// # UPSTREAM-PARITY
2532///
2533/// ```c
2534/// void xmlFreeTextReader(xmlTextReaderPtr reader);
2535/// ```
2536///
2537/// # Safety
2538///
2539/// `reader` must be a valid pointer returned by `xmlNewTextReader` or one of the
2540/// `xmlReaderFor*` functions, or NULL (in which case this is a no-op).
2541#[no_mangle]
2542pub unsafe extern "C" fn xmlFreeTextReader(reader: *mut XmlTextReader) {
2543    if reader.is_null() {
2544        return;
2545    }
2546    // SAFETY: reader was created via Box::into_raw, so we reconstruct the Box
2547    // and let it drop, which calls the Drop impl.
2548    unsafe {
2549        let _ = Box::from_raw(reader);
2550    }
2551}
2552
2553/// Setup/reinitialize a reader with new input.
2554///
2555/// # UPSTREAM-PARITY
2556///
2557/// ```c
2558/// int xmlTextReaderSetup(xmlTextReaderPtr reader,
2559///                        xmlParserInputBufferPtr input,
2560///                        const char *URL, const char *encoding, int options);
2561/// ```
2562///
2563/// # Safety
2564///
2565/// - `reader` must be a valid pointer or NULL.
2566/// - `input` must be a valid `_xmlParserInputBuffer` pointer or NULL.
2567/// - `URL` and `encoding` must be valid C strings or NULL.
2568#[no_mangle]
2569pub unsafe extern "C" fn xmlTextReaderSetup(
2570    reader: *mut XmlTextReader,
2571    input: *mut _xmlParserInputBuffer,
2572    URL: *const c_char,
2573    encoding: *const c_char,
2574    options: c_int,
2575) -> c_int {
2576    if reader.is_null() {
2577        return -1;
2578    }
2579
2580    // SAFETY: reader is valid.
2581    let r = unsafe { &mut *reader };
2582
2583    // Reset the reader state.
2584    r.clear_cached_name();
2585    r.clear_cached_value();
2586
2587    // Free the old document.
2588    if !r.doc.is_null() {
2589        // SAFETY: doc was allocated by the parser.
2590        unsafe { tree::free_doc(r.doc) };
2591        r.doc = ptr::null_mut();
2592    }
2593
2594    // Free old parser context.
2595    if !r.ctxt.is_null() {
2596        // SAFETY: ctxt was created by create_parser_ctxt.
2597        unsafe { free_parser_ctxt(r.ctxt) };
2598        r.ctxt = ptr::null_mut();
2599    }
2600
2601    r.events.clear();
2602    r.event_index = 0;
2603    r.state = ReadState::INITIALIZED;
2604    r.cur_node = ptr::null_mut();
2605    r.node_type = ReaderNodeType::NONE;
2606    r.depth = 0;
2607    r.attribute_count = -1;
2608    r.cur_attribute = -1;
2609    r.options = options;
2610    r.parsed = false;
2611    r.errors.clear();
2612
2613    // Update URL.
2614    if !r.URL.is_null() {
2615        // SAFETY: URL was allocated by xmlMalloc.
2616        unsafe { xmlFree(r.URL as *mut c_void) };
2617        r.URL = ptr::null_mut();
2618    }
2619    if !URL.is_null() {
2620        // SAFETY: URL is a valid C string.
2621        let url_str = unsafe { std::ffi::CStr::from_ptr(URL) };
2622        // SAFETY: bytes_to_xmlstr allocates via xmlMalloc.
2623        r.URL = unsafe { bytes_to_xmlstr(url_str.to_bytes()) };
2624    }
2625
2626    // Update encoding.
2627    if !r.encoding.is_null() {
2628        // SAFETY: encoding was allocated by xmlMalloc.
2629        unsafe { xmlFree(r.encoding as *mut c_void) };
2630        r.encoding = ptr::null_mut();
2631    }
2632    if !encoding.is_null() {
2633        // SAFETY: encoding is a valid C string.
2634        let enc_str = unsafe { std::ffi::CStr::from_ptr(encoding) };
2635        // SAFETY: bytes_to_xmlstr allocates via xmlMalloc.
2636        r.encoding = unsafe { bytes_to_xmlstr(enc_str.to_bytes()) };
2637    }
2638
2639    // Create new parser context and set up input.
2640    if !input.is_null() {
2641        // SAFETY: create_parser_ctxt returns a valid context or NULL.
2642        let ctxt = unsafe { create_parser_ctxt() };
2643        if ctxt.is_null() {
2644            return -1;
2645        }
2646
2647        // Read all data from the input buffer.
2648        let mut data = Vec::new();
2649        let mut tmp = [0u8; 4096];
2650
2651        // SAFETY: input is valid.
2652        let read_cb = unsafe { (*input).readcallback };
2653        let ioctx = unsafe { (*input).context };
2654
2655        if let Some(read) = read_cb {
2656            loop {
2657                // SAFETY: callbacks are valid.
2658                let n = unsafe { read(ioctx, tmp.as_mut_ptr() as *mut c_char, tmp.len() as c_int) };
2659                if n <= 0 {
2660                    break;
2661                }
2662                data.extend_from_slice(&tmp[..n as usize]);
2663            }
2664        }
2665
2666        // Close the input.
2667        let close_cb = unsafe { (*input).closecallback };
2668        if let Some(close) = close_cb {
2669            // SAFETY: close callback is valid.
2670            unsafe { close(ioctx) };
2671        }
2672
2673        let input_buf = InputBuffer::from_memory(&data, None);
2674
2675        // SAFETY: ctxt and input_buf are valid.
2676        unsafe { setup_parser_input(ctxt, input_buf) };
2677        unsafe {
2678            (*ctxt).options = options;
2679        }
2680
2681        r.ctxt = ctxt;
2682    }
2683
2684    0
2685}
2686
2687/// Get the current document from the reader.
2688///
2689/// Returns a pointer to the `_xmlDoc` or NULL.
2690///
2691/// # UPSTREAM-PARITY
2692///
2693/// ```c
2694/// xmlDocPtr xmlTextReaderCurrentDoc(xmlTextReaderPtr reader);
2695/// ```
2696///
2697/// # Safety
2698///
2699/// `reader` must be a valid pointer or NULL.
2700#[no_mangle]
2701pub unsafe extern "C" fn xmlTextReaderCurrentDoc(reader: *mut XmlTextReader) -> *mut _xmlDoc {
2702    if reader.is_null() {
2703        return ptr::null_mut();
2704    }
2705    // SAFETY: reader is valid.
2706    unsafe { (*reader).CurrentDoc() }
2707}
2708
2709// ═══════════════════════════════════════════════════════════════════════════════
2710// Tests
2711// ═══════════════════════════════════════════════════════════════════════════════
2712
2713#[cfg(test)]
2714mod tests {
2715    use super::*;
2716    use crate::abi::allocator::xmlFree;
2717    use core::ffi::c_void;
2718    use std::os::raw::c_char;
2719
2720    /// Helper: create a reader from a string.
2721    unsafe fn create_reader(xml: &str) -> *mut XmlTextReader {
2722        let bytes = xml.as_bytes();
2723        xmlReaderForMemory(
2724            bytes.as_ptr() as *const c_char,
2725            bytes.len() as c_int,
2726            ptr::null(),
2727            ptr::null(),
2728            0,
2729        )
2730    }
2731
2732    /// Helper: free a reader.
2733    unsafe fn free_reader(reader: *mut XmlTextReader) {
2734        if !reader.is_null() {
2735            xmlFreeTextReader(reader);
2736        }
2737    }
2738
2739    /// Helper: read through all nodes and collect their types and names.
2740    unsafe fn collect_nodes(reader: *mut XmlTextReader) -> Vec<(ReaderNodeType, String, i32)> {
2741        let mut result = Vec::new();
2742        loop {
2743            let ret = xmlTextReaderRead(reader);
2744            if ret <= 0 {
2745                break;
2746            }
2747            // SAFETY: reader is valid.
2748            let r = &*reader;
2749            let ntype = r.NodeType();
2750            let name = if r.name.is_null() {
2751                String::new()
2752            } else {
2753                xmlstr_to_string(r.name as *const xmlChar)
2754            };
2755            let depth = r.Depth();
2756            result.push((ntype, name, depth));
2757        }
2758        result
2759    }
2760
2761    // ─── Basic tests ───────────────────────────────────────────────────────
2762
2763    #[test]
2764    fn test_create_reader_from_memory() {
2765        unsafe {
2766            let reader = create_reader("<root/>");
2767            assert!(!reader.is_null());
2768            assert_eq!((*reader).ReadState(), ReadState::INITIALIZED);
2769            free_reader(reader);
2770        }
2771    }
2772
2773    #[test]
2774    fn test_read_simple_document() {
2775        unsafe {
2776            let reader = create_reader("<root><child>text</child></root>");
2777            assert!(!reader.is_null());
2778
2779            let nodes = collect_nodes(reader);
2780            // Expected sequence:
2781            // ELEMENT root (depth=0)
2782            // ELEMENT child (depth=1)
2783            // TEXT text (depth=2)
2784            // END_ELEMENT child (depth=1)
2785            // END_ELEMENT root (depth=0)
2786
2787            assert_eq!(nodes.len(), 5);
2788            assert_eq!(nodes[0], (ReaderNodeType::ELEMENT, "root".to_string(), 0));
2789            assert_eq!(nodes[1], (ReaderNodeType::ELEMENT, "child".to_string(), 1));
2790            assert_eq!(nodes[2], (ReaderNodeType::TEXT, "".to_string(), 2));
2791            assert_eq!(
2792                nodes[3],
2793                (ReaderNodeType::END_ELEMENT, "child".to_string(), 1)
2794            );
2795            assert_eq!(
2796                nodes[4],
2797                (ReaderNodeType::END_ELEMENT, "root".to_string(), 0)
2798            );
2799
2800            assert_eq!((*reader).ReadState(), ReadState::EOF);
2801            free_reader(reader);
2802        }
2803    }
2804
2805    #[test]
2806    fn test_read_state_transitions() {
2807        unsafe {
2808            let reader = create_reader("<root/>");
2809            assert!(!reader.is_null());
2810            assert_eq!((*reader).ReadState(), ReadState::INITIALIZED);
2811
2812            // First read.
2813            assert_eq!(xmlTextReaderRead(reader), 1);
2814            assert_eq!((*reader).ReadState(), ReadState::READING);
2815            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
2816            assert_eq!((*reader).Depth(), 0);
2817
2818            // Second read — should be END_ELEMENT.
2819            assert_eq!(xmlTextReaderRead(reader), 1);
2820            assert_eq!((*reader).NodeType(), ReaderNodeType::END_ELEMENT);
2821
2822            // Third read — EOF.
2823            assert_eq!(xmlTextReaderRead(reader), 0);
2824            assert_eq!((*reader).ReadState(), ReadState::EOF);
2825
2826            free_reader(reader);
2827        }
2828    }
2829
2830    #[test]
2831    fn test_null_reader_returns_error() {
2832        unsafe {
2833            assert_eq!(xmlTextReaderRead(ptr::null_mut()), -1);
2834            assert_eq!(xmlTextReaderDepth(ptr::null_mut()), -1);
2835            assert_eq!(xmlTextReaderNodeType(ptr::null_mut()), -1);
2836            assert!(xmlTextReaderName(ptr::null_mut()).is_null());
2837            assert!(xmlTextReaderValue(ptr::null_mut()).is_null());
2838            assert_eq!(xmlTextReaderHasValue(ptr::null_mut()), 0);
2839            assert_eq!(xmlTextReaderIsEmptyElement(ptr::null_mut()), 0);
2840            assert_eq!(
2841                xmlTextReaderReadState(ptr::null_mut()),
2842                ReadState::ERROR as c_int
2843            );
2844        }
2845    }
2846
2847    #[test]
2848    fn test_xmlFreeTextReader_null() {
2849        unsafe {
2850            // Should not crash.
2851            xmlFreeTextReader(ptr::null_mut());
2852        }
2853    }
2854
2855    #[test]
2856    fn test_reader_name_and_value() {
2857        unsafe {
2858            let reader = create_reader("<root>hello</root>");
2859            assert!(!reader.is_null());
2860
2861            // Read root element.
2862            assert_eq!(xmlTextReaderRead(reader), 1);
2863            let name = xmlTextReaderName(reader);
2864            assert!(!name.is_null());
2865            assert_eq!(xmlstr_to_string(name), "root");
2866            xmlFree(name as *mut c_void);
2867
2868            assert_eq!(xmlTextReaderHasValue(reader), 0);
2869
2870            // Read text node.
2871            assert_eq!(xmlTextReaderRead(reader), 1);
2872            assert_eq!((*reader).NodeType(), ReaderNodeType::TEXT);
2873            assert_eq!((*reader).HasValue(), 1);
2874
2875            let val = xmlTextReaderValue(reader);
2876            assert!(!val.is_null());
2877            assert_eq!(xmlstr_to_string(val), "hello");
2878            xmlFree(val as *mut c_void);
2879
2880            free_reader(reader);
2881        }
2882    }
2883
2884    #[test]
2885    fn test_empty_element() {
2886        unsafe {
2887            let reader = create_reader("<empty/>");
2888            assert!(!reader.is_null());
2889
2890            assert_eq!(xmlTextReaderRead(reader), 1);
2891            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
2892            assert_eq!((*reader).IsEmptyElement(), 1);
2893            assert_eq!((*reader).HasAttributes(), 0);
2894            assert_eq!((*reader).AttributeCount(), 0);
2895
2896            // END_ELEMENT.
2897            assert_eq!(xmlTextReaderRead(reader), 1);
2898            assert_eq!((*reader).NodeType(), ReaderNodeType::END_ELEMENT);
2899
2900            free_reader(reader);
2901        }
2902    }
2903
2904    #[test]
2905    fn test_element_with_attributes() {
2906        unsafe {
2907            let reader = create_reader(r#"<root a="1" b="2"/>"#);
2908            assert!(!reader.is_null());
2909
2910            assert_eq!(xmlTextReaderRead(reader), 1);
2911            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
2912            assert_eq!((*reader).HasAttributes(), 1);
2913
2914            // We know the attribute count if we've built the events properly.
2915            // The count_attributes checks the element's properties list.
2916            let attrs = xmlTextReaderAttributeCount(reader);
2917            assert_eq!(attrs, 2);
2918
2919            free_reader(reader);
2920        }
2921    }
2922
2923    #[test]
2924    fn test_attribute_navigation() {
2925        unsafe {
2926            let reader = create_reader(r#"<root a="1" b="2"></root>"#);
2927            assert!(!reader.is_null());
2928
2929            // Position on root element.
2930            assert_eq!(xmlTextReaderRead(reader), 1);
2931            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
2932
2933            // Move to first attribute.
2934            assert_eq!(xmlTextReaderMoveToFirstAttribute(reader), 1);
2935            assert_eq!((*reader).NodeType(), ReaderNodeType::ATTRIBUTE);
2936
2937            let name = xmlTextReaderConstName(reader);
2938            assert!(!name.is_null());
2939            assert_eq!(xmlstr_to_bytes(name), b"a");
2940
2941            let val = xmlTextReaderConstValue(reader);
2942            assert!(!val.is_null());
2943            assert_eq!(xmlstr_to_bytes(val), b"1");
2944
2945            // Move to next attribute.
2946            assert_eq!(xmlTextReaderMoveToNextAttribute(reader), 1);
2947            let name = xmlTextReaderConstName(reader);
2948            assert!(!name.is_null());
2949            assert_eq!(xmlstr_to_bytes(name), b"b");
2950            let val = xmlTextReaderConstValue(reader);
2951            assert!(!val.is_null());
2952            assert_eq!(xmlstr_to_bytes(val), b"2");
2953
2954            // No more attributes.
2955            assert_eq!(xmlTextReaderMoveToNextAttribute(reader), 0);
2956
2957            // Move back to element.
2958            assert_eq!(xmlTextReaderMoveToElement(reader), 1);
2959            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
2960
2961            // Move to attribute by name.
2962            assert_eq!(
2963                xmlTextReaderMoveToAttribute(reader, b"a\0" as *const u8 as *const xmlChar),
2964                1
2965            );
2966            assert_eq!((*reader).NodeType(), ReaderNodeType::ATTRIBUTE);
2967
2968            // Move to attribute by index.
2969            assert_eq!(xmlTextReaderMoveToElement(reader), 1);
2970            assert_eq!(xmlTextReaderMoveToAttributeNo(reader, 1), 1);
2971            assert_eq!((*reader).NodeType(), ReaderNodeType::ATTRIBUTE);
2972
2973            free_reader(reader);
2974        }
2975    }
2976
2977    #[test]
2978    fn test_get_attribute() {
2979        unsafe {
2980            let reader = create_reader(r#"<root a="hello" b="world"/>"#);
2981            assert!(!reader.is_null());
2982
2983            assert_eq!(xmlTextReaderRead(reader), 1);
2984
2985            // Get attribute by name.
2986            let val = xmlTextReaderGetAttribute(reader, b"a\0" as *const u8 as *const xmlChar);
2987            assert!(!val.is_null());
2988            assert_eq!(xmlstr_to_bytes(val), b"hello");
2989            xmlFree(val as *mut c_void);
2990
2991            let val = xmlTextReaderGetAttribute(reader, b"b\0" as *const u8 as *const xmlChar);
2992            assert!(!val.is_null());
2993            assert_eq!(xmlstr_to_bytes(val), b"world");
2994            xmlFree(val as *mut c_void);
2995
2996            // Non-existent attribute.
2997            let val = xmlTextReaderGetAttribute(reader, b"c\0" as *const u8 as *const xmlChar);
2998            assert!(val.is_null());
2999
3000            // Get attribute by index.
3001            let val = xmlTextReaderGetAttributeNo(reader, 0);
3002            assert!(!val.is_null());
3003            assert_eq!(xmlstr_to_bytes(val), b"hello");
3004            xmlFree(val as *mut c_void);
3005
3006            let val = xmlTextReaderGetAttributeNo(reader, 1);
3007            assert!(!val.is_null());
3008            assert_eq!(xmlstr_to_bytes(val), b"world");
3009            xmlFree(val as *mut c_void);
3010
3011            let val = xmlTextReaderGetAttributeNo(reader, 2);
3012            assert!(val.is_null());
3013
3014            free_reader(reader);
3015        }
3016    }
3017
3018    #[test]
3019    fn test_depth_tracking() {
3020        unsafe {
3021            let reader = create_reader("<a><b><c/></b></a>");
3022            assert!(!reader.is_null());
3023
3024            let nodes = collect_nodes(reader);
3025            // ELEMENT a (0), ELEMENT b (1), ELEMENT c (2),
3026            // END_ELEMENT c (2), END_ELEMENT b (1), END_ELEMENT a (0)
3027            assert_eq!(nodes.len(), 6);
3028            assert_eq!(nodes[0].2, 0); // a depth 0
3029            assert_eq!(nodes[1].2, 1); // b depth 1
3030            assert_eq!(nodes[2].2, 2); // c depth 2
3031            assert_eq!(nodes[3].2, 2); // END c depth 2
3032            assert_eq!(nodes[4].2, 1); // END b depth 1
3033            assert_eq!(nodes[5].2, 0); // END a depth 0
3034
3035            free_reader(reader);
3036        }
3037    }
3038
3039    #[test]
3040    fn test_multiple_siblings() {
3041        unsafe {
3042            let reader = create_reader("<root><a>A</a><b>B</b><c>C</c></root>");
3043            assert!(!reader.is_null());
3044
3045            let nodes = collect_nodes(reader);
3046            // ELEMENT root(0), ELEMENT a(1), TEXT(2), END a(1),
3047            // ELEMENT b(1), TEXT(2), END b(1),
3048            // ELEMENT c(1), TEXT(2), END c(1),
3049            // END root(0)
3050            assert_eq!(nodes.len(), 11);
3051
3052            // Check the sibling elements.
3053            assert_eq!(nodes[1], (ReaderNodeType::ELEMENT, "a".to_string(), 1));
3054            assert_eq!(nodes[4], (ReaderNodeType::ELEMENT, "b".to_string(), 1));
3055            assert_eq!(nodes[7], (ReaderNodeType::ELEMENT, "c".to_string(), 1));
3056
3057            free_reader(reader);
3058        }
3059    }
3060
3061    #[test]
3062    fn test_next_skip_to_sibling() {
3063        unsafe {
3064            let reader = create_reader("<root><a>A</a><b>B</b><c>C</c></root>");
3065            assert!(!reader.is_null());
3066
3067            // Read to first node (root element).
3068            assert_eq!(xmlTextReaderRead(reader), 1);
3069            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
3070
3071            // Read to a.
3072            assert_eq!(xmlTextReaderRead(reader), 1);
3073            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
3074            assert_eq!(xmlstr_to_string((*reader).name as *const xmlChar), "a");
3075
3076            // Read to text of a.
3077            assert_eq!(xmlTextReaderRead(reader), 1);
3078            assert_eq!((*reader).NodeType(), ReaderNodeType::TEXT);
3079
3080            // Skip to next sibling — should skip END a and go to ELEMENT b.
3081            assert_eq!(xmlTextReaderNext(reader), 1);
3082            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
3083            assert_eq!(xmlstr_to_string((*reader).name as *const xmlChar), "b");
3084
3085            // Next again — should go to c.
3086            assert_eq!(xmlTextReaderNext(reader), 1);
3087            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
3088            assert_eq!(xmlstr_to_string((*reader).name as *const xmlChar), "c");
3089
3090            // Next again — no more siblings.
3091            assert_eq!(xmlTextReaderNext(reader), 0);
3092
3093            free_reader(reader);
3094        }
3095    }
3096
3097    #[test]
3098    fn test_comment_and_pi_nodes() {
3099        unsafe {
3100            let xml = b"<?pi target?><root><!-- comment -->text</root>\0";
3101            let reader = xmlReaderForMemory(
3102                xml.as_ptr() as *const c_char,
3103                (xml.len() - 1) as c_int,
3104                ptr::null(),
3105                ptr::null(),
3106                0,
3107            );
3108            assert!(!reader.is_null());
3109
3110            let nodes = collect_nodes(reader);
3111            // PI, ELEMENT root, COMMENT, TEXT, END_ELEMENT root
3112            // Note: PI appears as PROCESSING_INSTRUCTION node.
3113            assert!(!nodes.is_empty(), "no nodes collected: {:?}", nodes);
3114
3115            // Check PI.
3116            assert_eq!(
3117                nodes[0].0,
3118                ReaderNodeType::PROCESSING_INSTRUCTION,
3119                "expected PI at nodes[0], got {:?} name={}",
3120                nodes[0].0,
3121                nodes[0].1
3122            );
3123            assert_eq!(
3124                nodes[0].0,
3125                ReaderNodeType::PROCESSING_INSTRUCTION,
3126                "expected PI at nodes[0], got {:?} name={}",
3127                nodes[0].0,
3128                nodes[0].1
3129            );
3130
3131            // Check root element.
3132            let root_idx = nodes
3133                .iter()
3134                .position(|(t, n, _)| *t == ReaderNodeType::ELEMENT && n == "root");
3135            assert!(
3136                root_idx.is_some(),
3137                "no ELEMENT root found in nodes: {:?}",
3138                nodes
3139                    .iter()
3140                    .map(|(t, n, _)| format!("{:?}:{}", t, n))
3141                    .collect::<Vec<_>>()
3142            );
3143
3144            // Check comment.
3145            let comment_idx = nodes
3146                .iter()
3147                .position(|(t, _, _)| *t == ReaderNodeType::COMMENT);
3148            assert!(comment_idx.is_some(), "no COMMENT found");
3149
3150            // Check text.
3151            let text_idx = nodes
3152                .iter()
3153                .position(|(t, _, _)| *t == ReaderNodeType::TEXT);
3154            assert!(text_idx.is_some(), "no TEXT found");
3155
3156            free_reader(reader);
3157        }
3158    }
3159
3160    #[test]
3161    fn test_local_name() {
3162        unsafe {
3163            // We need a namespace-aware element. For now, test without namespace.
3164            let reader = create_reader("<root/>");
3165            assert!(!reader.is_null());
3166
3167            assert_eq!(xmlTextReaderRead(reader), 1);
3168            let local = xmlTextReaderLocalName(reader);
3169            assert!(!local.is_null());
3170            assert_eq!(xmlstr_to_bytes(local), b"root");
3171            xmlFree(local as *mut c_void);
3172
3173            free_reader(reader);
3174        }
3175    }
3176
3177    #[test]
3178    fn test_base_uri() {
3179        unsafe {
3180            let reader = create_reader("<root/>");
3181            assert!(!reader.is_null());
3182
3183            assert_eq!(xmlTextReaderRead(reader), 1);
3184            // Base URI should be NULL for memory-created readers.
3185            let uri = xmlTextReaderBaseUri(reader);
3186            assert!(uri.is_null());
3187
3188            free_reader(reader);
3189        }
3190    }
3191
3192    #[test]
3193    fn test_lookup_namespace() {
3194        unsafe {
3195            let reader = create_reader(r#"<root xmlns:ns="http://example.com"><ns:child/></root>"#);
3196            assert!(!reader.is_null());
3197
3198            // Read to root.
3199            assert_eq!(xmlTextReaderRead(reader), 1);
3200            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
3201
3202            // Read to child (ns:child).
3203            assert_eq!(xmlTextReaderRead(reader), 1);
3204
3205            // Lookup the "ns" prefix.
3206            let uri = xmlTextReaderLookupNamespace(reader, b"ns\0" as *const u8 as *const xmlChar);
3207            assert!(!uri.is_null());
3208            assert_eq!(xmlstr_to_bytes(uri), b"http://example.com");
3209            xmlFree(uri as *mut c_void);
3210
3211            // Lookup default namespace (NULL prefix).
3212            let uri = xmlTextReaderLookupNamespace(reader, ptr::null());
3213            assert!(uri.is_null());
3214
3215            // Lookup non-existent prefix.
3216            let uri = xmlTextReaderLookupNamespace(
3217                reader,
3218                b"nonexistent\0" as *const u8 as *const xmlChar,
3219            );
3220            assert!(uri.is_null());
3221
3222            free_reader(reader);
3223        }
3224    }
3225
3226    #[test]
3227    fn test_parser_properties() {
3228        unsafe {
3229            let reader = create_reader("<root/>");
3230            assert!(!reader.is_null());
3231
3232            // Get default properties.
3233            assert_eq!(xmlTextReaderGetParserProp(reader, 1), 0); // LOADDTD
3234            assert_eq!(xmlTextReaderGetParserProp(reader, 2), 0); // DEFAULTATTRS
3235            assert_eq!(xmlTextReaderGetParserProp(reader, 3), 0); // VALIDATE
3236            assert_eq!(xmlTextReaderGetParserProp(reader, 4), 0); // SUBST_ENTITIES
3237
3238            // Set and verify.
3239            assert_eq!(xmlTextReaderSetParserProp(reader, 1, 1), 0);
3240            assert_eq!(xmlTextReaderGetParserProp(reader, 1), 1);
3241
3242            assert_eq!(xmlTextReaderSetParserProp(reader, 4, 1), 0);
3243            assert_eq!(xmlTextReaderGetParserProp(reader, 4), 1);
3244
3245            // Invalid property.
3246            assert_eq!(xmlTextReaderGetParserProp(reader, 99), -1);
3247            assert_eq!(xmlTextReaderSetParserProp(reader, 99, 1), -1);
3248
3249            free_reader(reader);
3250        }
3251    }
3252
3253    #[test]
3254    fn test_current_doc() {
3255        unsafe {
3256            let reader = create_reader("<root/>");
3257            assert!(!reader.is_null());
3258
3259            // Before reading, doc should be null.
3260            assert!((*reader).CurrentDoc().is_null());
3261
3262            // After reading, doc should be available.
3263            assert_eq!(xmlTextReaderRead(reader), 1);
3264            let doc = xmlTextReaderCurrentDoc(reader);
3265            assert!(!doc.is_null());
3266
3267            free_reader(reader);
3268        }
3269    }
3270
3271    #[test]
3272    fn test_free_reader_after_read() {
3273        unsafe {
3274            let reader = create_reader("<root><child/></root>");
3275            assert!(!reader.is_null());
3276
3277            // Read through the document.
3278            while xmlTextReaderRead(reader) > 0 {}
3279            assert_eq!((*reader).ReadState(), ReadState::EOF);
3280
3281            // Free should not crash.
3282            free_reader(reader);
3283        }
3284    }
3285
3286    #[test]
3287    fn test_reader_for_memory_null_buffer() {
3288        unsafe {
3289            let reader = xmlReaderForMemory(ptr::null(), 10, ptr::null(), ptr::null(), 0);
3290            assert!(reader.is_null());
3291        }
3292    }
3293
3294    #[test]
3295    fn test_reader_for_memory_empty_size() {
3296        unsafe {
3297            let data = b"<root/>";
3298            let reader = xmlReaderForMemory(
3299                data.as_ptr() as *const c_char,
3300                0,
3301                ptr::null(),
3302                ptr::null(),
3303                0,
3304            );
3305            assert!(reader.is_null());
3306        }
3307    }
3308
3309    #[test]
3310    fn test_reader_for_file_not_found() {
3311        unsafe {
3312            let filename = b"/nonexistent/file.xml\0" as *const u8 as *const c_char;
3313            let reader = xmlReaderForFile(filename, ptr::null(), 0);
3314            assert!(reader.is_null());
3315        }
3316    }
3317
3318    #[test]
3319    fn test_const_name_and_value() {
3320        unsafe {
3321            let reader = create_reader("<root>text</root>");
3322            assert!(!reader.is_null());
3323
3324            // Root element.
3325            assert_eq!(xmlTextReaderRead(reader), 1);
3326            let cname = xmlTextReaderConstName(reader);
3327            assert!(!cname.is_null());
3328            assert_eq!(xmlstr_to_bytes(cname), b"root");
3329
3330            // Text node.
3331            assert_eq!(xmlTextReaderRead(reader), 1);
3332            let cval = xmlTextReaderConstValue(reader);
3333            assert!(!cval.is_null());
3334            assert_eq!(xmlstr_to_bytes(cval), b"text");
3335
3336            free_reader(reader);
3337        }
3338    }
3339
3340    #[test]
3341    fn test_complex_nested_document() {
3342        unsafe {
3343            let xml = r#"<?xml version="1.0"?>
3344<library>
3345  <book id="1">
3346    <title>XML Fundamentals</title>
3347    <author>John Doe</author>
3348  </book>
3349  <book id="2">
3350    <title>XSLT Recipes</title>
3351    <author>Jane Smith</author>
3352  </book>
3353</library>"#;
3354
3355            let reader = create_reader(xml);
3356            assert!(!reader.is_null());
3357
3358            let mut element_count = 0;
3359            let mut end_element_count = 0;
3360            let mut text_count = 0;
3361            let mut pi_count = 0;
3362
3363            loop {
3364                let ret = xmlTextReaderRead(reader);
3365                if ret <= 0 {
3366                    break;
3367                }
3368                match (*reader).NodeType() {
3369                    ReaderNodeType::ELEMENT => element_count += 1,
3370                    ReaderNodeType::END_ELEMENT => end_element_count += 1,
3371                    ReaderNodeType::TEXT => text_count += 1,
3372                    ReaderNodeType::PROCESSING_INSTRUCTION => pi_count += 1,
3373                    _ => {}
3374                }
3375            }
3376
3377            // Elements: library, book(2), title(2), author(2) = 7
3378            assert_eq!(element_count, 7);
3379            // End elements: same count as elements
3380            assert_eq!(end_element_count, 7);
3381            // Text nodes: one per title and author = 4
3382            assert_eq!(text_count, 4);
3383            // UPSTREAM-PARITY: XML declaration (<?xml ...?>) is NOT stored as
3384            // a PI node in the tree. It is consumed by the parser and stored
3385            // in the document's version/encoding fields. Only <?pi ...?> nodes
3386            // (processing instructions) appear as XML_PI_NODE in the tree.
3387            assert_eq!(pi_count, 0);
3388
3389            free_reader(reader);
3390        }
3391    }
3392
3393    #[test]
3394    fn test_setup_reinitialize() {
3395        unsafe {
3396            let reader = create_reader("<root/>");
3397            assert!(!reader.is_null());
3398
3399            // Read through.
3400            assert_eq!(xmlTextReaderRead(reader), 1);
3401            assert_eq!((*reader).ReadState(), ReadState::READING);
3402
3403            // Setup with new input (simulate re-initialization).
3404            // For this test, we just verify the setup function exists and
3405            // handles a NULL input gracefully (resetting the reader).
3406            assert_eq!(
3407                xmlTextReaderSetup(reader, ptr::null_mut(), ptr::null(), ptr::null(), 0),
3408                0
3409            );
3410            assert_eq!((*reader).ReadState(), ReadState::INITIALIZED);
3411
3412            free_reader(reader);
3413        }
3414    }
3415
3416    #[test]
3417    fn test_has_attributes_on_non_element() {
3418        unsafe {
3419            let reader = create_reader("<root>text</root>");
3420            assert!(!reader.is_null());
3421
3422            // Position on text node.
3423            assert_eq!(xmlTextReaderRead(reader), 1); // root element
3424            assert_eq!((*reader).HasAttributes(), 0); // 0 attributes on root
3425            assert_eq!(xmlTextReaderRead(reader), 1); // text
3426            assert_eq!((*reader).HasAttributes(), 0);
3427
3428            free_reader(reader);
3429        }
3430    }
3431
3432    #[test]
3433    fn test_prev_sibling() {
3434        unsafe {
3435            let reader = create_reader("<root><a/><b/><c/></root>");
3436            assert!(!reader.is_null());
3437
3438            // Read through the document.
3439            while xmlTextReaderRead(reader) > 0 {
3440                // Skip to END_ELEMENT root or beyond.
3441            }
3442
3443            // Can't go prev after EOF.
3444            assert_eq!(xmlTextReaderPrev(reader), -1);
3445
3446            free_reader(reader);
3447        }
3448    }
3449
3450    #[test]
3451    fn test_move_to_attribute_no_not_on_element() {
3452        unsafe {
3453            let reader = create_reader("<root>text</root>");
3454            assert!(!reader.is_null());
3455
3456            assert_eq!(xmlTextReaderRead(reader), 1); // root
3457            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
3458
3459            // Move to non-existent attribute index.
3460            assert_eq!(xmlTextReaderMoveToAttributeNo(reader, 0), 0);
3461
3462            free_reader(reader);
3463        }
3464    }
3465
3466    #[test]
3467    fn test_get_attribute_ns() {
3468        unsafe {
3469            let reader = create_reader(r#"<root a="1" b="2"/>"#);
3470            assert!(!reader.is_null());
3471
3472            assert_eq!(xmlTextReaderRead(reader), 1);
3473
3474            // Get attribute by local name only (namespaceURI is NULL).
3475            let val = xmlTextReaderGetAttributeNs(
3476                reader,
3477                b"a\0" as *const u8 as *const xmlChar,
3478                ptr::null(),
3479            );
3480            assert!(!val.is_null());
3481            assert_eq!(xmlstr_to_bytes(val), b"1");
3482            xmlFree(val as *mut c_void);
3483
3484            free_reader(reader);
3485        }
3486    }
3487
3488    #[test]
3489    fn test_mixed_content() {
3490        unsafe {
3491            let reader = create_reader("<root>before<child/>after</root>");
3492            assert!(!reader.is_null());
3493
3494            let nodes = collect_nodes(reader);
3495            // ELEMENT root(0), TEXT "before"(1), ELEMENT child(1),
3496            // END_ELEMENT child(1), TEXT "after"(1), END_ELEMENT root(0)
3497            assert_eq!(nodes.len(), 6);
3498            assert_eq!(nodes[0], (ReaderNodeType::ELEMENT, "root".to_string(), 0));
3499            assert_eq!(nodes[1].0, ReaderNodeType::TEXT);
3500            assert_eq!(nodes[2], (ReaderNodeType::ELEMENT, "child".to_string(), 1));
3501            assert_eq!(nodes[4].0, ReaderNodeType::TEXT);
3502
3503            free_reader(reader);
3504        }
3505    }
3506
3507    #[test]
3508    fn test_error_handling_invalid_xml() {
3509        unsafe {
3510            // Malformed XML.
3511            let data = b"<root><\0" as *const u8 as *const c_char;
3512            let reader = xmlReaderForMemory(data, 7, ptr::null(), ptr::null(), 0);
3513            assert!(!reader.is_null());
3514
3515            // Reading should fail.
3516            let ret = xmlTextReaderRead(reader);
3517            assert!(ret == -1 || ret == 0);
3518
3519            free_reader(reader);
3520        }
3521    }
3522
3523    #[test]
3524    fn test_reader_with_options() {
3525        unsafe {
3526            let data = b"<root/>\0" as *const u8 as *const c_char;
3527            let reader = xmlReaderForMemory(
3528                data,
3529                7,
3530                ptr::null(),
3531                ptr::null(),
3532                XML_PARSE_NOENT | XML_PARSE_DTDLOAD,
3533            );
3534            assert!(!reader.is_null());
3535
3536            // Verify options were set.
3537            assert_eq!((*reader).options & XML_PARSE_NOENT, XML_PARSE_NOENT);
3538            assert_eq!((*reader).options & XML_PARSE_DTDLOAD, XML_PARSE_DTDLOAD);
3539
3540            assert_eq!(xmlTextReaderRead(reader), 1);
3541            free_reader(reader);
3542        }
3543    }
3544
3545    #[test]
3546    fn test_reader_for_fd() {
3547        unsafe {
3548            // Create a temp file and test xmlReaderForFd.
3549            let tmp_path = "/tmp/libxml_rs_test_reader_fd.xml";
3550            let tmp_cstr = std::ffi::CString::new(tmp_path).unwrap();
3551            let content = b"<root><data/></root>";
3552            let fd = libc::open(
3553                tmp_cstr.as_ptr(),
3554                libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC,
3555                0o644,
3556            );
3557            assert!(fd >= 0);
3558            libc::write(fd, content.as_ptr() as *const c_void, content.len());
3559            libc::close(fd);
3560
3561            // Open for reading.
3562            let fd = libc::open(tmp_cstr.as_ptr(), libc::O_RDONLY, 0);
3563            assert!(fd >= 0);
3564
3565            let reader = xmlReaderForFd(fd, ptr::null(), ptr::null(), 0);
3566            assert!(!reader.is_null());
3567
3568            let nodes = collect_nodes(reader);
3569            assert_eq!(nodes.len(), 4); // ELEMENT root, ELEMENT data, END data, END root
3570
3571            free_reader(reader);
3572            libc::close(fd);
3573            std::fs::remove_file(tmp_path).ok();
3574        }
3575    }
3576
3577    #[test]
3578    fn test_reader_for_io() {
3579        unsafe {
3580            extern "C" fn io_read(context: *mut c_void, buffer: *mut c_char, len: c_int) -> c_int {
3581                if context.is_null() || buffer.is_null() || len <= 0 {
3582                    return -1;
3583                }
3584                // SAFETY: context points to an IoCtx struct.
3585                let ctx = unsafe { &mut *(context as *mut IoCtx) };
3586                if ctx.pos >= ctx.data.len() {
3587                    return 0;
3588                }
3589                let remaining = ctx.data.len() - ctx.pos;
3590                let to_copy = if (remaining as c_int) < len {
3591                    remaining
3592                } else {
3593                    len as usize
3594                };
3595                // SAFETY: buffer has at least `len` bytes of space.
3596                unsafe {
3597                    std::ptr::copy_nonoverlapping(
3598                        ctx.data.as_ptr().add(ctx.pos),
3599                        buffer as *mut u8,
3600                        to_copy,
3601                    );
3602                }
3603                ctx.pos += to_copy;
3604                to_copy as c_int
3605            }
3606
3607            extern "C" fn io_close(_context: *mut c_void) -> c_int {
3608                0
3609            }
3610
3611            struct IoCtx {
3612                data: &'static [u8],
3613                pos: usize,
3614            }
3615            let mut ctx = IoCtx {
3616                data: b"<root/>",
3617                pos: 0,
3618            };
3619
3620            let reader = xmlReaderForIO(
3621                Some(io_read),
3622                Some(io_close),
3623                &mut ctx as *mut IoCtx as *mut c_void,
3624                ptr::null(),
3625                ptr::null(),
3626                0,
3627            );
3628            assert!(!reader.is_null());
3629
3630            // Read through the document.
3631            assert_eq!(xmlTextReaderRead(reader), 1);
3632            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
3633            let cname = xmlTextReaderConstName(reader);
3634            assert!(!cname.is_null());
3635            assert_eq!(xmlstr_to_bytes(cname), b"root");
3636
3637            assert_eq!(xmlTextReaderRead(reader), 1);
3638            assert_eq!((*reader).NodeType(), ReaderNodeType::END_ELEMENT);
3639
3640            assert_eq!(xmlTextReaderRead(reader), 0); // EOF
3641
3642            free_reader(reader);
3643        }
3644    }
3645}