Skip to main content

libxml_rs/xml/reader/
mod.rs

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