Skip to main content

libxml_rs/xml/reader/
mod.rs

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