Skip to main content

libxml_rs/xml/reader/
mod.rs

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