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