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