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