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 =
2640        match unsafe { crate::abi::exports_parser::open_filename_routed_input_only(filename) } {
2641            crate::abi::exports_parser::RoutedFileOpen::Loaded(input) => input,
2642            crate::abi::exports_parser::RoutedFileOpen::Failed
2643            | crate::abi::exports_parser::RoutedFileOpen::EntityLoaderFailed => {
2644                // SAFETY: ctxt is valid.
2645                unsafe { free_parser_ctxt(ctxt) };
2646                return ptr::null_mut();
2647            }
2648            crate::abi::exports_parser::RoutedFileOpen::Builtin => {
2649                match unsafe { input_from_file(filename) } {
2650                    Ok(input) => input,
2651                    Err(_) => {
2652                        // SAFETY: ctxt is valid.
2653                        unsafe { free_parser_ctxt(ctxt) };
2654                        return ptr::null_mut();
2655                    }
2656                }
2657            }
2658        };
2659
2660    // SAFETY: ctxt and input are valid.
2661    unsafe { setup_parser_input(ctxt, input) };
2662    unsafe {
2663        (*ctxt).options = options;
2664    }
2665
2666    let enc_bytes = if encoding.is_null() {
2667        None
2668    } else {
2669        // SAFETY: encoding is a valid C string.
2670        unsafe {
2671            let cstr = std::ffi::CStr::from_ptr(encoding);
2672            Some(cstr.to_bytes().to_vec())
2673        }
2674    };
2675
2676    let mut reader = XmlTextReader::new(ctxt, None, enc_bytes.as_deref());
2677    reader.options = options;
2678    Box::into_raw(Box::new(reader))
2679}
2680
2681/// Create a text reader from memory.
2682///
2683/// # UPSTREAM-PARITY
2684///
2685/// ```c
2686/// xmlTextReaderPtr xmlReaderForMemory(const char *buffer, int size,
2687///                                     const char *URL, const char *encoding, int options);
2688/// ```
2689///
2690/// # Safety
2691///
2692/// - `buffer` must be a valid pointer with at least `size` readable bytes.
2693/// - `URL` and `encoding` must be valid C strings or NULL.
2694#[no_mangle]
2695pub unsafe extern "C" fn xmlReaderForMemory(
2696    buffer: *const c_char,
2697    size: c_int,
2698    URL: *const c_char,
2699    encoding: *const c_char,
2700    options: c_int,
2701) -> *mut XmlTextReader {
2702    if buffer.is_null() || size <= 0 {
2703        return ptr::null_mut();
2704    }
2705
2706    // SAFETY: create_parser_ctxt returns a valid context or NULL.
2707    let ctxt = unsafe { create_parser_ctxt() };
2708    if ctxt.is_null() {
2709        return ptr::null_mut();
2710    }
2711
2712    // SAFETY: input_from_memory copies the data; buffer and size are valid.
2713    // UPSTREAM-PARITY: the URL is recorded as the input's filename.
2714    let input = unsafe { input_from_memory_named(buffer, size, URL) };
2715
2716    // SAFETY: ctxt and input are valid.
2717    unsafe { setup_parser_input(ctxt, input) };
2718    unsafe {
2719        (*ctxt).options = options;
2720    }
2721
2722    let url_bytes = if URL.is_null() {
2723        None
2724    } else {
2725        // SAFETY: URL is a valid C string.
2726        unsafe {
2727            let cstr = std::ffi::CStr::from_ptr(URL);
2728            Some(cstr.to_bytes().to_vec())
2729        }
2730    };
2731
2732    let enc_bytes = if encoding.is_null() {
2733        None
2734    } else {
2735        // SAFETY: encoding is a valid C string.
2736        unsafe {
2737            let cstr = std::ffi::CStr::from_ptr(encoding);
2738            Some(cstr.to_bytes().to_vec())
2739        }
2740    };
2741
2742    let mut reader = XmlTextReader::new(ctxt, url_bytes.as_deref(), enc_bytes.as_deref());
2743    reader.options = options;
2744    Box::into_raw(Box::new(reader))
2745}
2746
2747/// Create a text reader from a file descriptor.
2748///
2749/// # UPSTREAM-PARITY
2750///
2751/// ```c
2752/// xmlTextReaderPtr xmlReaderForFd(int fd, const char *URL,
2753///                                 const char *encoding, int options);
2754/// ```
2755///
2756/// # Safety
2757///
2758/// - `fd` must be a valid open file descriptor.
2759/// - `URL` and `encoding` must be valid C strings or NULL.
2760#[no_mangle]
2761pub unsafe extern "C" fn xmlReaderForFd(
2762    fd: c_int,
2763    URL: *const c_char,
2764    encoding: *const c_char,
2765    options: c_int,
2766) -> *mut XmlTextReader {
2767    // SAFETY: create_parser_ctxt returns a valid context or NULL.
2768    let ctxt = unsafe { create_parser_ctxt() };
2769    if ctxt.is_null() {
2770        return ptr::null_mut();
2771    }
2772
2773    // Read all data from the fd.
2774    let mut buf = Vec::new();
2775    let mut tmp = [0u8; 4096];
2776    loop {
2777        // SAFETY: fd must be a valid open file descriptor.
2778        let n = unsafe { libc::read(fd, tmp.as_mut_ptr() as *mut c_void, tmp.len()) };
2779        if n <= 0 {
2780            break;
2781        }
2782        buf.extend_from_slice(&tmp[..n as usize]);
2783    }
2784
2785    // SAFETY: input_from_memory copies the buffer contents.
2786    let input = unsafe { input_from_memory(buf.as_ptr() as *const c_char, buf.len() as c_int) };
2787
2788    // SAFETY: ctxt and input are valid.
2789    unsafe { setup_parser_input(ctxt, input) };
2790    unsafe {
2791        (*ctxt).options = options;
2792    }
2793
2794    let url_bytes = if URL.is_null() {
2795        None
2796    } else {
2797        // SAFETY: URL is a valid C string.
2798        unsafe {
2799            let cstr = std::ffi::CStr::from_ptr(URL);
2800            Some(cstr.to_bytes().to_vec())
2801        }
2802    };
2803
2804    let enc_bytes = if encoding.is_null() {
2805        None
2806    } else {
2807        // SAFETY: encoding is a valid C string.
2808        unsafe {
2809            let cstr = std::ffi::CStr::from_ptr(encoding);
2810            Some(cstr.to_bytes().to_vec())
2811        }
2812    };
2813
2814    let mut reader = XmlTextReader::new(ctxt, url_bytes.as_deref(), enc_bytes.as_deref());
2815    reader.options = options;
2816    Box::into_raw(Box::new(reader))
2817}
2818
2819/// Create a text reader from I/O callbacks.
2820///
2821/// # UPSTREAM-PARITY
2822///
2823/// ```c
2824/// xmlTextReaderPtr xmlReaderForIO(xmlInputReadCallback ioread, xmlInputCloseCallback ioclose,
2825///                                 void *ioctx, const char *URL,
2826///                                 const char *encoding, int options);
2827/// ```
2828///
2829/// # Safety
2830///
2831/// - `ioread` and `ioclose` must be valid function pointers or None.
2832/// - `ioctx` must be a valid context pointer for the callbacks.
2833/// - `URL` and `encoding` must be valid C strings or NULL.
2834#[no_mangle]
2835pub unsafe extern "C" fn xmlReaderForIO(
2836    ioread: Option<xmlInputReadCallback>,
2837    ioclose: Option<xmlInputCloseCallback>,
2838    ioctx: *mut c_void,
2839    URL: *const c_char,
2840    encoding: *const c_char,
2841    options: c_int,
2842) -> *mut XmlTextReader {
2843    // SAFETY: create_parser_ctxt returns a valid context or NULL.
2844    let ctxt = unsafe { create_parser_ctxt() };
2845    if ctxt.is_null() {
2846        return ptr::null_mut();
2847    }
2848
2849    // SAFETY: input_from_io reads all data via callbacks.
2850    let input = unsafe { input_from_io(ioread, ioclose, ioctx) };
2851
2852    // SAFETY: ctxt and input are valid.
2853    unsafe { setup_parser_input(ctxt, input) };
2854    unsafe {
2855        (*ctxt).options = options;
2856    }
2857
2858    let url_bytes = if URL.is_null() {
2859        None
2860    } else {
2861        // SAFETY: URL is a valid C string.
2862        unsafe {
2863            let cstr = std::ffi::CStr::from_ptr(URL);
2864            Some(cstr.to_bytes().to_vec())
2865        }
2866    };
2867
2868    let enc_bytes = if encoding.is_null() {
2869        None
2870    } else {
2871        // SAFETY: encoding is a valid C string.
2872        unsafe {
2873            let cstr = std::ffi::CStr::from_ptr(encoding);
2874            Some(cstr.to_bytes().to_vec())
2875        }
2876    };
2877
2878    let mut reader = XmlTextReader::new(ctxt, url_bytes.as_deref(), enc_bytes.as_deref());
2879    reader.options = options;
2880    Box::into_raw(Box::new(reader))
2881}
2882
2883// ─────────────────────────────────────────────────────────────────────────────
2884// Navigation functions
2885// ─────────────────────────────────────────────────────────────────────────────
2886
2887/// Advance the reader to the next node in document order.
2888///
2889/// Returns 1 on success, 0 if EOF, -1 on error.
2890///
2891/// # UPSTREAM-PARITY
2892///
2893/// ```c
2894/// int xmlTextReaderRead(xmlTextReaderPtr reader);
2895/// ```
2896///
2897/// # Safety
2898///
2899/// `reader` must be a valid pointer returned by `xmlNewTextReader` or one of the
2900/// `xmlReaderFor*` functions, or NULL (in which case -1 is returned).
2901#[no_mangle]
2902pub unsafe extern "C" fn xmlTextReaderRead(reader: *mut XmlTextReader) -> c_int {
2903    if reader.is_null() {
2904        return -1;
2905    }
2906    // SAFETY: reader is valid.
2907    unsafe { (*reader).Read() }
2908}
2909
2910/// Skip to the next sibling of the current node.
2911///
2912/// Returns 1 on success, 0 if no more siblings, -1 on error.
2913///
2914/// # UPSTREAM-PARITY
2915///
2916/// ```c
2917/// int xmlTextReaderNext(xmlTextReaderPtr reader);
2918/// ```
2919///
2920/// # Safety
2921///
2922/// `reader` must be a valid pointer or NULL.
2923#[no_mangle]
2924pub unsafe extern "C" fn xmlTextReaderNext(reader: *mut XmlTextReader) -> c_int {
2925    if reader.is_null() {
2926        return -1;
2927    }
2928    // SAFETY: reader is valid.
2929    unsafe { (*reader).Next() }
2930}
2931
2932/// Skip to the next sibling (same as xmlTextReaderNext).
2933///
2934/// # UPSTREAM-PARITY
2935///
2936/// ```c
2937/// int xmlTextReaderNextSibling(xmlTextReaderPtr reader);
2938/// ```
2939///
2940/// # Safety
2941///
2942/// `reader` must be a valid pointer or NULL.
2943#[no_mangle]
2944pub unsafe extern "C" fn xmlTextReaderNextSibling(reader: *mut XmlTextReader) -> c_int {
2945    if reader.is_null() {
2946        return -1;
2947    }
2948    // SAFETY: reader is valid.
2949    unsafe { (*reader).Next() }
2950}
2951
2952/// Skip to the previous sibling of the current node.
2953///
2954/// Returns 1 on success, 0 if no previous sibling, -1 on error.
2955///
2956/// # UPSTREAM-PARITY
2957///
2958/// ```c
2959/// int xmlTextReaderPrev(xmlTextReaderPtr reader);
2960/// ```
2961///
2962/// # Safety
2963///
2964/// `reader` must be a valid pointer or NULL.
2965#[no_mangle]
2966pub unsafe extern "C" fn xmlTextReaderPrev(reader: *mut XmlTextReader) -> c_int {
2967    if reader.is_null() {
2968        return -1;
2969    }
2970    // SAFETY: reader is valid.
2971    unsafe { (*reader).Prev() }
2972}
2973
2974/// Move the reader back to the parent element (from an attribute).
2975///
2976/// Returns 1 on success, 0 if not on an attribute, -1 on error.
2977///
2978/// # UPSTREAM-PARITY
2979///
2980/// ```c
2981/// int xmlTextReaderMoveToElement(xmlTextReaderPtr reader);
2982/// ```
2983///
2984/// # Safety
2985///
2986/// `reader` must be a valid pointer or NULL.
2987#[no_mangle]
2988pub unsafe extern "C" fn xmlTextReaderMoveToElement(reader: *mut XmlTextReader) -> c_int {
2989    if reader.is_null() {
2990        return -1;
2991    }
2992    // SAFETY: reader is valid.
2993    unsafe { (*reader).MoveToElement() }
2994}
2995
2996/// Move to an attribute by name.
2997///
2998/// Returns 1 on success, 0 if not found, -1 on error.
2999///
3000/// # UPSTREAM-PARITY
3001///
3002/// ```c
3003/// int xmlTextReaderMoveToAttribute(xmlTextReaderPtr reader, const xmlChar *name);
3004/// ```
3005///
3006/// # Safety
3007///
3008/// `reader` must be a valid pointer or NULL. `name` must be a valid C string or NULL.
3009#[no_mangle]
3010pub unsafe extern "C" fn xmlTextReaderMoveToAttribute(
3011    reader: *mut XmlTextReader,
3012    name: *const xmlChar,
3013) -> c_int {
3014    if reader.is_null() || name.is_null() {
3015        return -1;
3016    }
3017    // SAFETY: reader and name are valid.
3018    unsafe { (*reader).MoveToAttribute(name) }
3019}
3020
3021/// Move to an attribute by index.
3022///
3023/// Returns 1 on success, 0 if not found, -1 on error.
3024///
3025/// # UPSTREAM-PARITY
3026///
3027/// ```c
3028/// int xmlTextReaderMoveToAttributeNo(xmlTextReaderPtr reader, int index);
3029/// ```
3030///
3031/// # Safety
3032///
3033/// `reader` must be a valid pointer or NULL.
3034#[no_mangle]
3035pub unsafe extern "C" fn xmlTextReaderMoveToAttributeNo(
3036    reader: *mut XmlTextReader,
3037    index: c_int,
3038) -> c_int {
3039    if reader.is_null() {
3040        return -1;
3041    }
3042    // SAFETY: reader is valid.
3043    unsafe { (*reader).MoveToAttributeNo(index) }
3044}
3045
3046/// Move to the first attribute of the current element.
3047///
3048/// Returns 1 on success, 0 if no attributes, -1 on error.
3049///
3050/// # UPSTREAM-PARITY
3051///
3052/// ```c
3053/// int xmlTextReaderMoveToFirstAttribute(xmlTextReaderPtr reader);
3054/// ```
3055///
3056/// # Safety
3057///
3058/// `reader` must be a valid pointer or NULL.
3059#[no_mangle]
3060pub unsafe extern "C" fn xmlTextReaderMoveToFirstAttribute(reader: *mut XmlTextReader) -> c_int {
3061    if reader.is_null() {
3062        return -1;
3063    }
3064    // SAFETY: reader is valid.
3065    unsafe { (*reader).MoveToFirstAttribute() }
3066}
3067
3068/// Move to the next attribute.
3069///
3070/// Returns 1 on success, 0 if no more attributes, -1 on error.
3071///
3072/// # UPSTREAM-PARITY
3073///
3074/// ```c
3075/// int xmlTextReaderMoveToNextAttribute(xmlTextReaderPtr reader);
3076/// ```
3077///
3078/// # Safety
3079///
3080/// `reader` must be a valid pointer or NULL.
3081#[no_mangle]
3082pub unsafe extern "C" fn xmlTextReaderMoveToNextAttribute(reader: *mut XmlTextReader) -> c_int {
3083    if reader.is_null() {
3084        return -1;
3085    }
3086    // SAFETY: reader is valid.
3087    unsafe { (*reader).MoveToNextAttribute() }
3088}
3089
3090// ─────────────────────────────────────────────────────────────────────────────
3091// Information methods
3092// ─────────────────────────────────────────────────────────────────────────────
3093
3094/// Get the attribute count of the current element.
3095///
3096/// Returns the number of attributes, or -1 if not on an element.
3097///
3098/// # UPSTREAM-PARITY
3099///
3100/// ```c
3101/// int xmlTextReaderAttributeCount(xmlTextReaderPtr reader);
3102/// ```
3103///
3104/// # Safety
3105///
3106/// `reader` must be a valid pointer or NULL.
3107#[no_mangle]
3108pub unsafe extern "C" fn xmlTextReaderAttributeCount(reader: *mut XmlTextReader) -> c_int {
3109    if reader.is_null() {
3110        return -1;
3111    }
3112    // SAFETY: reader is valid.
3113    unsafe { (*reader).AttributeCount() }
3114}
3115
3116/// Get the depth of the current node.
3117///
3118/// Returns the depth (0 for root element), or -1 on error.
3119///
3120/// # UPSTREAM-PARITY
3121///
3122/// ```c
3123/// int xmlTextReaderDepth(xmlTextReaderPtr reader);
3124/// ```
3125///
3126/// # Safety
3127///
3128/// `reader` must be a valid pointer or NULL.
3129#[no_mangle]
3130pub unsafe extern "C" fn xmlTextReaderDepth(reader: *mut XmlTextReader) -> c_int {
3131    if reader.is_null() {
3132        return -1;
3133    }
3134    // SAFETY: reader is valid.
3135    unsafe { (*reader).Depth() }
3136}
3137
3138/// Get the node type of the current node.
3139///
3140/// Returns one of the `xmlReaderTypes` constants, or -1 on error.
3141///
3142/// # UPSTREAM-PARITY
3143///
3144/// ```c
3145/// int xmlTextReaderNodeType(xmlTextReaderPtr reader);
3146/// ```
3147///
3148/// # Safety
3149///
3150/// `reader` must be a valid pointer or NULL.
3151#[no_mangle]
3152pub unsafe extern "C" fn xmlTextReaderNodeType(reader: *mut XmlTextReader) -> c_int {
3153    if reader.is_null() {
3154        return -1;
3155    }
3156    // SAFETY: reader is valid.
3157    unsafe { (*reader).NodeType() as c_int }
3158}
3159
3160/// Get the name of the current node.
3161///
3162/// Returns a newly allocated string (caller must free with `xmlFree`),
3163/// or NULL if there is no name.
3164///
3165/// # UPSTREAM-PARITY
3166///
3167/// ```c
3168/// xmlChar *xmlTextReaderName(xmlTextReaderPtr reader);
3169/// ```
3170///
3171/// # Safety
3172///
3173/// `reader` must be a valid pointer or NULL.
3174#[no_mangle]
3175pub unsafe extern "C" fn xmlTextReaderName(reader: *mut XmlTextReader) -> *mut xmlChar {
3176    if reader.is_null() {
3177        return ptr::null_mut();
3178    }
3179    // SAFETY: reader is valid.
3180    unsafe { (*reader).Name() }
3181}
3182
3183/// Get the value of the current node.
3184///
3185/// Returns a newly allocated string (caller must free with `xmlFree`),
3186/// or NULL if there is no value.
3187///
3188/// # UPSTREAM-PARITY
3189///
3190/// ```c
3191/// xmlChar *xmlTextReaderValue(xmlTextReaderPtr reader);
3192/// ```
3193///
3194/// # Safety
3195///
3196/// `reader` must be a valid pointer or NULL.
3197#[no_mangle]
3198pub unsafe extern "C" fn xmlTextReaderValue(reader: *mut XmlTextReader) -> *mut xmlChar {
3199    if reader.is_null() {
3200        return ptr::null_mut();
3201    }
3202    // SAFETY: reader is valid.
3203    unsafe { (*reader).Value() }
3204}
3205
3206/// Get a constant pointer to the name (no copy).
3207///
3208/// The returned pointer is valid only while the reader is alive and positioned
3209/// on the same node.
3210///
3211/// # UPSTREAM-PARITY
3212///
3213/// ```c
3214/// const xmlChar *xmlTextReaderConstName(xmlTextReaderPtr reader);
3215/// ```
3216///
3217/// # Safety
3218///
3219/// `reader` must be a valid pointer or NULL.
3220#[no_mangle]
3221pub unsafe extern "C" fn xmlTextReaderConstName(reader: *mut XmlTextReader) -> *const xmlChar {
3222    if reader.is_null() {
3223        return ptr::null();
3224    }
3225    // SAFETY: reader is valid.
3226    unsafe { (*reader).ConstName() }
3227}
3228
3229/// Get a constant pointer to the value (no copy).
3230///
3231/// The returned pointer is valid only while the reader is alive and positioned
3232/// on the same node.
3233///
3234/// # UPSTREAM-PARITY
3235///
3236/// ```c
3237/// const xmlChar *xmlTextReaderConstValue(xmlTextReaderPtr reader);
3238/// ```
3239///
3240/// # Safety
3241///
3242/// `reader` must be a valid pointer or NULL.
3243#[no_mangle]
3244pub unsafe extern "C" fn xmlTextReaderConstValue(reader: *mut XmlTextReader) -> *const xmlChar {
3245    if reader.is_null() {
3246        return ptr::null();
3247    }
3248    // SAFETY: reader is valid.
3249    unsafe { (*reader).ConstValue() }
3250}
3251
3252/// Get the base URI of the current node.
3253///
3254/// Returns a newly allocated string (caller must free with `xmlFree`),
3255/// or NULL if not available.
3256///
3257/// # UPSTREAM-PARITY
3258///
3259/// ```c
3260/// xmlChar *xmlTextReaderBaseUri(xmlTextReaderPtr reader);
3261/// ```
3262///
3263/// # Safety
3264///
3265/// `reader` must be a valid pointer or NULL.
3266#[no_mangle]
3267pub unsafe extern "C" fn xmlTextReaderBaseUri(reader: *mut XmlTextReader) -> *mut xmlChar {
3268    if reader.is_null() {
3269        return ptr::null_mut();
3270    }
3271    // SAFETY: reader is valid.
3272    unsafe { (*reader).BaseUri() }
3273}
3274
3275/// Get the local name of the current node.
3276///
3277/// Returns a newly allocated string, or NULL.
3278///
3279/// # UPSTREAM-PARITY
3280///
3281/// ```c
3282/// xmlChar *xmlTextReaderLocalName(xmlTextReaderPtr reader);
3283/// ```
3284///
3285/// # Safety
3286///
3287/// `reader` must be a valid pointer or NULL.
3288#[no_mangle]
3289pub unsafe extern "C" fn xmlTextReaderLocalName(reader: *mut XmlTextReader) -> *mut xmlChar {
3290    if reader.is_null() {
3291        return ptr::null_mut();
3292    }
3293    // SAFETY: reader is valid.
3294    unsafe { (*reader).LocalName() }
3295}
3296
3297/// Get the namespace URI of the current node.
3298///
3299/// Returns a newly allocated string, or NULL.
3300///
3301/// # UPSTREAM-PARITY
3302///
3303/// ```c
3304/// xmlChar *xmlTextReaderNamespaceUri(xmlTextReaderPtr reader);
3305/// ```
3306///
3307/// # Safety
3308///
3309/// `reader` must be a valid pointer or NULL.
3310#[no_mangle]
3311pub unsafe extern "C" fn xmlTextReaderNamespaceUri(reader: *mut XmlTextReader) -> *mut xmlChar {
3312    if reader.is_null() {
3313        return ptr::null_mut();
3314    }
3315    // SAFETY: reader is valid.
3316    unsafe { (*reader).NamespaceUri() }
3317}
3318
3319/// Get the prefix of the current node.
3320///
3321/// Returns a newly allocated string, or NULL.
3322///
3323/// # UPSTREAM-PARITY
3324///
3325/// ```c
3326/// xmlChar *xmlTextReaderPrefix(xmlTextReaderPtr reader);
3327/// ```
3328///
3329/// # Safety
3330///
3331/// `reader` must be a valid pointer or NULL.
3332#[no_mangle]
3333pub unsafe extern "C" fn xmlTextReaderPrefix(reader: *mut XmlTextReader) -> *mut xmlChar {
3334    if reader.is_null() {
3335        return ptr::null_mut();
3336    }
3337    // SAFETY: reader is valid.
3338    unsafe { (*reader).Prefix() }
3339}
3340
3341/// Check if the current node has a value.
3342///
3343/// Returns 1 if the node has a value, 0 otherwise.
3344///
3345/// # UPSTREAM-PARITY
3346///
3347/// ```c
3348/// int xmlTextReaderHasValue(xmlTextReaderPtr reader);
3349/// ```
3350///
3351/// # Safety
3352///
3353/// `reader` must be a valid pointer or NULL.
3354#[no_mangle]
3355pub unsafe extern "C" fn xmlTextReaderHasValue(reader: *mut XmlTextReader) -> c_int {
3356    if reader.is_null() {
3357        return 0;
3358    }
3359    // SAFETY: reader is valid.
3360    unsafe { (*reader).HasValue() }
3361}
3362
3363/// Check if the current node has attributes.
3364///
3365/// Returns 1 if the node has attributes, 0 otherwise.
3366///
3367/// # UPSTREAM-PARITY
3368///
3369/// ```c
3370/// int xmlTextReaderHasAttributes(xmlTextReaderPtr reader);
3371/// ```
3372///
3373/// # Safety
3374///
3375/// `reader` must be a valid pointer or NULL.
3376#[no_mangle]
3377pub unsafe extern "C" fn xmlTextReaderHasAttributes(reader: *mut XmlTextReader) -> c_int {
3378    if reader.is_null() {
3379        return 0;
3380    }
3381    // SAFETY: reader is valid.
3382    unsafe { (*reader).HasAttributes() }
3383}
3384
3385/// Check if the current element is an empty element (no children).
3386///
3387/// Returns 1 if empty, 0 otherwise.
3388///
3389/// # UPSTREAM-PARITY
3390///
3391/// ```c
3392/// int xmlTextReaderIsEmptyElement(xmlTextReaderPtr reader);
3393/// ```
3394///
3395/// # Safety
3396///
3397/// `reader` must be a valid pointer or NULL.
3398#[no_mangle]
3399pub unsafe extern "C" fn xmlTextReaderIsEmptyElement(reader: *mut XmlTextReader) -> c_int {
3400    if reader.is_null() {
3401        return 0;
3402    }
3403    // SAFETY: reader is valid.
3404    unsafe { (*reader).IsEmptyElement() }
3405}
3406
3407/// Get the read state.
3408///
3409/// Returns one of the `xmlTextReaderReadState` constants.
3410///
3411/// # UPSTREAM-PARITY
3412///
3413/// ```c
3414/// int xmlTextReaderReadState(xmlTextReaderPtr reader);
3415/// ```
3416///
3417/// # Safety
3418///
3419/// `reader` must be a valid pointer or NULL.
3420#[no_mangle]
3421pub unsafe extern "C" fn xmlTextReaderReadState(reader: *mut XmlTextReader) -> c_int {
3422    if reader.is_null() {
3423        return ReadState::ERROR as c_int;
3424    }
3425    // SAFETY: reader is valid.
3426    unsafe { (*reader).ReadState() as c_int }
3427}
3428
3429// ─────────────────────────────────────────────────────────────────────────────
3430// Attribute access
3431// ─────────────────────────────────────────────────────────────────────────────
3432
3433/// Get an attribute value by name.
3434///
3435/// Returns a newly allocated string, or NULL.
3436///
3437/// # UPSTREAM-PARITY
3438///
3439/// ```c
3440/// xmlChar *xmlTextReaderGetAttribute(xmlTextReaderPtr reader, const xmlChar *name);
3441/// ```
3442///
3443/// # Safety
3444///
3445/// `reader` and `name` must be valid pointers or NULL.
3446#[no_mangle]
3447pub unsafe extern "C" fn xmlTextReaderGetAttribute(
3448    reader: *mut XmlTextReader,
3449    name: *const xmlChar,
3450) -> *mut xmlChar {
3451    if reader.is_null() || name.is_null() {
3452        return ptr::null_mut();
3453    }
3454    // SAFETY: reader and name are valid.
3455    unsafe { (*reader).GetAttribute(name) }
3456}
3457
3458/// Get an attribute value by index.
3459///
3460/// Returns a newly allocated string, or NULL.
3461///
3462/// # UPSTREAM-PARITY
3463///
3464/// ```c
3465/// xmlChar *xmlTextReaderGetAttributeNo(xmlTextReaderPtr reader, int index);
3466/// ```
3467///
3468/// # Safety
3469///
3470/// `reader` must be a valid pointer or NULL.
3471#[no_mangle]
3472pub unsafe extern "C" fn xmlTextReaderGetAttributeNo(
3473    reader: *mut XmlTextReader,
3474    index: c_int,
3475) -> *mut xmlChar {
3476    if reader.is_null() {
3477        return ptr::null_mut();
3478    }
3479    // SAFETY: reader is valid.
3480    unsafe { (*reader).GetAttributeNo(index) }
3481}
3482
3483/// Get an attribute value by local name and namespace URI.
3484///
3485/// Returns a newly allocated string, or NULL.
3486///
3487/// # UPSTREAM-PARITY
3488///
3489/// ```c
3490/// xmlChar *xmlTextReaderGetAttributeNs(xmlTextReaderPtr reader,
3491///                                      const xmlChar *localName,
3492///                                      const xmlChar *namespaceURI);
3493/// ```
3494///
3495/// # Safety
3496///
3497/// `reader`, `localName`, and `namespaceURI` must be valid pointers or NULL.
3498#[no_mangle]
3499pub unsafe extern "C" fn xmlTextReaderGetAttributeNs(
3500    reader: *mut XmlTextReader,
3501    localName: *const xmlChar,
3502    namespaceURI: *const xmlChar,
3503) -> *mut xmlChar {
3504    if reader.is_null() || localName.is_null() {
3505        return ptr::null_mut();
3506    }
3507    // SAFETY: reader, localName, and namespaceURI are valid.
3508    unsafe { (*reader).GetAttributeNs(localName, namespaceURI) }
3509}
3510
3511/// Look up a namespace by prefix.
3512///
3513/// Returns a newly allocated string with the namespace URI, or NULL.
3514///
3515/// # UPSTREAM-PARITY
3516///
3517/// ```c
3518/// xmlChar *xmlTextReaderLookupNamespace(xmlTextReaderPtr reader, const xmlChar *prefix);
3519/// ```
3520///
3521/// # Safety
3522///
3523/// `reader` and `prefix` must be valid pointers or NULL.
3524#[no_mangle]
3525pub unsafe extern "C" fn xmlTextReaderLookupNamespace(
3526    reader: *mut XmlTextReader,
3527    prefix: *const xmlChar,
3528) -> *mut xmlChar {
3529    if reader.is_null() {
3530        return ptr::null_mut();
3531    }
3532    // SAFETY: reader and prefix are valid.
3533    unsafe { (*reader).LookupNamespace(prefix) }
3534}
3535
3536// ─────────────────────────────────────────────────────────────────────────────
3537// Parser properties
3538// ─────────────────────────────────────────────────────────────────────────────
3539
3540/// Get a parser property.
3541///
3542/// Returns the property value (0 or 1), or -1 on error.
3543///
3544/// # UPSTREAM-PARITY
3545///
3546/// ```c
3547/// int xmlTextReaderGetParserProp(xmlTextReaderPtr reader, int prop);
3548/// ```
3549///
3550/// # Safety
3551///
3552/// `reader` must be a valid pointer or NULL.
3553#[no_mangle]
3554pub unsafe extern "C" fn xmlTextReaderGetParserProp(
3555    reader: *mut XmlTextReader,
3556    prop: c_int,
3557) -> c_int {
3558    if reader.is_null() {
3559        return -1;
3560    }
3561    // SAFETY: reader is valid.
3562    unsafe { (*reader).GetParserProp(prop) }
3563}
3564
3565/// Set a parser property.
3566///
3567/// Returns 0 on success, -1 on error.
3568///
3569/// # UPSTREAM-PARITY
3570///
3571/// ```c
3572/// int xmlTextReaderSetParserProp(xmlTextReaderPtr reader, int prop, int value);
3573/// ```
3574///
3575/// # Safety
3576///
3577/// `reader` must be a valid pointer or NULL.
3578#[no_mangle]
3579pub unsafe extern "C" fn xmlTextReaderSetParserProp(
3580    reader: *mut XmlTextReader,
3581    prop: c_int,
3582    value: c_int,
3583) -> c_int {
3584    if reader.is_null() {
3585        return -1;
3586    }
3587    // SAFETY: reader is valid.
3588    unsafe { (*reader).SetParserProp(prop, value) }
3589}
3590
3591// ─────────────────────────────────────────────────────────────────────────────
3592// Lifecycle
3593// ─────────────────────────────────────────────────────────────────────────────
3594
3595/// Free a text reader.
3596///
3597/// # UPSTREAM-PARITY
3598///
3599/// ```c
3600/// void xmlFreeTextReader(xmlTextReaderPtr reader);
3601/// ```
3602///
3603/// # Safety
3604///
3605/// `reader` must be a valid pointer returned by `xmlNewTextReader` or one of the
3606/// `xmlReaderFor*` functions, or NULL (in which case this is a no-op).
3607#[no_mangle]
3608pub unsafe extern "C" fn xmlFreeTextReader(reader: *mut XmlTextReader) {
3609    if reader.is_null() {
3610        return;
3611    }
3612    // SAFETY: reader was created via Box::into_raw, so we reconstruct the Box
3613    // and let it drop, which calls the Drop impl.
3614    unsafe {
3615        let _ = Box::from_raw(reader);
3616    }
3617}
3618
3619/// Setup/reinitialize a reader with new input.
3620///
3621/// # UPSTREAM-PARITY
3622///
3623/// ```c
3624/// int xmlTextReaderSetup(xmlTextReaderPtr reader,
3625///                        xmlParserInputBufferPtr input,
3626///                        const char *URL, const char *encoding, int options);
3627/// ```
3628///
3629/// # Safety
3630///
3631/// - `reader` must be a valid pointer or NULL.
3632/// - `input` must be a valid `_xmlParserInputBuffer` pointer or NULL.
3633/// - `URL` and `encoding` must be valid C strings or NULL.
3634#[no_mangle]
3635pub unsafe extern "C" fn xmlTextReaderSetup(
3636    reader: *mut XmlTextReader,
3637    input: *mut _xmlParserInputBuffer,
3638    URL: *const c_char,
3639    encoding: *const c_char,
3640    options: c_int,
3641) -> c_int {
3642    if reader.is_null() {
3643        return -1;
3644    }
3645
3646    // SAFETY: reader is valid.
3647    let r = unsafe { &mut *reader };
3648
3649    // Reset the reader state.
3650    r.clear_cached_name();
3651    r.clear_cached_value();
3652
3653    r.events.clear();
3654    r.event_index = 0;
3655    r.state = ReadState::INITIALIZED;
3656    r.cur_node = ptr::null_mut();
3657    r.node_type = ReaderNodeType::NONE;
3658    r.depth = 0;
3659    r.attribute_count = -1;
3660    r.cur_attribute = -1;
3661    r.options = options;
3662    r.parsed = false;
3663    r.errors.clear();
3664    r.doc_incomplete = false;
3665    r.finalized = false;
3666    r.reparse_input = None;
3667
3668    // Update URL.
3669    if !r.URL.is_null() {
3670        // SAFETY: URL was allocated by xmlMalloc.
3671        unsafe { xmlFreeImpl(r.URL as *mut c_void) };
3672        r.URL = ptr::null_mut();
3673    }
3674    if !URL.is_null() {
3675        // SAFETY: URL is a valid C string.
3676        let url_str = unsafe { std::ffi::CStr::from_ptr(URL) };
3677        // SAFETY: bytes_to_xmlstr allocates via xmlMalloc.
3678        r.URL = unsafe { bytes_to_xmlstr(url_str.to_bytes()) };
3679    }
3680
3681    // Update encoding.
3682    if !r.encoding.is_null() {
3683        // SAFETY: encoding was allocated by xmlMalloc.
3684        unsafe { xmlFreeImpl(r.encoding as *mut c_void) };
3685        r.encoding = ptr::null_mut();
3686    }
3687    if !encoding.is_null() {
3688        // SAFETY: encoding is a valid C string.
3689        let enc_str = unsafe { std::ffi::CStr::from_ptr(encoding) };
3690        // SAFETY: bytes_to_xmlstr allocates via xmlMalloc.
3691        r.encoding = unsafe { bytes_to_xmlstr(enc_str.to_bytes()) };
3692    }
3693
3694    // UPSTREAM-PARITY (xmlreader.c xmlTextReaderSetup): a NULL input means
3695    // "use the input the reader was created with" — PHP's XMLReader::XML() /
3696    // fromString() call xmlNewTextReader(inputbfr, uri) and then
3697    // xmlTextReaderSetup(reader, NULL, uri, encoding, options). The
3698    // candidate consumed that buffer in reader_from_input (the context is
3699    // already wired), so a NULL input keeps the existing context; a NEW
3700    // input replaces it (C consumers re-arming the reader).
3701    if input.is_null() {
3702        // UPSTREAM-PARITY: a NULL input promotes the source the reader was
3703        // created with (php XMLReader::XML()/fromString(): xmlNewTextReader
3704        // then xmlTextReaderSetup(reader, NULL, ...)). The candidate
3705        // consumed that source in reader_from_input, so the context built
3706        // there is kept. A reader that ALREADY parsed (its context was
3707        // consumed by parse_and_build_events) discards the old document and
3708        // resets — upstream xmlTextReaderSetup tears the previous parse
3709        // state down.
3710        if r.ctxt.is_null() && !r.doc.is_null() {
3711            // SAFETY: doc was allocated by the parser.
3712            unsafe { tree::free_doc(r.doc) };
3713            r.doc = ptr::null_mut();
3714        }
3715        return 0;
3716    }
3717
3718    // Free the old document.
3719    if !r.doc.is_null() {
3720        // SAFETY: doc was allocated by the parser.
3721        unsafe { tree::free_doc(r.doc) };
3722        r.doc = ptr::null_mut();
3723    }
3724
3725    // Free old parser context.
3726    if !r.ctxt.is_null() {
3727        // SAFETY: ctxt was created by create_parser_ctxt.
3728        unsafe { free_parser_ctxt(r.ctxt) };
3729        r.ctxt = ptr::null_mut();
3730    }
3731
3732    // Create new parser context and set up input.
3733    // SAFETY: create_parser_ctxt returns a valid context or NULL.
3734    let ctxt = unsafe { create_parser_ctxt() };
3735    if ctxt.is_null() {
3736        return -1;
3737    }
3738
3739    // Read all data from the input buffer. A memory buffer
3740    // (xmlParserInputBufferCreateMem) has no read callback — its content
3741    // lives in the input-buffer content stash (helpers.rs).
3742    let mut data = Vec::new();
3743    let mut tmp = [0u8; 4096];
3744
3745    // SAFETY: input is valid.
3746    let read_cb = unsafe { (*input).readcallback };
3747    let ioctx = unsafe { (*input).context };
3748
3749    if let Some(read) = read_cb {
3750        loop {
3751            // SAFETY: callbacks are valid.
3752            let n = unsafe { read(ioctx, tmp.as_mut_ptr() as *mut c_char, tmp.len() as c_int) };
3753            if n <= 0 {
3754                break;
3755            }
3756            data.extend_from_slice(&tmp[..n as usize]);
3757        }
3758    } else {
3759        data = crate::xml::parser::helpers::take_input_buf_content(input).unwrap_or_default();
3760    }
3761
3762    // Close the input.
3763    let close_cb = unsafe { (*input).closecallback };
3764    if let Some(close) = close_cb {
3765        // SAFETY: close callback is valid.
3766        unsafe { close(ioctx) };
3767    }
3768
3769    let input_buf = InputBuffer::from_memory(&data, None);
3770
3771    // SAFETY: ctxt and input_buf are valid.
3772    unsafe { setup_parser_input(ctxt, input_buf) };
3773    unsafe {
3774        (*ctxt).options = options;
3775    }
3776
3777    r.ctxt = ctxt;
3778
3779    0
3780}
3781
3782/// Get the current document from the reader.
3783///
3784/// Returns a pointer to the `_xmlDoc` or NULL.
3785///
3786/// # UPSTREAM-PARITY
3787///
3788/// ```c
3789/// xmlDocPtr xmlTextReaderCurrentDoc(xmlTextReaderPtr reader);
3790/// ```
3791///
3792/// # Safety
3793///
3794/// `reader` must be a valid pointer or NULL.
3795#[no_mangle]
3796pub unsafe extern "C" fn xmlTextReaderCurrentDoc(reader: *mut XmlTextReader) -> *mut _xmlDoc {
3797    if reader.is_null() {
3798        return ptr::null_mut();
3799    }
3800    // SAFETY: reader is valid. Upstream xmlTextReaderCurrentDoc sets
3801    // reader->preserve = 1 and returns ctxt->myDoc: the document ownership
3802    // moves to the caller (freed with xmlFreeDoc). Mirror that so the
3803    // reader's Drop does not free a doc the caller still owns (upstream
3804    // reader3.c pattern; Phase-12 EXTERNAL-CONSUMERS court).
3805    unsafe {
3806        (*reader).preserve = true;
3807        (*reader).CurrentDoc()
3808    }
3809}
3810
3811/// Close the reader, releasing the document and parser state.
3812///
3813/// # UPSTREAM-PARITY
3814///
3815/// Upstream `xmlTextReaderClose` (xmlreader.c): sets the mode to
3816/// XML_TEXTREADER_MODE_CLOSED, drops the current node, and tears down the
3817/// validation state. The reader object itself is freed separately with
3818/// `xmlFreeTextReader`.
3819///
3820/// ```c
3821/// int xmlTextReaderClose(xmlTextReaderPtr reader);
3822/// ```
3823///
3824/// Returns 0 on success, -1 if `reader` is NULL.
3825///
3826/// # Safety
3827///
3828/// `reader` must be a valid pointer or NULL.
3829#[no_mangle]
3830pub unsafe extern "C" fn xmlTextReaderClose(reader: *mut XmlTextReader) -> c_int {
3831    if reader.is_null() {
3832        return -1;
3833    }
3834    // SAFETY: reader is valid; close resets cursor state and marks the
3835    // reader closed, mirroring upstream's mode transition.
3836    unsafe {
3837        let r = &mut *reader;
3838        r.cur_node = ptr::null_mut();
3839        r.node_type = ReaderNodeType::NONE;
3840        r.clear_cached_name();
3841        r.clear_cached_value();
3842        r.state = ReadState::CLOSED;
3843    }
3844    0
3845}
3846
3847/// Return the current node of the reader.
3848///
3849/// # UPSTREAM-PARITY
3850///
3851/// ```c
3852/// xmlNodePtr xmlTextReaderCurrentNode(xmlTextReaderPtr reader);
3853/// ```
3854///
3855/// Returns the current node or NULL. The node is owned by the document;
3856/// the caller must not free it.
3857///
3858/// # Safety
3859///
3860/// `reader` must be a valid pointer or NULL.
3861#[no_mangle]
3862pub unsafe extern "C" fn xmlTextReaderCurrentNode(reader: *mut XmlTextReader) -> *mut _xmlNode {
3863    if reader.is_null() {
3864        return ptr::null_mut();
3865    }
3866    // SAFETY: reader is valid.
3867    unsafe { (*reader).cur_node }
3868}
3869
3870/// Expand entity references at the current position.
3871///
3872/// # UPSTREAM-PARITY
3873///
3874/// Upstream `xmlTextReaderExpand` (xmlreader.c) forces substitution of the
3875/// current entity reference so the node can be read in full. When the parser
3876/// ran with XML_PARSE_NOENT the entities are already substituted during
3877/// parsing; the function then simply returns the current node.
3878///
3879/// ```c
3880/// xmlNodePtr xmlTextReaderExpand(xmlTextReaderPtr reader);
3881/// ```
3882///
3883/// Returns the (expanded) current node, or NULL if the reader is NULL or
3884/// not positioned on a node.
3885///
3886/// # Safety
3887///
3888/// `reader` must be a valid pointer or NULL.
3889#[no_mangle]
3890pub unsafe extern "C" fn xmlTextReaderExpand(reader: *mut XmlTextReader) -> *mut _xmlNode {
3891    if reader.is_null() {
3892        return ptr::null_mut();
3893    }
3894    // SAFETY: reader is valid.
3895    unsafe { (*reader).cur_node }
3896}
3897
3898/// Return the parser line number of the current node.
3899///
3900/// # UPSTREAM-PARITY
3901///
3902/// Upstream `xmlTextReaderGetParserLineNumber` returns the input stream's
3903/// current line. The candidate records `line` per node during parsing, which
3904/// is equivalent for the read cursor.
3905///
3906/// ```c
3907/// int xmlTextReaderGetParserLineNumber(xmlTextReaderPtr reader);
3908/// ```
3909///
3910/// Returns the line number, or 0 when unavailable.
3911///
3912/// # Safety
3913///
3914/// `reader` must be a valid pointer or NULL.
3915#[no_mangle]
3916pub unsafe extern "C" fn xmlTextReaderGetParserLineNumber(reader: *mut XmlTextReader) -> c_int {
3917    if reader.is_null() {
3918        return 0;
3919    }
3920    // SAFETY: reader is valid; cur_node is owned by the doc.
3921    unsafe {
3922        let node = (*reader).cur_node;
3923        if node.is_null() {
3924            0
3925        } else {
3926            (*node).line as c_int
3927        }
3928    }
3929}
3930
3931/// Return the parser column number of the current node.
3932///
3933/// # UPSTREAM-PARITY
3934///
3935/// Upstream `xmlTextReaderGetParserColumnNumber` returns the input stream's
3936/// column. Columns are not tracked per-node in the candidate tree (upstream
3937/// exposes -1 when no column information is available either); return -1.
3938///
3939/// ```c
3940/// int xmlTextReaderGetParserColumnNumber(xmlTextReaderPtr reader);
3941/// ```
3942///
3943/// # Safety
3944///
3945/// `reader` must be a valid pointer or NULL.
3946#[no_mangle]
3947pub const unsafe extern "C" fn xmlTextReaderGetParserColumnNumber(
3948    reader: *mut XmlTextReader,
3949) -> c_int {
3950    if reader.is_null() {
3951        return -1;
3952    }
3953    -1
3954}
3955
3956/// Return the validation status of the reader.
3957///
3958/// # UPSTREAM-PARITY
3959///
3960/// Upstream `xmlTextReaderIsValid` (xmlreader.c): with a RELAX NG
3961/// validation context attached the RELAX NG outcome wins, then the XSD
3962/// outcome, then the DTD-parse validity (`ctxt->valid`); 0 when no
3963/// validation was performed or it failed, -1 for a NULL reader.
3964///
3965/// ```c
3966/// int xmlTextReaderIsValid(xmlTextReaderPtr reader);
3967/// ```
3968///
3969/// # Safety
3970///
3971/// `reader` must be a valid pointer or NULL.
3972#[no_mangle]
3973pub unsafe extern "C" fn xmlTextReaderIsValid(reader: *mut XmlTextReader) -> c_int {
3974    if reader.is_null() {
3975        return -1;
3976    }
3977    unsafe {
3978        let r = &*reader;
3979        if !r.rng.is_null() {
3980            if r.rng_result >= 0 {
3981                r.rng_result
3982            } else {
3983                // Validation not run yet (schema attached, first Read not
3984                // done): upstream's rngValidCtxt->valid starts at 0.
3985                0
3986            }
3987        } else if !r.schema.is_null() {
3988            if r.xsd_result >= 0 {
3989                r.xsd_result
3990            } else {
3991                0
3992            }
3993        } else if r.did_validate == 1 {
3994            r.was_valid
3995        } else {
3996            0
3997        }
3998    }
3999}
4000
4001/// Return the normalization status of the reader.
4002///
4003/// # UPSTREAM-PARITY
4004///
4005/// Upstream `xmlTextReaderNormalization` returns 1 when the reader performs
4006/// whitespace normalization (it always reports 1 unless the parser was
4007/// configured otherwise). The candidate normalizes attribute values per the
4008/// XML spec during parsing, so report 1.
4009///
4010/// ```c
4011/// int xmlTextReaderNormalization(xmlTextReaderPtr reader);
4012/// ```
4013///
4014/// # Safety
4015///
4016/// `reader` must be a valid pointer or NULL.
4017#[no_mangle]
4018pub const unsafe extern "C" fn xmlTextReaderNormalization(reader: *mut XmlTextReader) -> c_int {
4019    if reader.is_null() {
4020        return -1;
4021    }
4022    1
4023}
4024
4025/// Read the value of an attribute as a text node (attribute-value mode).
4026///
4027/// # UPSTREAM-PARITY
4028///
4029/// Upstream `xmlTextReaderReadAttributeValue` moves the reader so that the
4030/// value of the current attribute is available as a text node, returning 1
4031/// on success and 0 when already at the end. The candidate tree stores
4032/// attribute values directly on the attribute node, so the value is already
4033/// available via `xmlTextReaderValue`; report 1 when positioned on an
4034/// attribute with a value.
4035///
4036/// ```c
4037/// int xmlTextReaderReadAttributeValue(xmlTextReaderPtr reader);
4038/// ```
4039///
4040/// # Safety
4041///
4042/// `reader` must be a valid pointer or NULL.
4043#[no_mangle]
4044pub unsafe extern "C" fn xmlTextReaderReadAttributeValue(reader: *mut XmlTextReader) -> c_int {
4045    if reader.is_null() {
4046        return -1;
4047    }
4048    // SAFETY: reader is valid.
4049    unsafe {
4050        let r = &*reader;
4051        if r.node_type == ReaderNodeType::ATTRIBUTE && !r.cur_node.is_null() {
4052            1
4053        } else {
4054            0
4055        }
4056    }
4057}
4058
4059/// Read the content of the current node as a string.
4060///
4061/// # UPSTREAM-PARITY
4062///
4063/// Upstream `xmlTextReaderReadString` concatenates the text of the current
4064/// node's subtree (recursively) into one string. It behaves like
4065/// `xmlNodeGetContent` for the current node.
4066///
4067/// ```c
4068/// xmlChar *xmlTextReaderReadString(xmlTextReaderPtr reader);
4069/// ```
4070///
4071/// Returns a newly allocated string (free with `xmlFree`) or NULL.
4072///
4073/// # Safety
4074///
4075/// `reader` must be a valid pointer or NULL.
4076#[no_mangle]
4077pub unsafe extern "C" fn xmlTextReaderReadString(reader: *mut XmlTextReader) -> *mut xmlChar {
4078    if reader.is_null() {
4079        return ptr::null_mut();
4080    }
4081    // SAFETY: reader is valid; node owned by the document.
4082    unsafe {
4083        let node = (*reader).cur_node;
4084        if node.is_null() {
4085            return ptr::null_mut();
4086        }
4087        tree::node_get_content(node)
4088    }
4089}
4090
4091/// Read the inner XML of the current node as a string.
4092///
4093/// # UPSTREAM-PARITY
4094///
4095/// Upstream `xmlTextReaderReadInnerXml` serializes the children of the
4096/// current node. The candidate uses its serializer on the children list.
4097///
4098/// ```c
4099/// xmlChar *xmlTextReaderReadInnerXml(xmlTextReaderPtr reader);
4100/// ```
4101///
4102/// Returns a newly allocated string (free with `xmlFree`) or NULL.
4103///
4104/// # Safety
4105///
4106/// `reader` must be a valid pointer or NULL.
4107#[no_mangle]
4108pub unsafe extern "C" fn xmlTextReaderReadInnerXml(reader: *mut XmlTextReader) -> *mut xmlChar {
4109    if reader.is_null() {
4110        return ptr::null_mut();
4111    }
4112    // SAFETY: reader is valid; node owned by the document.
4113    unsafe {
4114        let node = (*reader).cur_node;
4115        if node.is_null() {
4116            return ptr::null_mut();
4117        }
4118        let buf = crate::xml::io::buf_create(-1);
4119        if buf.is_null() {
4120            return ptr::null_mut();
4121        }
4122        let mut child = (*node).children;
4123        while !child.is_null() {
4124            tree::serialize_node(child, buf, 0, 0);
4125            child = (*child).next;
4126        }
4127        let len = crate::xml::io::buf_length(buf) as usize;
4128        let content = crate::xml::io::buf_content(buf);
4129        if content.is_null() || len == 0 {
4130            crate::xml::io::buf_free(buf);
4131            return ptr::null_mut();
4132        }
4133        let out = xml_strdup(content);
4134        crate::xml::io::buf_free(buf);
4135        out
4136    }
4137}
4138
4139/// Read the outer XML of the current node as a string.
4140///
4141/// # UPSTREAM-PARITY
4142///
4143/// Upstream `xmlTextReaderReadOuterXml` serializes the current node itself.
4144///
4145/// ```c
4146/// xmlChar *xmlTextReaderReadOuterXml(xmlTextReaderPtr reader);
4147/// ```
4148///
4149/// Returns a newly allocated string (free with `xmlFree`) or NULL.
4150///
4151/// # Safety
4152///
4153/// `reader` must be a valid pointer or NULL.
4154#[no_mangle]
4155pub unsafe extern "C" fn xmlTextReaderReadOuterXml(reader: *mut XmlTextReader) -> *mut xmlChar {
4156    if reader.is_null() {
4157        return ptr::null_mut();
4158    }
4159    // SAFETY: reader is valid; node owned by the document.
4160    unsafe {
4161        let node = (*reader).cur_node;
4162        if node.is_null() {
4163            return ptr::null_mut();
4164        }
4165        let buf = crate::xml::io::buf_create(-1);
4166        if buf.is_null() {
4167            return ptr::null_mut();
4168        }
4169        tree::serialize_node(node, buf, 0, 0);
4170        let len = crate::xml::io::buf_length(buf) as usize;
4171        let content = crate::xml::io::buf_content(buf);
4172        if content.is_null() || len == 0 {
4173            crate::xml::io::buf_free(buf);
4174            return ptr::null_mut();
4175        }
4176        let out = xml_strdup(content);
4177        crate::xml::io::buf_free(buf);
4178        out
4179    }
4180}
4181
4182/// Return the standalone flag of the document being read.
4183///
4184/// # UPSTREAM-PARITY
4185///
4186/// Upstream `xmlTextReaderStandalone` returns the document's standalone
4187/// value (1 = standalone, 0 = not, -1 = no XML declaration / NULL reader).
4188///
4189/// ```c
4190/// int xmlTextReaderStandalone(xmlTextReaderPtr reader);
4191/// ```
4192///
4193/// # Safety
4194///
4195/// `reader` must be a valid pointer or NULL.
4196#[no_mangle]
4197pub unsafe extern "C" fn xmlTextReaderStandalone(reader: *mut XmlTextReader) -> c_int {
4198    if reader.is_null() {
4199        return -1;
4200    }
4201    // SAFETY: reader is valid; doc owned by the reader.
4202    unsafe {
4203        let doc = (*reader).doc;
4204        if doc.is_null() {
4205            return -1;
4206        }
4207        (*doc).standalone
4208    }
4209}
4210
4211/// Return the xml:lang of the current node.
4212///
4213/// # UPSTREAM-PARITY
4214///
4215/// Upstream `xmlTextReaderXmlLang` returns `xmlNodeGetLang(node)`: the
4216/// nearest `xml:lang` attribute on the node or an ancestor.
4217///
4218/// ```c
4219/// xmlChar *xmlTextReaderXmlLang(xmlTextReaderPtr reader);
4220/// ```
4221///
4222/// Returns a newly allocated string (free with `xmlFree`) or NULL.
4223///
4224/// # Safety
4225///
4226/// `reader` must be a valid pointer or NULL.
4227#[no_mangle]
4228pub unsafe extern "C" fn xmlTextReaderXmlLang(reader: *mut XmlTextReader) -> *mut xmlChar {
4229    if reader.is_null() {
4230        return ptr::null_mut();
4231    }
4232    // SAFETY: reader is valid; node owned by the document.
4233    unsafe {
4234        let mut node = (*reader).cur_node;
4235        while !node.is_null() {
4236            // walk the property list for xml:lang
4237            let mut prop = (*node).properties;
4238            while !prop.is_null() {
4239                if !(*prop).name.is_null() {
4240                    let name = crate::xml::string::xmlstr_to_bytes((*prop).name);
4241                    if name == b"lang" && !(*prop).ns.is_null() {
4242                        let ns_href = crate::xml::string::xmlstr_to_bytes((*(*prop).ns).href);
4243                        if ns_href == b"http://www.w3.org/XML/1998/namespace" {
4244                            let v = (*prop).children;
4245                            if !v.is_null() && !(*v).content.is_null() {
4246                                return xml_strdup((*v).content);
4247                            }
4248                        }
4249                    }
4250                }
4251                prop = (*prop).next;
4252            }
4253            node = (*node).parent;
4254        }
4255        ptr::null_mut()
4256    }
4257}
4258
4259// ═══════════════════════════════════════════════════════════════════════════════
4260// Tests
4261// ═══════════════════════════════════════════════════════════════════════════════
4262
4263#[cfg(test)]
4264mod tests {
4265    use super::*;
4266    use crate::abi::allocator::xmlFreeImpl;
4267    use core::ffi::c_void;
4268    use std::os::raw::c_char;
4269
4270    /// Helper: create a reader from a string.
4271    unsafe fn create_reader(xml: &str) -> *mut XmlTextReader {
4272        let bytes = xml.as_bytes();
4273        xmlReaderForMemory(
4274            bytes.as_ptr() as *const c_char,
4275            bytes.len() as c_int,
4276            ptr::null(),
4277            ptr::null(),
4278            0,
4279        )
4280    }
4281
4282    /// Helper: free a reader.
4283    unsafe fn free_reader(reader: *mut XmlTextReader) {
4284        if !reader.is_null() {
4285            xmlFreeTextReader(reader);
4286        }
4287    }
4288
4289    /// Helper: read through all nodes and collect their types and names.
4290    unsafe fn collect_nodes(reader: *mut XmlTextReader) -> Vec<(ReaderNodeType, String, i32)> {
4291        let mut result = Vec::new();
4292        loop {
4293            let ret = xmlTextReaderRead(reader);
4294            if ret <= 0 {
4295                break;
4296            }
4297            // SAFETY: reader is valid.
4298            let r = &*reader;
4299            let ntype = r.NodeType();
4300            let name = if r.name.is_null() {
4301                String::new()
4302            } else {
4303                xmlstr_to_string(r.name as *const xmlChar)
4304            };
4305            let depth = r.Depth();
4306            result.push((ntype, name, depth));
4307        }
4308        result
4309    }
4310
4311    // ─── Basic tests ───────────────────────────────────────────────────────
4312
4313    /// Creates a reader from a memory buffer and checks its initial state.
4314    ///
4315    /// # Safety
4316    ///
4317    /// - The reader is created from a static string literal that stays alive
4318    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
4319    ///   before it is dereferenced and is freed exactly once with
4320    ///   `free_reader`.
4321    #[test]
4322    fn test_create_reader_from_memory() {
4323        unsafe {
4324            let reader = create_reader("<root/>");
4325            assert!(!reader.is_null());
4326            assert_eq!((*reader).ReadState(), ReadState::INITIALIZED);
4327            free_reader(reader);
4328        }
4329    }
4330
4331    /// Reads a simple document and verifies the event sequence.
4332    ///
4333    /// # Safety
4334    ///
4335    /// - The reader is created from a static string literal that stays alive
4336    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
4337    ///   before it is dereferenced and is freed exactly once with
4338    ///   `free_reader`.
4339    #[test]
4340    fn test_read_simple_document() {
4341        unsafe {
4342            let reader = create_reader("<root><child>text</child></root>");
4343            assert!(!reader.is_null());
4344
4345            let nodes = collect_nodes(reader);
4346            // Expected sequence:
4347            // ELEMENT root (depth=0)
4348            // ELEMENT child (depth=1)
4349            // TEXT text (depth=2)
4350            // END_ELEMENT child (depth=1)
4351            // END_ELEMENT root (depth=0)
4352
4353            assert_eq!(nodes.len(), 5);
4354            assert_eq!(nodes[0], (ReaderNodeType::ELEMENT, "root".to_string(), 0));
4355            assert_eq!(nodes[1], (ReaderNodeType::ELEMENT, "child".to_string(), 1));
4356            // UPSTREAM-PARITY: text nodes report the fixed name "#text".
4357            assert_eq!(nodes[2], (ReaderNodeType::TEXT, "#text".to_string(), 2));
4358            assert_eq!(
4359                nodes[3],
4360                (ReaderNodeType::END_ELEMENT, "child".to_string(), 1)
4361            );
4362            assert_eq!(
4363                nodes[4],
4364                (ReaderNodeType::END_ELEMENT, "root".to_string(), 0)
4365            );
4366
4367            assert_eq!((*reader).ReadState(), ReadState::EOF);
4368            free_reader(reader);
4369        }
4370    }
4371
4372    /// SP-14.3.1-7 (fromStream_broken_stream): a document that is NOT
4373    /// complete when the input ends (an unterminated root) still delivers the
4374    /// completed prefix events — the root element and the comment — and the
4375    /// premature-EOF failure surfaces only on the read that runs past the
4376    /// last event, leaving the cursor on the last delivered node (upstream
4377    /// xmlTextReaderPushData's terminating xmlParseChunk).
4378    ///
4379    /// # Safety
4380    ///
4381    /// - The reader is created from a static string literal that stays alive
4382    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
4383    ///   before it is dereferenced and is freed exactly once with
4384    ///   `free_reader`.
4385    #[test]
4386    fn test_read_incomplete_document_defers_eof_error() {
4387        unsafe {
4388            // Deliberately unterminated: <root><!--my comment--> (no </root>).
4389            let reader = create_reader("<root><!--my comment-->");
4390            assert!(!reader.is_null());
4391
4392            // read #1: the root element is delivered even though the document
4393            // is not complete.
4394            assert_eq!(xmlTextReaderRead(reader), 1);
4395            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
4396            assert_eq!(xmlstr_to_bytes((*reader).Name()), b"root");
4397            assert_eq!((*reader).Depth(), 0);
4398
4399            // read #2: the comment.
4400            assert_eq!(xmlTextReaderRead(reader), 1);
4401            assert_eq!((*reader).NodeType(), ReaderNodeType::COMMENT);
4402            assert_eq!(xmlstr_to_bytes((*reader).Value()), b"my comment");
4403            assert_eq!((*reader).Depth(), 1);
4404
4405            // read #3: the parse needs more input than the source supplies;
4406            // the deferred EOF finalize reports the failure and the cursor
4407            // stays on the last delivered event (upstream
4408            // fromStream_broken_stream).
4409            assert_eq!(xmlTextReaderRead(reader), -1);
4410            assert_eq!((*reader).NodeType(), ReaderNodeType::COMMENT);
4411            assert_eq!((*reader).Depth(), 1);
4412
4413            // Later reads keep failing with the cursor frozen.
4414            assert_eq!(xmlTextReaderRead(reader), -1);
4415            assert_eq!((*reader).NodeType(), ReaderNodeType::COMMENT);
4416            assert_eq!((*reader).Depth(), 1);
4417
4418            free_reader(reader);
4419        }
4420    }
4421
4422    /// Verifies read state transitions from INITIALIZED to EOF.
4423    ///
4424    /// # Safety
4425    ///
4426    /// - The reader is created from a static string literal that stays alive
4427    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
4428    ///   before it is dereferenced and is freed exactly once with
4429    ///   `free_reader`.
4430    #[test]
4431    fn test_read_state_transitions() {
4432        unsafe {
4433            let reader = create_reader("<root/>");
4434            assert!(!reader.is_null());
4435            assert_eq!((*reader).ReadState(), ReadState::INITIALIZED);
4436
4437            // First read.
4438            assert_eq!(xmlTextReaderRead(reader), 1);
4439            assert_eq!((*reader).ReadState(), ReadState::READING);
4440            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
4441            assert_eq!((*reader).Depth(), 0);
4442
4443            // UPSTREAM-PARITY (oracle-verified 2.15.3): empty elements emit no
4444            // END_ELEMENT event — the second Read returns EOF directly.
4445            assert_eq!(xmlTextReaderRead(reader), 0);
4446            assert_eq!((*reader).ReadState(), ReadState::EOF);
4447
4448            free_reader(reader);
4449        }
4450    }
4451
4452    /// Verifies reader API entry points return error indicators for a NULL
4453    /// reader pointer.
4454    ///
4455    /// # Safety
4456    ///
4457    /// - NULL is passed only to reader API entry points that accept NULL and
4458    ///   return error indicators without dereferencing the pointer.
4459    #[test]
4460    fn test_null_reader_returns_error() {
4461        unsafe {
4462            assert_eq!(xmlTextReaderRead(ptr::null_mut()), -1);
4463            assert_eq!(xmlTextReaderDepth(ptr::null_mut()), -1);
4464            assert_eq!(xmlTextReaderNodeType(ptr::null_mut()), -1);
4465            assert!(xmlTextReaderName(ptr::null_mut()).is_null());
4466            assert!(xmlTextReaderValue(ptr::null_mut()).is_null());
4467            assert_eq!(xmlTextReaderHasValue(ptr::null_mut()), 0);
4468            assert_eq!(xmlTextReaderIsEmptyElement(ptr::null_mut()), 0);
4469            assert_eq!(
4470                xmlTextReaderReadState(ptr::null_mut()),
4471                ReadState::ERROR as c_int
4472            );
4473        }
4474    }
4475
4476    /// Verifies freeing a NULL reader does not crash.
4477    ///
4478    /// # Safety
4479    ///
4480    /// - `xmlFreeTextReader` must tolerate a NULL pointer without
4481    ///   dereferencing it.
4482    #[test]
4483    fn test_xmlFreeTextReader_null() {
4484        unsafe {
4485            // Should not crash.
4486            xmlFreeTextReader(ptr::null_mut());
4487        }
4488    }
4489
4490    /// Verifies `xmlTextReaderName` and `xmlTextReaderValue` results.
4491    ///
4492    /// # Safety
4493    ///
4494    /// - The `reader` pointer is asserted non-NULL before use and freed
4495    ///   exactly once with `free_reader`.
4496    /// - `xmlTextReaderName` and `xmlTextReaderValue` return heap strings
4497    ///   allocated via `xml_strdup`; each non-NULL result is freed exactly
4498    ///   once with `xmlFreeImpl`.
4499    #[test]
4500    fn test_reader_name_and_value() {
4501        unsafe {
4502            let reader = create_reader("<root>hello</root>");
4503            assert!(!reader.is_null());
4504
4505            // Read root element.
4506            assert_eq!(xmlTextReaderRead(reader), 1);
4507            let name = xmlTextReaderName(reader);
4508            assert!(!name.is_null());
4509            assert_eq!(xmlstr_to_string(name), "root");
4510            xmlFreeImpl(name as *mut c_void);
4511
4512            assert_eq!(xmlTextReaderHasValue(reader), 0);
4513
4514            // Read text node.
4515            assert_eq!(xmlTextReaderRead(reader), 1);
4516            assert_eq!((*reader).NodeType(), ReaderNodeType::TEXT);
4517            assert_eq!((*reader).HasValue(), 1);
4518
4519            let val = xmlTextReaderValue(reader);
4520            assert!(!val.is_null());
4521            assert_eq!(xmlstr_to_string(val), "hello");
4522            xmlFreeImpl(val as *mut c_void);
4523
4524            free_reader(reader);
4525        }
4526    }
4527
4528    /// Verifies an empty element reports no END_ELEMENT event.
4529    ///
4530    /// # Safety
4531    ///
4532    /// - The reader is created from a static string literal that stays alive
4533    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
4534    ///   before it is dereferenced and is freed exactly once with
4535    ///   `free_reader`.
4536    #[test]
4537    fn test_empty_element() {
4538        unsafe {
4539            let reader = create_reader("<empty/>");
4540            assert!(!reader.is_null());
4541
4542            assert_eq!(xmlTextReaderRead(reader), 1);
4543            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
4544            assert_eq!((*reader).IsEmptyElement(), 1);
4545            assert_eq!((*reader).HasAttributes(), 0);
4546            assert_eq!((*reader).AttributeCount(), 0);
4547
4548            // UPSTREAM-PARITY (oracle-verified 2.15.3): empty elements emit
4549            // NO END_ELEMENT event; the next Read returns EOF.
4550            assert_eq!(xmlTextReaderRead(reader), 0);
4551            assert_eq!((*reader).ReadState(), ReadState::EOF);
4552
4553            free_reader(reader);
4554        }
4555    }
4556
4557    /// Verifies attribute counting on an element with two attributes.
4558    ///
4559    /// # Safety
4560    ///
4561    /// - The reader is created from a static string literal that stays alive
4562    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
4563    ///   before it is dereferenced and is freed exactly once with
4564    ///   `free_reader`.
4565    #[test]
4566    fn test_element_with_attributes() {
4567        unsafe {
4568            let reader = create_reader(r#"<root a="1" b="2"/>"#);
4569            assert!(!reader.is_null());
4570
4571            assert_eq!(xmlTextReaderRead(reader), 1);
4572            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
4573            assert_eq!((*reader).HasAttributes(), 1);
4574
4575            // We know the attribute count if we've built the events properly.
4576            // The count_attributes checks the element's properties list.
4577            let attrs = xmlTextReaderAttributeCount(reader);
4578            assert_eq!(attrs, 2);
4579
4580            free_reader(reader);
4581        }
4582    }
4583
4584    /// Verifies attribute navigation (first, next, by name, by index).
4585    ///
4586    /// # Safety
4587    ///
4588    /// - The reader is created from a static string literal that stays alive
4589    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
4590    ///   before it is dereferenced and is freed exactly once with
4591    ///   `free_reader`.
4592    /// - The pointers returned by `xmlTextReaderConstName` and
4593    ///   `xmlTextReaderConstValue` are borrowed from the reader and are not
4594    ///   freed by the test.
4595    #[test]
4596    fn test_attribute_navigation() {
4597        unsafe {
4598            let reader = create_reader(r#"<root a="1" b="2"></root>"#);
4599            assert!(!reader.is_null());
4600
4601            // Position on root element.
4602            assert_eq!(xmlTextReaderRead(reader), 1);
4603            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
4604
4605            // Move to first attribute.
4606            assert_eq!(xmlTextReaderMoveToFirstAttribute(reader), 1);
4607            assert_eq!((*reader).NodeType(), ReaderNodeType::ATTRIBUTE);
4608
4609            let name = xmlTextReaderConstName(reader);
4610            assert!(!name.is_null());
4611            assert_eq!(xmlstr_to_bytes(name), b"a");
4612
4613            let val = xmlTextReaderConstValue(reader);
4614            assert!(!val.is_null());
4615            assert_eq!(xmlstr_to_bytes(val), b"1");
4616
4617            // Move to next attribute.
4618            assert_eq!(xmlTextReaderMoveToNextAttribute(reader), 1);
4619            let name = xmlTextReaderConstName(reader);
4620            assert!(!name.is_null());
4621            assert_eq!(xmlstr_to_bytes(name), b"b");
4622            let val = xmlTextReaderConstValue(reader);
4623            assert!(!val.is_null());
4624            assert_eq!(xmlstr_to_bytes(val), b"2");
4625
4626            // No more attributes.
4627            assert_eq!(xmlTextReaderMoveToNextAttribute(reader), 0);
4628
4629            // Move back to element.
4630            assert_eq!(xmlTextReaderMoveToElement(reader), 1);
4631            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
4632
4633            // Move to attribute by name.
4634            assert_eq!(
4635                xmlTextReaderMoveToAttribute(reader, b"a\0" as *const u8 as *const xmlChar),
4636                1
4637            );
4638            assert_eq!((*reader).NodeType(), ReaderNodeType::ATTRIBUTE);
4639
4640            // Move to attribute by index.
4641            assert_eq!(xmlTextReaderMoveToElement(reader), 1);
4642            assert_eq!(xmlTextReaderMoveToAttributeNo(reader, 1), 1);
4643            assert_eq!((*reader).NodeType(), ReaderNodeType::ATTRIBUTE);
4644
4645            free_reader(reader);
4646        }
4647    }
4648
4649    /// Verifies attribute lookup by name and by index.
4650    ///
4651    /// # Safety
4652    ///
4653    /// - The `reader` pointer is asserted non-NULL before use and freed
4654    ///   exactly once with `free_reader`.
4655    /// - `xmlTextReaderGetAttribute` and `xmlTextReaderGetAttributeNo` return
4656    ///   heap strings; each non-NULL result is freed exactly once with
4657    ///   `xmlFreeImpl`.
4658    #[test]
4659    fn test_get_attribute() {
4660        unsafe {
4661            let reader = create_reader(r#"<root a="hello" b="world"/>"#);
4662            assert!(!reader.is_null());
4663
4664            assert_eq!(xmlTextReaderRead(reader), 1);
4665
4666            // Get attribute by name.
4667            let val = xmlTextReaderGetAttribute(reader, b"a\0" as *const u8 as *const xmlChar);
4668            assert!(!val.is_null());
4669            assert_eq!(xmlstr_to_bytes(val), b"hello");
4670            xmlFreeImpl(val as *mut c_void);
4671
4672            let val = xmlTextReaderGetAttribute(reader, b"b\0" as *const u8 as *const xmlChar);
4673            assert!(!val.is_null());
4674            assert_eq!(xmlstr_to_bytes(val), b"world");
4675            xmlFreeImpl(val as *mut c_void);
4676
4677            // Non-existent attribute.
4678            let val = xmlTextReaderGetAttribute(reader, b"c\0" as *const u8 as *const xmlChar);
4679            assert!(val.is_null());
4680
4681            // Get attribute by index.
4682            let val = xmlTextReaderGetAttributeNo(reader, 0);
4683            assert!(!val.is_null());
4684            assert_eq!(xmlstr_to_bytes(val), b"hello");
4685            xmlFreeImpl(val as *mut c_void);
4686
4687            let val = xmlTextReaderGetAttributeNo(reader, 1);
4688            assert!(!val.is_null());
4689            assert_eq!(xmlstr_to_bytes(val), b"world");
4690            xmlFreeImpl(val as *mut c_void);
4691
4692            let val = xmlTextReaderGetAttributeNo(reader, 2);
4693            assert!(val.is_null());
4694
4695            free_reader(reader);
4696        }
4697    }
4698
4699    /// Verifies depth tracking across nested elements.
4700    ///
4701    /// # Safety
4702    ///
4703    /// - The reader is created from a static string literal that stays alive
4704    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
4705    ///   before it is dereferenced and is freed exactly once with
4706    ///   `free_reader`.
4707    #[test]
4708    fn test_depth_tracking() {
4709        unsafe {
4710            let reader = create_reader("<a><b><c/></b></a>");
4711            assert!(!reader.is_null());
4712
4713            let nodes = collect_nodes(reader);
4714            // UPSTREAM-PARITY (oracle-verified 2.15.3): empty elements emit no
4715            // END_ELEMENT event.
4716            // ELEMENT a (0), ELEMENT b (1), ELEMENT c (2),
4717            // END_ELEMENT b (1), END_ELEMENT a (0)
4718            assert_eq!(nodes.len(), 5);
4719            assert_eq!(nodes[0].2, 0); // a depth 0
4720            assert_eq!(nodes[1].2, 1); // b depth 1
4721            assert_eq!(nodes[2].2, 2); // c depth 2
4722            assert_eq!(nodes[3].2, 1); // END b depth 1
4723            assert_eq!(nodes[4].2, 0); // END a depth 0
4724
4725            free_reader(reader);
4726        }
4727    }
4728
4729    /// Verifies traversal of multiple sibling elements.
4730    ///
4731    /// # Safety
4732    ///
4733    /// - The reader is created from a static string literal that stays alive
4734    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
4735    ///   before it is dereferenced and is freed exactly once with
4736    ///   `free_reader`.
4737    #[test]
4738    fn test_multiple_siblings() {
4739        unsafe {
4740            let reader = create_reader("<root><a>A</a><b>B</b><c>C</c></root>");
4741            assert!(!reader.is_null());
4742
4743            let nodes = collect_nodes(reader);
4744            // ELEMENT root(0), ELEMENT a(1), TEXT(2), END a(1),
4745            // ELEMENT b(1), TEXT(2), END b(1),
4746            // ELEMENT c(1), TEXT(2), END c(1),
4747            // END root(0)
4748            assert_eq!(nodes.len(), 11);
4749
4750            // Check the sibling elements.
4751            assert_eq!(nodes[1], (ReaderNodeType::ELEMENT, "a".to_string(), 1));
4752            assert_eq!(nodes[4], (ReaderNodeType::ELEMENT, "b".to_string(), 1));
4753            assert_eq!(nodes[7], (ReaderNodeType::ELEMENT, "c".to_string(), 1));
4754
4755            free_reader(reader);
4756        }
4757    }
4758
4759    /// Verifies `xmlTextReaderNext` semantics against the upstream streaming
4760    /// reader (xmlreader.c xmlTextReaderNext): from a NON-element node Next
4761    /// degrades to a plain Read — one step forward in document order, which
4762    /// may land on the parent's END_ELEMENT — and only an element START is
4763    /// skipped (subtree + END events) to the next sibling. On exhaustion the
4764    /// cursor is cleared (EOF).
4765    ///
4766    /// # Safety
4767    ///
4768    /// - The reader is created from a static string literal that stays alive
4769    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
4770    ///   before it is dereferenced and is freed exactly once with
4771    ///   `free_reader`.
4772    /// - The `name` field read through `(*reader)` is borrowed from the
4773    ///   reader and is not freed.
4774    #[test]
4775    fn test_next_skip_to_sibling() {
4776        unsafe {
4777            let reader = create_reader("<root><a>A</a><b>B</b><c>C</c></root>");
4778            assert!(!reader.is_null());
4779
4780            // Read to first node (root element).
4781            assert_eq!(xmlTextReaderRead(reader), 1);
4782            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
4783
4784            // Read to a.
4785            assert_eq!(xmlTextReaderRead(reader), 1);
4786            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
4787            assert_eq!(xmlstr_to_string((*reader).name as *const xmlChar), "a");
4788
4789            // Read to text of a.
4790            assert_eq!(xmlTextReaderRead(reader), 1);
4791            assert_eq!((*reader).NodeType(), ReaderNodeType::TEXT);
4792
4793            // Next from a TEXT node is a plain Read: it lands on the END of
4794            // the parent element (upstream xmlTextReaderNext, oracle-verified
4795            // via XMLReader 010/next_basic).
4796            assert_eq!(xmlTextReaderNext(reader), 1);
4797            assert_eq!((*reader).NodeType(), ReaderNodeType::END_ELEMENT);
4798            assert_eq!(xmlstr_to_string((*reader).name as *const xmlChar), "a");
4799
4800            // Next again from the END event — a single step to element b.
4801            assert_eq!(xmlTextReaderNext(reader), 1);
4802            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
4803            assert_eq!(xmlstr_to_string((*reader).name as *const xmlChar), "b");
4804
4805            // Next from the b element start skips its subtree to c.
4806            assert_eq!(xmlTextReaderNext(reader), 1);
4807            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
4808            assert_eq!(xmlstr_to_string((*reader).name as *const xmlChar), "c");
4809
4810            // Next again — the traversal is exhausted and the cursor clears.
4811            assert_eq!(xmlTextReaderNext(reader), 0);
4812            assert_eq!((*reader).NodeType(), ReaderNodeType::NONE);
4813
4814            free_reader(reader);
4815        }
4816    }
4817
4818    /// Verifies comment and processing-instruction nodes are reported.
4819    ///
4820    /// # Safety
4821    ///
4822    /// - The static NUL-terminated `xml` buffer stays valid for the
4823    ///   `xmlReaderForMemory` call; the returned `reader` is asserted non-NULL
4824    ///   before use and is freed exactly once with `free_reader`.
4825    #[test]
4826    fn test_comment_and_pi_nodes() {
4827        unsafe {
4828            let xml = b"<?pi target?><root><!-- comment -->text</root>\0";
4829            let reader = xmlReaderForMemory(
4830                xml.as_ptr() as *const c_char,
4831                (xml.len() - 1) as c_int,
4832                ptr::null(),
4833                ptr::null(),
4834                0,
4835            );
4836            assert!(!reader.is_null());
4837
4838            let nodes = collect_nodes(reader);
4839            // PI, ELEMENT root, COMMENT, TEXT, END_ELEMENT root
4840            // Note: PI appears as PROCESSING_INSTRUCTION node.
4841            assert!(!nodes.is_empty(), "no nodes collected: {:?}", nodes);
4842
4843            // Check PI.
4844            assert_eq!(
4845                nodes[0].0,
4846                ReaderNodeType::PROCESSING_INSTRUCTION,
4847                "expected PI at nodes[0], got {:?} name={}",
4848                nodes[0].0,
4849                nodes[0].1
4850            );
4851            assert_eq!(
4852                nodes[0].0,
4853                ReaderNodeType::PROCESSING_INSTRUCTION,
4854                "expected PI at nodes[0], got {:?} name={}",
4855                nodes[0].0,
4856                nodes[0].1
4857            );
4858
4859            // Check root element.
4860            let root_idx = nodes
4861                .iter()
4862                .position(|(t, n, _)| *t == ReaderNodeType::ELEMENT && n == "root");
4863            assert!(
4864                root_idx.is_some(),
4865                "no ELEMENT root found in nodes: {:?}",
4866                nodes
4867                    .iter()
4868                    .map(|(t, n, _)| format!("{:?}:{}", t, n))
4869                    .collect::<Vec<_>>()
4870            );
4871
4872            // Check comment.
4873            let comment_idx = nodes
4874                .iter()
4875                .position(|(t, _, _)| *t == ReaderNodeType::COMMENT);
4876            assert!(comment_idx.is_some(), "no COMMENT found");
4877
4878            // Check text.
4879            let text_idx = nodes
4880                .iter()
4881                .position(|(t, _, _)| *t == ReaderNodeType::TEXT);
4882            assert!(text_idx.is_some(), "no TEXT found");
4883
4884            free_reader(reader);
4885        }
4886    }
4887
4888    /// Verifies `xmlTextReaderLocalName` returns the local name.
4889    ///
4890    /// # Safety
4891    ///
4892    /// - The `reader` pointer is asserted non-NULL before use and freed
4893    ///   exactly once with `free_reader`.
4894    /// - `xmlTextReaderLocalName` returns a heap string that is freed exactly
4895    ///   once with `xmlFreeImpl`.
4896    #[test]
4897    fn test_local_name() {
4898        unsafe {
4899            // We need a namespace-aware element. For now, test without namespace.
4900            let reader = create_reader("<root/>");
4901            assert!(!reader.is_null());
4902
4903            assert_eq!(xmlTextReaderRead(reader), 1);
4904            let local = xmlTextReaderLocalName(reader);
4905            assert!(!local.is_null());
4906            assert_eq!(xmlstr_to_bytes(local), b"root");
4907            xmlFreeImpl(local as *mut c_void);
4908
4909            free_reader(reader);
4910        }
4911    }
4912
4913    /// Verifies the base URI is NULL for memory-created readers.
4914    ///
4915    /// # Safety
4916    ///
4917    /// - The reader is created from a static string literal that stays alive
4918    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
4919    ///   before it is dereferenced and is freed exactly once with
4920    ///   `free_reader`.
4921    #[test]
4922    fn test_base_uri() {
4923        unsafe {
4924            let reader = create_reader("<root/>");
4925            assert!(!reader.is_null());
4926
4927            assert_eq!(xmlTextReaderRead(reader), 1);
4928            // Base URI should be NULL for memory-created readers.
4929            let uri = xmlTextReaderBaseUri(reader);
4930            assert!(uri.is_null());
4931
4932            free_reader(reader);
4933        }
4934    }
4935
4936    /// Verifies namespace prefix lookup on the reader.
4937    ///
4938    /// # Safety
4939    ///
4940    /// - The `reader` pointer is asserted non-NULL before use and freed
4941    ///   exactly once with `free_reader`.
4942    /// - `xmlTextReaderLookupNamespace` returns a heap string; the non-NULL
4943    ///   result is freed exactly once with `xmlFreeImpl`.
4944    #[test]
4945    fn test_lookup_namespace() {
4946        unsafe {
4947            let reader = create_reader(r#"<root xmlns:ns="http://example.com"><ns:child/></root>"#);
4948            assert!(!reader.is_null());
4949
4950            // Read to root.
4951            assert_eq!(xmlTextReaderRead(reader), 1);
4952            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
4953
4954            // Read to child (ns:child).
4955            assert_eq!(xmlTextReaderRead(reader), 1);
4956
4957            // Lookup the "ns" prefix.
4958            let uri = xmlTextReaderLookupNamespace(reader, b"ns\0" as *const u8 as *const xmlChar);
4959            assert!(!uri.is_null());
4960            assert_eq!(xmlstr_to_bytes(uri), b"http://example.com");
4961            xmlFreeImpl(uri as *mut c_void);
4962
4963            // Lookup default namespace (NULL prefix).
4964            let uri = xmlTextReaderLookupNamespace(reader, ptr::null());
4965            assert!(uri.is_null());
4966
4967            // Lookup non-existent prefix.
4968            let uri = xmlTextReaderLookupNamespace(
4969                reader,
4970                b"nonexistent\0" as *const u8 as *const xmlChar,
4971            );
4972            assert!(uri.is_null());
4973
4974            free_reader(reader);
4975        }
4976    }
4977
4978    /// Verifies parser property get/set round trips and error codes.
4979    ///
4980    /// # Safety
4981    ///
4982    /// - The reader is created from a static string literal that stays alive
4983    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
4984    ///   before it is dereferenced and is freed exactly once with
4985    ///   `free_reader`.
4986    #[test]
4987    fn test_parser_properties() {
4988        unsafe {
4989            let reader = create_reader("<root/>");
4990            assert!(!reader.is_null());
4991
4992            // Get default properties.
4993            assert_eq!(xmlTextReaderGetParserProp(reader, 1), 0); // LOADDTD
4994            assert_eq!(xmlTextReaderGetParserProp(reader, 2), 0); // DEFAULTATTRS
4995            assert_eq!(xmlTextReaderGetParserProp(reader, 3), 0); // VALIDATE
4996            assert_eq!(xmlTextReaderGetParserProp(reader, 4), 0); // SUBST_ENTITIES
4997
4998            // Set and verify.
4999            assert_eq!(xmlTextReaderSetParserProp(reader, 1, 1), 0);
5000            assert_eq!(xmlTextReaderGetParserProp(reader, 1), 1);
5001
5002            assert_eq!(xmlTextReaderSetParserProp(reader, 4, 1), 0);
5003            assert_eq!(xmlTextReaderGetParserProp(reader, 4), 1);
5004
5005            // Invalid property.
5006            assert_eq!(xmlTextReaderGetParserProp(reader, 99), -1);
5007            assert_eq!(xmlTextReaderSetParserProp(reader, 99, 1), -1);
5008
5009            free_reader(reader);
5010        }
5011    }
5012
5013    /// Verifies the current document is available after the first read.
5014    ///
5015    /// # Safety
5016    ///
5017    /// - The reader is created from a static string literal that stays alive
5018    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
5019    ///   before it is dereferenced and is freed exactly once with
5020    ///   `free_reader`.
5021    #[test]
5022    fn test_current_doc() {
5023        unsafe {
5024            let reader = create_reader("<root/>");
5025            assert!(!reader.is_null());
5026
5027            // Before reading, doc should be null.
5028            assert!((*reader).CurrentDoc().is_null());
5029
5030            // After reading, doc should be available.
5031            assert_eq!(xmlTextReaderRead(reader), 1);
5032            let doc = xmlTextReaderCurrentDoc(reader);
5033            assert!(!doc.is_null());
5034
5035            free_reader(reader);
5036        }
5037    }
5038
5039    /// Verifies a reader can be freed after reading to EOF.
5040    ///
5041    /// # Safety
5042    ///
5043    /// - The reader is created from a static string literal that stays alive
5044    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
5045    ///   before it is dereferenced and is freed exactly once with
5046    ///   `free_reader`.
5047    #[test]
5048    fn test_free_reader_after_read() {
5049        unsafe {
5050            let reader = create_reader("<root><child/></root>");
5051            assert!(!reader.is_null());
5052
5053            // Read through the document.
5054            while xmlTextReaderRead(reader) > 0 {}
5055            assert_eq!((*reader).ReadState(), ReadState::EOF);
5056
5057            // Free should not crash.
5058            free_reader(reader);
5059        }
5060    }
5061
5062    /// Verifies a NULL buffer with a non-zero size is rejected.
5063    ///
5064    /// # Safety
5065    ///
5066    /// - A NULL buffer with a non-zero size must be rejected by
5067    ///   `xmlReaderForMemory` without reading any memory.
5068    #[test]
5069    fn test_reader_for_memory_null_buffer() {
5070        unsafe {
5071            let reader = xmlReaderForMemory(ptr::null(), 10, ptr::null(), ptr::null(), 0);
5072            assert!(reader.is_null());
5073        }
5074    }
5075
5076    /// Verifies a zero size makes `xmlReaderForMemory` return NULL.
5077    ///
5078    /// # Safety
5079    ///
5080    /// - The static buffer is not read: a size of 0 must make
5081    ///   `xmlReaderForMemory` return a NULL reader.
5082    #[test]
5083    fn test_reader_for_memory_empty_size() {
5084        unsafe {
5085            let data = b"<root/>";
5086            let reader = xmlReaderForMemory(
5087                data.as_ptr() as *const c_char,
5088                0,
5089                ptr::null(),
5090                ptr::null(),
5091                0,
5092            );
5093            assert!(reader.is_null());
5094        }
5095    }
5096
5097    /// Verifies a nonexistent file yields a NULL reader.
5098    ///
5099    /// # Safety
5100    ///
5101    /// - The static NUL-terminated filename stays valid for the
5102    ///   `xmlReaderForFile` call; a nonexistent file must yield a NULL reader
5103    ///   without reading.
5104    #[test]
5105    fn test_reader_for_file_not_found() {
5106        unsafe {
5107            let filename = b"/nonexistent/file.xml\0" as *const u8 as *const c_char;
5108            let reader = xmlReaderForFile(filename, ptr::null(), 0);
5109            assert!(reader.is_null());
5110        }
5111    }
5112
5113    /// Verifies const name and value pointers borrowed from the reader.
5114    ///
5115    /// # Safety
5116    ///
5117    /// - The `reader` pointer is asserted non-NULL before use and freed
5118    ///   exactly once with `free_reader`.
5119    /// - The pointers returned by `xmlTextReaderConstName` and
5120    ///   `xmlTextReaderConstValue` are borrowed from the reader and must not
5121    ///   be freed by the caller.
5122    #[test]
5123    fn test_const_name_and_value() {
5124        unsafe {
5125            let reader = create_reader("<root>text</root>");
5126            assert!(!reader.is_null());
5127
5128            // Root element.
5129            assert_eq!(xmlTextReaderRead(reader), 1);
5130            let cname = xmlTextReaderConstName(reader);
5131            assert!(!cname.is_null());
5132            assert_eq!(xmlstr_to_bytes(cname), b"root");
5133
5134            // Text node.
5135            assert_eq!(xmlTextReaderRead(reader), 1);
5136            let cval = xmlTextReaderConstValue(reader);
5137            assert!(!cval.is_null());
5138            assert_eq!(xmlstr_to_bytes(cval), b"text");
5139
5140            free_reader(reader);
5141        }
5142    }
5143
5144    /// Reads a complex nested document and counts node types.
5145    ///
5146    /// # Safety
5147    ///
5148    /// - The reader is created from a static string literal that stays alive
5149    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
5150    ///   before it is dereferenced and is freed exactly once with
5151    ///   `free_reader`.
5152    #[test]
5153    fn test_complex_nested_document() {
5154        unsafe {
5155            let xml = r#"<?xml version="1.0"?>
5156<library>
5157  <book id="1">
5158    <title>XML Fundamentals</title>
5159    <author>John Doe</author>
5160  </book>
5161  <book id="2">
5162    <title>XSLT Recipes</title>
5163    <author>Jane Smith</author>
5164  </book>
5165</library>"#;
5166
5167            let reader = create_reader(xml);
5168            assert!(!reader.is_null());
5169
5170            let mut element_count = 0;
5171            let mut end_element_count = 0;
5172            let mut text_count = 0;
5173            let mut pi_count = 0;
5174
5175            loop {
5176                let ret = xmlTextReaderRead(reader);
5177                if ret <= 0 {
5178                    break;
5179                }
5180                match (*reader).NodeType() {
5181                    ReaderNodeType::ELEMENT => element_count += 1,
5182                    ReaderNodeType::END_ELEMENT => end_element_count += 1,
5183                    ReaderNodeType::TEXT => text_count += 1,
5184                    ReaderNodeType::PROCESSING_INSTRUCTION => pi_count += 1,
5185                    _ => {}
5186                }
5187            }
5188
5189            // Elements: library, book(2), title(2), author(2) = 7
5190            assert_eq!(element_count, 7);
5191            // End elements: same count as elements
5192            assert_eq!(end_element_count, 7);
5193            // Text nodes: one per title and author = 4
5194            assert_eq!(text_count, 4);
5195            // UPSTREAM-PARITY: XML declaration (<?xml ...?>) is NOT stored as
5196            // a PI node in the tree. It is consumed by the parser and stored
5197            // in the document's version/encoding fields. Only <?pi ...?> nodes
5198            // (processing instructions) appear as XML_PI_NODE in the tree.
5199            assert_eq!(pi_count, 0);
5200
5201            free_reader(reader);
5202        }
5203    }
5204
5205    /// Verifies `xmlTextReaderSetup` reinitializes a used reader.
5206    ///
5207    /// # Safety
5208    ///
5209    /// - The `reader` pointer is asserted non-NULL before use and freed
5210    ///   exactly once with `free_reader`.
5211    /// - `xmlTextReaderSetup` with a NULL input must reset the reader without
5212    ///   dereferencing the NULL input.
5213    #[test]
5214    fn test_setup_reinitialize() {
5215        unsafe {
5216            let reader = create_reader("<root/>");
5217            assert!(!reader.is_null());
5218
5219            // Read through.
5220            assert_eq!(xmlTextReaderRead(reader), 1);
5221            assert_eq!((*reader).ReadState(), ReadState::READING);
5222
5223            // Setup with new input (simulate re-initialization).
5224            // For this test, we just verify the setup function exists and
5225            // handles a NULL input gracefully (resetting the reader).
5226            assert_eq!(
5227                xmlTextReaderSetup(reader, ptr::null_mut(), ptr::null(), ptr::null(), 0),
5228                0
5229            );
5230            assert_eq!((*reader).ReadState(), ReadState::INITIALIZED);
5231
5232            free_reader(reader);
5233        }
5234    }
5235
5236    /// Verifies attribute queries on non-element and attribute-less nodes.
5237    ///
5238    /// # Safety
5239    ///
5240    /// - The reader is created from a static string literal that stays alive
5241    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
5242    ///   before it is dereferenced and is freed exactly once with
5243    ///   `free_reader`.
5244    #[test]
5245    fn test_has_attributes_on_non_element() {
5246        unsafe {
5247            let reader = create_reader("<root>text</root>");
5248            assert!(!reader.is_null());
5249
5250            // Position on text node.
5251            assert_eq!(xmlTextReaderRead(reader), 1); // root element
5252            assert_eq!((*reader).HasAttributes(), 0); // 0 attributes on root
5253            assert_eq!(xmlTextReaderRead(reader), 1); // text
5254            assert_eq!((*reader).HasAttributes(), 0);
5255
5256            free_reader(reader);
5257        }
5258    }
5259
5260    /// Verifies `xmlTextReaderPrev` fails after EOF.
5261    ///
5262    /// # Safety
5263    ///
5264    /// - The reader is created from a static string literal that stays alive
5265    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
5266    ///   before it is dereferenced and is freed exactly once with
5267    ///   `free_reader`.
5268    #[test]
5269    fn test_prev_sibling() {
5270        unsafe {
5271            let reader = create_reader("<root><a/><b/><c/></root>");
5272            assert!(!reader.is_null());
5273
5274            // Read through the document.
5275            while xmlTextReaderRead(reader) > 0 {
5276                // Skip to END_ELEMENT root or beyond.
5277            }
5278
5279            // Can't go prev after EOF.
5280            assert_eq!(xmlTextReaderPrev(reader), -1);
5281
5282            free_reader(reader);
5283        }
5284    }
5285
5286    /// Verifies moving to an attribute index fails on an element without
5287    /// attributes.
5288    ///
5289    /// # Safety
5290    ///
5291    /// - The reader is created from a static string literal that stays alive
5292    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
5293    ///   before it is dereferenced and is freed exactly once with
5294    ///   `free_reader`.
5295    #[test]
5296    fn test_move_to_attribute_no_not_on_element() {
5297        unsafe {
5298            let reader = create_reader("<root>text</root>");
5299            assert!(!reader.is_null());
5300
5301            assert_eq!(xmlTextReaderRead(reader), 1); // root
5302            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
5303
5304            // Move to non-existent attribute index.
5305            assert_eq!(xmlTextReaderMoveToAttributeNo(reader, 0), 0);
5306
5307            free_reader(reader);
5308        }
5309    }
5310
5311    /// Verifies namespaced attribute lookup with a NULL namespace URI.
5312    ///
5313    /// # Safety
5314    ///
5315    /// - The `reader` pointer is asserted non-NULL before use and freed
5316    ///   exactly once with `free_reader`.
5317    /// - `xmlTextReaderGetAttributeNs` returns a heap string; the non-NULL
5318    ///   result is freed exactly once with `xmlFreeImpl`.
5319    #[test]
5320    fn test_get_attribute_ns() {
5321        unsafe {
5322            let reader = create_reader(r#"<root a="1" b="2"/>"#);
5323            assert!(!reader.is_null());
5324
5325            assert_eq!(xmlTextReaderRead(reader), 1);
5326
5327            // Get attribute by local name only (namespaceURI is NULL).
5328            let val = xmlTextReaderGetAttributeNs(
5329                reader,
5330                b"a\0" as *const u8 as *const xmlChar,
5331                ptr::null(),
5332            );
5333            assert!(!val.is_null());
5334            assert_eq!(xmlstr_to_bytes(val), b"1");
5335            xmlFreeImpl(val as *mut c_void);
5336
5337            free_reader(reader);
5338        }
5339    }
5340
5341    /// Verifies mixed content (text and elements) event ordering.
5342    ///
5343    /// # Safety
5344    ///
5345    /// - The reader is created from a static string literal that stays alive
5346    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
5347    ///   before it is dereferenced and is freed exactly once with
5348    ///   `free_reader`.
5349    #[test]
5350    fn test_mixed_content() {
5351        unsafe {
5352            let reader = create_reader("<root>before<child/>after</root>");
5353            assert!(!reader.is_null());
5354
5355            let nodes = collect_nodes(reader);
5356            // UPSTREAM-PARITY (oracle-verified 2.15.3): empty elements emit no
5357            // END_ELEMENT event.
5358            // ELEMENT root(0), TEXT "before"(1), ELEMENT child(1),
5359            // TEXT "after"(1), END_ELEMENT root(0)
5360            assert_eq!(nodes.len(), 5);
5361            assert_eq!(nodes[0], (ReaderNodeType::ELEMENT, "root".to_string(), 0));
5362            assert_eq!(nodes[1].0, ReaderNodeType::TEXT);
5363            assert_eq!(nodes[2], (ReaderNodeType::ELEMENT, "child".to_string(), 1));
5364            assert_eq!(nodes[3].0, ReaderNodeType::TEXT);
5365
5366            free_reader(reader);
5367        }
5368    }
5369
5370    /// Verifies malformed XML makes reading fail gracefully.
5371    ///
5372    /// # Safety
5373    ///
5374    /// - The static `data` buffer is valid for the 7-byte `xmlReaderForMemory`
5375    ///   read; the returned `reader` is asserted non-NULL before being read
5376    ///   and is freed exactly once with `free_reader`.
5377    #[test]
5378    fn test_error_handling_invalid_xml() {
5379        unsafe {
5380            // Malformed XML: `<root><` is a completed `<root>` start tag
5381            // followed by a lone `<` at end of input — an invalid element
5382            // name. UPSTREAM-PARITY (oracle-verified 2.15.3 xmlreader.c):
5383            // the first xmlTextReaderRead delivers the completed `<root>`
5384            // element node (returns 1); the second read runs past it and
5385            // surfaces the trailing "StartTag: invalid element name"
5386            // diagnostic (returns -1).
5387            let data = b"<root><\0" as *const u8 as *const c_char;
5388            let reader = xmlReaderForMemory(data, 7, ptr::null(), ptr::null(), 0);
5389            assert!(!reader.is_null());
5390
5391            // First read delivers the `<root>` element node.
5392            let ret = xmlTextReaderRead(reader);
5393            assert_eq!(ret, 1);
5394
5395            // Second read fails on the trailing invalid element name.
5396            let ret = xmlTextReaderRead(reader);
5397            assert_eq!(ret, -1);
5398
5399            free_reader(reader);
5400        }
5401    }
5402
5403    /// Verifies parser options are stored and honored by the reader.
5404    ///
5405    /// # Safety
5406    ///
5407    /// - The static NUL-terminated `data` buffer stays valid for the
5408    ///   `xmlReaderForMemory` call; the returned `reader` is asserted non-NULL
5409    ///   before dereferencing its `options` field and before
5410    ///   `xmlTextReaderRead`, and is freed exactly once with `free_reader`.
5411    #[test]
5412    fn test_reader_with_options() {
5413        unsafe {
5414            let data = b"<root/>\0" as *const u8 as *const c_char;
5415            let reader = xmlReaderForMemory(
5416                data,
5417                7,
5418                ptr::null(),
5419                ptr::null(),
5420                XML_PARSE_NOENT | XML_PARSE_DTDLOAD,
5421            );
5422            assert!(!reader.is_null());
5423
5424            // Verify options were set.
5425            assert_eq!((*reader).options & XML_PARSE_NOENT, XML_PARSE_NOENT);
5426            assert_eq!((*reader).options & XML_PARSE_DTDLOAD, XML_PARSE_DTDLOAD);
5427
5428            assert_eq!(xmlTextReaderRead(reader), 1);
5429            free_reader(reader);
5430        }
5431    }
5432
5433    /// Verifies reading a document from an open file descriptor.
5434    ///
5435    /// # Safety
5436    ///
5437    /// - The descriptor returned by `libc::open` must be a valid open
5438    ///   descriptor passed to `xmlReaderForFd`, which reads from it; the
5439    ///   returned `reader` is asserted non-NULL before use, freed with
5440    ///   `free_reader`, and the descriptor is closed afterwards.
5441    #[test]
5442    fn test_reader_for_fd() {
5443        unsafe {
5444            // Create a temp file and test xmlReaderForFd.
5445            let tmp_path = "/tmp/libxml_rs_test_reader_fd.xml";
5446            let tmp_cstr = std::ffi::CString::new(tmp_path).unwrap();
5447            let content = b"<root><data/></root>";
5448            let fd = libc::open(
5449                tmp_cstr.as_ptr(),
5450                libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC,
5451                0o644,
5452            );
5453            assert!(fd >= 0);
5454            libc::write(fd, content.as_ptr() as *const c_void, content.len());
5455            libc::close(fd);
5456
5457            // Open for reading.
5458            let fd = libc::open(tmp_cstr.as_ptr(), libc::O_RDONLY, 0);
5459            assert!(fd >= 0);
5460
5461            let reader = xmlReaderForFd(fd, ptr::null(), ptr::null(), 0);
5462            assert!(!reader.is_null());
5463
5464            let nodes = collect_nodes(reader);
5465            // UPSTREAM-PARITY (oracle-verified 2.15.3): `<data/>` is empty, so
5466            // it contributes no END_ELEMENT: root, data, END root.
5467            assert_eq!(nodes.len(), 3);
5468
5469            free_reader(reader);
5470            libc::close(fd);
5471            std::fs::remove_file(tmp_path).ok();
5472        }
5473    }
5474
5475    #[test]
5476    fn test_reader_for_io() {
5477        unsafe {
5478            extern "C" fn io_read(context: *mut c_void, buffer: *mut c_char, len: c_int) -> c_int {
5479                if context.is_null() || buffer.is_null() || len <= 0 {
5480                    return -1;
5481                }
5482                // SAFETY: context points to an IoCtx struct.
5483                let ctx = unsafe { &mut *(context as *mut IoCtx) };
5484                if ctx.pos >= ctx.data.len() {
5485                    return 0;
5486                }
5487                let remaining = ctx.data.len() - ctx.pos;
5488                let to_copy = if (remaining as c_int) < len {
5489                    remaining
5490                } else {
5491                    len as usize
5492                };
5493                // SAFETY: buffer has at least `len` bytes of space.
5494                unsafe {
5495                    std::ptr::copy_nonoverlapping(
5496                        ctx.data.as_ptr().add(ctx.pos),
5497                        buffer as *mut u8,
5498                        to_copy,
5499                    );
5500                }
5501                ctx.pos += to_copy;
5502                to_copy as c_int
5503            }
5504
5505            extern "C" fn io_close(_context: *mut c_void) -> c_int {
5506                0
5507            }
5508
5509            struct IoCtx {
5510                data: &'static [u8],
5511                pos: usize,
5512            }
5513            let mut ctx = IoCtx {
5514                data: b"<root/>",
5515                pos: 0,
5516            };
5517
5518            let reader = xmlReaderForIO(
5519                Some(io_read),
5520                Some(io_close),
5521                &mut ctx as *mut IoCtx as *mut c_void,
5522                ptr::null(),
5523                ptr::null(),
5524                0,
5525            );
5526            assert!(!reader.is_null());
5527
5528            // Read through the document.
5529            assert_eq!(xmlTextReaderRead(reader), 1);
5530            assert_eq!((*reader).NodeType(), ReaderNodeType::ELEMENT);
5531            let cname = xmlTextReaderConstName(reader);
5532            assert!(!cname.is_null());
5533            assert_eq!(xmlstr_to_bytes(cname), b"root");
5534
5535            // UPSTREAM-PARITY (oracle-verified 2.15.3): `<root/>` is empty, so
5536            // there is no END_ELEMENT — the second Read returns EOF.
5537            assert_eq!(xmlTextReaderRead(reader), 0);
5538
5539            free_reader(reader);
5540        }
5541    }
5542
5543    /// xmlTextReaderPreservePattern: after a full traversal, the document
5544    /// contains only the pattern-matched nodes and their element ancestors
5545    /// (upstream NODE_IS_PRESERVED streaming prune; reader3.c — Phase-12
5546    /// EXTERNAL-CONSUMERS court).
5547    ///
5548    /// # Safety
5549    ///
5550    /// - The reader is created from a static string literal that stays alive
5551    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
5552    ///   and freed exactly once with `free_reader`; the returned doc is freed
5553    ///   with `tree::free_doc` exactly once.
5554    #[test]
5555    fn test_preserve_pattern_prunes() {
5556        unsafe {
5557            let reader = create_reader("<doc><parent><drop/><keep/><drop/></parent></doc>");
5558            assert!(!reader.is_null());
5559            // register the pattern BEFORE the first Read, like reader3.c
5560            let pat = b"keep\0";
5561            assert!(
5562                xmlTextReaderPreservePattern(
5563                    reader,
5564                    pat.as_ptr() as *const xmlChar,
5565                    ptr::null_mut(),
5566                ) >= 0
5567            );
5568            let mut ret = xmlTextReaderRead(reader);
5569            while ret == 1 {
5570                ret = xmlTextReaderRead(reader);
5571            }
5572            let doc = xmlTextReaderCurrentDoc(reader);
5573            assert!(!doc.is_null());
5574            free_reader(reader);
5575
5576            // the pruned doc keeps doc -> parent -> keep; drop nodes gone
5577            let root = tree::doc_get_root_element(doc);
5578            assert!(!root.is_null());
5579            assert_eq!(xmlstr_to_bytes((*root).name), b"doc");
5580            let mut children: Vec<*mut _xmlNode> = Vec::new();
5581            let mut c = (*root).children;
5582            while !c.is_null() {
5583                children.push(c);
5584                c = (*c).next;
5585            }
5586            // the doc's only child is the preserved ancestor <parent>
5587            assert_eq!(children.len(), 1);
5588            assert_eq!(xmlstr_to_bytes((*children[0]).name), b"parent");
5589            // <parent> keeps only the matched <keep/>; both <drop/> are gone
5590            let mut keep: Vec<*mut _xmlNode> = Vec::new();
5591            let mut c = (*children[0]).children;
5592            while !c.is_null() {
5593                keep.push(c);
5594                c = (*c).next;
5595            }
5596            assert_eq!(keep.len(), 1);
5597            assert_eq!(xmlstr_to_bytes((*keep[0]).name), b"keep");
5598            tree::free_doc(doc);
5599        }
5600    }
5601
5602    /// XML_PARSE_DTDVALID on a no-DTD document: the parser raises the
5603    /// no-DTD validity error and clears ctxt->valid (parse2.c — Phase-12
5604    /// EXTERNAL-CONSUMERS court).
5605    ///
5606    /// # Safety
5607    ///
5608    /// - The reader is created from a static string literal that stays alive
5609    ///   for the reader's lifetime; the `reader` pointer is asserted non-NULL
5610    ///   and freed exactly once with `free_reader`.
5611    #[test]
5612    fn test_reader_dtdvalid_no_dtd() {
5613        unsafe {
5614            let bytes = b"<doc/>";
5615            let ctxt = create_parser_ctxt();
5616            assert!(!ctxt.is_null());
5617            let input = input_from_memory(bytes.as_ptr() as *const c_char, bytes.len() as c_int);
5618            setup_parser_input(ctxt, input);
5619            crate::abi::exports_parser::apply_options(ctxt, XML_PARSE_DTDVALID);
5620            assert_eq!(parse_document(ctxt), 0);
5621            assert_eq!(
5622                (*ctxt).valid,
5623                0,
5624                "no-DTD validating parse must clear ctxt->valid"
5625            );
5626            assert_eq!((*ctxt).errNo, XML_DTD_NO_DTD);
5627            free_parser_ctxt(ctxt);
5628        }
5629    }
5630}
5631
5632// ═══════════════════════════════════════════════════════════════════════════════
5633// 11.1-I reader closure — remaining xmlTextReader API (R-000136)
5634// ═══════════════════════════════════════════════════════════════════════════════
5635
5636/// Error severity (upstream `xmlParserSeverities`, reader.h).
5637pub const XML_PARSER_SEVERITY_VALIDITY_WARNING: c_int = 1;
5638pub const XML_PARSER_SEVERITY_VALIDITY_ERROR: c_int = 2;
5639pub const XML_PARSER_SEVERITY_WARNING: c_int = 3;
5640pub const XML_PARSER_SEVERITY_ERROR: c_int = 4;
5641
5642/// Opaque locator passed to the reader error handler (upstream
5643/// `xmlTextReaderLocator`).
5644#[derive(Debug)]
5645#[repr(C)]
5646pub struct XmlTextReaderLocator {
5647    pub reader: *mut XmlTextReader,
5648}
5649
5650/// Reader error callback (upstream `xmlTextReaderErrorFunc`).
5651pub type xmlTextReaderErrorFunc = unsafe extern "C" fn(
5652    arg: *mut c_void,
5653    msg: *const c_char,
5654    severity: c_int,
5655    locator: *mut XmlTextReaderLocator,
5656);
5657
5658/// `xmlTextReaderPtr xmlReaderForDoc(const xmlChar *cur, const char *URL,
5659/// const char *encoding, int options)` — reader over an in-memory XML string.
5660///
5661/// # SAFETY
5662///
5663/// - `cur` must be a valid NUL-terminated XML document string.
5664#[no_mangle]
5665pub unsafe extern "C" fn xmlReaderForDoc(
5666    cur: *const xmlChar,
5667    URL: *const c_char,
5668    encoding: *const c_char,
5669    options: c_int,
5670) -> *mut XmlTextReader {
5671    if cur.is_null() {
5672        return ptr::null_mut();
5673    }
5674    let len = unsafe { libc::strlen(cur as *const libc::c_char) } as c_int;
5675    unsafe { xmlReaderForMemory(cur as *const c_char, len, URL, encoding, options) }
5676}
5677
5678/// `xmlTextReaderPtr xmlNewTextReaderFilename(const char *URI)` — upstream
5679/// xmlreader.c: creates a reader over the file, no encoding/options
5680/// (R-000176: the candidate previously exported a 3-argument extension).
5681///
5682/// # SAFETY
5683///
5684/// - `URI` must point to a valid NUL-terminated string or NULL.
5685#[no_mangle]
5686pub unsafe extern "C" fn xmlNewTextReaderFilename(URI: *const c_char) -> *mut XmlTextReader {
5687    unsafe { xmlReaderForFile(URI, ptr::null(), 0) }
5688}
5689
5690/// Rebuild a reader in place (upstream `xmlReaderNew*` reuse contract).
5691///
5692/// Upstream reuses the caller's existing reader allocation, so a caller's
5693/// pointer remains valid across `xmlReaderNew*`. The candidate mirrors that by
5694/// moving the freshly built reader's contents into the caller's allocation and
5695/// releasing the temporary allocation without dropping the moved contents.
5696///
5697/// # SAFETY
5698///
5699/// - `reader` must be a valid, non-NULL reader pointer.
5700/// - `new_reader` must be a valid, non-NULL reader pointer distinct from `reader`.
5701unsafe fn reader_renew(reader: *mut XmlTextReader, new_reader: *mut XmlTextReader) {
5702    debug_assert!(!reader.is_null() && !new_reader.is_null() && reader != new_reader);
5703    unsafe {
5704        // Drop the old contents, then bitwise-move the new reader into the
5705        // caller's allocation. The temporary allocation is deallocated without
5706        // dropping (its contents now live at `reader`).
5707        core::ptr::drop_in_place(reader);
5708        core::ptr::copy_nonoverlapping(new_reader, reader, 1);
5709        let layout = std::alloc::Layout::new::<XmlTextReader>();
5710        std::alloc::dealloc(new_reader as *mut u8, layout);
5711    }
5712}
5713
5714/// `int xmlReaderNewDoc(xmlTextReaderPtr reader, const xmlChar *cur, const char *URL, const char *encoding, int options)`.
5715///
5716/// # SAFETY
5717///
5718/// - `reader` must be valid pointers (or NULL
5719///   where the upstream C contract allows), obtained from the
5720///   matching constructor/owner and not yet freed; the callee may
5721///   take or keep ownership exactly as the C API specifies.
5722///
5723/// - `cur`, `URL`, `encoding` must point to valid NUL-terminated
5724///   strings (or NULL where the C contract allows) for the lifetime
5725///   of the call.
5726///
5727/// The caller must not race this call with concurrent mutation of the
5728/// same objects from other threads (per-object state is not internally
5729/// synchronized). Violating any of the above is undefined behavior.
5730///
5731/// Exercised by the C-API differential courts
5732/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5733/// courts; those pass byte-for-byte against the upstream oracle.
5734#[no_mangle]
5735pub unsafe extern "C" fn xmlReaderNewDoc(
5736    reader: *mut XmlTextReader,
5737    cur: *const xmlChar,
5738    URL: *const c_char,
5739    encoding: *const c_char,
5740    options: c_int,
5741) -> c_int {
5742    // UPSTREAM-PARITY: the New* family rejects a NULL reader before any work
5743    // (xmlreader.c: `if (reader == NULL) return (-1);`). It never allocates.
5744    if reader.is_null() || cur.is_null() {
5745        return -1;
5746    }
5747    let r = unsafe { xmlReaderForDoc(cur, URL, encoding, options) };
5748    if r.is_null() {
5749        return -1;
5750    }
5751    unsafe { reader_renew(reader, r) };
5752    0
5753}
5754
5755/// `int xmlReaderNewFile(xmlTextReaderPtr reader, const char *filename, const char *encoding, int options)`.
5756///
5757/// # SAFETY
5758///
5759/// - `reader` must be valid pointers (or NULL
5760///   where the upstream C contract allows), obtained from the
5761///   matching constructor/owner and not yet freed; the callee may
5762///   take or keep ownership exactly as the C API specifies.
5763///
5764/// - `filename`, `encoding` must point to valid NUL-terminated
5765///   strings (or NULL where the C contract allows) for the lifetime
5766///   of the call.
5767///
5768/// The caller must not race this call with concurrent mutation of the
5769/// same objects from other threads (per-object state is not internally
5770/// synchronized). Violating any of the above is undefined behavior.
5771///
5772/// Exercised by the C-API differential courts
5773/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5774/// courts; those pass byte-for-byte against the upstream oracle.
5775#[no_mangle]
5776pub unsafe extern "C" fn xmlReaderNewFile(
5777    reader: *mut XmlTextReader,
5778    filename: *const c_char,
5779    encoding: *const c_char,
5780    options: c_int,
5781) -> c_int {
5782    if reader.is_null() {
5783        return -1;
5784    }
5785    let r = unsafe { xmlReaderForFile(filename, encoding, options) };
5786    if r.is_null() {
5787        return -1;
5788    }
5789    unsafe { reader_renew(reader, r) };
5790    0
5791}
5792
5793/// `int xmlReaderNewMemory(xmlTextReaderPtr reader, const char *buffer, int size, const char *URL, const char *encoding, int options)`.
5794///
5795/// # SAFETY
5796///
5797/// - `reader` must be valid pointers (or NULL
5798///   where the upstream C contract allows), obtained from the
5799///   matching constructor/owner and not yet freed; the callee may
5800///   take or keep ownership exactly as the C API specifies.
5801///
5802/// - `buffer`, `URL`, `encoding` must point to valid NUL-terminated
5803///   strings (or NULL where the C contract allows) for the lifetime
5804///   of the call.
5805///
5806/// The caller must not race this call with concurrent mutation of the
5807/// same objects from other threads (per-object state is not internally
5808/// synchronized). Violating any of the above is undefined behavior.
5809///
5810/// Exercised by the C-API differential courts
5811/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5812/// courts; those pass byte-for-byte against the upstream oracle.
5813#[no_mangle]
5814pub unsafe extern "C" fn xmlReaderNewMemory(
5815    reader: *mut XmlTextReader,
5816    buffer: *const c_char,
5817    size: c_int,
5818    URL: *const c_char,
5819    encoding: *const c_char,
5820    options: c_int,
5821) -> c_int {
5822    if reader.is_null() || buffer.is_null() {
5823        return -1;
5824    }
5825    let r = unsafe { xmlReaderForMemory(buffer, size, URL, encoding, options) };
5826    if r.is_null() {
5827        return -1;
5828    }
5829    unsafe { reader_renew(reader, r) };
5830    0
5831}
5832
5833/// `int xmlReaderNewFd(xmlTextReaderPtr reader, int fd, const char *URL, const char *encoding, int options)`.
5834///
5835/// # SAFETY
5836///
5837/// - `reader` must be valid pointers (or NULL
5838///   where the upstream C contract allows), obtained from the
5839///   matching constructor/owner and not yet freed; the callee may
5840///   take or keep ownership exactly as the C API specifies.
5841///
5842/// - `URL`, `encoding` must point to valid NUL-terminated
5843///   strings (or NULL where the C contract allows) for the lifetime
5844///   of the call.
5845///
5846/// The caller must not race this call with concurrent mutation of the
5847/// same objects from other threads (per-object state is not internally
5848/// synchronized). Violating any of the above is undefined behavior.
5849///
5850/// Exercised by the C-API differential courts
5851/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5852/// courts; those pass byte-for-byte against the upstream oracle.
5853#[no_mangle]
5854pub unsafe extern "C" fn xmlReaderNewFd(
5855    reader: *mut XmlTextReader,
5856    fd: c_int,
5857    URL: *const c_char,
5858    encoding: *const c_char,
5859    options: c_int,
5860) -> c_int {
5861    if reader.is_null() {
5862        return -1;
5863    }
5864    let r = unsafe { xmlReaderForFd(fd, URL, encoding, options) };
5865    if r.is_null() {
5866        return -1;
5867    }
5868    unsafe { reader_renew(reader, r) };
5869    0
5870}
5871
5872/// `int xmlReaderNewIO(xmlTextReaderPtr reader, xmlInputReadCallback ioread, xmlInputCloseCallback ioclose, void *ioctx, const char *URL, const char *encoding, int options)`.
5873///
5874/// # SAFETY
5875///
5876/// - `reader`, `ioctx` must be valid pointers (or NULL
5877///   where the upstream C contract allows), obtained from the
5878///   matching constructor/owner and not yet freed; the callee may
5879///   take or keep ownership exactly as the C API specifies.
5880///
5881/// - `URL`, `encoding` must point to valid NUL-terminated
5882///   strings (or NULL where the C contract allows) for the lifetime
5883///   of the call.
5884///
5885/// - `ioread`, `ioclose` must be a valid callback (or None);
5886///   the callback is invoked with the documented context pointer and
5887///   must itself uphold the same pointer invariants.
5888///
5889/// The caller must not race this call with concurrent mutation of the
5890/// same objects from other threads (per-object state is not internally
5891/// synchronized). Violating any of the above is undefined behavior.
5892///
5893/// Exercised by the C-API differential courts
5894/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5895/// courts; those pass byte-for-byte against the upstream oracle.
5896#[no_mangle]
5897pub unsafe extern "C" fn xmlReaderNewIO(
5898    reader: *mut XmlTextReader,
5899    ioread: Option<xmlInputReadCallback>,
5900    ioclose: Option<xmlInputCloseCallback>,
5901    ioctx: *mut c_void,
5902    URL: *const c_char,
5903    encoding: *const c_char,
5904    options: c_int,
5905) -> c_int {
5906    // UPSTREAM-PARITY: NULL reader or NULL read callback is rejected (-1).
5907    if reader.is_null() || ioread.is_none() {
5908        return -1;
5909    }
5910    let r = unsafe { xmlReaderForIO(ioread, ioclose, ioctx, URL, encoding, options) };
5911    if r.is_null() {
5912        return -1;
5913    }
5914    unsafe { reader_renew(reader, r) };
5915    0
5916}
5917
5918/// `xmlTextReaderPtr xmlReaderWalker(xmlDocPtr doc)` — reader walking an
5919/// existing document tree.
5920///
5921/// # SAFETY
5922///
5923/// - `doc` must be a valid document.
5924#[no_mangle]
5925pub unsafe extern "C" fn xmlReaderWalker(doc: *mut _xmlDoc) -> *mut XmlTextReader {
5926    if doc.is_null() {
5927        return ptr::null_mut();
5928    }
5929    let mut reader = XmlTextReader::new(ptr::null_mut(), None, None);
5930    reader.doc = doc;
5931    reader.parsed = true;
5932    reader.owns_doc = false; // walker borrows the caller's document
5933    reader.state = ReadState::READING;
5934    reader.build_events();
5935    Box::into_raw(Box::new(reader))
5936}
5937
5938/// `int xmlReaderNewWalker(xmlTextReaderPtr reader, xmlDocPtr doc)`.
5939///
5940/// # SAFETY
5941///
5942/// - `reader`, `doc` must be valid pointers (or NULL
5943///   where the upstream C contract allows), obtained from the
5944///   matching constructor/owner and not yet freed; the callee may
5945///   take or keep ownership exactly as the C API specifies.
5946///
5947/// The caller must not race this call with concurrent mutation of the
5948/// same objects from other threads (per-object state is not internally
5949/// synchronized). Violating any of the above is undefined behavior.
5950///
5951/// Exercised by the C-API differential courts
5952/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5953/// courts; those pass byte-for-byte against the upstream oracle.
5954#[no_mangle]
5955pub unsafe extern "C" fn xmlReaderNewWalker(
5956    reader: *mut XmlTextReader,
5957    doc: *mut _xmlDoc,
5958) -> c_int {
5959    // UPSTREAM-PARITY: NULL reader or NULL doc is rejected (-1).
5960    if reader.is_null() || doc.is_null() {
5961        return -1;
5962    }
5963    let r = unsafe { xmlReaderWalker(doc) };
5964    if r.is_null() {
5965        return -1;
5966    }
5967    unsafe { reader_renew(reader, r) };
5968    0
5969}
5970
5971/// `long xmlTextReaderByteConsumed(xmlTextReaderPtr reader)`.
5972///
5973/// Returns the total bytes consumed from the input (0 when unavailable —
5974/// the candidate parses the full input up front; documented divergence).
5975///
5976/// # SAFETY
5977///
5978/// - `reader` must be valid pointers (or NULL
5979///   where the upstream C contract allows), obtained from the
5980///   matching constructor/owner and not yet freed; the callee may
5981///   take or keep ownership exactly as the C API specifies.
5982///
5983/// The caller must not race this call with concurrent mutation of the
5984/// same objects from other threads (per-object state is not internally
5985/// synchronized). Violating any of the above is undefined behavior.
5986///
5987/// Exercised by the C-API differential courts
5988/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5989/// courts; those pass byte-for-byte against the upstream oracle.
5990#[no_mangle]
5991pub const unsafe extern "C" fn xmlTextReaderByteConsumed(reader: *mut XmlTextReader) -> c_long {
5992    if reader.is_null() {
5993        return -1;
5994    }
5995    0
5996}
5997
5998/// `const xmlChar *xmlTextReaderConstBaseUri(xmlTextReaderPtr reader)` — the
5999/// base URI, valid until the reader is freed (no copy).
6000///
6001/// # SAFETY
6002///
6003/// - `reader` must be valid pointers (or NULL
6004///   where the upstream C contract allows), obtained from the
6005///   matching constructor/owner and not yet freed; the callee may
6006///   take or keep ownership exactly as the C API specifies.
6007///
6008/// The caller must not race this call with concurrent mutation of the
6009/// same objects from other threads (per-object state is not internally
6010/// synchronized). Violating any of the above is undefined behavior.
6011///
6012/// Exercised by the C-API differential courts
6013/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6014/// courts; those pass byte-for-byte against the upstream oracle.
6015#[no_mangle]
6016pub unsafe extern "C" fn xmlTextReaderConstBaseUri(reader: *mut XmlTextReader) -> *const xmlChar {
6017    if reader.is_null() {
6018        return ptr::null();
6019    }
6020    // UPSTREAM-PARITY (xmlreader.c xmlTextReaderConstBaseUri): the base URI
6021    // is exposed only once a node is current (upstream `reader->node == NULL`
6022    // returns NULL). Pre-first-Read and after EOF the PHP property must read
6023    // as the empty string, not the reader's setup URL.
6024    let r = unsafe { &*reader };
6025    if r.cur_node.is_null() {
6026        return ptr::null();
6027    }
6028    r.URL
6029}
6030
6031/// `const xmlChar *xmlTextReaderConstEncoding(xmlTextReaderPtr reader)`.
6032///
6033/// # SAFETY
6034///
6035/// - `reader` must be valid pointers (or NULL
6036///   where the upstream C contract allows), obtained from the
6037///   matching constructor/owner and not yet freed; the callee may
6038///   take or keep ownership exactly as the C API specifies.
6039///
6040/// The caller must not race this call with concurrent mutation of the
6041/// same objects from other threads (per-object state is not internally
6042/// synchronized). Violating any of the above is undefined behavior.
6043///
6044/// Exercised by the C-API differential courts
6045/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6046/// courts; those pass byte-for-byte against the upstream oracle.
6047#[no_mangle]
6048pub unsafe extern "C" fn xmlTextReaderConstEncoding(reader: *mut XmlTextReader) -> *const xmlChar {
6049    if reader.is_null() {
6050        return ptr::null();
6051    }
6052    let r = unsafe { &*reader };
6053    if !r.encoding.is_null() {
6054        return r.encoding;
6055    }
6056    if !r.doc.is_null() {
6057        return unsafe { (*r.doc).encoding };
6058    }
6059    ptr::null()
6060}
6061
6062/// `const xmlChar *xmlTextReaderConstLocalName(xmlTextReaderPtr reader)`.
6063///
6064/// UPSTREAM-PARITY: at an attribute position this is the attribute's local
6065/// name (or "xmlns"/the prefix for a namespace declaration); at an element
6066/// position the tree's local name.
6067///
6068/// # SAFETY
6069///
6070/// - `reader` must be valid pointers (or NULL
6071///   where the upstream C contract allows), obtained from the
6072///   matching constructor/owner and not yet freed; the callee may
6073///   take or keep ownership exactly as the C API specifies.
6074///
6075/// The caller must not race this call with concurrent mutation of the
6076/// same objects from other threads (per-object state is not internally
6077/// synchronized). Violating any of the above is undefined behavior.
6078///
6079/// Exercised by the C-API differential courts
6080/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6081/// courts; those pass byte-for-byte against the upstream oracle.
6082#[no_mangle]
6083pub unsafe extern "C" fn xmlTextReaderConstLocalName(reader: *mut XmlTextReader) -> *const xmlChar {
6084    if reader.is_null() {
6085        return ptr::null();
6086    }
6087    let r = unsafe { &*reader };
6088    if r.cur_node.is_null() {
6089        return ptr::null();
6090    }
6091    // Attribute position: the attribute's local name (upstream node->name for
6092    // XML_ATTRIBUTE_NODE; "xmlns"/prefix for a namespace declaration).
6093    if r.cur_attribute >= 0 {
6094        let target = unsafe { r.attr_at(r.cur_node, r.cur_attribute) };
6095        return match target {
6096            AttrTarget::Ns(ns) => {
6097                if ns.is_null() {
6098                    ptr::null()
6099                } else if unsafe { (*ns).prefix }.is_null() {
6100                    c"xmlns".as_ptr() as *const xmlChar
6101                } else {
6102                    unsafe { (*ns).prefix }
6103                }
6104            }
6105            AttrTarget::Prop(p) => {
6106                if p.is_null() || unsafe { (*p).name }.is_null() {
6107                    ptr::null()
6108                } else {
6109                    unsafe { (*p).name }
6110                }
6111            }
6112            AttrTarget::None => ptr::null(),
6113        };
6114    }
6115    // Element position: the tree's local name (upstream node->name).
6116    let etype = unsafe { (*r.cur_node).type_ };
6117    if etype == XML_ELEMENT_NODE as c_int || etype == XML_ATTRIBUTE_NODE as c_int {
6118        unsafe { (*r.cur_node).name }
6119    } else {
6120        ptr::null()
6121    }
6122}
6123
6124/// `const xmlChar *xmlTextReaderConstNamespaceUri(xmlTextReaderPtr reader)`.
6125///
6126/// UPSTREAM-PARITY: at an attribute position the namespace comes from the
6127/// attribute (or namespace declaration) itself; elsewhere from the node.
6128///
6129/// # SAFETY
6130///
6131/// - `reader` must be valid pointers (or NULL
6132///   where the upstream C contract allows), obtained from the
6133///   matching constructor/owner and not yet freed; the callee may
6134///   take or keep ownership exactly as the C API specifies.
6135///
6136/// The caller must not race this call with concurrent mutation of the
6137/// same objects from other threads (per-object state is not internally
6138/// synchronized). Violating any of the above is undefined behavior.
6139///
6140/// Exercised by the C-API differential courts
6141/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6142/// courts; those pass byte-for-byte against the upstream oracle.
6143#[no_mangle]
6144pub unsafe extern "C" fn xmlTextReaderConstNamespaceUri(
6145    reader: *mut XmlTextReader,
6146) -> *const xmlChar {
6147    if reader.is_null() {
6148        return ptr::null();
6149    }
6150    let r = unsafe { &*reader };
6151    if r.cur_node.is_null() {
6152        return ptr::null();
6153    }
6154    // Attribute position: resolve the current attribute's namespace.
6155    if r.cur_attribute >= 0 {
6156        let target = unsafe { r.attr_at(r.cur_node, r.cur_attribute) };
6157        return match target {
6158            AttrTarget::Ns(_ns) => {
6159                // UPSTREAM-PARITY (xmlTextReaderConstNamespaceUri): a
6160                // namespace declaration reports the xmlns namespace URI,
6161                // not the declared URI.
6162                c"http://www.w3.org/2000/xmlns/".as_ptr() as *const xmlChar
6163            }
6164            AttrTarget::Prop(p) => {
6165                if p.is_null() || unsafe { (*p).ns }.is_null() {
6166                    ptr::null()
6167                } else {
6168                    unsafe { (*(*p).ns).href }
6169                }
6170            }
6171            AttrTarget::None => ptr::null(),
6172        };
6173    }
6174    let ns = unsafe { (*r.cur_node).ns };
6175    if ns.is_null() || unsafe { (*ns).href }.is_null() {
6176        ptr::null()
6177    } else {
6178        unsafe { (*ns).href }
6179    }
6180}
6181
6182/// `const xmlChar *xmlTextReaderConstPrefix(xmlTextReaderPtr reader)`.
6183///
6184/// UPSTREAM-PARITY: at an attribute position the prefix comes from the
6185/// attribute; for a namespace declaration the prefix is reported as "xmlns"
6186/// (and NULL for the default declaration) — an upstream quirk reproduced here.
6187///
6188/// # SAFETY
6189///
6190/// - `reader` must be valid pointers (or NULL
6191///   where the upstream C contract allows), obtained from the
6192///   matching constructor/owner and not yet freed; the callee may
6193///   take or keep ownership exactly as the C API specifies.
6194///
6195/// The caller must not race this call with concurrent mutation of the
6196/// same objects from other threads (per-object state is not internally
6197/// synchronized). Violating any of the above is undefined behavior.
6198///
6199/// Exercised by the C-API differential courts
6200/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6201/// courts; those pass byte-for-byte against the upstream oracle.
6202#[no_mangle]
6203pub unsafe extern "C" fn xmlTextReaderConstPrefix(reader: *mut XmlTextReader) -> *const xmlChar {
6204    if reader.is_null() {
6205        return ptr::null();
6206    }
6207    let r = unsafe { &*reader };
6208    if r.cur_node.is_null() {
6209        return ptr::null();
6210    }
6211    // Attribute position: resolve the current attribute's namespace.
6212    if r.cur_attribute >= 0 {
6213        let target = unsafe { r.attr_at(r.cur_node, r.cur_attribute) };
6214        return match target {
6215            AttrTarget::Ns(ns) => {
6216                if ns.is_null() || unsafe { (*ns).prefix }.is_null() {
6217                    ptr::null()
6218                } else {
6219                    c"xmlns".as_ptr() as *const xmlChar
6220                }
6221            }
6222            AttrTarget::Prop(p) => {
6223                if p.is_null() || unsafe { (*p).ns }.is_null() {
6224                    ptr::null()
6225                } else {
6226                    unsafe { (*(*p).ns).prefix }
6227                }
6228            }
6229            AttrTarget::None => ptr::null(),
6230        };
6231    }
6232    let ns = unsafe { (*r.cur_node).ns };
6233    if ns.is_null() || unsafe { (*ns).prefix }.is_null() {
6234        ptr::null()
6235    } else {
6236        unsafe { (*ns).prefix }
6237    }
6238}
6239
6240/// `const xmlChar *xmlTextReaderConstString(xmlTextReaderPtr reader, const xmlChar *str)`
6241/// — the reader's dictionary-internalized copy of `str`; the candidate
6242/// returns `str` unchanged (dictionary interning is an internal detail).
6243///
6244/// # SAFETY
6245///
6246/// - `_reader` must be valid pointers (or NULL
6247///   where the upstream C contract allows), obtained from the
6248///   matching constructor/owner and not yet freed; the callee may
6249///   take or keep ownership exactly as the C API specifies.
6250///
6251/// - `str` must point to valid NUL-terminated
6252///   strings (or NULL where the C contract allows) for the lifetime
6253///   of the call.
6254///
6255/// The caller must not race this call with concurrent mutation of the
6256/// same objects from other threads (per-object state is not internally
6257/// synchronized). Violating any of the above is undefined behavior.
6258///
6259/// Exercised by the C-API differential courts
6260/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6261/// courts; those pass byte-for-byte against the upstream oracle.
6262#[no_mangle]
6263pub const unsafe extern "C" fn xmlTextReaderConstString(
6264    _reader: *mut XmlTextReader,
6265    str: *const xmlChar,
6266) -> *const xmlChar {
6267    str
6268}
6269
6270/// `const xmlChar *xmlTextReaderConstXmlLang(xmlTextReaderPtr reader)`.
6271///
6272/// # SAFETY
6273///
6274/// - `reader` must be valid pointers (or NULL
6275///   where the upstream C contract allows), obtained from the
6276///   matching constructor/owner and not yet freed; the callee may
6277///   take or keep ownership exactly as the C API specifies.
6278///
6279/// The caller must not race this call with concurrent mutation of the
6280/// same objects from other threads (per-object state is not internally
6281/// synchronized). Violating any of the above is undefined behavior.
6282///
6283/// Exercised by the C-API differential courts
6284/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6285/// courts; those pass byte-for-byte against the upstream oracle.
6286#[no_mangle]
6287pub unsafe extern "C" fn xmlTextReaderConstXmlLang(reader: *mut XmlTextReader) -> *const xmlChar {
6288    if reader.is_null() {
6289        return ptr::null();
6290    }
6291    let r = unsafe { &*reader };
6292    let mut node = r.cur_node;
6293    while !node.is_null() {
6294        let mut prop = unsafe { (*node).properties };
6295        while !prop.is_null() {
6296            let p = unsafe { &*prop };
6297            if !p.name.is_null()
6298                && unsafe { *p.name } == b'x'
6299                && unsafe { *p.name.add(1) } == b'm'
6300                && unsafe { *p.name.add(2) } == b'l'
6301                && unsafe { *p.name.add(3) } == b':'
6302                && unsafe { *p.name.add(4) } == b'l'
6303                && unsafe { *p.name.add(5) } == b'a'
6304                && unsafe { *p.name.add(6) } == b'n'
6305                && unsafe { *p.name.add(7) } == b'g'
6306                && unsafe { *p.name.add(8) } == 0
6307            {
6308                if !p.children.is_null() {
6309                    let txt = p.children;
6310                    if unsafe { (*txt).type_ }
6311                        == crate::abi::types::xmlElementType::XML_TEXT_NODE as c_int
6312                    {
6313                        return unsafe { (*txt).content };
6314                    }
6315                }
6316                return ptr::null();
6317            }
6318            prop = p.next;
6319        }
6320        node = unsafe { (*node).parent };
6321    }
6322    ptr::null()
6323}
6324
6325/// `const xmlChar *xmlTextReaderConstXmlVersion(xmlTextReaderPtr reader)`.
6326///
6327/// # SAFETY
6328///
6329/// - `reader` must be valid pointers (or NULL
6330///   where the upstream C contract allows), obtained from the
6331///   matching constructor/owner and not yet freed; the callee may
6332///   take or keep ownership exactly as the C API specifies.
6333///
6334/// The caller must not race this call with concurrent mutation of the
6335/// same objects from other threads (per-object state is not internally
6336/// synchronized). Violating any of the above is undefined behavior.
6337///
6338/// Exercised by the C-API differential courts
6339/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6340/// courts; those pass byte-for-byte against the upstream oracle.
6341#[no_mangle]
6342pub unsafe extern "C" fn xmlTextReaderConstXmlVersion(
6343    reader: *mut XmlTextReader,
6344) -> *const xmlChar {
6345    if reader.is_null() {
6346        return ptr::null();
6347    }
6348    let r = unsafe { &*reader };
6349    if r.doc.is_null() {
6350        return ptr::null();
6351    }
6352    unsafe { (*r.doc).version }
6353}
6354
6355/// `int xmlTextReaderQuoteChar(xmlTextReaderPtr reader)`.
6356///
6357/// UPSTREAM-PARITY: libxml2 2.13/2.15 returns `'"'` unconditionally for any
6358/// non-NULL reader (the implementation is a placeholder that does not inspect
6359/// the attribute; see the `/* TODO maybe lookup the attribute value */` comment
6360/// in xmlreader.c). The candidate reproduces that historical behavior exactly.
6361///
6362/// # SAFETY
6363///
6364/// - `reader` must be valid pointers (or NULL
6365///   where the upstream C contract allows), obtained from the
6366///   matching constructor/owner and not yet freed; the callee may
6367///   take or keep ownership exactly as the C API specifies.
6368///
6369/// The caller must not race this call with concurrent mutation of the
6370/// same objects from other threads (per-object state is not internally
6371/// synchronized). Violating any of the above is undefined behavior.
6372///
6373/// Exercised by the C-API differential courts
6374/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6375/// courts; those pass byte-for-byte against the upstream oracle.
6376#[no_mangle]
6377pub const unsafe extern "C" fn xmlTextReaderQuoteChar(reader: *mut XmlTextReader) -> c_int {
6378    if reader.is_null() {
6379        return -1;
6380    }
6381    b'"' as c_int
6382}
6383
6384/// `int xmlTextReaderIsDefault(xmlTextReaderPtr reader)` — whether the current
6385/// attribute came from the DTD default. The candidate returns 0 for a valid
6386/// reader (DTD default attribute expansion is not annotated; documented
6387/// divergence), -1 for a NULL reader (upstream contract).
6388///
6389/// # SAFETY
6390///
6391/// - `reader` must be valid pointers (or NULL
6392///   where the upstream C contract allows), obtained from the
6393///   matching constructor/owner and not yet freed; the callee may
6394///   take or keep ownership exactly as the C API specifies.
6395///
6396/// The caller must not race this call with concurrent mutation of the
6397/// same objects from other threads (per-object state is not internally
6398/// synchronized). Violating any of the above is undefined behavior.
6399///
6400/// Exercised by the C-API differential courts
6401/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6402/// courts; those pass byte-for-byte against the upstream oracle.
6403#[no_mangle]
6404pub const unsafe extern "C" fn xmlTextReaderIsDefault(reader: *mut XmlTextReader) -> c_int {
6405    if reader.is_null() {
6406        return -1;
6407    }
6408    0
6409}
6410
6411/// `int xmlTextReaderIsNamespaceDecl(xmlTextReaderPtr reader)` — whether the
6412/// current attribute position is a namespace declaration.
6413///
6414/// # SAFETY
6415///
6416/// - `reader` must be valid pointers (or NULL
6417///   where the upstream C contract allows), obtained from the
6418///   matching constructor/owner and not yet freed; the callee may
6419///   take or keep ownership exactly as the C API specifies.
6420///
6421/// The caller must not race this call with concurrent mutation of the
6422/// same objects from other threads (per-object state is not internally
6423/// synchronized). Violating any of the above is undefined behavior.
6424///
6425/// Exercised by the C-API differential courts
6426/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6427/// courts; those pass byte-for-byte against the upstream oracle.
6428#[no_mangle]
6429pub unsafe extern "C" fn xmlTextReaderIsNamespaceDecl(reader: *mut XmlTextReader) -> c_int {
6430    if reader.is_null() {
6431        return -1;
6432    }
6433    let r = unsafe { &*reader };
6434    if r.cur_node.is_null() {
6435        return -1;
6436    }
6437    r.cur_attr_is_ns as c_int
6438}
6439
6440/// `int xmlTextReaderMoveToAttributeNs(xmlTextReaderPtr reader, const xmlChar *localName, const xmlChar *namespaceURI)`.
6441///
6442/// UPSTREAM-PARITY (xmlreader.c, 2.15): NULL reader/localName/namespaceURI
6443/// returns -1; a NULL `namespaceURI` is NOT treated as "no namespace" — the
6444/// caller must pass the actual URI. The `http://www.w3.org/2000/xmlns/`
6445/// namespace searches namespace declarations (matching the default `xmlns`
6446/// declaration or a prefix), everything else searches only namespace-qualified
6447/// properties (`prop->ns != NULL`).
6448///
6449/// # SAFETY
6450///
6451/// - `localName`/`namespaceURI` must be valid strings (non-NULL).
6452#[no_mangle]
6453pub unsafe extern "C" fn xmlTextReaderMoveToAttributeNs(
6454    reader: *mut XmlTextReader,
6455    localName: *const xmlChar,
6456    namespaceURI: *const xmlChar,
6457) -> c_int {
6458    if reader.is_null() || localName.is_null() || namespaceURI.is_null() {
6459        return -1;
6460    }
6461    let r = unsafe { &mut *reader };
6462    let node = r.cur_node;
6463    if node.is_null() {
6464        return -1;
6465    }
6466    if unsafe { (*node).type_ } != XML_ELEMENT_NODE as c_int {
6467        return 0;
6468    }
6469
6470    const XMLNS_URI: &[u8] = b"http://www.w3.org/2000/xmlns/\0";
6471    if libc::strcmp(
6472        namespaceURI as *const libc::c_char,
6473        XMLNS_URI.as_ptr() as *const libc::c_char,
6474    ) == 0
6475    {
6476        // Namespace-declaration search: localName "xmlns" addresses the
6477        // default declaration, any other localName is a prefix.
6478        let is_default = libc::strcmp(
6479            localName as *const libc::c_char,
6480            c"xmlns".as_ptr() as *const libc::c_char,
6481        ) == 0;
6482        let mut ns = unsafe { (*node).nsDef };
6483        let mut index = 0;
6484        while !ns.is_null() {
6485            let n = unsafe { &*ns };
6486            let prefix_match = if is_default {
6487                n.prefix.is_null()
6488            } else {
6489                !n.prefix.is_null()
6490                    && libc::strcmp(
6491                        n.prefix as *const libc::c_char,
6492                        localName as *const libc::c_char,
6493                    ) == 0
6494            };
6495            if prefix_match {
6496                r.cur_attribute = index;
6497                r.node_type = ReaderNodeType::ATTRIBUTE;
6498                r.cache_attribute_info(AttrTarget::Ns(ns));
6499                return 1;
6500            }
6501            index += 1;
6502            ns = unsafe { (*ns).next };
6503        }
6504        return 0;
6505    }
6506
6507    // Property search: only namespace-qualified attributes are matchable.
6508    let mut prop = unsafe { (*node).properties };
6509    let mut index = 0;
6510    let mut ns_count = 0;
6511    let mut ns = unsafe { (*node).nsDef };
6512    while !ns.is_null() {
6513        ns_count += 1;
6514        ns = unsafe { (*ns).next };
6515    }
6516    while !prop.is_null() {
6517        let p = unsafe { &*prop };
6518        if !p.name.is_null()
6519            && !p.ns.is_null()
6520            && !(*p.ns).href.is_null()
6521            && libc::strcmp(
6522                p.name as *const libc::c_char,
6523                localName as *const libc::c_char,
6524            ) == 0
6525            && libc::strcmp(
6526                (*p.ns).href as *const libc::c_char,
6527                namespaceURI as *const libc::c_char,
6528            ) == 0
6529        {
6530            r.cur_attribute = ns_count + index;
6531            r.node_type = ReaderNodeType::ATTRIBUTE;
6532            r.cache_attribute_info(AttrTarget::Prop(prop));
6533            return 1;
6534        }
6535        index += 1;
6536        prop = unsafe { (*prop).next };
6537    }
6538    0
6539}
6540
6541/// `xmlNodePtr xmlTextReaderPreserve(xmlTextReaderPtr reader)` — the current
6542/// node (the candidate's reader owns the whole tree, so no separate
6543/// preservation step is needed).
6544///
6545/// # SAFETY
6546///
6547/// - `reader` must be valid pointers (or NULL
6548///   where the upstream C contract allows), obtained from the
6549///   matching constructor/owner and not yet freed; the callee may
6550///   take or keep ownership exactly as the C API specifies.
6551///
6552/// The caller must not race this call with concurrent mutation of the
6553/// same objects from other threads (per-object state is not internally
6554/// synchronized). Violating any of the above is undefined behavior.
6555///
6556/// Exercised by the C-API differential courts
6557/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6558/// courts; those pass byte-for-byte against the upstream oracle.
6559#[no_mangle]
6560pub unsafe extern "C" fn xmlTextReaderPreserve(reader: *mut XmlTextReader) -> *mut _xmlNode {
6561    if reader.is_null() {
6562        return ptr::null_mut();
6563    }
6564    unsafe { (*reader).cur_node }
6565}
6566
6567/// `int xmlTextReaderPreservePattern(xmlTextReaderPtr reader, const xmlChar *pattern, const xmlChar **namespaces)`.
6568///
6569/// Compiles the XPath-subset pattern (upstream `xmlPatterncompile`) and
6570/// registers it like upstream `reader->patternTab`; after the parse,
6571/// matched nodes and their element ancestors are kept and every other node
6572/// is unlinked and freed (upstream's NODE_IS_PRESERVED streaming prune,
6573/// applied post-parse by the candidate's whole-tree reader). Returns the
6574/// index of the registered pattern, or -1 on error (upstream xmlreader.c
6575/// xmlTextReaderPreservePattern).
6576///
6577/// # SAFETY
6578///
6579/// - `reader` must be valid pointers (or NULL
6580///   where the upstream C contract allows), obtained from the
6581///   matching constructor/owner and not yet freed; the callee may
6582///   take or keep ownership exactly as the C API specifies.
6583///
6584/// - `pattern` must point to a valid NUL-terminated string;
6585///   `namespaces` must be NULL or a NULL-terminated array of
6586///   NUL-terminated strings, both live for the duration of the call.
6587///
6588/// The caller must not race this call with concurrent mutation of the
6589/// same objects from other threads (per-object state is not internally
6590/// synchronized). Violating any of the above is undefined behavior.
6591///
6592/// Exercised by the Phase-12 EXTERNAL-CONSUMERS court (reader3.c).
6593#[no_mangle]
6594pub unsafe extern "C" fn xmlTextReaderPreservePattern(
6595    reader: *mut XmlTextReader,
6596    pattern: *const xmlChar,
6597    namespaces: *mut *const xmlChar,
6598) -> c_int {
6599    if reader.is_null() || pattern.is_null() {
6600        return -1;
6601    }
6602    let comp = unsafe {
6603        crate::abi::exports_automata::xmlPatterncompile(
6604            pattern,
6605            ptr::null_mut(),
6606            0,
6607            namespaces as *const *const xmlChar,
6608        )
6609    };
6610    if comp.is_null() {
6611        return -1;
6612    }
6613    // SAFETY: reader is valid (checked); pattern_tab owns the compiled
6614    // pattern and frees it in Drop.
6615    let idx = unsafe { (*reader).pattern_tab.len() };
6616    unsafe { (*reader).pattern_tab.push(comp) };
6617    idx as c_int
6618}
6619
6620/// `int xmlTextReaderSetErrorHandler(xmlTextReaderPtr reader, xmlTextReaderErrorFunc f, void *arg)`.
6621///
6622/// # SAFETY
6623///
6624/// - `f` must be a valid callback or NULL.
6625#[no_mangle]
6626pub unsafe extern "C" fn xmlTextReaderSetErrorHandler(
6627    reader: *mut XmlTextReader,
6628    f: Option<xmlTextReaderErrorFunc>,
6629    arg: *mut c_void,
6630) {
6631    if reader.is_null() {
6632        return;
6633    }
6634    unsafe {
6635        (*reader).error_handler = f;
6636        (*reader).error_arg = arg;
6637    }
6638}
6639
6640/// `void xmlTextReaderGetErrorHandler(xmlTextReaderPtr reader, xmlTextReaderErrorFunc *f, void **arg)`.
6641///
6642/// # SAFETY
6643///
6644/// - `f`/`arg` must be valid out-pointers or NULL.
6645#[no_mangle]
6646pub unsafe extern "C" fn xmlTextReaderGetErrorHandler(
6647    reader: *mut XmlTextReader,
6648    f: *mut Option<xmlTextReaderErrorFunc>,
6649    arg: *mut *mut c_void,
6650) {
6651    if reader.is_null() {
6652        return;
6653    }
6654    unsafe {
6655        if !f.is_null() {
6656            *f = (*reader).error_handler;
6657        }
6658        if !arg.is_null() {
6659            *arg = (*reader).error_arg;
6660        }
6661    }
6662}
6663
6664/// `void xmlTextReaderSetStructuredErrorHandler(xmlTextReaderPtr reader, xmlStructuredErrorFunc f, void *arg)`.
6665///
6666/// # SAFETY
6667///
6668/// - `reader`, `arg` must be valid pointers (or NULL
6669///   where the upstream C contract allows), obtained from the
6670///   matching constructor/owner and not yet freed; the callee may
6671///   take or keep ownership exactly as the C API specifies.
6672///
6673/// - `f` must be a valid callback (or None);
6674///   the callback is invoked with the documented context pointer and
6675///   must itself uphold the same pointer invariants.
6676///
6677/// The caller must not race this call with concurrent mutation of the
6678/// same objects from other threads (per-object state is not internally
6679/// synchronized). Violating any of the above is undefined behavior.
6680///
6681/// Exercised by the C-API differential courts
6682/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6683/// courts; those pass byte-for-byte against the upstream oracle.
6684#[no_mangle]
6685pub unsafe extern "C" fn xmlTextReaderSetStructuredErrorHandler(
6686    reader: *mut XmlTextReader,
6687    f: Option<crate::abi::callbacks::xmlStructuredErrorFunc>,
6688    arg: *mut c_void,
6689) {
6690    if reader.is_null() {
6691        return;
6692    }
6693    unsafe {
6694        (*reader).structured_handler = f;
6695        (*reader).structured_arg = arg;
6696    }
6697}
6698
6699/// `void xmlTextReaderSetResourceLoader(xmlTextReaderPtr reader,
6700/// xmlResourceLoader loader, void *data)` — install a custom resource
6701/// loader; stored on the reader and forwarded to its parser context
6702/// (upstream xmlreader.c).
6703///
6704/// # SAFETY
6705///
6706/// - `reader`, `data` must be valid pointers (or NULL
6707///   where the upstream C contract allows), obtained from the
6708///   matching constructor/owner and not yet freed; the callee may
6709///   take or keep ownership exactly as the C API specifies.
6710///
6711/// - `loader` must be a valid callback (or None);
6712///   the callback is invoked with the documented context pointer and
6713///   must itself uphold the same pointer invariants.
6714///
6715/// The caller must not race this call with concurrent mutation of the
6716/// same objects from other threads (per-object state is not internally
6717/// synchronized). Violating any of the above is undefined behavior.
6718///
6719/// Exercised by the C-API differential courts
6720/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6721/// courts; those pass byte-for-byte against the upstream oracle.
6722#[no_mangle]
6723pub unsafe extern "C" fn xmlTextReaderSetResourceLoader(
6724    reader: *mut XmlTextReader,
6725    loader: Option<crate::abi::callbacks::xmlResourceLoader>,
6726    data: *mut c_void,
6727) {
6728    if reader.is_null() {
6729        return;
6730    }
6731    unsafe {
6732        if !(*reader).ctxt.is_null() {
6733            crate::abi::exports_parserint::xmlCtxtSetResourceLoader((*reader).ctxt, loader, data);
6734        }
6735    }
6736}
6737
6738/// `const xmlError *xmlTextReaderGetLastError(xmlTextReaderPtr reader)` —
6739/// pointer to the reader's embedded `_xmlError` (upstream returns
6740/// `&reader->ctxt->lastError`, which is always present while the reader
6741/// exists; valid until the next error is collected).
6742///
6743/// # SAFETY
6744///
6745/// - `reader` must be valid pointers (or NULL
6746///   where the upstream C contract allows), obtained from the
6747///   matching constructor/owner and not yet freed; the callee may
6748///   take or keep ownership exactly as the C API specifies.
6749///
6750/// The caller must not race this call with concurrent mutation of the
6751/// same objects from other threads (per-object state is not internally
6752/// synchronized). Violating any of the above is undefined behavior.
6753///
6754/// Exercised by the C-API differential courts
6755/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6756/// courts; those pass byte-for-byte against the upstream oracle.
6757#[no_mangle]
6758pub unsafe extern "C" fn xmlTextReaderGetLastError(
6759    reader: *mut XmlTextReader,
6760) -> *const crate::abi::structs::_xmlError {
6761    if reader.is_null() {
6762        return ptr::null();
6763    }
6764    let r = unsafe { &mut *reader };
6765    // Sync the embedded struct from the most recent collected error, if any.
6766    // With no errors the struct stays zeroed (message NULL) — matching the
6767    // oracle, which still returns a non-NULL pointer here.
6768    if let Some(msg) = r.errors.last() {
6769        unsafe {
6770            // Message is a fresh NUL-terminated xmlMalloc copy owned by the
6771            // reader (freed on replacement and on drop).
6772            let bytes = msg.as_bytes();
6773            let m = libc::malloc(bytes.len() + 1) as *mut xmlChar;
6774            if !m.is_null() {
6775                libc::memcpy(
6776                    m as *mut libc::c_void,
6777                    bytes.as_ptr() as *const libc::c_void,
6778                    bytes.len(),
6779                );
6780                *m.add(bytes.len()) = 0;
6781                if !r.last_err.message.is_null() {
6782                    libc::free(r.last_err.message as *mut libc::c_void);
6783                }
6784                (*reader).last_err.message = m as *mut c_char;
6785                (*reader).last_err.domain = crate::abi::types::XML_FROM_PARSER as c_int;
6786                (*reader).last_err.level = crate::abi::types::xmlErrorLevel::XML_ERR_ERROR as c_int;
6787                (*reader).last_err.code = crate::abi::types::XML_ERR_INTERNAL_ERROR as c_int;
6788            }
6789        }
6790    }
6791    &(*reader).last_err as *const crate::abi::structs::_xmlError
6792}
6793
6794/// `xmlChar *xmlTextReaderLocatorBaseURI(xmlTextReaderLocatorPtr locator)`.
6795///
6796/// # SAFETY
6797///
6798/// - `locator` must be valid or NULL.
6799#[no_mangle]
6800pub unsafe extern "C" fn xmlTextReaderLocatorBaseURI(
6801    locator: *mut XmlTextReaderLocator,
6802) -> *mut xmlChar {
6803    if locator.is_null() {
6804        return ptr::null_mut();
6805    }
6806    unsafe {
6807        let r = (*locator).reader;
6808        if r.is_null() {
6809            return ptr::null_mut();
6810        }
6811        xml_strdup((*r).URL)
6812    }
6813}
6814
6815/// `int xmlTextReaderLocatorLineNumber(xmlTextReaderLocatorPtr locator)`.
6816///
6817/// # SAFETY
6818///
6819/// - `locator` must be valid pointers (or NULL
6820///   where the upstream C contract allows), obtained from the
6821///   matching constructor/owner and not yet freed; the callee may
6822///   take or keep ownership exactly as the C API specifies.
6823///
6824/// The caller must not race this call with concurrent mutation of the
6825/// same objects from other threads (per-object state is not internally
6826/// synchronized). Violating any of the above is undefined behavior.
6827///
6828/// Exercised by the C-API differential courts
6829/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6830/// courts; those pass byte-for-byte against the upstream oracle.
6831#[no_mangle]
6832pub unsafe extern "C" fn xmlTextReaderLocatorLineNumber(
6833    locator: *mut XmlTextReaderLocator,
6834) -> c_int {
6835    if locator.is_null() {
6836        return -1;
6837    }
6838    unsafe {
6839        let r = (*locator).reader;
6840        if r.is_null() {
6841            return -1;
6842        }
6843        let node = (*r).cur_node;
6844        if node.is_null() {
6845            return -1;
6846        }
6847        (*node).line as c_int
6848    }
6849}
6850
6851/// `xmlParserInputBufferPtr xmlTextReaderGetRemainder(xmlTextReaderPtr reader)`.
6852///
6853/// Returns NULL — the candidate reads the whole input up front (documented
6854/// divergence: no unconsumed input remains).
6855///
6856/// # SAFETY
6857///
6858/// - `_reader` must be valid pointers (or NULL
6859///   where the upstream C contract allows), obtained from the
6860///   matching constructor/owner and not yet freed; the callee may
6861///   take or keep ownership exactly as the C API specifies.
6862///
6863/// The caller must not race this call with concurrent mutation of the
6864/// same objects from other threads (per-object state is not internally
6865/// synchronized). Violating any of the above is undefined behavior.
6866///
6867/// Exercised by the C-API differential courts
6868/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6869/// courts; those pass byte-for-byte against the upstream oracle.
6870#[no_mangle]
6871pub const unsafe extern "C" fn xmlTextReaderGetRemainder(
6872    _reader: *mut XmlTextReader,
6873) -> *mut crate::abi::structs::_xmlParserInputBuffer {
6874    ptr::null_mut()
6875}
6876
6877/// `void xmlTextReaderSetMaxAmplification(xmlTextReaderPtr reader, unsigned maxAmpl)`.
6878///
6879/// # SAFETY
6880///
6881/// - `reader` must be valid pointers (or NULL
6882///   where the upstream C contract allows), obtained from the
6883///   matching constructor/owner and not yet freed; the callee may
6884///   take or keep ownership exactly as the C API specifies.
6885///
6886/// The caller must not race this call with concurrent mutation of the
6887/// same objects from other threads (per-object state is not internally
6888/// synchronized). Violating any of the above is undefined behavior.
6889///
6890/// Exercised by the C-API differential courts
6891/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6892/// courts; those pass byte-for-byte against the upstream oracle.
6893#[no_mangle]
6894pub unsafe extern "C" fn xmlTextReaderSetMaxAmplification(
6895    reader: *mut XmlTextReader,
6896    maxAmpl: c_uint,
6897) {
6898    if reader.is_null() {
6899        return;
6900    }
6901    unsafe { (*reader).max_amplification = maxAmpl as c_int };
6902}
6903
6904/// `int xmlTextReaderSchemaValidate(xmlTextReaderPtr reader, const char *xsd)` —
6905/// parse `xsd` and validate the reader's document as it is processed.
6906///
6907/// UPSTREAM-PARITY (xmlreader.c xmlTextReaderSchemaValidateInternal):
6908/// activation is only possible before the first Read(); the schema is
6909/// compiled NOW (a compile failure returns -1, which php reports as
6910/// "Schema contains errors") and the document is validated at read time —
6911/// validity diagnostics surface on the reader's error channel while
6912/// Read() streams, not as this call's return value.
6913///
6914/// # SAFETY
6915///
6916/// - `xsd` must be a valid path or NULL.
6917#[no_mangle]
6918pub unsafe extern "C" fn xmlTextReaderSchemaValidate(
6919    reader: *mut XmlTextReader,
6920    xsd: *const c_char,
6921) -> c_int {
6922    if reader.is_null() {
6923        return -1;
6924    }
6925    let r = unsafe { &mut *reader };
6926    if r.parsed {
6927        // Upstream: only XML_TEXTREADER_MODE_INITIAL readers can activate.
6928        return -1;
6929    }
6930    if xsd.is_null() {
6931        // Deactivate validation.
6932        if r.schema_owned && !r.schema.is_null() {
6933            // SAFETY: schema was compiled by xmlSchemaParse.
6934            unsafe { crate::xml::schemas::xmlSchemaFree(r.schema) };
6935        }
6936        r.schema = ptr::null_mut();
6937        r.schema_owned = false;
6938        r.xsd_result = -1;
6939        return 0;
6940    }
6941    // Compile the schema now. I/O and well-formedness diagnostics of the
6942    // schema document are raised by the parser machinery inside this call
6943    // (php prefixes them with XMLReader::setSchema()).
6944    let pctxt = crate::xml::schemas::xmlSchemaNewParserCtxt(xsd);
6945    if pctxt.is_null() {
6946        return -1;
6947    }
6948    let schema = crate::xml::schemas::xmlSchemaParse(pctxt);
6949    crate::xml::schemas::xmlSchemaFreeParserCtxt(pctxt);
6950    if schema.is_null() {
6951        return -1;
6952    }
6953    // Drop a previously attached reader-owned schema.
6954    if r.schema_owned && !r.schema.is_null() {
6955        // SAFETY: schema was compiled by xmlSchemaParse.
6956        unsafe { crate::xml::schemas::xmlSchemaFree(r.schema) };
6957    }
6958    r.schema = schema;
6959    r.schema_owned = true;
6960    r.xsd_result = -1;
6961    0
6962}
6963
6964/// `int xmlTextReaderSchemaValidateCtxt(xmlTextReaderPtr reader, xmlSchemaValidCtxtPtr ctxt, int options)`.
6965///
6966/// # SAFETY
6967///
6968/// - `reader`, `ctxt` must be valid pointers (or NULL
6969///   where the upstream C contract allows), obtained from the
6970///   matching constructor/owner and not yet freed; the callee may
6971///   take or keep ownership exactly as the C API specifies.
6972///
6973/// The caller must not race this call with concurrent mutation of the
6974/// same objects from other threads (per-object state is not internally
6975/// synchronized). Violating any of the above is undefined behavior.
6976///
6977/// Exercised by the C-API differential courts
6978/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
6979/// courts; those pass byte-for-byte against the upstream oracle.
6980#[no_mangle]
6981pub unsafe extern "C" fn xmlTextReaderSchemaValidateCtxt(
6982    reader: *mut XmlTextReader,
6983    ctxt: *mut c_void,
6984    _options: c_int,
6985) -> c_int {
6986    if reader.is_null() || ctxt.is_null() {
6987        return -1;
6988    }
6989    if unsafe { (*reader).doc }.is_null() && !unsafe { (*reader).parsed } {
6990        unsafe { (*reader).Read() };
6991    }
6992    crate::xml::schemas::xmlSchemaValidateDoc(ctxt, unsafe { (*reader).doc })
6993}
6994
6995/// `int xmlTextReaderSetSchema(xmlTextReaderPtr reader, xmlSchemaPtr schema)`.
6996///
6997/// # SAFETY
6998///
6999/// - `reader`, `schema` must be valid pointers (or NULL
7000///   where the upstream C contract allows), obtained from the
7001///   matching constructor/owner and not yet freed; the callee may
7002///   take or keep ownership exactly as the C API specifies.
7003///
7004/// The caller must not race this call with concurrent mutation of the
7005/// same objects from other threads (per-object state is not internally
7006/// synchronized). Violating any of the above is undefined behavior.
7007///
7008/// Exercised by the C-API differential courts
7009/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
7010/// courts; those pass byte-for-byte against the upstream oracle.
7011#[no_mangle]
7012pub unsafe extern "C" fn xmlTextReaderSetSchema(
7013    reader: *mut XmlTextReader,
7014    schema: *mut c_void,
7015) -> c_int {
7016    if reader.is_null() {
7017        return -1;
7018    }
7019    unsafe {
7020        let r = &mut *reader;
7021        // Drop a previously attached reader-owned schema (a caller-owned one
7022        // is released by its owner; upstream never frees the passed-in
7023        // schema).
7024        if r.schema_owned && !r.schema.is_null() && r.schema != schema {
7025            crate::xml::schemas::xmlSchemaFree(r.schema);
7026        }
7027        r.schema = schema;
7028        r.schema_owned = false;
7029        r.xsd_result = -1;
7030    }
7031    0
7032}
7033
7034/// `int xmlTextReaderRelaxNGValidate(xmlTextReaderPtr reader, const char *rng)`.
7035///
7036/// # SAFETY
7037///
7038/// - `reader` must be valid pointers (or NULL
7039///   where the upstream C contract allows), obtained from the
7040///   matching constructor/owner and not yet freed; the callee may
7041///   take or keep ownership exactly as the C API specifies.
7042///
7043/// - `rng` must point to valid NUL-terminated
7044///   strings (or NULL where the C contract allows) for the lifetime
7045///   of the call.
7046///
7047/// The caller must not race this call with concurrent mutation of the
7048/// same objects from other threads (per-object state is not internally
7049/// synchronized). Violating any of the above is undefined behavior.
7050///
7051/// Exercised by the C-API differential courts
7052/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
7053/// courts; those pass byte-for-byte against the upstream oracle.
7054#[no_mangle]
7055pub unsafe extern "C" fn xmlTextReaderRelaxNGValidate(
7056    reader: *mut XmlTextReader,
7057    rng: *const c_char,
7058) -> c_int {
7059    if reader.is_null() {
7060        return -1;
7061    }
7062    let r = unsafe { &mut *reader };
7063    if r.parsed {
7064        // Upstream: only XML_TEXTREADER_MODE_INITIAL readers can activate.
7065        return -1;
7066    }
7067    if rng.is_null() {
7068        // Deactivate validation.
7069        if r.rng_owned && !r.rng.is_null() {
7070            // SAFETY: rng was compiled by xmlRelaxNGParse.
7071            unsafe { crate::xml::relaxng::xmlRelaxNGFree(r.rng) };
7072        }
7073        r.rng = ptr::null_mut();
7074        r.rng_owned = false;
7075        r.rng_result = -1;
7076        return 0;
7077    }
7078    // Compile the schema now (I/O diagnostics of the schema resource are
7079    // raised inside this call); validation itself is deferred to read time.
7080    let pctxt = crate::xml::relaxng::xmlRelaxNGNewParserCtxt(rng);
7081    if pctxt.is_null() {
7082        return -1;
7083    }
7084    let schema = crate::xml::relaxng::xmlRelaxNGParse(pctxt);
7085    crate::xml::relaxng::xmlRelaxNGFreeParserCtxt(pctxt);
7086    if schema.is_null() {
7087        return -1;
7088    }
7089    if r.rng_owned && !r.rng.is_null() {
7090        // SAFETY: rng was compiled by xmlRelaxNGParse.
7091        unsafe { crate::xml::relaxng::xmlRelaxNGFree(r.rng) };
7092    }
7093    r.rng = schema;
7094    r.rng_owned = true;
7095    r.rng_result = -1;
7096    0
7097}
7098
7099/// `int xmlTextReaderRelaxNGValidateCtxt(xmlTextReaderPtr reader, xmlRelaxNGValidCtxtPtr ctxt, int options)`.
7100///
7101/// # SAFETY
7102///
7103/// - `reader`, `ctxt` must be valid pointers (or NULL
7104///   where the upstream C contract allows), obtained from the
7105///   matching constructor/owner and not yet freed; the callee may
7106///   take or keep ownership exactly as the C API specifies.
7107///
7108/// The caller must not race this call with concurrent mutation of the
7109/// same objects from other threads (per-object state is not internally
7110/// synchronized). Violating any of the above is undefined behavior.
7111///
7112/// Exercised by the C-API differential courts
7113/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
7114/// courts; those pass byte-for-byte against the upstream oracle.
7115#[no_mangle]
7116pub unsafe extern "C" fn xmlTextReaderRelaxNGValidateCtxt(
7117    reader: *mut XmlTextReader,
7118    ctxt: *mut c_void,
7119    _options: c_int,
7120) -> c_int {
7121    if reader.is_null() || ctxt.is_null() {
7122        return -1;
7123    }
7124    if unsafe { (*reader).doc }.is_null() && !unsafe { (*reader).parsed } {
7125        unsafe { (*reader).Read() };
7126    }
7127    crate::xml::relaxng::xmlRelaxNGValidateDoc(ctxt, unsafe { (*reader).doc })
7128}
7129
7130/// `int xmlTextReaderRelaxNGSetSchema(xmlTextReaderPtr reader, xmlRelaxNGPtr schema)`.
7131///
7132/// # SAFETY
7133///
7134/// - `reader`, `schema` must be valid pointers (or NULL
7135///   where the upstream C contract allows), obtained from the
7136///   matching constructor/owner and not yet freed; the callee may
7137///   take or keep ownership exactly as the C API specifies.
7138///
7139/// The caller must not race this call with concurrent mutation of the
7140/// same objects from other threads (per-object state is not internally
7141/// synchronized). Violating any of the above is undefined behavior.
7142///
7143/// Exercised by the C-API differential courts
7144/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
7145/// courts; those pass byte-for-byte against the upstream oracle.
7146#[no_mangle]
7147pub unsafe extern "C" fn xmlTextReaderRelaxNGSetSchema(
7148    reader: *mut XmlTextReader,
7149    schema: *mut c_void,
7150) -> c_int {
7151    if reader.is_null() {
7152        return -1;
7153    }
7154    unsafe {
7155        let r = &mut *reader;
7156        // Drop a previously attached reader-owned schema (a caller-owned one
7157        // — php keeps it in intern->schema — is released by its owner).
7158        if r.rng_owned && !r.rng.is_null() && r.rng != schema {
7159            crate::xml::relaxng::xmlRelaxNGFree(r.rng);
7160        }
7161        r.rng = schema;
7162        r.rng_owned = false;
7163        r.rng_result = -1;
7164    }
7165    0
7166}