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, c_long, c_uint};
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    input_from_memory_named, 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    NAMESPACE = 18,
100}
101
102/// Reader read state (xmlTextReaderReadState enum).
103///
104/// # UPSTREAM-PARITY
105///
106/// ```c
107/// typedef enum {
108///     XML_TEXTREADER_NOT_INITIALIZED = 0,
109///     XML_TEXTREADER_INITIALIZED = 1,
110///     XML_TEXTREADER_READING = 2,
111///     XML_TEXTREADER_EOF = 3,
112///     XML_TEXTREADER_CLOSED = 4,
113///     XML_TEXTREADER_ERROR = 5
114/// } xmlTextReaderReadState;
115/// ```
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117#[repr(i32)]
118pub(crate) enum ReadState {
119    NOT_INITIALIZED = 0,
120    INITIALIZED = 1,
121    READING = 2,
122    EOF = 3,
123    CLOSED = 4,
124    ERROR = 5,
125}
126
127/// Parser properties for xmlTextReaderGetParserProp / SetParserProp.
128///
129/// # UPSTREAM-PARITY
130///
131/// ```c
132/// typedef enum {
133///     XML_PARSER_LOADDTD = 1,
134///     XML_PARSER_DEFAULTATTRS = 2,
135///     XML_PARSER_VALIDATE = 3,
136///     XML_PARSER_SUBST_ENTITIES = 4
137/// } xmlParserProperties;
138/// ```
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140#[repr(i32)]
141pub(crate) enum ParserProp {
142    LOADDTD = 1,
143    DEFAULTATTRS = 2,
144    VALIDATE = 3,
145    SUBST_ENTITIES = 4,
146}
147
148/// A traversal event in the document-order walk of the parsed tree.
149///
150/// Each event represents either entering a node (ELEMENT, TEXT, etc.) or
151/// exiting an element (END_ELEMENT). The `depth` is the element nesting
152/// depth at the time of the event.
153#[derive(Debug, Clone)]
154struct TraversalEvent {
155    /// The node this event refers to.
156    node: *mut _xmlNode,
157    /// Whether this is an "exit" event (END_ELEMENT).
158    is_end: bool,
159    /// The depth at this event (number of ancestor elements).
160    depth: i32,
161}
162
163/// Compute the element nesting depth of a node in the tree.
164///
165/// Counts the number of `XML_ELEMENT_NODE` ancestors.
166///
167/// # Safety
168///
169/// `node` must be a valid pointer to a node in a valid tree, or NULL.
170unsafe fn compute_depth(node: *mut _xmlNode) -> i32 {
171    if node.is_null() {
172        return 0;
173    }
174    let mut depth: i32 = 0;
175    // SAFETY: node is valid, and parent pointers form a tree.
176    let mut cur = unsafe { (*node).parent };
177    while !cur.is_null() {
178        // SAFETY: cur is valid.
179        if unsafe { (*cur).type_ } == XML_ELEMENT_NODE as c_int {
180            depth += 1;
181        }
182        // SAFETY: cur's parent is valid.
183        cur = unsafe { (*cur).parent };
184    }
185    depth
186}
187
188/// Convert an `xmlElementType` to the corresponding `ReaderNodeType`.
189fn element_type_to_reader_type(etype: c_int) -> ReaderNodeType {
190    match etype {
191        x if x == XML_ELEMENT_NODE as c_int => ReaderNodeType::ELEMENT,
192        x if x == XML_ATTRIBUTE_NODE as c_int => ReaderNodeType::ATTRIBUTE,
193        x if x == XML_TEXT_NODE as c_int => ReaderNodeType::TEXT,
194        x if x == XML_CDATA_SECTION_NODE as c_int => ReaderNodeType::CDATA,
195        x if x == XML_ENTITY_REF_NODE as c_int => ReaderNodeType::ENTITY_REFERENCE,
196        x if x == XML_ENTITY_NODE as c_int => ReaderNodeType::ENTITY,
197        x if x == XML_PI_NODE as c_int => ReaderNodeType::PROCESSING_INSTRUCTION,
198        x if x == XML_COMMENT_NODE as c_int => ReaderNodeType::COMMENT,
199        x if x == XML_DOCUMENT_NODE as c_int => ReaderNodeType::DOCUMENT,
200        x if x == XML_DOCUMENT_TYPE_NODE as c_int => ReaderNodeType::DOCUMENT_TYPE,
201        x if x == XML_DOCUMENT_FRAG_NODE as c_int => ReaderNodeType::DOCUMENT_FRAGMENT,
202        x if x == XML_NOTATION_NODE as c_int => ReaderNodeType::NOTATION,
203        x if x == XML_DTD_NODE as c_int => ReaderNodeType::DOCUMENT_TYPE,
204        x if x == XML_NAMESPACE_DECL as c_int => ReaderNodeType::NONE,
205        _ => ReaderNodeType::NONE,
206    }
207}
208
209/// Check whether a text node consists entirely of whitespace.
210fn is_whitespace_only(text: &[u8]) -> bool {
211    text.iter()
212        .all(|&b| b == b' ' || b == b'\t' || b == b'\n' || b == b'\r')
213}
214
215// ═══════════════════════════════════════════════════════════════════════════════
216// XmlTextReader — Internal Rust Type
217// ═══════════════════════════════════════════════════════════════════════════════
218
219/// The internal representation of an `xmlTextReader`.
220///
221/// This struct holds all state for the reader cursor: the parsed document,
222/// the current position in the traversal, attribute navigation state, and
223/// cached information about the current node.
224/// The element the reader is currently positioned on.
225#[derive(Clone, Copy)]
226enum AttrTarget {
227    None,
228    Ns(*mut crate::abi::structs::_xmlNs),
229    Prop(*mut _xmlAttr),
230}
231
232pub(crate) struct XmlTextReader {
233    /// The parsed XML document.
234    doc: *mut _xmlDoc,
235    /// The parser context used to parse the document (NULL after parsing).
236    ctxt: *mut _xmlParserCtxt,
237    /// The traversal events computed from the parsed tree.
238    events: Vec<TraversalEvent>,
239    /// Index into `events` for the current position.
240    event_index: usize,
241    /// Current read state.
242    state: ReadState,
243    /// The current node we're positioned on.
244    cur_node: *mut _xmlNode,
245    /// Current node type (reader node type).
246    node_type: ReaderNodeType,
247    /// Current depth.
248    depth: i32,
249    /// Cached name of the current node (xmlMalloc'd, NULL if none).
250    name: *mut xmlChar,
251    /// Cached value of the current node (xmlMalloc'd, NULL if none).
252    value: *mut xmlChar,
253    /// Number of attributes on the current element (-1 if not applicable).
254    attribute_count: i32,
255    /// Current attribute index (-1 = not on an attribute).
256    cur_attribute: i32,
257    /// Parser options bitmask.
258    options: c_int,
259    /// Document encoding string (xmlMalloc'd).
260    encoding: *mut xmlChar,
261    /// Document URL (xmlMalloc'd).
262    URL: *mut xmlChar,
263    /// Collected error messages.
264    errors: Vec<String>,
265    /// Whether the document has been parsed.
266    parsed: bool,
267    /// Reader error callback (xmlTextReaderSetErrorHandler).
268    error_handler: Option<xmlTextReaderErrorFunc>,
269    /// User data for the error callback.
270    error_arg: *mut c_void,
271    /// Structured error callback (xmlTextReaderSetStructuredErrorHandler).
272    structured_handler: Option<crate::abi::callbacks::xmlStructuredErrorFunc>,
273    /// User data for the structured callback.
274    structured_arg: *mut c_void,
275    /// Cached last-error struct (xmlTextReaderGetLastError); message owned.
276    last_err: crate::abi::structs::_xmlError,
277    /// Maximum entity amplification ratio (xmlTextReaderSetMaxAmplification).
278    max_amplification: c_int,
279    /// Schema set via xmlTextReaderSetSchema.
280    schema: *mut c_void,
281    /// RELAX NG schema set via xmlTextReaderRelaxNGSetSchema.
282    rng: *mut c_void,
283    /// Whether the reader owns `doc` (parse paths: yes; walker: no).
284    owns_doc: bool,
285    /// Whether the current attribute position is a namespace declaration
286    /// (xmlTextReaderIsNamespaceDecl; ns-decls are exposed as attributes).
287    cur_attr_is_ns: bool,
288}
289
290impl XmlTextReader {
291    /// Create a new reader with the given parser context and options.
292    ///
293    /// The reader takes ownership of the parser context. The document will be
294    /// parsed on the first call to `Read()`.
295    ///
296    /// # Safety
297    ///
298    /// `ctxt` must be a valid parser context created by `create_parser_ctxt`
299    /// and set up with input via `setup_parser_input`.
300    unsafe fn new(ctxt: *mut _xmlParserCtxt, URL: Option<&[u8]>, encoding: Option<&[u8]>) -> Self {
301        let url_ptr = URL
302            .map(|u| unsafe { bytes_to_xmlstr(u) })
303            .unwrap_or(ptr::null_mut());
304        let enc_ptr = encoding
305            .map(|e| unsafe { bytes_to_xmlstr(e) })
306            .unwrap_or(ptr::null_mut());
307
308        XmlTextReader {
309            doc: ptr::null_mut(),
310            ctxt,
311            events: Vec::new(),
312            event_index: 0,
313            state: ReadState::INITIALIZED,
314            cur_node: ptr::null_mut(),
315            node_type: ReaderNodeType::NONE,
316            depth: 0,
317            name: ptr::null_mut(),
318            value: ptr::null_mut(),
319            attribute_count: -1,
320            cur_attribute: -1,
321            options: 0,
322            encoding: enc_ptr,
323            URL: url_ptr,
324            errors: Vec::new(),
325            parsed: false,
326            error_handler: None,
327            error_arg: ptr::null_mut(),
328            structured_handler: None,
329            structured_arg: ptr::null_mut(),
330            last_err: unsafe { core::mem::zeroed() },
331            max_amplification: 0,
332            schema: ptr::null_mut(),
333            rng: ptr::null_mut(),
334            owns_doc: true,
335            cur_attr_is_ns: false,
336        }
337    }
338
339    /// Parse the document and build the event list.
340    ///
341    /// Returns 0 on success, -1 on error.
342    ///
343    /// # Safety
344    ///
345    /// `ctxt` must be a valid parser context with input set up.
346    unsafe fn parse_and_build_events(&mut self) -> c_int {
347        if self.ctxt.is_null() {
348            self.state = ReadState::ERROR;
349            self.errors.push("No parser context".to_string());
350            return -1;
351        }
352
353        // Set options on the context.
354        unsafe {
355            (*self.ctxt).options = self.options;
356        }
357
358        // Parse the document.
359        let result = unsafe { parse_document(self.ctxt) };
360
361        // Get the parsed document.
362        let doc = unsafe { (*self.ctxt).myDoc };
363        self.doc = doc;
364
365        // Free the parser context - we no longer need it.
366        if !self.ctxt.is_null() {
367            unsafe { free_parser_ctxt(self.ctxt) };
368        }
369        self.ctxt = ptr::null_mut();
370
371        if result != 0 || doc.is_null() {
372            self.state = ReadState::ERROR;
373            self.errors.push("Failed to parse document".to_string());
374            return -1;
375        }
376
377        // Set the encoding from the document if not already set.
378        if self.encoding.is_null() && !doc.is_null() {
379            // SAFETY: doc is valid.
380            let doc_enc = unsafe { (*doc).encoding };
381            if !doc_enc.is_null() {
382                self.encoding = unsafe { xml_strdup(doc_enc as *const xmlChar) };
383            }
384        }
385
386        // Build traversal events from the tree.
387        self.build_events();
388
389        self.parsed = true;
390        0
391    }
392
393    /// Walk the tree in document order and build traversal events.
394    ///
395    /// Generates events for all nodes (ELEMENT, TEXT, COMMENT, PI, etc.)
396    /// and END_ELEMENT events for elements.
397    fn build_events(&mut self) {
398        self.events.clear();
399
400        if self.doc.is_null() {
401            return;
402        }
403
404        // SAFETY: doc is valid.
405        let root = unsafe { (*self.doc).children };
406        if root.is_null() {
407            return;
408        }
409
410        // Walk all top-level children (PIs, comments, the root element, etc.)
411        // SAFETY: The tree is valid and all pointers are valid.
412        unsafe {
413            let mut n = root;
414            while !n.is_null() {
415                self.walk_tree(n, 0);
416                n = (*n).next;
417            }
418        }
419    }
420
421    /// Recursively walk a subtree and generate events.
422    ///
423    /// # Safety
424    ///
425    /// `node` must be a valid pointer to a node in the parsed tree.
426    unsafe fn walk_tree(&mut self, node: *mut _xmlNode, depth: i32) {
427        if node.is_null() {
428            return;
429        }
430
431        // SAFETY: node is valid.
432        let node_type = unsafe { (*node).type_ };
433
434        // For elements, generate an enter event and then recursively visit children,
435        // then generate an exit (END_ELEMENT) event — unless the element is
436        // empty (upstream: empty elements produce only the start event).
437        if node_type == XML_ELEMENT_NODE as c_int {
438            self.events.push(TraversalEvent {
439                node,
440                is_end: false,
441                depth,
442            });
443
444            // Walk children.
445            // SAFETY: node's children are valid.
446            let mut child = unsafe { (*node).children };
447            while !child.is_null() {
448                let child_depth = depth + 1;
449                self.walk_tree(child, child_depth);
450                // SAFETY: child's next pointer is valid.
451                child = unsafe { (*child).next };
452            }
453
454            // Generate END_ELEMENT for non-empty elements only (upstream
455            // xmlreader.c: empty elements have no end event).
456            if !unsafe { (*node).children }.is_null() {
457                self.events.push(TraversalEvent {
458                    node,
459                    is_end: true,
460                    depth,
461                });
462            }
463        } else if node_type == XML_TEXT_NODE as c_int
464            || node_type == XML_CDATA_SECTION_NODE as c_int
465            || node_type == XML_COMMENT_NODE as c_int
466            || node_type == XML_PI_NODE as c_int
467            || node_type == XML_ENTITY_REF_NODE as c_int
468        {
469            // Leaf nodes: text, CDATA, comment, PI, entity reference.
470            // Whitespace-only text is emitted (as SIGNIFICANT_WHITESPACE by
471            // position_at) — upstream reader default behavior without
472            // XML_PARSE_NOBLANKS.
473            self.events.push(TraversalEvent {
474                node,
475                is_end: false,
476                depth,
477            });
478        } else {
479            // Other node types (ENTITY, NOTATION, DTD, etc.) — skip or just enter.
480            self.events.push(TraversalEvent {
481                node,
482                is_end: false,
483                depth,
484            });
485        }
486    }
487
488    /// Position the reader on the event at the given index.
489    ///
490    /// Updates all cached fields (name, value, depth, node_type, etc.).
491    fn position_at(&mut self, index: usize) {
492        if index >= self.events.len() {
493            self.state = ReadState::EOF;
494            self.cur_node = ptr::null_mut();
495            self.node_type = ReaderNodeType::NONE;
496            self.depth = 0;
497            self.clear_cached_name();
498            self.clear_cached_value();
499            self.attribute_count = -1;
500            self.cur_attribute = -1;
501            return;
502        }
503
504        // Copy event data before any mutable self access to avoid borrow conflicts.
505        let ev_node: *mut _xmlNode;
506        let ev_is_end: bool;
507        let ev_depth: i32;
508        {
509            let event = &self.events[index];
510            ev_node = event.node;
511            ev_is_end = event.is_end;
512            ev_depth = event.depth;
513        }
514
515        self.event_index = index;
516        self.cur_node = ev_node;
517        self.depth = ev_depth;
518
519        // SAFETY: node is valid.
520        let etype = unsafe { (*ev_node).type_ };
521
522        if ev_is_end {
523            self.node_type = ReaderNodeType::END_ELEMENT;
524        } else {
525            self.node_type = element_type_to_reader_type(etype);
526            // UPSTREAM-PARITY: whitespace-only text is reported as
527            // SIGNIFICANT_WHITESPACE (14) unless XML_PARSE_NOBLANKS.
528            if etype == XML_TEXT_NODE as c_int || etype == XML_CDATA_SECTION_NODE as c_int {
529                let content = unsafe { (*ev_node).content };
530                if !content.is_null() {
531                    // SAFETY: content is a valid NUL-terminated C string owned by the node.
532                    let len = unsafe { libc::strlen(content as *const libc::c_char) as usize };
533                    // SAFETY: content points to len valid bytes (the NUL-terminated string).
534                    let slice = unsafe { core::slice::from_raw_parts(content, len) };
535                    if is_whitespace_only(slice) {
536                        self.node_type = ReaderNodeType::SIGNIFICANT_WHITESPACE;
537                    }
538                }
539            }
540        }
541
542        // Cache name and value.
543        // SAFETY: ev_node is a valid node pointer.
544        unsafe { self.cache_name_and_value(ev_node, ev_is_end) };
545
546        // Count attributes if this is an element.
547        if etype == XML_ELEMENT_NODE as c_int && !ev_is_end {
548            // SAFETY: ev_node is a valid element node.
549            self.attribute_count = unsafe { self.count_attributes(ev_node) };
550        } else {
551            self.attribute_count = -1;
552        }
553
554        // Reset attribute cursor.
555        self.cur_attribute = -1;
556        self.cur_attr_is_ns = false;
557    }
558
559    /// Cache the name of the current node.
560    ///
561    /// # Safety
562    ///
563    /// `node` must be a valid node pointer or NULL.
564    unsafe fn cache_name_and_value(&mut self, node: *mut _xmlNode, is_end: bool) {
565        self.clear_cached_name();
566        self.clear_cached_value();
567
568        if node.is_null() {
569            return;
570        }
571
572        // SAFETY: node is valid.
573        let etype = unsafe { (*node).type_ };
574
575        // Determine name.
576        let name: *mut xmlChar = if is_end {
577            // For END_ELEMENT, the name is the element name.
578            // SAFETY: node is valid.
579            unsafe { (*node).name as *mut xmlChar }
580        } else {
581            if etype == XML_ELEMENT_NODE as c_int
582                || etype == XML_PI_NODE as c_int
583                || etype == XML_ENTITY_REF_NODE as c_int
584                || etype == XML_ENTITY_NODE as c_int
585                || etype == XML_DOCUMENT_TYPE_NODE as c_int
586                || etype == XML_NOTATION_NODE as c_int
587            {
588                // SAFETY: node is valid.
589                unsafe { (*node).name as *mut xmlChar }
590            } else if etype == XML_ATTRIBUTE_NODE as c_int {
591                // For attribute nodes accessed via MoveToAttribute.
592                ptr::null_mut()
593            } else {
594                ptr::null_mut()
595            }
596        };
597
598        if !name.is_null() {
599            // UPSTREAM-PARITY: xmlTextReaderName/ConstName return the
600            // qualified name for namespaced elements (e.g. "x:child"); the
601            // candidate rebuilds it from the node's ns prefix.
602            let qualified: *mut xmlChar = if etype == XML_ELEMENT_NODE as c_int && !node.is_null() {
603                let ns = unsafe { (*node).ns };
604                if !ns.is_null() && !unsafe { (*ns).prefix }.is_null() {
605                    let plen =
606                        libc::strlen(unsafe { (*ns).prefix } as *const libc::c_char) as usize;
607                    let nlen = libc::strlen(name as *const libc::c_char) as usize;
608                    let p = crate::abi::allocator::xmlMalloc(plen + 1 + nlen + 1) as *mut xmlChar;
609                    if !p.is_null() {
610                        libc::memcpy(
611                            p as *mut libc::c_void,
612                            unsafe { (*ns).prefix } as *const libc::c_void,
613                            plen,
614                        );
615                        *p.add(plen) = b':';
616                        libc::memcpy(
617                            p.add(plen + 1) as *mut libc::c_void,
618                            name as *const libc::c_void,
619                            nlen,
620                        );
621                        *p.add(plen + 1 + nlen) = 0;
622                    }
623                    p
624                } else {
625                    unsafe { xml_strdup(name as *const xmlChar) }
626                }
627            } else {
628                unsafe { xml_strdup(name as *const xmlChar) }
629            };
630            self.name = qualified;
631        } else if !is_end {
632            // UPSTREAM-PARITY (xmlTextReaderConstName): typed node kinds are
633            // reported with fixed names rather than NULL.
634            let fixed: &[u8] = match etype {
635                x if x == XML_TEXT_NODE as c_int => b"#text\0",
636                x if x == XML_CDATA_SECTION_NODE as c_int => b"#cdata-section\0",
637                x if x == XML_COMMENT_NODE as c_int => b"#comment\0",
638                x if x == XML_DOCUMENT_NODE as c_int => b"#document\0",
639                x if x == XML_HTML_DOCUMENT_NODE as c_int => b"#document\0",
640                x if x == XML_DOCUMENT_FRAG_NODE as c_int => b"#document-fragment\0",
641                _ => b"",
642            };
643            if !fixed.is_empty() {
644                self.name = unsafe { xml_strdup(fixed.as_ptr() as *const xmlChar) };
645            }
646        }
647
648        // Determine value.
649        let value: *mut xmlChar = if etype == XML_TEXT_NODE as c_int
650            || etype == XML_CDATA_SECTION_NODE as c_int
651            || etype == XML_COMMENT_NODE as c_int
652        {
653            // SAFETY: node is valid.
654            unsafe { (*node).content }
655        } else if etype == XML_PI_NODE as c_int {
656            // PI nodes store content as the PI value (after the target).
657            // SAFETY: node is valid.
658            unsafe { (*node).content }
659        } else if etype == XML_ENTITY_REF_NODE as c_int {
660            // Entity references may have content.
661            // SAFETY: node is valid.
662            unsafe { (*node).content }
663        } else {
664            ptr::null_mut()
665        };
666
667        if !value.is_null() {
668            // SAFETY: value is a valid null-terminated xmlChar string.
669            self.value = unsafe { xml_strdup(value as *const xmlChar) };
670        }
671    }
672
673    /// Count the number of attributes on an element node.
674    ///
675    /// # Safety
676    ///
677    /// `node` must be a valid element node pointer.
678    unsafe fn count_attributes(&self, node: *mut _xmlNode) -> i32 {
679        let mut count: i32 = 0;
680        // UPSTREAM-PARITY: namespace declarations count as attributes
681        // (xmlTextReaderAttributeCount includes them, xmlreader.c).
682        let mut ns = unsafe { (*node).nsDef };
683        while !ns.is_null() {
684            count += 1;
685            ns = unsafe { (*ns).next };
686        }
687        // SAFETY: node is a valid element.
688        let mut prop = unsafe { (*node).properties };
689        while !prop.is_null() {
690            count += 1;
691            // SAFETY: prop is valid.
692            prop = unsafe { (*prop).next };
693        }
694        count
695    }
696
697    /// Unified attribute addressing: namespace declarations first, then
698    /// regular attributes (upstream reader attribute iteration).
699    unsafe fn attr_at(&self, node: *mut _xmlNode, index: i32) -> AttrTarget {
700        if node.is_null() || index < 0 {
701            return AttrTarget::None;
702        }
703        let mut i = 0;
704        let mut ns = unsafe { (*node).nsDef };
705        while !ns.is_null() {
706            if i == index {
707                return AttrTarget::Ns(ns);
708            }
709            i += 1;
710            ns = unsafe { (*ns).next };
711        }
712        let mut prop = unsafe { (*node).properties };
713        while !prop.is_null() {
714            if i == index {
715                return AttrTarget::Prop(prop);
716            }
717            i += 1;
718            prop = unsafe { (*prop).next };
719        }
720        AttrTarget::None
721    }
722
723    /// The index of the attribute matching `name` (ns decls use
724    /// "xmlns:prefix"/"xmlns" names), or -1.
725    unsafe fn attr_index_by_name(&self, node: *mut _xmlNode, name: *const xmlChar) -> i32 {
726        if node.is_null() || name.is_null() {
727            return -1;
728        }
729        let mut i = 0;
730        let mut ns = unsafe { (*node).nsDef };
731        while !ns.is_null() {
732            let n = unsafe { &*ns };
733            let nsname: Vec<u8> = if n.prefix.is_null() {
734                b"xmlns\0".to_vec()
735            } else {
736                let mut v = b"xmlns:\0".to_vec();
737                let plen = libc::strlen(n.prefix as *const libc::c_char) as usize;
738                v.extend_from_slice(core::slice::from_raw_parts(n.prefix, plen));
739                v.push(0);
740                v
741            };
742            let nlen = libc::strlen(name as *const libc::c_char) as usize;
743            let nbytes = core::slice::from_raw_parts(name as *const u8, nlen);
744            if nbytes == &nsname[..nsname.len() - 1] {
745                return i;
746            }
747            i += 1;
748            ns = unsafe { (*ns).next };
749        }
750        let mut prop = unsafe { (*node).properties };
751        while !prop.is_null() {
752            let pn = unsafe { (*prop).name };
753            if !pn.is_null()
754                && libc::strcmp(pn as *const libc::c_char, name as *const libc::c_char) == 0
755            {
756                return i;
757            }
758            i += 1;
759            prop = unsafe { (*prop).next };
760        }
761        -1
762    }
763
764    /// Free the cached name.
765    fn clear_cached_name(&mut self) {
766        if !self.name.is_null() {
767            // SAFETY: name was allocated by xmlMalloc (via xml_strdup).
768            unsafe { xmlFree(self.name as *mut c_void) };
769            self.name = ptr::null_mut();
770        }
771    }
772
773    /// Free the cached value.
774    fn clear_cached_value(&mut self) {
775        if !self.value.is_null() {
776            // SAFETY: value was allocated by xmlMalloc (via xml_strdup).
777            unsafe { xmlFree(self.value as *mut c_void) };
778            self.value = ptr::null_mut();
779        }
780    }
781
782    // ─────────────────────────────────────────────────────────────────────────
783    // Navigation methods
784    // ─────────────────────────────────────────────────────────────────────────
785
786    /// Read the next node in document order.
787    ///
788    /// Returns 1 if a node was read, 0 if no more nodes (EOF), -1 on error.
789    pub unsafe fn Read(&mut self) -> c_int {
790        if self.state == ReadState::ERROR || self.state == ReadState::CLOSED {
791            return -1;
792        }
793
794        // On first call, parse the document and build events.
795        if !self.parsed {
796            if self.parse_and_build_events() != 0 {
797                self.state = ReadState::ERROR;
798                return -1;
799            }
800            self.state = ReadState::READING;
801        }
802
803        if self.state == ReadState::EOF {
804            return 0;
805        }
806
807        // If we're positioned on an attribute, return to the element first.
808        if self.cur_attribute >= 0 {
809            self.cur_attribute = -1;
810            self.cur_attr_is_ns = false;
811            // Re-cache the element info.
812            if !self.cur_node.is_null() {
813                // SAFETY: cur_node is valid.
814                unsafe { self.cache_name_and_value(self.cur_node, false) };
815            }
816        }
817
818        // Advance to the next event.
819        // If no events, we're at EOF.
820        if self.events.is_empty() {
821            self.state = ReadState::EOF;
822            return 0;
823        }
824
825        // Determine the next event index to position on.
826        // If cur_node is NULL, this is the first Read() after parsing —
827        // position at event 0. On subsequent calls, advance to the next event.
828        // We use cur_node.is_null() rather than event_index checks because
829        // after position_at(0), event_index == 0 and state == READING, which
830        // is indistinguishable from the pre-read state.
831        let next_index = if self.cur_node.is_null() {
832            // First Read() after parsing — position at event 0.
833            0
834        } else {
835            self.event_index + 1
836        };
837
838        if next_index < self.events.len() {
839            self.position_at(next_index);
840            1
841        } else {
842            self.state = ReadState::EOF;
843            self.cur_node = ptr::null_mut();
844            self.node_type = ReaderNodeType::NONE;
845            self.depth = 0;
846            self.clear_cached_name();
847            self.clear_cached_value();
848            self.attribute_count = -1;
849            self.cur_attribute = -1;
850            0
851        }
852    }
853
854    /// Skip to the next sibling of the current node.
855    ///
856    /// Returns 1 on success, 0 if no more siblings, -1 on error.
857    pub unsafe fn Next(&mut self) -> c_int {
858        if self.state != ReadState::READING || self.cur_node.is_null() {
859            return -1;
860        }
861
862        // Find the next sibling by scanning forward through events.
863        // We need to find the next event at depth <= current_depth that is not
864        // an END_ELEMENT. This skips:
865        // - All events in the current subtree (depth > current_depth)
866        // - END_ELEMENT events (which close the current element)
867        let current_depth = self.depth;
868        let mut i = self.event_index + 1;
869
870        while i < self.events.len() {
871            let event = &self.events[i];
872            if event.depth <= current_depth && !event.is_end {
873                self.position_at(i);
874                return 1;
875            }
876            i += 1;
877        }
878
879        0
880    }
881
882    /// Move to the parent element (if currently on an attribute).
883    ///
884    /// Returns 1 on success, 0 if not on an attribute, -1 on error.
885    pub unsafe fn MoveToElement(&mut self) -> c_int {
886        if self.cur_attribute < 0 {
887            return 0;
888        }
889        self.cur_attribute = -1;
890        self.cur_attr_is_ns = false;
891        if !self.cur_node.is_null() {
892            // SAFETY: cur_node is valid.
893            unsafe { self.cache_name_and_value(self.cur_node, false) };
894            self.node_type = ReaderNodeType::ELEMENT;
895        }
896        1
897    }
898
899    /// Move to an attribute by name.
900    ///
901    /// Returns 1 on success, 0 if attribute not found, -1 on error.
902    pub unsafe fn MoveToAttribute(&mut self, name: *const xmlChar) -> c_int {
903        if self.cur_node.is_null() {
904            return -1;
905        }
906
907        // SAFETY: cur_node is valid.
908        let etype = unsafe { (*self.cur_node).type_ };
909        if etype != XML_ELEMENT_NODE as c_int {
910            return -1;
911        }
912
913        // SAFETY: cur_node is an element.
914        let idx = unsafe { self.attr_index_by_name(self.cur_node, name) };
915        if idx < 0 {
916            return 0;
917        }
918        self.cur_attribute = idx;
919        let target = unsafe { self.attr_at(self.cur_node, idx) };
920        // Cache attribute info.
921        unsafe { self.cache_attribute_info(target) };
922        1
923    }
924
925    /// Move to an attribute by index.
926    ///
927    /// Returns 1 on success, 0 if index out of range, -1 on error.
928    pub unsafe fn MoveToAttributeNo(&mut self, index: c_int) -> c_int {
929        if self.cur_node.is_null() || index < 0 {
930            return -1;
931        }
932
933        // SAFETY: cur_node is valid.
934        let etype = unsafe { (*self.cur_node).type_ };
935        if etype != XML_ELEMENT_NODE as c_int {
936            return -1;
937        }
938
939        // SAFETY: cur_node is an element.
940        let target = unsafe { self.attr_at(self.cur_node, index) };
941        match target {
942            AttrTarget::None => 0,
943            t => {
944                self.cur_attribute = index;
945                // Cache attribute info.
946                unsafe { self.cache_attribute_info(t) };
947                1
948            }
949        }
950    }
951
952    /// Move to the first attribute of the current element.
953    ///
954    /// Returns 1 on success, 0 if no attributes, -1 on error.
955    pub unsafe fn MoveToFirstAttribute(&mut self) -> c_int {
956        if self.cur_node.is_null() {
957            return -1;
958        }
959
960        // SAFETY: cur_node is valid.
961        let etype = unsafe { (*self.cur_node).type_ };
962        if etype != XML_ELEMENT_NODE as c_int {
963            return -1;
964        }
965
966        // SAFETY: cur_node is an element.
967        let first = unsafe { self.attr_at(self.cur_node, 0) };
968        match first {
969            AttrTarget::None => 0,
970            t => {
971                self.cur_attribute = 0;
972                // Cache attribute info.
973                unsafe { self.cache_attribute_info(t) };
974                1
975            }
976        }
977    }
978
979    /// Move to the next attribute.
980    ///
981    /// Returns 1 on success, 0 if no more attributes, -1 on error.
982    pub unsafe fn MoveToNextAttribute(&mut self) -> c_int {
983        if self.cur_attribute < 0 || self.cur_node.is_null() {
984            return -1;
985        }
986
987        // SAFETY: cur_node is valid.
988        let etype = unsafe { (*self.cur_node).type_ };
989        if etype != XML_ELEMENT_NODE as c_int {
990            return -1;
991        }
992
993        // Find the attribute at cur_attribute index, then move to the next.
994        let next_index = self.cur_attribute + 1;
995        let target = unsafe { self.attr_at(self.cur_node, next_index) };
996        match target {
997            AttrTarget::None => 0,
998            t => {
999                self.cur_attribute = next_index;
1000                unsafe { self.cache_attribute_info(t) };
1001                1
1002            }
1003        }
1004    }
1005
1006    /// Cache the current position info for an attribute (or namespace
1007    /// declaration, which the reader presents as an attribute).
1008    ///
1009    /// # Safety
1010    ///
1011    /// `target` must be a valid AttrTarget::Prop(_xmlAttr) or
1012    /// AttrTarget::Ns(_xmlNs).
1013    unsafe fn cache_attribute_info(&mut self, target: AttrTarget) {
1014        self.node_type = ReaderNodeType::ATTRIBUTE;
1015        self.cur_attr_is_ns = matches!(target, AttrTarget::Ns(_));
1016        self.clear_cached_name();
1017        self.clear_cached_value();
1018        match target {
1019            AttrTarget::Ns(ns) => {
1020                // UPSTREAM-PARITY: a namespace declaration is exposed as an
1021                // attribute named "xmlns:prefix" (or "xmlns" for the default
1022                // namespace) whose value is the namespace URI.
1023                let n = unsafe { &*ns };
1024                if n.prefix.is_null() {
1025                    self.name = unsafe { xml_strdup(b"xmlns\0".as_ptr() as *const xmlChar) };
1026                } else {
1027                    let plen = libc::strlen(n.prefix as *const libc::c_char) as usize;
1028                    let mut v = Vec::with_capacity(6 + plen);
1029                    v.extend_from_slice(b"xmlns:");
1030                    v.extend_from_slice(core::slice::from_raw_parts(n.prefix, plen));
1031                    v.push(0);
1032                    let p = crate::abi::allocator::xmlMalloc(v.len()) as *mut xmlChar;
1033                    if !p.is_null() {
1034                        libc::memcpy(
1035                            p as *mut libc::c_void,
1036                            v.as_ptr() as *const libc::c_void,
1037                            v.len(),
1038                        );
1039                        self.name = p;
1040                    }
1041                }
1042                if !n.href.is_null() {
1043                    self.value = unsafe { xml_strdup(n.href as *const xmlChar) };
1044                }
1045            }
1046            AttrTarget::Prop(prop) => {
1047                // SAFETY: prop is valid.
1048                let attr = unsafe { &*prop };
1049
1050                // Name — UPSTREAM-PARITY (xmlTextReaderConstName): a
1051                // namespace-qualified attribute is reported as
1052                // "prefix:localname" (constQString), unqualified attributes
1053                // keep the local name.
1054                if !attr.name.is_null() {
1055                    if !attr.ns.is_null() && !unsafe { (*attr.ns).prefix }.is_null() {
1056                        let plen = libc::strlen(unsafe { (*attr.ns).prefix } as *const libc::c_char)
1057                            as usize;
1058                        let nlen = libc::strlen(attr.name as *const libc::c_char) as usize;
1059                        let p =
1060                            crate::abi::allocator::xmlMalloc(plen + 1 + nlen + 1) as *mut xmlChar;
1061                        if !p.is_null() {
1062                            libc::memcpy(
1063                                p as *mut libc::c_void,
1064                                unsafe { (*attr.ns).prefix } as *const libc::c_void,
1065                                plen,
1066                            );
1067                            *p.add(plen) = b':';
1068                            libc::memcpy(
1069                                p.add(plen + 1) as *mut libc::c_void,
1070                                attr.name as *const libc::c_void,
1071                                nlen,
1072                            );
1073                            *p.add(plen + 1 + nlen) = 0;
1074                            self.name = p;
1075                        }
1076                    } else {
1077                        // SAFETY: attr.name is null-terminated.
1078                        self.name = unsafe { xml_strdup(attr.name as *const xmlChar) };
1079                    }
1080                }
1081
1082                // Value — the attribute's text content is in its child text node.
1083                if !attr.children.is_null() {
1084                    // SAFETY: attr.children is a text node.
1085                    let val = unsafe { (*attr.children).content };
1086                    if !val.is_null() {
1087                        // SAFETY: val is null-terminated.
1088                        self.value = unsafe { xml_strdup(val as *const xmlChar) };
1089                    }
1090                }
1091            }
1092            AttrTarget::None => {}
1093        }
1094    }
1095
1096    /// Move to the previous sibling.
1097    ///
1098    /// Returns 1 on success, 0 if no previous sibling, -1 on error.
1099    pub unsafe fn Prev(&mut self) -> c_int {
1100        if self.state != ReadState::READING || self.cur_node.is_null() {
1101            return -1;
1102        }
1103
1104        // Scan backward through events to find the previous sibling.
1105        let current_depth = self.depth;
1106        let mut i = if self.event_index > 0 {
1107            self.event_index - 1
1108        } else {
1109            return 0;
1110        };
1111
1112        loop {
1113            let event = &self.events[i];
1114            if event.depth == current_depth && !event.is_end {
1115                self.position_at(i);
1116                return 1;
1117            }
1118            if i == 0 {
1119                break;
1120            }
1121            i -= 1;
1122        }
1123
1124        0
1125    }
1126
1127    // ─────────────────────────────────────────────────────────────────────────
1128    // Information methods
1129    // ─────────────────────────────────────────────────────────────────────────
1130
1131    /// Get the depth of the current node.
1132    pub fn Depth(&self) -> c_int {
1133        self.depth
1134    }
1135
1136    /// Get the node type of the current node.
1137    pub fn NodeType(&self) -> ReaderNodeType {
1138        self.node_type
1139    }
1140
1141    /// Get the name of the current node.
1142    ///
1143    /// Returns a pointer to a newly allocated string (caller must free with `xmlFree`),
1144    /// or NULL if there is no name.
1145    pub unsafe fn Name(&self) -> *mut xmlChar {
1146        if self.name.is_null() {
1147            return ptr::null_mut();
1148        }
1149        // SAFETY: name is a valid null-terminated xmlChar string.
1150        unsafe { xml_strdup(self.name as *const xmlChar) }
1151    }
1152
1153    /// Get the value of the current node.
1154    ///
1155    /// Returns a pointer to a newly allocated string (caller must free with `xmlFree`),
1156    /// or NULL if there is no value.
1157    pub unsafe fn Value(&self) -> *mut xmlChar {
1158        if self.value.is_null() {
1159            return ptr::null_mut();
1160        }
1161        // SAFETY: value is a valid null-terminated xmlChar string.
1162        unsafe { xml_strdup(self.value as *const xmlChar) }
1163    }
1164
1165    /// Get a constant pointer to the name (no copy).
1166    ///
1167    /// The returned pointer is valid only while the reader is alive and positioned
1168    /// on the same node.
1169    pub fn ConstName(&self) -> *const xmlChar {
1170        self.name as *const xmlChar
1171    }
1172
1173    /// Get a constant pointer to the value (no copy).
1174    ///
1175    /// The returned pointer is valid only while the reader is alive and positioned
1176    /// on the same node.
1177    pub fn ConstValue(&self) -> *const xmlChar {
1178        self.value as *const xmlChar
1179    }
1180
1181    /// Check if the current node has a value.
1182    pub fn HasValue(&self) -> c_int {
1183        if self.value.is_null() {
1184            0
1185        } else {
1186            1
1187        }
1188    }
1189
1190    /// Check if the current node has attributes.
1191    pub fn HasAttributes(&self) -> c_int {
1192        if self.cur_node.is_null() {
1193            return 0;
1194        }
1195        // SAFETY: cur_node is valid.
1196        let etype = unsafe { (*self.cur_node).type_ };
1197        if etype != XML_ELEMENT_NODE as c_int {
1198            return 0;
1199        }
1200        // SAFETY: cur_node is an element.
1201        // UPSTREAM-PARITY: namespace declarations count as attributes.
1202        let props = unsafe { (*self.cur_node).properties };
1203        let nsdefs = unsafe { (*self.cur_node).nsDef };
1204        if props.is_null() && nsdefs.is_null() {
1205            0
1206        } else {
1207            1
1208        }
1209    }
1210
1211    /// Check if the current element is an empty element (no children).
1212    pub fn IsEmptyElement(&self) -> c_int {
1213        if self.cur_node.is_null() {
1214            return 0;
1215        }
1216        // SAFETY: cur_node is valid.
1217        let etype = unsafe { (*self.cur_node).type_ };
1218        if etype != XML_ELEMENT_NODE as c_int {
1219            return 0;
1220        }
1221        // SAFETY: cur_node is an element.
1222        let children = unsafe { (*self.cur_node).children };
1223        if children.is_null() {
1224            1
1225        } else {
1226            0
1227        }
1228    }
1229
1230    /// Get the base URI of the current node.
1231    ///
1232    /// Returns a newly allocated string (caller must free with `xmlFree`),
1233    /// or NULL if not available.
1234    pub unsafe fn BaseUri(&self) -> *mut xmlChar {
1235        // The base URI is typically the document URL.
1236        if self.doc.is_null() {
1237            return ptr::null_mut();
1238        }
1239        // SAFETY: doc is valid.
1240        let url = unsafe { (*self.doc).URL };
1241        if url.is_null() {
1242            return ptr::null_mut();
1243        }
1244        // SAFETY: url is null-terminated.
1245        unsafe { xml_strdup(url as *const xmlChar) }
1246    }
1247
1248    /// Get the local name of the current node.
1249    ///
1250    /// For namespaced names, this strips the prefix.
1251    /// Returns a newly allocated string, or NULL.
1252    pub unsafe fn LocalName(&self) -> *mut xmlChar {
1253        if self.name.is_null() {
1254            return ptr::null_mut();
1255        }
1256
1257        // SAFETY: name is a valid null-terminated string.
1258        let name_bytes = unsafe { xmlstr_to_bytes(self.name as *const xmlChar) };
1259
1260        // Find the colon separator.
1261        if let Some(pos) = name_bytes.iter().position(|&b| b == b':') {
1262            // Return everything after the colon.
1263            let local = &name_bytes[pos + 1..];
1264            if local.is_empty() {
1265                return ptr::null_mut();
1266            }
1267            // SAFETY: bytes_to_xmlstr allocates via xmlMalloc.
1268            unsafe { bytes_to_xmlstr(local) }
1269        } else {
1270            // No prefix, return the name as-is.
1271            // SAFETY: xml_strdup allocates via xmlMalloc.
1272            unsafe { xml_strdup(self.name as *const xmlChar) }
1273        }
1274    }
1275
1276    /// Get the namespace URI of the current node.
1277    ///
1278    /// Returns a newly allocated string, or NULL.
1279    pub unsafe fn NamespaceUri(&self) -> *mut xmlChar {
1280        if self.cur_node.is_null() {
1281            return ptr::null_mut();
1282        }
1283
1284        // SAFETY: cur_node is valid.
1285        let ns = unsafe { (*self.cur_node).ns };
1286        if ns.is_null() {
1287            return ptr::null_mut();
1288        }
1289
1290        // SAFETY: ns is valid.
1291        let href = unsafe { (*ns).href };
1292        if href.is_null() {
1293            return ptr::null_mut();
1294        }
1295
1296        // SAFETY: href is null-terminated.
1297        unsafe { xml_strdup(href as *const xmlChar) }
1298    }
1299
1300    /// Get the prefix of the current node.
1301    ///
1302    /// Returns a newly allocated string, or NULL.
1303    pub unsafe fn Prefix(&self) -> *mut xmlChar {
1304        if self.cur_node.is_null() {
1305            return ptr::null_mut();
1306        }
1307
1308        // SAFETY: cur_node is valid.
1309        let ns = unsafe { (*self.cur_node).ns };
1310        if ns.is_null() {
1311            return ptr::null_mut();
1312        }
1313
1314        // SAFETY: ns is valid.
1315        let prefix = unsafe { (*ns).prefix };
1316        if prefix.is_null() {
1317            return ptr::null_mut();
1318        }
1319
1320        // SAFETY: prefix is null-terminated.
1321        unsafe { xml_strdup(prefix as *const xmlChar) }
1322    }
1323
1324    /// Get the attribute count of the current element.
1325    pub fn AttributeCount(&self) -> c_int {
1326        self.attribute_count
1327    }
1328
1329    /// Get the read state.
1330    pub fn ReadState(&self) -> ReadState {
1331        self.state
1332    }
1333
1334    /// Get an attribute value by name.
1335    ///
1336    /// Returns a newly allocated string, or NULL.
1337    pub unsafe fn GetAttribute(&self, name: *const xmlChar) -> *mut xmlChar {
1338        if self.cur_node.is_null() {
1339            return ptr::null_mut();
1340        }
1341
1342        // SAFETY: cur_node is valid.
1343        let etype = unsafe { (*self.cur_node).type_ };
1344        if etype != XML_ELEMENT_NODE as c_int {
1345            return ptr::null_mut();
1346        }
1347
1348        // SAFETY: cur_node is an element.
1349        let idx = unsafe { self.attr_index_by_name(self.cur_node, name) };
1350        if idx < 0 {
1351            return ptr::null_mut();
1352        }
1353        match unsafe { self.attr_at(self.cur_node, idx) } {
1354            AttrTarget::Ns(ns) => {
1355                let href = unsafe { (*ns).href };
1356                if href.is_null() {
1357                    ptr::null_mut()
1358                } else {
1359                    unsafe { xml_strdup(href as *const xmlChar) }
1360                }
1361            }
1362            AttrTarget::Prop(prop) => {
1363                // Get the attribute value from its child text node.
1364                let val = unsafe { (*prop).children };
1365                if !val.is_null() {
1366                    let content = unsafe { (*val).content };
1367                    if !content.is_null() {
1368                        return unsafe { xml_strdup(content as *const xmlChar) };
1369                    }
1370                }
1371                ptr::null_mut()
1372            }
1373            AttrTarget::None => ptr::null_mut(),
1374        }
1375    }
1376
1377    /// Get an attribute value by index.
1378    ///
1379    /// Returns a newly allocated string, or NULL.
1380    pub unsafe fn GetAttributeNo(&self, index: c_int) -> *mut xmlChar {
1381        if self.cur_node.is_null() || index < 0 {
1382            return ptr::null_mut();
1383        }
1384
1385        // SAFETY: cur_node is valid.
1386        let etype = unsafe { (*self.cur_node).type_ };
1387        if etype != XML_ELEMENT_NODE as c_int {
1388            return ptr::null_mut();
1389        }
1390
1391        // SAFETY: cur_node is an element.
1392        match unsafe { self.attr_at(self.cur_node, index) } {
1393            AttrTarget::Ns(ns) => {
1394                let href = unsafe { (*ns).href };
1395                if href.is_null() {
1396                    ptr::null_mut()
1397                } else {
1398                    unsafe { xml_strdup(href as *const xmlChar) }
1399                }
1400            }
1401            AttrTarget::Prop(prop) => {
1402                let val = unsafe { (*prop).children };
1403                if !val.is_null() {
1404                    let content = unsafe { (*val).content };
1405                    if !content.is_null() {
1406                        return unsafe { xml_strdup(content as *const xmlChar) };
1407                    }
1408                }
1409                ptr::null_mut()
1410            }
1411            AttrTarget::None => ptr::null_mut(),
1412        }
1413    }
1414
1415    /// Get an attribute value by local name and namespace URI.
1416    ///
1417    /// Returns a newly allocated string, or NULL.
1418    pub unsafe fn GetAttributeNs(
1419        &self,
1420        localName: *const xmlChar,
1421        namespaceURI: *const xmlChar,
1422    ) -> *mut xmlChar {
1423        if self.cur_node.is_null() {
1424            return ptr::null_mut();
1425        }
1426
1427        // SAFETY: cur_node is valid.
1428        let etype = unsafe { (*self.cur_node).type_ };
1429        if etype != XML_ELEMENT_NODE as c_int {
1430            return ptr::null_mut();
1431        }
1432
1433        // SAFETY: cur_node is an element.
1434        let mut prop = unsafe { (*self.cur_node).properties };
1435        while !prop.is_null() {
1436            // SAFETY: prop is valid.
1437            let prop_local = unsafe { (*prop).name };
1438            let prop_ns = unsafe { (*prop).ns };
1439
1440            // Check local name match.
1441            if prop_local.is_null() {
1442                // SAFETY: prop's next is valid.
1443                prop = unsafe { (*prop).next };
1444                continue;
1445            }
1446
1447            // SAFETY: prop_local is null-terminated.
1448            let name_match = unsafe {
1449                crate::xml::string::xml_strcmp(prop_local as *const xmlChar, localName) == 0
1450            };
1451
1452            if name_match {
1453                // Check namespace URI match.
1454                let ns_match = if namespaceURI.is_null() {
1455                    prop_ns.is_null()
1456                } else if prop_ns.is_null() {
1457                    false
1458                } else {
1459                    // SAFETY: Both hrefs are null-terminated.
1460                    unsafe {
1461                        crate::xml::string::xml_strcmp(
1462                            (*prop_ns).href as *const xmlChar,
1463                            namespaceURI,
1464                        ) == 0
1465                    }
1466                };
1467
1468                if ns_match {
1469                    // SAFETY: prop is valid.
1470                    let val = unsafe { (*prop).children };
1471                    if !val.is_null() {
1472                        // SAFETY: val's content is null-terminated.
1473                        let content = unsafe { (*val).content };
1474                        if !content.is_null() {
1475                            // SAFETY: content is null-terminated.
1476                            return unsafe { xml_strdup(content as *const xmlChar) };
1477                        }
1478                    }
1479                    return ptr::null_mut();
1480                }
1481            }
1482
1483            // SAFETY: prop's next is valid.
1484            prop = unsafe { (*prop).next };
1485        }
1486
1487        ptr::null_mut()
1488    }
1489
1490    /// Look up a namespace by prefix.
1491    ///
1492    /// Returns a newly allocated string with the namespace URI, or NULL.
1493    pub unsafe fn LookupNamespace(&self, prefix: *const xmlChar) -> *mut xmlChar {
1494        if self.cur_node.is_null() {
1495            return ptr::null_mut();
1496        }
1497
1498        // Walk up the tree looking for a namespace declaration matching the prefix.
1499        // SAFETY: cur_node is valid.
1500        let mut cur = self.cur_node;
1501        while !cur.is_null() {
1502            // SAFETY: cur is valid.
1503            let mut ns_def = unsafe { (*cur).nsDef };
1504            while !ns_def.is_null() {
1505                // SAFETY: ns_def is valid.
1506                let ns_prefix = unsafe { (*ns_def).prefix };
1507
1508                let match_prefix = if prefix.is_null() || *prefix == 0 {
1509                    // Looking for default namespace.
1510                    ns_prefix.is_null()
1511                } else if ns_prefix.is_null() {
1512                    false
1513                } else {
1514                    // SAFETY: Both are null-terminated.
1515                    unsafe {
1516                        crate::xml::string::xml_strcmp(ns_prefix as *const xmlChar, prefix) == 0
1517                    }
1518                };
1519
1520                if match_prefix {
1521                    // SAFETY: ns_def is valid.
1522                    let href = unsafe { (*ns_def).href };
1523                    if !href.is_null() {
1524                        // SAFETY: href is null-terminated.
1525                        return unsafe { xml_strdup(href as *const xmlChar) };
1526                    }
1527                    return ptr::null_mut();
1528                }
1529
1530                // SAFETY: ns_def's next is valid.
1531                ns_def = unsafe { (*ns_def).next };
1532            }
1533
1534            // SAFETY: cur's parent is valid.
1535            cur = unsafe { (*cur).parent };
1536        }
1537
1538        ptr::null_mut()
1539    }
1540
1541    /// Get a parser property.
1542    pub fn GetParserProp(&self, prop: c_int) -> c_int {
1543        match prop {
1544            1 /* XML_PARSER_LOADDTD */ => {
1545                if (self.options & XML_PARSE_DTDLOAD) != 0 { 1 } else { 0 }
1546            }
1547            2 /* XML_PARSER_DEFAULTATTRS */ => {
1548                if (self.options & XML_PARSE_DTDATTR) != 0 { 1 } else { 0 }
1549            }
1550            3 /* XML_PARSER_VALIDATE */ => {
1551                if (self.options & XML_PARSE_DTDVALID) != 0 { 1 } else { 0 }
1552            }
1553            4 /* XML_PARSER_SUBST_ENTITIES */ => {
1554                if (self.options & XML_PARSE_NOENT) != 0 { 1 } else { 0 }
1555            }
1556            _ => -1,
1557        }
1558    }
1559
1560    /// Set a parser property.
1561    pub fn SetParserProp(&mut self, prop: c_int, value: c_int) -> c_int {
1562        match prop {
1563            1 /* XML_PARSER_LOADDTD */ => {
1564                if value != 0 {
1565                    self.options |= XML_PARSE_DTDLOAD;
1566                } else {
1567                    self.options &= !XML_PARSE_DTDLOAD;
1568                }
1569                0
1570            }
1571            2 /* XML_PARSER_DEFAULTATTRS */ => {
1572                if value != 0 {
1573                    self.options |= XML_PARSE_DTDATTR;
1574                } else {
1575                    self.options &= !XML_PARSE_DTDATTR;
1576                }
1577                0
1578            }
1579            3 /* XML_PARSER_VALIDATE */ => {
1580                if value != 0 {
1581                    self.options |= XML_PARSE_DTDVALID;
1582                } else {
1583                    self.options &= !XML_PARSE_DTDVALID;
1584                }
1585                0
1586            }
1587            4 /* XML_PARSER_SUBST_ENTITIES */ => {
1588                if value != 0 {
1589                    self.options |= XML_PARSE_NOENT;
1590                } else {
1591                    self.options &= !XML_PARSE_NOENT;
1592                }
1593                0
1594            }
1595            _ => -1,
1596        }
1597    }
1598
1599    /// Get the current document.
1600    pub fn CurrentDoc(&self) -> *mut _xmlDoc {
1601        self.doc
1602    }
1603}
1604
1605impl Drop for XmlTextReader {
1606    fn drop(&mut self) {
1607        // Free cached strings.
1608        self.clear_cached_name();
1609        self.clear_cached_value();
1610
1611        // Free the cached last-error message (owned, xmlMalloc'd).
1612        if !self.last_err.message.is_null() {
1613            // SAFETY: message was allocated by xmlMalloc in GetLastError.
1614            unsafe { libc::free(self.last_err.message as *mut libc::c_void) };
1615            self.last_err.message = ptr::null_mut();
1616        }
1617
1618        // Free encoding and URL.
1619        if !self.encoding.is_null() {
1620            // SAFETY: encoding was allocated by xmlMalloc.
1621            unsafe { xmlFree(self.encoding as *mut c_void) };
1622            self.encoding = ptr::null_mut();
1623        }
1624        if !self.URL.is_null() {
1625            // SAFETY: URL was allocated by xmlMalloc.
1626            unsafe { xmlFree(self.URL as *mut c_void) };
1627            self.URL = ptr::null_mut();
1628        }
1629
1630        // Free the document if we own it (walker readers borrow the doc).
1631        if !self.doc.is_null() && self.owns_doc {
1632            // SAFETY: doc was created by the parser, which allocates via xmlMalloc.
1633            // We own the doc since we created it.
1634            unsafe { tree::free_doc(self.doc) };
1635            self.doc = ptr::null_mut();
1636        }
1637
1638        // Free the parser context if still alive.
1639        if !self.ctxt.is_null() {
1640            // SAFETY: ctxt was created by create_parser_ctxt.
1641            unsafe { free_parser_ctxt(self.ctxt) };
1642            self.ctxt = ptr::null_mut();
1643        }
1644    }
1645}
1646
1647// ═══════════════════════════════════════════════════════════════════════════════
1648// Reader construction helpers
1649// ═══════════════════════════════════════════════════════════════════════════════
1650
1651/// Create a reader from a parser input buffer.
1652///
1653/// # Safety
1654///
1655/// `input` must be a valid `_xmlParserInputBuffer` pointer.
1656unsafe fn reader_from_input(
1657    input: *mut _xmlParserInputBuffer,
1658    URL: *const c_char,
1659    encoding: *const c_char,
1660    options: c_int,
1661) -> *mut XmlTextReader {
1662    if input.is_null() {
1663        return ptr::null_mut();
1664    }
1665
1666    // Create a parser context.
1667    let ctxt = create_parser_ctxt();
1668    if ctxt.is_null() {
1669        return ptr::null_mut();
1670    }
1671
1672    // Read all data from the input buffer using the read callback.
1673    let mut data = Vec::new();
1674    let mut tmp = [0u8; 4096];
1675
1676    // SAFETY: input is valid.
1677    let read_cb = unsafe { (*input).readcallback };
1678    let ioctx = unsafe { (*input).context };
1679
1680    if let Some(read) = read_cb {
1681        loop {
1682            // SAFETY: The read callback must be valid and ioctx must be a valid context.
1683            let n = unsafe { read(ioctx, tmp.as_mut_ptr() as *mut c_char, tmp.len() as c_int) };
1684            if n <= 0 {
1685                break;
1686            }
1687            data.extend_from_slice(&tmp[..n as usize]);
1688        }
1689    }
1690
1691    // Close the input if there's a close callback.
1692    // SAFETY: input is valid.
1693    let close_cb = unsafe { (*input).closecallback };
1694    if let Some(close) = close_cb {
1695        // SAFETY: The close callback must be valid.
1696        unsafe { close(ioctx) };
1697    }
1698
1699    // Create an InputBuffer from the data.
1700    let input_buf = InputBuffer::from_memory(&data, None);
1701
1702    // Set up the parser context with the input.
1703    setup_parser_input(ctxt, input_buf);
1704
1705    // Set options.
1706    unsafe {
1707        (*ctxt).options = options;
1708    }
1709
1710    // Build URL and encoding strings.
1711    let url_bytes = if URL.is_null() {
1712        None
1713    } else {
1714        // SAFETY: URL is a valid C string.
1715        unsafe {
1716            let cstr = std::ffi::CStr::from_ptr(URL);
1717            Some(cstr.to_bytes().to_vec())
1718        }
1719    };
1720
1721    let enc_bytes = if encoding.is_null() {
1722        None
1723    } else {
1724        // SAFETY: encoding is a valid C string.
1725        unsafe {
1726            let cstr = std::ffi::CStr::from_ptr(encoding);
1727            Some(cstr.to_bytes().to_vec())
1728        }
1729    };
1730
1731    // Create the reader.
1732    let mut reader = XmlTextReader::new(ctxt, url_bytes.as_deref(), enc_bytes.as_deref());
1733    reader.options = options;
1734
1735    // Box and leak the reader to return a raw pointer.
1736    Box::into_raw(Box::new(reader))
1737}
1738
1739// ═══════════════════════════════════════════════════════════════════════════════
1740// Public API functions
1741// ═══════════════════════════════════════════════════════════════════════════════
1742
1743/// Create a new text reader from an input buffer.
1744///
1745/// # UPSTREAM-PARITY
1746///
1747/// ```c
1748/// xmlTextReaderPtr xmlNewTextReader(xmlParserInputBufferPtr input, const char *URI);
1749/// ```
1750///
1751/// # Safety
1752///
1753/// - `input` must be a valid `_xmlParserInputBuffer` pointer or NULL.
1754/// - `URI` must be a valid C string or NULL.
1755#[no_mangle]
1756pub unsafe extern "C" fn xmlNewTextReader(
1757    input: *mut _xmlParserInputBuffer,
1758    URI: *const c_char,
1759) -> *mut XmlTextReader {
1760    // SAFETY: Forward to the helper.
1761    unsafe { reader_from_input(input, URI, ptr::null(), 0) }
1762}
1763
1764/// Create a text reader for a file.
1765///
1766/// # UPSTREAM-PARITY
1767///
1768/// ```c
1769/// xmlTextReaderPtr xmlReaderForFile(const char *filename, const char *encoding, int options);
1770/// ```
1771///
1772/// # Safety
1773///
1774/// - `filename` must be a valid C string or NULL.
1775/// - `encoding` must be a valid C string or NULL.
1776#[no_mangle]
1777pub unsafe extern "C" fn xmlReaderForFile(
1778    filename: *const c_char,
1779    encoding: *const c_char,
1780    options: c_int,
1781) -> *mut XmlTextReader {
1782    if filename.is_null() {
1783        return ptr::null_mut();
1784    }
1785
1786    // SAFETY: create_parser_ctxt returns a valid context or NULL.
1787    let ctxt = unsafe { create_parser_ctxt() };
1788    if ctxt.is_null() {
1789        return ptr::null_mut();
1790    }
1791
1792    // SAFETY: input_from_file reads the file; filename is a valid C string.
1793    let input = match unsafe { input_from_file(filename) } {
1794        Ok(input) => input,
1795        Err(_) => {
1796            // SAFETY: ctxt is valid.
1797            unsafe { free_parser_ctxt(ctxt) };
1798            return ptr::null_mut();
1799        }
1800    };
1801
1802    // SAFETY: ctxt and input are valid.
1803    unsafe { setup_parser_input(ctxt, input) };
1804    unsafe {
1805        (*ctxt).options = options;
1806    }
1807
1808    let enc_bytes = if encoding.is_null() {
1809        None
1810    } else {
1811        // SAFETY: encoding is a valid C string.
1812        unsafe {
1813            let cstr = std::ffi::CStr::from_ptr(encoding);
1814            Some(cstr.to_bytes().to_vec())
1815        }
1816    };
1817
1818    let mut reader = XmlTextReader::new(ctxt, None, enc_bytes.as_deref());
1819    reader.options = options;
1820    Box::into_raw(Box::new(reader))
1821}
1822
1823/// Create a text reader from memory.
1824///
1825/// # UPSTREAM-PARITY
1826///
1827/// ```c
1828/// xmlTextReaderPtr xmlReaderForMemory(const char *buffer, int size,
1829///                                     const char *URL, const char *encoding, int options);
1830/// ```
1831///
1832/// # Safety
1833///
1834/// - `buffer` must be a valid pointer with at least `size` readable bytes.
1835/// - `URL` and `encoding` must be valid C strings or NULL.
1836#[no_mangle]
1837pub unsafe extern "C" fn xmlReaderForMemory(
1838    buffer: *const c_char,
1839    size: c_int,
1840    URL: *const c_char,
1841    encoding: *const c_char,
1842    options: c_int,
1843) -> *mut XmlTextReader {
1844    if buffer.is_null() || size <= 0 {
1845        return ptr::null_mut();
1846    }
1847
1848    // SAFETY: create_parser_ctxt returns a valid context or NULL.
1849    let ctxt = unsafe { create_parser_ctxt() };
1850    if ctxt.is_null() {
1851        return ptr::null_mut();
1852    }
1853
1854    // SAFETY: input_from_memory copies the data; buffer and size are valid.
1855    // UPSTREAM-PARITY: the URL is recorded as the input's filename.
1856    let input = unsafe { input_from_memory_named(buffer, size, URL) };
1857
1858    // SAFETY: ctxt and input are valid.
1859    unsafe { setup_parser_input(ctxt, input) };
1860    unsafe {
1861        (*ctxt).options = options;
1862    }
1863
1864    let url_bytes = if URL.is_null() {
1865        None
1866    } else {
1867        // SAFETY: URL is a valid C string.
1868        unsafe {
1869            let cstr = std::ffi::CStr::from_ptr(URL);
1870            Some(cstr.to_bytes().to_vec())
1871        }
1872    };
1873
1874    let enc_bytes = if encoding.is_null() {
1875        None
1876    } else {
1877        // SAFETY: encoding is a valid C string.
1878        unsafe {
1879            let cstr = std::ffi::CStr::from_ptr(encoding);
1880            Some(cstr.to_bytes().to_vec())
1881        }
1882    };
1883
1884    let mut reader = XmlTextReader::new(ctxt, url_bytes.as_deref(), enc_bytes.as_deref());
1885    reader.options = options;
1886    Box::into_raw(Box::new(reader))
1887}
1888
1889/// Create a text reader from a file descriptor.
1890///
1891/// # UPSTREAM-PARITY
1892///
1893/// ```c
1894/// xmlTextReaderPtr xmlReaderForFd(int fd, const char *URL,
1895///                                 const char *encoding, int options);
1896/// ```
1897///
1898/// # Safety
1899///
1900/// - `fd` must be a valid open file descriptor.
1901/// - `URL` and `encoding` must be valid C strings or NULL.
1902#[no_mangle]
1903pub unsafe extern "C" fn xmlReaderForFd(
1904    fd: c_int,
1905    URL: *const c_char,
1906    encoding: *const c_char,
1907    options: c_int,
1908) -> *mut XmlTextReader {
1909    // SAFETY: create_parser_ctxt returns a valid context or NULL.
1910    let ctxt = unsafe { create_parser_ctxt() };
1911    if ctxt.is_null() {
1912        return ptr::null_mut();
1913    }
1914
1915    // Read all data from the fd.
1916    let mut buf = Vec::new();
1917    let mut tmp = [0u8; 4096];
1918    loop {
1919        // SAFETY: fd must be a valid open file descriptor.
1920        let n = unsafe { libc::read(fd, tmp.as_mut_ptr() as *mut c_void, tmp.len()) };
1921        if n <= 0 {
1922            break;
1923        }
1924        buf.extend_from_slice(&tmp[..n as usize]);
1925    }
1926
1927    // SAFETY: input_from_memory copies the buffer contents.
1928    let input = unsafe { input_from_memory(buf.as_ptr() as *const c_char, buf.len() as c_int) };
1929
1930    // SAFETY: ctxt and input are valid.
1931    unsafe { setup_parser_input(ctxt, input) };
1932    unsafe {
1933        (*ctxt).options = options;
1934    }
1935
1936    let url_bytes = if URL.is_null() {
1937        None
1938    } else {
1939        // SAFETY: URL is a valid C string.
1940        unsafe {
1941            let cstr = std::ffi::CStr::from_ptr(URL);
1942            Some(cstr.to_bytes().to_vec())
1943        }
1944    };
1945
1946    let enc_bytes = if encoding.is_null() {
1947        None
1948    } else {
1949        // SAFETY: encoding is a valid C string.
1950        unsafe {
1951            let cstr = std::ffi::CStr::from_ptr(encoding);
1952            Some(cstr.to_bytes().to_vec())
1953        }
1954    };
1955
1956    let mut reader = XmlTextReader::new(ctxt, url_bytes.as_deref(), enc_bytes.as_deref());
1957    reader.options = options;
1958    Box::into_raw(Box::new(reader))
1959}
1960
1961/// Create a text reader from I/O callbacks.
1962///
1963/// # UPSTREAM-PARITY
1964///
1965/// ```c
1966/// xmlTextReaderPtr xmlReaderForIO(xmlInputReadCallback ioread, xmlInputCloseCallback ioclose,
1967///                                 void *ioctx, const char *URL,
1968///                                 const char *encoding, int options);
1969/// ```
1970///
1971/// # Safety
1972///
1973/// - `ioread` and `ioclose` must be valid function pointers or None.
1974/// - `ioctx` must be a valid context pointer for the callbacks.
1975/// - `URL` and `encoding` must be valid C strings or NULL.
1976#[no_mangle]
1977pub unsafe extern "C" fn xmlReaderForIO(
1978    ioread: Option<xmlInputReadCallback>,
1979    ioclose: Option<xmlInputCloseCallback>,
1980    ioctx: *mut c_void,
1981    URL: *const c_char,
1982    encoding: *const c_char,
1983    options: c_int,
1984) -> *mut XmlTextReader {
1985    // SAFETY: create_parser_ctxt returns a valid context or NULL.
1986    let ctxt = unsafe { create_parser_ctxt() };
1987    if ctxt.is_null() {
1988        return ptr::null_mut();
1989    }
1990
1991    // SAFETY: input_from_io reads all data via callbacks.
1992    let input = unsafe { input_from_io(ioread, ioclose, ioctx) };
1993
1994    // SAFETY: ctxt and input are valid.
1995    unsafe { setup_parser_input(ctxt, input) };
1996    unsafe {
1997        (*ctxt).options = options;
1998    }
1999
2000    let url_bytes = if URL.is_null() {
2001        None
2002    } else {
2003        // SAFETY: URL is a valid C string.
2004        unsafe {
2005            let cstr = std::ffi::CStr::from_ptr(URL);
2006            Some(cstr.to_bytes().to_vec())
2007        }
2008    };
2009
2010    let enc_bytes = if encoding.is_null() {
2011        None
2012    } else {
2013        // SAFETY: encoding is a valid C string.
2014        unsafe {
2015            let cstr = std::ffi::CStr::from_ptr(encoding);
2016            Some(cstr.to_bytes().to_vec())
2017        }
2018    };
2019
2020    let mut reader = XmlTextReader::new(ctxt, url_bytes.as_deref(), enc_bytes.as_deref());
2021    reader.options = options;
2022    Box::into_raw(Box::new(reader))
2023}
2024
2025// ─────────────────────────────────────────────────────────────────────────────
2026// Navigation functions
2027// ─────────────────────────────────────────────────────────────────────────────
2028
2029/// Advance the reader to the next node in document order.
2030///
2031/// Returns 1 on success, 0 if EOF, -1 on error.
2032///
2033/// # UPSTREAM-PARITY
2034///
2035/// ```c
2036/// int xmlTextReaderRead(xmlTextReaderPtr reader);
2037/// ```
2038///
2039/// # Safety
2040///
2041/// `reader` must be a valid pointer returned by `xmlNewTextReader` or one of the
2042/// `xmlReaderFor*` functions, or NULL (in which case -1 is returned).
2043#[no_mangle]
2044pub unsafe extern "C" fn xmlTextReaderRead(reader: *mut XmlTextReader) -> c_int {
2045    if reader.is_null() {
2046        return -1;
2047    }
2048    // SAFETY: reader is valid.
2049    unsafe { (*reader).Read() }
2050}
2051
2052/// Skip to the next sibling of the current node.
2053///
2054/// Returns 1 on success, 0 if no more siblings, -1 on error.
2055///
2056/// # UPSTREAM-PARITY
2057///
2058/// ```c
2059/// int xmlTextReaderNext(xmlTextReaderPtr reader);
2060/// ```
2061///
2062/// # Safety
2063///
2064/// `reader` must be a valid pointer or NULL.
2065#[no_mangle]
2066pub unsafe extern "C" fn xmlTextReaderNext(reader: *mut XmlTextReader) -> c_int {
2067    if reader.is_null() {
2068        return -1;
2069    }
2070    // SAFETY: reader is valid.
2071    unsafe { (*reader).Next() }
2072}
2073
2074/// Skip to the next sibling (same as xmlTextReaderNext).
2075///
2076/// # UPSTREAM-PARITY
2077///
2078/// ```c
2079/// int xmlTextReaderNextSibling(xmlTextReaderPtr reader);
2080/// ```
2081///
2082/// # Safety
2083///
2084/// `reader` must be a valid pointer or NULL.
2085#[no_mangle]
2086pub unsafe extern "C" fn xmlTextReaderNextSibling(reader: *mut XmlTextReader) -> c_int {
2087    if reader.is_null() {
2088        return -1;
2089    }
2090    // SAFETY: reader is valid.
2091    unsafe { (*reader).Next() }
2092}
2093
2094/// Skip to the previous sibling of the current node.
2095///
2096/// Returns 1 on success, 0 if no previous sibling, -1 on error.
2097///
2098/// # UPSTREAM-PARITY
2099///
2100/// ```c
2101/// int xmlTextReaderPrev(xmlTextReaderPtr reader);
2102/// ```
2103///
2104/// # Safety
2105///
2106/// `reader` must be a valid pointer or NULL.
2107#[no_mangle]
2108pub unsafe extern "C" fn xmlTextReaderPrev(reader: *mut XmlTextReader) -> c_int {
2109    if reader.is_null() {
2110        return -1;
2111    }
2112    // SAFETY: reader is valid.
2113    unsafe { (*reader).Prev() }
2114}
2115
2116/// Move the reader back to the parent element (from an attribute).
2117///
2118/// Returns 1 on success, 0 if not on an attribute, -1 on error.
2119///
2120/// # UPSTREAM-PARITY
2121///
2122/// ```c
2123/// int xmlTextReaderMoveToElement(xmlTextReaderPtr reader);
2124/// ```
2125///
2126/// # Safety
2127///
2128/// `reader` must be a valid pointer or NULL.
2129#[no_mangle]
2130pub unsafe extern "C" fn xmlTextReaderMoveToElement(reader: *mut XmlTextReader) -> c_int {
2131    if reader.is_null() {
2132        return -1;
2133    }
2134    // SAFETY: reader is valid.
2135    unsafe { (*reader).MoveToElement() }
2136}
2137
2138/// Move to an attribute by name.
2139///
2140/// Returns 1 on success, 0 if not found, -1 on error.
2141///
2142/// # UPSTREAM-PARITY
2143///
2144/// ```c
2145/// int xmlTextReaderMoveToAttribute(xmlTextReaderPtr reader, const xmlChar *name);
2146/// ```
2147///
2148/// # Safety
2149///
2150/// `reader` must be a valid pointer or NULL. `name` must be a valid C string or NULL.
2151#[no_mangle]
2152pub unsafe extern "C" fn xmlTextReaderMoveToAttribute(
2153    reader: *mut XmlTextReader,
2154    name: *const xmlChar,
2155) -> c_int {
2156    if reader.is_null() || name.is_null() {
2157        return -1;
2158    }
2159    // SAFETY: reader and name are valid.
2160    unsafe { (*reader).MoveToAttribute(name) }
2161}
2162
2163/// Move to an attribute by index.
2164///
2165/// Returns 1 on success, 0 if not found, -1 on error.
2166///
2167/// # UPSTREAM-PARITY
2168///
2169/// ```c
2170/// int xmlTextReaderMoveToAttributeNo(xmlTextReaderPtr reader, int index);
2171/// ```
2172///
2173/// # Safety
2174///
2175/// `reader` must be a valid pointer or NULL.
2176#[no_mangle]
2177pub unsafe extern "C" fn xmlTextReaderMoveToAttributeNo(
2178    reader: *mut XmlTextReader,
2179    index: c_int,
2180) -> c_int {
2181    if reader.is_null() {
2182        return -1;
2183    }
2184    // SAFETY: reader is valid.
2185    unsafe { (*reader).MoveToAttributeNo(index) }
2186}
2187
2188/// Move to the first attribute of the current element.
2189///
2190/// Returns 1 on success, 0 if no attributes, -1 on error.
2191///
2192/// # UPSTREAM-PARITY
2193///
2194/// ```c
2195/// int xmlTextReaderMoveToFirstAttribute(xmlTextReaderPtr reader);
2196/// ```
2197///
2198/// # Safety
2199///
2200/// `reader` must be a valid pointer or NULL.
2201#[no_mangle]
2202pub unsafe extern "C" fn xmlTextReaderMoveToFirstAttribute(reader: *mut XmlTextReader) -> c_int {
2203    if reader.is_null() {
2204        return -1;
2205    }
2206    // SAFETY: reader is valid.
2207    unsafe { (*reader).MoveToFirstAttribute() }
2208}
2209
2210/// Move to the next attribute.
2211///
2212/// Returns 1 on success, 0 if no more attributes, -1 on error.
2213///
2214/// # UPSTREAM-PARITY
2215///
2216/// ```c
2217/// int xmlTextReaderMoveToNextAttribute(xmlTextReaderPtr reader);
2218/// ```
2219///
2220/// # Safety
2221///
2222/// `reader` must be a valid pointer or NULL.
2223#[no_mangle]
2224pub unsafe extern "C" fn xmlTextReaderMoveToNextAttribute(reader: *mut XmlTextReader) -> c_int {
2225    if reader.is_null() {
2226        return -1;
2227    }
2228    // SAFETY: reader is valid.
2229    unsafe { (*reader).MoveToNextAttribute() }
2230}
2231
2232// ─────────────────────────────────────────────────────────────────────────────
2233// Information methods
2234// ─────────────────────────────────────────────────────────────────────────────
2235
2236/// Get the attribute count of the current element.
2237///
2238/// Returns the number of attributes, or -1 if not on an element.
2239///
2240/// # UPSTREAM-PARITY
2241///
2242/// ```c
2243/// int xmlTextReaderAttributeCount(xmlTextReaderPtr reader);
2244/// ```
2245///
2246/// # Safety
2247///
2248/// `reader` must be a valid pointer or NULL.
2249#[no_mangle]
2250pub unsafe extern "C" fn xmlTextReaderAttributeCount(reader: *mut XmlTextReader) -> c_int {
2251    if reader.is_null() {
2252        return -1;
2253    }
2254    // SAFETY: reader is valid.
2255    unsafe { (*reader).AttributeCount() }
2256}
2257
2258/// Get the depth of the current node.
2259///
2260/// Returns the depth (0 for root element), or -1 on error.
2261///
2262/// # UPSTREAM-PARITY
2263///
2264/// ```c
2265/// int xmlTextReaderDepth(xmlTextReaderPtr reader);
2266/// ```
2267///
2268/// # Safety
2269///
2270/// `reader` must be a valid pointer or NULL.
2271#[no_mangle]
2272pub unsafe extern "C" fn xmlTextReaderDepth(reader: *mut XmlTextReader) -> c_int {
2273    if reader.is_null() {
2274        return -1;
2275    }
2276    // SAFETY: reader is valid.
2277    unsafe { (*reader).Depth() }
2278}
2279
2280/// Get the node type of the current node.
2281///
2282/// Returns one of the `xmlReaderTypes` constants, or -1 on error.
2283///
2284/// # UPSTREAM-PARITY
2285///
2286/// ```c
2287/// int xmlTextReaderNodeType(xmlTextReaderPtr reader);
2288/// ```
2289///
2290/// # Safety
2291///
2292/// `reader` must be a valid pointer or NULL.
2293#[no_mangle]
2294pub unsafe extern "C" fn xmlTextReaderNodeType(reader: *mut XmlTextReader) -> c_int {
2295    if reader.is_null() {
2296        return -1;
2297    }
2298    // SAFETY: reader is valid.
2299    unsafe { (*reader).NodeType() as c_int }
2300}
2301
2302/// Get the name of the current node.
2303///
2304/// Returns a newly allocated string (caller must free with `xmlFree`),
2305/// or NULL if there is no name.
2306///
2307/// # UPSTREAM-PARITY
2308///
2309/// ```c
2310/// xmlChar *xmlTextReaderName(xmlTextReaderPtr reader);
2311/// ```
2312///
2313/// # Safety
2314///
2315/// `reader` must be a valid pointer or NULL.
2316#[no_mangle]
2317pub unsafe extern "C" fn xmlTextReaderName(reader: *mut XmlTextReader) -> *mut xmlChar {
2318    if reader.is_null() {
2319        return ptr::null_mut();
2320    }
2321    // SAFETY: reader is valid.
2322    unsafe { (*reader).Name() }
2323}
2324
2325/// Get the value of the current node.
2326///
2327/// Returns a newly allocated string (caller must free with `xmlFree`),
2328/// or NULL if there is no value.
2329///
2330/// # UPSTREAM-PARITY
2331///
2332/// ```c
2333/// xmlChar *xmlTextReaderValue(xmlTextReaderPtr reader);
2334/// ```
2335///
2336/// # Safety
2337///
2338/// `reader` must be a valid pointer or NULL.
2339#[no_mangle]
2340pub unsafe extern "C" fn xmlTextReaderValue(reader: *mut XmlTextReader) -> *mut xmlChar {
2341    if reader.is_null() {
2342        return ptr::null_mut();
2343    }
2344    // SAFETY: reader is valid.
2345    unsafe { (*reader).Value() }
2346}
2347
2348/// Get a constant pointer to the name (no copy).
2349///
2350/// The returned pointer is valid only while the reader is alive and positioned
2351/// on the same node.
2352///
2353/// # UPSTREAM-PARITY
2354///
2355/// ```c
2356/// const xmlChar *xmlTextReaderConstName(xmlTextReaderPtr reader);
2357/// ```
2358///
2359/// # Safety
2360///
2361/// `reader` must be a valid pointer or NULL.
2362#[no_mangle]
2363pub unsafe extern "C" fn xmlTextReaderConstName(reader: *mut XmlTextReader) -> *const xmlChar {
2364    if reader.is_null() {
2365        return ptr::null();
2366    }
2367    // SAFETY: reader is valid.
2368    unsafe { (*reader).ConstName() }
2369}
2370
2371/// Get a constant pointer to the value (no copy).
2372///
2373/// The returned pointer is valid only while the reader is alive and positioned
2374/// on the same node.
2375///
2376/// # UPSTREAM-PARITY
2377///
2378/// ```c
2379/// const xmlChar *xmlTextReaderConstValue(xmlTextReaderPtr reader);
2380/// ```
2381///
2382/// # Safety
2383///
2384/// `reader` must be a valid pointer or NULL.
2385#[no_mangle]
2386pub unsafe extern "C" fn xmlTextReaderConstValue(reader: *mut XmlTextReader) -> *const xmlChar {
2387    if reader.is_null() {
2388        return ptr::null();
2389    }
2390    // SAFETY: reader is valid.
2391    unsafe { (*reader).ConstValue() }
2392}
2393
2394/// Get the base URI of the current node.
2395///
2396/// Returns a newly allocated string (caller must free with `xmlFree`),
2397/// or NULL if not available.
2398///
2399/// # UPSTREAM-PARITY
2400///
2401/// ```c
2402/// xmlChar *xmlTextReaderBaseUri(xmlTextReaderPtr reader);
2403/// ```
2404///
2405/// # Safety
2406///
2407/// `reader` must be a valid pointer or NULL.
2408#[no_mangle]
2409pub unsafe extern "C" fn xmlTextReaderBaseUri(reader: *mut XmlTextReader) -> *mut xmlChar {
2410    if reader.is_null() {
2411        return ptr::null_mut();
2412    }
2413    // SAFETY: reader is valid.
2414    unsafe { (*reader).BaseUri() }
2415}
2416
2417/// Get the local name of the current node.
2418///
2419/// Returns a newly allocated string, or NULL.
2420///
2421/// # UPSTREAM-PARITY
2422///
2423/// ```c
2424/// xmlChar *xmlTextReaderLocalName(xmlTextReaderPtr reader);
2425/// ```
2426///
2427/// # Safety
2428///
2429/// `reader` must be a valid pointer or NULL.
2430#[no_mangle]
2431pub unsafe extern "C" fn xmlTextReaderLocalName(reader: *mut XmlTextReader) -> *mut xmlChar {
2432    if reader.is_null() {
2433        return ptr::null_mut();
2434    }
2435    // SAFETY: reader is valid.
2436    unsafe { (*reader).LocalName() }
2437}
2438
2439/// Get the namespace URI of the current node.
2440///
2441/// Returns a newly allocated string, or NULL.
2442///
2443/// # UPSTREAM-PARITY
2444///
2445/// ```c
2446/// xmlChar *xmlTextReaderNamespaceUri(xmlTextReaderPtr reader);
2447/// ```
2448///
2449/// # Safety
2450///
2451/// `reader` must be a valid pointer or NULL.
2452#[no_mangle]
2453pub unsafe extern "C" fn xmlTextReaderNamespaceUri(reader: *mut XmlTextReader) -> *mut xmlChar {
2454    if reader.is_null() {
2455        return ptr::null_mut();
2456    }
2457    // SAFETY: reader is valid.
2458    unsafe { (*reader).NamespaceUri() }
2459}
2460
2461/// Get the prefix of the current node.
2462///
2463/// Returns a newly allocated string, or NULL.
2464///
2465/// # UPSTREAM-PARITY
2466///
2467/// ```c
2468/// xmlChar *xmlTextReaderPrefix(xmlTextReaderPtr reader);
2469/// ```
2470///
2471/// # Safety
2472///
2473/// `reader` must be a valid pointer or NULL.
2474#[no_mangle]
2475pub unsafe extern "C" fn xmlTextReaderPrefix(reader: *mut XmlTextReader) -> *mut xmlChar {
2476    if reader.is_null() {
2477        return ptr::null_mut();
2478    }
2479    // SAFETY: reader is valid.
2480    unsafe { (*reader).Prefix() }
2481}
2482
2483/// Check if the current node has a value.
2484///
2485/// Returns 1 if the node has a value, 0 otherwise.
2486///
2487/// # UPSTREAM-PARITY
2488///
2489/// ```c
2490/// int xmlTextReaderHasValue(xmlTextReaderPtr reader);
2491/// ```
2492///
2493/// # Safety
2494///
2495/// `reader` must be a valid pointer or NULL.
2496#[no_mangle]
2497pub unsafe extern "C" fn xmlTextReaderHasValue(reader: *mut XmlTextReader) -> c_int {
2498    if reader.is_null() {
2499        return 0;
2500    }
2501    // SAFETY: reader is valid.
2502    unsafe { (*reader).HasValue() }
2503}
2504
2505/// Check if the current node has attributes.
2506///
2507/// Returns 1 if the node has attributes, 0 otherwise.
2508///
2509/// # UPSTREAM-PARITY
2510///
2511/// ```c
2512/// int xmlTextReaderHasAttributes(xmlTextReaderPtr reader);
2513/// ```
2514///
2515/// # Safety
2516///
2517/// `reader` must be a valid pointer or NULL.
2518#[no_mangle]
2519pub unsafe extern "C" fn xmlTextReaderHasAttributes(reader: *mut XmlTextReader) -> c_int {
2520    if reader.is_null() {
2521        return 0;
2522    }
2523    // SAFETY: reader is valid.
2524    unsafe { (*reader).HasAttributes() }
2525}
2526
2527/// Check if the current element is an empty element (no children).
2528///
2529/// Returns 1 if empty, 0 otherwise.
2530///
2531/// # UPSTREAM-PARITY
2532///
2533/// ```c
2534/// int xmlTextReaderIsEmptyElement(xmlTextReaderPtr reader);
2535/// ```
2536///
2537/// # Safety
2538///
2539/// `reader` must be a valid pointer or NULL.
2540#[no_mangle]
2541pub unsafe extern "C" fn xmlTextReaderIsEmptyElement(reader: *mut XmlTextReader) -> c_int {
2542    if reader.is_null() {
2543        return 0;
2544    }
2545    // SAFETY: reader is valid.
2546    unsafe { (*reader).IsEmptyElement() }
2547}
2548
2549/// Get the read state.
2550///
2551/// Returns one of the `xmlTextReaderReadState` constants.
2552///
2553/// # UPSTREAM-PARITY
2554///
2555/// ```c
2556/// int xmlTextReaderReadState(xmlTextReaderPtr reader);
2557/// ```
2558///
2559/// # Safety
2560///
2561/// `reader` must be a valid pointer or NULL.
2562#[no_mangle]
2563pub unsafe extern "C" fn xmlTextReaderReadState(reader: *mut XmlTextReader) -> c_int {
2564    if reader.is_null() {
2565        return ReadState::ERROR as c_int;
2566    }
2567    // SAFETY: reader is valid.
2568    unsafe { (*reader).ReadState() as c_int }
2569}
2570
2571// ─────────────────────────────────────────────────────────────────────────────
2572// Attribute access
2573// ─────────────────────────────────────────────────────────────────────────────
2574
2575/// Get an attribute value by name.
2576///
2577/// Returns a newly allocated string, or NULL.
2578///
2579/// # UPSTREAM-PARITY
2580///
2581/// ```c
2582/// xmlChar *xmlTextReaderGetAttribute(xmlTextReaderPtr reader, const xmlChar *name);
2583/// ```
2584///
2585/// # Safety
2586///
2587/// `reader` and `name` must be valid pointers or NULL.
2588#[no_mangle]
2589pub unsafe extern "C" fn xmlTextReaderGetAttribute(
2590    reader: *mut XmlTextReader,
2591    name: *const xmlChar,
2592) -> *mut xmlChar {
2593    if reader.is_null() || name.is_null() {
2594        return ptr::null_mut();
2595    }
2596    // SAFETY: reader and name are valid.
2597    unsafe { (*reader).GetAttribute(name) }
2598}
2599
2600/// Get an attribute value by index.
2601///
2602/// Returns a newly allocated string, or NULL.
2603///
2604/// # UPSTREAM-PARITY
2605///
2606/// ```c
2607/// xmlChar *xmlTextReaderGetAttributeNo(xmlTextReaderPtr reader, int index);
2608/// ```
2609///
2610/// # Safety
2611///
2612/// `reader` must be a valid pointer or NULL.
2613#[no_mangle]
2614pub unsafe extern "C" fn xmlTextReaderGetAttributeNo(
2615    reader: *mut XmlTextReader,
2616    index: c_int,
2617) -> *mut xmlChar {
2618    if reader.is_null() {
2619        return ptr::null_mut();
2620    }
2621    // SAFETY: reader is valid.
2622    unsafe { (*reader).GetAttributeNo(index) }
2623}
2624
2625/// Get an attribute value by local name and namespace URI.
2626///
2627/// Returns a newly allocated string, or NULL.
2628///
2629/// # UPSTREAM-PARITY
2630///
2631/// ```c
2632/// xmlChar *xmlTextReaderGetAttributeNs(xmlTextReaderPtr reader,
2633///                                      const xmlChar *localName,
2634///                                      const xmlChar *namespaceURI);
2635/// ```
2636///
2637/// # Safety
2638///
2639/// `reader`, `localName`, and `namespaceURI` must be valid pointers or NULL.
2640#[no_mangle]
2641pub unsafe extern "C" fn xmlTextReaderGetAttributeNs(
2642    reader: *mut XmlTextReader,
2643    localName: *const xmlChar,
2644    namespaceURI: *const xmlChar,
2645) -> *mut xmlChar {
2646    if reader.is_null() || localName.is_null() {
2647        return ptr::null_mut();
2648    }
2649    // SAFETY: reader, localName, and namespaceURI are valid.
2650    unsafe { (*reader).GetAttributeNs(localName, namespaceURI) }
2651}
2652
2653/// Look up a namespace by prefix.
2654///
2655/// Returns a newly allocated string with the namespace URI, or NULL.
2656///
2657/// # UPSTREAM-PARITY
2658///
2659/// ```c
2660/// xmlChar *xmlTextReaderLookupNamespace(xmlTextReaderPtr reader, const xmlChar *prefix);
2661/// ```
2662///
2663/// # Safety
2664///
2665/// `reader` and `prefix` must be valid pointers or NULL.
2666#[no_mangle]
2667pub unsafe extern "C" fn xmlTextReaderLookupNamespace(
2668    reader: *mut XmlTextReader,
2669    prefix: *const xmlChar,
2670) -> *mut xmlChar {
2671    if reader.is_null() {
2672        return ptr::null_mut();
2673    }
2674    // SAFETY: reader and prefix are valid.
2675    unsafe { (*reader).LookupNamespace(prefix) }
2676}
2677
2678// ─────────────────────────────────────────────────────────────────────────────
2679// Parser properties
2680// ─────────────────────────────────────────────────────────────────────────────
2681
2682/// Get a parser property.
2683///
2684/// Returns the property value (0 or 1), or -1 on error.
2685///
2686/// # UPSTREAM-PARITY
2687///
2688/// ```c
2689/// int xmlTextReaderGetParserProp(xmlTextReaderPtr reader, int prop);
2690/// ```
2691///
2692/// # Safety
2693///
2694/// `reader` must be a valid pointer or NULL.
2695#[no_mangle]
2696pub unsafe extern "C" fn xmlTextReaderGetParserProp(
2697    reader: *mut XmlTextReader,
2698    prop: c_int,
2699) -> c_int {
2700    if reader.is_null() {
2701        return -1;
2702    }
2703    // SAFETY: reader is valid.
2704    unsafe { (*reader).GetParserProp(prop) }
2705}
2706
2707/// Set a parser property.
2708///
2709/// Returns 0 on success, -1 on error.
2710///
2711/// # UPSTREAM-PARITY
2712///
2713/// ```c
2714/// int xmlTextReaderSetParserProp(xmlTextReaderPtr reader, int prop, int value);
2715/// ```
2716///
2717/// # Safety
2718///
2719/// `reader` must be a valid pointer or NULL.
2720#[no_mangle]
2721pub unsafe extern "C" fn xmlTextReaderSetParserProp(
2722    reader: *mut XmlTextReader,
2723    prop: c_int,
2724    value: c_int,
2725) -> c_int {
2726    if reader.is_null() {
2727        return -1;
2728    }
2729    // SAFETY: reader is valid.
2730    unsafe { (*reader).SetParserProp(prop, value) }
2731}
2732
2733// ─────────────────────────────────────────────────────────────────────────────
2734// Lifecycle
2735// ─────────────────────────────────────────────────────────────────────────────
2736
2737/// Free a text reader.
2738///
2739/// # UPSTREAM-PARITY
2740///
2741/// ```c
2742/// void xmlFreeTextReader(xmlTextReaderPtr reader);
2743/// ```
2744///
2745/// # Safety
2746///
2747/// `reader` must be a valid pointer returned by `xmlNewTextReader` or one of the
2748/// `xmlReaderFor*` functions, or NULL (in which case this is a no-op).
2749#[no_mangle]
2750pub unsafe extern "C" fn xmlFreeTextReader(reader: *mut XmlTextReader) {
2751    if reader.is_null() {
2752        return;
2753    }
2754    // SAFETY: reader was created via Box::into_raw, so we reconstruct the Box
2755    // and let it drop, which calls the Drop impl.
2756    unsafe {
2757        let _ = Box::from_raw(reader);
2758    }
2759}
2760
2761/// Setup/reinitialize a reader with new input.
2762///
2763/// # UPSTREAM-PARITY
2764///
2765/// ```c
2766/// int xmlTextReaderSetup(xmlTextReaderPtr reader,
2767///                        xmlParserInputBufferPtr input,
2768///                        const char *URL, const char *encoding, int options);
2769/// ```
2770///
2771/// # Safety
2772///
2773/// - `reader` must be a valid pointer or NULL.
2774/// - `input` must be a valid `_xmlParserInputBuffer` pointer or NULL.
2775/// - `URL` and `encoding` must be valid C strings or NULL.
2776#[no_mangle]
2777pub unsafe extern "C" fn xmlTextReaderSetup(
2778    reader: *mut XmlTextReader,
2779    input: *mut _xmlParserInputBuffer,
2780    URL: *const c_char,
2781    encoding: *const c_char,
2782    options: c_int,
2783) -> c_int {
2784    if reader.is_null() {
2785        return -1;
2786    }
2787
2788    // SAFETY: reader is valid.
2789    let r = unsafe { &mut *reader };
2790
2791    // Reset the reader state.
2792    r.clear_cached_name();
2793    r.clear_cached_value();
2794
2795    // Free the old document.
2796    if !r.doc.is_null() {
2797        // SAFETY: doc was allocated by the parser.
2798        unsafe { tree::free_doc(r.doc) };
2799        r.doc = ptr::null_mut();
2800    }
2801
2802    // Free old parser context.
2803    if !r.ctxt.is_null() {
2804        // SAFETY: ctxt was created by create_parser_ctxt.
2805        unsafe { free_parser_ctxt(r.ctxt) };
2806        r.ctxt = ptr::null_mut();
2807    }
2808
2809    r.events.clear();
2810    r.event_index = 0;
2811    r.state = ReadState::INITIALIZED;
2812    r.cur_node = ptr::null_mut();
2813    r.node_type = ReaderNodeType::NONE;
2814    r.depth = 0;
2815    r.attribute_count = -1;
2816    r.cur_attribute = -1;
2817    r.options = options;
2818    r.parsed = false;
2819    r.errors.clear();
2820
2821    // Update URL.
2822    if !r.URL.is_null() {
2823        // SAFETY: URL was allocated by xmlMalloc.
2824        unsafe { xmlFree(r.URL as *mut c_void) };
2825        r.URL = ptr::null_mut();
2826    }
2827    if !URL.is_null() {
2828        // SAFETY: URL is a valid C string.
2829        let url_str = unsafe { std::ffi::CStr::from_ptr(URL) };
2830        // SAFETY: bytes_to_xmlstr allocates via xmlMalloc.
2831        r.URL = unsafe { bytes_to_xmlstr(url_str.to_bytes()) };
2832    }
2833
2834    // Update encoding.
2835    if !r.encoding.is_null() {
2836        // SAFETY: encoding was allocated by xmlMalloc.
2837        unsafe { xmlFree(r.encoding as *mut c_void) };
2838        r.encoding = ptr::null_mut();
2839    }
2840    if !encoding.is_null() {
2841        // SAFETY: encoding is a valid C string.
2842        let enc_str = unsafe { std::ffi::CStr::from_ptr(encoding) };
2843        // SAFETY: bytes_to_xmlstr allocates via xmlMalloc.
2844        r.encoding = unsafe { bytes_to_xmlstr(enc_str.to_bytes()) };
2845    }
2846
2847    // Create new parser context and set up input.
2848    if !input.is_null() {
2849        // SAFETY: create_parser_ctxt returns a valid context or NULL.
2850        let ctxt = unsafe { create_parser_ctxt() };
2851        if ctxt.is_null() {
2852            return -1;
2853        }
2854
2855        // Read all data from the input buffer.
2856        let mut data = Vec::new();
2857        let mut tmp = [0u8; 4096];
2858
2859        // SAFETY: input is valid.
2860        let read_cb = unsafe { (*input).readcallback };
2861        let ioctx = unsafe { (*input).context };
2862
2863        if let Some(read) = read_cb {
2864            loop {
2865                // SAFETY: callbacks are valid.
2866                let n = unsafe { read(ioctx, tmp.as_mut_ptr() as *mut c_char, tmp.len() as c_int) };
2867                if n <= 0 {
2868                    break;
2869                }
2870                data.extend_from_slice(&tmp[..n as usize]);
2871            }
2872        }
2873
2874        // Close the input.
2875        let close_cb = unsafe { (*input).closecallback };
2876        if let Some(close) = close_cb {
2877            // SAFETY: close callback is valid.
2878            unsafe { close(ioctx) };
2879        }
2880
2881        let input_buf = InputBuffer::from_memory(&data, None);
2882
2883        // SAFETY: ctxt and input_buf are valid.
2884        unsafe { setup_parser_input(ctxt, input_buf) };
2885        unsafe {
2886            (*ctxt).options = options;
2887        }
2888
2889        r.ctxt = ctxt;
2890    }
2891
2892    0
2893}
2894
2895/// Get the current document from the reader.
2896///
2897/// Returns a pointer to the `_xmlDoc` or NULL.
2898///
2899/// # UPSTREAM-PARITY
2900///
2901/// ```c
2902/// xmlDocPtr xmlTextReaderCurrentDoc(xmlTextReaderPtr reader);
2903/// ```
2904///
2905/// # Safety
2906///
2907/// `reader` must be a valid pointer or NULL.
2908#[no_mangle]
2909pub unsafe extern "C" fn xmlTextReaderCurrentDoc(reader: *mut XmlTextReader) -> *mut _xmlDoc {
2910    if reader.is_null() {
2911        return ptr::null_mut();
2912    }
2913    // SAFETY: reader is valid.
2914    unsafe { (*reader).CurrentDoc() }
2915}
2916
2917/// Close the reader, releasing the document and parser state.
2918///
2919/// # UPSTREAM-PARITY
2920///
2921/// Upstream `xmlTextReaderClose` (xmlreader.c): sets the mode to
2922/// XML_TEXTREADER_MODE_CLOSED, drops the current node, and tears down the
2923/// validation state. The reader object itself is freed separately with
2924/// `xmlFreeTextReader`.
2925///
2926/// ```c
2927/// int xmlTextReaderClose(xmlTextReaderPtr reader);
2928/// ```
2929///
2930/// Returns 0 on success, -1 if `reader` is NULL.
2931///
2932/// # Safety
2933///
2934/// `reader` must be a valid pointer or NULL.
2935#[no_mangle]
2936pub unsafe extern "C" fn xmlTextReaderClose(reader: *mut XmlTextReader) -> c_int {
2937    if reader.is_null() {
2938        return -1;
2939    }
2940    // SAFETY: reader is valid; close resets cursor state and marks the
2941    // reader closed, mirroring upstream's mode transition.
2942    unsafe {
2943        let r = &mut *reader;
2944        r.cur_node = ptr::null_mut();
2945        r.node_type = ReaderNodeType::NONE;
2946        r.clear_cached_name();
2947        r.clear_cached_value();
2948        r.state = ReadState::CLOSED;
2949    }
2950    0
2951}
2952
2953/// Return the current node of the reader.
2954///
2955/// # UPSTREAM-PARITY
2956///
2957/// ```c
2958/// xmlNodePtr xmlTextReaderCurrentNode(xmlTextReaderPtr reader);
2959/// ```
2960///
2961/// Returns the current node or NULL. The node is owned by the document;
2962/// the caller must not free it.
2963///
2964/// # Safety
2965///
2966/// `reader` must be a valid pointer or NULL.
2967#[no_mangle]
2968pub unsafe extern "C" fn xmlTextReaderCurrentNode(reader: *mut XmlTextReader) -> *mut _xmlNode {
2969    if reader.is_null() {
2970        return ptr::null_mut();
2971    }
2972    // SAFETY: reader is valid.
2973    unsafe { (*reader).cur_node }
2974}
2975
2976/// Expand entity references at the current position.
2977///
2978/// # UPSTREAM-PARITY
2979///
2980/// Upstream `xmlTextReaderExpand` (xmlreader.c) forces substitution of the
2981/// current entity reference so the node can be read in full. When the parser
2982/// ran with XML_PARSE_NOENT the entities are already substituted during
2983/// parsing; the function then simply returns the current node.
2984///
2985/// ```c
2986/// xmlNodePtr xmlTextReaderExpand(xmlTextReaderPtr reader);
2987/// ```
2988///
2989/// Returns the (expanded) current node, or NULL if the reader is NULL or
2990/// not positioned on a node.
2991///
2992/// # Safety
2993///
2994/// `reader` must be a valid pointer or NULL.
2995#[no_mangle]
2996pub unsafe extern "C" fn xmlTextReaderExpand(reader: *mut XmlTextReader) -> *mut _xmlNode {
2997    if reader.is_null() {
2998        return ptr::null_mut();
2999    }
3000    // SAFETY: reader is valid.
3001    unsafe { (*reader).cur_node }
3002}
3003
3004/// Return the parser line number of the current node.
3005///
3006/// # UPSTREAM-PARITY
3007///
3008/// Upstream `xmlTextReaderGetParserLineNumber` returns the input stream's
3009/// current line. The candidate records `line` per node during parsing, which
3010/// is equivalent for the read cursor.
3011///
3012/// ```c
3013/// int xmlTextReaderGetParserLineNumber(xmlTextReaderPtr reader);
3014/// ```
3015///
3016/// Returns the line number, or 0 when unavailable.
3017///
3018/// # Safety
3019///
3020/// `reader` must be a valid pointer or NULL.
3021#[no_mangle]
3022pub unsafe extern "C" fn xmlTextReaderGetParserLineNumber(reader: *mut XmlTextReader) -> c_int {
3023    if reader.is_null() {
3024        return 0;
3025    }
3026    // SAFETY: reader is valid; cur_node is owned by the doc.
3027    unsafe {
3028        let node = (*reader).cur_node;
3029        if node.is_null() {
3030            0
3031        } else {
3032            (*node).line as c_int
3033        }
3034    }
3035}
3036
3037/// Return the parser column number of the current node.
3038///
3039/// # UPSTREAM-PARITY
3040///
3041/// Upstream `xmlTextReaderGetParserColumnNumber` returns the input stream's
3042/// column. Columns are not tracked per-node in the candidate tree (upstream
3043/// exposes -1 when no column information is available either); return -1.
3044///
3045/// ```c
3046/// int xmlTextReaderGetParserColumnNumber(xmlTextReaderPtr reader);
3047/// ```
3048///
3049/// # Safety
3050///
3051/// `reader` must be a valid pointer or NULL.
3052#[no_mangle]
3053pub unsafe extern "C" fn xmlTextReaderGetParserColumnNumber(reader: *mut XmlTextReader) -> c_int {
3054    if reader.is_null() {
3055        return -1;
3056    }
3057    -1
3058}
3059
3060/// Return the validation status of the reader.
3061///
3062/// # UPSTREAM-PARITY
3063///
3064/// Upstream `xmlTextReaderIsValid` returns 1 when the document validated
3065/// successfully, 0 when no validation was performed, and -1 for a NULL
3066/// reader. The candidate reader does not yet perform DTD/XSD/RNG
3067/// validation (tracked in the parity ledger), so it reports 0 unless the
3068/// parse was run with validation requested.
3069///
3070/// ```c
3071/// int xmlTextReaderIsValid(xmlTextReaderPtr reader);
3072/// ```
3073///
3074/// # Safety
3075///
3076/// `reader` must be a valid pointer or NULL.
3077#[no_mangle]
3078pub unsafe extern "C" fn xmlTextReaderIsValid(reader: *mut XmlTextReader) -> c_int {
3079    if reader.is_null() {
3080        return -1;
3081    }
3082    0
3083}
3084
3085/// Return the normalization status of the reader.
3086///
3087/// # UPSTREAM-PARITY
3088///
3089/// Upstream `xmlTextReaderNormalization` returns 1 when the reader performs
3090/// whitespace normalization (it always reports 1 unless the parser was
3091/// configured otherwise). The candidate normalizes attribute values per the
3092/// XML spec during parsing, so report 1.
3093///
3094/// ```c
3095/// int xmlTextReaderNormalization(xmlTextReaderPtr reader);
3096/// ```
3097///
3098/// # Safety
3099///
3100/// `reader` must be a valid pointer or NULL.
3101#[no_mangle]
3102pub unsafe extern "C" fn xmlTextReaderNormalization(reader: *mut XmlTextReader) -> c_int {
3103    if reader.is_null() {
3104        return -1;
3105    }
3106    1
3107}
3108
3109/// Read the value of an attribute as a text node (attribute-value mode).
3110///
3111/// # UPSTREAM-PARITY
3112///
3113/// Upstream `xmlTextReaderReadAttributeValue` moves the reader so that the
3114/// value of the current attribute is available as a text node, returning 1
3115/// on success and 0 when already at the end. The candidate tree stores
3116/// attribute values directly on the attribute node, so the value is already
3117/// available via `xmlTextReaderValue`; report 1 when positioned on an
3118/// attribute with a value.
3119///
3120/// ```c
3121/// int xmlTextReaderReadAttributeValue(xmlTextReaderPtr reader);
3122/// ```
3123///
3124/// # Safety
3125///
3126/// `reader` must be a valid pointer or NULL.
3127#[no_mangle]
3128pub unsafe extern "C" fn xmlTextReaderReadAttributeValue(reader: *mut XmlTextReader) -> c_int {
3129    if reader.is_null() {
3130        return -1;
3131    }
3132    // SAFETY: reader is valid.
3133    unsafe {
3134        let r = &*reader;
3135        if r.node_type == ReaderNodeType::ATTRIBUTE && !r.cur_node.is_null() {
3136            1
3137        } else {
3138            0
3139        }
3140    }
3141}
3142
3143/// Read the content of the current node as a string.
3144///
3145/// # UPSTREAM-PARITY
3146///
3147/// Upstream `xmlTextReaderReadString` concatenates the text of the current
3148/// node's subtree (recursively) into one string. It behaves like
3149/// `xmlNodeGetContent` for the current node.
3150///
3151/// ```c
3152/// xmlChar *xmlTextReaderReadString(xmlTextReaderPtr reader);
3153/// ```
3154///
3155/// Returns a newly allocated string (free with `xmlFree`) or NULL.
3156///
3157/// # Safety
3158///
3159/// `reader` must be a valid pointer or NULL.
3160#[no_mangle]
3161pub unsafe extern "C" fn xmlTextReaderReadString(reader: *mut XmlTextReader) -> *mut xmlChar {
3162    if reader.is_null() {
3163        return ptr::null_mut();
3164    }
3165    // SAFETY: reader is valid; node owned by the document.
3166    unsafe {
3167        let node = (*reader).cur_node;
3168        if node.is_null() {
3169            return ptr::null_mut();
3170        }
3171        tree::node_get_content(node)
3172    }
3173}
3174
3175/// Read the inner XML of the current node as a string.
3176///
3177/// # UPSTREAM-PARITY
3178///
3179/// Upstream `xmlTextReaderReadInnerXml` serializes the children of the
3180/// current node. The candidate uses its serializer on the children list.
3181///
3182/// ```c
3183/// xmlChar *xmlTextReaderReadInnerXml(xmlTextReaderPtr reader);
3184/// ```
3185///
3186/// Returns a newly allocated string (free with `xmlFree`) or NULL.
3187///
3188/// # Safety
3189///
3190/// `reader` must be a valid pointer or NULL.
3191#[no_mangle]
3192pub unsafe extern "C" fn xmlTextReaderReadInnerXml(reader: *mut XmlTextReader) -> *mut xmlChar {
3193    if reader.is_null() {
3194        return ptr::null_mut();
3195    }
3196    // SAFETY: reader is valid; node owned by the document.
3197    unsafe {
3198        let node = (*reader).cur_node;
3199        if node.is_null() {
3200            return ptr::null_mut();
3201        }
3202        let buf = crate::xml::io::buf_create(-1);
3203        if buf.is_null() {
3204            return ptr::null_mut();
3205        }
3206        let mut child = (*node).children;
3207        while !child.is_null() {
3208            tree::serialize_node(child, buf, 0, 0);
3209            child = (*child).next;
3210        }
3211        let len = crate::xml::io::buf_length(buf) as usize;
3212        let content = crate::xml::io::buf_content(buf);
3213        if content.is_null() || len == 0 {
3214            crate::xml::io::buf_free(buf);
3215            return ptr::null_mut();
3216        }
3217        let out = xml_strdup(content);
3218        crate::xml::io::buf_free(buf);
3219        out
3220    }
3221}
3222
3223/// Read the outer XML of the current node as a string.
3224///
3225/// # UPSTREAM-PARITY
3226///
3227/// Upstream `xmlTextReaderReadOuterXml` serializes the current node itself.
3228///
3229/// ```c
3230/// xmlChar *xmlTextReaderReadOuterXml(xmlTextReaderPtr reader);
3231/// ```
3232///
3233/// Returns a newly allocated string (free with `xmlFree`) or NULL.
3234///
3235/// # Safety
3236///
3237/// `reader` must be a valid pointer or NULL.
3238#[no_mangle]
3239pub unsafe extern "C" fn xmlTextReaderReadOuterXml(reader: *mut XmlTextReader) -> *mut xmlChar {
3240    if reader.is_null() {
3241        return ptr::null_mut();
3242    }
3243    // SAFETY: reader is valid; node owned by the document.
3244    unsafe {
3245        let node = (*reader).cur_node;
3246        if node.is_null() {
3247            return ptr::null_mut();
3248        }
3249        let buf = crate::xml::io::buf_create(-1);
3250        if buf.is_null() {
3251            return ptr::null_mut();
3252        }
3253        tree::serialize_node(node, buf, 0, 0);
3254        let len = crate::xml::io::buf_length(buf) as usize;
3255        let content = crate::xml::io::buf_content(buf);
3256        if content.is_null() || len == 0 {
3257            crate::xml::io::buf_free(buf);
3258            return ptr::null_mut();
3259        }
3260        let out = xml_strdup(content);
3261        crate::xml::io::buf_free(buf);
3262        out
3263    }
3264}
3265
3266/// Return the standalone flag of the document being read.
3267///
3268/// # UPSTREAM-PARITY
3269///
3270/// Upstream `xmlTextReaderStandalone` returns the document's standalone
3271/// value (1 = standalone, 0 = not, -1 = no XML declaration / NULL reader).
3272///
3273/// ```c
3274/// int xmlTextReaderStandalone(xmlTextReaderPtr reader);
3275/// ```
3276///
3277/// # Safety
3278///
3279/// `reader` must be a valid pointer or NULL.
3280#[no_mangle]
3281pub unsafe extern "C" fn xmlTextReaderStandalone(reader: *mut XmlTextReader) -> c_int {
3282    if reader.is_null() {
3283        return -1;
3284    }
3285    // SAFETY: reader is valid; doc owned by the reader.
3286    unsafe {
3287        let doc = (*reader).doc;
3288        if doc.is_null() {
3289            return -1;
3290        }
3291        (*doc).standalone
3292    }
3293}
3294
3295/// Return the xml:lang of the current node.
3296///
3297/// # UPSTREAM-PARITY
3298///
3299/// Upstream `xmlTextReaderXmlLang` returns `xmlNodeGetLang(node)`: the
3300/// nearest `xml:lang` attribute on the node or an ancestor.
3301///
3302/// ```c
3303/// xmlChar *xmlTextReaderXmlLang(xmlTextReaderPtr reader);
3304/// ```
3305///
3306/// Returns a newly allocated string (free with `xmlFree`) or NULL.
3307///
3308/// # Safety
3309///
3310/// `reader` must be a valid pointer or NULL.
3311#[no_mangle]
3312pub unsafe extern "C" fn xmlTextReaderXmlLang(reader: *mut XmlTextReader) -> *mut xmlChar {
3313    if reader.is_null() {
3314        return ptr::null_mut();
3315    }
3316    // SAFETY: reader is valid; node owned by the document.
3317    unsafe {
3318        let mut node = (*reader).cur_node;
3319        while !node.is_null() {
3320            // walk the property list for xml:lang
3321            let mut prop = (*node).properties;
3322            while !prop.is_null() {
3323                if !(*prop).name.is_null() {
3324                    let name = crate::xml::string::xmlstr_to_bytes((*prop).name);
3325                    if name == b"lang" && !(*prop).ns.is_null() {
3326                        let ns_href = crate::xml::string::xmlstr_to_bytes((*(*prop).ns).href);
3327                        if ns_href == b"http://www.w3.org/XML/1998/namespace" {
3328                            let v = (*prop).children;
3329                            if !v.is_null() && !(*v).content.is_null() {
3330                                return xml_strdup((*v).content);
3331                            }
3332                        }
3333                    }
3334                }
3335                prop = (*prop).next;
3336            }
3337            node = (*node).parent;
3338        }
3339        ptr::null_mut()
3340    }
3341}
3342
3343// ═══════════════════════════════════════════════════════════════════════════════
3344// Tests
3345// ═══════════════════════════════════════════════════════════════════════════════
3346
3347#[cfg(test)]
3348mod tests {
3349    use super::*;
3350    use crate::abi::allocator::xmlFree;
3351    use core::ffi::c_void;
3352    use std::os::raw::c_char;
3353
3354    /// Helper: create a reader from a string.
3355    unsafe fn create_reader(xml: &str) -> *mut XmlTextReader {
3356        let bytes = xml.as_bytes();
3357        xmlReaderForMemory(
3358            bytes.as_ptr() as *const c_char,
3359            bytes.len() as c_int,
3360            ptr::null(),
3361            ptr::null(),
3362            0,
3363        )
3364    }
3365
3366    /// Helper: free a reader.
3367    unsafe fn free_reader(reader: *mut XmlTextReader) {
3368        if !reader.is_null() {
3369            xmlFreeTextReader(reader);
3370        }
3371    }
3372
3373    /// Helper: read through all nodes and collect their types and names.
3374    unsafe fn collect_nodes(reader: *mut XmlTextReader) -> Vec<(ReaderNodeType, String, i32)> {
3375        let mut result = Vec::new();
3376        loop {
3377            let ret = xmlTextReaderRead(reader);
3378            if ret <= 0 {
3379                break;
3380            }
3381            // SAFETY: reader is valid.
3382            let r = &*reader;
3383            let ntype = r.NodeType();
3384            let name = if r.name.is_null() {
3385                String::new()
3386            } else {
3387                xmlstr_to_string(r.name as *const xmlChar)
3388            };
3389            let depth = r.Depth();
3390            result.push((ntype, name, depth));
3391        }
3392        result
3393    }
3394
3395    // ─── Basic tests ───────────────────────────────────────────────────────
3396
3397    #[test]
3398    fn test_create_reader_from_memory() {
3399        unsafe {
3400            let reader = create_reader("<root/>");
3401            assert!(!reader.is_null());
3402            assert_eq!((*reader).ReadState(), ReadState::INITIALIZED);
3403            free_reader(reader);
3404        }
3405    }
3406
3407    #[test]
3408    fn test_read_simple_document() {
3409        unsafe {
3410            let reader = create_reader("<root><child>text</child></root>");
3411            assert!(!reader.is_null());
3412
3413            let nodes = collect_nodes(reader);
3414            // Expected sequence:
3415            // ELEMENT root (depth=0)
3416            // ELEMENT child (depth=1)
3417            // TEXT text (depth=2)
3418            // END_ELEMENT child (depth=1)
3419            // END_ELEMENT root (depth=0)
3420
3421            assert_eq!(nodes.len(), 5);
3422            assert_eq!(nodes[0], (ReaderNodeType::ELEMENT, "root".to_string(), 0));
3423            assert_eq!(nodes[1], (ReaderNodeType::ELEMENT, "child".to_string(), 1));
3424            // UPSTREAM-PARITY: text nodes report the fixed name "#text".
3425            assert_eq!(nodes[2], (ReaderNodeType::TEXT, "#text".to_string(), 2));
3426            assert_eq!(
3427                nodes[3],
3428                (ReaderNodeType::END_ELEMENT, "child".to_string(), 1)
3429            );
3430            assert_eq!(
3431                nodes[4],
3432                (ReaderNodeType::END_ELEMENT, "root".to_string(), 0)
3433            );
3434
3435            assert_eq!((*reader).ReadState(), ReadState::EOF);
3436            free_reader(reader);
3437        }
3438    }
3439
3440    #[test]
3441    fn test_read_state_transitions() {
3442        unsafe {
3443            let reader = create_reader("<root/>");
3444            assert!(!reader.is_null());
3445            assert_eq!((*reader).ReadState(), ReadState::INITIALIZED);
3446
3447            // First read.
3448            assert_eq!(xmlTextReaderRead(reader), 1);
3449            assert_eq!((*reader).ReadState(), ReadState::READING);
3450            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
3451            assert_eq!((*reader).Depth(), 0);
3452
3453            // UPSTREAM-PARITY (oracle-verified 2.15.3): empty elements emit no
3454            // END_ELEMENT event — the second Read returns EOF directly.
3455            assert_eq!(xmlTextReaderRead(reader), 0);
3456            assert_eq!((*reader).ReadState(), ReadState::EOF);
3457
3458            free_reader(reader);
3459        }
3460    }
3461
3462    #[test]
3463    fn test_null_reader_returns_error() {
3464        unsafe {
3465            assert_eq!(xmlTextReaderRead(ptr::null_mut()), -1);
3466            assert_eq!(xmlTextReaderDepth(ptr::null_mut()), -1);
3467            assert_eq!(xmlTextReaderNodeType(ptr::null_mut()), -1);
3468            assert!(xmlTextReaderName(ptr::null_mut()).is_null());
3469            assert!(xmlTextReaderValue(ptr::null_mut()).is_null());
3470            assert_eq!(xmlTextReaderHasValue(ptr::null_mut()), 0);
3471            assert_eq!(xmlTextReaderIsEmptyElement(ptr::null_mut()), 0);
3472            assert_eq!(
3473                xmlTextReaderReadState(ptr::null_mut()),
3474                ReadState::ERROR as c_int
3475            );
3476        }
3477    }
3478
3479    #[test]
3480    fn test_xmlFreeTextReader_null() {
3481        unsafe {
3482            // Should not crash.
3483            xmlFreeTextReader(ptr::null_mut());
3484        }
3485    }
3486
3487    #[test]
3488    fn test_reader_name_and_value() {
3489        unsafe {
3490            let reader = create_reader("<root>hello</root>");
3491            assert!(!reader.is_null());
3492
3493            // Read root element.
3494            assert_eq!(xmlTextReaderRead(reader), 1);
3495            let name = xmlTextReaderName(reader);
3496            assert!(!name.is_null());
3497            assert_eq!(xmlstr_to_string(name), "root");
3498            xmlFree(name as *mut c_void);
3499
3500            assert_eq!(xmlTextReaderHasValue(reader), 0);
3501
3502            // Read text node.
3503            assert_eq!(xmlTextReaderRead(reader), 1);
3504            assert_eq!((*reader).NodeType(), ReaderNodeType::TEXT);
3505            assert_eq!((*reader).HasValue(), 1);
3506
3507            let val = xmlTextReaderValue(reader);
3508            assert!(!val.is_null());
3509            assert_eq!(xmlstr_to_string(val), "hello");
3510            xmlFree(val as *mut c_void);
3511
3512            free_reader(reader);
3513        }
3514    }
3515
3516    #[test]
3517    fn test_empty_element() {
3518        unsafe {
3519            let reader = create_reader("<empty/>");
3520            assert!(!reader.is_null());
3521
3522            assert_eq!(xmlTextReaderRead(reader), 1);
3523            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
3524            assert_eq!((*reader).IsEmptyElement(), 1);
3525            assert_eq!((*reader).HasAttributes(), 0);
3526            assert_eq!((*reader).AttributeCount(), 0);
3527
3528            // UPSTREAM-PARITY (oracle-verified 2.15.3): empty elements emit
3529            // NO END_ELEMENT event; the next Read returns EOF.
3530            assert_eq!(xmlTextReaderRead(reader), 0);
3531            assert_eq!((*reader).ReadState(), ReadState::EOF);
3532
3533            free_reader(reader);
3534        }
3535    }
3536
3537    #[test]
3538    fn test_element_with_attributes() {
3539        unsafe {
3540            let reader = create_reader(r#"<root a="1" b="2"/>"#);
3541            assert!(!reader.is_null());
3542
3543            assert_eq!(xmlTextReaderRead(reader), 1);
3544            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
3545            assert_eq!((*reader).HasAttributes(), 1);
3546
3547            // We know the attribute count if we've built the events properly.
3548            // The count_attributes checks the element's properties list.
3549            let attrs = xmlTextReaderAttributeCount(reader);
3550            assert_eq!(attrs, 2);
3551
3552            free_reader(reader);
3553        }
3554    }
3555
3556    #[test]
3557    fn test_attribute_navigation() {
3558        unsafe {
3559            let reader = create_reader(r#"<root a="1" b="2"></root>"#);
3560            assert!(!reader.is_null());
3561
3562            // Position on root element.
3563            assert_eq!(xmlTextReaderRead(reader), 1);
3564            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
3565
3566            // Move to first attribute.
3567            assert_eq!(xmlTextReaderMoveToFirstAttribute(reader), 1);
3568            assert_eq!((*reader).NodeType(), ReaderNodeType::ATTRIBUTE);
3569
3570            let name = xmlTextReaderConstName(reader);
3571            assert!(!name.is_null());
3572            assert_eq!(xmlstr_to_bytes(name), b"a");
3573
3574            let val = xmlTextReaderConstValue(reader);
3575            assert!(!val.is_null());
3576            assert_eq!(xmlstr_to_bytes(val), b"1");
3577
3578            // Move to next attribute.
3579            assert_eq!(xmlTextReaderMoveToNextAttribute(reader), 1);
3580            let name = xmlTextReaderConstName(reader);
3581            assert!(!name.is_null());
3582            assert_eq!(xmlstr_to_bytes(name), b"b");
3583            let val = xmlTextReaderConstValue(reader);
3584            assert!(!val.is_null());
3585            assert_eq!(xmlstr_to_bytes(val), b"2");
3586
3587            // No more attributes.
3588            assert_eq!(xmlTextReaderMoveToNextAttribute(reader), 0);
3589
3590            // Move back to element.
3591            assert_eq!(xmlTextReaderMoveToElement(reader), 1);
3592            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
3593
3594            // Move to attribute by name.
3595            assert_eq!(
3596                xmlTextReaderMoveToAttribute(reader, b"a\0" as *const u8 as *const xmlChar),
3597                1
3598            );
3599            assert_eq!((*reader).NodeType(), ReaderNodeType::ATTRIBUTE);
3600
3601            // Move to attribute by index.
3602            assert_eq!(xmlTextReaderMoveToElement(reader), 1);
3603            assert_eq!(xmlTextReaderMoveToAttributeNo(reader, 1), 1);
3604            assert_eq!((*reader).NodeType(), ReaderNodeType::ATTRIBUTE);
3605
3606            free_reader(reader);
3607        }
3608    }
3609
3610    #[test]
3611    fn test_get_attribute() {
3612        unsafe {
3613            let reader = create_reader(r#"<root a="hello" b="world"/>"#);
3614            assert!(!reader.is_null());
3615
3616            assert_eq!(xmlTextReaderRead(reader), 1);
3617
3618            // Get attribute by name.
3619            let val = xmlTextReaderGetAttribute(reader, b"a\0" as *const u8 as *const xmlChar);
3620            assert!(!val.is_null());
3621            assert_eq!(xmlstr_to_bytes(val), b"hello");
3622            xmlFree(val as *mut c_void);
3623
3624            let val = xmlTextReaderGetAttribute(reader, b"b\0" as *const u8 as *const xmlChar);
3625            assert!(!val.is_null());
3626            assert_eq!(xmlstr_to_bytes(val), b"world");
3627            xmlFree(val as *mut c_void);
3628
3629            // Non-existent attribute.
3630            let val = xmlTextReaderGetAttribute(reader, b"c\0" as *const u8 as *const xmlChar);
3631            assert!(val.is_null());
3632
3633            // Get attribute by index.
3634            let val = xmlTextReaderGetAttributeNo(reader, 0);
3635            assert!(!val.is_null());
3636            assert_eq!(xmlstr_to_bytes(val), b"hello");
3637            xmlFree(val as *mut c_void);
3638
3639            let val = xmlTextReaderGetAttributeNo(reader, 1);
3640            assert!(!val.is_null());
3641            assert_eq!(xmlstr_to_bytes(val), b"world");
3642            xmlFree(val as *mut c_void);
3643
3644            let val = xmlTextReaderGetAttributeNo(reader, 2);
3645            assert!(val.is_null());
3646
3647            free_reader(reader);
3648        }
3649    }
3650
3651    #[test]
3652    fn test_depth_tracking() {
3653        unsafe {
3654            let reader = create_reader("<a><b><c/></b></a>");
3655            assert!(!reader.is_null());
3656
3657            let nodes = collect_nodes(reader);
3658            // UPSTREAM-PARITY (oracle-verified 2.15.3): empty elements emit no
3659            // END_ELEMENT event.
3660            // ELEMENT a (0), ELEMENT b (1), ELEMENT c (2),
3661            // END_ELEMENT b (1), END_ELEMENT a (0)
3662            assert_eq!(nodes.len(), 5);
3663            assert_eq!(nodes[0].2, 0); // a depth 0
3664            assert_eq!(nodes[1].2, 1); // b depth 1
3665            assert_eq!(nodes[2].2, 2); // c depth 2
3666            assert_eq!(nodes[3].2, 1); // END b depth 1
3667            assert_eq!(nodes[4].2, 0); // END a depth 0
3668
3669            free_reader(reader);
3670        }
3671    }
3672
3673    #[test]
3674    fn test_multiple_siblings() {
3675        unsafe {
3676            let reader = create_reader("<root><a>A</a><b>B</b><c>C</c></root>");
3677            assert!(!reader.is_null());
3678
3679            let nodes = collect_nodes(reader);
3680            // ELEMENT root(0), ELEMENT a(1), TEXT(2), END a(1),
3681            // ELEMENT b(1), TEXT(2), END b(1),
3682            // ELEMENT c(1), TEXT(2), END c(1),
3683            // END root(0)
3684            assert_eq!(nodes.len(), 11);
3685
3686            // Check the sibling elements.
3687            assert_eq!(nodes[1], (ReaderNodeType::ELEMENT, "a".to_string(), 1));
3688            assert_eq!(nodes[4], (ReaderNodeType::ELEMENT, "b".to_string(), 1));
3689            assert_eq!(nodes[7], (ReaderNodeType::ELEMENT, "c".to_string(), 1));
3690
3691            free_reader(reader);
3692        }
3693    }
3694
3695    #[test]
3696    fn test_next_skip_to_sibling() {
3697        unsafe {
3698            let reader = create_reader("<root><a>A</a><b>B</b><c>C</c></root>");
3699            assert!(!reader.is_null());
3700
3701            // Read to first node (root element).
3702            assert_eq!(xmlTextReaderRead(reader), 1);
3703            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
3704
3705            // Read to a.
3706            assert_eq!(xmlTextReaderRead(reader), 1);
3707            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
3708            assert_eq!(xmlstr_to_string((*reader).name as *const xmlChar), "a");
3709
3710            // Read to text of a.
3711            assert_eq!(xmlTextReaderRead(reader), 1);
3712            assert_eq!((*reader).NodeType(), ReaderNodeType::TEXT);
3713
3714            // Skip to next sibling — should skip END a and go to ELEMENT b.
3715            assert_eq!(xmlTextReaderNext(reader), 1);
3716            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
3717            assert_eq!(xmlstr_to_string((*reader).name as *const xmlChar), "b");
3718
3719            // Next again — should go to c.
3720            assert_eq!(xmlTextReaderNext(reader), 1);
3721            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
3722            assert_eq!(xmlstr_to_string((*reader).name as *const xmlChar), "c");
3723
3724            // Next again — no more siblings.
3725            assert_eq!(xmlTextReaderNext(reader), 0);
3726
3727            free_reader(reader);
3728        }
3729    }
3730
3731    #[test]
3732    fn test_comment_and_pi_nodes() {
3733        unsafe {
3734            let xml = b"<?pi target?><root><!-- comment -->text</root>\0";
3735            let reader = xmlReaderForMemory(
3736                xml.as_ptr() as *const c_char,
3737                (xml.len() - 1) as c_int,
3738                ptr::null(),
3739                ptr::null(),
3740                0,
3741            );
3742            assert!(!reader.is_null());
3743
3744            let nodes = collect_nodes(reader);
3745            // PI, ELEMENT root, COMMENT, TEXT, END_ELEMENT root
3746            // Note: PI appears as PROCESSING_INSTRUCTION node.
3747            assert!(!nodes.is_empty(), "no nodes collected: {:?}", nodes);
3748
3749            // Check PI.
3750            assert_eq!(
3751                nodes[0].0,
3752                ReaderNodeType::PROCESSING_INSTRUCTION,
3753                "expected PI at nodes[0], got {:?} name={}",
3754                nodes[0].0,
3755                nodes[0].1
3756            );
3757            assert_eq!(
3758                nodes[0].0,
3759                ReaderNodeType::PROCESSING_INSTRUCTION,
3760                "expected PI at nodes[0], got {:?} name={}",
3761                nodes[0].0,
3762                nodes[0].1
3763            );
3764
3765            // Check root element.
3766            let root_idx = nodes
3767                .iter()
3768                .position(|(t, n, _)| *t == ReaderNodeType::ELEMENT && n == "root");
3769            assert!(
3770                root_idx.is_some(),
3771                "no ELEMENT root found in nodes: {:?}",
3772                nodes
3773                    .iter()
3774                    .map(|(t, n, _)| format!("{:?}:{}", t, n))
3775                    .collect::<Vec<_>>()
3776            );
3777
3778            // Check comment.
3779            let comment_idx = nodes
3780                .iter()
3781                .position(|(t, _, _)| *t == ReaderNodeType::COMMENT);
3782            assert!(comment_idx.is_some(), "no COMMENT found");
3783
3784            // Check text.
3785            let text_idx = nodes
3786                .iter()
3787                .position(|(t, _, _)| *t == ReaderNodeType::TEXT);
3788            assert!(text_idx.is_some(), "no TEXT found");
3789
3790            free_reader(reader);
3791        }
3792    }
3793
3794    #[test]
3795    fn test_local_name() {
3796        unsafe {
3797            // We need a namespace-aware element. For now, test without namespace.
3798            let reader = create_reader("<root/>");
3799            assert!(!reader.is_null());
3800
3801            assert_eq!(xmlTextReaderRead(reader), 1);
3802            let local = xmlTextReaderLocalName(reader);
3803            assert!(!local.is_null());
3804            assert_eq!(xmlstr_to_bytes(local), b"root");
3805            xmlFree(local as *mut c_void);
3806
3807            free_reader(reader);
3808        }
3809    }
3810
3811    #[test]
3812    fn test_base_uri() {
3813        unsafe {
3814            let reader = create_reader("<root/>");
3815            assert!(!reader.is_null());
3816
3817            assert_eq!(xmlTextReaderRead(reader), 1);
3818            // Base URI should be NULL for memory-created readers.
3819            let uri = xmlTextReaderBaseUri(reader);
3820            assert!(uri.is_null());
3821
3822            free_reader(reader);
3823        }
3824    }
3825
3826    #[test]
3827    fn test_lookup_namespace() {
3828        unsafe {
3829            let reader = create_reader(r#"<root xmlns:ns="http://example.com"><ns:child/></root>"#);
3830            assert!(!reader.is_null());
3831
3832            // Read to root.
3833            assert_eq!(xmlTextReaderRead(reader), 1);
3834            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
3835
3836            // Read to child (ns:child).
3837            assert_eq!(xmlTextReaderRead(reader), 1);
3838
3839            // Lookup the "ns" prefix.
3840            let uri = xmlTextReaderLookupNamespace(reader, b"ns\0" as *const u8 as *const xmlChar);
3841            assert!(!uri.is_null());
3842            assert_eq!(xmlstr_to_bytes(uri), b"http://example.com");
3843            xmlFree(uri as *mut c_void);
3844
3845            // Lookup default namespace (NULL prefix).
3846            let uri = xmlTextReaderLookupNamespace(reader, ptr::null());
3847            assert!(uri.is_null());
3848
3849            // Lookup non-existent prefix.
3850            let uri = xmlTextReaderLookupNamespace(
3851                reader,
3852                b"nonexistent\0" as *const u8 as *const xmlChar,
3853            );
3854            assert!(uri.is_null());
3855
3856            free_reader(reader);
3857        }
3858    }
3859
3860    #[test]
3861    fn test_parser_properties() {
3862        unsafe {
3863            let reader = create_reader("<root/>");
3864            assert!(!reader.is_null());
3865
3866            // Get default properties.
3867            assert_eq!(xmlTextReaderGetParserProp(reader, 1), 0); // LOADDTD
3868            assert_eq!(xmlTextReaderGetParserProp(reader, 2), 0); // DEFAULTATTRS
3869            assert_eq!(xmlTextReaderGetParserProp(reader, 3), 0); // VALIDATE
3870            assert_eq!(xmlTextReaderGetParserProp(reader, 4), 0); // SUBST_ENTITIES
3871
3872            // Set and verify.
3873            assert_eq!(xmlTextReaderSetParserProp(reader, 1, 1), 0);
3874            assert_eq!(xmlTextReaderGetParserProp(reader, 1), 1);
3875
3876            assert_eq!(xmlTextReaderSetParserProp(reader, 4, 1), 0);
3877            assert_eq!(xmlTextReaderGetParserProp(reader, 4), 1);
3878
3879            // Invalid property.
3880            assert_eq!(xmlTextReaderGetParserProp(reader, 99), -1);
3881            assert_eq!(xmlTextReaderSetParserProp(reader, 99, 1), -1);
3882
3883            free_reader(reader);
3884        }
3885    }
3886
3887    #[test]
3888    fn test_current_doc() {
3889        unsafe {
3890            let reader = create_reader("<root/>");
3891            assert!(!reader.is_null());
3892
3893            // Before reading, doc should be null.
3894            assert!((*reader).CurrentDoc().is_null());
3895
3896            // After reading, doc should be available.
3897            assert_eq!(xmlTextReaderRead(reader), 1);
3898            let doc = xmlTextReaderCurrentDoc(reader);
3899            assert!(!doc.is_null());
3900
3901            free_reader(reader);
3902        }
3903    }
3904
3905    #[test]
3906    fn test_free_reader_after_read() {
3907        unsafe {
3908            let reader = create_reader("<root><child/></root>");
3909            assert!(!reader.is_null());
3910
3911            // Read through the document.
3912            while xmlTextReaderRead(reader) > 0 {}
3913            assert_eq!((*reader).ReadState(), ReadState::EOF);
3914
3915            // Free should not crash.
3916            free_reader(reader);
3917        }
3918    }
3919
3920    #[test]
3921    fn test_reader_for_memory_null_buffer() {
3922        unsafe {
3923            let reader = xmlReaderForMemory(ptr::null(), 10, ptr::null(), ptr::null(), 0);
3924            assert!(reader.is_null());
3925        }
3926    }
3927
3928    #[test]
3929    fn test_reader_for_memory_empty_size() {
3930        unsafe {
3931            let data = b"<root/>";
3932            let reader = xmlReaderForMemory(
3933                data.as_ptr() as *const c_char,
3934                0,
3935                ptr::null(),
3936                ptr::null(),
3937                0,
3938            );
3939            assert!(reader.is_null());
3940        }
3941    }
3942
3943    #[test]
3944    fn test_reader_for_file_not_found() {
3945        unsafe {
3946            let filename = b"/nonexistent/file.xml\0" as *const u8 as *const c_char;
3947            let reader = xmlReaderForFile(filename, ptr::null(), 0);
3948            assert!(reader.is_null());
3949        }
3950    }
3951
3952    #[test]
3953    fn test_const_name_and_value() {
3954        unsafe {
3955            let reader = create_reader("<root>text</root>");
3956            assert!(!reader.is_null());
3957
3958            // Root element.
3959            assert_eq!(xmlTextReaderRead(reader), 1);
3960            let cname = xmlTextReaderConstName(reader);
3961            assert!(!cname.is_null());
3962            assert_eq!(xmlstr_to_bytes(cname), b"root");
3963
3964            // Text node.
3965            assert_eq!(xmlTextReaderRead(reader), 1);
3966            let cval = xmlTextReaderConstValue(reader);
3967            assert!(!cval.is_null());
3968            assert_eq!(xmlstr_to_bytes(cval), b"text");
3969
3970            free_reader(reader);
3971        }
3972    }
3973
3974    #[test]
3975    fn test_complex_nested_document() {
3976        unsafe {
3977            let xml = r#"<?xml version="1.0"?>
3978<library>
3979  <book id="1">
3980    <title>XML Fundamentals</title>
3981    <author>John Doe</author>
3982  </book>
3983  <book id="2">
3984    <title>XSLT Recipes</title>
3985    <author>Jane Smith</author>
3986  </book>
3987</library>"#;
3988
3989            let reader = create_reader(xml);
3990            assert!(!reader.is_null());
3991
3992            let mut element_count = 0;
3993            let mut end_element_count = 0;
3994            let mut text_count = 0;
3995            let mut pi_count = 0;
3996
3997            loop {
3998                let ret = xmlTextReaderRead(reader);
3999                if ret <= 0 {
4000                    break;
4001                }
4002                match (*reader).NodeType() {
4003                    ReaderNodeType::ELEMENT => element_count += 1,
4004                    ReaderNodeType::END_ELEMENT => end_element_count += 1,
4005                    ReaderNodeType::TEXT => text_count += 1,
4006                    ReaderNodeType::PROCESSING_INSTRUCTION => pi_count += 1,
4007                    _ => {}
4008                }
4009            }
4010
4011            // Elements: library, book(2), title(2), author(2) = 7
4012            assert_eq!(element_count, 7);
4013            // End elements: same count as elements
4014            assert_eq!(end_element_count, 7);
4015            // Text nodes: one per title and author = 4
4016            assert_eq!(text_count, 4);
4017            // UPSTREAM-PARITY: XML declaration (<?xml ...?>) is NOT stored as
4018            // a PI node in the tree. It is consumed by the parser and stored
4019            // in the document's version/encoding fields. Only <?pi ...?> nodes
4020            // (processing instructions) appear as XML_PI_NODE in the tree.
4021            assert_eq!(pi_count, 0);
4022
4023            free_reader(reader);
4024        }
4025    }
4026
4027    #[test]
4028    fn test_setup_reinitialize() {
4029        unsafe {
4030            let reader = create_reader("<root/>");
4031            assert!(!reader.is_null());
4032
4033            // Read through.
4034            assert_eq!(xmlTextReaderRead(reader), 1);
4035            assert_eq!((*reader).ReadState(), ReadState::READING);
4036
4037            // Setup with new input (simulate re-initialization).
4038            // For this test, we just verify the setup function exists and
4039            // handles a NULL input gracefully (resetting the reader).
4040            assert_eq!(
4041                xmlTextReaderSetup(reader, ptr::null_mut(), ptr::null(), ptr::null(), 0),
4042                0
4043            );
4044            assert_eq!((*reader).ReadState(), ReadState::INITIALIZED);
4045
4046            free_reader(reader);
4047        }
4048    }
4049
4050    #[test]
4051    fn test_has_attributes_on_non_element() {
4052        unsafe {
4053            let reader = create_reader("<root>text</root>");
4054            assert!(!reader.is_null());
4055
4056            // Position on text node.
4057            assert_eq!(xmlTextReaderRead(reader), 1); // root element
4058            assert_eq!((*reader).HasAttributes(), 0); // 0 attributes on root
4059            assert_eq!(xmlTextReaderRead(reader), 1); // text
4060            assert_eq!((*reader).HasAttributes(), 0);
4061
4062            free_reader(reader);
4063        }
4064    }
4065
4066    #[test]
4067    fn test_prev_sibling() {
4068        unsafe {
4069            let reader = create_reader("<root><a/><b/><c/></root>");
4070            assert!(!reader.is_null());
4071
4072            // Read through the document.
4073            while xmlTextReaderRead(reader) > 0 {
4074                // Skip to END_ELEMENT root or beyond.
4075            }
4076
4077            // Can't go prev after EOF.
4078            assert_eq!(xmlTextReaderPrev(reader), -1);
4079
4080            free_reader(reader);
4081        }
4082    }
4083
4084    #[test]
4085    fn test_move_to_attribute_no_not_on_element() {
4086        unsafe {
4087            let reader = create_reader("<root>text</root>");
4088            assert!(!reader.is_null());
4089
4090            assert_eq!(xmlTextReaderRead(reader), 1); // root
4091            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
4092
4093            // Move to non-existent attribute index.
4094            assert_eq!(xmlTextReaderMoveToAttributeNo(reader, 0), 0);
4095
4096            free_reader(reader);
4097        }
4098    }
4099
4100    #[test]
4101    fn test_get_attribute_ns() {
4102        unsafe {
4103            let reader = create_reader(r#"<root a="1" b="2"/>"#);
4104            assert!(!reader.is_null());
4105
4106            assert_eq!(xmlTextReaderRead(reader), 1);
4107
4108            // Get attribute by local name only (namespaceURI is NULL).
4109            let val = xmlTextReaderGetAttributeNs(
4110                reader,
4111                b"a\0" as *const u8 as *const xmlChar,
4112                ptr::null(),
4113            );
4114            assert!(!val.is_null());
4115            assert_eq!(xmlstr_to_bytes(val), b"1");
4116            xmlFree(val as *mut c_void);
4117
4118            free_reader(reader);
4119        }
4120    }
4121
4122    #[test]
4123    fn test_mixed_content() {
4124        unsafe {
4125            let reader = create_reader("<root>before<child/>after</root>");
4126            assert!(!reader.is_null());
4127
4128            let nodes = collect_nodes(reader);
4129            // UPSTREAM-PARITY (oracle-verified 2.15.3): empty elements emit no
4130            // END_ELEMENT event.
4131            // ELEMENT root(0), TEXT "before"(1), ELEMENT child(1),
4132            // TEXT "after"(1), END_ELEMENT root(0)
4133            assert_eq!(nodes.len(), 5);
4134            assert_eq!(nodes[0], (ReaderNodeType::ELEMENT, "root".to_string(), 0));
4135            assert_eq!(nodes[1].0, ReaderNodeType::TEXT);
4136            assert_eq!(nodes[2], (ReaderNodeType::ELEMENT, "child".to_string(), 1));
4137            assert_eq!(nodes[3].0, ReaderNodeType::TEXT);
4138
4139            free_reader(reader);
4140        }
4141    }
4142
4143    #[test]
4144    fn test_error_handling_invalid_xml() {
4145        unsafe {
4146            // Malformed XML.
4147            let data = b"<root><\0" as *const u8 as *const c_char;
4148            let reader = xmlReaderForMemory(data, 7, ptr::null(), ptr::null(), 0);
4149            assert!(!reader.is_null());
4150
4151            // Reading should fail.
4152            let ret = xmlTextReaderRead(reader);
4153            assert!(ret == -1 || ret == 0);
4154
4155            free_reader(reader);
4156        }
4157    }
4158
4159    #[test]
4160    fn test_reader_with_options() {
4161        unsafe {
4162            let data = b"<root/>\0" as *const u8 as *const c_char;
4163            let reader = xmlReaderForMemory(
4164                data,
4165                7,
4166                ptr::null(),
4167                ptr::null(),
4168                XML_PARSE_NOENT | XML_PARSE_DTDLOAD,
4169            );
4170            assert!(!reader.is_null());
4171
4172            // Verify options were set.
4173            assert_eq!((*reader).options & XML_PARSE_NOENT, XML_PARSE_NOENT);
4174            assert_eq!((*reader).options & XML_PARSE_DTDLOAD, XML_PARSE_DTDLOAD);
4175
4176            assert_eq!(xmlTextReaderRead(reader), 1);
4177            free_reader(reader);
4178        }
4179    }
4180
4181    #[test]
4182    fn test_reader_for_fd() {
4183        unsafe {
4184            // Create a temp file and test xmlReaderForFd.
4185            let tmp_path = "/tmp/libxml_rs_test_reader_fd.xml";
4186            let tmp_cstr = std::ffi::CString::new(tmp_path).unwrap();
4187            let content = b"<root><data/></root>";
4188            let fd = libc::open(
4189                tmp_cstr.as_ptr(),
4190                libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC,
4191                0o644,
4192            );
4193            assert!(fd >= 0);
4194            libc::write(fd, content.as_ptr() as *const c_void, content.len());
4195            libc::close(fd);
4196
4197            // Open for reading.
4198            let fd = libc::open(tmp_cstr.as_ptr(), libc::O_RDONLY, 0);
4199            assert!(fd >= 0);
4200
4201            let reader = xmlReaderForFd(fd, ptr::null(), ptr::null(), 0);
4202            assert!(!reader.is_null());
4203
4204            let nodes = collect_nodes(reader);
4205            // UPSTREAM-PARITY (oracle-verified 2.15.3): `<data/>` is empty, so
4206            // it contributes no END_ELEMENT: root, data, END root.
4207            assert_eq!(nodes.len(), 3);
4208
4209            free_reader(reader);
4210            libc::close(fd);
4211            std::fs::remove_file(tmp_path).ok();
4212        }
4213    }
4214
4215    #[test]
4216    fn test_reader_for_io() {
4217        unsafe {
4218            extern "C" fn io_read(context: *mut c_void, buffer: *mut c_char, len: c_int) -> c_int {
4219                if context.is_null() || buffer.is_null() || len <= 0 {
4220                    return -1;
4221                }
4222                // SAFETY: context points to an IoCtx struct.
4223                let ctx = unsafe { &mut *(context as *mut IoCtx) };
4224                if ctx.pos >= ctx.data.len() {
4225                    return 0;
4226                }
4227                let remaining = ctx.data.len() - ctx.pos;
4228                let to_copy = if (remaining as c_int) < len {
4229                    remaining
4230                } else {
4231                    len as usize
4232                };
4233                // SAFETY: buffer has at least `len` bytes of space.
4234                unsafe {
4235                    std::ptr::copy_nonoverlapping(
4236                        ctx.data.as_ptr().add(ctx.pos),
4237                        buffer as *mut u8,
4238                        to_copy,
4239                    );
4240                }
4241                ctx.pos += to_copy;
4242                to_copy as c_int
4243            }
4244
4245            extern "C" fn io_close(_context: *mut c_void) -> c_int {
4246                0
4247            }
4248
4249            struct IoCtx {
4250                data: &'static [u8],
4251                pos: usize,
4252            }
4253            let mut ctx = IoCtx {
4254                data: b"<root/>",
4255                pos: 0,
4256            };
4257
4258            let reader = xmlReaderForIO(
4259                Some(io_read),
4260                Some(io_close),
4261                &mut ctx as *mut IoCtx as *mut c_void,
4262                ptr::null(),
4263                ptr::null(),
4264                0,
4265            );
4266            assert!(!reader.is_null());
4267
4268            // Read through the document.
4269            assert_eq!(xmlTextReaderRead(reader), 1);
4270            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
4271            let cname = xmlTextReaderConstName(reader);
4272            assert!(!cname.is_null());
4273            assert_eq!(xmlstr_to_bytes(cname), b"root");
4274
4275            // UPSTREAM-PARITY (oracle-verified 2.15.3): `<root/>` is empty, so
4276            // there is no END_ELEMENT — the second Read returns EOF.
4277            assert_eq!(xmlTextReaderRead(reader), 0);
4278
4279            free_reader(reader);
4280        }
4281    }
4282}
4283
4284// ═══════════════════════════════════════════════════════════════════════════════
4285// 11.1-I reader closure — remaining xmlTextReader API (R-000136)
4286// ═══════════════════════════════════════════════════════════════════════════════
4287
4288/// Error severity (upstream `xmlParserSeverities`, reader.h).
4289pub const XML_PARSER_SEVERITY_VALIDITY_WARNING: c_int = 1;
4290pub const XML_PARSER_SEVERITY_VALIDITY_ERROR: c_int = 2;
4291pub const XML_PARSER_SEVERITY_WARNING: c_int = 3;
4292pub const XML_PARSER_SEVERITY_ERROR: c_int = 4;
4293
4294/// Opaque locator passed to the reader error handler (upstream
4295/// `xmlTextReaderLocator`).
4296#[repr(C)]
4297pub struct XmlTextReaderLocator {
4298    pub reader: *mut XmlTextReader,
4299}
4300
4301/// Reader error callback (upstream `xmlTextReaderErrorFunc`).
4302pub type xmlTextReaderErrorFunc = unsafe extern "C" fn(
4303    arg: *mut c_void,
4304    msg: *const c_char,
4305    severity: c_int,
4306    locator: *mut XmlTextReaderLocator,
4307);
4308
4309/// `xmlTextReaderPtr xmlReaderForDoc(const xmlChar *cur, const char *URL,
4310/// const char *encoding, int options)` — reader over an in-memory XML string.
4311///
4312/// # SAFETY
4313///
4314/// - `cur` must be a valid NUL-terminated XML document string.
4315#[no_mangle]
4316pub unsafe extern "C" fn xmlReaderForDoc(
4317    cur: *const xmlChar,
4318    URL: *const c_char,
4319    encoding: *const c_char,
4320    options: c_int,
4321) -> *mut XmlTextReader {
4322    if cur.is_null() {
4323        return ptr::null_mut();
4324    }
4325    let len = unsafe { libc::strlen(cur as *const libc::c_char) } as c_int;
4326    unsafe { xmlReaderForMemory(cur as *const c_char, len, URL, encoding, options) }
4327}
4328
4329/// `xmlTextReaderPtr xmlNewTextReaderFilename(const char *URI, const char *encoding, int options)`.
4330#[no_mangle]
4331pub unsafe extern "C" fn xmlNewTextReaderFilename(
4332    URI: *const c_char,
4333    encoding: *const c_char,
4334    options: c_int,
4335) -> *mut XmlTextReader {
4336    unsafe { xmlReaderForFile(URI, encoding, options) }
4337}
4338
4339/// Rebuild a reader in place (upstream `xmlReaderNew*` reuse contract).
4340///
4341/// Upstream reuses the caller's existing reader allocation, so a caller's
4342/// pointer remains valid across `xmlReaderNew*`. The candidate mirrors that by
4343/// moving the freshly built reader's contents into the caller's allocation and
4344/// releasing the temporary allocation without dropping the moved contents.
4345///
4346/// # SAFETY
4347///
4348/// - `reader` must be a valid, non-NULL reader pointer.
4349/// - `new_reader` must be a valid, non-NULL reader pointer distinct from `reader`.
4350unsafe fn reader_renew(reader: *mut XmlTextReader, new_reader: *mut XmlTextReader) {
4351    debug_assert!(!reader.is_null() && !new_reader.is_null() && reader != new_reader);
4352    unsafe {
4353        // Drop the old contents, then bitwise-move the new reader into the
4354        // caller's allocation. The temporary allocation is deallocated without
4355        // dropping (its contents now live at `reader`).
4356        core::ptr::drop_in_place(reader);
4357        core::ptr::copy_nonoverlapping(new_reader, reader, 1);
4358        let layout = std::alloc::Layout::new::<XmlTextReader>();
4359        std::alloc::dealloc(new_reader as *mut u8, layout);
4360    }
4361}
4362
4363/// `int xmlReaderNewDoc(xmlTextReaderPtr reader, const xmlChar *cur, const char *URL, const char *encoding, int options)`.
4364#[no_mangle]
4365pub unsafe extern "C" fn xmlReaderNewDoc(
4366    reader: *mut XmlTextReader,
4367    cur: *const xmlChar,
4368    URL: *const c_char,
4369    encoding: *const c_char,
4370    options: c_int,
4371) -> c_int {
4372    // UPSTREAM-PARITY: the New* family rejects a NULL reader before any work
4373    // (xmlreader.c: `if (reader == NULL) return (-1);`). It never allocates.
4374    if reader.is_null() || cur.is_null() {
4375        return -1;
4376    }
4377    let r = unsafe { xmlReaderForDoc(cur, URL, encoding, options) };
4378    if r.is_null() {
4379        return -1;
4380    }
4381    unsafe { reader_renew(reader, r) };
4382    0
4383}
4384
4385/// `int xmlReaderNewFile(xmlTextReaderPtr reader, const char *filename, const char *encoding, int options)`.
4386#[no_mangle]
4387pub unsafe extern "C" fn xmlReaderNewFile(
4388    reader: *mut XmlTextReader,
4389    filename: *const c_char,
4390    encoding: *const c_char,
4391    options: c_int,
4392) -> c_int {
4393    if reader.is_null() {
4394        return -1;
4395    }
4396    let r = unsafe { xmlReaderForFile(filename, encoding, options) };
4397    if r.is_null() {
4398        return -1;
4399    }
4400    unsafe { reader_renew(reader, r) };
4401    0
4402}
4403
4404/// `int xmlReaderNewMemory(xmlTextReaderPtr reader, const char *buffer, int size, const char *URL, const char *encoding, int options)`.
4405#[no_mangle]
4406pub unsafe extern "C" fn xmlReaderNewMemory(
4407    reader: *mut XmlTextReader,
4408    buffer: *const c_char,
4409    size: c_int,
4410    URL: *const c_char,
4411    encoding: *const c_char,
4412    options: c_int,
4413) -> c_int {
4414    if reader.is_null() || buffer.is_null() {
4415        return -1;
4416    }
4417    let r = unsafe { xmlReaderForMemory(buffer, size, URL, encoding, options) };
4418    if r.is_null() {
4419        return -1;
4420    }
4421    unsafe { reader_renew(reader, r) };
4422    0
4423}
4424
4425/// `int xmlReaderNewFd(xmlTextReaderPtr reader, int fd, const char *URL, const char *encoding, int options)`.
4426#[no_mangle]
4427pub unsafe extern "C" fn xmlReaderNewFd(
4428    reader: *mut XmlTextReader,
4429    fd: c_int,
4430    URL: *const c_char,
4431    encoding: *const c_char,
4432    options: c_int,
4433) -> c_int {
4434    if reader.is_null() {
4435        return -1;
4436    }
4437    let r = unsafe { xmlReaderForFd(fd, URL, encoding, options) };
4438    if r.is_null() {
4439        return -1;
4440    }
4441    unsafe { reader_renew(reader, r) };
4442    0
4443}
4444
4445/// `int xmlReaderNewIO(xmlTextReaderPtr reader, xmlInputReadCallback ioread, xmlInputCloseCallback ioclose, void *ioctx, const char *URL, const char *encoding, int options)`.
4446#[no_mangle]
4447pub unsafe extern "C" fn xmlReaderNewIO(
4448    reader: *mut XmlTextReader,
4449    ioread: Option<xmlInputReadCallback>,
4450    ioclose: Option<xmlInputCloseCallback>,
4451    ioctx: *mut c_void,
4452    URL: *const c_char,
4453    encoding: *const c_char,
4454    options: c_int,
4455) -> c_int {
4456    // UPSTREAM-PARITY: NULL reader or NULL read callback is rejected (-1).
4457    if reader.is_null() || ioread.is_none() {
4458        return -1;
4459    }
4460    let r = unsafe { xmlReaderForIO(ioread, ioclose, ioctx, URL, encoding, options) };
4461    if r.is_null() {
4462        return -1;
4463    }
4464    unsafe { reader_renew(reader, r) };
4465    0
4466}
4467
4468/// `xmlTextReaderPtr xmlReaderWalker(xmlDocPtr doc)` — reader walking an
4469/// existing document tree.
4470///
4471/// # SAFETY
4472///
4473/// - `doc` must be a valid document.
4474#[no_mangle]
4475pub unsafe extern "C" fn xmlReaderWalker(doc: *mut _xmlDoc) -> *mut XmlTextReader {
4476    if doc.is_null() {
4477        return ptr::null_mut();
4478    }
4479    let mut reader = XmlTextReader::new(ptr::null_mut(), None, None);
4480    reader.doc = doc;
4481    reader.parsed = true;
4482    reader.owns_doc = false; // walker borrows the caller's document
4483    reader.state = ReadState::READING;
4484    reader.build_events();
4485    Box::into_raw(Box::new(reader))
4486}
4487
4488/// `int xmlReaderNewWalker(xmlTextReaderPtr reader, xmlDocPtr doc)`.
4489#[no_mangle]
4490pub unsafe extern "C" fn xmlReaderNewWalker(
4491    reader: *mut XmlTextReader,
4492    doc: *mut _xmlDoc,
4493) -> c_int {
4494    // UPSTREAM-PARITY: NULL reader or NULL doc is rejected (-1).
4495    if reader.is_null() || doc.is_null() {
4496        return -1;
4497    }
4498    let r = unsafe { xmlReaderWalker(doc) };
4499    if r.is_null() {
4500        return -1;
4501    }
4502    unsafe { reader_renew(reader, r) };
4503    0
4504}
4505
4506/// `long xmlTextReaderByteConsumed(xmlTextReaderPtr reader)`.
4507///
4508/// Returns the total bytes consumed from the input (0 when unavailable —
4509/// the candidate parses the full input up front; documented divergence).
4510#[no_mangle]
4511pub unsafe extern "C" fn xmlTextReaderByteConsumed(reader: *mut XmlTextReader) -> c_long {
4512    if reader.is_null() {
4513        return -1;
4514    }
4515    0
4516}
4517
4518/// `const xmlChar *xmlTextReaderConstBaseUri(xmlTextReaderPtr reader)` — the
4519/// base URI, valid until the reader is freed (no copy).
4520#[no_mangle]
4521pub unsafe extern "C" fn xmlTextReaderConstBaseUri(reader: *mut XmlTextReader) -> *const xmlChar {
4522    if reader.is_null() {
4523        return ptr::null();
4524    }
4525    unsafe { (*reader).URL }
4526}
4527
4528/// `const xmlChar *xmlTextReaderConstEncoding(xmlTextReaderPtr reader)`.
4529#[no_mangle]
4530pub unsafe extern "C" fn xmlTextReaderConstEncoding(reader: *mut XmlTextReader) -> *const xmlChar {
4531    if reader.is_null() {
4532        return ptr::null();
4533    }
4534    let r = unsafe { &*reader };
4535    if !r.encoding.is_null() {
4536        return r.encoding;
4537    }
4538    if !r.doc.is_null() {
4539        return unsafe { (*r.doc).encoding };
4540    }
4541    ptr::null()
4542}
4543
4544/// `const xmlChar *xmlTextReaderConstLocalName(xmlTextReaderPtr reader)`.
4545///
4546/// UPSTREAM-PARITY: at an attribute position this is the attribute's local
4547/// name (or "xmlns"/the prefix for a namespace declaration); at an element
4548/// position the tree's local name.
4549#[no_mangle]
4550pub unsafe extern "C" fn xmlTextReaderConstLocalName(reader: *mut XmlTextReader) -> *const xmlChar {
4551    if reader.is_null() {
4552        return ptr::null();
4553    }
4554    let r = unsafe { &*reader };
4555    if r.cur_node.is_null() {
4556        return ptr::null();
4557    }
4558    // Attribute position: the attribute's local name (upstream node->name for
4559    // XML_ATTRIBUTE_NODE; "xmlns"/prefix for a namespace declaration).
4560    if r.cur_attribute >= 0 {
4561        let target = unsafe { r.attr_at(r.cur_node, r.cur_attribute) };
4562        return match target {
4563            AttrTarget::Ns(ns) => {
4564                if ns.is_null() {
4565                    ptr::null()
4566                } else if unsafe { (*ns).prefix }.is_null() {
4567                    b"xmlns\0".as_ptr() as *const xmlChar
4568                } else {
4569                    unsafe { (*ns).prefix }
4570                }
4571            }
4572            AttrTarget::Prop(p) => {
4573                if p.is_null() || unsafe { (*p).name }.is_null() {
4574                    ptr::null()
4575                } else {
4576                    unsafe { (*p).name }
4577                }
4578            }
4579            AttrTarget::None => ptr::null(),
4580        };
4581    }
4582    // Element position: the tree's local name (upstream node->name).
4583    let etype = unsafe { (*r.cur_node).type_ };
4584    if etype == XML_ELEMENT_NODE as c_int || etype == XML_ATTRIBUTE_NODE as c_int {
4585        unsafe { (*r.cur_node).name }
4586    } else {
4587        ptr::null()
4588    }
4589}
4590
4591/// `const xmlChar *xmlTextReaderConstNamespaceUri(xmlTextReaderPtr reader)`.
4592///
4593/// UPSTREAM-PARITY: at an attribute position the namespace comes from the
4594/// attribute (or namespace declaration) itself; elsewhere from the node.
4595#[no_mangle]
4596pub unsafe extern "C" fn xmlTextReaderConstNamespaceUri(
4597    reader: *mut XmlTextReader,
4598) -> *const xmlChar {
4599    if reader.is_null() {
4600        return ptr::null();
4601    }
4602    let r = unsafe { &*reader };
4603    if r.cur_node.is_null() {
4604        return ptr::null();
4605    }
4606    // Attribute position: resolve the current attribute's namespace.
4607    if r.cur_attribute >= 0 {
4608        let target = unsafe { r.attr_at(r.cur_node, r.cur_attribute) };
4609        return match target {
4610            AttrTarget::Ns(_ns) => {
4611                // UPSTREAM-PARITY (xmlTextReaderConstNamespaceUri): a
4612                // namespace declaration reports the xmlns namespace URI,
4613                // not the declared URI.
4614                b"http://www.w3.org/2000/xmlns/\0".as_ptr() as *const xmlChar
4615            }
4616            AttrTarget::Prop(p) => {
4617                if p.is_null() || unsafe { (*p).ns }.is_null() {
4618                    ptr::null()
4619                } else {
4620                    unsafe { (*(*p).ns).href }
4621                }
4622            }
4623            AttrTarget::None => ptr::null(),
4624        };
4625    }
4626    let ns = unsafe { (*r.cur_node).ns };
4627    if ns.is_null() || unsafe { (*ns).href }.is_null() {
4628        ptr::null()
4629    } else {
4630        unsafe { (*ns).href }
4631    }
4632}
4633
4634/// `const xmlChar *xmlTextReaderConstPrefix(xmlTextReaderPtr reader)`.
4635///
4636/// UPSTREAM-PARITY: at an attribute position the prefix comes from the
4637/// attribute; for a namespace declaration the prefix is reported as "xmlns"
4638/// (and NULL for the default declaration) — an upstream quirk reproduced here.
4639#[no_mangle]
4640pub unsafe extern "C" fn xmlTextReaderConstPrefix(reader: *mut XmlTextReader) -> *const xmlChar {
4641    if reader.is_null() {
4642        return ptr::null();
4643    }
4644    let r = unsafe { &*reader };
4645    if r.cur_node.is_null() {
4646        return ptr::null();
4647    }
4648    // Attribute position: resolve the current attribute's namespace.
4649    if r.cur_attribute >= 0 {
4650        let target = unsafe { r.attr_at(r.cur_node, r.cur_attribute) };
4651        return match target {
4652            AttrTarget::Ns(ns) => {
4653                if ns.is_null() || unsafe { (*ns).prefix }.is_null() {
4654                    ptr::null()
4655                } else {
4656                    b"xmlns\0".as_ptr() as *const xmlChar
4657                }
4658            }
4659            AttrTarget::Prop(p) => {
4660                if p.is_null() || unsafe { (*p).ns }.is_null() {
4661                    ptr::null()
4662                } else {
4663                    unsafe { (*(*p).ns).prefix }
4664                }
4665            }
4666            AttrTarget::None => ptr::null(),
4667        };
4668    }
4669    let ns = unsafe { (*r.cur_node).ns };
4670    if ns.is_null() || unsafe { (*ns).prefix }.is_null() {
4671        ptr::null()
4672    } else {
4673        unsafe { (*ns).prefix }
4674    }
4675}
4676
4677/// `const xmlChar *xmlTextReaderConstString(xmlTextReaderPtr reader, const xmlChar *str)`
4678/// — the reader's dictionary-internalized copy of `str`; the candidate
4679/// returns `str` unchanged (dictionary interning is an internal detail).
4680#[no_mangle]
4681pub unsafe extern "C" fn xmlTextReaderConstString(
4682    _reader: *mut XmlTextReader,
4683    str: *const xmlChar,
4684) -> *const xmlChar {
4685    str
4686}
4687
4688/// `const xmlChar *xmlTextReaderConstXmlLang(xmlTextReaderPtr reader)`.
4689#[no_mangle]
4690pub unsafe extern "C" fn xmlTextReaderConstXmlLang(reader: *mut XmlTextReader) -> *const xmlChar {
4691    if reader.is_null() {
4692        return ptr::null();
4693    }
4694    let r = unsafe { &*reader };
4695    let mut node = r.cur_node;
4696    while !node.is_null() {
4697        let mut prop = unsafe { (*node).properties };
4698        while !prop.is_null() {
4699            let p = unsafe { &*prop };
4700            if !p.name.is_null()
4701                && unsafe { *p.name } == b'x'
4702                && unsafe { *p.name.add(1) } == b'm'
4703                && unsafe { *p.name.add(2) } == b'l'
4704                && unsafe { *p.name.add(3) } == b':'
4705                && unsafe { *p.name.add(4) } == b'l'
4706                && unsafe { *p.name.add(5) } == b'a'
4707                && unsafe { *p.name.add(6) } == b'n'
4708                && unsafe { *p.name.add(7) } == b'g'
4709                && unsafe { *p.name.add(8) } == 0
4710            {
4711                if !p.children.is_null() {
4712                    let txt = p.children;
4713                    if unsafe { (*txt).type_ }
4714                        == crate::abi::types::xmlElementType::XML_TEXT_NODE as c_int
4715                    {
4716                        return unsafe { (*txt).content };
4717                    }
4718                }
4719                return ptr::null();
4720            }
4721            prop = p.next;
4722        }
4723        node = unsafe { (*node).parent };
4724    }
4725    ptr::null()
4726}
4727
4728/// `const xmlChar *xmlTextReaderConstXmlVersion(xmlTextReaderPtr reader)`.
4729#[no_mangle]
4730pub unsafe extern "C" fn xmlTextReaderConstXmlVersion(
4731    reader: *mut XmlTextReader,
4732) -> *const xmlChar {
4733    if reader.is_null() {
4734        return ptr::null();
4735    }
4736    let r = unsafe { &*reader };
4737    if r.doc.is_null() {
4738        return ptr::null();
4739    }
4740    unsafe { (*r.doc).version }
4741}
4742
4743/// `int xmlTextReaderQuoteChar(xmlTextReaderPtr reader)`.
4744///
4745/// UPSTREAM-PARITY: libxml2 2.13/2.15 returns `'"'` unconditionally for any
4746/// non-NULL reader (the implementation is a placeholder that does not inspect
4747/// the attribute; see the `/* TODO maybe lookup the attribute value */` comment
4748/// in xmlreader.c). The candidate reproduces that historical behavior exactly.
4749#[no_mangle]
4750pub unsafe extern "C" fn xmlTextReaderQuoteChar(reader: *mut XmlTextReader) -> c_int {
4751    if reader.is_null() {
4752        return -1;
4753    }
4754    b'"' as c_int
4755}
4756
4757/// `int xmlTextReaderIsDefault(xmlTextReaderPtr reader)` — whether the current
4758/// attribute came from the DTD default. The candidate returns 0 for a valid
4759/// reader (DTD default attribute expansion is not annotated; documented
4760/// divergence), -1 for a NULL reader (upstream contract).
4761#[no_mangle]
4762pub unsafe extern "C" fn xmlTextReaderIsDefault(reader: *mut XmlTextReader) -> c_int {
4763    if reader.is_null() {
4764        return -1;
4765    }
4766    0
4767}
4768
4769/// `int xmlTextReaderIsNamespaceDecl(xmlTextReaderPtr reader)` — whether the
4770/// current attribute position is a namespace declaration.
4771#[no_mangle]
4772pub unsafe extern "C" fn xmlTextReaderIsNamespaceDecl(reader: *mut XmlTextReader) -> c_int {
4773    if reader.is_null() {
4774        return -1;
4775    }
4776    let r = unsafe { &*reader };
4777    if r.cur_node.is_null() {
4778        return -1;
4779    }
4780    r.cur_attr_is_ns as c_int
4781}
4782
4783/// `int xmlTextReaderMoveToAttributeNs(xmlTextReaderPtr reader, const xmlChar *localName, const xmlChar *namespaceURI)`.
4784///
4785/// UPSTREAM-PARITY (xmlreader.c, 2.15): NULL reader/localName/namespaceURI
4786/// returns -1; a NULL `namespaceURI` is NOT treated as "no namespace" — the
4787/// caller must pass the actual URI. The `http://www.w3.org/2000/xmlns/`
4788/// namespace searches namespace declarations (matching the default `xmlns`
4789/// declaration or a prefix), everything else searches only namespace-qualified
4790/// properties (`prop->ns != NULL`).
4791///
4792/// # SAFETY
4793///
4794/// - `localName`/`namespaceURI` must be valid strings (non-NULL).
4795#[no_mangle]
4796pub unsafe extern "C" fn xmlTextReaderMoveToAttributeNs(
4797    reader: *mut XmlTextReader,
4798    localName: *const xmlChar,
4799    namespaceURI: *const xmlChar,
4800) -> c_int {
4801    if reader.is_null() || localName.is_null() || namespaceURI.is_null() {
4802        return -1;
4803    }
4804    let r = unsafe { &mut *reader };
4805    let node = r.cur_node;
4806    if node.is_null() {
4807        return -1;
4808    }
4809    if unsafe { (*node).type_ } != XML_ELEMENT_NODE as c_int {
4810        return 0;
4811    }
4812
4813    const XMLNS_URI: &[u8] = b"http://www.w3.org/2000/xmlns/\0";
4814    if libc::strcmp(
4815        namespaceURI as *const libc::c_char,
4816        XMLNS_URI.as_ptr() as *const libc::c_char,
4817    ) == 0
4818    {
4819        // Namespace-declaration search: localName "xmlns" addresses the
4820        // default declaration, any other localName is a prefix.
4821        let is_default = libc::strcmp(
4822            localName as *const libc::c_char,
4823            b"xmlns\0".as_ptr() as *const libc::c_char,
4824        ) == 0;
4825        let mut ns = unsafe { (*node).nsDef };
4826        let mut index = 0;
4827        while !ns.is_null() {
4828            let n = unsafe { &*ns };
4829            let prefix_match = if is_default {
4830                n.prefix.is_null()
4831            } else {
4832                !n.prefix.is_null()
4833                    && libc::strcmp(
4834                        n.prefix as *const libc::c_char,
4835                        localName as *const libc::c_char,
4836                    ) == 0
4837            };
4838            if prefix_match {
4839                r.cur_attribute = index;
4840                r.node_type = ReaderNodeType::ATTRIBUTE;
4841                r.cache_attribute_info(AttrTarget::Ns(ns));
4842                return 1;
4843            }
4844            index += 1;
4845            ns = unsafe { (*ns).next };
4846        }
4847        return 0;
4848    }
4849
4850    // Property search: only namespace-qualified attributes are matchable.
4851    let mut prop = unsafe { (*node).properties };
4852    let mut index = 0;
4853    let mut ns_count = 0;
4854    let mut ns = unsafe { (*node).nsDef };
4855    while !ns.is_null() {
4856        ns_count += 1;
4857        ns = unsafe { (*ns).next };
4858    }
4859    while !prop.is_null() {
4860        let p = unsafe { &*prop };
4861        if !p.name.is_null()
4862            && !p.ns.is_null()
4863            && !(*p.ns).href.is_null()
4864            && libc::strcmp(
4865                p.name as *const libc::c_char,
4866                localName as *const libc::c_char,
4867            ) == 0
4868            && libc::strcmp(
4869                (*p.ns).href as *const libc::c_char,
4870                namespaceURI as *const libc::c_char,
4871            ) == 0
4872        {
4873            r.cur_attribute = ns_count + index;
4874            r.node_type = ReaderNodeType::ATTRIBUTE;
4875            r.cache_attribute_info(AttrTarget::Prop(prop));
4876            return 1;
4877        }
4878        index += 1;
4879        prop = unsafe { (*prop).next };
4880    }
4881    0
4882}
4883
4884/// `xmlNodePtr xmlTextReaderPreserve(xmlTextReaderPtr reader)` — the current
4885/// node (the candidate's reader owns the whole tree, so no separate
4886/// preservation step is needed).
4887#[no_mangle]
4888pub unsafe extern "C" fn xmlTextReaderPreserve(reader: *mut XmlTextReader) -> *mut _xmlNode {
4889    if reader.is_null() {
4890        return ptr::null_mut();
4891    }
4892    unsafe { (*reader).cur_node }
4893}
4894
4895/// `int xmlTextReaderPreservePattern(xmlTextReaderPtr reader, const xmlChar *pattern, const xmlChar **namespaces)`.
4896///
4897/// The candidate preserves every node; returns 0 (documented divergence:
4898/// pattern-based selective preservation is not tracked).
4899#[no_mangle]
4900pub unsafe extern "C" fn xmlTextReaderPreservePattern(
4901    reader: *mut XmlTextReader,
4902    _pattern: *const xmlChar,
4903    _namespaces: *mut *const xmlChar,
4904) -> c_int {
4905    if reader.is_null() {
4906        return -1;
4907    }
4908    0
4909}
4910
4911/// `int xmlTextReaderSetErrorHandler(xmlTextReaderPtr reader, xmlTextReaderErrorFunc f, void *arg)`.
4912///
4913/// # SAFETY
4914///
4915/// - `f` must be a valid callback or NULL.
4916#[no_mangle]
4917pub unsafe extern "C" fn xmlTextReaderSetErrorHandler(
4918    reader: *mut XmlTextReader,
4919    f: Option<xmlTextReaderErrorFunc>,
4920    arg: *mut c_void,
4921) {
4922    if reader.is_null() {
4923        return;
4924    }
4925    unsafe {
4926        (*reader).error_handler = f;
4927        (*reader).error_arg = arg;
4928    }
4929}
4930
4931/// `void xmlTextReaderGetErrorHandler(xmlTextReaderPtr reader, xmlTextReaderErrorFunc *f, void **arg)`.
4932///
4933/// # SAFETY
4934///
4935/// - `f`/`arg` must be valid out-pointers or NULL.
4936#[no_mangle]
4937pub unsafe extern "C" fn xmlTextReaderGetErrorHandler(
4938    reader: *mut XmlTextReader,
4939    f: *mut Option<xmlTextReaderErrorFunc>,
4940    arg: *mut *mut c_void,
4941) {
4942    if reader.is_null() {
4943        return;
4944    }
4945    unsafe {
4946        if !f.is_null() {
4947            *f = (*reader).error_handler;
4948        }
4949        if !arg.is_null() {
4950            *arg = (*reader).error_arg;
4951        }
4952    }
4953}
4954
4955/// `void xmlTextReaderSetStructuredErrorHandler(xmlTextReaderPtr reader, xmlStructuredErrorFunc f, void *arg)`.
4956#[no_mangle]
4957pub unsafe extern "C" fn xmlTextReaderSetStructuredErrorHandler(
4958    reader: *mut XmlTextReader,
4959    f: Option<crate::abi::callbacks::xmlStructuredErrorFunc>,
4960    arg: *mut c_void,
4961) {
4962    if reader.is_null() {
4963        return;
4964    }
4965    unsafe {
4966        (*reader).structured_handler = f;
4967        (*reader).structured_arg = arg;
4968    }
4969}
4970
4971/// `const xmlError *xmlTextReaderGetLastError(xmlTextReaderPtr reader)` —
4972/// pointer to the reader's embedded `_xmlError` (upstream returns
4973/// `&reader->ctxt->lastError`, which is always present while the reader
4974/// exists; valid until the next error is collected).
4975#[no_mangle]
4976pub unsafe extern "C" fn xmlTextReaderGetLastError(
4977    reader: *mut XmlTextReader,
4978) -> *const crate::abi::structs::_xmlError {
4979    if reader.is_null() {
4980        return ptr::null();
4981    }
4982    let r = unsafe { &mut *reader };
4983    // Sync the embedded struct from the most recent collected error, if any.
4984    // With no errors the struct stays zeroed (message NULL) — matching the
4985    // oracle, which still returns a non-NULL pointer here.
4986    if let Some(msg) = r.errors.last() {
4987        unsafe {
4988            // Message is a fresh NUL-terminated xmlMalloc copy owned by the
4989            // reader (freed on replacement and on drop).
4990            let bytes = msg.as_bytes();
4991            let m = libc::malloc(bytes.len() + 1) as *mut xmlChar;
4992            if !m.is_null() {
4993                libc::memcpy(
4994                    m as *mut libc::c_void,
4995                    bytes.as_ptr() as *const libc::c_void,
4996                    bytes.len(),
4997                );
4998                *m.add(bytes.len()) = 0;
4999                if !r.last_err.message.is_null() {
5000                    libc::free(r.last_err.message as *mut libc::c_void);
5001                }
5002                (*reader).last_err.message = m as *mut c_char;
5003                (*reader).last_err.domain = crate::abi::types::XML_FROM_PARSER as c_int;
5004                (*reader).last_err.level = crate::abi::types::xmlErrorLevel::XML_ERR_ERROR as c_int;
5005                (*reader).last_err.code = crate::abi::types::XML_ERR_INTERNAL_ERROR as c_int;
5006            }
5007        }
5008    }
5009    &(*reader).last_err as *const crate::abi::structs::_xmlError
5010}
5011
5012/// `xmlChar *xmlTextReaderLocatorBaseURI(xmlTextReaderLocatorPtr locator)`.
5013///
5014/// # SAFETY
5015///
5016/// - `locator` must be valid or NULL.
5017#[no_mangle]
5018pub unsafe extern "C" fn xmlTextReaderLocatorBaseURI(
5019    locator: *mut XmlTextReaderLocator,
5020) -> *mut xmlChar {
5021    if locator.is_null() {
5022        return ptr::null_mut();
5023    }
5024    unsafe {
5025        let r = (*locator).reader;
5026        if r.is_null() {
5027            return ptr::null_mut();
5028        }
5029        xml_strdup((*r).URL)
5030    }
5031}
5032
5033/// `int xmlTextReaderLocatorLineNumber(xmlTextReaderLocatorPtr locator)`.
5034#[no_mangle]
5035pub unsafe extern "C" fn xmlTextReaderLocatorLineNumber(
5036    locator: *mut XmlTextReaderLocator,
5037) -> c_int {
5038    if locator.is_null() {
5039        return -1;
5040    }
5041    unsafe {
5042        let r = (*locator).reader;
5043        if r.is_null() {
5044            return -1;
5045        }
5046        let node = (*r).cur_node;
5047        if node.is_null() {
5048            return -1;
5049        }
5050        (*node).line as c_int
5051    }
5052}
5053
5054/// `xmlParserInputBufferPtr xmlTextReaderGetRemainder(xmlTextReaderPtr reader)`.
5055///
5056/// Returns NULL — the candidate reads the whole input up front (documented
5057/// divergence: no unconsumed input remains).
5058#[no_mangle]
5059pub unsafe extern "C" fn xmlTextReaderGetRemainder(
5060    _reader: *mut XmlTextReader,
5061) -> *mut crate::abi::structs::_xmlParserInputBuffer {
5062    ptr::null_mut()
5063}
5064
5065/// `void xmlTextReaderSetMaxAmplification(xmlTextReaderPtr reader, unsigned maxAmpl)`.
5066#[no_mangle]
5067pub unsafe extern "C" fn xmlTextReaderSetMaxAmplification(
5068    reader: *mut XmlTextReader,
5069    maxAmpl: c_uint,
5070) {
5071    if reader.is_null() {
5072        return;
5073    }
5074    unsafe { (*reader).max_amplification = maxAmpl as c_int };
5075}
5076
5077/// `int xmlTextReaderSchemaValidate(xmlTextReaderPtr reader, const char *xsd)` —
5078/// parse `xsd` and validate the reader's document.
5079///
5080/// # SAFETY
5081///
5082/// - `xsd` must be a valid path or NULL.
5083#[no_mangle]
5084pub unsafe extern "C" fn xmlTextReaderSchemaValidate(
5085    reader: *mut XmlTextReader,
5086    xsd: *const c_char,
5087) -> c_int {
5088    if reader.is_null() || xsd.is_null() {
5089        return -1;
5090    }
5091    // Ensure the document is parsed.
5092    if unsafe { (*reader).doc }.is_null() {
5093        if unsafe { (*reader).parsed } == false {
5094            unsafe { (*reader).Read() };
5095        }
5096    }
5097    let ctxt = crate::xml::schemas::xmlSchemaNewParserCtxt(xsd);
5098    if ctxt.is_null() {
5099        return -1;
5100    }
5101    let schema = crate::xml::schemas::xmlSchemaParse(ctxt);
5102    if schema.is_null() {
5103        crate::xml::schemas::xmlSchemaFreeParserCtxt(ctxt);
5104        return -1;
5105    }
5106    let vctxt = crate::xml::schemas::xmlSchemaNewValidCtxt(schema);
5107    if vctxt.is_null() {
5108        crate::xml::schemas::xmlSchemaFree(schema);
5109        crate::xml::schemas::xmlSchemaFreeParserCtxt(ctxt);
5110        return -1;
5111    }
5112    let ret = crate::xml::schemas::xmlSchemaValidateDoc(vctxt, unsafe { (*reader).doc });
5113    crate::xml::schemas::xmlSchemaFreeValidCtxt(vctxt);
5114    crate::xml::schemas::xmlSchemaFree(schema);
5115    crate::xml::schemas::xmlSchemaFreeParserCtxt(ctxt);
5116    ret
5117}
5118
5119/// `int xmlTextReaderSchemaValidateCtxt(xmlTextReaderPtr reader, xmlSchemaValidCtxtPtr ctxt, int options)`.
5120#[no_mangle]
5121pub unsafe extern "C" fn xmlTextReaderSchemaValidateCtxt(
5122    reader: *mut XmlTextReader,
5123    ctxt: *mut c_void,
5124    _options: c_int,
5125) -> c_int {
5126    if reader.is_null() || ctxt.is_null() {
5127        return -1;
5128    }
5129    if unsafe { (*reader).doc }.is_null() {
5130        if unsafe { (*reader).parsed } == false {
5131            unsafe { (*reader).Read() };
5132        }
5133    }
5134    crate::xml::schemas::xmlSchemaValidateDoc(ctxt, unsafe { (*reader).doc })
5135}
5136
5137/// `int xmlTextReaderSetSchema(xmlTextReaderPtr reader, xmlSchemaPtr schema)`.
5138#[no_mangle]
5139pub unsafe extern "C" fn xmlTextReaderSetSchema(
5140    reader: *mut XmlTextReader,
5141    schema: *mut c_void,
5142) -> c_int {
5143    if reader.is_null() {
5144        return -1;
5145    }
5146    unsafe {
5147        (*reader).schema = schema;
5148    }
5149    0
5150}
5151
5152/// `int xmlTextReaderRelaxNGValidate(xmlTextReaderPtr reader, const char *rng)`.
5153#[no_mangle]
5154pub unsafe extern "C" fn xmlTextReaderRelaxNGValidate(
5155    reader: *mut XmlTextReader,
5156    rng: *const c_char,
5157) -> c_int {
5158    if reader.is_null() || rng.is_null() {
5159        return -1;
5160    }
5161    if unsafe { (*reader).doc }.is_null() {
5162        if unsafe { (*reader).parsed } == false {
5163            unsafe { (*reader).Read() };
5164        }
5165    }
5166    let ctxt = crate::xml::relaxng::xmlRelaxNGNewParserCtxt(rng);
5167    if ctxt.is_null() {
5168        return -1;
5169    }
5170    let schema = crate::xml::relaxng::xmlRelaxNGParse(ctxt);
5171    if schema.is_null() {
5172        crate::xml::relaxng::xmlRelaxNGFreeParserCtxt(ctxt);
5173        return -1;
5174    }
5175    let vctxt = crate::xml::relaxng::xmlRelaxNGNewValidCtxt(schema);
5176    if vctxt.is_null() {
5177        crate::xml::relaxng::xmlRelaxNGFree(schema);
5178        crate::xml::relaxng::xmlRelaxNGFreeParserCtxt(ctxt);
5179        return -1;
5180    }
5181    let ret = crate::xml::relaxng::xmlRelaxNGValidateDoc(vctxt, unsafe { (*reader).doc });
5182    crate::xml::relaxng::xmlRelaxNGFreeValidCtxt(vctxt);
5183    crate::xml::relaxng::xmlRelaxNGFree(schema);
5184    crate::xml::relaxng::xmlRelaxNGFreeParserCtxt(ctxt);
5185    ret
5186}
5187
5188/// `int xmlTextReaderRelaxNGValidateCtxt(xmlTextReaderPtr reader, xmlRelaxNGValidCtxtPtr ctxt, int options)`.
5189#[no_mangle]
5190pub unsafe extern "C" fn xmlTextReaderRelaxNGValidateCtxt(
5191    reader: *mut XmlTextReader,
5192    ctxt: *mut c_void,
5193    _options: c_int,
5194) -> c_int {
5195    if reader.is_null() || ctxt.is_null() {
5196        return -1;
5197    }
5198    if unsafe { (*reader).doc }.is_null() {
5199        if unsafe { (*reader).parsed } == false {
5200            unsafe { (*reader).Read() };
5201        }
5202    }
5203    crate::xml::relaxng::xmlRelaxNGValidateDoc(ctxt, unsafe { (*reader).doc })
5204}
5205
5206/// `int xmlTextReaderRelaxNGSetSchema(xmlTextReaderPtr reader, xmlRelaxNGPtr schema)`.
5207#[no_mangle]
5208pub unsafe extern "C" fn xmlTextReaderRelaxNGSetSchema(
5209    reader: *mut XmlTextReader,
5210    schema: *mut c_void,
5211) -> c_int {
5212    if reader.is_null() {
5213        return -1;
5214    }
5215    unsafe {
5216        (*reader).rng = schema;
5217    }
5218    0
5219}