Skip to main content

libxml_rs/abi/
exports_parser.rs

1//! exports_parser — C ABI exports for the XML parser family (§11.1-I).
2//!
3//! Implements the parser/parserInternals/xmlIO/encoding/tree export surface:
4//! parser-context creation and lifecycle, the `xmlCtxtRead*` family, parser
5//! input buffers and streams, encoding switches, the deprecated node-info
6//! sequence, global I/O callback registration, external-entity loaders, the
7//! `xmlFile*` I/O callbacks, low-level character scanning helpers and the
8//! SAX/DTD parse front-ends.
9//!
10//! Where an internal engine entry point exists (e.g. `crate::xml::parser::helpers`),
11//! the export wraps it; otherwise the function is ported from upstream
12//! `parser.c` / `parserInternals.c` / `xmlIO.c` / `error.c` / `encoding.c`
13//! (see `archaeology/libxml2-git`).
14//!
15//! # Upstream contract
16//!
17//! Parity target is upstream `parser.c`, `parserInternals.c`, `xmlIO.c`,
18//! `error.c` and `encoding.c` (libxml2 2.15.3) with the `parser.h`/
19//! `parserInternals.h`/`xmlIO.h` signatures. Residuals R-000164 (parser/tree
20//! structural parity), R-000165 (parser-context accessors and input
21//! constructors) and R-000169 (input filename ownership) all land here.
22//!
23//! # Conceptual behavior
24//!
25//! This module implements the parser export surface: parser-context
26//! creation/lifecycle, the `xmlCtxtRead*`/`xmlRead*` families, parser input
27//! buffers and streams, encoding switches, the deprecated node-info sequence,
28//! global I/O callback registration, external-entity loaders, the `xmlFile*`
29//! I/O callbacks and the SAX/DTD parse front-ends. Internal engine entry
30//! points are wrapped; the rest are ported from the upstream sources.
31//!
32//! # Ownership & safety invariants
33//!
34//! Parser contexts are caller-owned (freed with `xmlFreeParserCtxt`); docs
35//! returned by `xmlRead*` are caller-owned (freed with `xmlFreeDoc`); inputs
36//! created by `xmlNewInputFrom*` are owned by the context once pushed.
37//! Filenames stored in `_xmlParserInput.filename` and `doc->URL` are owned
38//! copies — R-000169 fixed xml_strndup on non-NUL-terminated Rust Strings
39//! (heap-buffer-overflow) and borrowed filename pointers.
40//!
41//! # Historical quirks & epochs
42//!
43//! QUIRK-0001/LORE-0001: since 2.9.0 (commit `52d8ade7`, 2012-07-30) default
44//! parser limits apply unless `XML_PARSE_HUGE` is set. E-002: parse-error
45//! diagnostics changed across 2.9.10 (non-recursive parser refactor) and
46//! 2.12.x (error-handling rework); E-005: exit codes reworked in 2.13.0.
47//! R-000164 (11.1-N) aligned the parse-time DOM construction with upstream
48//! (TREE-001 byte-identical).
49//!
50//! # Deliberate oddities
51//!
52//! The deprecated `xmlParse*` no-ops and the `xmlFileMatch`/
53//! `xmlParserInputRead` trivial bodies are deliberate (R-000138 set:
54//! upstreams own bodies are empty/trivial). `xmlReadMemory` accepts size-0
55//! input (R-000163) and applies options/URL on both success and recovery
56//! paths (R-000164).
57//!
58//! # Proving courts
59//!
60//! The PARSER court family, the ERROR-001 probe (error-family-probe.c, 48/48
61//! byte-identical), the TREE-001 structural probe and the DSO-LOADER/
62//! HEADER-COMPILE courts cover this module; the parser unit suite runs under
63//! cargo test.
64//!
65//! # Tempting simplifications that would break parity
66//!
67//! A tempting simplification is to store the input filename as a borrowed
68//! pointer into a Rust String — R-000169 proved that produces dangling
69//! `filename`/`doc->URL` pointers and heap-reuse garbage on the second parse;
70//! every construction path must own its filename copy. Another shortcut,
71//! skipping the `XML_PARSE_HUGE`/limits logic, would diverge from the 2.9.0+
72//! oracle on large documents (PARSER-LIMIT courts).
73
74#![allow(missing_docs)]
75#![allow(non_snake_case)]
76#![allow(non_camel_case_types)]
77#![allow(non_upper_case_globals)]
78
79// SAFETY-SCOPE: EXPORT-PARSER-MECHANICAL-001
80// (11.1-Z.3 proof scope, classified-generated) — this module is the
81// mechanical extern-"C" export surface: every `unsafe` block in it is
82// the documented indirection/registry-access pattern whose validity
83// rests on the upstream C contract, and the exported signatures are
84// machine-measured by the ABI-FUNCTION-SIGNATURE and DSO-LOADER
85// courts and the C-API differential probes. The safety contract of
86// each export is stated in its own doc comment; this scope covers the
87// mechanical wrappers' unsafe blocks.
88
89use core::ffi::CStr;
90use core::ptr;
91use std::os::raw::{c_char, c_int, c_long, c_uchar, c_uint, c_ulong, c_void};
92
93use parking_lot::Mutex;
94
95use crate::abi::allocator::{
96    xmlFreeImpl, xmlMallocImpl, xmlMallocZero, xmlMemStrdupImpl, xmlReallocImpl,
97};
98use crate::abi::callbacks::{
99    xmlGenericErrorFunc, xmlInputCloseCallback, xmlInputReadCallback, xmlOutputCloseCallback,
100    xmlOutputWriteCallback, xmlStructuredErrorFunc,
101};
102use crate::abi::structs::*;
103use crate::abi::types::*;
104use crate::xml::parser::helpers;
105use crate::xml::parser::input::InputBuffer;
106use crate::xml::{dtd, encoding, entities, errors, globals, io, string, tree};
107
108// ═══════════════════════════════════════════════════════════════════════════════
109// Local ABI types (upstream xmlIO.h / parser.h, not present in callbacks.rs)
110// ═══════════════════════════════════════════════════════════════════════════════
111
112/// `xmlInputMatchCallback` — decide whether a filename is handled by the
113/// registered input callback pair.
114type xmlInputMatchCallback = unsafe extern "C" fn(filename: *const c_char) -> c_int;
115
116/// `xmlInputOpenCallback` — open a resource and return an I/O context.
117type xmlInputOpenCallback = unsafe extern "C" fn(filename: *const c_char) -> *mut c_void;
118
119/// `xmlOutputMatchCallback` — decide whether a filename is handled by the
120/// registered output callback pair.
121type xmlOutputMatchCallback = unsafe extern "C" fn(filename: *const c_char) -> c_int;
122
123/// `xmlOutputOpenCallback` — open a resource for writing and return a context.
124type xmlOutputOpenCallback = unsafe extern "C" fn(filename: *const c_char) -> *mut c_void;
125
126/// `xmlExternalEntityLoader` — resolve an external entity to a parser input.
127type xmlExternalEntityLoader = unsafe extern "C" fn(
128    URL: *const c_char,
129    ID: *const c_char,
130    ctxt: *mut _xmlParserCtxt,
131) -> *mut _xmlParserInput;
132
133#[derive(Clone, Copy)]
134struct InputCallbackEntry {
135    matchcb: Option<xmlInputMatchCallback>,
136    opencb: Option<xmlInputOpenCallback>,
137    readcb: Option<xmlInputReadCallback>,
138    closecb: Option<xmlInputCloseCallback>,
139}
140
141#[allow(dead_code)]
142#[derive(Clone, Copy)]
143struct OutputCallbackEntry {
144    matchcb: Option<xmlOutputMatchCallback>,
145    opencb: Option<xmlOutputOpenCallback>,
146    writecb: Option<xmlOutputWriteCallback>,
147    closecb: Option<xmlOutputCloseCallback>,
148}
149
150static INPUT_CALLBACKS: Mutex<Vec<InputCallbackEntry>> = Mutex::new(Vec::new());
151static OUTPUT_CALLBACKS: Mutex<Vec<OutputCallbackEntry>> = Mutex::new(Vec::new());
152
153static EXTERNAL_ENTITY_LOADER: Mutex<Option<xmlExternalEntityLoader>> =
154    Mutex::new(Some(default_external_entity_loader));
155
156// Deprecated legacy function codes not present in types.rs (upstream xmlerror.h).
157const XML_ERR_USER_STOP: c_int = 111;
158#[allow(dead_code)]
159const XML_ERR_RESOURCE_LIMIT: c_int = 114;
160
161// XML_SCAN_* flags (upstream include/private/parser.h).
162const XML_SCAN_NC: c_int = 1;
163const XML_SCAN_NMTOKEN: c_int = 2;
164const XML_SCAN_OLD10: c_int = 4;
165
166// xmlParserLoadSubset bits (upstream parser.h, 2.15.3):
167//   XML_DETECT_IDS = 2, XML_COMPLETE_ATTRS = 4, XML_SKIP_IDS = 8.
168// These are internal bits of `ctxt->loadsubset`, NOT parse-option flags — they
169// must match the header values exactly or consumers that inspect `loadsubset`
170// directly (lxml's SAX target bridge gates default-attribute delivery on
171// `loadsubset & XML_COMPLETE_ATTRS`) drop DTD default attributes.
172#[allow(dead_code)]
173const XML_DETECT_IDS: c_int = 2;
174const XML_COMPLETE_ATTRS: c_int = 4;
175const XML_SKIP_IDS: c_int = 8;
176
177/// Keep enough input around to show errors in context (parserInternals.c).
178const LINE_LEN: usize = 80;
179
180/// Minimal amount of data the parser expects in the buffer (parserInternals.c).
181#[allow(dead_code)]
182const INPUT_CHUNK: usize = 100;
183
184const XML_INVALID_CHAR: c_int = -1;
185
186// ═══════════════════════════════════════════════════════════════════════════════
187// Internal helpers
188// ═══════════════════════════════════════════════════════════════════════════════
189
190/// Shared context initialisation: zeroes `ctxt`, installs the SAX handler and
191/// sets the initial parser state (upstream `xmlInitSAXParserCtxt`).
192///
193/// # Safety
194///
195/// `ctxt` must be a valid, writable, freshly allocated parser context.
196unsafe fn init_sax_parser_ctxt(
197    ctxt: *mut _xmlParserCtxt,
198    sax: *const _xmlSAXHandler,
199    userData: *mut c_void,
200) -> c_int {
201    unsafe {
202        ptr::write_bytes(ctxt as *mut u8, 0, core::mem::size_of::<_xmlParserCtxt>());
203
204        let c = &mut *ctxt;
205
206        // SAX handler.
207        if c.sax.is_null() {
208            let new_sax =
209                xmlMallocZero(core::mem::size_of::<_xmlSAXHandler>()) as *mut _xmlSAXHandler;
210            if new_sax.is_null() {
211                return -1;
212            }
213            c.sax = new_sax;
214        }
215        if sax.is_null() {
216            crate::xml::sax::xmlSAX2InitDefaultSAXHandler(c.sax);
217            c.userData = ctxt as *mut c_void;
218        } else if (*sax).initialized == XML_SAX2_MAGIC as c_uint {
219            // Full SAX2 handler copy.
220            ptr::copy_nonoverlapping(sax, c.sax, 1);
221            c.userData = if userData.is_null() {
222                ctxt as *mut c_void
223            } else {
224                userData
225            };
226        } else {
227            // SAX1 handler: only the V1 prefix is meaningful.
228            ptr::write_bytes(c.sax as *mut u8, 0, core::mem::size_of::<_xmlSAXHandler>());
229            ptr::copy_nonoverlapping(
230                sax as *const u8,
231                c.sax as *mut u8,
232                core::mem::size_of::<_xmlSAXHandlerV1>(),
233            );
234            c.userData = if userData.is_null() {
235                ctxt as *mut c_void
236            } else {
237                userData
238            };
239        }
240
241        c.wellFormed = 1;
242        c.standalone = -1;
243        c.errNo = XML_ERR_OK;
244        c.valid = 1;
245        c.nsWellFormed = 1;
246        c.instate = xmlParserInputState::XML_PARSER_START as c_int;
247        c.keepBlanks = globals::get_keep_blanks_default();
248        c.replaceEntities = globals::get_substitute_entities_default();
249        c.linenumbers = 1;
250        c.charset = xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int;
251        c.pedantic = globals::get_pedantic_parser_default();
252        c.loadsubset = globals::get_load_ext_dtd_default();
253        c.docdict = 1;
254        c.options = 0;
255
256        c.vctxt.userData = ctxt as *mut c_void;
257        c.vctxt.valid = 1;
258    }
259    0
260}
261
262/// Mirror `options` into the parser context's historical struct members
263/// (upstream `xmlCtxtSetOptionsInternal`).
264///
265/// `keepBlanks` is special: the executed 2.15.3 oracle seeds it from the
266/// deprecated `xmlKeepBlanksDefaultValue` at context creation and only ever
267/// LOWERS it (XML_PARSE_NOBLANKS) — option application never re-raises it.
268/// Empirical: a context created while `xmlKeepBlanksDefault(0)` drops
269/// whitespace-only text for ALL its reads, even reused ones, and
270/// `xmlCtxtUseOptions`/read options without NOBLANKS do not restore it.
271///
272/// # Safety
273///
274/// `ctxt` must be a valid, writable parser context.
275pub(crate) unsafe fn apply_options(ctxt: *mut _xmlParserCtxt, options: c_int) {
276    unsafe {
277        let c = &mut *ctxt;
278        c.options = options;
279        c.recovery = (options & XML_PARSE_RECOVER != 0) as c_int;
280        c.replaceEntities = (options & XML_PARSE_NOENT != 0) as c_int;
281        // UPSTREAM-PARITY (parser.c xmlCtxtUseOptions):
282        //   ctxt->loadsubset = (options & XML_PARSE_DTDLOAD) ? XML_DETECT_IDS : 0;
283        //   ctxt->loadsubset |= (options & XML_PARSE_DTDATTR) ? XML_COMPLETE_ATTRS : 0;
284        //   ctxt->loadsubset |= (options & XML_PARSE_SKIP_IDS) ? XML_SKIP_IDS : 0;
285        // `loadsubset` is an ABI-visible field; lxml's SAX target bridge reads
286        // `loadsubset & XML_COMPLETE_ATTRS` to decide whether to deliver DTD
287        // default attributes, so the bit values must be the upstream ones
288        // (XML_DETECT_IDS=2, XML_COMPLETE_ATTRS=4, XML_SKIP_IDS=8).
289        c.loadsubset = if options & XML_PARSE_DTDLOAD != 0 {
290            XML_DETECT_IDS
291        } else {
292            0
293        };
294        if options & XML_PARSE_DTDATTR != 0 {
295            c.loadsubset |= XML_COMPLETE_ATTRS;
296        }
297        if options & XML_PARSE_SKIP_IDS != 0 {
298            c.loadsubset |= XML_SKIP_IDS;
299        }
300        c.validate = (options & XML_PARSE_DTDVALID != 0) as c_int;
301        c.pedantic = (options & XML_PARSE_PEDANTIC != 0) as c_int;
302        if options & XML_PARSE_NOBLANKS != 0 {
303            c.keepBlanks = 0;
304        }
305        c.dictNames = if options & XML_PARSE_NODICT != 0 {
306            0
307        } else {
308            1
309        };
310    }
311}
312
313/// Find the registered encoding handler for an `xmlCharEncoding` value, or NULL.
314unsafe fn encoding_handler_for(enc: c_int) -> *mut _xmlCharEncodingHandler {
315    let e: xmlCharEncoding = unsafe { core::mem::transmute(enc) };
316    match encoding::encoding_name(e) {
317        Some(name) => {
318            let mut nul = name.to_vec();
319            nul.push(0);
320            encoding::find_encoding_handler(nul.as_ptr() as *const xmlChar)
321        }
322        None => ptr::null_mut(),
323    }
324}
325
326/// Build a `_xmlParserInput` that references the data owned by `buf` (an input
327/// buffer previously created by the xmlIO layer). The buffer keeps the data
328/// alive; the returned input must be freed with `helpers::free_parser_input`.
329///
330/// # Safety
331///
332/// `buf` must be a valid input buffer or NULL, and must outlive the returned
333/// input.
334unsafe fn parser_input_from_buf(buf: *mut _xmlParserInputBuffer) -> *mut _xmlParserInput {
335    let input =
336        unsafe { xmlMallocZero(core::mem::size_of::<_xmlParserInput>()) } as *mut _xmlParserInput;
337    if input.is_null() {
338        return ptr::null_mut();
339    }
340    unsafe {
341        (*input).buf = buf;
342        (*input).line = 1;
343        (*input).col = 1;
344        if !buf.is_null() {
345            let b = &*buf;
346            if !b.buffer.is_null() {
347                let xbuf = &*(b.buffer as *mut _xmlBuffer);
348                if !xbuf.content.is_null() {
349                    (*input).base = xbuf.content;
350                    (*input).cur = xbuf.content;
351                    (*input).end = xbuf.content.add(xbuf.use_ as usize);
352                    (*input).length = xbuf.use_ as c_int;
353                }
354            }
355        }
356    }
357    input
358}
359
360/// `pub(crate)` wrapper of [`parser_input_from_buf`] for the sibling
361/// xmlNewInputFrom* family (11.1-X R-000165 closure).
362pub(crate) unsafe fn parser_input_from_buf_pub(
363    buf: *mut _xmlParserInputBuffer,
364) -> *mut _xmlParserInput {
365    unsafe { parser_input_from_buf(buf) }
366}
367
368/// Materialise an `InputBuffer` (owned copy) from a raw `_xmlParserInput`,
369/// so the data survives the caller's input lifetime.
370///
371/// # Safety
372///
373/// `input` must be a valid pointer to a `_xmlParserInput`.
374unsafe fn input_buffer_from_parser_input(input: *mut _xmlParserInput) -> InputBuffer {
375    unsafe {
376        let pi = &*input;
377        if !pi.buf.is_null() {
378            let b = &*pi.buf;
379            if let Some(read) = b.readcallback {
380                return helpers::input_from_io(Some(read), b.closecallback, b.context);
381            }
382        }
383        if !pi.base.is_null() && !pi.end.is_null() && pi.end >= pi.base {
384            let len = (pi.end as usize).saturating_sub(pi.base as usize);
385            let slice = core::slice::from_raw_parts(pi.base, len);
386            return InputBuffer::from_memory(slice, None);
387        }
388        InputBuffer::from_memory(&[], None)
389    }
390}
391
392/// Core of `xmlCtxtRead*`: reset the context, wire an input buffer, parse,
393/// and return the resulting document (freed on hard error unless recovery).
394///
395/// # Safety
396///
397/// `ctxt` must be a valid parser context; `input` is consumed.
398unsafe fn ctxt_read_doc(
399    ctxt: *mut _xmlParserCtxt,
400    input: InputBuffer,
401    url: *const c_char,
402    options: c_int,
403) -> *mut _xmlDoc {
404    unsafe {
405        xmlCtxtReset(ctxt);
406        apply_options(ctxt, options);
407        helpers::setup_parser_input(ctxt, input);
408        if helpers::parse_document(ctxt) != 0 {
409            let doc = (*ctxt).myDoc;
410            (*ctxt).myDoc = ptr::null_mut();
411            if options & XML_PARSE_RECOVER != 0 {
412                return doc;
413            }
414            if !doc.is_null() {
415                tree::free_doc(doc);
416            }
417            return ptr::null_mut();
418        }
419        let doc = (*ctxt).myDoc;
420        // UPSTREAM-PARITY (xmlDoRead/xmlCtxtReadMemory): the parsed document
421        // is returned to the CALLER; the context no longer owns it. Nulling
422        // myDoc here prevents xmlCtxtReset (the next ctxt_read_doc call) from
423        // double-freeing a doc the caller already freed (parse_ctx_reuse).
424        (*ctxt).myDoc = ptr::null_mut();
425        if !doc.is_null() && !url.is_null() {
426            (*doc).URL = string::xml_strdup(url as *const xmlChar);
427        }
428        doc
429    }
430}
431
432/// Parse DTD declaration text using the internal engine by wrapping it in a
433/// synthetic document (`<!DOCTYPE none [ ... ]><none/>`) when the text is a
434/// bare DTD subset, or parsing it directly when it is already a document.
435///
436/// Returns a detached DTD (never owned by a document), or NULL.
437///
438/// # Safety
439///
440/// `ctxt` must be a valid parser context; `data` must be readable for `len`
441/// bytes.
442unsafe fn parse_dtd_text(
443    ctxt: *mut _xmlParserCtxt,
444    data: &[u8],
445    public_id: *const xmlChar,
446    system_id: *const xmlChar,
447) -> *mut _xmlDtd {
448    unsafe {
449        // If the content is already a full document (contains a DOCTYPE),
450        // parse it directly; otherwise wrap the declarations.
451        let has_doctype = data
452            .windows(9)
453            .any(|w| w.eq_ignore_ascii_case(b"<!DOCTYPE"));
454        let mut wrapped: Vec<u8>;
455        let parse_data: &[u8] = if has_doctype {
456            data
457        } else {
458            wrapped = Vec::with_capacity(data.len() + 32);
459            wrapped.extend_from_slice(b"<!DOCTYPE none [");
460            wrapped.extend_from_slice(data);
461            wrapped.extend_from_slice(b"]><none/>");
462            &wrapped
463        };
464
465        let input = InputBuffer::from_memory(parse_data, None);
466        helpers::setup_parser_input(ctxt, input);
467        let rc = helpers::parse_document(ctxt);
468        let doc = (*ctxt).myDoc;
469        (*ctxt).myDoc = ptr::null_mut();
470
471        if rc == 0 && !doc.is_null() && !(*doc).intSubset.is_null() {
472            let dtd = (*doc).intSubset;
473            (*doc).intSubset = ptr::null_mut();
474            (*dtd).parent = ptr::null_mut();
475            (*dtd).doc = ptr::null_mut();
476            if !public_id.is_null() {
477                (*dtd).ExternalID = string::xml_strdup(public_id);
478            }
479            if !system_id.is_null() {
480                (*dtd).SystemID = string::xml_strdup(system_id);
481            }
482            tree::free_doc(doc);
483            return dtd;
484        }
485
486        if !doc.is_null() {
487            tree::free_doc(doc);
488        }
489        // Fallback: an empty DTD carrying the identifiers.
490
491        dtd::new_dtd(
492            ptr::null_mut(),
493            c"none".as_ptr() as *const xmlChar,
494            public_id,
495            system_id,
496        )
497    }
498}
499
500// ═══════════════════════════════════════════════════════════════════════════════
501// Context creation / lifecycle
502// ═══════════════════════════════════════════════════════════════════════════════
503
504/// Create a new parser context with a default SAX2 handler.
505///
506/// # UPSTREAM-PARITY
507///
508/// ```c
509/// xmlParserCtxtPtr xmlNewParserCtxt(void);
510/// ```
511///
512/// # SAFETY
513///
514/// The function touches crate-global state only; it is safe
515/// as long as the caller respects the library's global
516/// initialization/cleanup ordering (xmlInitParser before use,
517/// xmlCleanupParser only after all users are done).
518///
519/// Violating the global lifecycle ordering, or calling this after
520/// teardown or from a signal handler, is undefined behavior.
521#[no_mangle]
522pub unsafe extern "C" fn xmlNewParserCtxt() -> *mut _xmlParserCtxt {
523    unsafe {
524        globals::init_parser();
525        let ctxt = xmlMallocZero(core::mem::size_of::<_xmlParserCtxt>()) as *mut _xmlParserCtxt;
526        if ctxt.is_null() {
527            return ptr::null_mut();
528        }
529        if init_sax_parser_ctxt(ctxt, ptr::null(), ptr::null_mut()) < 0 {
530            helpers::free_parser_ctxt(ctxt);
531            return ptr::null_mut();
532        }
533        ctxt
534    }
535}
536
537/// Create a new parser context using the given SAX handler (or the default
538/// SAX2 handler when `sax` is NULL).
539///
540/// # UPSTREAM-PARITY
541///
542/// ```c
543/// xmlParserCtxtPtr xmlNewSAXParserCtxt(const xmlSAXHandler *sax, void *userData);
544/// ```
545///
546/// # SAFETY
547///
548/// - `sax`, `userData` must be valid pointers (or NULL
549///   where the upstream C contract allows), obtained from the
550///   matching constructor/owner and not yet freed; the callee may
551///   take or keep ownership exactly as the C API specifies.
552///
553/// The caller must not race this call with concurrent mutation of the
554/// same objects from other threads (per-object state is not internally
555/// synchronized). Violating any of the above is undefined behavior.
556///
557/// Exercised by the C-API differential courts
558/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
559/// courts; those pass byte-for-byte against the upstream oracle.
560#[no_mangle]
561pub unsafe extern "C" fn xmlNewSAXParserCtxt(
562    sax: *const _xmlSAXHandler,
563    userData: *mut c_void,
564) -> *mut _xmlParserCtxt {
565    unsafe {
566        globals::init_parser();
567        let ctxt = xmlMallocZero(core::mem::size_of::<_xmlParserCtxt>()) as *mut _xmlParserCtxt;
568        if ctxt.is_null() {
569            return ptr::null_mut();
570        }
571        if init_sax_parser_ctxt(ctxt, sax, userData) < 0 {
572            helpers::free_parser_ctxt(ctxt);
573            return ptr::null_mut();
574        }
575        ctxt
576    }
577}
578
579/// Initialise a parser context (legacy API): zeroes the context, installs a
580/// default SAX2 handler and sets the initial parser state.
581///
582/// # UPSTREAM-PARITY
583///
584/// ```c
585/// int xmlInitParserCtxt(xmlParserCtxtPtr ctxt);
586/// ```
587///
588/// # SAFETY
589///
590/// - `ctxt` must be valid pointers (or NULL
591///   where the upstream C contract allows), obtained from the
592///   matching constructor/owner and not yet freed; the callee may
593///   take or keep ownership exactly as the C API specifies.
594///
595/// The caller must not race this call with concurrent mutation of the
596/// same objects from other threads (per-object state is not internally
597/// synchronized). Violating any of the above is undefined behavior.
598///
599/// Exercised by the C-API differential courts
600/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
601/// courts; those pass byte-for-byte against the upstream oracle.
602#[no_mangle]
603pub unsafe extern "C" fn xmlInitParserCtxt(ctxt: *mut _xmlParserCtxt) -> c_int {
604    unsafe { init_sax_parser_ctxt(ctxt, ptr::null(), ptr::null_mut()) }
605}
606
607/// Clear (reset) a parser context.
608///
609/// # UPSTREAM-PARITY
610///
611/// ```c
612/// void xmlClearParserCtxt(xmlParserCtxtPtr ctxt);
613/// ```
614///
615/// # SAFETY
616///
617/// - `ctxt` must be valid pointers (or NULL
618///   where the upstream C contract allows), obtained from the
619///   matching constructor/owner and not yet freed; the callee may
620///   take or keep ownership exactly as the C API specifies.
621///
622/// The caller must not race this call with concurrent mutation of the
623/// same objects from other threads (per-object state is not internally
624/// synchronized). Violating any of the above is undefined behavior.
625///
626/// Exercised by the C-API differential courts
627/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
628/// courts; those pass byte-for-byte against the upstream oracle.
629#[no_mangle]
630pub unsafe extern "C" fn xmlClearParserCtxt(ctxt: *mut _xmlParserCtxt) {
631    unsafe { xmlCtxtReset(ctxt) }
632}
633
634/// Reset a parser context: drop the input stack, node/name stacks, strings,
635/// document and error state so the context can be reused.
636///
637/// # UPSTREAM-PARITY
638///
639/// ```c
640/// void xmlCtxtReset(xmlParserCtxtPtr ctxt);
641/// ```
642///
643/// # SAFETY
644///
645/// - `ctxt` must be valid pointers (or NULL
646///   where the upstream C contract allows), obtained from the
647///   matching constructor/owner and not yet freed; the callee may
648///   take or keep ownership exactly as the C API specifies.
649///
650/// The caller must not race this call with concurrent mutation of the
651/// same objects from other threads (per-object state is not internally
652/// synchronized). Violating any of the above is undefined behavior.
653///
654/// Exercised by the C-API differential courts
655/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
656/// courts; those pass byte-for-byte against the upstream oracle.
657#[no_mangle]
658pub unsafe extern "C" fn xmlCtxtReset(ctxt: *mut _xmlParserCtxt) {
659    if ctxt.is_null() {
660        return;
661    }
662    unsafe {
663        let c = &mut *ctxt;
664
665        // Free all inputs on the stack.
666        let input_nr = c.inputNr;
667        let input_tab = c.inputTab;
668        if !input_tab.is_null() {
669            for i in 0..input_nr {
670                let input = *input_tab.add(i as usize);
671                if !input.is_null() {
672                    helpers::free_parser_input(input);
673                }
674            }
675            xmlFreeImpl(input_tab as *mut c_void);
676        }
677        c.inputTab = ptr::null_mut();
678        c.inputMax = 0;
679        c.inputNr = 0;
680        c.input = ptr::null_mut();
681
682        // Free the stored InputBuffer (stashed by setup_parser_input; the
683        // side table keeps ctxt._private application data — 11.1-X).
684        helpers::free_stashed_input_buffer(ctxt);
685        // Drop any incremental-push state (SP-14.3.1-3).
686        helpers::free_push_state(ctxt);
687
688        // Node stack (array only; nodes are owned by the doc).
689        if !c.nodeTab.is_null() {
690            xmlFreeImpl(c.nodeTab as *mut c_void);
691        }
692        c.nodeTab = ptr::null_mut();
693        c.nodeMax = 0;
694        c.nodeNr = 0;
695        c.node = ptr::null_mut();
696
697        // Name stack: reclaim every live entry (heap NUL-terminated name
698        // owned by the context nameTab) before the array (Phase 16 ASan fuzz
699        // fix — mirrors free_parser_ctxt).
700        if !c.nameTab.is_null() {
701            for i in 0..c.nameNr {
702                let entry = *c.nameTab.add(i as usize);
703                if !entry.is_null() {
704                    xmlFreeImpl(entry as *mut c_void);
705                }
706            }
707            xmlFreeImpl(c.nameTab as *mut c_void);
708        }
709        c.nameTab = ptr::null_mut();
710        c.nameMax = 0;
711        c.nameNr = 0;
712        c.name = ptr::null();
713
714        // Space stack: keep the allocation, reset the counter.
715        c.spaceNr = 0;
716        c.space = ptr::null_mut();
717
718        // Namespaces.
719        c.nsNr = 0;
720
721        // Strings owned by the context.
722        if !c.version.is_null() {
723            xmlFreeImpl(c.version as *mut c_void);
724            c.version = ptr::null_mut();
725        }
726        if !c.encoding.is_null() {
727            xmlFreeImpl(c.encoding as *mut c_void);
728            c.encoding = ptr::null_mut();
729        }
730        if !c.extSubURI.is_null() {
731            xmlFreeImpl(c.extSubURI as *mut c_void);
732            c.extSubURI = ptr::null_mut();
733        }
734        if !c.extSubSystem.is_null() {
735            xmlFreeImpl(c.extSubSystem as *mut c_void);
736            c.extSubSystem = ptr::null_mut();
737        }
738        if !c.directory.is_null() {
739            xmlFreeImpl(c.directory as *mut c_void);
740            c.directory = ptr::null_mut();
741        }
742
743        // Document: the context owns it until reset/free.
744        if !c.myDoc.is_null() {
745            tree::free_doc(c.myDoc);
746        }
747        c.myDoc = ptr::null_mut();
748
749        // Parser state.
750        c.standalone = -1;
751        c.hasExternalSubset = 0;
752        c.hasPErefs = 0;
753        c.instate = xmlParserInputState::XML_PARSER_START as c_int;
754        c.wellFormed = 1;
755        c.nsWellFormed = 1;
756        c.disableSAX = 0;
757        c.valid = 1;
758        c.record_info = 0;
759        c.checkIndex = 0;
760        c.inSubset = 0;
761        c.errNo = XML_ERR_OK;
762        c.depth = 0;
763        c.nbentities = 0;
764        c.sizeentities = 0;
765        c.nbErrors = 0;
766        c.nbWarnings = 0;
767
768        xmlInitNodeInfoSeq(&mut c.node_seq);
769
770        if c.lastError.code != XML_ERR_OK {
771            errors::reset_error(&mut c.lastError);
772        }
773    }
774}
775
776/// Reset a push-parser context and set up a fresh input chunk.
777///
778/// # UPSTREAM-PARITY
779///
780/// ```c
781/// int xmlCtxtResetPush(xmlParserCtxtPtr ctxt, const char *chunk, int size,
782///                      const char *filename, const char *encoding);
783/// ```
784///
785/// # SAFETY
786///
787/// - `ctxt` must be valid pointers (or NULL
788///   where the upstream C contract allows), obtained from the
789///   matching constructor/owner and not yet freed; the callee may
790///   take or keep ownership exactly as the C API specifies.
791///
792/// - `chunk`, `filename`, `encoding` must point to valid NUL-terminated
793///   strings (or NULL where the C contract allows) for the lifetime
794///   of the call.
795///
796/// The caller must not race this call with concurrent mutation of the
797/// same objects from other threads (per-object state is not internally
798/// synchronized). Violating any of the above is undefined behavior.
799///
800/// Exercised by the C-API differential courts
801/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
802/// courts; those pass byte-for-byte against the upstream oracle.
803#[no_mangle]
804pub unsafe extern "C" fn xmlCtxtResetPush(
805    ctxt: *mut _xmlParserCtxt,
806    chunk: *const c_char,
807    size: c_int,
808    filename: *const c_char,
809    encoding: *const c_char,
810) -> c_int {
811    if ctxt.is_null() {
812        return 1;
813    }
814    unsafe {
815        xmlCtxtReset(ctxt);
816
817        let slice = if size > 0 && !chunk.is_null() {
818            core::slice::from_raw_parts(chunk as *const u8, size as usize)
819        } else {
820            &[]
821        };
822        let uri = if filename.is_null() {
823            None
824        } else {
825            CStr::from_ptr(filename).to_str().ok()
826        };
827        let input = InputBuffer::from_memory(slice, uri);
828        helpers::setup_parser_input(ctxt, input);
829
830        if !encoding.is_null() {
831            let handler = encoding::find_encoding_handler(encoding as *const xmlChar);
832            if !handler.is_null() {
833                xmlSwitchToEncoding(ctxt, handler);
834            }
835        }
836    }
837    0
838}
839
840/// Apply a full set of parser options, clearing options not present.
841///
842/// # UPSTREAM-PARITY
843///
844/// ```c
845/// int xmlCtxtSetOptions(xmlParserCtxtPtr ctxt, int options);
846/// ```
847///
848/// # SAFETY
849///
850/// - `ctxt` must be valid pointers (or NULL
851///   where the upstream C contract allows), obtained from the
852///   matching constructor/owner and not yet freed; the callee may
853///   take or keep ownership exactly as the C API specifies.
854///
855/// The caller must not race this call with concurrent mutation of the
856/// same objects from other threads (per-object state is not internally
857/// synchronized). Violating any of the above is undefined behavior.
858///
859/// Exercised by the C-API differential courts
860/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
861/// courts; those pass byte-for-byte against the upstream oracle.
862#[no_mangle]
863pub unsafe extern "C" fn xmlCtxtSetOptions(ctxt: *mut _xmlParserCtxt, options: c_int) -> c_int {
864    if ctxt.is_null() {
865        return -1;
866    }
867    const ALL_MASK: c_int = XML_PARSE_RECOVER
868        | XML_PARSE_NOENT
869        | XML_PARSE_DTDLOAD
870        | XML_PARSE_DTDATTR
871        | XML_PARSE_DTDVALID
872        | XML_PARSE_NOERROR
873        | XML_PARSE_NOWARNING
874        | XML_PARSE_PEDANTIC
875        | XML_PARSE_NOBLANKS
876        | XML_PARSE_SAX1
877        | XML_PARSE_NONET
878        | XML_PARSE_NODICT
879        | XML_PARSE_NSCLEAN
880        | XML_PARSE_NOCDATA
881        | XML_PARSE_COMPACT
882        | XML_PARSE_OLD10
883        | XML_PARSE_HUGE
884        | XML_PARSE_OLDSAX
885        | XML_PARSE_IGNORE_ENC
886        | XML_PARSE_BIG_LINES;
887
888    unsafe {
889        apply_options(ctxt, options & ALL_MASK);
890    }
891    options & !ALL_MASK
892}
893
894/// Install a per-context structured error handler.
895///
896/// # UPSTREAM-PARITY
897///
898/// ```c
899/// void xmlCtxtSetErrorHandler(xmlParserCtxtPtr ctxt,
900///                             xmlStructuredErrorFunc handler, void *data);
901/// ```
902///
903/// # SAFETY
904///
905/// - `ctxt`, `data` must be valid pointers (or NULL
906///   where the upstream C contract allows), obtained from the
907///   matching constructor/owner and not yet freed; the callee may
908///   take or keep ownership exactly as the C API specifies.
909///
910/// - `handler` must be a valid callback (or None);
911///   the callback is invoked with the documented context pointer and
912///   must itself uphold the same pointer invariants.
913///
914/// The caller must not race this call with concurrent mutation of the
915/// same objects from other threads (per-object state is not internally
916/// synchronized). Violating any of the above is undefined behavior.
917///
918/// Exercised by the C-API differential courts
919/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
920/// courts; those pass byte-for-byte against the upstream oracle.
921#[no_mangle]
922pub unsafe extern "C" fn xmlCtxtSetErrorHandler(
923    ctxt: *mut _xmlParserCtxt,
924    handler: Option<xmlStructuredErrorFunc>,
925    data: *mut c_void,
926) {
927    if ctxt.is_null() {
928        return;
929    }
930    unsafe {
931        (*ctxt).errorHandler = handler;
932        (*ctxt).errorCtxt = data;
933    }
934}
935
936/// Set the maximum entity expansion amplification factor.
937///
938/// # UPSTREAM-PARITY
939///
940/// ```c
941/// void xmlCtxtSetMaxAmplification(xmlParserCtxtPtr ctxt, unsigned maxAmpl);
942/// ```
943///
944/// # SAFETY
945///
946/// - `ctxt` must be valid pointers (or NULL
947///   where the upstream C contract allows), obtained from the
948///   matching constructor/owner and not yet freed; the callee may
949///   take or keep ownership exactly as the C API specifies.
950///
951/// The caller must not race this call with concurrent mutation of the
952/// same objects from other threads (per-object state is not internally
953/// synchronized). Violating any of the above is undefined behavior.
954///
955/// Exercised by the C-API differential courts
956/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
957/// courts; those pass byte-for-byte against the upstream oracle.
958#[no_mangle]
959pub unsafe extern "C" fn xmlCtxtSetMaxAmplification(ctxt: *mut _xmlParserCtxt, maxAmpl: c_uint) {
960    if ctxt.is_null() || maxAmpl == 0 {
961        return;
962    }
963    unsafe {
964        (*ctxt).maxAmpl = maxAmpl;
965    }
966}
967
968/// Get the last error raised on the context, or NULL.
969///
970/// # UPSTREAM-PARITY
971///
972/// ```c
973/// const xmlError *xmlCtxtGetLastError(void *ctx);
974/// ```
975///
976/// # SAFETY
977///
978/// - `ctx` must be valid pointers (or NULL
979///   where the upstream C contract allows), obtained from the
980///   matching constructor/owner and not yet freed; the callee may
981///   take or keep ownership exactly as the C API specifies.
982///
983/// The caller must not race this call with concurrent mutation of the
984/// same objects from other threads (per-object state is not internally
985/// synchronized). Violating any of the above is undefined behavior.
986///
987/// Exercised by the C-API differential courts
988/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
989/// courts; those pass byte-for-byte against the upstream oracle.
990#[no_mangle]
991pub unsafe extern "C" fn xmlCtxtGetLastError(ctx: *mut c_void) -> *const _xmlError {
992    if ctx.is_null() {
993        return ptr::null();
994    }
995    let ctxt = ctx as *mut _xmlParserCtxt;
996    unsafe {
997        if (*ctxt).lastError.code == XML_ERR_OK {
998            return ptr::null();
999        }
1000        &(*ctxt).lastError
1001    }
1002}
1003
1004/// Reset the context's last-error state.
1005///
1006/// # UPSTREAM-PARITY
1007///
1008/// ```c
1009/// void xmlCtxtResetLastError(void *ctx);
1010/// ```
1011///
1012/// # SAFETY
1013///
1014/// - `ctx` must be valid pointers (or NULL
1015///   where the upstream C contract allows), obtained from the
1016///   matching constructor/owner and not yet freed; the callee may
1017///   take or keep ownership exactly as the C API specifies.
1018///
1019/// The caller must not race this call with concurrent mutation of the
1020/// same objects from other threads (per-object state is not internally
1021/// synchronized). Violating any of the above is undefined behavior.
1022///
1023/// Exercised by the C-API differential courts
1024/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1025/// courts; those pass byte-for-byte against the upstream oracle.
1026#[no_mangle]
1027pub unsafe extern "C" fn xmlCtxtResetLastError(ctx: *mut c_void) {
1028    if ctx.is_null() {
1029        return;
1030    }
1031    let ctxt = ctx as *mut _xmlParserCtxt;
1032    unsafe {
1033        (*ctxt).errNo = XML_ERR_OK;
1034        if (*ctxt).lastError.code != XML_ERR_OK {
1035            // Upstream xmlResetError frees the owned strings.
1036            crate::xml::globals::free_error_strings(&(*ctxt).lastError);
1037            errors::reset_error(&mut (*ctxt).lastError);
1038        }
1039    }
1040}
1041
1042/// Handle an out-of-memory error on a parser context.
1043///
1044/// # UPSTREAM-PARITY
1045///
1046/// ```c
1047/// void xmlCtxtErrMemory(xmlParserCtxtPtr ctxt);
1048/// ```
1049///
1050/// # SAFETY
1051///
1052/// - `ctxt` must be valid pointers (or NULL
1053///   where the upstream C contract allows), obtained from the
1054///   matching constructor/owner and not yet freed; the callee may
1055///   take or keep ownership exactly as the C API specifies.
1056///
1057/// The caller must not race this call with concurrent mutation of the
1058/// same objects from other threads (per-object state is not internally
1059/// synchronized). Violating any of the above is undefined behavior.
1060///
1061/// Exercised by the C-API differential courts
1062/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1063/// courts; those pass byte-for-byte against the upstream oracle.
1064#[no_mangle]
1065pub unsafe extern "C" fn xmlCtxtErrMemory(ctxt: *mut _xmlParserCtxt) {
1066    if ctxt.is_null() {
1067        return;
1068    }
1069    unsafe {
1070        let c = &mut *ctxt;
1071        c.errNo = XML_ERR_NO_MEMORY;
1072        c.instate = xmlParserInputState::XML_PARSER_EOF as c_int;
1073        c.wellFormed = 0;
1074        c.disableSAX = 2;
1075
1076        c.lastError.domain = XML_FROM_PARSER;
1077        c.lastError.code = XML_ERR_NO_MEMORY;
1078        c.lastError.level = xmlErrorLevel::XML_ERR_FATAL as c_int;
1079        // Owned copy (upstream xmlRaiseMemoryError): the per-context last
1080        // error strings are freed on reset/free, so static literals would be
1081        // a double-free/UB hazard.
1082        c.lastError.message =
1083            crate::abi::allocator::xmlMemStrdupImpl(c"out of memory\n".as_ptr()) as *mut c_char;
1084
1085        if let Some(handler) = c.errorHandler {
1086            handler(c.errorCtxt, &c.lastError);
1087        } else if !c.sax.is_null() {
1088            if let Some(serror) = (*c.sax).serror {
1089                serror(c.userData, &c.lastError);
1090            }
1091        }
1092    }
1093}
1094
1095/// Stop the parser: no further processing will happen.
1096///
1097/// # UPSTREAM-PARITY
1098///
1099/// ```c
1100/// void xmlStopParser(xmlParserCtxtPtr ctxt);
1101/// ```
1102///
1103/// # SAFETY
1104///
1105/// - `ctxt` must be valid pointers (or NULL
1106///   where the upstream C contract allows), obtained from the
1107///   matching constructor/owner and not yet freed; the callee may
1108///   take or keep ownership exactly as the C API specifies.
1109///
1110/// The caller must not race this call with concurrent mutation of the
1111/// same objects from other threads (per-object state is not internally
1112/// synchronized). Violating any of the above is undefined behavior.
1113///
1114/// Exercised by the C-API differential courts
1115/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1116/// courts; those pass byte-for-byte against the upstream oracle.
1117#[no_mangle]
1118pub unsafe extern "C" fn xmlStopParser(ctxt: *mut _xmlParserCtxt) {
1119    if ctxt.is_null() {
1120        return;
1121    }
1122    unsafe {
1123        (*ctxt).disableSAX = 2;
1124        if (*ctxt).errNo == XML_ERR_OK {
1125            (*ctxt).errNo = XML_ERR_USER_STOP;
1126            (*ctxt).lastError.code = XML_ERR_USER_STOP;
1127            (*ctxt).wellFormed = 0;
1128        }
1129    }
1130}
1131
1132/// Return the byte offset of the current parse position within the current
1133/// entity, or -1 when it cannot be computed.
1134///
1135/// # UPSTREAM-PARITY
1136///
1137/// ```c
1138/// long xmlByteConsumed(xmlParserCtxtPtr ctxt);
1139/// ```
1140///
1141/// # SAFETY
1142///
1143/// - `ctxt` must be valid pointers (or NULL
1144///   where the upstream C contract allows), obtained from the
1145///   matching constructor/owner and not yet freed; the callee may
1146///   take or keep ownership exactly as the C API specifies.
1147///
1148/// The caller must not race this call with concurrent mutation of the
1149/// same objects from other threads (per-object state is not internally
1150/// synchronized). Violating any of the above is undefined behavior.
1151///
1152/// Exercised by the C-API differential courts
1153/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1154/// courts; those pass byte-for-byte against the upstream oracle.
1155#[no_mangle]
1156pub unsafe extern "C" fn xmlByteConsumed(ctxt: *mut _xmlParserCtxt) -> c_long {
1157    if ctxt.is_null() {
1158        return -1;
1159    }
1160    unsafe {
1161        let input = (*ctxt).input;
1162        if input.is_null() {
1163            return -1;
1164        }
1165        if !(*input).buf.is_null() && !(*(*input).buf).encoder.is_null() {
1166            // With an encoder we cannot cheaply compute the original byte
1167            // position; report the raw consumed count.
1168            return (*(*input).buf).rawconsumed as c_long;
1169        }
1170        let consumed = (*input).consumed;
1171        if (*input).base.is_null() {
1172            return consumed as c_long;
1173        }
1174        (consumed + ((*input).cur as usize).saturating_sub((*input).base as usize) as c_ulong)
1175            as c_long
1176    }
1177}
1178
1179/// Extract the directory part of a filename (newly allocated).
1180///
1181/// # UPSTREAM-PARITY
1182///
1183/// ```c
1184/// char *xmlParserGetDirectory(const char *filename);
1185/// ```
1186///
1187/// # SAFETY
1188///
1189///
1190/// - `filename` must point to valid NUL-terminated
1191///   strings (or NULL where the C contract allows) for the lifetime
1192///   of the call.
1193///
1194/// The caller must not race this call with concurrent mutation of the
1195/// same objects from other threads (per-object state is not internally
1196/// synchronized). Violating any of the above is undefined behavior.
1197///
1198/// Exercised by the C-API differential courts
1199/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1200/// courts; those pass byte-for-byte against the upstream oracle.
1201#[no_mangle]
1202pub unsafe extern "C" fn xmlParserGetDirectory(filename: *const c_char) -> *mut c_char {
1203    if filename.is_null() {
1204        return ptr::null_mut();
1205    }
1206    unsafe {
1207        let len = libc::strlen(filename);
1208        let mut last_sep: Option<usize> = None;
1209        for i in 0..len {
1210            if *filename.add(i) == b'/' as c_char {
1211                last_sep = Some(i);
1212            }
1213        }
1214        match last_sep {
1215            Some(0) => xmlMemStrdupImpl(c"/".as_ptr() as *const c_char) as *mut c_char,
1216            Some(pos) => {
1217                let slice = core::slice::from_raw_parts(filename as *const u8, pos);
1218                let mut v = slice.to_vec();
1219                v.push(0);
1220                xmlMemStrdupImpl(v.as_ptr() as *const c_char) as *mut c_char
1221            }
1222            None => xmlMemStrdupImpl(c".".as_ptr() as *const c_char) as *mut c_char,
1223        }
1224    }
1225}
1226
1227/// Check whether a file exists: 0 if stat fails, 2 if it is a directory,
1228/// 1 otherwise.
1229///
1230/// # UPSTREAM-PARITY
1231///
1232/// ```c
1233/// int xmlCheckFilename(const char *path);
1234/// ```
1235///
1236/// # SAFETY
1237///
1238///
1239/// - `path` must point to valid NUL-terminated
1240///   strings (or NULL where the C contract allows) for the lifetime
1241///   of the call.
1242///
1243/// The caller must not race this call with concurrent mutation of the
1244/// same objects from other threads (per-object state is not internally
1245/// synchronized). Violating any of the above is undefined behavior.
1246///
1247/// Exercised by the C-API differential courts
1248/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1249/// courts; those pass byte-for-byte against the upstream oracle.
1250#[no_mangle]
1251pub unsafe extern "C" fn xmlCheckFilename(path: *const c_char) -> c_int {
1252    if path.is_null() {
1253        return 0;
1254    }
1255    unsafe {
1256        let mut st: libc::stat = core::mem::zeroed();
1257        if libc::stat(path, &mut st) != 0 {
1258            return 0;
1259        }
1260        if st.st_mode & libc::S_IFMT == libc::S_IFDIR {
1261            2
1262        } else {
1263            1
1264        }
1265    }
1266}
1267
1268/// Test whether a public/system ID pair is one of the XHTML DTDs.
1269///
1270/// # UPSTREAM-PARITY
1271///
1272/// ```c
1273/// int xmlIsXHTML(const xmlChar *systemID, const xmlChar *publicID);
1274/// ```
1275///
1276/// # SAFETY
1277///
1278///
1279/// - `systemID`, `publicID` must point to valid NUL-terminated
1280///   strings (or NULL where the C contract allows) for the lifetime
1281///   of the call.
1282///
1283/// The caller must not race this call with concurrent mutation of the
1284/// same objects from other threads (per-object state is not internally
1285/// synchronized). Violating any of the above is undefined behavior.
1286///
1287/// Exercised by the C-API differential courts
1288/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1289/// courts; those pass byte-for-byte against the upstream oracle.
1290#[no_mangle]
1291pub unsafe extern "C" fn xmlIsXHTML(systemID: *const xmlChar, publicID: *const xmlChar) -> c_int {
1292    const XHTML_STRICT_PUBLIC_ID: &[u8] = b"-//W3C//DTD XHTML 1.0 Strict//EN\0";
1293    const XHTML_STRICT_SYSTEM_ID: &[u8] = b"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\0";
1294    const XHTML_FRAME_PUBLIC_ID: &[u8] = b"-//W3C//DTD XHTML 1.0 Frameset//EN\0";
1295    const XHTML_FRAME_SYSTEM_ID: &[u8] = b"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd\0";
1296    const XHTML_TRANS_PUBLIC_ID: &[u8] = b"-//W3C//DTD XHTML 1.0 Transitional//EN\0";
1297    const XHTML_TRANS_SYSTEM_ID: &[u8] =
1298        b"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\0";
1299
1300    if systemID.is_null() && publicID.is_null() {
1301        return -1;
1302    }
1303    unsafe {
1304        if !publicID.is_null()
1305            && (string::xml_strcmp(publicID, XHTML_STRICT_PUBLIC_ID.as_ptr() as *const xmlChar)
1306                == 0
1307                || string::xml_strcmp(publicID, XHTML_FRAME_PUBLIC_ID.as_ptr() as *const xmlChar)
1308                    == 0
1309                || string::xml_strcmp(publicID, XHTML_TRANS_PUBLIC_ID.as_ptr() as *const xmlChar)
1310                    == 0)
1311        {
1312            return 1;
1313        }
1314        if !systemID.is_null()
1315            && (string::xml_strcmp(systemID, XHTML_STRICT_SYSTEM_ID.as_ptr() as *const xmlChar)
1316                == 0
1317                || string::xml_strcmp(systemID, XHTML_FRAME_SYSTEM_ID.as_ptr() as *const xmlChar)
1318                    == 0
1319                || string::xml_strcmp(systemID, XHTML_TRANS_SYSTEM_ID.as_ptr() as *const xmlChar)
1320                    == 0)
1321        {
1322            return 1;
1323        }
1324    }
1325    0
1326}
1327
1328// ═══════════════════════════════════════════════════════════════════════════════
1329// Context creation from sources
1330// ═══════════════════════════════════════════════════════════════════════════════
1331
1332/// Create a parser context for an in-memory document.
1333///
1334/// # UPSTREAM-PARITY
1335///
1336/// ```c
1337/// xmlParserCtxtPtr xmlCreateMemoryParserCtxt(const char *buffer, int size);
1338/// ```
1339///
1340/// # SAFETY
1341///
1342///
1343/// - `buffer` must point to valid NUL-terminated
1344///   strings (or NULL where the C contract allows) for the lifetime
1345///   of the call.
1346///
1347/// The caller must not race this call with concurrent mutation of the
1348/// same objects from other threads (per-object state is not internally
1349/// synchronized). Violating any of the above is undefined behavior.
1350///
1351/// Exercised by the C-API differential courts
1352/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1353/// courts; those pass byte-for-byte against the upstream oracle.
1354#[no_mangle]
1355pub unsafe extern "C" fn xmlCreateMemoryParserCtxt(
1356    buffer: *const c_char,
1357    size: c_int,
1358) -> *mut _xmlParserCtxt {
1359    if buffer.is_null() || size < 0 {
1360        return ptr::null_mut();
1361    }
1362    unsafe {
1363        let ctxt = xmlNewParserCtxt();
1364        if ctxt.is_null() {
1365            return ptr::null_mut();
1366        }
1367        let input = helpers::input_from_memory(buffer, size);
1368        helpers::setup_parser_input(ctxt, input);
1369        ctxt
1370    }
1371}
1372
1373/// Create a parser context for push parsing.
1374///
1375/// # UPSTREAM-PARITY
1376///
1377/// ```c
1378/// xmlParserCtxtPtr xmlCreatePushParserCtxt(xmlSAXHandler *sax, void *user_data,
1379///                                          const char *chunk, int size,
1380///                                          const char *filename);
1381/// ```
1382///
1383/// # SAFETY
1384///
1385/// - `sax`, `user_data` must be valid pointers (or NULL
1386///   where the upstream C contract allows), obtained from the
1387///   matching constructor/owner and not yet freed; the callee may
1388///   take or keep ownership exactly as the C API specifies.
1389///
1390/// - `chunk`, `filename` must point to valid NUL-terminated
1391///   strings (or NULL where the C contract allows) for the lifetime
1392///   of the call.
1393///
1394/// The caller must not race this call with concurrent mutation of the
1395/// same objects from other threads (per-object state is not internally
1396/// synchronized). Violating any of the above is undefined behavior.
1397///
1398/// Exercised by the C-API differential courts
1399/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1400/// courts; those pass byte-for-byte against the upstream oracle.
1401#[no_mangle]
1402pub unsafe extern "C" fn xmlCreatePushParserCtxt(
1403    sax: *mut _xmlSAXHandler,
1404    user_data: *mut c_void,
1405    chunk: *const c_char,
1406    size: c_int,
1407    filename: *const c_char,
1408) -> *mut _xmlParserCtxt {
1409    unsafe {
1410        let ctxt = xmlNewSAXParserCtxt(sax, user_data);
1411        if ctxt.is_null() {
1412            return ptr::null_mut();
1413        }
1414        // UPSTREAM-PARITY (parser.c xmlCreatePushParserCtxt): the push
1415        // context forces dictNames on (and clears XML_PARSE_NODICT), so
1416        // element/attribute names are interned in the document dictionary
1417        // and pointer-identical to xmlDictLookup results — lxml's
1418        // _MultiTagMatcher (iterparse tag=...) compares name pointers.
1419        (*ctxt).options &= !crate::abi::types::XML_PARSE_NODICT;
1420        (*ctxt).dictNames = 1;
1421        let slice = if size > 0 && !chunk.is_null() {
1422            core::slice::from_raw_parts(chunk as *const u8, size as usize)
1423        } else {
1424            &[]
1425        };
1426        let uri = if filename.is_null() {
1427            None
1428        } else {
1429            CStr::from_ptr(filename).to_str().ok()
1430        };
1431        let input = InputBuffer::from_memory(slice, uri);
1432        helpers::setup_parser_input(ctxt, input);
1433        ctxt
1434    }
1435}
1436
1437/// Create a parser context for an I/O stream.
1438///
1439/// # UPSTREAM-PARITY
1440///
1441/// ```c
1442/// xmlParserCtxtPtr xmlCreateIOParserCtxt(xmlSAXHandler *sax, void *user_data,
1443///                                        xmlInputReadCallback ioread,
1444///                                        xmlInputCloseCallback ioclose,
1445///                                        void *ioctx, xmlCharEncoding enc);
1446/// ```
1447///
1448/// # SAFETY
1449///
1450/// - `sax`, `user_data`, `ioctx` must be valid pointers (or NULL
1451///   where the upstream C contract allows), obtained from the
1452///   matching constructor/owner and not yet freed; the callee may
1453///   take or keep ownership exactly as the C API specifies.
1454///
1455/// - `ioread`, `ioclose` must be a valid callback (or None);
1456///   the callback is invoked with the documented context pointer and
1457///   must itself uphold the same pointer invariants.
1458///
1459/// The caller must not race this call with concurrent mutation of the
1460/// same objects from other threads (per-object state is not internally
1461/// synchronized). Violating any of the above is undefined behavior.
1462///
1463/// Exercised by the C-API differential courts
1464/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1465/// courts; those pass byte-for-byte against the upstream oracle.
1466#[no_mangle]
1467pub unsafe extern "C" fn xmlCreateIOParserCtxt(
1468    sax: *mut _xmlSAXHandler,
1469    user_data: *mut c_void,
1470    ioread: Option<xmlInputReadCallback>,
1471    ioclose: Option<xmlInputCloseCallback>,
1472    ioctx: *mut c_void,
1473    enc: c_int,
1474) -> *mut _xmlParserCtxt {
1475    unsafe {
1476        let ctxt = xmlNewSAXParserCtxt(sax, user_data);
1477        if ctxt.is_null() {
1478            return ptr::null_mut();
1479        }
1480        let input = helpers::input_from_io(ioread, ioclose, ioctx);
1481        helpers::setup_parser_input(ctxt, input);
1482        if enc != xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int {
1483            xmlSwitchEncoding(ctxt, enc);
1484        }
1485        ctxt
1486    }
1487}
1488
1489/// Create a parser context for a file or URL.
1490///
1491/// # UPSTREAM-PARITY
1492///
1493/// ```c
1494/// xmlParserCtxtPtr xmlCreateURLParserCtxt(const char *filename, int options);
1495/// ```
1496///
1497/// # SAFETY
1498///
1499///
1500/// - `filename` must point to valid NUL-terminated
1501///   strings (or NULL where the C contract allows) for the lifetime
1502///   of the call.
1503///
1504/// The caller must not race this call with concurrent mutation of the
1505/// same objects from other threads (per-object state is not internally
1506/// synchronized). Violating any of the above is undefined behavior.
1507///
1508/// Exercised by the C-API differential courts
1509/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1510/// courts; those pass byte-for-byte against the upstream oracle.
1511#[no_mangle]
1512pub unsafe extern "C" fn xmlCreateURLParserCtxt(
1513    filename: *const c_char,
1514    options: c_int,
1515) -> *mut _xmlParserCtxt {
1516    if filename.is_null() {
1517        return ptr::null_mut();
1518    }
1519    unsafe {
1520        let ctxt = xmlNewParserCtxt();
1521        if ctxt.is_null() {
1522            return ptr::null_mut();
1523        }
1524        apply_options(ctxt, options);
1525        let input = match open_filename_routed(filename, ctxt) {
1526            RoutedFileOpen::Loaded(i) => i,
1527            RoutedFileOpen::Failed => {
1528                // UPSTREAM-PARITY (parserInternals.c xmlNewInputFromFile via
1529                // the registered loader): NULL loader result is XML_IO_ENOENT
1530                // — raise xmlCtxtErrIO, no built-in fallback.
1531                emit_io_warning(ctxt, io_load_failure_message(filename));
1532                helpers::free_parser_ctxt(ctxt);
1533                return ptr::null_mut();
1534            }
1535            RoutedFileOpen::EntityLoaderFailed => {
1536                // UPSTREAM-PARITY (parser.c xmlCreateURLParserCtxt ->
1537                // xmlLoadResource): a custom entity loader returning NULL
1538                // fails the open SILENTLY.
1539                helpers::free_parser_ctxt(ctxt);
1540                return ptr::null_mut();
1541            }
1542            RoutedFileOpen::Builtin => match helpers::input_from_file(filename) {
1543                Ok(i) => i,
1544                Err(_) => {
1545                    helpers::free_parser_ctxt(ctxt);
1546                    return ptr::null_mut();
1547                }
1548            },
1549        };
1550        helpers::setup_parser_input(ctxt, input);
1551        ctxt
1552    }
1553}
1554
1555/// Create a parser context for an external entity.
1556///
1557/// # UPSTREAM-PARITY
1558///
1559/// ```c
1560/// xmlParserCtxtPtr xmlCreateEntityParserCtxt(const xmlChar *URL,
1561///                                            const xmlChar *ID,
1562///                                            const xmlChar *base);
1563/// ```
1564///
1565/// # SAFETY
1566///
1567///
1568/// - `URL`, `ID`, `base` must point to valid NUL-terminated
1569///   strings (or NULL where the C contract allows) for the lifetime
1570///   of the call.
1571///
1572/// The caller must not race this call with concurrent mutation of the
1573/// same objects from other threads (per-object state is not internally
1574/// synchronized). Violating any of the above is undefined behavior.
1575///
1576/// Exercised by the C-API differential courts
1577/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1578/// courts; those pass byte-for-byte against the upstream oracle.
1579#[no_mangle]
1580pub unsafe extern "C" fn xmlCreateEntityParserCtxt(
1581    URL: *const xmlChar,
1582    ID: *const xmlChar,
1583    base: *const xmlChar,
1584) -> *mut _xmlParserCtxt {
1585    let _ = base; // base URI resolution is a no-op here
1586    unsafe {
1587        let ctxt = xmlNewParserCtxt();
1588        if ctxt.is_null() {
1589            return ptr::null_mut();
1590        }
1591        let input = xmlLoadExternalEntity(URL as *const c_char, ID as *const c_char, ctxt);
1592        if input.is_null() {
1593            helpers::free_parser_ctxt(ctxt);
1594            return ptr::null_mut();
1595        }
1596        if xmlPushInput(ctxt, input) < 0 {
1597            helpers::free_parser_input(input);
1598            helpers::free_parser_ctxt(ctxt);
1599            return ptr::null_mut();
1600        }
1601        ctxt
1602    }
1603}
1604
1605// ═══════════════════════════════════════════════════════════════════════════════
1606// CtxtRead family
1607// ═══════════════════════════════════════════════════════════════════════════════
1608
1609/// Parse an XML in-memory document with a given context.
1610///
1611/// # UPSTREAM-PARITY
1612///
1613/// ```c
1614/// xmlDocPtr xmlCtxtReadDoc(xmlParserCtxtPtr ctxt, const xmlChar *cur,
1615///                          const char *URL, const char *encoding, int options);
1616/// ```
1617///
1618/// # SAFETY
1619///
1620/// - `ctxt` must be valid pointers (or NULL
1621///   where the upstream C contract allows), obtained from the
1622///   matching constructor/owner and not yet freed; the callee may
1623///   take or keep ownership exactly as the C API specifies.
1624///
1625/// - `cur`, `URL`, `_encoding` must point to valid NUL-terminated
1626///   strings (or NULL where the C contract allows) for the lifetime
1627///   of the call.
1628///
1629/// The caller must not race this call with concurrent mutation of the
1630/// same objects from other threads (per-object state is not internally
1631/// synchronized). Violating any of the above is undefined behavior.
1632///
1633/// Exercised by the C-API differential courts
1634/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1635/// courts; those pass byte-for-byte against the upstream oracle.
1636#[no_mangle]
1637pub unsafe extern "C" fn xmlCtxtReadDoc(
1638    ctxt: *mut _xmlParserCtxt,
1639    cur: *const xmlChar,
1640    URL: *const c_char,
1641    encoding: *const c_char,
1642    options: c_int,
1643) -> *mut _xmlDoc {
1644    if ctxt.is_null() || cur.is_null() {
1645        return ptr::null_mut();
1646    }
1647    unsafe {
1648        let len = string::xml_strlen(cur);
1649        let mut input = helpers::input_from_memory(cur as *const c_char, len as c_int);
1650        if !encoding.is_null() {
1651            let name = core::ffi::CStr::from_ptr(encoding).to_bytes();
1652            input.apply_explicit_input_encoding(name);
1653        }
1654        ctxt_read_doc(ctxt, input, URL, options)
1655    }
1656}
1657
1658/// Parse an XML file with a given context.
1659///
1660/// # UPSTREAM-PARITY
1661///
1662/// ```c
1663/// xmlDocPtr xmlCtxtReadFile(xmlParserCtxtPtr ctxt, const char *filename,
1664///                           const char *encoding, int options);
1665/// ```
1666///
1667/// # SAFETY
1668///
1669/// - `ctxt` must be valid pointers (or NULL
1670///   where the upstream C contract allows), obtained from the
1671///   matching constructor/owner and not yet freed; the callee may
1672///   take or keep ownership exactly as the C API specifies.
1673///
1674/// - `filename`, `_encoding` must point to valid NUL-terminated
1675///   strings (or NULL where the C contract allows) for the lifetime
1676///   of the call.
1677///
1678/// The caller must not race this call with concurrent mutation of the
1679/// same objects from other threads (per-object state is not internally
1680/// synchronized). Violating any of the above is undefined behavior.
1681///
1682/// Exercised by the C-API differential courts
1683/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1684/// courts; those pass byte-for-byte against the upstream oracle.
1685#[no_mangle]
1686pub unsafe extern "C" fn xmlCtxtReadFile(
1687    ctxt: *mut _xmlParserCtxt,
1688    filename: *const c_char,
1689    _encoding: *const c_char,
1690    options: c_int,
1691) -> *mut _xmlDoc {
1692    if ctxt.is_null() || filename.is_null() {
1693        return ptr::null_mut();
1694    }
1695    unsafe {
1696        // UPSTREAM-PARITY (parser.c xmlCtxtReadFile -> xmlCtxtNewInputFromUrl
1697        // -> xmlLoadResource): a registered external entity loader governs the
1698        // open; below it the xmlParserInputBufferCreateFilenameDefault (php
1699        // streams loader) is consulted; a NULL loader result raises
1700        // xmlCtxtErrIO(XML_IO_ENOENT, filename) — "I/O warning : failed to
1701        // load \"%s\": %s\n" — and parsing fails.
1702        match open_filename_routed(filename, ctxt) {
1703            RoutedFileOpen::Loaded(input) => ctxt_read_doc(ctxt, input, filename, options),
1704            RoutedFileOpen::Failed => {
1705                emit_io_warning(ctxt, io_load_failure_message(filename));
1706                ptr::null_mut()
1707            }
1708            RoutedFileOpen::EntityLoaderFailed => {
1709                // UPSTREAM-PARITY (parser.c xmlCtxtReadFile): a custom entity
1710                // loader returning NULL fails the read SILENTLY.
1711                ptr::null_mut()
1712            }
1713            RoutedFileOpen::Builtin => match helpers::input_from_file(filename) {
1714                Ok(input) => ctxt_read_doc(ctxt, input, filename, options),
1715                Err(_) => ptr::null_mut(),
1716            },
1717        }
1718    }
1719}
1720
1721/// Parse an XML in-memory block with a given context.
1722///
1723/// # UPSTREAM-PARITY
1724///
1725/// ```c
1726/// xmlDocPtr xmlCtxtReadMemory(xmlParserCtxtPtr ctxt, const char *buffer,
1727///                             int size, const char *URL, const char *encoding,
1728///                             int options);
1729/// ```
1730///
1731/// # SAFETY
1732///
1733/// - `ctxt` must be valid pointers (or NULL
1734///   where the upstream C contract allows), obtained from the
1735///   matching constructor/owner and not yet freed; the callee may
1736///   take or keep ownership exactly as the C API specifies.
1737///
1738/// - `buffer`, `URL`, `_encoding` must point to valid NUL-terminated
1739///   strings (or NULL where the C contract allows) for the lifetime
1740///   of the call.
1741///
1742/// The caller must not race this call with concurrent mutation of the
1743/// same objects from other threads (per-object state is not internally
1744/// synchronized). Violating any of the above is undefined behavior.
1745///
1746/// Exercised by the C-API differential courts
1747/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1748/// courts; those pass byte-for-byte against the upstream oracle.
1749#[no_mangle]
1750pub unsafe extern "C" fn xmlCtxtReadMemory(
1751    ctxt: *mut _xmlParserCtxt,
1752    buffer: *const c_char,
1753    size: c_int,
1754    URL: *const c_char,
1755    encoding: *const c_char,
1756    options: c_int,
1757) -> *mut _xmlDoc {
1758    if ctxt.is_null() || buffer.is_null() || size < 0 {
1759        return ptr::null_mut();
1760    }
1761    unsafe {
1762        let mut input = helpers::input_from_memory(buffer, size);
1763        // UPSTREAM-PARITY (xmlCtxtNewInputFromMemory): an explicit `encoding`
1764        // argument switches the input to that encoding BEFORE the parse, so
1765        // the parser never sees raw UCS-2/UCS-4/Latin-1 bytes (lxml feeds
1766        // PEP-393 python strings this way). Best-effort: when the name is
1767        // unknown or the buffer was already converted the raw bytes stay and
1768        // BOM/declaration detection decides as usual.
1769        if !encoding.is_null() {
1770            let name = core::ffi::CStr::from_ptr(encoding).to_bytes();
1771            input.apply_explicit_input_encoding(name);
1772        }
1773        ctxt_read_doc(ctxt, input, URL, options)
1774    }
1775}
1776
1777/// Parse an XML document from a file descriptor with a given context.
1778///
1779/// # UPSTREAM-PARITY
1780///
1781/// ```c
1782/// xmlDocPtr xmlCtxtReadFd(xmlParserCtxtPtr ctxt, int fd, const char *URL,
1783///                         const char *encoding, int options);
1784/// ```
1785///
1786/// # SAFETY
1787///
1788/// - `ctxt` must be valid pointers (or NULL
1789///   where the upstream C contract allows), obtained from the
1790///   matching constructor/owner and not yet freed; the callee may
1791///   take or keep ownership exactly as the C API specifies.
1792///
1793/// - `URL`, `_encoding` must point to valid NUL-terminated
1794///   strings (or NULL where the C contract allows) for the lifetime
1795///   of the call.
1796///
1797/// The caller must not race this call with concurrent mutation of the
1798/// same objects from other threads (per-object state is not internally
1799/// synchronized). Violating any of the above is undefined behavior.
1800///
1801/// Exercised by the C-API differential courts
1802/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1803/// courts; those pass byte-for-byte against the upstream oracle.
1804#[no_mangle]
1805pub unsafe extern "C" fn xmlCtxtReadFd(
1806    ctxt: *mut _xmlParserCtxt,
1807    fd: c_int,
1808    URL: *const c_char,
1809    _encoding: *const c_char,
1810    options: c_int,
1811) -> *mut _xmlDoc {
1812    if ctxt.is_null() || fd < 0 {
1813        return ptr::null_mut();
1814    }
1815    unsafe {
1816        let mut buf = Vec::new();
1817        let mut tmp = [0u8; 4096];
1818        loop {
1819            let n = libc::read(fd, tmp.as_mut_ptr() as *mut c_void, tmp.len());
1820            if n <= 0 {
1821                break;
1822            }
1823            buf.extend_from_slice(&tmp[..n as usize]);
1824        }
1825        let input = helpers::input_from_memory(buf.as_ptr() as *const c_char, buf.len() as c_int);
1826        ctxt_read_doc(ctxt, input, URL, options)
1827    }
1828}
1829
1830/// Parse an XML document from I/O callbacks with a given context.
1831///
1832/// # UPSTREAM-PARITY
1833///
1834/// ```c
1835/// xmlDocPtr xmlCtxtReadIO(xmlParserCtxtPtr ctxt, xmlInputReadCallback ioread,
1836///                         xmlInputCloseCallback ioclose, void *ioctx,
1837///                         const char *URL, const char *encoding, int options);
1838/// ```
1839///
1840/// # SAFETY
1841///
1842/// - `ctxt`, `ioctx` must be valid pointers (or NULL
1843///   where the upstream C contract allows), obtained from the
1844///   matching constructor/owner and not yet freed; the callee may
1845///   take or keep ownership exactly as the C API specifies.
1846///
1847/// - `URL`, `_encoding` must point to valid NUL-terminated
1848///   strings (or NULL where the C contract allows) for the lifetime
1849///   of the call.
1850///
1851/// - `ioread`, `ioclose` must be a valid callback (or None);
1852///   the callback is invoked with the documented context pointer and
1853///   must itself uphold the same pointer invariants.
1854///
1855/// The caller must not race this call with concurrent mutation of the
1856/// same objects from other threads (per-object state is not internally
1857/// synchronized). Violating any of the above is undefined behavior.
1858///
1859/// Exercised by the C-API differential courts
1860/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1861/// courts; those pass byte-for-byte against the upstream oracle.
1862#[no_mangle]
1863pub unsafe extern "C" fn xmlCtxtReadIO(
1864    ctxt: *mut _xmlParserCtxt,
1865    ioread: Option<xmlInputReadCallback>,
1866    ioclose: Option<xmlInputCloseCallback>,
1867    ioctx: *mut c_void,
1868    URL: *const c_char,
1869    _encoding: *const c_char,
1870    options: c_int,
1871) -> *mut _xmlDoc {
1872    if ctxt.is_null() {
1873        return ptr::null_mut();
1874    }
1875    unsafe {
1876        let input = helpers::input_from_io(ioread, ioclose, ioctx);
1877        // UPSTREAM-PARITY (parser.c xmlCtxtNewInputFromIO): the URL becomes
1878        // the input's filename, which feeds the `file:line:` error prefix.
1879        let input = if !URL.is_null() {
1880            match std::ffi::CStr::from_ptr(URL).to_str() {
1881                Ok(s) => input.with_filename(s),
1882                Err(_) => input,
1883            }
1884        } else {
1885            input
1886        };
1887        ctxt_read_doc(ctxt, input, URL, options)
1888    }
1889}
1890
1891/// Parse a document from a raw parser input, taking ownership of `input`.
1892///
1893/// # UPSTREAM-PARITY
1894///
1895/// ```c
1896/// xmlDocPtr xmlCtxtParseDocument(xmlParserCtxtPtr ctxt, xmlParserInputPtr input);
1897/// ```
1898///
1899/// # SAFETY
1900///
1901/// - `ctxt`, `input` must be valid pointers (or NULL
1902///   where the upstream C contract allows), obtained from the
1903///   matching constructor/owner and not yet freed; the callee may
1904///   take or keep ownership exactly as the C API specifies.
1905///
1906/// The caller must not race this call with concurrent mutation of the
1907/// same objects from other threads (per-object state is not internally
1908/// synchronized). Violating any of the above is undefined behavior.
1909///
1910/// Exercised by the C-API differential courts
1911/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1912/// courts; those pass byte-for-byte against the upstream oracle.
1913#[no_mangle]
1914pub unsafe extern "C" fn xmlCtxtParseDocument(
1915    ctxt: *mut _xmlParserCtxt,
1916    input: *mut _xmlParserInput,
1917) -> *mut _xmlDoc {
1918    if ctxt.is_null() || input.is_null() {
1919        return ptr::null_mut();
1920    }
1921    unsafe {
1922        // Determine whether the caller's input is already owned by the
1923        // context's input stack (pushed via xmlPushInput).
1924        let mut owned = false;
1925        let nr = (*ctxt).inputNr;
1926        let tab = (*ctxt).inputTab;
1927        if !tab.is_null() {
1928            for i in 0..nr {
1929                if *tab.add(i as usize) == input {
1930                    owned = true;
1931                    break;
1932                }
1933            }
1934        }
1935        if (*ctxt).input == input {
1936            owned = true;
1937        }
1938
1939        // Copy the data first so the context reset cannot invalidate it.
1940        let ib = input_buffer_from_parser_input(input);
1941
1942        xmlCtxtReset(ctxt);
1943        helpers::setup_parser_input(ctxt, ib);
1944        helpers::parse_document(ctxt);
1945
1946        if !owned {
1947            helpers::free_parser_input(input);
1948        }
1949
1950        (*ctxt).myDoc
1951    }
1952}
1953
1954// ═══════════════════════════════════════════════════════════════════════════════
1955// Parser input buffers / streams
1956// ═══════════════════════════════════════════════════════════════════════════════
1957
1958/// Allocate a parser input buffer for the given encoding.
1959///
1960/// # UPSTREAM-PARITY
1961///
1962/// ```c
1963/// xmlParserInputBufferPtr xmlAllocParserInputBuffer(xmlCharEncoding enc);
1964/// ```
1965///
1966/// # SAFETY
1967///
1968/// The function touches crate-global state only; it is safe
1969/// as long as the caller respects the library's global
1970/// initialization/cleanup ordering (xmlInitParser before use,
1971/// xmlCleanupParser only after all users are done).
1972///
1973/// Violating the global lifecycle ordering, or calling this after
1974/// teardown or from a signal handler, is undefined behavior.
1975#[no_mangle]
1976pub unsafe extern "C" fn xmlAllocParserInputBuffer(enc: c_int) -> *mut _xmlParserInputBuffer {
1977    unsafe {
1978        let buf = xmlMallocZero(core::mem::size_of::<_xmlParserInputBuffer>())
1979            as *mut _xmlParserInputBuffer;
1980        if buf.is_null() {
1981            return ptr::null_mut();
1982        }
1983        let b = &mut *buf;
1984        b.buffer = io::buf_create(crate::abi::data_globals::xmlDefaultBufferSize) as *mut c_void;
1985        b.raw = io::buf_create(crate::abi::data_globals::xmlDefaultBufferSize) as *mut c_void;
1986        if b.buffer.is_null() || b.raw.is_null() {
1987            io::buf_free(b.buffer as *mut _xmlBuffer);
1988            io::buf_free(b.raw as *mut _xmlBuffer);
1989            xmlFreeImpl(buf as *mut c_void);
1990            return ptr::null_mut();
1991        }
1992        b.compressed = -1;
1993
1994        if enc != xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int
1995            && enc != xmlCharEncoding::XML_CHAR_ENCODING_ERROR as c_int
1996        {
1997            let handler = encoding_handler_for(enc);
1998            if !handler.is_null() {
1999                b.encoder = handler as *mut c_void;
2000            }
2001        }
2002        buf
2003    }
2004}
2005
2006/// Grow an input buffer by reading up to `len` bytes from its source.
2007///
2008/// # UPSTREAM-PARITY
2009///
2010/// ```c
2011/// int xmlParserInputBufferGrow(xmlParserInputBufferPtr in, int len);
2012/// ```
2013///
2014/// # SAFETY
2015///
2016/// - `in_` must be valid pointers (or NULL
2017///   where the upstream C contract allows), obtained from the
2018///   matching constructor/owner and not yet freed; the callee may
2019///   take or keep ownership exactly as the C API specifies.
2020///
2021/// The caller must not race this call with concurrent mutation of the
2022/// same objects from other threads (per-object state is not internally
2023/// synchronized). Violating any of the above is undefined behavior.
2024///
2025/// Exercised by the C-API differential courts
2026/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2027/// courts; those pass byte-for-byte against the upstream oracle.
2028#[no_mangle]
2029pub unsafe extern "C" fn xmlParserInputBufferGrow(
2030    in_: *mut _xmlParserInputBuffer,
2031    len: c_int,
2032) -> c_int {
2033    if in_.is_null() || len <= 0 {
2034        return 0;
2035    }
2036    unsafe {
2037        let b = &mut *in_;
2038        if b.error != 0 {
2039            return -1;
2040        }
2041        let Some(read_cb) = b.readcallback else {
2042            // Memory-based buffer: nothing to grow.
2043            return 0;
2044        };
2045        let mut tmp = vec![0u8; len as usize];
2046        let n = read_cb(b.context, tmp.as_mut_ptr() as *mut c_char, len);
2047        if n < 0 {
2048            b.error = 1;
2049            return -1;
2050        }
2051        if n == 0 {
2052            return 0;
2053        }
2054        io::input_buffer_push(in_, tmp.as_ptr() as *const c_char, n);
2055        n
2056    }
2057}
2058
2059/// Push `len` bytes into an input buffer (push parser).
2060///
2061/// # UPSTREAM-PARITY
2062///
2063/// ```c
2064/// int xmlParserInputBufferPush(xmlParserInputBufferPtr in, int len, const char *buf);
2065/// ```
2066///
2067/// # SAFETY
2068///
2069/// - `in_` must be valid pointers (or NULL
2070///   where the upstream C contract allows), obtained from the
2071///   matching constructor/owner and not yet freed; the callee may
2072///   take or keep ownership exactly as the C API specifies.
2073///
2074/// - `buf` must point to valid NUL-terminated
2075///   strings (or NULL where the C contract allows) for the lifetime
2076///   of the call.
2077///
2078/// The caller must not race this call with concurrent mutation of the
2079/// same objects from other threads (per-object state is not internally
2080/// synchronized). Violating any of the above is undefined behavior.
2081///
2082/// Exercised by the C-API differential courts
2083/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2084/// courts; those pass byte-for-byte against the upstream oracle.
2085#[no_mangle]
2086pub unsafe extern "C" fn xmlParserInputBufferPush(
2087    in_: *mut _xmlParserInputBuffer,
2088    len: c_int,
2089    buf: *const c_char,
2090) -> c_int {
2091    if in_.is_null() {
2092        return -1;
2093    }
2094    if len < 0 || (len > 0 && buf.is_null()) {
2095        return -1;
2096    }
2097    if len == 0 {
2098        return 0;
2099    }
2100    io::input_buffer_push(in_, buf, len)
2101}
2102
2103/// Read up to `len` bytes from an input buffer's source.
2104///
2105/// # UPSTREAM-PARITY
2106///
2107/// ```c
2108/// int xmlParserInputBufferRead(xmlParserInputBufferPtr in, int len);
2109/// ```
2110///
2111/// # SAFETY
2112///
2113/// - `in_` must be valid pointers (or NULL
2114///   where the upstream C contract allows), obtained from the
2115///   matching constructor/owner and not yet freed; the callee may
2116///   take or keep ownership exactly as the C API specifies.
2117///
2118/// The caller must not race this call with concurrent mutation of the
2119/// same objects from other threads (per-object state is not internally
2120/// synchronized). Violating any of the above is undefined behavior.
2121///
2122/// Exercised by the C-API differential courts
2123/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2124/// courts; those pass byte-for-byte against the upstream oracle.
2125#[no_mangle]
2126pub unsafe extern "C" fn xmlParserInputBufferRead(
2127    in_: *mut _xmlParserInputBuffer,
2128    len: c_int,
2129) -> c_int {
2130    xmlParserInputBufferGrow(in_, len)
2131}
2132
2133/// Deprecated: reading directly from an input stream is an error.
2134///
2135/// # UPSTREAM-PARITY
2136///
2137/// ```c
2138/// int xmlParserInputRead(xmlParserInputPtr in, int len);
2139/// ```
2140///
2141/// # SAFETY
2142///
2143/// - `_in_` must be valid pointers (or NULL
2144///   where the upstream C contract allows), obtained from the
2145///   matching constructor/owner and not yet freed; the callee may
2146///   take or keep ownership exactly as the C API specifies.
2147///
2148/// The caller must not race this call with concurrent mutation of the
2149/// same objects from other threads (per-object state is not internally
2150/// synchronized). Violating any of the above is undefined behavior.
2151///
2152/// Exercised by the C-API differential courts
2153/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2154/// courts; those pass byte-for-byte against the upstream oracle.
2155#[no_mangle]
2156pub const unsafe extern "C" fn xmlParserInputRead(
2157    _in_: *mut _xmlParserInput,
2158    _len: c_int,
2159) -> c_int {
2160    -1
2161}
2162
2163/// Grow a parser input's buffer by reading more data from its source.
2164///
2165/// # UPSTREAM-PARITY
2166///
2167/// ```c
2168/// int xmlParserInputGrow(xmlParserInputPtr in, int len);
2169/// ```
2170///
2171/// # SAFETY
2172///
2173/// - `in_` must be valid pointers (or NULL
2174///   where the upstream C contract allows), obtained from the
2175///   matching constructor/owner and not yet freed; the callee may
2176///   take or keep ownership exactly as the C API specifies.
2177///
2178/// The caller must not race this call with concurrent mutation of the
2179/// same objects from other threads (per-object state is not internally
2180/// synchronized). Violating any of the above is undefined behavior.
2181///
2182/// Exercised by the C-API differential courts
2183/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2184/// courts; those pass byte-for-byte against the upstream oracle.
2185#[no_mangle]
2186pub unsafe extern "C" fn xmlParserInputGrow(in_: *mut _xmlParserInput, len: c_int) -> c_int {
2187    if in_.is_null() || len < 0 {
2188        return -1;
2189    }
2190    unsafe {
2191        let pi = &*in_;
2192        if pi.base.is_null() || pi.cur.is_null() {
2193            return -1;
2194        }
2195        if pi.buf.is_null() {
2196            // Pure memory input: nothing to grow.
2197            return 0;
2198        }
2199        let b = &*pi.buf;
2200        // Memory buffers are not growable.
2201        if b.readcallback.is_none() && b.encoder.is_null() {
2202            return 0;
2203        }
2204        xmlParserInputBufferGrow(pi.buf, len)
2205    }
2206}
2207
2208/// Shrink a parser input, releasing already-consumed data from the buffer.
2209///
2210/// # UPSTREAM-PARITY
2211///
2212/// ```c
2213/// void xmlParserInputShrink(xmlParserInputPtr in);
2214/// ```
2215///
2216/// # SAFETY
2217///
2218/// - `in_` must be valid pointers (or NULL
2219///   where the upstream C contract allows), obtained from the
2220///   matching constructor/owner and not yet freed; the callee may
2221///   take or keep ownership exactly as the C API specifies.
2222///
2223/// The caller must not race this call with concurrent mutation of the
2224/// same objects from other threads (per-object state is not internally
2225/// synchronized). Violating any of the above is undefined behavior.
2226///
2227/// Exercised by the C-API differential courts
2228/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2229/// courts; those pass byte-for-byte against the upstream oracle.
2230#[no_mangle]
2231pub unsafe extern "C" fn xmlParserInputShrink(in_: *mut _xmlParserInput) {
2232    if in_.is_null() {
2233        return;
2234    }
2235    unsafe {
2236        let pi = &mut *in_;
2237        if pi.buf.is_null() || pi.base.is_null() || pi.cur.is_null() {
2238            return;
2239        }
2240        let used = (pi.cur as usize).saturating_sub(pi.base as usize);
2241        if used > LINE_LEN {
2242            // The candidate's inputs are backed by stable memory buffers, so
2243            // the base pointer cannot move; account for the consumed bytes.
2244            pi.consumed = pi.consumed.saturating_add((used - LINE_LEN) as c_ulong);
2245        }
2246    }
2247}
2248
2249/// Create a new (empty) parser input stream.
2250///
2251/// # UPSTREAM-PARITY
2252///
2253/// ```c
2254/// xmlParserInputPtr xmlNewInputStream(xmlParserCtxtPtr ctxt);
2255/// ```
2256///
2257/// # SAFETY
2258///
2259/// - `ctxt` must be valid pointers (or NULL
2260///   where the upstream C contract allows), obtained from the
2261///   matching constructor/owner and not yet freed; the callee may
2262///   take or keep ownership exactly as the C API specifies.
2263///
2264/// The caller must not race this call with concurrent mutation of the
2265/// same objects from other threads (per-object state is not internally
2266/// synchronized). Violating any of the above is undefined behavior.
2267///
2268/// Exercised by the C-API differential courts
2269/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2270/// courts; those pass byte-for-byte against the upstream oracle.
2271#[no_mangle]
2272pub unsafe extern "C" fn xmlNewInputStream(ctxt: *mut _xmlParserCtxt) -> *mut _xmlParserInput {
2273    unsafe {
2274        let input = xmlMallocZero(core::mem::size_of::<_xmlParserInput>()) as *mut _xmlParserInput;
2275        if input.is_null() {
2276            if !ctxt.is_null() {
2277                xmlCtxtErrMemory(ctxt);
2278            }
2279            return ptr::null_mut();
2280        }
2281        (*input).line = 1;
2282        (*input).col = 1;
2283        input
2284    }
2285}
2286
2287/// Wrap an input buffer in a parser input stream.
2288///
2289/// # UPSTREAM-PARITY
2290///
2291/// ```c
2292/// xmlParserInputPtr xmlNewIOInputStream(xmlParserCtxtPtr ctxt,
2293///                                       xmlParserInputBufferPtr input,
2294///                                       xmlCharEncoding enc);
2295/// ```
2296///
2297/// # SAFETY
2298///
2299/// - `ctxt`, `input` must be valid pointers (or NULL
2300///   where the upstream C contract allows), obtained from the
2301///   matching constructor/owner and not yet freed; the callee may
2302///   take or keep ownership exactly as the C API specifies.
2303///
2304/// The caller must not race this call with concurrent mutation of the
2305/// same objects from other threads (per-object state is not internally
2306/// synchronized). Violating any of the above is undefined behavior.
2307///
2308/// Exercised by the C-API differential courts
2309/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2310/// courts; those pass byte-for-byte against the upstream oracle.
2311#[no_mangle]
2312pub unsafe extern "C" fn xmlNewIOInputStream(
2313    ctxt: *mut _xmlParserCtxt,
2314    input: *mut _xmlParserInputBuffer,
2315    enc: c_int,
2316) -> *mut _xmlParserInput {
2317    if ctxt.is_null() || input.is_null() {
2318        return ptr::null_mut();
2319    }
2320    unsafe {
2321        let pi = xmlNewInputStream(ctxt);
2322        if pi.is_null() {
2323            return ptr::null_mut();
2324        }
2325        (*pi).buf = input;
2326        if enc != xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int
2327            && enc != xmlCharEncoding::XML_CHAR_ENCODING_ERROR as c_int
2328        {
2329            let handler = encoding_handler_for(enc);
2330            if !handler.is_null() {
2331                io::input_buffer_set_encoder(input, handler);
2332            }
2333        }
2334        pi
2335    }
2336}
2337
2338/// Create a parser input stream from a zero-terminated string. The string
2339/// must remain valid for the lifetime of the input (static mode).
2340///
2341/// # UPSTREAM-PARITY
2342///
2343/// ```c
2344/// xmlParserInputPtr xmlNewStringInputStream(xmlParserCtxtPtr ctxt,
2345///                                           const xmlChar *buffer);
2346/// ```
2347///
2348/// # SAFETY
2349///
2350/// - `ctxt` must be valid pointers (or NULL
2351///   where the upstream C contract allows), obtained from the
2352///   matching constructor/owner and not yet freed; the callee may
2353///   take or keep ownership exactly as the C API specifies.
2354///
2355/// - `buffer` must point to valid NUL-terminated
2356///   strings (or NULL where the C contract allows) for the lifetime
2357///   of the call.
2358///
2359/// The caller must not race this call with concurrent mutation of the
2360/// same objects from other threads (per-object state is not internally
2361/// synchronized). Violating any of the above is undefined behavior.
2362///
2363/// Exercised by the C-API differential courts
2364/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2365/// courts; those pass byte-for-byte against the upstream oracle.
2366#[no_mangle]
2367pub unsafe extern "C" fn xmlNewStringInputStream(
2368    ctxt: *mut _xmlParserCtxt,
2369    buffer: *const xmlChar,
2370) -> *mut _xmlParserInput {
2371    if ctxt.is_null() || buffer.is_null() {
2372        return ptr::null_mut();
2373    }
2374    unsafe {
2375        let input = xmlNewInputStream(ctxt);
2376        if input.is_null() {
2377            return ptr::null_mut();
2378        }
2379        let len = string::xml_strlen(buffer);
2380        (*input).base = buffer;
2381        (*input).cur = buffer;
2382        (*input).end = buffer.add(len);
2383        (*input).length = len as c_int;
2384        input
2385    }
2386}
2387
2388/// Setup the parser context to parse a new buffer (legacy API).
2389///
2390/// # UPSTREAM-PARITY
2391///
2392/// ```c
2393/// void xmlSetupParserForBuffer(xmlParserCtxtPtr ctxt, const xmlChar* buffer,
2394///                              const char *filename);
2395/// ```
2396///
2397/// # SAFETY
2398///
2399/// - `ctxt` must be valid pointers (or NULL
2400///   where the upstream C contract allows), obtained from the
2401///   matching constructor/owner and not yet freed; the callee may
2402///   take or keep ownership exactly as the C API specifies.
2403///
2404/// - `buffer`, `filename` must point to valid NUL-terminated
2405///   strings (or NULL where the C contract allows) for the lifetime
2406///   of the call.
2407///
2408/// The caller must not race this call with concurrent mutation of the
2409/// same objects from other threads (per-object state is not internally
2410/// synchronized). Violating any of the above is undefined behavior.
2411///
2412/// Exercised by the C-API differential courts
2413/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2414/// courts; those pass byte-for-byte against the upstream oracle.
2415#[no_mangle]
2416pub unsafe extern "C" fn xmlSetupParserForBuffer(
2417    ctxt: *mut _xmlParserCtxt,
2418    buffer: *const xmlChar,
2419    filename: *const c_char,
2420) {
2421    if ctxt.is_null() || buffer.is_null() {
2422        return;
2423    }
2424    unsafe {
2425        xmlCtxtReset(ctxt);
2426        let len = string::xml_strlen(buffer);
2427        let uri = if filename.is_null() {
2428            None
2429        } else {
2430            CStr::from_ptr(filename).to_str().ok()
2431        };
2432        let input = InputBuffer::from_memory(core::slice::from_raw_parts(buffer, len), uri);
2433        helpers::setup_parser_input(ctxt, input);
2434    }
2435}
2436
2437/// Push an input stream onto the context's input stack.
2438///
2439/// # UPSTREAM-PARITY
2440///
2441/// ```c
2442/// int xmlPushInput(xmlParserCtxtPtr ctxt, xmlParserInputPtr input);
2443/// ```
2444///
2445/// # SAFETY
2446///
2447/// - `ctxt`, `input` must be valid pointers (or NULL
2448///   where the upstream C contract allows), obtained from the
2449///   matching constructor/owner and not yet freed; the callee may
2450///   take or keep ownership exactly as the C API specifies.
2451///
2452/// The caller must not race this call with concurrent mutation of the
2453/// same objects from other threads (per-object state is not internally
2454/// synchronized). Violating any of the above is undefined behavior.
2455///
2456/// Exercised by the C-API differential courts
2457/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2458/// courts; those pass byte-for-byte against the upstream oracle.
2459#[no_mangle]
2460pub unsafe extern "C" fn xmlPushInput(
2461    ctxt: *mut _xmlParserCtxt,
2462    input: *mut _xmlParserInput,
2463) -> c_int {
2464    if ctxt.is_null() || input.is_null() {
2465        return -1;
2466    }
2467    unsafe {
2468        let c = &mut *ctxt;
2469        if c.inputNr >= c.inputMax {
2470            let new_max = if c.inputMax == 0 { 5 } else { c.inputMax * 2 };
2471            let new_tab = xmlReallocImpl(
2472                c.inputTab as *mut c_void,
2473                (new_max as usize) * core::mem::size_of::<*mut _xmlParserInput>(),
2474            ) as *mut *mut _xmlParserInput;
2475            if new_tab.is_null() {
2476                return -1;
2477            }
2478            c.inputTab = new_tab;
2479            c.inputMax = new_max;
2480        }
2481        *c.inputTab.add(c.inputNr as usize) = input;
2482        c.input = input;
2483        (*input).id = c.input_id;
2484        c.input_id += 1;
2485        let idx = c.inputNr;
2486        c.inputNr += 1;
2487        idx
2488    }
2489}
2490
2491/// Pop the top input from the context's input stack and free it; returns the
2492/// current character after the pop (0 at end of input).
2493///
2494/// # UPSTREAM-PARITY
2495///
2496/// ```c
2497/// xmlChar xmlPopInput(xmlParserCtxtPtr ctxt);
2498/// ```
2499///
2500/// # SAFETY
2501///
2502/// - `ctxt` must be valid pointers (or NULL
2503///   where the upstream C contract allows), obtained from the
2504///   matching constructor/owner and not yet freed; the callee may
2505///   take or keep ownership exactly as the C API specifies.
2506///
2507/// The caller must not race this call with concurrent mutation of the
2508/// same objects from other threads (per-object state is not internally
2509/// synchronized). Violating any of the above is undefined behavior.
2510///
2511/// Exercised by the C-API differential courts
2512/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2513/// courts; those pass byte-for-byte against the upstream oracle.
2514#[no_mangle]
2515pub unsafe extern "C" fn xmlPopInput(ctxt: *mut _xmlParserCtxt) -> xmlChar {
2516    if ctxt.is_null() || (*ctxt).inputNr <= 1 {
2517        return 0;
2518    }
2519    unsafe {
2520        let c = &mut *ctxt;
2521        c.inputNr -= 1;
2522        let popped = *c.inputTab.add(c.inputNr as usize);
2523        *c.inputTab.add(c.inputNr as usize) = ptr::null_mut();
2524        if c.inputNr > 0 {
2525            c.input = *c.inputTab.add((c.inputNr - 1) as usize);
2526        } else {
2527            c.input = ptr::null_mut();
2528        }
2529        if !popped.is_null() {
2530            helpers::free_parser_input(popped);
2531        }
2532        if c.input.is_null() {
2533            return 0;
2534        }
2535        let cur = (*c.input).cur;
2536        let end = (*c.input).end;
2537        if cur.is_null() || cur >= end {
2538            0
2539        } else {
2540            *cur
2541        }
2542    }
2543}
2544
2545// ═══════════════════════════════════════════════════════════════════════════════
2546// Encoding switching
2547// ═══════════════════════════════════════════════════════════════════════════════
2548
2549/// Switch the input encoding of the current input.
2550///
2551/// # UPSTREAM-PARITY
2552///
2553/// ```c
2554/// int xmlSwitchEncoding(xmlParserCtxtPtr ctxt, xmlCharEncoding enc);
2555/// ```
2556///
2557/// # SAFETY
2558///
2559/// - `ctxt` must be valid pointers (or NULL
2560///   where the upstream C contract allows), obtained from the
2561///   matching constructor/owner and not yet freed; the callee may
2562///   take or keep ownership exactly as the C API specifies.
2563///
2564/// The caller must not race this call with concurrent mutation of the
2565/// same objects from other threads (per-object state is not internally
2566/// synchronized). Violating any of the above is undefined behavior.
2567///
2568/// Exercised by the C-API differential courts
2569/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2570/// courts; those pass byte-for-byte against the upstream oracle.
2571#[no_mangle]
2572pub unsafe extern "C" fn xmlSwitchEncoding(ctxt: *mut _xmlParserCtxt, enc: c_int) -> c_int {
2573    if ctxt.is_null() || (*ctxt).input.is_null() {
2574        return -1;
2575    }
2576    if enc == xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int {
2577        return 0;
2578    }
2579    unsafe {
2580        let handler = encoding_handler_for(enc);
2581        if handler.is_null() {
2582            return -1;
2583        }
2584        xmlSwitchToEncoding(ctxt, handler)
2585    }
2586}
2587
2588/// Switch the input encoding by name.
2589///
2590/// # UPSTREAM-PARITY
2591///
2592/// ```c
2593/// int xmlSwitchEncodingName(xmlParserCtxtPtr ctxt, const char *encoding);
2594/// ```
2595///
2596/// # SAFETY
2597///
2598/// - `ctxt` must be valid pointers (or NULL
2599///   where the upstream C contract allows), obtained from the
2600///   matching constructor/owner and not yet freed; the callee may
2601///   take or keep ownership exactly as the C API specifies.
2602///
2603/// - `encoding` must point to valid NUL-terminated
2604///   strings (or NULL where the C contract allows) for the lifetime
2605///   of the call.
2606///
2607/// The caller must not race this call with concurrent mutation of the
2608/// same objects from other threads (per-object state is not internally
2609/// synchronized). Violating any of the above is undefined behavior.
2610///
2611/// Exercised by the C-API differential courts
2612/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2613/// courts; those pass byte-for-byte against the upstream oracle.
2614#[no_mangle]
2615pub unsafe extern "C" fn xmlSwitchEncodingName(
2616    ctxt: *mut _xmlParserCtxt,
2617    encoding: *const c_char,
2618) -> c_int {
2619    if ctxt.is_null() || encoding.is_null() {
2620        return -1;
2621    }
2622    unsafe {
2623        let handler = encoding::find_encoding_handler(encoding as *const xmlChar);
2624        if handler.is_null() {
2625            return -1;
2626        }
2627        xmlSwitchToEncoding(ctxt, handler)
2628    }
2629}
2630
2631/// Switch the encoding of a specific parser input using an encoding handler.
2632///
2633/// # UPSTREAM-PARITY
2634///
2635/// ```c
2636/// int xmlSwitchInputEncoding(xmlParserCtxtPtr ctxt, xmlParserInputPtr input,
2637///                            xmlCharEncodingHandlerPtr handler);
2638/// ```
2639///
2640/// # SAFETY
2641///
2642/// - `ctxt`, `input`, `handler` must be valid pointers (or NULL
2643///   where the upstream C contract allows), obtained from the
2644///   matching constructor/owner and not yet freed; the callee may
2645///   take or keep ownership exactly as the C API specifies.
2646///
2647/// The caller must not race this call with concurrent mutation of the
2648/// same objects from other threads (per-object state is not internally
2649/// synchronized). Violating any of the above is undefined behavior.
2650///
2651/// Exercised by the C-API differential courts
2652/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2653/// courts; those pass byte-for-byte against the upstream oracle.
2654#[no_mangle]
2655pub unsafe extern "C" fn xmlSwitchInputEncoding(
2656    ctxt: *mut _xmlParserCtxt,
2657    input: *mut _xmlParserInput,
2658    handler: *mut _xmlCharEncodingHandler,
2659) -> c_int {
2660    let _ = ctxt;
2661    if input.is_null() {
2662        return -1;
2663    }
2664    unsafe {
2665        if (*input).buf.is_null() {
2666            return -1;
2667        }
2668        io::input_buffer_set_encoder((*input).buf, handler);
2669    }
2670    0
2671}
2672
2673/// Switch the encoding of the current input using an encoding handler.
2674///
2675/// # UPSTREAM-PARITY
2676///
2677/// ```c
2678/// int xmlSwitchToEncoding(xmlParserCtxtPtr ctxt,
2679///                         xmlCharEncodingHandlerPtr handler);
2680/// ```
2681///
2682/// # SAFETY
2683///
2684/// - `ctxt`, `handler` must be valid pointers (or NULL
2685///   where the upstream C contract allows), obtained from the
2686///   matching constructor/owner and not yet freed; the callee may
2687///   take or keep ownership exactly as the C API specifies.
2688///
2689/// The caller must not race this call with concurrent mutation of the
2690/// same objects from other threads (per-object state is not internally
2691/// synchronized). Violating any of the above is undefined behavior.
2692///
2693/// Exercised by the C-API differential courts
2694/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2695/// courts; those pass byte-for-byte against the upstream oracle.
2696#[no_mangle]
2697pub unsafe extern "C" fn xmlSwitchToEncoding(
2698    ctxt: *mut _xmlParserCtxt,
2699    handler: *mut _xmlCharEncodingHandler,
2700) -> c_int {
2701    if ctxt.is_null() {
2702        return -1;
2703    }
2704    unsafe {
2705        let input = (*ctxt).input;
2706        if input.is_null() {
2707            return -1;
2708        }
2709        // Memory-parser inputs (xmlCreateMemoryParserCtxt) carry buf == NULL;
2710        // their bytes live in the Rust-side InputBuffer (helpers.rs side
2711        // table), which already transcoded any BOM/declared encoding. A
2712        // caller-driven switch (PHP dom overrideEncoding) must therefore
2713        // transcode the whole buffered stream there (upstream applies the
2714        // input-buffer encoder before any read).
2715        if (*input).buf.is_null() && !(*handler).name.is_null() {
2716            return helpers::apply_memory_encoding_override(ctxt, (*handler).name);
2717        }
2718        io::input_buffer_set_encoder((*input).buf, handler);
2719    }
2720    0
2721}
2722
2723// ═══════════════════════════════════════════════════════════════════════════════
2724// Node info sequence (deprecated, parser.h)
2725// ═══════════════════════════════════════════════════════════════════════════════
2726
2727/// Initialise a node info sequence.
2728///
2729/// # UPSTREAM-PARITY
2730///
2731/// ```c
2732/// void xmlInitNodeInfoSeq(xmlParserNodeInfoSeqPtr seq);
2733/// ```
2734///
2735/// # SAFETY
2736///
2737/// - `seq` must be valid pointers (or NULL
2738///   where the upstream C contract allows), obtained from the
2739///   matching constructor/owner and not yet freed; the callee may
2740///   take or keep ownership exactly as the C API specifies.
2741///
2742/// The caller must not race this call with concurrent mutation of the
2743/// same objects from other threads (per-object state is not internally
2744/// synchronized). Violating any of the above is undefined behavior.
2745///
2746/// Exercised by the C-API differential courts
2747/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2748/// courts; those pass byte-for-byte against the upstream oracle.
2749#[no_mangle]
2750pub unsafe extern "C" fn xmlInitNodeInfoSeq(seq: *mut _xmlParserNodeInfoSeq) {
2751    if seq.is_null() {
2752        return;
2753    }
2754    unsafe {
2755        (*seq).length = 0;
2756        (*seq).maximum = 0;
2757        (*seq).buffer = ptr::null_mut();
2758    }
2759}
2760
2761/// Clear (release and reinitialise) a node info sequence.
2762///
2763/// # UPSTREAM-PARITY
2764///
2765/// ```c
2766/// void xmlClearNodeInfoSeq(xmlParserNodeInfoSeqPtr seq);
2767/// ```
2768///
2769/// # SAFETY
2770///
2771/// - `seq` must be valid pointers (or NULL
2772///   where the upstream C contract allows), obtained from the
2773///   matching constructor/owner and not yet freed; the callee may
2774///   take or keep ownership exactly as the C API specifies.
2775///
2776/// The caller must not race this call with concurrent mutation of the
2777/// same objects from other threads (per-object state is not internally
2778/// synchronized). Violating any of the above is undefined behavior.
2779///
2780/// Exercised by the C-API differential courts
2781/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2782/// courts; those pass byte-for-byte against the upstream oracle.
2783#[no_mangle]
2784pub unsafe extern "C" fn xmlClearNodeInfoSeq(seq: *mut _xmlParserNodeInfoSeq) {
2785    if seq.is_null() {
2786        return;
2787    }
2788    unsafe {
2789        if !(*seq).buffer.is_null() {
2790            xmlFreeImpl((*seq).buffer as *mut c_void);
2791        }
2792        xmlInitNodeInfoSeq(seq);
2793    }
2794}
2795
2796/// Find the index where the info record for `node` is (or should be) in the
2797/// sorted sequence; binary search by node pointer.
2798///
2799/// # UPSTREAM-PARITY
2800///
2801/// ```c
2802/// unsigned long xmlParserFindNodeInfoIndex(xmlParserNodeInfoSeqPtr seq,
2803///                                          xmlNodePtr node);
2804/// ```
2805///
2806/// # SAFETY
2807///
2808/// - `seq`, `node` must be valid pointers (or NULL
2809///   where the upstream C contract allows), obtained from the
2810///   matching constructor/owner and not yet freed; the callee may
2811///   take or keep ownership exactly as the C API specifies.
2812///
2813/// The caller must not race this call with concurrent mutation of the
2814/// same objects from other threads (per-object state is not internally
2815/// synchronized). Violating any of the above is undefined behavior.
2816///
2817/// Exercised by the C-API differential courts
2818/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2819/// courts; those pass byte-for-byte against the upstream oracle.
2820#[no_mangle]
2821pub unsafe extern "C" fn xmlParserFindNodeInfoIndex(
2822    seq: *mut _xmlParserNodeInfoSeq,
2823    node: *mut _xmlNode,
2824) -> c_ulong {
2825    if seq.is_null() || node.is_null() {
2826        return c_ulong::MAX;
2827    }
2828    unsafe {
2829        let s = &*seq;
2830        if s.buffer.is_null() || s.length == 0 {
2831            return 0;
2832        }
2833        let mut lower: usize = 0;
2834        let mut upper: usize = s.length as usize;
2835        while lower < upper {
2836            let middle = lower + (upper - lower) / 2;
2837            let cur_node = (*s.buffer.add(middle)).node;
2838            if cur_node == node {
2839                return middle as c_ulong;
2840            }
2841            if (cur_node as usize) < (node as usize) {
2842                lower = middle + 1;
2843            } else {
2844                upper = middle;
2845            }
2846        }
2847        lower as c_ulong
2848    }
2849}
2850
2851/// Find the node info record for a given node, or NULL.
2852///
2853/// # UPSTREAM-PARITY
2854///
2855/// ```c
2856/// const xmlParserNodeInfo *xmlParserFindNodeInfo(xmlParserCtxtPtr ctxt,
2857///                                                xmlNodePtr node);
2858/// ```
2859///
2860/// # SAFETY
2861///
2862/// - `ctxt`, `node` must be valid pointers (or NULL
2863///   where the upstream C contract allows), obtained from the
2864///   matching constructor/owner and not yet freed; the callee may
2865///   take or keep ownership exactly as the C API specifies.
2866///
2867/// The caller must not race this call with concurrent mutation of the
2868/// same objects from other threads (per-object state is not internally
2869/// synchronized). Violating any of the above is undefined behavior.
2870///
2871/// Exercised by the C-API differential courts
2872/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2873/// courts; those pass byte-for-byte against the upstream oracle.
2874#[no_mangle]
2875pub unsafe extern "C" fn xmlParserFindNodeInfo(
2876    ctxt: *mut _xmlParserCtxt,
2877    node: *mut _xmlNode,
2878) -> *const _xmlParserNodeInfo {
2879    if ctxt.is_null() || node.is_null() {
2880        return ptr::null();
2881    }
2882    unsafe {
2883        let seq = &(*ctxt).node_seq;
2884        let seq_mut = seq as *const _ as *mut _xmlParserNodeInfoSeq;
2885        let pos = xmlParserFindNodeInfoIndex(seq_mut, node);
2886        if !seq.buffer.is_null() && (pos as usize) < (seq.length as usize) {
2887            let info = &*seq.buffer.add(pos as usize);
2888            if info.node == node {
2889                return info;
2890            }
2891        }
2892        ptr::null()
2893    }
2894}
2895
2896/// Insert a node info record into the context's sorted sequence.
2897///
2898/// # UPSTREAM-PARITY
2899///
2900/// ```c
2901/// void xmlParserAddNodeInfo(xmlParserCtxtPtr ctxt, xmlParserNodeInfoPtr info);
2902/// ```
2903///
2904/// # SAFETY
2905///
2906/// - `ctxt`, `info` must be valid pointers (or NULL
2907///   where the upstream C contract allows), obtained from the
2908///   matching constructor/owner and not yet freed; the callee may
2909///   take or keep ownership exactly as the C API specifies.
2910///
2911/// The caller must not race this call with concurrent mutation of the
2912/// same objects from other threads (per-object state is not internally
2913/// synchronized). Violating any of the above is undefined behavior.
2914///
2915/// Exercised by the C-API differential courts
2916/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2917/// courts; those pass byte-for-byte against the upstream oracle.
2918#[no_mangle]
2919pub unsafe extern "C" fn xmlParserAddNodeInfo(
2920    ctxt: *mut _xmlParserCtxt,
2921    info: *mut _xmlParserNodeInfo,
2922) {
2923    if ctxt.is_null() || info.is_null() {
2924        return;
2925    }
2926    unsafe {
2927        let seq = &mut (*ctxt).node_seq;
2928        let node = (*info).node;
2929        let pos = xmlParserFindNodeInfoIndex(seq, node as *mut _xmlNode) as usize;
2930
2931        if pos < seq.length as usize && !seq.buffer.is_null() && (*seq.buffer.add(pos)).node == node
2932        {
2933            // Node already recorded: update the record in place.
2934            ptr::copy_nonoverlapping(info, seq.buffer.add(pos), 1);
2935            return;
2936        }
2937
2938        // Grow the buffer (upstream xmlGrowCapacity: 50% growth from a
2939        // minimum of 4, capped at XML_MAX_ITEMS = 1 billion).
2940        if seq.length + 1 > seq.maximum {
2941            let new_max = xml_grow_capacity(seq.maximum);
2942            if new_max < 0 {
2943                xmlCtxtErrMemory(ctxt);
2944                return;
2945            }
2946            let new_buf = xmlReallocImpl(
2947                seq.buffer as *mut c_void,
2948                (new_max as usize) * core::mem::size_of::<_xmlParserNodeInfo>(),
2949            ) as *mut _xmlParserNodeInfo;
2950            if new_buf.is_null() {
2951                xmlCtxtErrMemory(ctxt);
2952                return;
2953            }
2954            seq.buffer = new_buf;
2955            seq.maximum = new_max as c_ulong;
2956        }
2957
2958        // Shift elements right to make room at `pos`.
2959        let length = seq.length as usize;
2960        for i in (pos + 1..=length).rev() {
2961            ptr::copy_nonoverlapping(seq.buffer.add(i - 1), seq.buffer.add(i), 1);
2962        }
2963        ptr::copy_nonoverlapping(info, seq.buffer.add(pos), 1);
2964        seq.length += 1;
2965    }
2966}
2967
2968/// Upstream `xmlGrowCapacity` (private/memory.h) for a zero-based capacity:
2969/// 50% growth, minimum initial allocation 4, capped at XML_MAX_ITEMS.
2970/// Returns the new capacity or -1 on overflow/cap exhaustion.
2971// The `as u64` casts are width-correcting for 32-bit platforms where
2972// `c_ulong` is 32 bits; on x86-64 they are identity casts.
2973#[allow(clippy::unnecessary_cast)]
2974const unsafe fn xml_grow_capacity(capacity: c_ulong) -> c_int {
2975    const XML_MAX_ITEMS: u64 = 1_000_000_000;
2976    const ELEM_SIZE: usize = core::mem::size_of::<_xmlParserNodeInfo>();
2977    if capacity == 0 {
2978        return 4;
2979    }
2980    if capacity as u64 >= XML_MAX_ITEMS || (capacity as usize) > usize::MAX / 2 / ELEM_SIZE {
2981        return -1;
2982    }
2983    let extra = capacity.div_ceil(2);
2984    if capacity as u64 > XML_MAX_ITEMS - extra as u64 {
2985        return XML_MAX_ITEMS as c_int;
2986    }
2987    (capacity + extra) as c_int
2988}
2989
2990// ═══════════════════════════════════════════════════════════════════════════════
2991// I/O callback registration (xmlIO.h)
2992// ═══════════════════════════════════════════════════════════════════════════════
2993
2994/// Register a new set of input I/O callbacks.
2995///
2996/// # UPSTREAM-PARITY
2997///
2998/// ```c
2999/// int xmlRegisterInputCallbacks(xmlInputMatchCallback matchFunc,
3000///                               xmlInputOpenCallback openFunc,
3001///                               xmlInputReadCallback readFunc,
3002///                               xmlInputCloseCallback closeFunc);
3003/// ```
3004///
3005/// # SAFETY
3006///
3007///
3008/// - `matchFunc`, `openFunc`, `readFunc`, `closeFunc` must be a valid callback (or None);
3009///   the callback is invoked with the documented context pointer and
3010///   must itself uphold the same pointer invariants.
3011///
3012/// The caller must not race this call with concurrent mutation of the
3013/// same objects from other threads (per-object state is not internally
3014/// synchronized). Violating any of the above is undefined behavior.
3015///
3016/// Exercised by the C-API differential courts
3017/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3018/// courts; those pass byte-for-byte against the upstream oracle.
3019#[no_mangle]
3020pub unsafe extern "C" fn xmlRegisterInputCallbacks(
3021    matchFunc: Option<xmlInputMatchCallback>,
3022    openFunc: Option<xmlInputOpenCallback>,
3023    readFunc: Option<xmlInputReadCallback>,
3024    closeFunc: Option<xmlInputCloseCallback>,
3025) -> c_int {
3026    unsafe {
3027        globals::init_parser();
3028    }
3029    let mut table = INPUT_CALLBACKS.lock();
3030    if table.len() >= 10 {
3031        return -1;
3032    }
3033    table.push(InputCallbackEntry {
3034        matchcb: matchFunc,
3035        opencb: openFunc,
3036        readcb: readFunc,
3037        closecb: closeFunc,
3038    });
3039    (table.len() - 1) as c_int
3040}
3041
3042/// Register the default compiled-in input callbacks (the `xmlFile*` pair).
3043///
3044/// # UPSTREAM-PARITY
3045///
3046/// ```c
3047/// void xmlRegisterDefaultInputCallbacks(void);
3048/// ```
3049///
3050/// # SAFETY
3051///
3052/// The function touches crate-global state only; it is safe
3053/// as long as the caller respects the library's global
3054/// initialization/cleanup ordering (xmlInitParser before use,
3055/// xmlCleanupParser only after all users are done).
3056///
3057/// Violating the global lifecycle ordering, or calling this after
3058/// teardown or from a signal handler, is undefined behavior.
3059#[no_mangle]
3060pub unsafe extern "C" fn xmlRegisterDefaultInputCallbacks() {
3061    unsafe {
3062        xmlRegisterInputCallbacks(
3063            Some(xmlFileMatch),
3064            Some(xmlFileOpen),
3065            Some(xmlFileRead),
3066            Some(xmlFileClose),
3067        );
3068    }
3069}
3070
3071/// Remove the top input callback from the stack.
3072///
3073/// # UPSTREAM-PARITY
3074///
3075/// ```c
3076/// int xmlPopInputCallbacks(void);
3077/// ```
3078///
3079/// # SAFETY
3080///
3081/// The function touches crate-global state only; it is safe
3082/// as long as the caller respects the library's global
3083/// initialization/cleanup ordering (xmlInitParser before use,
3084/// xmlCleanupParser only after all users are done).
3085///
3086/// Violating the global lifecycle ordering, or calling this after
3087/// teardown or from a signal handler, is undefined behavior.
3088#[no_mangle]
3089pub unsafe extern "C" fn xmlPopInputCallbacks() -> c_int {
3090    unsafe {
3091        globals::init_parser();
3092    }
3093    let mut table = INPUT_CALLBACKS.lock();
3094    if table.is_empty() {
3095        return -1;
3096    }
3097    table.pop();
3098    table.len() as c_int
3099}
3100
3101/// Clear the entire input callback table.
3102///
3103/// # UPSTREAM-PARITY
3104///
3105/// ```c
3106/// void xmlCleanupInputCallbacks(void);
3107/// ```
3108///
3109/// # SAFETY
3110///
3111/// The function touches crate-global state only; it is safe
3112/// as long as the caller respects the library's global
3113/// initialization/cleanup ordering (xmlInitParser before use,
3114/// xmlCleanupParser only after all users are done).
3115///
3116/// Violating the global lifecycle ordering, or calling this after
3117/// teardown or from a signal handler, is undefined behavior.
3118#[no_mangle]
3119pub unsafe extern "C" fn xmlCleanupInputCallbacks() {
3120    unsafe {
3121        globals::init_parser();
3122    }
3123    INPUT_CALLBACKS.lock().clear();
3124}
3125
3126/// Read a URI through the registered input callbacks (upstream
3127/// `xmlParserInputBufferCreateFilename`): the first registered pair whose
3128/// match callback accepts the URI is opened, read to EOF, and closed.
3129/// Returns `None` when no registered pair matches — callers fall back to
3130/// the regular file path. NULL callbacks inside a matching pair are treated
3131/// like upstream (an entry whose match callback is NULL is skipped).
3132///
3133/// Used by the XInclude loader so custom I/O schemes registered through
3134/// `xmlRegisterInputCallbacks` are honored (upstream xmlXIncludeLoadDoc →
3135/// xmlNewInputFromFile; Phase-12 EXTERNAL-CONSUMERS court: io1.c registers
3136/// an sql: scheme and XInclude hrefs route through it).
3137///
3138/// # SAFETY
3139///
3140/// - `uri` must be a valid NUL-terminated C string live for the call.
3141pub(crate) unsafe fn read_uri_via_input_callbacks(uri: *const c_char) -> Option<Vec<u8>> {
3142    let table = INPUT_CALLBACKS.lock();
3143    for e in table.iter() {
3144        let Some(matchcb) = e.matchcb else {
3145            continue;
3146        };
3147        // SAFETY: callbacks were registered by the caller and must uphold
3148        // the xmlInput*Callback contracts.
3149        if unsafe { matchcb(uri) } == 0 {
3150            continue;
3151        }
3152        let (Some(opencb), Some(readcb)) = (e.opencb, e.readcb) else {
3153            return None;
3154        };
3155        // SAFETY: the open callback returns a context for read/close.
3156        let ctx = unsafe { opencb(uri) };
3157        if ctx.is_null() {
3158            return None;
3159        }
3160        let mut data = Vec::new();
3161        let mut buf = [0u8; 4096];
3162        loop {
3163            // SAFETY: readcb fills `buf` per the xmlInputReadCallback contract.
3164            let n = unsafe { readcb(ctx, buf.as_mut_ptr() as *mut c_char, buf.len() as c_int) };
3165            if n < 0 {
3166                if let Some(closecb) = e.closecb {
3167                    unsafe { closecb(ctx) };
3168                }
3169                return None;
3170            }
3171            if n == 0 {
3172                break;
3173            }
3174            data.extend_from_slice(&buf[..n as usize]);
3175        }
3176        if let Some(closecb) = e.closecb {
3177            unsafe { closecb(ctx) };
3178        }
3179        return Some(data);
3180    }
3181    None
3182}
3183
3184/// Register a new set of output I/O callbacks.
3185///
3186/// # UPSTREAM-PARITY
3187///
3188/// ```c
3189/// int xmlRegisterOutputCallbacks(xmlOutputMatchCallback matchFunc,
3190///                                xmlOutputOpenCallback openFunc,
3191///                                xmlOutputWriteCallback writeFunc,
3192///                                xmlOutputCloseCallback closeFunc);
3193/// ```
3194///
3195/// # SAFETY
3196///
3197///
3198/// - `matchFunc`, `openFunc`, `writeFunc`, `closeFunc` must be a valid callback (or None);
3199///   the callback is invoked with the documented context pointer and
3200///   must itself uphold the same pointer invariants.
3201///
3202/// The caller must not race this call with concurrent mutation of the
3203/// same objects from other threads (per-object state is not internally
3204/// synchronized). Violating any of the above is undefined behavior.
3205///
3206/// Exercised by the C-API differential courts
3207/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3208/// courts; those pass byte-for-byte against the upstream oracle.
3209#[no_mangle]
3210pub unsafe extern "C" fn xmlRegisterOutputCallbacks(
3211    matchFunc: Option<xmlOutputMatchCallback>,
3212    openFunc: Option<xmlOutputOpenCallback>,
3213    writeFunc: Option<xmlOutputWriteCallback>,
3214    closeFunc: Option<xmlOutputCloseCallback>,
3215) -> c_int {
3216    unsafe {
3217        globals::init_parser();
3218    }
3219    let mut table = OUTPUT_CALLBACKS.lock();
3220    if table.len() >= 10 {
3221        return -1;
3222    }
3223    table.push(OutputCallbackEntry {
3224        matchcb: matchFunc,
3225        opencb: openFunc,
3226        writecb: writeFunc,
3227        closecb: closeFunc,
3228    });
3229    (table.len() - 1) as c_int
3230}
3231
3232/// Register the default compiled-in output callbacks.
3233///
3234/// # UPSTREAM-PARITY
3235///
3236/// ```c
3237/// void xmlRegisterDefaultOutputCallbacks(void);
3238/// ```
3239///
3240/// # SAFETY
3241///
3242/// The function touches crate-global state only; it is safe
3243/// as long as the caller respects the library's global
3244/// initialization/cleanup ordering (xmlInitParser before use,
3245/// xmlCleanupParser only after all users are done).
3246///
3247/// Violating the global lifecycle ordering, or calling this after
3248/// teardown or from a signal handler, is undefined behavior.
3249#[no_mangle]
3250pub unsafe extern "C" fn xmlRegisterDefaultOutputCallbacks() {
3251    unsafe {
3252        xmlRegisterOutputCallbacks(Some(xmlFileMatch), None, None, None);
3253    }
3254}
3255
3256/// Register the HTTP POST output callbacks (upstream: default output callbacks).
3257///
3258/// # UPSTREAM-PARITY
3259///
3260/// ```c
3261/// void xmlRegisterHTTPPostCallbacks(void);
3262/// ```
3263///
3264/// # SAFETY
3265///
3266/// The function touches crate-global state only; it is safe
3267/// as long as the caller respects the library's global
3268/// initialization/cleanup ordering (xmlInitParser before use,
3269/// xmlCleanupParser only after all users are done).
3270///
3271/// Violating the global lifecycle ordering, or calling this after
3272/// teardown or from a signal handler, is undefined behavior.
3273#[no_mangle]
3274pub unsafe extern "C" fn xmlRegisterHTTPPostCallbacks() {
3275    unsafe { xmlRegisterDefaultOutputCallbacks() }
3276}
3277
3278/// Remove the top output callback from the stack.
3279///
3280/// # UPSTREAM-PARITY
3281///
3282/// ```c
3283/// int xmlPopOutputCallbacks(void);
3284/// ```
3285///
3286/// # SAFETY
3287///
3288/// The function touches crate-global state only; it is safe
3289/// as long as the caller respects the library's global
3290/// initialization/cleanup ordering (xmlInitParser before use,
3291/// xmlCleanupParser only after all users are done).
3292///
3293/// Violating the global lifecycle ordering, or calling this after
3294/// teardown or from a signal handler, is undefined behavior.
3295#[no_mangle]
3296pub unsafe extern "C" fn xmlPopOutputCallbacks() -> c_int {
3297    unsafe {
3298        globals::init_parser();
3299    }
3300    let mut table = OUTPUT_CALLBACKS.lock();
3301    if table.is_empty() {
3302        return -1;
3303    }
3304    table.pop();
3305    table.len() as c_int
3306}
3307
3308/// Clear the entire output callback table.
3309///
3310/// # UPSTREAM-PARITY
3311///
3312/// ```c
3313/// void xmlCleanupOutputCallbacks(void);
3314/// ```
3315///
3316/// # SAFETY
3317///
3318/// The function touches crate-global state only; it is safe
3319/// as long as the caller respects the library's global
3320/// initialization/cleanup ordering (xmlInitParser before use,
3321/// xmlCleanupParser only after all users are done).
3322///
3323/// Violating the global lifecycle ordering, or calling this after
3324/// teardown or from a signal handler, is undefined behavior.
3325#[no_mangle]
3326pub unsafe extern "C" fn xmlCleanupOutputCallbacks() {
3327    unsafe {
3328        globals::init_parser();
3329    }
3330    OUTPUT_CALLBACKS.lock().clear();
3331}
3332
3333// ═══════════════════════════════════════════════════════════════════════════════
3334// External entity loaders (parser.h)
3335// ═══════════════════════════════════════════════════════════════════════════════
3336
3337/// Default external entity loader: resolve `url` against the filesystem,
3338/// honouring XML_PARSE_NONET.
3339///
3340/// # Safety
3341///
3342/// `url`/`publicId` must be valid C strings or NULL; `ctxt` may be NULL.
3343unsafe extern "C" fn default_external_entity_loader(
3344    url: *const c_char,
3345    public_id: *const c_char,
3346    ctxt: *mut _xmlParserCtxt,
3347) -> *mut _xmlParserInput {
3348    let _ = public_id;
3349    if url.is_null() {
3350        return ptr::null_mut();
3351    }
3352    unsafe {
3353        // Refuse network access when NONET is set.
3354        if !ctxt.is_null() && (*ctxt).options & XML_PARSE_NONET != 0 {
3355            let len = libc::strlen(url);
3356            if len >= 7 && libc::strncasecmp(url, c"http://".as_ptr() as *const c_char, 7) == 0 {
3357                return ptr::null_mut();
3358            }
3359        }
3360        // UPSTREAM-PARITY (parserInternals.c xmlDefaultExternalEntityLoader
3361        // -> xmlNewInputFromFile -> xmlNewInputFromUrl): the registered
3362        // xmlParserInputBufferCreateFilenameDefault (php streams loader) is
3363        // consulted BEFORE the input-callback table and the built-in open. A
3364        // NULL loader result is XML_IO_ENOENT — xmlCtxtErrIO is raised and
3365        // there is no fallback.
3366        if globals::get_parser_input_buffer_create_filename_value_cross_dso().is_some() {
3367            // SAFETY: url is a valid NUL-terminated C string for the call.
3368            return match call_loader_materialize(url) {
3369                Err(()) => {
3370                    emit_io_warning(ctxt, io_load_failure_message(url));
3371                    ptr::null_mut()
3372                }
3373                Ok(data) => {
3374                    // Build a MEMORY-backed C input: the entity machinery
3375                    // consumes the loader result through base/end (upstream
3376                    // buffers the external entity content the same way). A
3377                    // zero-length result (php://memory, 0-byte file) is a
3378                    // VALID empty input — the parse reports "Document is
3379                    // empty" (php DOM createFromFile).
3380                    let mem = if data.is_empty() {
3381                        crate::xml::io::input_buffer_create_empty()
3382                    } else {
3383                        io::input_buffer_create_mem(
3384                            data.as_ptr() as *const c_char,
3385                            data.len() as c_int,
3386                            xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int,
3387                        )
3388                    };
3389                    if mem.is_null() {
3390                        return ptr::null_mut();
3391                    }
3392                    parser_input_from_buf(mem)
3393                }
3394            };
3395        }
3396        // Try the registered input callbacks first.
3397        let table = INPUT_CALLBACKS.lock();
3398        for entry in table.iter() {
3399            if let (Some(match_cb), Some(open_cb)) = (entry.matchcb, entry.opencb) {
3400                if match_cb(url) != 0 {
3401                    let ctx = open_cb(url);
3402                    if !ctx.is_null() {
3403                        let buf = helpers::alloc_parser_input_buffer();
3404                        if buf.is_null() {
3405                            if let Some(close_cb) = entry.closecb {
3406                                close_cb(ctx);
3407                            }
3408                            return ptr::null_mut();
3409                        }
3410                        (*buf).context = ctx;
3411                        (*buf).readcallback = entry.readcb;
3412                        (*buf).closecallback = entry.closecb;
3413                        return parser_input_from_buf(buf);
3414                    }
3415                }
3416            }
3417        }
3418
3419        // Fall back to a plain file open.
3420        let buf =
3421            io::input_buffer_create_file(url, xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int);
3422        if buf.is_null() {
3423            // UPSTREAM-PARITY (parserInternals.c xmlNewInputFromFile): a
3424            // failed load raises xmlCtxtErrIO(ctxt, XML_IO_ENOENT, url) —
3425            // "I/O warning : failed to load \"%s\": %s\n" with the
3426            // strerror text (HOSTILE-FAILURE F7).
3427            let errno = *libc::__errno_location();
3428            let errstr = if errno == 0 {
3429                String::new()
3430            } else {
3431                std::ffi::CStr::from_ptr(libc::strerror(errno))
3432                    .to_string_lossy()
3433                    .into_owned()
3434            };
3435            let url_str = std::ffi::CStr::from_ptr(url).to_string_lossy();
3436            emit_io_warning(ctxt, format!("failed to load \"{url_str}\": {errstr}\n"));
3437            return ptr::null_mut();
3438        }
3439        parser_input_from_buf(buf)
3440    }
3441}
3442
3443/// UPSTREAM-PARITY (parserInternals.c xmlCtxtErrIO): raise an I/O warning
3444/// (XML_FROM_IO, XML_IO_ENOENT, XML_ERR_WARNING) through the parser's
3445/// channel — "I/O warning : <message>".
3446pub(crate) unsafe fn emit_io_warning(ctxt: *mut _xmlParserCtxt, message: String) {
3447    let msg_c = std::ffi::CString::new(message).unwrap_or_default();
3448    let delivery = if ctxt.is_null() {
3449        crate::xml::errors::GenericDelivery::Stream
3450    } else {
3451        unsafe { crate::xml::errors::parser_delivery(ctxt) }
3452    };
3453    unsafe {
3454        crate::xml::errors::raise_error_streamed(
3455            ctxt as *mut c_void,
3456            crate::abi::types::XML_FROM_IO,
3457            crate::abi::types::XML_IO_ENOENT,
3458            crate::abi::types::xmlErrorLevel::XML_ERR_WARNING as c_int,
3459            ptr::null(),
3460            0,
3461            0,
3462            ptr::null(),
3463            ptr::null(),
3464            ptr::null(),
3465            0,
3466            msg_c.as_ptr(),
3467            None,
3468            None,
3469            delivery,
3470            None,
3471        );
3472    }
3473}
3474
3475/// Result of routing a filename open through the registered loaders
3476/// (upstream 2.14+ `xmlLoadResource` layering).
3477#[allow(dead_code)]
3478pub(crate) enum RoutedFileOpen {
3479    /// No custom loader is registered — the caller falls back to the built-in
3480    /// file open (`helpers::input_from_file`).
3481    Builtin,
3482    /// A registered loader returned NULL: upstream reports `XML_IO_ENOENT`
3483    /// with NO built-in fallback (php streams loader: missing file, percent-
3484    /// encoded-NUL guard, disabled entity loader).
3485    Failed,
3486    /// A registered EXTERNAL ENTITY loader (`xmlSetExternalEntityLoader`)
3487    /// returned NULL for a file/URL open. Upstream `xmlCtxtNewInputFromUrl`
3488    /// propagates that NULL silently (no `xmlCtxtErrIO` — the custom loader
3489    /// owns its own error reporting), so callers fail without a warning.
3490    EntityLoaderFailed,
3491    /// The loader produced an input buffer whose bytes were materialized
3492    /// (filename = the original URI).
3493    Loaded(InputBuffer),
3494}
3495
3496/// UPSTREAM-PARITY (parserInternals.c `xmlNewInputFromUrl`): when a custom
3497/// `xmlParserInputBufferCreateFilenameDefault` is registered (PHP installs
3498/// its streams loader at request init), filename opens consult it FIRST —
3499/// php streams unescape `file://` URIs, enforce the percent-encoded-NUL
3500/// guard, honor stream contexts and emit their own failure warnings. A NULL
3501/// loader result is `XML_IO_ENOENT`; upstream does NOT fall back to the
3502/// built-in open in that case. Without a registered loader the caller keeps
3503/// the built-in path.
3504///
3505/// Invoke the registered loader and materialize the produced buffer's bytes
3506/// through its read callback, releasing the C buffer/stream exactly once
3507/// (the close callback runs when the buffer is freed). Returns `Err(())` on
3508/// a NULL loader result or a read-callback error.
3509///
3510/// # Safety
3511///
3512/// - `uri` must be a valid NUL-terminated C string live for the call; the
3513///   registered loader callback (if any) must uphold the
3514///   `xmlParserInputBufferCreateFilenameFunc` contract.
3515pub(crate) unsafe fn call_loader_materialize(uri: *const c_char) -> Result<Vec<u8>, ()> {
3516    // SAFETY: reads the per-thread loader slot — through the R-000177
3517    // cross-DSO bridge so the whole-archive facade copies observe the
3518    // loader a consumer registered via the core DSO's exported setter
3519    // (upstream: single core DSO, registration visible everywhere).
3520    let Some(func) = globals::get_parser_input_buffer_create_filename_value_cross_dso() else {
3521        return Err(());
3522    };
3523    // SAFETY: `func` is the consumer-registered C loader and must uphold the
3524    // xmlParserInputBufferCreateFilenameFunc contract (uri + enc in, buffer
3525    // out, or NULL on failure).
3526    let buf = unsafe { func(uri, xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int) };
3527    if buf.is_null() {
3528        return Err(());
3529    }
3530    let (read, ctx) = unsafe {
3531        let b = &*buf;
3532        (b.readcallback, b.context)
3533    };
3534    let mut data: Vec<u8> = Vec::new();
3535    let mut result = Err(());
3536    if let Some(read) = read {
3537        let mut tmp = [0u8; 4096];
3538        loop {
3539            // SAFETY: the loader's buffer carries the consumer's read
3540            // callback + context (xmlParserInputBufferCreateIO contract).
3541            let n = unsafe { read(ctx, tmp.as_mut_ptr() as *mut c_char, tmp.len() as c_int) };
3542            if n < 0 {
3543                break;
3544            }
3545            if n == 0 {
3546                result = Ok(());
3547                break;
3548            }
3549            data.extend_from_slice(&tmp[..n as usize]);
3550        }
3551    } else {
3552        // A memory-backed loader buffer (no read callback): copy its content.
3553        unsafe {
3554            let b = &*buf;
3555            if !b.buffer.is_null() {
3556                let xbuf = &*(b.buffer as *mut _xmlBuffer);
3557                if !xbuf.content.is_null() && xbuf.use_ > 0 {
3558                    data.extend_from_slice(std::slice::from_raw_parts(
3559                        xbuf.content as *const u8,
3560                        xbuf.use_ as usize,
3561                    ));
3562                }
3563            }
3564        }
3565        result = Ok(());
3566    }
3567    // Release the loader's C buffer: the close callback (php streams IO
3568    // close) runs exactly once now that the bytes are owned here.
3569    io::input_buffer_free(buf);
3570    result.map(|()| data)
3571}
3572
3573/// Route a filename open through the registered loaders, materializing the
3574/// result into an owned [`InputBuffer`] (filename = the original URI).
3575///
3576/// UPSTREAM LAYERING (2.14+ `xmlLoadResource`, R-000177): a REGISTERED
3577/// external entity loader (`xmlSetExternalEntityLoader`) is consulted first
3578/// for file/URL opens — main documents go through the same resource loader
3579/// as entities (`xmlCtxtNewInputFromUrl` -> `xmlLoadResource` ->
3580/// `xmlCurrentExternalEntityLoader`). A NULL custom-loader result is
3581/// `EntityLoaderFailed` (silent upstream — no `xmlCtxtErrIO`, the custom
3582/// loader reports its own errors). With NO custom entity loader the default
3583/// loader's tail is the `xmlParserInputBufferCreateFilenameDefault` (php
3584/// streams) loader, which is what the rest of this function implements
3585/// (upstream `xmlNewInputFromUrl`).
3586///
3587/// The registration is read through the R-000177 cross-DSO bridge (facade
3588/// copies must see a loader registered via the core DSO's exported setter).
3589///
3590/// # Safety
3591///
3592/// - `uri` must be a valid NUL-terminated C string live for the call.
3593/// - `ctxt` must be NULL or a valid parser context live for the call (passed
3594///   to the entity loader exactly as upstream `xmlLoadResource` does).
3595pub(crate) unsafe fn open_filename_routed(
3596    uri: *const c_char,
3597    ctxt: *mut _xmlParserCtxt,
3598) -> RoutedFileOpen {
3599    // A custom external entity loader governs file/URL opens too.
3600    if external_entity_loader_active() {
3601        // SAFETY: uri is a valid C string; ctxt is NULL or valid.
3602        let input = xmlLoadExternalEntity(uri, ptr::null(), ctxt);
3603        if input.is_null() {
3604            return RoutedFileOpen::EntityLoaderFailed;
3605        }
3606        // Materialize the loader's bytes into an owned InputBuffer (the
3607        // loader result is freed here; the parse consumes the copy).
3608        let loaded = input_bytes_owned(input);
3609        let named = if uri.is_null() {
3610            None
3611        } else {
3612            // SAFETY: uri is a valid NUL-terminated C string.
3613            Some(
3614                unsafe { CStr::from_ptr(uri) }
3615                    .to_string_lossy()
3616                    .into_owned(),
3617            )
3618        };
3619        return RoutedFileOpen::Loaded(InputBuffer::from_memory(
3620            loaded.as_deref().unwrap_or(&[]),
3621            named.as_deref(),
3622        ));
3623    }
3624    // No registered loader: the caller keeps the built-in open. The slot is
3625    // read through the R-000177 cross-DSO bridge (facade copies must see a
3626    // loader registered via the core DSO's exported setter).
3627    if globals::get_parser_input_buffer_create_filename_value_cross_dso().is_none() {
3628        return RoutedFileOpen::Builtin;
3629    }
3630    // SAFETY: uri is a valid NUL-terminated C string for the call.
3631    let loaded = unsafe { call_loader_materialize(uri) };
3632    match loaded {
3633        Err(()) => RoutedFileOpen::Failed,
3634        Ok(bytes) => {
3635            let named = if uri.is_null() {
3636                None
3637            } else {
3638                // SAFETY: uri is a valid NUL-terminated C string.
3639                Some(
3640                    unsafe { CStr::from_ptr(uri) }
3641                        .to_string_lossy()
3642                        .into_owned(),
3643                )
3644            };
3645            RoutedFileOpen::Loaded(InputBuffer::from_memory(&bytes, named.as_deref()))
3646        }
3647    }
3648}
3649
3650/// True when a custom external entity loader is registered process-wide
3651/// (the core DSO's `xmlSetExternalEntityLoader` registration, or this DSO's
3652/// own when the accessor does not resolve in a single-DSO link). The
3653/// process-visible registration is authoritative (R-000177).
3654fn external_entity_loader_active() -> bool {
3655    match foreign_external_entity_loader() {
3656        Some(_) => true,
3657        None => EXTERNAL_ENTITY_LOADER.lock().is_some(),
3658    }
3659}
3660
3661/// Route a filename open through the `xmlParserInputBufferCreateFilenameDefault`
3662/// (php streams) loader ONLY — no external-entity-loader consult.
3663///
3664/// The xmlTextReader family reads through `xmlNewInputFromFile` upstream,
3665/// which does NOT go through the external entity loader (verified against
3666/// the executed 2.15.3 oracle), so the reader must not pick up an
3667/// `xmlSetExternalEntityLoader` registration.
3668///
3669/// # Safety
3670///
3671/// - `uri` must be a valid NUL-terminated C string live for the call.
3672pub(crate) unsafe fn open_filename_routed_input_only(uri: *const c_char) -> RoutedFileOpen {
3673    // No registered loader: the caller keeps the built-in open. The slot is
3674    // read through the R-000177 cross-DSO bridge (facade copies must see a
3675    // loader registered via the core DSO's exported setter).
3676    if globals::get_parser_input_buffer_create_filename_value_cross_dso().is_none() {
3677        return RoutedFileOpen::Builtin;
3678    }
3679    // SAFETY: uri is a valid NUL-terminated C string for the call.
3680    let loaded = unsafe { call_loader_materialize(uri) };
3681    match loaded {
3682        Err(()) => RoutedFileOpen::Failed,
3683        Ok(bytes) => {
3684            let named = if uri.is_null() {
3685                None
3686            } else {
3687                // SAFETY: uri is a valid NUL-terminated C string.
3688                Some(
3689                    unsafe { CStr::from_ptr(uri) }
3690                        .to_string_lossy()
3691                        .into_owned(),
3692                )
3693            };
3694            RoutedFileOpen::Loaded(InputBuffer::from_memory(&bytes, named.as_deref()))
3695        }
3696    }
3697}
3698
3699/// Copy the bytes of a loader-produced `_xmlParserInput` into an owned
3700/// `Vec` and release the input (upstream `xmlCtxtParseDocument` consumes the
3701/// input; the candidate's parse paths own an [`InputBuffer`]). The input's
3702/// underlying buffer is freed with the input (xmlFreeInputStream).
3703///
3704/// # Safety
3705///
3706/// - `input` must be a valid `_xmlParserInput` produced by a registered
3707///   loader / `xmlLoadExternalEntity`, not yet freed.
3708unsafe fn input_bytes_owned(input: *mut _xmlParserInput) -> Option<Vec<u8>> {
3709    unsafe {
3710        let base = (*input).base;
3711        let end = (*input).end;
3712        let len = if base.is_null() {
3713            0
3714        } else {
3715            end.offset_from(base).max(0) as usize
3716        };
3717        let bytes = if base.is_null() || len == 0 {
3718            None
3719        } else {
3720            Some(core::slice::from_raw_parts(base, len).to_vec())
3721        };
3722        crate::abi::exports_xml2::xmlFreeInputStream(input);
3723        bytes
3724    }
3725}
3726
3727/// Compose the upstream `xmlCtxtErrIO(XML_IO_ENOENT, uri)` message text:
3728/// `failed to load "<uri>": <errno text>\n`. When errno is stale (the
3729/// registered php streams loader returned NULL without touching errno, e.g.
3730/// the percent-NUL guard) the `XML_IO_ENOENT` table text is used.
3731///
3732/// # Safety
3733///
3734/// - `uri` must be NULL or a valid NUL-terminated C string live for the call.
3735pub(crate) fn io_load_failure_message(uri: *const c_char) -> String {
3736    // SAFETY: reads errno only.
3737    let errno = unsafe { *libc::__errno_location() };
3738    let errstr = if errno == 0 {
3739        // xmlErrString(XML_IO_ENOENT) table text (error.c 2.15).
3740        "No such file or directory".to_string()
3741    } else {
3742        // SAFETY: strerror(errno) returns a static message for the value.
3743        unsafe { std::ffi::CStr::from_ptr(libc::strerror(errno)) }
3744            .to_string_lossy()
3745            .into_owned()
3746    };
3747    let url_str = if uri.is_null() {
3748        String::new()
3749    } else {
3750        // SAFETY: uri is a valid NUL-terminated C string.
3751        unsafe { std::ffi::CStr::from_ptr(uri) }
3752            .to_string_lossy()
3753            .into_owned()
3754    };
3755    format!("failed to load \"{url_str}\": {errstr}\n")
3756}
3757
3758/// Set the application-wide external entity loader.
3759///
3760/// # UPSTREAM-PARITY
3761///
3762/// ```c
3763/// void xmlSetExternalEntityLoader(xmlExternalEntityLoader f);
3764/// ```
3765///
3766/// # SAFETY
3767///
3768///
3769/// - `f` must be a valid callback (or None);
3770///   the callback is invoked with the documented context pointer and
3771///   must itself uphold the same pointer invariants.
3772///
3773/// The caller must not race this call with concurrent mutation of the
3774/// same objects from other threads (per-object state is not internally
3775/// synchronized). Violating any of the above is undefined behavior.
3776///
3777/// Exercised by the C-API differential courts
3778/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3779/// courts; those pass byte-for-byte against the upstream oracle.
3780#[no_mangle]
3781pub unsafe extern "C" fn xmlSetExternalEntityLoader(f: Option<xmlExternalEntityLoader>) {
3782    *EXTERNAL_ENTITY_LOADER.lock() = f;
3783}
3784
3785/// Get the current external entity loader.
3786///
3787/// # UPSTREAM-PARITY
3788///
3789/// ```c
3790/// xmlExternalEntityLoader xmlGetExternalEntityLoader(void);
3791/// ```
3792///
3793/// # SAFETY
3794///
3795/// The function touches crate-global state only; it is safe
3796/// as long as the caller respects the library's global
3797/// initialization/cleanup ordering (xmlInitParser before use,
3798/// xmlCleanupParser only after all users are done).
3799///
3800/// Violating the global lifecycle ordering, or calling this after
3801/// teardown or from a signal handler, is undefined behavior.
3802#[no_mangle]
3803pub unsafe extern "C" fn xmlGetExternalEntityLoader() -> Option<xmlExternalEntityLoader> {
3804    *EXTERNAL_ENTITY_LOADER.lock()
3805}
3806
3807/// External entity loader that disables network access.
3808///
3809/// # UPSTREAM-PARITY
3810///
3811/// ```c
3812/// xmlParserInputPtr xmlNoNetExternalEntityLoader(const char *URL,
3813///                                                const char *ID,
3814///                                                xmlParserCtxtPtr ctxt);
3815/// ```
3816///
3817/// # SAFETY
3818///
3819/// - `ctxt` must be valid pointers (or NULL
3820///   where the upstream C contract allows), obtained from the
3821///   matching constructor/owner and not yet freed; the callee may
3822///   take or keep ownership exactly as the C API specifies.
3823///
3824/// - `URL`, `ID` must point to valid NUL-terminated
3825///   strings (or NULL where the C contract allows) for the lifetime
3826///   of the call.
3827///
3828/// The caller must not race this call with concurrent mutation of the
3829/// same objects from other threads (per-object state is not internally
3830/// synchronized). Violating any of the above is undefined behavior.
3831///
3832/// Exercised by the C-API differential courts
3833/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3834/// courts; those pass byte-for-byte against the upstream oracle.
3835#[no_mangle]
3836pub unsafe extern "C" fn xmlNoNetExternalEntityLoader(
3837    URL: *const c_char,
3838    ID: *const c_char,
3839    ctxt: *mut _xmlParserCtxt,
3840) -> *mut _xmlParserInput {
3841    unsafe {
3842        let old_options = if ctxt.is_null() { 0 } else { (*ctxt).options };
3843        if !ctxt.is_null() {
3844            (*ctxt).options |= XML_PARSE_NONET;
3845        }
3846        let input = default_external_entity_loader(URL, ID, ctxt);
3847        if !ctxt.is_null() {
3848            (*ctxt).options = old_options;
3849        }
3850        input
3851    }
3852}
3853
3854/// Load an external entity using the registered loader.
3855///
3856/// # UPSTREAM-PARITY
3857///
3858/// ```c
3859/// xmlParserInputPtr xmlLoadExternalEntity(const char *URL, const char *ID,
3860///                                         xmlParserCtxtPtr ctxt);
3861/// ```
3862///
3863/// # SAFETY
3864///
3865/// - `ctxt` must be valid pointers (or NULL
3866///   where the upstream C contract allows), obtained from the
3867///   matching constructor/owner and not yet freed; the callee may
3868///   take or keep ownership exactly as the C API specifies.
3869///
3870/// - `URL`, `ID` must point to valid NUL-terminated
3871///   strings (or NULL where the C contract allows) for the lifetime
3872///   of the call.
3873///
3874/// The caller must not race this call with concurrent mutation of the
3875/// same objects from other threads (per-object state is not internally
3876/// synchronized). Violating any of the above is undefined behavior.
3877///
3878/// Exercised by the C-API differential courts
3879/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3880/// courts; those pass byte-for-byte against the upstream oracle.
3881#[no_mangle]
3882pub unsafe extern "C" fn xmlLoadExternalEntity(
3883    URL: *const c_char,
3884    ID: *const c_char,
3885    ctxt: *mut _xmlParserCtxt,
3886) -> *mut _xmlParserInput {
3887    // R-000177: the loader xmlSetExternalEntityLoader registers binds to the
3888    // CORE DSO, so a load performed by a whole-archive facade's private copy
3889    // must consult the process-visible registration first (upstream: one
3890    // core instance, one loader). Single-DSO links resolve their own export.
3891    let loader = match foreign_external_entity_loader() {
3892        Some(f) => Some(f),
3893        None => *EXTERNAL_ENTITY_LOADER.lock(),
3894    };
3895    match loader {
3896        Some(f) => unsafe { f(URL, ID, ctxt) },
3897        None => unsafe { default_external_entity_loader(URL, ID, ctxt) },
3898    }
3899}
3900
3901/// Resolve the process-visible `xmlGetExternalEntityLoader` (the CORE DSO's
3902/// registration) via the dynamic symbol scope.
3903#[cfg(target_os = "linux")]
3904fn foreign_external_entity_loader() -> Option<xmlExternalEntityLoader> {
3905    use std::sync::OnceLock;
3906    type Getter = unsafe extern "C" fn() -> Option<xmlExternalEntityLoader>;
3907    static GETTER: OnceLock<Option<Getter>> = OnceLock::new();
3908    let getter = *GETTER.get_or_init(|| {
3909        // SAFETY: dlsym(RTLD_DEFAULT) returns the exported accessor address
3910        // or NULL; the transmute (pointer-sized) is sound.
3911        unsafe {
3912            let sym = libc::dlsym(libc::RTLD_DEFAULT, c"xmlGetExternalEntityLoader".as_ptr());
3913            if sym.is_null() {
3914                None
3915            } else {
3916                Some(std::mem::transmute::<*mut c_void, Getter>(sym))
3917            }
3918        }
3919    });
3920    getter.and_then(|g| unsafe { g() })
3921}
3922
3923#[cfg(not(target_os = "linux"))]
3924fn foreign_external_entity_loader() -> Option<xmlExternalEntityLoader> {
3925    None
3926}
3927
3928/// Check an input for HTTP access; with XML_PARSE_NONET set, HTTP inputs are
3929/// refused and freed.
3930///
3931/// # UPSTREAM-PARITY
3932///
3933/// ```c
3934/// xmlParserInputPtr xmlCheckHTTPInput(xmlParserCtxtPtr ctxt,
3935///                                     xmlParserInputPtr ret);
3936/// ```
3937///
3938/// # SAFETY
3939///
3940/// - `ctxt`, `ret` must be valid pointers (or NULL
3941///   where the upstream C contract allows), obtained from the
3942///   matching constructor/owner and not yet freed; the callee may
3943///   take or keep ownership exactly as the C API specifies.
3944///
3945/// The caller must not race this call with concurrent mutation of the
3946/// same objects from other threads (per-object state is not internally
3947/// synchronized). Violating any of the above is undefined behavior.
3948///
3949/// Exercised by the C-API differential courts
3950/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3951/// courts; those pass byte-for-byte against the upstream oracle.
3952#[no_mangle]
3953pub unsafe extern "C" fn xmlCheckHTTPInput(
3954    ctxt: *mut _xmlParserCtxt,
3955    ret: *mut _xmlParserInput,
3956) -> *mut _xmlParserInput {
3957    if ret.is_null() {
3958        return ptr::null_mut();
3959    }
3960    unsafe {
3961        if !ctxt.is_null() && (*ctxt).options & XML_PARSE_NONET != 0 {
3962            let filename = (*ret).filename;
3963            if !filename.is_null() {
3964                let len = libc::strlen(filename);
3965                if len >= 7
3966                    && libc::strncasecmp(filename, c"http://".as_ptr() as *const c_char, 7) == 0
3967                {
3968                    // free_parser_input now frees the owned buffer (upstream
3969                    // xmlFreeInputStream semantics); no separate buf free.
3970                    helpers::free_parser_input(ret);
3971                    return ptr::null_mut();
3972                }
3973            }
3974        }
3975        ret
3976    }
3977}
3978
3979// ═══════════════════════════════════════════════════════════════════════════════
3980// xmlFile* I/O callbacks (xmlIO.c)
3981// ═══════════════════════════════════════════════════════════════════════════════
3982
3983/// Match callback: the file I/O handlers accept every filename.
3984///
3985/// # UPSTREAM-PARITY
3986///
3987/// ```c
3988/// int xmlFileMatch(const char *filename);
3989/// ```
3990///
3991/// # SAFETY
3992///
3993///
3994/// - `_filename` must point to valid NUL-terminated
3995///   strings (or NULL where the C contract allows) for the lifetime
3996///   of the call.
3997///
3998/// The caller must not race this call with concurrent mutation of the
3999/// same objects from other threads (per-object state is not internally
4000/// synchronized). Violating any of the above is undefined behavior.
4001///
4002/// Exercised by the C-API differential courts
4003/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4004/// courts; those pass byte-for-byte against the upstream oracle.
4005#[no_mangle]
4006pub const unsafe extern "C" fn xmlFileMatch(_filename: *const c_char) -> c_int {
4007    1
4008}
4009
4010/// Open a file and return a `FILE *` I/O context (cast to `void *`).
4011///
4012/// # UPSTREAM-PARITY
4013///
4014/// ```c
4015/// void *xmlFileOpen(const char *filename);
4016/// ```
4017///
4018/// # SAFETY
4019///
4020///
4021/// - `filename` must point to valid NUL-terminated
4022///   strings (or NULL where the C contract allows) for the lifetime
4023///   of the call.
4024///
4025/// The caller must not race this call with concurrent mutation of the
4026/// same objects from other threads (per-object state is not internally
4027/// synchronized). Violating any of the above is undefined behavior.
4028///
4029/// Exercised by the C-API differential courts
4030/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4031/// courts; those pass byte-for-byte against the upstream oracle.
4032#[no_mangle]
4033pub unsafe extern "C" fn xmlFileOpen(filename: *const c_char) -> *mut c_void {
4034    if filename.is_null() {
4035        return ptr::null_mut();
4036    }
4037    unsafe { libc::fopen(filename, c"rb".as_ptr() as *const c_char) as *mut c_void }
4038}
4039
4040/// Read up to `len` bytes from a `FILE *` I/O context.
4041///
4042/// # UPSTREAM-PARITY
4043///
4044/// ```c
4045/// int xmlFileRead(void *context, char *buffer, int len);
4046/// ```
4047///
4048/// # SAFETY
4049///
4050/// - `context`, `buffer` must be valid pointers (or NULL
4051///   where the upstream C contract allows), obtained from the
4052///   matching constructor/owner and not yet freed; the callee may
4053///   take or keep ownership exactly as the C API specifies.
4054///
4055/// The caller must not race this call with concurrent mutation of the
4056/// same objects from other threads (per-object state is not internally
4057/// synchronized). Violating any of the above is undefined behavior.
4058///
4059/// Exercised by the C-API differential courts
4060/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4061/// courts; those pass byte-for-byte against the upstream oracle.
4062#[no_mangle]
4063pub unsafe extern "C" fn xmlFileRead(
4064    context: *mut c_void,
4065    buffer: *mut c_char,
4066    len: c_int,
4067) -> c_int {
4068    if context.is_null() || buffer.is_null() || len <= 0 {
4069        return -1;
4070    }
4071    unsafe {
4072        let n = libc::fread(
4073            buffer as *mut c_void,
4074            1,
4075            len as usize,
4076            context as *mut libc::FILE,
4077        );
4078        if n < len as usize && libc::ferror(context as *mut libc::FILE) != 0 {
4079            return -1;
4080        }
4081        n as c_int
4082    }
4083}
4084
4085/// Close a `FILE *` I/O context.
4086///
4087/// # UPSTREAM-PARITY
4088///
4089/// ```c
4090/// int xmlFileClose(void *context);
4091/// ```
4092///
4093/// # SAFETY
4094///
4095/// - `context` must be valid pointers (or NULL
4096///   where the upstream C contract allows), obtained from the
4097///   matching constructor/owner and not yet freed; the callee may
4098///   take or keep ownership exactly as the C API specifies.
4099///
4100/// The caller must not race this call with concurrent mutation of the
4101/// same objects from other threads (per-object state is not internally
4102/// synchronized). Violating any of the above is undefined behavior.
4103///
4104/// Exercised by the C-API differential courts
4105/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4106/// courts; those pass byte-for-byte against the upstream oracle.
4107#[no_mangle]
4108pub unsafe extern "C" fn xmlFileClose(context: *mut c_void) -> c_int {
4109    if context.is_null() {
4110        return -1;
4111    }
4112    unsafe {
4113        let file = context as *mut libc::FILE;
4114        let fd = libc::fileno(file);
4115        if fd == 0 {
4116            // stdin must not be closed.
4117            return 0;
4118        }
4119        if fd == 1 || fd == 2 {
4120            // stdout/stderr are only flushed.
4121            return if libc::fflush(file) == 0 { 0 } else { -1 };
4122        }
4123        libc::fclose(file)
4124    }
4125}
4126
4127// ═══════════════════════════════════════════════════════════════════════════════
4128// Low-level character scanning (parserInternals.c)
4129// ═══════════════════════════════════════════════════════════════════════════════
4130
4131/// Return the current character (UTF-8 decoded, EOL normalised) and its byte
4132/// length in `*len`. Does not advance the input pointer.
4133///
4134/// # UPSTREAM-PARITY
4135///
4136/// ```c
4137/// int xmlCurrentChar(xmlParserCtxtPtr ctxt, int *len);
4138/// ```
4139///
4140/// # SAFETY
4141///
4142/// - `ctxt`, `len` must be valid pointers (or NULL
4143///   where the upstream C contract allows), obtained from the
4144///   matching constructor/owner and not yet freed; the callee may
4145///   take or keep ownership exactly as the C API specifies.
4146///
4147/// The caller must not race this call with concurrent mutation of the
4148/// same objects from other threads (per-object state is not internally
4149/// synchronized). Violating any of the above is undefined behavior.
4150///
4151/// Exercised by the C-API differential courts
4152/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4153/// courts; those pass byte-for-byte against the upstream oracle.
4154#[no_mangle]
4155pub unsafe extern "C" fn xmlCurrentChar(ctxt: *mut _xmlParserCtxt, len: *mut c_int) -> c_int {
4156    if ctxt.is_null() || len.is_null() || (*ctxt).input.is_null() {
4157        return 0;
4158    }
4159    unsafe {
4160        let pi = &*((*ctxt).input);
4161        let cur = pi.cur;
4162        if cur.is_null() {
4163            *len = 0;
4164            return 0;
4165        }
4166        let avail = (pi.end as usize).saturating_sub(cur as usize);
4167        let c = *cur;
4168
4169        if c < 0x80 {
4170            if c == b'\r' {
4171                // EOL normalisation: CR (optionally CRLF) becomes LF.
4172                if avail >= 2 && *cur.add(1) == b'\n' {
4173                    (*(*ctxt).input).cur = cur.add(1);
4174                }
4175                *len = 1;
4176                return b'\n' as c_int;
4177            }
4178            if c == 0 {
4179                if avail == 0 {
4180                    *len = 0;
4181                } else {
4182                    *len = 1;
4183                }
4184                return 0;
4185            }
4186            *len = 1;
4187            return c as c_int;
4188        }
4189
4190        // Multi-byte UTF-8.
4191        if avail < 2 || (*cur.add(1) & 0xc0) != 0x80 {
4192            *len = 1;
4193            return XML_INVALID_CHAR;
4194        }
4195        if c < 0xe0 {
4196            if c < 0xc2 {
4197                *len = 1;
4198                return XML_INVALID_CHAR;
4199            }
4200            let val = (((c & 0x1f) as c_int) << 6) | ((*cur.add(1) & 0x3f) as c_int);
4201            *len = 2;
4202            return val;
4203        }
4204        if avail < 3 || (*cur.add(2) & 0xc0) != 0x80 {
4205            *len = 1;
4206            return XML_INVALID_CHAR;
4207        }
4208        if c < 0xf0 {
4209            let val = (((c & 0x0f) as c_int) << 12)
4210                | (((*cur.add(1) & 0x3f) as c_int) << 6)
4211                | ((*cur.add(2) & 0x3f) as c_int);
4212            if val < 0x800 || (0xd800..0xe000).contains(&val) {
4213                *len = 1;
4214                return XML_INVALID_CHAR;
4215            }
4216            *len = 3;
4217            return val;
4218        }
4219        if avail < 4 || (*cur.add(3) & 0xc0) != 0x80 {
4220            *len = 1;
4221            return XML_INVALID_CHAR;
4222        }
4223        let val = (((c & 0x07) as c_int) << 18)
4224            | (((*cur.add(1) & 0x3f) as c_int) << 12)
4225            | (((*cur.add(2) & 0x3f) as c_int) << 6)
4226            | ((*cur.add(3) & 0x3f) as c_int);
4227        if !(0x10000..0x110000).contains(&val) {
4228            *len = 1;
4229            return XML_INVALID_CHAR;
4230        }
4231        *len = 4;
4232        val
4233    }
4234}
4235
4236/// Advance to the next character, updating line/column accounting.
4237///
4238/// # UPSTREAM-PARITY
4239///
4240/// ```c
4241/// void xmlNextChar(xmlParserCtxtPtr ctxt);
4242/// ```
4243///
4244/// # SAFETY
4245///
4246/// - `ctxt` must be valid pointers (or NULL
4247///   where the upstream C contract allows), obtained from the
4248///   matching constructor/owner and not yet freed; the callee may
4249///   take or keep ownership exactly as the C API specifies.
4250///
4251/// The caller must not race this call with concurrent mutation of the
4252/// same objects from other threads (per-object state is not internally
4253/// synchronized). Violating any of the above is undefined behavior.
4254///
4255/// Exercised by the C-API differential courts
4256/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4257/// courts; those pass byte-for-byte against the upstream oracle.
4258#[no_mangle]
4259pub unsafe extern "C" fn xmlNextChar(ctxt: *mut _xmlParserCtxt) {
4260    if ctxt.is_null() || (*ctxt).input.is_null() {
4261        return;
4262    }
4263    unsafe {
4264        let pi = &mut *((*ctxt).input);
4265        let cur = pi.cur;
4266        if cur.is_null() {
4267            return;
4268        }
4269        let avail = (pi.end as usize).saturating_sub(cur as usize);
4270        if avail == 0 {
4271            return;
4272        }
4273        let c = *cur;
4274
4275        if c < 0x80 {
4276            if c == b'\n' {
4277                pi.cur = cur.add(1);
4278                pi.line += 1;
4279                pi.col = 1;
4280            } else if c == b'\r' {
4281                // CRLF is a single line break.
4282                pi.cur = cur.add(if avail >= 2 && *cur.add(1) == b'\n' {
4283                    2
4284                } else {
4285                    1
4286                });
4287                pi.line += 1;
4288                pi.col = 1;
4289            } else {
4290                pi.cur = cur.add(1);
4291                pi.col += 1;
4292            }
4293            return;
4294        }
4295
4296        pi.col += 1;
4297
4298        if avail < 2 || (*cur.add(1) & 0xc0) != 0x80 {
4299            pi.cur = cur.add(1);
4300            return;
4301        }
4302        if c < 0xe0 {
4303            if c < 0xc2 {
4304                pi.cur = cur.add(1);
4305                return;
4306            }
4307            pi.cur = cur.add(2);
4308            return;
4309        }
4310        if avail < 3 || (*cur.add(2) & 0xc0) != 0x80 {
4311            pi.cur = cur.add(1);
4312            return;
4313        }
4314        if c < 0xf0 {
4315            let val = (((c as c_int) << 8) as u32) | (*cur.add(1) as u32);
4316            if (val < 0xe0a0) || (0xeda0..0xee00).contains(&val) {
4317                pi.cur = cur.add(1);
4318                return;
4319            }
4320            pi.cur = cur.add(3);
4321            return;
4322        }
4323        if avail < 4 || (*cur.add(3) & 0xc0) != 0x80 {
4324            pi.cur = cur.add(1);
4325            return;
4326        }
4327        let val = (((c as c_int) << 8) as u32) | (*cur.add(1) as u32);
4328        if !(0xf090..0xf490).contains(&val) {
4329            pi.cur = cur.add(1);
4330            return;
4331        }
4332        pi.cur = cur.add(4);
4333    }
4334}
4335
4336/// Skip blank characters (space, tab, LF, CR), updating line/column.
4337/// Returns the number of blanks skipped.
4338///
4339/// # UPSTREAM-PARITY
4340///
4341/// ```c
4342/// int xmlSkipBlankChars(xmlParserCtxtPtr ctxt);
4343/// ```
4344///
4345/// # SAFETY
4346///
4347/// - `ctxt` must be valid pointers (or NULL
4348///   where the upstream C contract allows), obtained from the
4349///   matching constructor/owner and not yet freed; the callee may
4350///   take or keep ownership exactly as the C API specifies.
4351///
4352/// The caller must not race this call with concurrent mutation of the
4353/// same objects from other threads (per-object state is not internally
4354/// synchronized). Violating any of the above is undefined behavior.
4355///
4356/// Exercised by the C-API differential courts
4357/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4358/// courts; those pass byte-for-byte against the upstream oracle.
4359#[no_mangle]
4360pub unsafe extern "C" fn xmlSkipBlankChars(ctxt: *mut _xmlParserCtxt) -> c_int {
4361    if ctxt.is_null() || (*ctxt).input.is_null() {
4362        return 0;
4363    }
4364    unsafe {
4365        let pi = &mut *((*ctxt).input);
4366        let mut cur = pi.cur;
4367        if cur.is_null() {
4368            return 0;
4369        }
4370        let end = pi.end;
4371        let mut res = 0;
4372        while cur < end && (*cur == 0x20 || *cur == 0x09 || *cur == 0x0a || *cur == 0x0d) {
4373            if *cur == b'\n' {
4374                pi.line += 1;
4375                pi.col = 1;
4376            } else {
4377                pi.col += 1;
4378            }
4379            cur = cur.add(1);
4380            res += 1;
4381        }
4382        pi.cur = cur;
4383        res
4384    }
4385}
4386
4387/// XML 1.0 5th-edition NameStartChar predicate (upstream `xmlIsNameStartCharNew`).
4388const fn is_name_start_char_new(c: c_int) -> bool {
4389    if c == b' ' as c_int || c == b'>' as c_int || c == b'/' as c_int {
4390        return false;
4391    }
4392    (c >= b'a' as c_int && c <= b'z' as c_int)
4393        || (c >= b'A' as c_int && c <= b'Z' as c_int)
4394        || c == b'_' as c_int
4395        || c == b':' as c_int
4396        || (c >= 0xC0 && c <= 0xD6)
4397        || (c >= 0xD8 && c <= 0xF6)
4398        || (c >= 0xF8 && c <= 0x2FF)
4399        || (c >= 0x370 && c <= 0x37D)
4400        || (c >= 0x37F && c <= 0x1FFF)
4401        || (c >= 0x200C && c <= 0x200D)
4402        || (c >= 0x2070 && c <= 0x218F)
4403        || (c >= 0x2C00 && c <= 0x2FEF)
4404        || (c >= 0x3001 && c <= 0xD7FF)
4405        || (c >= 0xF900 && c <= 0xFDCF)
4406        || (c >= 0xFDF0 && c <= 0xFFFD)
4407        || (c >= 0x10000 && c <= 0xEFFFF)
4408}
4409
4410/// XML 1.0 5th-edition NameChar predicate (upstream `xmlIsNameCharNew`).
4411const fn is_name_char_new(c: c_int) -> bool {
4412    if c == b' ' as c_int || c == b'>' as c_int || c == b'/' as c_int {
4413        return false;
4414    }
4415    (c >= b'a' as c_int && c <= b'z' as c_int)
4416        || (c >= b'A' as c_int && c <= b'Z' as c_int)
4417        || (c >= b'0' as c_int && c <= b'9' as c_int)
4418        || c == b'_' as c_int
4419        || c == b':' as c_int
4420        || c == b'-' as c_int
4421        || c == b'.' as c_int
4422        || c == 0xB7
4423        || (c >= 0xC0 && c <= 0xD6)
4424        || (c >= 0xD8 && c <= 0xF6)
4425        || (c >= 0xF8 && c <= 0x2FF)
4426        || (c >= 0x300 && c <= 0x36F)
4427        || (c >= 0x370 && c <= 0x37D)
4428        || (c >= 0x37F && c <= 0x1FFF)
4429        || (c >= 0x200C && c <= 0x200D)
4430        || (c >= 0x203F && c <= 0x2040)
4431        || (c >= 0x2070 && c <= 0x218F)
4432        || (c >= 0x2C00 && c <= 0x2FEF)
4433        || (c >= 0x3001 && c <= 0xD7FF)
4434        || (c >= 0xF900 && c <= 0xFDCF)
4435        || (c >= 0xFDF0 && c <= 0xFFFD)
4436        || (c >= 0x10000 && c <= 0xEFFFF)
4437}
4438
4439/// Scan an XML Name (or NCName/Nmtoken) at `ctxt->input->cur`, advancing the
4440/// input pointer. Returns a pointer to the end of the name, or NULL when the
4441/// name exceeds `max` bytes.
4442///
4443/// # UPSTREAM-PARITY
4444///
4445/// ```c
4446/// const xmlChar *xmlScanName(xmlParserCtxtPtr ctxt, int max, int flags);
4447/// ```
4448///
4449/// # SAFETY
4450///
4451/// - `ctxt` must be valid pointers (or NULL
4452///   where the upstream C contract allows), obtained from the
4453///   matching constructor/owner and not yet freed; the callee may
4454///   take or keep ownership exactly as the C API specifies.
4455///
4456/// The caller must not race this call with concurrent mutation of the
4457/// same objects from other threads (per-object state is not internally
4458/// synchronized). Violating any of the above is undefined behavior.
4459///
4460/// Exercised by the C-API differential courts
4461/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4462/// courts; those pass byte-for-byte against the upstream oracle.
4463#[no_mangle]
4464pub unsafe extern "C" fn xmlScanName(
4465    ctxt: *mut _xmlParserCtxt,
4466    max: c_int,
4467    flags: c_int,
4468) -> *const xmlChar {
4469    if ctxt.is_null() || (*ctxt).input.is_null() || max <= 0 {
4470        return ptr::null();
4471    }
4472    unsafe {
4473        let pi = &mut *((*ctxt).input);
4474        let mut ptr = pi.cur;
4475        if ptr.is_null() {
4476            return ptr::null();
4477        }
4478        let end = pi.end;
4479        let mut remaining = max as usize;
4480        let stop: u8 = if flags & XML_SCAN_NC != 0 { b':' } else { 0 };
4481        let old10 = flags & XML_SCAN_OLD10 != 0;
4482        let mut f = flags;
4483
4484        loop {
4485            if ptr >= end {
4486                break;
4487            }
4488            let c = *ptr;
4489            let (cp, len) = if c < 0x80 {
4490                if stop != 0 && c == stop {
4491                    break;
4492                }
4493                (c as c_int, 1usize)
4494            } else {
4495                // Decode a multi-byte UTF-8 character.
4496                let avail = (end as usize).saturating_sub(ptr as usize);
4497                let mut l = 4usize;
4498                let cp = decode_utf8_char(ptr, avail, &mut l);
4499                if cp < 0 {
4500                    break;
4501                }
4502                (cp, l)
4503            };
4504
4505            let ok = if f & XML_SCAN_NMTOKEN != 0 {
4506                if old10 {
4507                    is_name_char_old10(cp)
4508                } else {
4509                    is_name_char_new(cp)
4510                }
4511            } else if old10 {
4512                is_name_start_char_old10(cp)
4513            } else {
4514                is_name_start_char_new(cp)
4515            };
4516            if !ok {
4517                break;
4518            }
4519            if len > remaining {
4520                return ptr::null();
4521            }
4522            ptr = ptr.add(len);
4523            remaining -= len;
4524            f |= XML_SCAN_NMTOKEN;
4525        }
4526
4527        pi.cur = ptr;
4528        ptr
4529    }
4530}
4531
4532/// Decode a UTF-8 character at `ptr` with `avail` bytes available; returns the
4533/// codepoint (or -1 on invalid/truncated input) and sets `*len` to its length.
4534const unsafe fn decode_utf8_char(ptr: *const u8, avail: usize, len: &mut usize) -> c_int {
4535    unsafe {
4536        let c = *ptr;
4537        if avail < 2 || (*ptr.add(1) & 0xc0) != 0x80 {
4538            return -1;
4539        }
4540        if c < 0xe0 {
4541            if c < 0xc2 {
4542                return -1;
4543            }
4544            *len = 2;
4545            return (((c & 0x1f) as c_int) << 6) | ((*ptr.add(1) & 0x3f) as c_int);
4546        }
4547        if avail < 3 || (*ptr.add(2) & 0xc0) != 0x80 {
4548            return -1;
4549        }
4550        if c < 0xf0 {
4551            let val = (((c & 0x0f) as c_int) << 12)
4552                | (((*ptr.add(1) & 0x3f) as c_int) << 6)
4553                | ((*ptr.add(2) & 0x3f) as c_int);
4554            if val < 0x800 || (val >= 0xd800 && val < 0xe000) {
4555                return -1;
4556            }
4557            *len = 3;
4558            return val;
4559        }
4560        if avail < 4 || (*ptr.add(3) & 0xc0) != 0x80 {
4561            return -1;
4562        }
4563        let val = (((c & 0x07) as c_int) << 18)
4564            | (((*ptr.add(1) & 0x3f) as c_int) << 12)
4565            | (((*ptr.add(2) & 0x3f) as c_int) << 6)
4566            | ((*ptr.add(3) & 0x3f) as c_int);
4567        if val < 0x10000 || val >= 0x110000 {
4568            return -1;
4569        }
4570        *len = 4;
4571        val
4572    }
4573}
4574
4575/// XML 1.0 (pre-revision-5) NameStartChar predicate: Letter, '_' or ':'.
4576const fn is_name_start_char_old10(c: c_int) -> bool {
4577    (c >= b'a' as c_int && c <= b'z' as c_int)
4578        || (c >= b'A' as c_int && c <= b'Z' as c_int)
4579        || c == b'_' as c_int
4580        || c == b':' as c_int
4581        || (c >= 0xC0 && c <= 0xD6)
4582        || (c >= 0xD8 && c <= 0xF6)
4583        || (c >= 0xF8 && c <= 0x2FF)
4584        || (c >= 0x370 && c <= 0x37D)
4585        || (c >= 0x37F && c <= 0x1FFF)
4586        || (c >= 0x200C && c <= 0x200D)
4587        || (c >= 0x2070 && c <= 0x218F)
4588        || (c >= 0x2C00 && c <= 0x2FEF)
4589        || (c >= 0x3001 && c <= 0xD7FF)
4590        || (c >= 0xF900 && c <= 0xFDCF)
4591        || (c >= 0xFDF0 && c <= 0xFFFD)
4592        || (c >= 0x10000 && c <= 0xEFFFF)
4593}
4594
4595/// XML 1.0 (pre-revision-5) NameChar predicate: NameStartChar, digits, '.',
4596/// '-', combining chars and extenders.
4597const fn is_name_char_old10(c: c_int) -> bool {
4598    is_name_start_char_old10(c)
4599        || (c >= b'0' as c_int && c <= b'9' as c_int)
4600        || c == b'.' as c_int
4601        || c == b'-' as c_int
4602        || c == 0xB7
4603        || (c >= 0x300 && c <= 0x36F)
4604        || c == 0x02D0
4605        || c == 0x02D1
4606        || c == 0x0387
4607        || c == 0x0640
4608        || c == 0x0E46
4609        || c == 0x0EC6
4610        || c == 0x3005
4611        || (c >= 0x3031 && c <= 0x3035)
4612        || (c >= 0x309D && c <= 0x309E)
4613        || (c >= 0x30FC && c <= 0x30FE)
4614}
4615
4616/// Decode entities from the current input position: char references and
4617/// (predefined and DTD-declared) entity references are substituted. Stops at
4618/// the first of `end`/`end2`/`end3`, or after `len` bytes (`len < 0` = rest).
4619///
4620/// # UPSTREAM-PARITY
4621///
4622/// ```c
4623/// xmlChar *xmlDecodeEntities(xmlParserCtxtPtr ctxt, int len, xmlChar end,
4624///                            xmlChar end2, xmlChar end3);
4625/// ```
4626///
4627/// # SAFETY
4628///
4629/// - `ctxt` must be valid pointers (or NULL
4630///   where the upstream C contract allows), obtained from the
4631///   matching constructor/owner and not yet freed; the callee may
4632///   take or keep ownership exactly as the C API specifies.
4633///
4634/// The caller must not race this call with concurrent mutation of the
4635/// same objects from other threads (per-object state is not internally
4636/// synchronized). Violating any of the above is undefined behavior.
4637///
4638/// Exercised by the C-API differential courts
4639/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4640/// courts; those pass byte-for-byte against the upstream oracle.
4641#[no_mangle]
4642pub unsafe extern "C" fn xmlDecodeEntities(
4643    ctxt: *mut _xmlParserCtxt,
4644    len: c_int,
4645    end: xmlChar,
4646    end2: xmlChar,
4647    end3: xmlChar,
4648) -> *mut xmlChar {
4649    if ctxt.is_null() || (*ctxt).input.is_null() {
4650        return ptr::null_mut();
4651    }
4652    unsafe {
4653        let pi = &*((*ctxt).input);
4654        let cur = pi.cur;
4655        if cur.is_null() {
4656            return ptr::null_mut();
4657        }
4658        let avail = (pi.end as usize).saturating_sub(cur as usize);
4659        let n = if len < 0 {
4660            avail
4661        } else {
4662            (len as usize).min(avail)
4663        };
4664
4665        let mut out: Vec<u8> = Vec::new();
4666        let mut i = 0usize;
4667
4668        while i < n {
4669            let c = *cur.add(i);
4670            if c == end || c == end2 || c == end3 {
4671                break;
4672            }
4673            if c != b'&' {
4674                out.push(c);
4675                i += 1;
4676                continue;
4677            }
4678
4679            // Character reference: &#...; or &#x...;
4680            if i + 1 < n && *cur.add(i + 1) == b'#' {
4681                let (value, consumed) = parse_char_ref(cur.add(i), n - i);
4682                if consumed == 0 {
4683                    out.push(b'&');
4684                    i += 1;
4685                    continue;
4686                }
4687                let mut buf = [0u8; 4];
4688                let blen = copy_char_utf8(&mut buf, value);
4689                out.extend_from_slice(&buf[..blen]);
4690                i += consumed;
4691                continue;
4692            }
4693
4694            // Entity reference: &name;
4695            let mut j = i + 1;
4696            while j < n
4697                && ((*cur.add(j)).is_ascii_alphanumeric()
4698                    || *cur.add(j) == b'_'
4699                    || *cur.add(j) == b'-'
4700                    || *cur.add(j) == b'.'
4701                    || *cur.add(j) == b':')
4702            {
4703                j += 1;
4704            }
4705            if j < n && *cur.add(j) == b';' {
4706                let name = core::slice::from_raw_parts(cur.add(i + 1), j - i - 1);
4707                let mut replaced = false;
4708                // Predefined entities.
4709                let content: Option<&[u8]> = match name {
4710                    b"amp" => Some(b"&"),
4711                    b"lt" => Some(b"<"),
4712                    b"gt" => Some(b">"),
4713                    b"quot" => Some(b"\""),
4714                    b"apos" => Some(b"'"),
4715                    _ => None,
4716                };
4717                if let Some(c) = content {
4718                    out.extend_from_slice(c);
4719                    replaced = true;
4720                } else {
4721                    // DTD-declared entity.
4722                    let mut name_nul = name.to_vec();
4723                    name_nul.push(0);
4724                    let ent = entities::get_entity((*ctxt).myDoc, name_nul.as_ptr());
4725                    if !ent.is_null() && !(*ent).content.is_null() {
4726                        let clen = string::xml_strlen((*ent).content);
4727                        out.extend_from_slice(core::slice::from_raw_parts((*ent).content, clen));
4728                        replaced = true;
4729                    }
4730                }
4731                if replaced {
4732                    i = j + 1;
4733                    continue;
4734                }
4735            }
4736            out.push(b'&');
4737            i += 1;
4738        }
4739
4740        out.push(0);
4741        let result = xmlMallocImpl(out.len()) as *mut xmlChar;
4742        if result.is_null() {
4743            return ptr::null_mut();
4744        }
4745        ptr::copy_nonoverlapping(out.as_ptr(), result, out.len());
4746        result
4747    }
4748}
4749
4750/// Parse a numeric character reference at `ptr` (starting at '&#'); returns
4751/// the value and total bytes consumed, or (0, 0) when malformed.
4752const unsafe fn parse_char_ref(ptr: *const u8, avail: usize) -> (c_int, usize) {
4753    unsafe {
4754        if avail < 3 || *ptr != b'&' || *ptr.add(1) != b'#' {
4755            return (0, 0);
4756        }
4757        let mut i = 2usize;
4758        let hex = i < avail && (*ptr.add(i) == b'x' || *ptr.add(i) == b'X');
4759        if hex {
4760            i += 1;
4761        }
4762        let start = i;
4763        let mut value: u32 = 0;
4764        while i < avail && *ptr.add(i) != b';' {
4765            let d = (*ptr.add(i) as char).to_digit(if hex { 16 } else { 10 });
4766            match d {
4767                Some(d) => {
4768                    value = value
4769                        .saturating_mul(if hex { 16 } else { 10 })
4770                        .saturating_add(d);
4771                    i += 1;
4772                }
4773                None => return (0, 0),
4774            }
4775        }
4776        if i == start || i >= avail || *ptr.add(i) != b';' {
4777            return (0, 0);
4778        }
4779        (value as c_int, i + 1)
4780    }
4781}
4782
4783/// Encode a codepoint into a UTF-8 byte sequence; returns the byte count.
4784const fn copy_char_utf8(out: &mut [u8; 4], val: c_int) -> usize {
4785    if val < 0x80 {
4786        out[0] = val as u8;
4787        1
4788    } else if val < 0x800 {
4789        out[0] = 0xC0 | ((val >> 6) as u8);
4790        out[1] = 0x80 | ((val & 0x3F) as u8);
4791        2
4792    } else if val < 0x10000 {
4793        out[0] = 0xE0 | ((val >> 12) as u8);
4794        out[1] = 0x80 | (((val >> 6) & 0x3F) as u8);
4795        out[2] = 0x80 | ((val & 0x3F) as u8);
4796        3
4797    } else if val < 0x110000 {
4798        out[0] = 0xF0 | ((val >> 18) as u8);
4799        out[1] = 0x80 | (((val >> 12) & 0x3F) as u8);
4800        out[2] = 0x80 | (((val >> 6) & 0x3F) as u8);
4801        out[3] = 0x80 | ((val & 0x3F) as u8);
4802        4
4803    } else {
4804        out[0] = 0;
4805        1
4806    }
4807}
4808
4809/// Detect the character encoding of a buffer from its initial bytes.
4810///
4811/// # UPSTREAM-PARITY
4812///
4813/// ```c
4814/// xmlCharEncoding xmlDetectCharEncoding(const unsigned char *in, int len);
4815/// ```
4816///
4817/// # SAFETY
4818///
4819/// - `in_` must be valid pointers (or NULL
4820///   where the upstream C contract allows), obtained from the
4821///   matching constructor/owner and not yet freed; the callee may
4822///   take or keep ownership exactly as the C API specifies.
4823///
4824/// The caller must not race this call with concurrent mutation of the
4825/// same objects from other threads (per-object state is not internally
4826/// synchronized). Violating any of the above is undefined behavior.
4827///
4828/// Exercised by the C-API differential courts
4829/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4830/// courts; those pass byte-for-byte against the upstream oracle.
4831#[no_mangle]
4832pub const unsafe extern "C" fn xmlDetectCharEncoding(in_: *const c_uchar, len: c_int) -> c_int {
4833    if in_.is_null() {
4834        return xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int;
4835    }
4836    unsafe {
4837        if len >= 4 {
4838            if *in_ == 0x00 && *in_.add(1) == 0x00 && *in_.add(2) == 0x00 && *in_.add(3) == 0x3C {
4839                return xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE as c_int;
4840            }
4841            if *in_ == 0x3C && *in_.add(1) == 0x00 && *in_.add(2) == 0x00 && *in_.add(3) == 0x00 {
4842                return xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE as c_int;
4843            }
4844            if *in_ == 0x4C && *in_.add(1) == 0x6F && *in_.add(2) == 0xA7 && *in_.add(3) == 0x94 {
4845                return xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC as c_int;
4846            }
4847            if *in_ == 0x3C && *in_.add(1) == 0x3F && *in_.add(2) == 0x78 && *in_.add(3) == 0x6D {
4848                return xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int;
4849            }
4850            if *in_ == 0x3C && *in_.add(1) == 0x00 && *in_.add(2) == 0x3F && *in_.add(3) == 0x00 {
4851                return xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE as c_int;
4852            }
4853            if *in_ == 0x00 && *in_.add(1) == 0x3C && *in_.add(2) == 0x00 && *in_.add(3) == 0x3F {
4854                return xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE as c_int;
4855            }
4856        }
4857        if len >= 3 && *in_ == 0xEF && *in_.add(1) == 0xBB && *in_.add(2) == 0xBF {
4858            return xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int;
4859        }
4860        if len >= 2 {
4861            if *in_ == 0xFE && *in_.add(1) == 0xFF {
4862                return xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE as c_int;
4863            }
4864            if *in_ == 0xFF && *in_.add(1) == 0xFE {
4865                return xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE as c_int;
4866            }
4867        }
4868    }
4869    xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int
4870}
4871
4872/// Convert the first line of `in` using the encoding handler, appending the
4873/// result to `out`.
4874///
4875/// # UPSTREAM-PARITY
4876///
4877/// ```c
4878/// int xmlCharEncFirstLine(xmlCharEncodingHandlerPtr handler,
4879///                         struct _xmlBuffer *out, struct _xmlBuffer *in);
4880/// ```
4881///
4882/// # SAFETY
4883///
4884/// - `handler`, `out`, `in_` must be valid pointers (or NULL
4885///   where the upstream C contract allows), obtained from the
4886///   matching constructor/owner and not yet freed; the callee may
4887///   take or keep ownership exactly as the C API specifies.
4888///
4889/// The caller must not race this call with concurrent mutation of the
4890/// same objects from other threads (per-object state is not internally
4891/// synchronized). Violating any of the above is undefined behavior.
4892///
4893/// Exercised by the C-API differential courts
4894/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4895/// courts; those pass byte-for-byte against the upstream oracle.
4896#[no_mangle]
4897pub unsafe extern "C" fn xmlCharEncFirstLine(
4898    handler: *mut _xmlCharEncodingHandler,
4899    out: *mut _xmlBuffer,
4900    in_: *mut _xmlBuffer,
4901) -> c_int {
4902    encoding::xmlCharEncInFunc(handler, out, in_)
4903}
4904
4905/// Check whether the current thread is the main thread.
4906///
4907/// # UPSTREAM-PARITY
4908///
4909/// ```c
4910/// int xmlIsMainThread(void);
4911/// ```
4912///
4913/// # SAFETY
4914///
4915/// The function touches crate-global state only; it is safe
4916/// as long as the caller respects the library's global
4917/// initialization/cleanup ordering (xmlInitParser before use,
4918/// xmlCleanupParser only after all users are done).
4919///
4920/// Violating the global lifecycle ordering, or calling this after
4921/// teardown or from a signal handler, is undefined behavior.
4922#[no_mangle]
4923pub const unsafe extern "C" fn xmlIsMainThread() -> c_int {
4924    1
4925}
4926
4927// ═══════════════════════════════════════════════════════════════════════════════
4928// Error reporting helpers (xmlerror.h)
4929// ═══════════════════════════════════════════════════════════════════════════════
4930
4931/// Print file and line information for a parser input to the generic error
4932/// channel.
4933///
4934/// # UPSTREAM-PARITY
4935///
4936/// ```c
4937/// void xmlParserPrintFileInfo(struct _xmlParserInput *input);
4938/// ```
4939///
4940/// # SAFETY
4941///
4942/// - `input` must be valid pointers (or NULL
4943///   where the upstream C contract allows), obtained from the
4944///   matching constructor/owner and not yet freed; the callee may
4945///   take or keep ownership exactly as the C API specifies.
4946///
4947/// The caller must not race this call with concurrent mutation of the
4948/// same objects from other threads (per-object state is not internally
4949/// synchronized). Violating any of the above is undefined behavior.
4950///
4951/// Exercised by the C-API differential courts
4952/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4953/// courts; those pass byte-for-byte against the upstream oracle.
4954#[no_mangle]
4955pub unsafe extern "C" fn xmlParserPrintFileInfo(input: *mut _xmlParserInput) {
4956    if input.is_null() {
4957        return;
4958    }
4959    unsafe {
4960        let channel: Option<xmlGenericErrorFunc> = globals::get_generic_error_func();
4961        let data = globals::get_generic_error_ctx();
4962        let Some(ch) = channel else { return };
4963
4964        let msg = if !(*input).filename.is_null() {
4965            let file = CStr::from_ptr((*input).filename);
4966            let s = format!("{}:{}: ", file.to_string_lossy(), (*input).line);
4967            std::ffi::CString::new(s).unwrap_or_default()
4968        } else {
4969            let s = format!("Entity: line {}: ", (*input).line);
4970            std::ffi::CString::new(s).unwrap_or_default()
4971        };
4972        ch(data, msg.as_ptr());
4973    }
4974}
4975
4976/// Print the input context around the current error position to the generic
4977/// error channel.
4978///
4979/// # UPSTREAM-PARITY
4980///
4981/// ```c
4982/// void xmlParserPrintFileContext(struct _xmlParserInput *input);
4983/// ```
4984///
4985/// # SAFETY
4986///
4987/// - `input` 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/// The caller must not race this call with concurrent mutation of the
4993/// same objects from other threads (per-object state is not internally
4994/// synchronized). Violating any of the above is undefined behavior.
4995///
4996/// Exercised by the C-API differential courts
4997/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4998/// courts; those pass byte-for-byte against the upstream oracle.
4999#[no_mangle]
5000pub unsafe extern "C" fn xmlParserPrintFileContext(input: *mut _xmlParserInput) {
5001    if input.is_null() || (*input).cur.is_null() {
5002        return;
5003    }
5004    unsafe {
5005        let channel: Option<xmlGenericErrorFunc> = globals::get_generic_error_func();
5006        let data = globals::get_generic_error_ctx();
5007        let Some(ch) = channel else { return };
5008
5009        let pi = &*input;
5010        let cur = pi.cur;
5011        let base = pi.base;
5012        let end = pi.end;
5013
5014        // Build a window of up to 80 bytes ending at `cur`.
5015        let before = if base.is_null() {
5016            0
5017        } else {
5018            (cur as usize).saturating_sub(base as usize)
5019        };
5020        let take = before.min(LINE_LEN);
5021        let start = cur.sub(take);
5022        let n = (end as usize).saturating_sub(start as usize).min(LINE_LEN);
5023
5024        let mut content = vec![0u8; n];
5025        if n > 0 {
5026            ptr::copy_nonoverlapping(start, content.as_mut_ptr(), n);
5027        }
5028        let line = std::ffi::CString::new(content.clone()).unwrap_or_default();
5029        ch(data, line.as_ptr());
5030
5031        // Caret line pointing at the current character.
5032        let mut caret = vec![b' '; take];
5033        if take < LINE_LEN + 1 {
5034            caret.push(b'^');
5035        }
5036        let caret_c = std::ffi::CString::new(caret).unwrap_or_default();
5037        ch(data, caret_c.as_ptr());
5038    }
5039}
5040
5041// ═══════════════════════════════════════════════════════════════════════════════
5042// SAX/DTD parse front-ends
5043// ═══════════════════════════════════════════════════════════════════════════════
5044
5045/// Handle an entity reference by pushing the entity's content as a new input
5046/// stream (deprecated internal API).
5047///
5048/// # UPSTREAM-PARITY
5049///
5050/// ```c
5051/// void xmlHandleEntity(xmlParserCtxtPtr ctxt, xmlEntityPtr entity);
5052/// ```
5053///
5054/// # SAFETY
5055///
5056/// - `ctxt`, `entity` must be valid pointers (or NULL
5057///   where the upstream C contract allows), obtained from the
5058///   matching constructor/owner and not yet freed; the callee may
5059///   take or keep ownership exactly as the C API specifies.
5060///
5061/// The caller must not race this call with concurrent mutation of the
5062/// same objects from other threads (per-object state is not internally
5063/// synchronized). Violating any of the above is undefined behavior.
5064///
5065/// Exercised by the C-API differential courts
5066/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5067/// courts; those pass byte-for-byte against the upstream oracle.
5068#[no_mangle]
5069pub unsafe extern "C" fn xmlHandleEntity(ctxt: *mut _xmlParserCtxt, entity: *mut c_void) {
5070    if ctxt.is_null() {
5071        return;
5072    }
5073    unsafe {
5074        let ent = entity as *mut _xmlEntity;
5075        if ent.is_null() {
5076            return;
5077        }
5078        // Unparsed entities cannot be included by reference.
5079        if (*ent).etype == xmlEntityType::XML_EXTERNAL_GENERAL_UNPARSED_ENTITY as c_int {
5080            return;
5081        }
5082
5083        let mut input = ptr::null_mut();
5084        if !(*ent).content.is_null() {
5085            // Internal entity: push its replacement text as a new stream.
5086            let content = (*ent).content;
5087            let pi = xmlNewInputStream(ctxt);
5088            if pi.is_null() {
5089                return;
5090            }
5091            let len = string::xml_strlen(content);
5092            (*pi).base = content;
5093            (*pi).cur = content;
5094            (*pi).end = content.add(len);
5095            (*pi).length = len as c_int;
5096            (*pi).entity = ent;
5097            input = pi;
5098        } else if !(*ent).URI.is_null() {
5099            // External parsed entity: load it through the entity loader.
5100            input = xmlLoadExternalEntity(
5101                (*ent).URI as *const c_char,
5102                (*ent).ExternalID as *const c_char,
5103                ctxt,
5104            );
5105            if !input.is_null() {
5106                (*input).entity = ent;
5107            }
5108        }
5109
5110        if input.is_null() {
5111            return;
5112        }
5113        xmlPushInput(ctxt, input);
5114    }
5115}
5116
5117/// Load and parse a DTD, returning the resulting `xmlDtd` (detached from any
5118/// document).
5119///
5120/// # UPSTREAM-PARITY
5121///
5122/// ```c
5123/// xmlDtdPtr xmlSAXParseDTD(xmlSAXHandlerPtr sax, const xmlChar *publicId,
5124///                          const xmlChar *systemId);
5125/// ```
5126///
5127/// # SAFETY
5128///
5129/// - `sax` must be valid pointers (or NULL
5130///   where the upstream C contract allows), obtained from the
5131///   matching constructor/owner and not yet freed; the callee may
5132///   take or keep ownership exactly as the C API specifies.
5133///
5134/// - `publicId`, `systemId` must point to valid NUL-terminated
5135///   strings (or NULL where the C contract allows) for the lifetime
5136///   of the call.
5137///
5138/// The caller must not race this call with concurrent mutation of the
5139/// same objects from other threads (per-object state is not internally
5140/// synchronized). Violating any of the above is undefined behavior.
5141///
5142/// Exercised by the C-API differential courts
5143/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5144/// courts; those pass byte-for-byte against the upstream oracle.
5145#[no_mangle]
5146pub unsafe extern "C" fn xmlSAXParseDTD(
5147    sax: *mut _xmlSAXHandler,
5148    publicId: *const xmlChar,
5149    systemId: *const xmlChar,
5150) -> *mut _xmlDtd {
5151    if publicId.is_null() && systemId.is_null() {
5152        return ptr::null_mut();
5153    }
5154    unsafe {
5155        let ctxt = xmlNewSAXParserCtxt(sax, ptr::null_mut());
5156        if ctxt.is_null() {
5157            return ptr::null_mut();
5158        }
5159        apply_options(ctxt, XML_PARSE_DTDLOAD);
5160
5161        // Resolve via the SAX resolveEntity callback when available, else
5162        // load the system ID directly.
5163        let mut input = ptr::null_mut();
5164        if !sax.is_null() {
5165            if let Some(resolve) = (*sax).resolveEntity {
5166                input = resolve((*ctxt).userData, publicId, systemId);
5167            }
5168        }
5169        if input.is_null() {
5170            if systemId.is_null() {
5171                helpers::free_parser_ctxt(ctxt);
5172                return ptr::null_mut();
5173            }
5174            input = xmlLoadExternalEntity(systemId as *const c_char, ptr::null(), ctxt);
5175        }
5176        if input.is_null() {
5177            helpers::free_parser_ctxt(ctxt);
5178            return ptr::null_mut();
5179        }
5180
5181        // Materialise the DTD text before freeing the input struct.
5182        let data: Vec<u8> = {
5183            let pi = &*input;
5184            if !pi.base.is_null() && !pi.end.is_null() && pi.end >= pi.base {
5185                let len = (pi.end as usize).saturating_sub(pi.base as usize);
5186                core::slice::from_raw_parts(pi.base, len).to_vec()
5187            } else if !pi.buf.is_null() {
5188                input_buffer_data(pi.buf)
5189            } else {
5190                Vec::new()
5191            }
5192        };
5193        helpers::free_parser_input(input);
5194
5195        let dtd = parse_dtd_text(ctxt, &data, publicId, systemId);
5196        helpers::free_parser_ctxt(ctxt);
5197        dtd
5198    }
5199}
5200
5201/// Load and parse a DTD from an input buffer.
5202///
5203/// # UPSTREAM-PARITY
5204///
5205/// ```c
5206/// xmlDtdPtr xmlIOParseDTD(xmlSAXHandlerPtr sax, xmlParserInputBufferPtr input,
5207///                         xmlCharEncoding enc);
5208/// ```
5209///
5210/// # SAFETY
5211///
5212/// - `sax`, `input` must be valid pointers (or NULL
5213///   where the upstream C contract allows), obtained from the
5214///   matching constructor/owner and not yet freed; the callee may
5215///   take or keep ownership exactly as the C API specifies.
5216///
5217/// The caller must not race this call with concurrent mutation of the
5218/// same objects from other threads (per-object state is not internally
5219/// synchronized). Violating any of the above is undefined behavior.
5220///
5221/// Exercised by the C-API differential courts
5222/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5223/// courts; those pass byte-for-byte against the upstream oracle.
5224#[no_mangle]
5225pub unsafe extern "C" fn xmlIOParseDTD(
5226    sax: *mut _xmlSAXHandler,
5227    input: *mut _xmlParserInputBuffer,
5228    enc: c_int,
5229) -> *mut _xmlDtd {
5230    if input.is_null() {
5231        return ptr::null_mut();
5232    }
5233    unsafe {
5234        let ctxt = xmlNewSAXParserCtxt(sax, ptr::null_mut());
5235        if ctxt.is_null() {
5236            io::input_buffer_free(input);
5237            return ptr::null_mut();
5238        }
5239        apply_options(ctxt, XML_PARSE_DTDLOAD);
5240        if enc != xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int {
5241            (*ctxt).charset = enc;
5242        }
5243
5244        // Materialise the data from the input buffer.
5245        let data: Vec<u8> = input_buffer_data(input);
5246        io::input_buffer_free(input);
5247
5248        let dtd = parse_dtd_text(ctxt, &data, ptr::null(), ptr::null());
5249        helpers::free_parser_ctxt(ctxt);
5250        dtd
5251    }
5252}
5253
5254/// Extract the buffered data of an input buffer as an owned byte vector.
5255unsafe fn input_buffer_data(buf: *mut _xmlParserInputBuffer) -> Vec<u8> {
5256    unsafe {
5257        if buf.is_null() {
5258            return Vec::new();
5259        }
5260        let b = &*buf;
5261        if let Some(read) = b.readcallback {
5262            let mut out = Vec::new();
5263            let mut tmp = [0u8; 4096];
5264            loop {
5265                let n = read(
5266                    b.context,
5267                    tmp.as_mut_ptr() as *mut c_char,
5268                    tmp.len() as c_int,
5269                );
5270                if n <= 0 {
5271                    break;
5272                }
5273                out.extend_from_slice(&tmp[..n as usize]);
5274            }
5275            return out;
5276        }
5277        if !b.buffer.is_null() {
5278            let xbuf = &*(b.buffer as *mut _xmlBuffer);
5279            if !xbuf.content.is_null() && xbuf.use_ > 0 {
5280                return core::slice::from_raw_parts(xbuf.content, xbuf.use_ as usize).to_vec();
5281            }
5282        }
5283        Vec::new()
5284    }
5285}
5286
5287/// Parse an external general entity and build a tree.
5288///
5289/// # UPSTREAM-PARITY
5290///
5291/// ```c
5292/// xmlDocPtr xmlSAXParseEntity(xmlSAXHandlerPtr sax, const char *filename);
5293/// ```
5294///
5295/// # SAFETY
5296///
5297/// - `sax` must be valid pointers (or NULL
5298///   where the upstream C contract allows), obtained from the
5299///   matching constructor/owner and not yet freed; the callee may
5300///   take or keep ownership exactly as the C API specifies.
5301///
5302/// - `filename` must point to valid NUL-terminated
5303///   strings (or NULL where the C contract allows) for the lifetime
5304///   of the call.
5305///
5306/// The caller must not race this call with concurrent mutation of the
5307/// same objects from other threads (per-object state is not internally
5308/// synchronized). Violating any of the above is undefined behavior.
5309///
5310/// Exercised by the C-API differential courts
5311/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5312/// courts; those pass byte-for-byte against the upstream oracle.
5313#[no_mangle]
5314pub unsafe extern "C" fn xmlSAXParseEntity(
5315    sax: *mut _xmlSAXHandler,
5316    filename: *const c_char,
5317) -> *mut _xmlDoc {
5318    if filename.is_null() {
5319        return ptr::null_mut();
5320    }
5321    unsafe {
5322        let ctxt = xmlNewSAXParserCtxt(sax, ptr::null_mut());
5323        if ctxt.is_null() {
5324            return ptr::null_mut();
5325        }
5326        let input = match open_filename_routed(filename, ctxt) {
5327            RoutedFileOpen::Loaded(i) => i,
5328            RoutedFileOpen::Failed => {
5329                emit_io_warning(ctxt, io_load_failure_message(filename));
5330                helpers::free_parser_ctxt(ctxt);
5331                return ptr::null_mut();
5332            }
5333            RoutedFileOpen::EntityLoaderFailed => {
5334                helpers::free_parser_ctxt(ctxt);
5335                return ptr::null_mut();
5336            }
5337            RoutedFileOpen::Builtin => match helpers::input_from_file(filename) {
5338                Ok(i) => i,
5339                Err(_) => {
5340                    helpers::free_parser_ctxt(ctxt);
5341                    return ptr::null_mut();
5342                }
5343            },
5344        };
5345        helpers::setup_parser_input(ctxt, input);
5346        let rc = helpers::parse_document(ctxt);
5347        let doc = (*ctxt).myDoc;
5348        (*ctxt).myDoc = ptr::null_mut();
5349        if rc != 0 || (*ctxt).wellFormed == 0 {
5350            if !doc.is_null() {
5351                tree::free_doc(doc);
5352            }
5353            helpers::free_parser_ctxt(ctxt);
5354            return ptr::null_mut();
5355        }
5356        helpers::free_parser_ctxt(ctxt);
5357        doc
5358    }
5359}
5360
5361// ═══════════════════════════════════════════════════════════════════════════════
5362// C14N: xmlC14NDocSave
5363// ═══════════════════════════════════════════════════════════════════════════════
5364
5365/// Canonicalise a document (or node set) and save it to a file.
5366///
5367/// # UPSTREAM-PARITY
5368///
5369/// ```c
5370/// int xmlC14NDocSave(xmlDocPtr doc, xmlNodeSetPtr nodes, int mode,
5371///                    xmlChar **inclusive_ns_prefixes, int with_comments,
5372///                    const char *filename, int compression);
5373/// ```
5374///
5375/// # SAFETY
5376///
5377/// - `doc`, `nodes`, `inclusive_ns_prefixes` 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/// - `filename` must point to valid NUL-terminated
5383///   strings (or NULL where the C contract allows) for the lifetime
5384///   of the call.
5385///
5386/// The caller must not race this call with concurrent mutation of the
5387/// same objects from other threads (per-object state is not internally
5388/// synchronized). Violating any of the above is undefined behavior.
5389///
5390/// Exercised by the C-API differential courts
5391/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5392/// courts; those pass byte-for-byte against the upstream oracle.
5393#[no_mangle]
5394pub unsafe extern "C" fn xmlC14NDocSave(
5395    doc: *mut _xmlDoc,
5396    nodes: *mut _xmlNodeSet,
5397    mode: c_int,
5398    inclusive_ns_prefixes: *mut *mut xmlChar,
5399    with_comments: c_int,
5400    filename: *const c_char,
5401    compression: c_int,
5402) -> c_int {
5403    if filename.is_null() {
5404        return -1;
5405    }
5406    unsafe {
5407        let output = io::output_buffer_create_filename(filename, ptr::null_mut(), compression);
5408        if output.is_null() {
5409            return -1;
5410        }
5411        let ret = crate::xml::c14n::xmlC14NDocSaveTo(
5412            doc,
5413            nodes,
5414            mode,
5415            inclusive_ns_prefixes,
5416            with_comments,
5417            output,
5418        );
5419        if ret < 0 {
5420            io::output_buffer_close(output);
5421            return -1;
5422        }
5423        let close_ret = io::output_buffer_close(output);
5424        if close_ret < 0 {
5425            -1
5426        } else {
5427            ret
5428        }
5429    }
5430}
5431
5432// ═══════════════════════════════════════════════════════════════════════════════
5433// Parser-internal stack primitives (parserInternals.h)
5434//
5435// Debian/2.9-era distro binaries link these directly (they are exported data
5436// in the distro DSO, not hidden). They manipulate the parser context's
5437// deprecated `inputTab`/`nameTab`/`nodeTab` arrays exactly as upstream
5438// parserInternals.c does. `valuePush`/`valuePop` operate on the XPath parser
5439// context value stack (xpath.c valuePush/valuePop).
5440// ═══════════════════════════════════════════════════════════════════════════════
5441
5442/// `inputPush` (parserInternals.h) — push an input onto the parser input stack.
5443#[no_mangle]
5444pub unsafe extern "C" fn inputPush(
5445    ctxt: *mut _xmlParserCtxt,
5446    value: *mut _xmlParserInput,
5447) -> *mut _xmlParserInput {
5448    if ctxt.is_null() || value.is_null() {
5449        return ptr::null_mut();
5450    }
5451    unsafe {
5452        let c = &mut *ctxt;
5453        if c.inputNr >= c.inputMax {
5454            let new_max = if c.inputMax > 0 { c.inputMax * 2 } else { 4 };
5455            let new_tab = crate::abi::allocator::xmlReallocImpl(
5456                c.inputTab as *mut core::ffi::c_void,
5457                (new_max as usize) * core::mem::size_of::<*mut _xmlParserInput>(),
5458            ) as *mut *mut _xmlParserInput;
5459            if new_tab.is_null() {
5460                return ptr::null_mut();
5461            }
5462            c.inputTab = new_tab;
5463            c.inputMax = new_max;
5464        }
5465        *c.inputTab.add(c.inputNr as usize) = value;
5466        c.inputNr += 1;
5467        value
5468    }
5469}
5470
5471/// `inputPop` (parserInternals.h) — pop an input off the parser input stack.
5472#[no_mangle]
5473pub unsafe extern "C" fn inputPop(ctxt: *mut _xmlParserCtxt) -> *mut _xmlParserInput {
5474    if ctxt.is_null() {
5475        return ptr::null_mut();
5476    }
5477    unsafe {
5478        let c = &mut *ctxt;
5479        if c.inputNr <= 0 {
5480            return ptr::null_mut();
5481        }
5482        c.inputNr -= 1;
5483        let ret = *c.inputTab.add(c.inputNr as usize);
5484        *c.inputTab.add(c.inputNr as usize) = ptr::null_mut();
5485        ret
5486    }
5487}
5488
5489/// `namePush` (parserInternals.h) — push a name onto the parser name stack.
5490#[no_mangle]
5491pub unsafe extern "C" fn namePush(
5492    ctxt: *mut _xmlParserCtxt,
5493    value: *const xmlChar,
5494) -> *const xmlChar {
5495    if ctxt.is_null() || value.is_null() {
5496        return ptr::null();
5497    }
5498    unsafe {
5499        let c = &mut *ctxt;
5500        if c.nameNr >= c.nameMax {
5501            let new_max = if c.nameMax > 0 { c.nameMax * 2 } else { 4 };
5502            let new_tab = crate::abi::allocator::xmlReallocImpl(
5503                c.nameTab as *mut core::ffi::c_void,
5504                (new_max as usize) * core::mem::size_of::<*const xmlChar>(),
5505            ) as *mut *const xmlChar;
5506            if new_tab.is_null() {
5507                return ptr::null();
5508            }
5509            c.nameTab = new_tab;
5510            c.nameMax = new_max;
5511        }
5512        *c.nameTab.add(c.nameNr as usize) = value;
5513        c.nameNr += 1;
5514        value
5515    }
5516}
5517
5518/// `namePop` (parserInternals.h) — pop a name off the parser name stack.
5519#[no_mangle]
5520pub unsafe extern "C" fn namePop(ctxt: *mut _xmlParserCtxt) -> *const xmlChar {
5521    if ctxt.is_null() {
5522        return ptr::null();
5523    }
5524    unsafe {
5525        let c = &mut *ctxt;
5526        if c.nameNr <= 0 {
5527            return ptr::null();
5528        }
5529        c.nameNr -= 1;
5530        let ret = *c.nameTab.add(c.nameNr as usize);
5531        *c.nameTab.add(c.nameNr as usize) = ptr::null();
5532        ret
5533    }
5534}
5535
5536/// `nodePush` (parserInternals.h) — push a node onto the parser node stack.
5537#[no_mangle]
5538pub unsafe extern "C" fn nodePush(
5539    ctxt: *mut _xmlParserCtxt,
5540    value: *mut _xmlNode,
5541) -> *mut _xmlNode {
5542    if ctxt.is_null() || value.is_null() {
5543        return ptr::null_mut();
5544    }
5545    unsafe {
5546        let c = &mut *ctxt;
5547        if c.nodeNr >= c.nodeMax {
5548            let new_max = if c.nodeMax > 0 { c.nodeMax * 2 } else { 4 };
5549            let new_tab = crate::abi::allocator::xmlReallocImpl(
5550                c.nodeTab as *mut core::ffi::c_void,
5551                (new_max as usize) * core::mem::size_of::<*mut _xmlNode>(),
5552            ) as *mut *mut _xmlNode;
5553            if new_tab.is_null() {
5554                return ptr::null_mut();
5555            }
5556            c.nodeTab = new_tab;
5557            c.nodeMax = new_max;
5558        }
5559        *c.nodeTab.add(c.nodeNr as usize) = value;
5560        c.nodeNr += 1;
5561        value
5562    }
5563}
5564
5565/// `nodePop` (parserInternals.h) — pop a node off the parser node stack.
5566#[no_mangle]
5567pub unsafe extern "C" fn nodePop(ctxt: *mut _xmlParserCtxt) -> *mut _xmlNode {
5568    if ctxt.is_null() {
5569        return ptr::null_mut();
5570    }
5571    unsafe {
5572        let c = &mut *ctxt;
5573        if c.nodeNr <= 0 {
5574            return ptr::null_mut();
5575        }
5576        c.nodeNr -= 1;
5577        let ret = *c.nodeTab.add(c.nodeNr as usize);
5578        *c.nodeTab.add(c.nodeNr as usize) = ptr::null_mut();
5579        ret
5580    }
5581}
5582
5583/// `valuePush` (xpath.c) — push an XPath object onto the parser value stack.
5584#[no_mangle]
5585pub unsafe extern "C" fn valuePush(
5586    ctxt: *mut crate::xml::xpath::parser_context::XmlXPathParserContext,
5587    value: *mut _xmlXPathObject,
5588) -> c_int {
5589    if crate::xml::xpath::parser_context::value_push(ctxt, value).is_null() {
5590        -1
5591    } else {
5592        0
5593    }
5594}
5595
5596/// `valuePop` (xpath.c) — pop an XPath object off the parser value stack.
5597#[no_mangle]
5598pub unsafe extern "C" fn valuePop(
5599    ctxt: *mut crate::xml::xpath::parser_context::XmlXPathParserContext,
5600) -> *mut _xmlXPathObject {
5601    crate::xml::xpath::parser_context::value_pop(ctxt)
5602}
5603
5604#[cfg(test)]
5605mod tests {
5606    use super::*;
5607
5608    /// A read-callback state pair mirroring PHP's streams IO loader: the
5609    /// registered `xmlParserInputBufferCreateFilenameDefault` serves bytes
5610    /// through an `xmlParserInputBufferCreateIO` buffer (php builds exactly
5611    /// this shape with php_libxml_streams_IO_read/close over a php_stream).
5612    /// The loader reaches the state through a thread-local pointer (the
5613    /// loader slot itself is per-thread TLS, so there is no cross-thread
5614    /// aliasing).
5615    struct ServeState {
5616        data: &'static [u8],
5617        pos: usize,
5618        closed: bool,
5619    }
5620
5621    thread_local! {
5622        static SERVE_STATE: std::cell::Cell<*mut ServeState> =
5623            std::cell::Cell::new(std::ptr::null_mut());
5624    }
5625
5626    unsafe extern "C" fn serve_read(ctx: *mut c_void, buffer: *mut c_char, len: c_int) -> c_int {
5627        // SAFETY: ctx is the ServeState set up by the test; buffer is a
5628        // writable len-byte region per the xmlInputReadCallback contract.
5629        let st = unsafe { &mut *(ctx as *mut ServeState) };
5630        if st.pos >= st.data.len() {
5631            return 0;
5632        }
5633        let n = (len as usize).min(st.data.len() - st.pos);
5634        unsafe {
5635            core::ptr::copy_nonoverlapping(st.data.as_ptr().add(st.pos), buffer as *mut u8, n);
5636        }
5637        st.pos += n;
5638        n as c_int
5639    }
5640
5641    unsafe extern "C" fn serve_close(ctx: *mut c_void) -> c_int {
5642        // SAFETY: ctx is the ServeState set up by the test.
5643        let st = unsafe { &mut *(ctx as *mut ServeState) };
5644        st.closed = true;
5645        0
5646    }
5647
5648    /// The php-shaped loader: build an IO buffer over the thread-local serve
5649    /// state. `uri` is deliberately ignored — php's loader opens whatever the
5650    /// php streams layer resolves, so a "file://" URI or a non-existent path
5651    /// both reach the stream; this guard proves the ENGINE consults the
5652    /// loader instead of the built-in path (which would fail on the bogus
5653    /// URI used here).
5654    unsafe extern "C" fn serving_loader(
5655        _uri: *const c_char,
5656        _enc: c_int,
5657    ) -> *mut _xmlParserInputBuffer {
5658        SERVE_STATE.with(|cell| {
5659            let st = cell.get();
5660            if st.is_null() {
5661                return ptr::null_mut();
5662            }
5663            crate::abi::exports_xml2::xmlParserInputBufferCreateIO(
5664                Some(serve_read as xmlInputReadCallback),
5665                Some(serve_close as xmlInputCloseCallback),
5666                st as *mut c_void,
5667                xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int,
5668            )
5669        })
5670    }
5671
5672    unsafe extern "C" fn record_message(ctx: *mut c_void, err: *const _xmlError) {
5673        if err.is_null() {
5674            return;
5675        }
5676        // SAFETY: ctx is the recording Vec set up by the test; the error is
5677        // live for the call and its message is NUL-terminated.
5678        let out = unsafe { &mut *(ctx as *mut Vec<u8>) };
5679        let msg = unsafe { (*err).message };
5680        if !msg.is_null() {
5681            // SAFETY: message is a NUL-terminated C string for the call.
5682            let bytes = unsafe { std::ffi::CStr::from_ptr(msg) }.to_bytes();
5683            out.extend_from_slice(bytes);
5684        }
5685    }
5686
5687    /// SP-14.3.2 S8 / dom-L2 (bug79971_1): a registered
5688    /// `xmlParserInputBufferCreateFilenameDefault` (PHP's streams loader) is
5689    /// consulted by the main-document file open (`xmlReadFile`/
5690    /// `xmlCtxtReadFile` -> xmlNewInputFromFile -> xmlNewInputFromUrl): its
5691    /// bytes are parsed even when the URI is not a real file, and a NULL
5692    /// loader result reports the xmlCtxtErrIO "failed to load" warning with
5693    /// NO built-in fallback.
5694    ///
5695    /// # Safety
5696    ///
5697    /// - the callbacks and stack state are valid for the duration of each
5698    ///   call; the loader/generic-handler TLS slots are restored before the
5699    ///   test ends (serialized via the error-handler test lock).
5700    #[test]
5701    fn test_main_doc_open_consults_registered_input_loader() {
5702        use crate::xml::globals::ERROR_HANDLER_TEST_LOCK;
5703
5704        // Serialize against the handler-slot tests (the generic func slot is
5705        // shared global state); the loader slot is this thread's TLS but it
5706        // is restored so later engine state stays pristine.
5707        let _guard = ERROR_HANDLER_TEST_LOCK.lock();
5708        let old_loader = globals::get_parser_input_buffer_create_filename_value();
5709        let old_struct = globals::get_structured_error_func();
5710        let old_struct_ctx = globals::get_structured_error_ctx();
5711
5712        let mut captured: Vec<u8> = Vec::new();
5713        let captured_ptr = &mut captured as *mut Vec<u8> as *mut c_void;
5714        // SAFETY: set/restore of the handler slots is serialized under
5715        // ERROR_HANDLER_TEST_LOCK for the test's duration.
5716        unsafe {
5717            globals::set_structured_error_func(
5718                captured_ptr,
5719                Some(record_message as xmlStructuredErrorFunc),
5720            );
5721        }
5722
5723        unsafe {
5724            let mut serve = ServeState {
5725                data: b"<root><a>1</a></root>",
5726                pos: 0,
5727                closed: false,
5728            };
5729            SERVE_STATE.with(|cell| cell.set(&mut serve as *mut ServeState));
5730            globals::set_parser_input_buffer_create_filename_value(Some(serving_loader));
5731
5732            // The URI names no real file — only the loader can satisfy it.
5733            let ctxt = helpers::create_parser_ctxt();
5734            assert!(!ctxt.is_null());
5735            let doc = xmlCtxtReadFile(
5736                ctxt,
5737                c"file:///definitely-not-a-file.xml".as_ptr(),
5738                ptr::null(),
5739                0,
5740            );
5741            assert!(
5742                !doc.is_null(),
5743                "registered loader must be consulted for the main document open"
5744            );
5745            let root = (*doc).children;
5746            assert!(
5747                !root.is_null() && !(*root).name.is_null(),
5748                "served document must produce a root element"
5749            );
5750            assert_eq!(
5751                crate::xml::string::xmlstr_to_bytes((*root).name as *const u8),
5752                b"root",
5753                "document served by the loader must be parsed"
5754            );
5755            assert!(serve.closed, "loader stream must be closed exactly once");
5756            tree::free_doc(doc);
5757            helpers::free_parser_ctxt(ctxt);
5758
5759            // A loader result of NULL is XML_IO_ENOENT: the built-in open is
5760            // NOT attempted and the xmlCtxtErrIO ENOENT report ("failed to
5761            // load") reaches the structured handler.
5762            globals::set_parser_input_buffer_create_filename_value(None);
5763            SERVE_STATE.with(|cell| cell.set(ptr::null_mut()));
5764            unsafe extern "C" fn null_loader(
5765                _uri: *const c_char,
5766                _enc: c_int,
5767            ) -> *mut _xmlParserInputBuffer {
5768                ptr::null_mut()
5769            }
5770            globals::set_parser_input_buffer_create_filename_value(Some(null_loader));
5771            let ctxt2 = helpers::create_parser_ctxt();
5772            assert!(!ctxt2.is_null());
5773            let doc2 = xmlCtxtReadFile(
5774                ctxt2,
5775                c"file:///definitely-not-a-file.xml".as_ptr(),
5776                ptr::null(),
5777                0,
5778            );
5779            assert!(doc2.is_null(), "NULL loader result must fail the open");
5780            let got = String::from_utf8_lossy(&captured);
5781            assert!(
5782                got.contains("failed to load"),
5783                "xmlCtxtErrIO report must reach the error channel: {got:?}"
5784            );
5785            helpers::free_parser_ctxt(ctxt2);
5786        }
5787
5788        // Restore both slots.
5789        unsafe {
5790            globals::set_parser_input_buffer_create_filename_value(old_loader);
5791            globals::set_structured_error_func(old_struct_ctx, old_struct);
5792        }
5793    }
5794}