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