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