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 input = helpers::input_from_memory(cur as *const c_char, len as c_int);
1618        ctxt_read_doc(ctxt, input, URL, options)
1619    }
1620}
1621
1622/// Parse an XML file with a given context.
1623///
1624/// # UPSTREAM-PARITY
1625///
1626/// ```c
1627/// xmlDocPtr xmlCtxtReadFile(xmlParserCtxtPtr ctxt, const char *filename,
1628///                           const char *encoding, int options);
1629/// ```
1630///
1631/// # SAFETY
1632///
1633/// - `ctxt` must be valid pointers (or NULL
1634///   where the upstream C contract allows), obtained from the
1635///   matching constructor/owner and not yet freed; the callee may
1636///   take or keep ownership exactly as the C API specifies.
1637///
1638/// - `filename`, `_encoding` must point to valid NUL-terminated
1639///   strings (or NULL where the C contract allows) for the lifetime
1640///   of the call.
1641///
1642/// The caller must not race this call with concurrent mutation of the
1643/// same objects from other threads (per-object state is not internally
1644/// synchronized). Violating any of the above is undefined behavior.
1645///
1646/// Exercised by the C-API differential courts
1647/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1648/// courts; those pass byte-for-byte against the upstream oracle.
1649#[no_mangle]
1650pub unsafe extern "C" fn xmlCtxtReadFile(
1651    ctxt: *mut _xmlParserCtxt,
1652    filename: *const c_char,
1653    _encoding: *const c_char,
1654    options: c_int,
1655) -> *mut _xmlDoc {
1656    if ctxt.is_null() || filename.is_null() {
1657        return ptr::null_mut();
1658    }
1659    unsafe {
1660        // UPSTREAM-PARITY (parser.c xmlCtxtReadFile -> xmlCtxtNewInputFromUrl
1661        // -> xmlLoadResource): a registered external entity loader governs the
1662        // open; below it the xmlParserInputBufferCreateFilenameDefault (php
1663        // streams loader) is consulted; a NULL loader result raises
1664        // xmlCtxtErrIO(XML_IO_ENOENT, filename) — "I/O warning : failed to
1665        // load \"%s\": %s\n" — and parsing fails.
1666        match open_filename_routed(filename, ctxt) {
1667            RoutedFileOpen::Loaded(input) => ctxt_read_doc(ctxt, input, filename, options),
1668            RoutedFileOpen::Failed => {
1669                emit_io_warning(ctxt, io_load_failure_message(filename));
1670                ptr::null_mut()
1671            }
1672            RoutedFileOpen::EntityLoaderFailed => {
1673                // UPSTREAM-PARITY (parser.c xmlCtxtReadFile): a custom entity
1674                // loader returning NULL fails the read SILENTLY.
1675                ptr::null_mut()
1676            }
1677            RoutedFileOpen::Builtin => match helpers::input_from_file(filename) {
1678                Ok(input) => ctxt_read_doc(ctxt, input, filename, options),
1679                Err(_) => ptr::null_mut(),
1680            },
1681        }
1682    }
1683}
1684
1685/// Parse an XML in-memory block with a given context.
1686///
1687/// # UPSTREAM-PARITY
1688///
1689/// ```c
1690/// xmlDocPtr xmlCtxtReadMemory(xmlParserCtxtPtr ctxt, const char *buffer,
1691///                             int size, const char *URL, const char *encoding,
1692///                             int options);
1693/// ```
1694///
1695/// # SAFETY
1696///
1697/// - `ctxt` must be valid pointers (or NULL
1698///   where the upstream C contract allows), obtained from the
1699///   matching constructor/owner and not yet freed; the callee may
1700///   take or keep ownership exactly as the C API specifies.
1701///
1702/// - `buffer`, `URL`, `_encoding` must point to valid NUL-terminated
1703///   strings (or NULL where the C contract allows) for the lifetime
1704///   of the call.
1705///
1706/// The caller must not race this call with concurrent mutation of the
1707/// same objects from other threads (per-object state is not internally
1708/// synchronized). Violating any of the above is undefined behavior.
1709///
1710/// Exercised by the C-API differential courts
1711/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1712/// courts; those pass byte-for-byte against the upstream oracle.
1713#[no_mangle]
1714pub unsafe extern "C" fn xmlCtxtReadMemory(
1715    ctxt: *mut _xmlParserCtxt,
1716    buffer: *const c_char,
1717    size: c_int,
1718    URL: *const c_char,
1719    _encoding: *const c_char,
1720    options: c_int,
1721) -> *mut _xmlDoc {
1722    if ctxt.is_null() || buffer.is_null() || size < 0 {
1723        return ptr::null_mut();
1724    }
1725    unsafe {
1726        let input = helpers::input_from_memory(buffer, size);
1727        ctxt_read_doc(ctxt, input, URL, options)
1728    }
1729}
1730
1731/// Parse an XML document from a file descriptor with a given context.
1732///
1733/// # UPSTREAM-PARITY
1734///
1735/// ```c
1736/// xmlDocPtr xmlCtxtReadFd(xmlParserCtxtPtr ctxt, int fd, const char *URL,
1737///                         const char *encoding, int options);
1738/// ```
1739///
1740/// # SAFETY
1741///
1742/// - `ctxt` must be valid pointers (or NULL
1743///   where the upstream C contract allows), obtained from the
1744///   matching constructor/owner and not yet freed; the callee may
1745///   take or keep ownership exactly as the C API specifies.
1746///
1747/// - `URL`, `_encoding` must point to valid NUL-terminated
1748///   strings (or NULL where the C contract allows) for the lifetime
1749///   of the call.
1750///
1751/// The caller must not race this call with concurrent mutation of the
1752/// same objects from other threads (per-object state is not internally
1753/// synchronized). Violating any of the above is undefined behavior.
1754///
1755/// Exercised by the C-API differential courts
1756/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1757/// courts; those pass byte-for-byte against the upstream oracle.
1758#[no_mangle]
1759pub unsafe extern "C" fn xmlCtxtReadFd(
1760    ctxt: *mut _xmlParserCtxt,
1761    fd: c_int,
1762    URL: *const c_char,
1763    _encoding: *const c_char,
1764    options: c_int,
1765) -> *mut _xmlDoc {
1766    if ctxt.is_null() || fd < 0 {
1767        return ptr::null_mut();
1768    }
1769    unsafe {
1770        let mut buf = Vec::new();
1771        let mut tmp = [0u8; 4096];
1772        loop {
1773            let n = libc::read(fd, tmp.as_mut_ptr() as *mut c_void, tmp.len());
1774            if n <= 0 {
1775                break;
1776            }
1777            buf.extend_from_slice(&tmp[..n as usize]);
1778        }
1779        let input = helpers::input_from_memory(buf.as_ptr() as *const c_char, buf.len() as c_int);
1780        ctxt_read_doc(ctxt, input, URL, options)
1781    }
1782}
1783
1784/// Parse an XML document from I/O callbacks with a given context.
1785///
1786/// # UPSTREAM-PARITY
1787///
1788/// ```c
1789/// xmlDocPtr xmlCtxtReadIO(xmlParserCtxtPtr ctxt, xmlInputReadCallback ioread,
1790///                         xmlInputCloseCallback ioclose, void *ioctx,
1791///                         const char *URL, const char *encoding, int options);
1792/// ```
1793///
1794/// # SAFETY
1795///
1796/// - `ctxt`, `ioctx` must be valid pointers (or NULL
1797///   where the upstream C contract allows), obtained from the
1798///   matching constructor/owner and not yet freed; the callee may
1799///   take or keep ownership exactly as the C API specifies.
1800///
1801/// - `URL`, `_encoding` must point to valid NUL-terminated
1802///   strings (or NULL where the C contract allows) for the lifetime
1803///   of the call.
1804///
1805/// - `ioread`, `ioclose` must be a valid callback (or None);
1806///   the callback is invoked with the documented context pointer and
1807///   must itself uphold the same pointer invariants.
1808///
1809/// The caller must not race this call with concurrent mutation of the
1810/// same objects from other threads (per-object state is not internally
1811/// synchronized). Violating any of the above is undefined behavior.
1812///
1813/// Exercised by the C-API differential courts
1814/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1815/// courts; those pass byte-for-byte against the upstream oracle.
1816#[no_mangle]
1817pub unsafe extern "C" fn xmlCtxtReadIO(
1818    ctxt: *mut _xmlParserCtxt,
1819    ioread: Option<xmlInputReadCallback>,
1820    ioclose: Option<xmlInputCloseCallback>,
1821    ioctx: *mut c_void,
1822    URL: *const c_char,
1823    _encoding: *const c_char,
1824    options: c_int,
1825) -> *mut _xmlDoc {
1826    if ctxt.is_null() {
1827        return ptr::null_mut();
1828    }
1829    unsafe {
1830        let input = helpers::input_from_io(ioread, ioclose, ioctx);
1831        // UPSTREAM-PARITY (parser.c xmlCtxtNewInputFromIO): the URL becomes
1832        // the input's filename, which feeds the `file:line:` error prefix.
1833        let input = if !URL.is_null() {
1834            match std::ffi::CStr::from_ptr(URL).to_str() {
1835                Ok(s) => input.with_filename(s),
1836                Err(_) => input,
1837            }
1838        } else {
1839            input
1840        };
1841        ctxt_read_doc(ctxt, input, URL, options)
1842    }
1843}
1844
1845/// Parse a document from a raw parser input, taking ownership of `input`.
1846///
1847/// # UPSTREAM-PARITY
1848///
1849/// ```c
1850/// xmlDocPtr xmlCtxtParseDocument(xmlParserCtxtPtr ctxt, xmlParserInputPtr input);
1851/// ```
1852///
1853/// # SAFETY
1854///
1855/// - `ctxt`, `input` must be valid pointers (or NULL
1856///   where the upstream C contract allows), obtained from the
1857///   matching constructor/owner and not yet freed; the callee may
1858///   take or keep ownership exactly as the C API specifies.
1859///
1860/// The caller must not race this call with concurrent mutation of the
1861/// same objects from other threads (per-object state is not internally
1862/// synchronized). Violating any of the above is undefined behavior.
1863///
1864/// Exercised by the C-API differential courts
1865/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1866/// courts; those pass byte-for-byte against the upstream oracle.
1867#[no_mangle]
1868pub unsafe extern "C" fn xmlCtxtParseDocument(
1869    ctxt: *mut _xmlParserCtxt,
1870    input: *mut _xmlParserInput,
1871) -> *mut _xmlDoc {
1872    if ctxt.is_null() || input.is_null() {
1873        return ptr::null_mut();
1874    }
1875    unsafe {
1876        // Determine whether the caller's input is already owned by the
1877        // context's input stack (pushed via xmlPushInput).
1878        let mut owned = false;
1879        let nr = (*ctxt).inputNr;
1880        let tab = (*ctxt).inputTab;
1881        if !tab.is_null() {
1882            for i in 0..nr {
1883                if *tab.add(i as usize) == input {
1884                    owned = true;
1885                    break;
1886                }
1887            }
1888        }
1889        if (*ctxt).input == input {
1890            owned = true;
1891        }
1892
1893        // Copy the data first so the context reset cannot invalidate it.
1894        let ib = input_buffer_from_parser_input(input);
1895
1896        xmlCtxtReset(ctxt);
1897        helpers::setup_parser_input(ctxt, ib);
1898        helpers::parse_document(ctxt);
1899
1900        if !owned {
1901            helpers::free_parser_input(input);
1902        }
1903
1904        (*ctxt).myDoc
1905    }
1906}
1907
1908// ═══════════════════════════════════════════════════════════════════════════════
1909// Parser input buffers / streams
1910// ═══════════════════════════════════════════════════════════════════════════════
1911
1912/// Allocate a parser input buffer for the given encoding.
1913///
1914/// # UPSTREAM-PARITY
1915///
1916/// ```c
1917/// xmlParserInputBufferPtr xmlAllocParserInputBuffer(xmlCharEncoding enc);
1918/// ```
1919///
1920/// # SAFETY
1921///
1922/// The function touches crate-global state only; it is safe
1923/// as long as the caller respects the library's global
1924/// initialization/cleanup ordering (xmlInitParser before use,
1925/// xmlCleanupParser only after all users are done).
1926///
1927/// Violating the global lifecycle ordering, or calling this after
1928/// teardown or from a signal handler, is undefined behavior.
1929#[no_mangle]
1930pub unsafe extern "C" fn xmlAllocParserInputBuffer(enc: c_int) -> *mut _xmlParserInputBuffer {
1931    unsafe {
1932        let buf = xmlMallocZero(core::mem::size_of::<_xmlParserInputBuffer>())
1933            as *mut _xmlParserInputBuffer;
1934        if buf.is_null() {
1935            return ptr::null_mut();
1936        }
1937        let b = &mut *buf;
1938        b.buffer = io::buf_create(crate::abi::data_globals::xmlDefaultBufferSize) as *mut c_void;
1939        b.raw = io::buf_create(crate::abi::data_globals::xmlDefaultBufferSize) as *mut c_void;
1940        if b.buffer.is_null() || b.raw.is_null() {
1941            io::buf_free(b.buffer as *mut _xmlBuffer);
1942            io::buf_free(b.raw as *mut _xmlBuffer);
1943            xmlFreeImpl(buf as *mut c_void);
1944            return ptr::null_mut();
1945        }
1946        b.compressed = -1;
1947
1948        if enc != xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int
1949            && enc != xmlCharEncoding::XML_CHAR_ENCODING_ERROR as c_int
1950        {
1951            let handler = encoding_handler_for(enc);
1952            if !handler.is_null() {
1953                b.encoder = handler as *mut c_void;
1954            }
1955        }
1956        buf
1957    }
1958}
1959
1960/// Grow an input buffer by reading up to `len` bytes from its source.
1961///
1962/// # UPSTREAM-PARITY
1963///
1964/// ```c
1965/// int xmlParserInputBufferGrow(xmlParserInputBufferPtr in, int len);
1966/// ```
1967///
1968/// # SAFETY
1969///
1970/// - `in_` must be valid pointers (or NULL
1971///   where the upstream C contract allows), obtained from the
1972///   matching constructor/owner and not yet freed; the callee may
1973///   take or keep ownership exactly as the C API specifies.
1974///
1975/// The caller must not race this call with concurrent mutation of the
1976/// same objects from other threads (per-object state is not internally
1977/// synchronized). Violating any of the above is undefined behavior.
1978///
1979/// Exercised by the C-API differential courts
1980/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1981/// courts; those pass byte-for-byte against the upstream oracle.
1982#[no_mangle]
1983pub unsafe extern "C" fn xmlParserInputBufferGrow(
1984    in_: *mut _xmlParserInputBuffer,
1985    len: c_int,
1986) -> c_int {
1987    if in_.is_null() || len <= 0 {
1988        return 0;
1989    }
1990    unsafe {
1991        let b = &mut *in_;
1992        if b.error != 0 {
1993            return -1;
1994        }
1995        let Some(read_cb) = b.readcallback else {
1996            // Memory-based buffer: nothing to grow.
1997            return 0;
1998        };
1999        let mut tmp = vec![0u8; len as usize];
2000        let n = read_cb(b.context, tmp.as_mut_ptr() as *mut c_char, len);
2001        if n < 0 {
2002            b.error = 1;
2003            return -1;
2004        }
2005        if n == 0 {
2006            return 0;
2007        }
2008        io::input_buffer_push(in_, tmp.as_ptr() as *const c_char, n);
2009        n
2010    }
2011}
2012
2013/// Push `len` bytes into an input buffer (push parser).
2014///
2015/// # UPSTREAM-PARITY
2016///
2017/// ```c
2018/// int xmlParserInputBufferPush(xmlParserInputBufferPtr in, int len, const char *buf);
2019/// ```
2020///
2021/// # SAFETY
2022///
2023/// - `in_` must be valid pointers (or NULL
2024///   where the upstream C contract allows), obtained from the
2025///   matching constructor/owner and not yet freed; the callee may
2026///   take or keep ownership exactly as the C API specifies.
2027///
2028/// - `buf` must point to valid NUL-terminated
2029///   strings (or NULL where the C contract allows) for the lifetime
2030///   of the call.
2031///
2032/// The caller must not race this call with concurrent mutation of the
2033/// same objects from other threads (per-object state is not internally
2034/// synchronized). Violating any of the above is undefined behavior.
2035///
2036/// Exercised by the C-API differential courts
2037/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2038/// courts; those pass byte-for-byte against the upstream oracle.
2039#[no_mangle]
2040pub unsafe extern "C" fn xmlParserInputBufferPush(
2041    in_: *mut _xmlParserInputBuffer,
2042    len: c_int,
2043    buf: *const c_char,
2044) -> c_int {
2045    if in_.is_null() {
2046        return -1;
2047    }
2048    if len < 0 || (len > 0 && buf.is_null()) {
2049        return -1;
2050    }
2051    if len == 0 {
2052        return 0;
2053    }
2054    io::input_buffer_push(in_, buf, len)
2055}
2056
2057/// Read up to `len` bytes from an input buffer's source.
2058///
2059/// # UPSTREAM-PARITY
2060///
2061/// ```c
2062/// int xmlParserInputBufferRead(xmlParserInputBufferPtr in, int len);
2063/// ```
2064///
2065/// # SAFETY
2066///
2067/// - `in_` must be valid pointers (or NULL
2068///   where the upstream C contract allows), obtained from the
2069///   matching constructor/owner and not yet freed; the callee may
2070///   take or keep ownership exactly as the C API specifies.
2071///
2072/// The caller must not race this call with concurrent mutation of the
2073/// same objects from other threads (per-object state is not internally
2074/// synchronized). Violating any of the above is undefined behavior.
2075///
2076/// Exercised by the C-API differential courts
2077/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2078/// courts; those pass byte-for-byte against the upstream oracle.
2079#[no_mangle]
2080pub unsafe extern "C" fn xmlParserInputBufferRead(
2081    in_: *mut _xmlParserInputBuffer,
2082    len: c_int,
2083) -> c_int {
2084    xmlParserInputBufferGrow(in_, len)
2085}
2086
2087/// Deprecated: reading directly from an input stream is an error.
2088///
2089/// # UPSTREAM-PARITY
2090///
2091/// ```c
2092/// int xmlParserInputRead(xmlParserInputPtr in, int len);
2093/// ```
2094///
2095/// # SAFETY
2096///
2097/// - `_in_` must be valid pointers (or NULL
2098///   where the upstream C contract allows), obtained from the
2099///   matching constructor/owner and not yet freed; the callee may
2100///   take or keep ownership exactly as the C API specifies.
2101///
2102/// The caller must not race this call with concurrent mutation of the
2103/// same objects from other threads (per-object state is not internally
2104/// synchronized). Violating any of the above is undefined behavior.
2105///
2106/// Exercised by the C-API differential courts
2107/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2108/// courts; those pass byte-for-byte against the upstream oracle.
2109#[no_mangle]
2110pub const unsafe extern "C" fn xmlParserInputRead(
2111    _in_: *mut _xmlParserInput,
2112    _len: c_int,
2113) -> c_int {
2114    -1
2115}
2116
2117/// Grow a parser input's buffer by reading more data from its source.
2118///
2119/// # UPSTREAM-PARITY
2120///
2121/// ```c
2122/// int xmlParserInputGrow(xmlParserInputPtr in, int len);
2123/// ```
2124///
2125/// # SAFETY
2126///
2127/// - `in_` must be valid pointers (or NULL
2128///   where the upstream C contract allows), obtained from the
2129///   matching constructor/owner and not yet freed; the callee may
2130///   take or keep ownership exactly as the C API specifies.
2131///
2132/// The caller must not race this call with concurrent mutation of the
2133/// same objects from other threads (per-object state is not internally
2134/// synchronized). Violating any of the above is undefined behavior.
2135///
2136/// Exercised by the C-API differential courts
2137/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2138/// courts; those pass byte-for-byte against the upstream oracle.
2139#[no_mangle]
2140pub unsafe extern "C" fn xmlParserInputGrow(in_: *mut _xmlParserInput, len: c_int) -> c_int {
2141    if in_.is_null() || len < 0 {
2142        return -1;
2143    }
2144    unsafe {
2145        let pi = &*in_;
2146        if pi.base.is_null() || pi.cur.is_null() {
2147            return -1;
2148        }
2149        if pi.buf.is_null() {
2150            // Pure memory input: nothing to grow.
2151            return 0;
2152        }
2153        let b = &*pi.buf;
2154        // Memory buffers are not growable.
2155        if b.readcallback.is_none() && b.encoder.is_null() {
2156            return 0;
2157        }
2158        xmlParserInputBufferGrow(pi.buf, len)
2159    }
2160}
2161
2162/// Shrink a parser input, releasing already-consumed data from the buffer.
2163///
2164/// # UPSTREAM-PARITY
2165///
2166/// ```c
2167/// void xmlParserInputShrink(xmlParserInputPtr in);
2168/// ```
2169///
2170/// # SAFETY
2171///
2172/// - `in_` must be valid pointers (or NULL
2173///   where the upstream C contract allows), obtained from the
2174///   matching constructor/owner and not yet freed; the callee may
2175///   take or keep ownership exactly as the C API specifies.
2176///
2177/// The caller must not race this call with concurrent mutation of the
2178/// same objects from other threads (per-object state is not internally
2179/// synchronized). Violating any of the above is undefined behavior.
2180///
2181/// Exercised by the C-API differential courts
2182/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2183/// courts; those pass byte-for-byte against the upstream oracle.
2184#[no_mangle]
2185pub unsafe extern "C" fn xmlParserInputShrink(in_: *mut _xmlParserInput) {
2186    if in_.is_null() {
2187        return;
2188    }
2189    unsafe {
2190        let pi = &mut *in_;
2191        if pi.buf.is_null() || pi.base.is_null() || pi.cur.is_null() {
2192            return;
2193        }
2194        let used = (pi.cur as usize).saturating_sub(pi.base as usize);
2195        if used > LINE_LEN {
2196            // The candidate's inputs are backed by stable memory buffers, so
2197            // the base pointer cannot move; account for the consumed bytes.
2198            pi.consumed = pi.consumed.saturating_add((used - LINE_LEN) as c_ulong);
2199        }
2200    }
2201}
2202
2203/// Create a new (empty) parser input stream.
2204///
2205/// # UPSTREAM-PARITY
2206///
2207/// ```c
2208/// xmlParserInputPtr xmlNewInputStream(xmlParserCtxtPtr ctxt);
2209/// ```
2210///
2211/// # SAFETY
2212///
2213/// - `ctxt` must be valid pointers (or NULL
2214///   where the upstream C contract allows), obtained from the
2215///   matching constructor/owner and not yet freed; the callee may
2216///   take or keep ownership exactly as the C API specifies.
2217///
2218/// The caller must not race this call with concurrent mutation of the
2219/// same objects from other threads (per-object state is not internally
2220/// synchronized). Violating any of the above is undefined behavior.
2221///
2222/// Exercised by the C-API differential courts
2223/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2224/// courts; those pass byte-for-byte against the upstream oracle.
2225#[no_mangle]
2226pub unsafe extern "C" fn xmlNewInputStream(ctxt: *mut _xmlParserCtxt) -> *mut _xmlParserInput {
2227    unsafe {
2228        let input = xmlMallocZero(core::mem::size_of::<_xmlParserInput>()) as *mut _xmlParserInput;
2229        if input.is_null() {
2230            if !ctxt.is_null() {
2231                xmlCtxtErrMemory(ctxt);
2232            }
2233            return ptr::null_mut();
2234        }
2235        (*input).line = 1;
2236        (*input).col = 1;
2237        input
2238    }
2239}
2240
2241/// Wrap an input buffer in a parser input stream.
2242///
2243/// # UPSTREAM-PARITY
2244///
2245/// ```c
2246/// xmlParserInputPtr xmlNewIOInputStream(xmlParserCtxtPtr ctxt,
2247///                                       xmlParserInputBufferPtr input,
2248///                                       xmlCharEncoding enc);
2249/// ```
2250///
2251/// # SAFETY
2252///
2253/// - `ctxt`, `input` must be valid pointers (or NULL
2254///   where the upstream C contract allows), obtained from the
2255///   matching constructor/owner and not yet freed; the callee may
2256///   take or keep ownership exactly as the C API specifies.
2257///
2258/// The caller must not race this call with concurrent mutation of the
2259/// same objects from other threads (per-object state is not internally
2260/// synchronized). Violating any of the above is undefined behavior.
2261///
2262/// Exercised by the C-API differential courts
2263/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2264/// courts; those pass byte-for-byte against the upstream oracle.
2265#[no_mangle]
2266pub unsafe extern "C" fn xmlNewIOInputStream(
2267    ctxt: *mut _xmlParserCtxt,
2268    input: *mut _xmlParserInputBuffer,
2269    enc: c_int,
2270) -> *mut _xmlParserInput {
2271    if ctxt.is_null() || input.is_null() {
2272        return ptr::null_mut();
2273    }
2274    unsafe {
2275        let pi = xmlNewInputStream(ctxt);
2276        if pi.is_null() {
2277            return ptr::null_mut();
2278        }
2279        (*pi).buf = input;
2280        if enc != xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int
2281            && enc != xmlCharEncoding::XML_CHAR_ENCODING_ERROR as c_int
2282        {
2283            let handler = encoding_handler_for(enc);
2284            if !handler.is_null() {
2285                io::input_buffer_set_encoder(input, handler);
2286            }
2287        }
2288        pi
2289    }
2290}
2291
2292/// Create a parser input stream from a zero-terminated string. The string
2293/// must remain valid for the lifetime of the input (static mode).
2294///
2295/// # UPSTREAM-PARITY
2296///
2297/// ```c
2298/// xmlParserInputPtr xmlNewStringInputStream(xmlParserCtxtPtr ctxt,
2299///                                           const xmlChar *buffer);
2300/// ```
2301///
2302/// # SAFETY
2303///
2304/// - `ctxt` must be valid pointers (or NULL
2305///   where the upstream C contract allows), obtained from the
2306///   matching constructor/owner and not yet freed; the callee may
2307///   take or keep ownership exactly as the C API specifies.
2308///
2309/// - `buffer` must point to valid NUL-terminated
2310///   strings (or NULL where the C contract allows) for the lifetime
2311///   of the call.
2312///
2313/// The caller must not race this call with concurrent mutation of the
2314/// same objects from other threads (per-object state is not internally
2315/// synchronized). Violating any of the above is undefined behavior.
2316///
2317/// Exercised by the C-API differential courts
2318/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2319/// courts; those pass byte-for-byte against the upstream oracle.
2320#[no_mangle]
2321pub unsafe extern "C" fn xmlNewStringInputStream(
2322    ctxt: *mut _xmlParserCtxt,
2323    buffer: *const xmlChar,
2324) -> *mut _xmlParserInput {
2325    if ctxt.is_null() || buffer.is_null() {
2326        return ptr::null_mut();
2327    }
2328    unsafe {
2329        let input = xmlNewInputStream(ctxt);
2330        if input.is_null() {
2331            return ptr::null_mut();
2332        }
2333        let len = string::xml_strlen(buffer);
2334        (*input).base = buffer;
2335        (*input).cur = buffer;
2336        (*input).end = buffer.add(len);
2337        (*input).length = len as c_int;
2338        input
2339    }
2340}
2341
2342/// Setup the parser context to parse a new buffer (legacy API).
2343///
2344/// # UPSTREAM-PARITY
2345///
2346/// ```c
2347/// void xmlSetupParserForBuffer(xmlParserCtxtPtr ctxt, const xmlChar* buffer,
2348///                              const char *filename);
2349/// ```
2350///
2351/// # SAFETY
2352///
2353/// - `ctxt` must be valid pointers (or NULL
2354///   where the upstream C contract allows), obtained from the
2355///   matching constructor/owner and not yet freed; the callee may
2356///   take or keep ownership exactly as the C API specifies.
2357///
2358/// - `buffer`, `filename` must point to valid NUL-terminated
2359///   strings (or NULL where the C contract allows) for the lifetime
2360///   of the call.
2361///
2362/// The caller must not race this call with concurrent mutation of the
2363/// same objects from other threads (per-object state is not internally
2364/// synchronized). Violating any of the above is undefined behavior.
2365///
2366/// Exercised by the C-API differential courts
2367/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2368/// courts; those pass byte-for-byte against the upstream oracle.
2369#[no_mangle]
2370pub unsafe extern "C" fn xmlSetupParserForBuffer(
2371    ctxt: *mut _xmlParserCtxt,
2372    buffer: *const xmlChar,
2373    filename: *const c_char,
2374) {
2375    if ctxt.is_null() || buffer.is_null() {
2376        return;
2377    }
2378    unsafe {
2379        xmlCtxtReset(ctxt);
2380        let len = string::xml_strlen(buffer);
2381        let uri = if filename.is_null() {
2382            None
2383        } else {
2384            CStr::from_ptr(filename).to_str().ok()
2385        };
2386        let input = InputBuffer::from_memory(core::slice::from_raw_parts(buffer, len), uri);
2387        helpers::setup_parser_input(ctxt, input);
2388    }
2389}
2390
2391/// Push an input stream onto the context's input stack.
2392///
2393/// # UPSTREAM-PARITY
2394///
2395/// ```c
2396/// int xmlPushInput(xmlParserCtxtPtr ctxt, xmlParserInputPtr input);
2397/// ```
2398///
2399/// # SAFETY
2400///
2401/// - `ctxt`, `input` must be valid pointers (or NULL
2402///   where the upstream C contract allows), obtained from the
2403///   matching constructor/owner and not yet freed; the callee may
2404///   take or keep ownership exactly as the C API specifies.
2405///
2406/// The caller must not race this call with concurrent mutation of the
2407/// same objects from other threads (per-object state is not internally
2408/// synchronized). Violating any of the above is undefined behavior.
2409///
2410/// Exercised by the C-API differential courts
2411/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2412/// courts; those pass byte-for-byte against the upstream oracle.
2413#[no_mangle]
2414pub unsafe extern "C" fn xmlPushInput(
2415    ctxt: *mut _xmlParserCtxt,
2416    input: *mut _xmlParserInput,
2417) -> c_int {
2418    if ctxt.is_null() || input.is_null() {
2419        return -1;
2420    }
2421    unsafe {
2422        let c = &mut *ctxt;
2423        if c.inputNr >= c.inputMax {
2424            let new_max = if c.inputMax == 0 { 5 } else { c.inputMax * 2 };
2425            let new_tab = xmlReallocImpl(
2426                c.inputTab as *mut c_void,
2427                (new_max as usize) * core::mem::size_of::<*mut _xmlParserInput>(),
2428            ) as *mut *mut _xmlParserInput;
2429            if new_tab.is_null() {
2430                return -1;
2431            }
2432            c.inputTab = new_tab;
2433            c.inputMax = new_max;
2434        }
2435        *c.inputTab.add(c.inputNr as usize) = input;
2436        c.input = input;
2437        (*input).id = c.input_id;
2438        c.input_id += 1;
2439        let idx = c.inputNr;
2440        c.inputNr += 1;
2441        idx
2442    }
2443}
2444
2445/// Pop the top input from the context's input stack and free it; returns the
2446/// current character after the pop (0 at end of input).
2447///
2448/// # UPSTREAM-PARITY
2449///
2450/// ```c
2451/// xmlChar xmlPopInput(xmlParserCtxtPtr ctxt);
2452/// ```
2453///
2454/// # SAFETY
2455///
2456/// - `ctxt` must be valid pointers (or NULL
2457///   where the upstream C contract allows), obtained from the
2458///   matching constructor/owner and not yet freed; the callee may
2459///   take or keep ownership exactly as the C API specifies.
2460///
2461/// The caller must not race this call with concurrent mutation of the
2462/// same objects from other threads (per-object state is not internally
2463/// synchronized). Violating any of the above is undefined behavior.
2464///
2465/// Exercised by the C-API differential courts
2466/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2467/// courts; those pass byte-for-byte against the upstream oracle.
2468#[no_mangle]
2469pub unsafe extern "C" fn xmlPopInput(ctxt: *mut _xmlParserCtxt) -> xmlChar {
2470    if ctxt.is_null() || (*ctxt).inputNr <= 1 {
2471        return 0;
2472    }
2473    unsafe {
2474        let c = &mut *ctxt;
2475        c.inputNr -= 1;
2476        let popped = *c.inputTab.add(c.inputNr as usize);
2477        *c.inputTab.add(c.inputNr as usize) = ptr::null_mut();
2478        if c.inputNr > 0 {
2479            c.input = *c.inputTab.add((c.inputNr - 1) as usize);
2480        } else {
2481            c.input = ptr::null_mut();
2482        }
2483        if !popped.is_null() {
2484            helpers::free_parser_input(popped);
2485        }
2486        if c.input.is_null() {
2487            return 0;
2488        }
2489        let cur = (*c.input).cur;
2490        let end = (*c.input).end;
2491        if cur.is_null() || cur >= end {
2492            0
2493        } else {
2494            *cur
2495        }
2496    }
2497}
2498
2499// ═══════════════════════════════════════════════════════════════════════════════
2500// Encoding switching
2501// ═══════════════════════════════════════════════════════════════════════════════
2502
2503/// Switch the input encoding of the current input.
2504///
2505/// # UPSTREAM-PARITY
2506///
2507/// ```c
2508/// int xmlSwitchEncoding(xmlParserCtxtPtr ctxt, xmlCharEncoding enc);
2509/// ```
2510///
2511/// # SAFETY
2512///
2513/// - `ctxt` must be valid pointers (or NULL
2514///   where the upstream C contract allows), obtained from the
2515///   matching constructor/owner and not yet freed; the callee may
2516///   take or keep ownership exactly as the C API specifies.
2517///
2518/// The caller must not race this call with concurrent mutation of the
2519/// same objects from other threads (per-object state is not internally
2520/// synchronized). Violating any of the above is undefined behavior.
2521///
2522/// Exercised by the C-API differential courts
2523/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2524/// courts; those pass byte-for-byte against the upstream oracle.
2525#[no_mangle]
2526pub unsafe extern "C" fn xmlSwitchEncoding(ctxt: *mut _xmlParserCtxt, enc: c_int) -> c_int {
2527    if ctxt.is_null() || (*ctxt).input.is_null() {
2528        return -1;
2529    }
2530    if enc == xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int {
2531        return 0;
2532    }
2533    unsafe {
2534        let handler = encoding_handler_for(enc);
2535        if handler.is_null() {
2536            return -1;
2537        }
2538        xmlSwitchToEncoding(ctxt, handler)
2539    }
2540}
2541
2542/// Switch the input encoding by name.
2543///
2544/// # UPSTREAM-PARITY
2545///
2546/// ```c
2547/// int xmlSwitchEncodingName(xmlParserCtxtPtr ctxt, const char *encoding);
2548/// ```
2549///
2550/// # SAFETY
2551///
2552/// - `ctxt` must be valid pointers (or NULL
2553///   where the upstream C contract allows), obtained from the
2554///   matching constructor/owner and not yet freed; the callee may
2555///   take or keep ownership exactly as the C API specifies.
2556///
2557/// - `encoding` must point to valid NUL-terminated
2558///   strings (or NULL where the C contract allows) for the lifetime
2559///   of the call.
2560///
2561/// The caller must not race this call with concurrent mutation of the
2562/// same objects from other threads (per-object state is not internally
2563/// synchronized). Violating any of the above is undefined behavior.
2564///
2565/// Exercised by the C-API differential courts
2566/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2567/// courts; those pass byte-for-byte against the upstream oracle.
2568#[no_mangle]
2569pub unsafe extern "C" fn xmlSwitchEncodingName(
2570    ctxt: *mut _xmlParserCtxt,
2571    encoding: *const c_char,
2572) -> c_int {
2573    if ctxt.is_null() || encoding.is_null() {
2574        return -1;
2575    }
2576    unsafe {
2577        let handler = encoding::find_encoding_handler(encoding as *const xmlChar);
2578        if handler.is_null() {
2579            return -1;
2580        }
2581        xmlSwitchToEncoding(ctxt, handler)
2582    }
2583}
2584
2585/// Switch the encoding of a specific parser input using an encoding handler.
2586///
2587/// # UPSTREAM-PARITY
2588///
2589/// ```c
2590/// int xmlSwitchInputEncoding(xmlParserCtxtPtr ctxt, xmlParserInputPtr input,
2591///                            xmlCharEncodingHandlerPtr handler);
2592/// ```
2593///
2594/// # SAFETY
2595///
2596/// - `ctxt`, `input`, `handler` must be valid pointers (or NULL
2597///   where the upstream C contract allows), obtained from the
2598///   matching constructor/owner and not yet freed; the callee may
2599///   take or keep ownership exactly as the C API specifies.
2600///
2601/// The caller must not race this call with concurrent mutation of the
2602/// same objects from other threads (per-object state is not internally
2603/// synchronized). Violating any of the above is undefined behavior.
2604///
2605/// Exercised by the C-API differential courts
2606/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2607/// courts; those pass byte-for-byte against the upstream oracle.
2608#[no_mangle]
2609pub unsafe extern "C" fn xmlSwitchInputEncoding(
2610    ctxt: *mut _xmlParserCtxt,
2611    input: *mut _xmlParserInput,
2612    handler: *mut _xmlCharEncodingHandler,
2613) -> c_int {
2614    let _ = ctxt;
2615    if input.is_null() {
2616        return -1;
2617    }
2618    unsafe {
2619        if (*input).buf.is_null() {
2620            return -1;
2621        }
2622        io::input_buffer_set_encoder((*input).buf, handler);
2623    }
2624    0
2625}
2626
2627/// Switch the encoding of the current input using an encoding handler.
2628///
2629/// # UPSTREAM-PARITY
2630///
2631/// ```c
2632/// int xmlSwitchToEncoding(xmlParserCtxtPtr ctxt,
2633///                         xmlCharEncodingHandlerPtr handler);
2634/// ```
2635///
2636/// # SAFETY
2637///
2638/// - `ctxt`, `handler` must be valid pointers (or NULL
2639///   where the upstream C contract allows), obtained from the
2640///   matching constructor/owner and not yet freed; the callee may
2641///   take or keep ownership exactly as the C API specifies.
2642///
2643/// The caller must not race this call with concurrent mutation of the
2644/// same objects from other threads (per-object state is not internally
2645/// synchronized). Violating any of the above is undefined behavior.
2646///
2647/// Exercised by the C-API differential courts
2648/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2649/// courts; those pass byte-for-byte against the upstream oracle.
2650#[no_mangle]
2651pub unsafe extern "C" fn xmlSwitchToEncoding(
2652    ctxt: *mut _xmlParserCtxt,
2653    handler: *mut _xmlCharEncodingHandler,
2654) -> c_int {
2655    if ctxt.is_null() {
2656        return -1;
2657    }
2658    unsafe {
2659        let input = (*ctxt).input;
2660        if input.is_null() {
2661            return -1;
2662        }
2663        // Memory-parser inputs (xmlCreateMemoryParserCtxt) carry buf == NULL;
2664        // their bytes live in the Rust-side InputBuffer (helpers.rs side
2665        // table), which already transcoded any BOM/declared encoding. A
2666        // caller-driven switch (PHP dom overrideEncoding) must therefore
2667        // transcode the whole buffered stream there (upstream applies the
2668        // input-buffer encoder before any read).
2669        if (*input).buf.is_null() && !(*handler).name.is_null() {
2670            return helpers::apply_memory_encoding_override(ctxt, (*handler).name);
2671        }
2672        io::input_buffer_set_encoder((*input).buf, handler);
2673    }
2674    0
2675}
2676
2677// ═══════════════════════════════════════════════════════════════════════════════
2678// Node info sequence (deprecated, parser.h)
2679// ═══════════════════════════════════════════════════════════════════════════════
2680
2681/// Initialise a node info sequence.
2682///
2683/// # UPSTREAM-PARITY
2684///
2685/// ```c
2686/// void xmlInitNodeInfoSeq(xmlParserNodeInfoSeqPtr seq);
2687/// ```
2688///
2689/// # SAFETY
2690///
2691/// - `seq` must be valid pointers (or NULL
2692///   where the upstream C contract allows), obtained from the
2693///   matching constructor/owner and not yet freed; the callee may
2694///   take or keep ownership exactly as the C API specifies.
2695///
2696/// The caller must not race this call with concurrent mutation of the
2697/// same objects from other threads (per-object state is not internally
2698/// synchronized). Violating any of the above is undefined behavior.
2699///
2700/// Exercised by the C-API differential courts
2701/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2702/// courts; those pass byte-for-byte against the upstream oracle.
2703#[no_mangle]
2704pub unsafe extern "C" fn xmlInitNodeInfoSeq(seq: *mut _xmlParserNodeInfoSeq) {
2705    if seq.is_null() {
2706        return;
2707    }
2708    unsafe {
2709        (*seq).length = 0;
2710        (*seq).maximum = 0;
2711        (*seq).buffer = ptr::null_mut();
2712    }
2713}
2714
2715/// Clear (release and reinitialise) a node info sequence.
2716///
2717/// # UPSTREAM-PARITY
2718///
2719/// ```c
2720/// void xmlClearNodeInfoSeq(xmlParserNodeInfoSeqPtr seq);
2721/// ```
2722///
2723/// # SAFETY
2724///
2725/// - `seq` must be valid pointers (or NULL
2726///   where the upstream C contract allows), obtained from the
2727///   matching constructor/owner and not yet freed; the callee may
2728///   take or keep ownership exactly as the C API specifies.
2729///
2730/// The caller must not race this call with concurrent mutation of the
2731/// same objects from other threads (per-object state is not internally
2732/// synchronized). Violating any of the above is undefined behavior.
2733///
2734/// Exercised by the C-API differential courts
2735/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2736/// courts; those pass byte-for-byte against the upstream oracle.
2737#[no_mangle]
2738pub unsafe extern "C" fn xmlClearNodeInfoSeq(seq: *mut _xmlParserNodeInfoSeq) {
2739    if seq.is_null() {
2740        return;
2741    }
2742    unsafe {
2743        if !(*seq).buffer.is_null() {
2744            xmlFreeImpl((*seq).buffer as *mut c_void);
2745        }
2746        xmlInitNodeInfoSeq(seq);
2747    }
2748}
2749
2750/// Find the index where the info record for `node` is (or should be) in the
2751/// sorted sequence; binary search by node pointer.
2752///
2753/// # UPSTREAM-PARITY
2754///
2755/// ```c
2756/// unsigned long xmlParserFindNodeInfoIndex(xmlParserNodeInfoSeqPtr seq,
2757///                                          xmlNodePtr node);
2758/// ```
2759///
2760/// # SAFETY
2761///
2762/// - `seq`, `node` must be valid pointers (or NULL
2763///   where the upstream C contract allows), obtained from the
2764///   matching constructor/owner and not yet freed; the callee may
2765///   take or keep ownership exactly as the C API specifies.
2766///
2767/// The caller must not race this call with concurrent mutation of the
2768/// same objects from other threads (per-object state is not internally
2769/// synchronized). Violating any of the above is undefined behavior.
2770///
2771/// Exercised by the C-API differential courts
2772/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2773/// courts; those pass byte-for-byte against the upstream oracle.
2774#[no_mangle]
2775pub unsafe extern "C" fn xmlParserFindNodeInfoIndex(
2776    seq: *mut _xmlParserNodeInfoSeq,
2777    node: *mut _xmlNode,
2778) -> c_ulong {
2779    if seq.is_null() || node.is_null() {
2780        return c_ulong::MAX;
2781    }
2782    unsafe {
2783        let s = &*seq;
2784        if s.buffer.is_null() || s.length == 0 {
2785            return 0;
2786        }
2787        let mut lower: usize = 0;
2788        let mut upper: usize = s.length as usize;
2789        while lower < upper {
2790            let middle = lower + (upper - lower) / 2;
2791            let cur_node = (*s.buffer.add(middle)).node;
2792            if cur_node == node {
2793                return middle as c_ulong;
2794            }
2795            if (cur_node as usize) < (node as usize) {
2796                lower = middle + 1;
2797            } else {
2798                upper = middle;
2799            }
2800        }
2801        lower as c_ulong
2802    }
2803}
2804
2805/// Find the node info record for a given node, or NULL.
2806///
2807/// # UPSTREAM-PARITY
2808///
2809/// ```c
2810/// const xmlParserNodeInfo *xmlParserFindNodeInfo(xmlParserCtxtPtr ctxt,
2811///                                                xmlNodePtr node);
2812/// ```
2813///
2814/// # SAFETY
2815///
2816/// - `ctxt`, `node` must be valid pointers (or NULL
2817///   where the upstream C contract allows), obtained from the
2818///   matching constructor/owner and not yet freed; the callee may
2819///   take or keep ownership exactly as the C API specifies.
2820///
2821/// The caller must not race this call with concurrent mutation of the
2822/// same objects from other threads (per-object state is not internally
2823/// synchronized). Violating any of the above is undefined behavior.
2824///
2825/// Exercised by the C-API differential courts
2826/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2827/// courts; those pass byte-for-byte against the upstream oracle.
2828#[no_mangle]
2829pub unsafe extern "C" fn xmlParserFindNodeInfo(
2830    ctxt: *mut _xmlParserCtxt,
2831    node: *mut _xmlNode,
2832) -> *const _xmlParserNodeInfo {
2833    if ctxt.is_null() || node.is_null() {
2834        return ptr::null();
2835    }
2836    unsafe {
2837        let seq = &(*ctxt).node_seq;
2838        let seq_mut = seq as *const _ as *mut _xmlParserNodeInfoSeq;
2839        let pos = xmlParserFindNodeInfoIndex(seq_mut, node);
2840        if !seq.buffer.is_null() && (pos as usize) < (seq.length as usize) {
2841            let info = &*seq.buffer.add(pos as usize);
2842            if info.node == node {
2843                return info;
2844            }
2845        }
2846        ptr::null()
2847    }
2848}
2849
2850/// Insert a node info record into the context's sorted sequence.
2851///
2852/// # UPSTREAM-PARITY
2853///
2854/// ```c
2855/// void xmlParserAddNodeInfo(xmlParserCtxtPtr ctxt, xmlParserNodeInfoPtr info);
2856/// ```
2857///
2858/// # SAFETY
2859///
2860/// - `ctxt`, `info` must be valid pointers (or NULL
2861///   where the upstream C contract allows), obtained from the
2862///   matching constructor/owner and not yet freed; the callee may
2863///   take or keep ownership exactly as the C API specifies.
2864///
2865/// The caller must not race this call with concurrent mutation of the
2866/// same objects from other threads (per-object state is not internally
2867/// synchronized). Violating any of the above is undefined behavior.
2868///
2869/// Exercised by the C-API differential courts
2870/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2871/// courts; those pass byte-for-byte against the upstream oracle.
2872#[no_mangle]
2873pub unsafe extern "C" fn xmlParserAddNodeInfo(
2874    ctxt: *mut _xmlParserCtxt,
2875    info: *mut _xmlParserNodeInfo,
2876) {
2877    if ctxt.is_null() || info.is_null() {
2878        return;
2879    }
2880    unsafe {
2881        let seq = &mut (*ctxt).node_seq;
2882        let node = (*info).node;
2883        let pos = xmlParserFindNodeInfoIndex(seq, node as *mut _xmlNode) as usize;
2884
2885        if pos < seq.length as usize && !seq.buffer.is_null() && (*seq.buffer.add(pos)).node == node
2886        {
2887            // Node already recorded: update the record in place.
2888            ptr::copy_nonoverlapping(info, seq.buffer.add(pos), 1);
2889            return;
2890        }
2891
2892        // Grow the buffer (upstream xmlGrowCapacity: 50% growth from a
2893        // minimum of 4, capped at XML_MAX_ITEMS = 1 billion).
2894        if seq.length + 1 > seq.maximum {
2895            let new_max = xml_grow_capacity(seq.maximum);
2896            if new_max < 0 {
2897                xmlCtxtErrMemory(ctxt);
2898                return;
2899            }
2900            let new_buf = xmlReallocImpl(
2901                seq.buffer as *mut c_void,
2902                (new_max as usize) * core::mem::size_of::<_xmlParserNodeInfo>(),
2903            ) as *mut _xmlParserNodeInfo;
2904            if new_buf.is_null() {
2905                xmlCtxtErrMemory(ctxt);
2906                return;
2907            }
2908            seq.buffer = new_buf;
2909            seq.maximum = new_max as c_ulong;
2910        }
2911
2912        // Shift elements right to make room at `pos`.
2913        let length = seq.length as usize;
2914        for i in (pos + 1..=length).rev() {
2915            ptr::copy_nonoverlapping(seq.buffer.add(i - 1), seq.buffer.add(i), 1);
2916        }
2917        ptr::copy_nonoverlapping(info, seq.buffer.add(pos), 1);
2918        seq.length += 1;
2919    }
2920}
2921
2922/// Upstream `xmlGrowCapacity` (private/memory.h) for a zero-based capacity:
2923/// 50% growth, minimum initial allocation 4, capped at XML_MAX_ITEMS.
2924/// Returns the new capacity or -1 on overflow/cap exhaustion.
2925// The `as u64` casts are width-correcting for 32-bit platforms where
2926// `c_ulong` is 32 bits; on x86-64 they are identity casts.
2927#[allow(clippy::unnecessary_cast)]
2928const unsafe fn xml_grow_capacity(capacity: c_ulong) -> c_int {
2929    const XML_MAX_ITEMS: u64 = 1_000_000_000;
2930    const ELEM_SIZE: usize = core::mem::size_of::<_xmlParserNodeInfo>();
2931    if capacity == 0 {
2932        return 4;
2933    }
2934    if capacity as u64 >= XML_MAX_ITEMS || (capacity as usize) > usize::MAX / 2 / ELEM_SIZE {
2935        return -1;
2936    }
2937    let extra = capacity.div_ceil(2);
2938    if capacity as u64 > XML_MAX_ITEMS - extra as u64 {
2939        return XML_MAX_ITEMS as c_int;
2940    }
2941    (capacity + extra) as c_int
2942}
2943
2944// ═══════════════════════════════════════════════════════════════════════════════
2945// I/O callback registration (xmlIO.h)
2946// ═══════════════════════════════════════════════════════════════════════════════
2947
2948/// Register a new set of input I/O callbacks.
2949///
2950/// # UPSTREAM-PARITY
2951///
2952/// ```c
2953/// int xmlRegisterInputCallbacks(xmlInputMatchCallback matchFunc,
2954///                               xmlInputOpenCallback openFunc,
2955///                               xmlInputReadCallback readFunc,
2956///                               xmlInputCloseCallback closeFunc);
2957/// ```
2958///
2959/// # SAFETY
2960///
2961///
2962/// - `matchFunc`, `openFunc`, `readFunc`, `closeFunc` must be a valid callback (or None);
2963///   the callback is invoked with the documented context pointer and
2964///   must itself uphold the same pointer invariants.
2965///
2966/// The caller must not race this call with concurrent mutation of the
2967/// same objects from other threads (per-object state is not internally
2968/// synchronized). Violating any of the above is undefined behavior.
2969///
2970/// Exercised by the C-API differential courts
2971/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2972/// courts; those pass byte-for-byte against the upstream oracle.
2973#[no_mangle]
2974pub unsafe extern "C" fn xmlRegisterInputCallbacks(
2975    matchFunc: Option<xmlInputMatchCallback>,
2976    openFunc: Option<xmlInputOpenCallback>,
2977    readFunc: Option<xmlInputReadCallback>,
2978    closeFunc: Option<xmlInputCloseCallback>,
2979) -> c_int {
2980    unsafe {
2981        globals::init_parser();
2982    }
2983    let mut table = INPUT_CALLBACKS.lock();
2984    if table.len() >= 10 {
2985        return -1;
2986    }
2987    table.push(InputCallbackEntry {
2988        matchcb: matchFunc,
2989        opencb: openFunc,
2990        readcb: readFunc,
2991        closecb: closeFunc,
2992    });
2993    (table.len() - 1) as c_int
2994}
2995
2996/// Register the default compiled-in input callbacks (the `xmlFile*` pair).
2997///
2998/// # UPSTREAM-PARITY
2999///
3000/// ```c
3001/// void xmlRegisterDefaultInputCallbacks(void);
3002/// ```
3003///
3004/// # SAFETY
3005///
3006/// The function touches crate-global state only; it is safe
3007/// as long as the caller respects the library's global
3008/// initialization/cleanup ordering (xmlInitParser before use,
3009/// xmlCleanupParser only after all users are done).
3010///
3011/// Violating the global lifecycle ordering, or calling this after
3012/// teardown or from a signal handler, is undefined behavior.
3013#[no_mangle]
3014pub unsafe extern "C" fn xmlRegisterDefaultInputCallbacks() {
3015    unsafe {
3016        xmlRegisterInputCallbacks(
3017            Some(xmlFileMatch),
3018            Some(xmlFileOpen),
3019            Some(xmlFileRead),
3020            Some(xmlFileClose),
3021        );
3022    }
3023}
3024
3025/// Remove the top input callback from the stack.
3026///
3027/// # UPSTREAM-PARITY
3028///
3029/// ```c
3030/// int xmlPopInputCallbacks(void);
3031/// ```
3032///
3033/// # SAFETY
3034///
3035/// The function touches crate-global state only; it is safe
3036/// as long as the caller respects the library's global
3037/// initialization/cleanup ordering (xmlInitParser before use,
3038/// xmlCleanupParser only after all users are done).
3039///
3040/// Violating the global lifecycle ordering, or calling this after
3041/// teardown or from a signal handler, is undefined behavior.
3042#[no_mangle]
3043pub unsafe extern "C" fn xmlPopInputCallbacks() -> c_int {
3044    unsafe {
3045        globals::init_parser();
3046    }
3047    let mut table = INPUT_CALLBACKS.lock();
3048    if table.is_empty() {
3049        return -1;
3050    }
3051    table.pop();
3052    table.len() as c_int
3053}
3054
3055/// Clear the entire input callback table.
3056///
3057/// # UPSTREAM-PARITY
3058///
3059/// ```c
3060/// void xmlCleanupInputCallbacks(void);
3061/// ```
3062///
3063/// # SAFETY
3064///
3065/// The function touches crate-global state only; it is safe
3066/// as long as the caller respects the library's global
3067/// initialization/cleanup ordering (xmlInitParser before use,
3068/// xmlCleanupParser only after all users are done).
3069///
3070/// Violating the global lifecycle ordering, or calling this after
3071/// teardown or from a signal handler, is undefined behavior.
3072#[no_mangle]
3073pub unsafe extern "C" fn xmlCleanupInputCallbacks() {
3074    unsafe {
3075        globals::init_parser();
3076    }
3077    INPUT_CALLBACKS.lock().clear();
3078}
3079
3080/// Read a URI through the registered input callbacks (upstream
3081/// `xmlParserInputBufferCreateFilename`): the first registered pair whose
3082/// match callback accepts the URI is opened, read to EOF, and closed.
3083/// Returns `None` when no registered pair matches — callers fall back to
3084/// the regular file path. NULL callbacks inside a matching pair are treated
3085/// like upstream (an entry whose match callback is NULL is skipped).
3086///
3087/// Used by the XInclude loader so custom I/O schemes registered through
3088/// `xmlRegisterInputCallbacks` are honored (upstream xmlXIncludeLoadDoc →
3089/// xmlNewInputFromFile; Phase-12 EXTERNAL-CONSUMERS court: io1.c registers
3090/// an sql: scheme and XInclude hrefs route through it).
3091///
3092/// # SAFETY
3093///
3094/// - `uri` must be a valid NUL-terminated C string live for the call.
3095pub(crate) unsafe fn read_uri_via_input_callbacks(uri: *const c_char) -> Option<Vec<u8>> {
3096    let table = INPUT_CALLBACKS.lock();
3097    for e in table.iter() {
3098        let Some(matchcb) = e.matchcb else {
3099            continue;
3100        };
3101        // SAFETY: callbacks were registered by the caller and must uphold
3102        // the xmlInput*Callback contracts.
3103        if unsafe { matchcb(uri) } == 0 {
3104            continue;
3105        }
3106        let (Some(opencb), Some(readcb)) = (e.opencb, e.readcb) else {
3107            return None;
3108        };
3109        // SAFETY: the open callback returns a context for read/close.
3110        let ctx = unsafe { opencb(uri) };
3111        if ctx.is_null() {
3112            return None;
3113        }
3114        let mut data = Vec::new();
3115        let mut buf = [0u8; 4096];
3116        loop {
3117            // SAFETY: readcb fills `buf` per the xmlInputReadCallback contract.
3118            let n = unsafe { readcb(ctx, buf.as_mut_ptr() as *mut c_char, buf.len() as c_int) };
3119            if n < 0 {
3120                if let Some(closecb) = e.closecb {
3121                    unsafe { closecb(ctx) };
3122                }
3123                return None;
3124            }
3125            if n == 0 {
3126                break;
3127            }
3128            data.extend_from_slice(&buf[..n as usize]);
3129        }
3130        if let Some(closecb) = e.closecb {
3131            unsafe { closecb(ctx) };
3132        }
3133        return Some(data);
3134    }
3135    None
3136}
3137
3138/// Register a new set of output I/O callbacks.
3139///
3140/// # UPSTREAM-PARITY
3141///
3142/// ```c
3143/// int xmlRegisterOutputCallbacks(xmlOutputMatchCallback matchFunc,
3144///                                xmlOutputOpenCallback openFunc,
3145///                                xmlOutputWriteCallback writeFunc,
3146///                                xmlOutputCloseCallback closeFunc);
3147/// ```
3148///
3149/// # SAFETY
3150///
3151///
3152/// - `matchFunc`, `openFunc`, `writeFunc`, `closeFunc` must be a valid callback (or None);
3153///   the callback is invoked with the documented context pointer and
3154///   must itself uphold the same pointer invariants.
3155///
3156/// The caller must not race this call with concurrent mutation of the
3157/// same objects from other threads (per-object state is not internally
3158/// synchronized). Violating any of the above is undefined behavior.
3159///
3160/// Exercised by the C-API differential courts
3161/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3162/// courts; those pass byte-for-byte against the upstream oracle.
3163#[no_mangle]
3164pub unsafe extern "C" fn xmlRegisterOutputCallbacks(
3165    matchFunc: Option<xmlOutputMatchCallback>,
3166    openFunc: Option<xmlOutputOpenCallback>,
3167    writeFunc: Option<xmlOutputWriteCallback>,
3168    closeFunc: Option<xmlOutputCloseCallback>,
3169) -> c_int {
3170    unsafe {
3171        globals::init_parser();
3172    }
3173    let mut table = OUTPUT_CALLBACKS.lock();
3174    if table.len() >= 10 {
3175        return -1;
3176    }
3177    table.push(OutputCallbackEntry {
3178        matchcb: matchFunc,
3179        opencb: openFunc,
3180        writecb: writeFunc,
3181        closecb: closeFunc,
3182    });
3183    (table.len() - 1) as c_int
3184}
3185
3186/// Register the default compiled-in output callbacks.
3187///
3188/// # UPSTREAM-PARITY
3189///
3190/// ```c
3191/// void xmlRegisterDefaultOutputCallbacks(void);
3192/// ```
3193///
3194/// # SAFETY
3195///
3196/// The function touches crate-global state only; it is safe
3197/// as long as the caller respects the library's global
3198/// initialization/cleanup ordering (xmlInitParser before use,
3199/// xmlCleanupParser only after all users are done).
3200///
3201/// Violating the global lifecycle ordering, or calling this after
3202/// teardown or from a signal handler, is undefined behavior.
3203#[no_mangle]
3204pub unsafe extern "C" fn xmlRegisterDefaultOutputCallbacks() {
3205    unsafe {
3206        xmlRegisterOutputCallbacks(Some(xmlFileMatch), None, None, None);
3207    }
3208}
3209
3210/// Register the HTTP POST output callbacks (upstream: default output callbacks).
3211///
3212/// # UPSTREAM-PARITY
3213///
3214/// ```c
3215/// void xmlRegisterHTTPPostCallbacks(void);
3216/// ```
3217///
3218/// # SAFETY
3219///
3220/// The function touches crate-global state only; it is safe
3221/// as long as the caller respects the library's global
3222/// initialization/cleanup ordering (xmlInitParser before use,
3223/// xmlCleanupParser only after all users are done).
3224///
3225/// Violating the global lifecycle ordering, or calling this after
3226/// teardown or from a signal handler, is undefined behavior.
3227#[no_mangle]
3228pub unsafe extern "C" fn xmlRegisterHTTPPostCallbacks() {
3229    unsafe { xmlRegisterDefaultOutputCallbacks() }
3230}
3231
3232/// Remove the top output callback from the stack.
3233///
3234/// # UPSTREAM-PARITY
3235///
3236/// ```c
3237/// int xmlPopOutputCallbacks(void);
3238/// ```
3239///
3240/// # SAFETY
3241///
3242/// The function touches crate-global state only; it is safe
3243/// as long as the caller respects the library's global
3244/// initialization/cleanup ordering (xmlInitParser before use,
3245/// xmlCleanupParser only after all users are done).
3246///
3247/// Violating the global lifecycle ordering, or calling this after
3248/// teardown or from a signal handler, is undefined behavior.
3249#[no_mangle]
3250pub unsafe extern "C" fn xmlPopOutputCallbacks() -> c_int {
3251    unsafe {
3252        globals::init_parser();
3253    }
3254    let mut table = OUTPUT_CALLBACKS.lock();
3255    if table.is_empty() {
3256        return -1;
3257    }
3258    table.pop();
3259    table.len() as c_int
3260}
3261
3262/// Clear the entire output callback table.
3263///
3264/// # UPSTREAM-PARITY
3265///
3266/// ```c
3267/// void xmlCleanupOutputCallbacks(void);
3268/// ```
3269///
3270/// # SAFETY
3271///
3272/// The function touches crate-global state only; it is safe
3273/// as long as the caller respects the library's global
3274/// initialization/cleanup ordering (xmlInitParser before use,
3275/// xmlCleanupParser only after all users are done).
3276///
3277/// Violating the global lifecycle ordering, or calling this after
3278/// teardown or from a signal handler, is undefined behavior.
3279#[no_mangle]
3280pub unsafe extern "C" fn xmlCleanupOutputCallbacks() {
3281    unsafe {
3282        globals::init_parser();
3283    }
3284    OUTPUT_CALLBACKS.lock().clear();
3285}
3286
3287// ═══════════════════════════════════════════════════════════════════════════════
3288// External entity loaders (parser.h)
3289// ═══════════════════════════════════════════════════════════════════════════════
3290
3291/// Default external entity loader: resolve `url` against the filesystem,
3292/// honouring XML_PARSE_NONET.
3293///
3294/// # Safety
3295///
3296/// `url`/`publicId` must be valid C strings or NULL; `ctxt` may be NULL.
3297unsafe extern "C" fn default_external_entity_loader(
3298    url: *const c_char,
3299    public_id: *const c_char,
3300    ctxt: *mut _xmlParserCtxt,
3301) -> *mut _xmlParserInput {
3302    let _ = public_id;
3303    if url.is_null() {
3304        return ptr::null_mut();
3305    }
3306    unsafe {
3307        // Refuse network access when NONET is set.
3308        if !ctxt.is_null() && (*ctxt).options & XML_PARSE_NONET != 0 {
3309            let len = libc::strlen(url);
3310            if len >= 7 && libc::strncasecmp(url, c"http://".as_ptr() as *const c_char, 7) == 0 {
3311                return ptr::null_mut();
3312            }
3313        }
3314        // UPSTREAM-PARITY (parserInternals.c xmlDefaultExternalEntityLoader
3315        // -> xmlNewInputFromFile -> xmlNewInputFromUrl): the registered
3316        // xmlParserInputBufferCreateFilenameDefault (php streams loader) is
3317        // consulted BEFORE the input-callback table and the built-in open. A
3318        // NULL loader result is XML_IO_ENOENT — xmlCtxtErrIO is raised and
3319        // there is no fallback.
3320        if globals::get_parser_input_buffer_create_filename_value_cross_dso().is_some() {
3321            // SAFETY: url is a valid NUL-terminated C string for the call.
3322            return match call_loader_materialize(url) {
3323                Err(()) => {
3324                    emit_io_warning(ctxt, io_load_failure_message(url));
3325                    ptr::null_mut()
3326                }
3327                Ok(data) => {
3328                    // Build a MEMORY-backed C input: the entity machinery
3329                    // consumes the loader result through base/end (upstream
3330                    // buffers the external entity content the same way). A
3331                    // zero-length result (php://memory, 0-byte file) is a
3332                    // VALID empty input — the parse reports "Document is
3333                    // empty" (php DOM createFromFile).
3334                    let mem = if data.is_empty() {
3335                        crate::xml::io::input_buffer_create_empty()
3336                    } else {
3337                        io::input_buffer_create_mem(
3338                            data.as_ptr() as *const c_char,
3339                            data.len() as c_int,
3340                            xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int,
3341                        )
3342                    };
3343                    if mem.is_null() {
3344                        return ptr::null_mut();
3345                    }
3346                    parser_input_from_buf(mem)
3347                }
3348            };
3349        }
3350        // Try the registered input callbacks first.
3351        let table = INPUT_CALLBACKS.lock();
3352        for entry in table.iter() {
3353            if let (Some(match_cb), Some(open_cb)) = (entry.matchcb, entry.opencb) {
3354                if match_cb(url) != 0 {
3355                    let ctx = open_cb(url);
3356                    if !ctx.is_null() {
3357                        let buf = helpers::alloc_parser_input_buffer();
3358                        if buf.is_null() {
3359                            if let Some(close_cb) = entry.closecb {
3360                                close_cb(ctx);
3361                            }
3362                            return ptr::null_mut();
3363                        }
3364                        (*buf).context = ctx;
3365                        (*buf).readcallback = entry.readcb;
3366                        (*buf).closecallback = entry.closecb;
3367                        return parser_input_from_buf(buf);
3368                    }
3369                }
3370            }
3371        }
3372
3373        // Fall back to a plain file open.
3374        let buf =
3375            io::input_buffer_create_file(url, xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int);
3376        if buf.is_null() {
3377            // UPSTREAM-PARITY (parserInternals.c xmlNewInputFromFile): a
3378            // failed load raises xmlCtxtErrIO(ctxt, XML_IO_ENOENT, url) —
3379            // "I/O warning : failed to load \"%s\": %s\n" with the
3380            // strerror text (HOSTILE-FAILURE F7).
3381            let errno = *libc::__errno_location();
3382            let errstr = if errno == 0 {
3383                String::new()
3384            } else {
3385                std::ffi::CStr::from_ptr(libc::strerror(errno))
3386                    .to_string_lossy()
3387                    .into_owned()
3388            };
3389            let url_str = std::ffi::CStr::from_ptr(url).to_string_lossy();
3390            emit_io_warning(ctxt, format!("failed to load \"{url_str}\": {errstr}\n"));
3391            return ptr::null_mut();
3392        }
3393        parser_input_from_buf(buf)
3394    }
3395}
3396
3397/// UPSTREAM-PARITY (parserInternals.c xmlCtxtErrIO): raise an I/O warning
3398/// (XML_FROM_IO, XML_IO_ENOENT, XML_ERR_WARNING) through the parser's
3399/// channel — "I/O warning : <message>".
3400pub(crate) unsafe fn emit_io_warning(ctxt: *mut _xmlParserCtxt, message: String) {
3401    let msg_c = std::ffi::CString::new(message).unwrap_or_default();
3402    let delivery = if ctxt.is_null() {
3403        crate::xml::errors::GenericDelivery::Stream
3404    } else {
3405        unsafe { crate::xml::errors::parser_delivery(ctxt) }
3406    };
3407    unsafe {
3408        crate::xml::errors::raise_error_streamed(
3409            ctxt as *mut c_void,
3410            crate::abi::types::XML_FROM_IO,
3411            crate::abi::types::XML_IO_ENOENT,
3412            crate::abi::types::xmlErrorLevel::XML_ERR_WARNING as c_int,
3413            ptr::null(),
3414            0,
3415            0,
3416            ptr::null(),
3417            ptr::null(),
3418            ptr::null(),
3419            0,
3420            msg_c.as_ptr(),
3421            None,
3422            None,
3423            delivery,
3424            None,
3425        );
3426    }
3427}
3428
3429/// Result of routing a filename open through the registered loaders
3430/// (upstream 2.14+ `xmlLoadResource` layering).
3431#[allow(dead_code)]
3432pub(crate) enum RoutedFileOpen {
3433    /// No custom loader is registered — the caller falls back to the built-in
3434    /// file open (`helpers::input_from_file`).
3435    Builtin,
3436    /// A registered loader returned NULL: upstream reports `XML_IO_ENOENT`
3437    /// with NO built-in fallback (php streams loader: missing file, percent-
3438    /// encoded-NUL guard, disabled entity loader).
3439    Failed,
3440    /// A registered EXTERNAL ENTITY loader (`xmlSetExternalEntityLoader`)
3441    /// returned NULL for a file/URL open. Upstream `xmlCtxtNewInputFromUrl`
3442    /// propagates that NULL silently (no `xmlCtxtErrIO` — the custom loader
3443    /// owns its own error reporting), so callers fail without a warning.
3444    EntityLoaderFailed,
3445    /// The loader produced an input buffer whose bytes were materialized
3446    /// (filename = the original URI).
3447    Loaded(InputBuffer),
3448}
3449
3450/// UPSTREAM-PARITY (parserInternals.c `xmlNewInputFromUrl`): when a custom
3451/// `xmlParserInputBufferCreateFilenameDefault` is registered (PHP installs
3452/// its streams loader at request init), filename opens consult it FIRST —
3453/// php streams unescape `file://` URIs, enforce the percent-encoded-NUL
3454/// guard, honor stream contexts and emit their own failure warnings. A NULL
3455/// loader result is `XML_IO_ENOENT`; upstream does NOT fall back to the
3456/// built-in open in that case. Without a registered loader the caller keeps
3457/// the built-in path.
3458///
3459/// Invoke the registered loader and materialize the produced buffer's bytes
3460/// through its read callback, releasing the C buffer/stream exactly once
3461/// (the close callback runs when the buffer is freed). Returns `Err(())` on
3462/// a NULL loader result or a read-callback error.
3463///
3464/// # Safety
3465///
3466/// - `uri` must be a valid NUL-terminated C string live for the call; the
3467///   registered loader callback (if any) must uphold the
3468///   `xmlParserInputBufferCreateFilenameFunc` contract.
3469pub(crate) unsafe fn call_loader_materialize(uri: *const c_char) -> Result<Vec<u8>, ()> {
3470    // SAFETY: reads the per-thread loader slot — through the R-000177
3471    // cross-DSO bridge so the whole-archive facade copies observe the
3472    // loader a consumer registered via the core DSO's exported setter
3473    // (upstream: single core DSO, registration visible everywhere).
3474    let Some(func) = globals::get_parser_input_buffer_create_filename_value_cross_dso() else {
3475        return Err(());
3476    };
3477    // SAFETY: `func` is the consumer-registered C loader and must uphold the
3478    // xmlParserInputBufferCreateFilenameFunc contract (uri + enc in, buffer
3479    // out, or NULL on failure).
3480    let buf = unsafe { func(uri, xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int) };
3481    if buf.is_null() {
3482        return Err(());
3483    }
3484    let (read, ctx) = unsafe {
3485        let b = &*buf;
3486        (b.readcallback, b.context)
3487    };
3488    let mut data: Vec<u8> = Vec::new();
3489    let mut result = Err(());
3490    if let Some(read) = read {
3491        let mut tmp = [0u8; 4096];
3492        loop {
3493            // SAFETY: the loader's buffer carries the consumer's read
3494            // callback + context (xmlParserInputBufferCreateIO contract).
3495            let n = unsafe { read(ctx, tmp.as_mut_ptr() as *mut c_char, tmp.len() as c_int) };
3496            if n < 0 {
3497                break;
3498            }
3499            if n == 0 {
3500                result = Ok(());
3501                break;
3502            }
3503            data.extend_from_slice(&tmp[..n as usize]);
3504        }
3505    } else {
3506        // A memory-backed loader buffer (no read callback): copy its content.
3507        unsafe {
3508            let b = &*buf;
3509            if !b.buffer.is_null() {
3510                let xbuf = &*(b.buffer as *mut _xmlBuffer);
3511                if !xbuf.content.is_null() && xbuf.use_ > 0 {
3512                    data.extend_from_slice(std::slice::from_raw_parts(
3513                        xbuf.content as *const u8,
3514                        xbuf.use_ as usize,
3515                    ));
3516                }
3517            }
3518        }
3519        result = Ok(());
3520    }
3521    // Release the loader's C buffer: the close callback (php streams IO
3522    // close) runs exactly once now that the bytes are owned here.
3523    io::input_buffer_free(buf);
3524    result.map(|()| data)
3525}
3526
3527/// Route a filename open through the registered loaders, materializing the
3528/// result into an owned [`InputBuffer`] (filename = the original URI).
3529///
3530/// UPSTREAM LAYERING (2.14+ `xmlLoadResource`, R-000177): a REGISTERED
3531/// external entity loader (`xmlSetExternalEntityLoader`) is consulted first
3532/// for file/URL opens — main documents go through the same resource loader
3533/// as entities (`xmlCtxtNewInputFromUrl` -> `xmlLoadResource` ->
3534/// `xmlCurrentExternalEntityLoader`). A NULL custom-loader result is
3535/// `EntityLoaderFailed` (silent upstream — no `xmlCtxtErrIO`, the custom
3536/// loader reports its own errors). With NO custom entity loader the default
3537/// loader's tail is the `xmlParserInputBufferCreateFilenameDefault` (php
3538/// streams) loader, which is what the rest of this function implements
3539/// (upstream `xmlNewInputFromUrl`).
3540///
3541/// The registration is read through the R-000177 cross-DSO bridge (facade
3542/// copies must see a loader registered via the core DSO's exported setter).
3543///
3544/// # Safety
3545///
3546/// - `uri` must be a valid NUL-terminated C string live for the call.
3547/// - `ctxt` must be NULL or a valid parser context live for the call (passed
3548///   to the entity loader exactly as upstream `xmlLoadResource` does).
3549pub(crate) unsafe fn open_filename_routed(
3550    uri: *const c_char,
3551    ctxt: *mut _xmlParserCtxt,
3552) -> RoutedFileOpen {
3553    // A custom external entity loader governs file/URL opens too.
3554    if external_entity_loader_active() {
3555        // SAFETY: uri is a valid C string; ctxt is NULL or valid.
3556        let input = xmlLoadExternalEntity(uri, ptr::null(), ctxt);
3557        if input.is_null() {
3558            return RoutedFileOpen::EntityLoaderFailed;
3559        }
3560        // Materialize the loader's bytes into an owned InputBuffer (the
3561        // loader result is freed here; the parse consumes the copy).
3562        let loaded = input_bytes_owned(input);
3563        let named = if uri.is_null() {
3564            None
3565        } else {
3566            // SAFETY: uri is a valid NUL-terminated C string.
3567            Some(
3568                unsafe { CStr::from_ptr(uri) }
3569                    .to_string_lossy()
3570                    .into_owned(),
3571            )
3572        };
3573        return RoutedFileOpen::Loaded(InputBuffer::from_memory(
3574            loaded.as_deref().unwrap_or(&[]),
3575            named.as_deref(),
3576        ));
3577    }
3578    // No registered loader: the caller keeps the built-in open. The slot is
3579    // read through the R-000177 cross-DSO bridge (facade copies must see a
3580    // loader registered via the core DSO's exported setter).
3581    if globals::get_parser_input_buffer_create_filename_value_cross_dso().is_none() {
3582        return RoutedFileOpen::Builtin;
3583    }
3584    // SAFETY: uri is a valid NUL-terminated C string for the call.
3585    let loaded = unsafe { call_loader_materialize(uri) };
3586    match loaded {
3587        Err(()) => RoutedFileOpen::Failed,
3588        Ok(bytes) => {
3589            let named = if uri.is_null() {
3590                None
3591            } else {
3592                // SAFETY: uri is a valid NUL-terminated C string.
3593                Some(
3594                    unsafe { CStr::from_ptr(uri) }
3595                        .to_string_lossy()
3596                        .into_owned(),
3597                )
3598            };
3599            RoutedFileOpen::Loaded(InputBuffer::from_memory(&bytes, named.as_deref()))
3600        }
3601    }
3602}
3603
3604/// True when a custom external entity loader is registered process-wide
3605/// (the core DSO's `xmlSetExternalEntityLoader` registration, or this DSO's
3606/// own when the accessor does not resolve in a single-DSO link). The
3607/// process-visible registration is authoritative (R-000177).
3608fn external_entity_loader_active() -> bool {
3609    match foreign_external_entity_loader() {
3610        Some(_) => true,
3611        None => EXTERNAL_ENTITY_LOADER.lock().is_some(),
3612    }
3613}
3614
3615/// Route a filename open through the `xmlParserInputBufferCreateFilenameDefault`
3616/// (php streams) loader ONLY — no external-entity-loader consult.
3617///
3618/// The xmlTextReader family reads through `xmlNewInputFromFile` upstream,
3619/// which does NOT go through the external entity loader (verified against
3620/// the executed 2.15.3 oracle), so the reader must not pick up an
3621/// `xmlSetExternalEntityLoader` registration.
3622///
3623/// # Safety
3624///
3625/// - `uri` must be a valid NUL-terminated C string live for the call.
3626pub(crate) unsafe fn open_filename_routed_input_only(uri: *const c_char) -> RoutedFileOpen {
3627    // No registered loader: the caller keeps the built-in open. The slot is
3628    // read through the R-000177 cross-DSO bridge (facade copies must see a
3629    // loader registered via the core DSO's exported setter).
3630    if globals::get_parser_input_buffer_create_filename_value_cross_dso().is_none() {
3631        return RoutedFileOpen::Builtin;
3632    }
3633    // SAFETY: uri is a valid NUL-terminated C string for the call.
3634    let loaded = unsafe { call_loader_materialize(uri) };
3635    match loaded {
3636        Err(()) => RoutedFileOpen::Failed,
3637        Ok(bytes) => {
3638            let named = if uri.is_null() {
3639                None
3640            } else {
3641                // SAFETY: uri is a valid NUL-terminated C string.
3642                Some(
3643                    unsafe { CStr::from_ptr(uri) }
3644                        .to_string_lossy()
3645                        .into_owned(),
3646                )
3647            };
3648            RoutedFileOpen::Loaded(InputBuffer::from_memory(&bytes, named.as_deref()))
3649        }
3650    }
3651}
3652
3653/// Copy the bytes of a loader-produced `_xmlParserInput` into an owned
3654/// `Vec` and release the input (upstream `xmlCtxtParseDocument` consumes the
3655/// input; the candidate's parse paths own an [`InputBuffer`]). The input's
3656/// underlying buffer is freed with the input (xmlFreeInputStream).
3657///
3658/// # Safety
3659///
3660/// - `input` must be a valid `_xmlParserInput` produced by a registered
3661///   loader / `xmlLoadExternalEntity`, not yet freed.
3662unsafe fn input_bytes_owned(input: *mut _xmlParserInput) -> Option<Vec<u8>> {
3663    unsafe {
3664        let base = (*input).base;
3665        let end = (*input).end;
3666        let len = if base.is_null() {
3667            0
3668        } else {
3669            end.offset_from(base).max(0) as usize
3670        };
3671        let bytes = if base.is_null() || len == 0 {
3672            None
3673        } else {
3674            Some(core::slice::from_raw_parts(base, len).to_vec())
3675        };
3676        crate::abi::exports_xml2::xmlFreeInputStream(input);
3677        bytes
3678    }
3679}
3680
3681/// Compose the upstream `xmlCtxtErrIO(XML_IO_ENOENT, uri)` message text:
3682/// `failed to load "<uri>": <errno text>\n`. When errno is stale (the
3683/// registered php streams loader returned NULL without touching errno, e.g.
3684/// the percent-NUL guard) the `XML_IO_ENOENT` table text is used.
3685///
3686/// # Safety
3687///
3688/// - `uri` must be NULL or a valid NUL-terminated C string live for the call.
3689pub(crate) fn io_load_failure_message(uri: *const c_char) -> String {
3690    // SAFETY: reads errno only.
3691    let errno = unsafe { *libc::__errno_location() };
3692    let errstr = if errno == 0 {
3693        // xmlErrString(XML_IO_ENOENT) table text (error.c 2.15).
3694        "No such file or directory".to_string()
3695    } else {
3696        // SAFETY: strerror(errno) returns a static message for the value.
3697        unsafe { std::ffi::CStr::from_ptr(libc::strerror(errno)) }
3698            .to_string_lossy()
3699            .into_owned()
3700    };
3701    let url_str = if uri.is_null() {
3702        String::new()
3703    } else {
3704        // SAFETY: uri is a valid NUL-terminated C string.
3705        unsafe { std::ffi::CStr::from_ptr(uri) }
3706            .to_string_lossy()
3707            .into_owned()
3708    };
3709    format!("failed to load \"{url_str}\": {errstr}\n")
3710}
3711
3712/// Set the application-wide external entity loader.
3713///
3714/// # UPSTREAM-PARITY
3715///
3716/// ```c
3717/// void xmlSetExternalEntityLoader(xmlExternalEntityLoader f);
3718/// ```
3719///
3720/// # SAFETY
3721///
3722///
3723/// - `f` must be a valid callback (or None);
3724///   the callback is invoked with the documented context pointer and
3725///   must itself uphold the same pointer invariants.
3726///
3727/// The caller must not race this call with concurrent mutation of the
3728/// same objects from other threads (per-object state is not internally
3729/// synchronized). Violating any of the above is undefined behavior.
3730///
3731/// Exercised by the C-API differential courts
3732/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3733/// courts; those pass byte-for-byte against the upstream oracle.
3734#[no_mangle]
3735pub unsafe extern "C" fn xmlSetExternalEntityLoader(f: Option<xmlExternalEntityLoader>) {
3736    *EXTERNAL_ENTITY_LOADER.lock() = f;
3737}
3738
3739/// Get the current external entity loader.
3740///
3741/// # UPSTREAM-PARITY
3742///
3743/// ```c
3744/// xmlExternalEntityLoader xmlGetExternalEntityLoader(void);
3745/// ```
3746///
3747/// # SAFETY
3748///
3749/// The function touches crate-global state only; it is safe
3750/// as long as the caller respects the library's global
3751/// initialization/cleanup ordering (xmlInitParser before use,
3752/// xmlCleanupParser only after all users are done).
3753///
3754/// Violating the global lifecycle ordering, or calling this after
3755/// teardown or from a signal handler, is undefined behavior.
3756#[no_mangle]
3757pub unsafe extern "C" fn xmlGetExternalEntityLoader() -> Option<xmlExternalEntityLoader> {
3758    *EXTERNAL_ENTITY_LOADER.lock()
3759}
3760
3761/// External entity loader that disables network access.
3762///
3763/// # UPSTREAM-PARITY
3764///
3765/// ```c
3766/// xmlParserInputPtr xmlNoNetExternalEntityLoader(const char *URL,
3767///                                                const char *ID,
3768///                                                xmlParserCtxtPtr ctxt);
3769/// ```
3770///
3771/// # SAFETY
3772///
3773/// - `ctxt` must be valid pointers (or NULL
3774///   where the upstream C contract allows), obtained from the
3775///   matching constructor/owner and not yet freed; the callee may
3776///   take or keep ownership exactly as the C API specifies.
3777///
3778/// - `URL`, `ID` must point to valid NUL-terminated
3779///   strings (or NULL where the C contract allows) for the lifetime
3780///   of the call.
3781///
3782/// The caller must not race this call with concurrent mutation of the
3783/// same objects from other threads (per-object state is not internally
3784/// synchronized). Violating any of the above is undefined behavior.
3785///
3786/// Exercised by the C-API differential courts
3787/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3788/// courts; those pass byte-for-byte against the upstream oracle.
3789#[no_mangle]
3790pub unsafe extern "C" fn xmlNoNetExternalEntityLoader(
3791    URL: *const c_char,
3792    ID: *const c_char,
3793    ctxt: *mut _xmlParserCtxt,
3794) -> *mut _xmlParserInput {
3795    unsafe {
3796        let old_options = if ctxt.is_null() { 0 } else { (*ctxt).options };
3797        if !ctxt.is_null() {
3798            (*ctxt).options |= XML_PARSE_NONET;
3799        }
3800        let input = default_external_entity_loader(URL, ID, ctxt);
3801        if !ctxt.is_null() {
3802            (*ctxt).options = old_options;
3803        }
3804        input
3805    }
3806}
3807
3808/// Load an external entity using the registered loader.
3809///
3810/// # UPSTREAM-PARITY
3811///
3812/// ```c
3813/// xmlParserInputPtr xmlLoadExternalEntity(const char *URL, const char *ID,
3814///                                         xmlParserCtxtPtr ctxt);
3815/// ```
3816///
3817/// # SAFETY
3818///
3819/// - `ctxt` must be valid pointers (or NULL
3820///   where the upstream C contract allows), obtained from the
3821///   matching constructor/owner and not yet freed; the callee may
3822///   take or keep ownership exactly as the C API specifies.
3823///
3824/// - `URL`, `ID` must point to valid NUL-terminated
3825///   strings (or NULL where the C contract allows) for the lifetime
3826///   of the call.
3827///
3828/// The caller must not race this call with concurrent mutation of the
3829/// same objects from other threads (per-object state is not internally
3830/// synchronized). Violating any of the above is undefined behavior.
3831///
3832/// Exercised by the C-API differential courts
3833/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3834/// courts; those pass byte-for-byte against the upstream oracle.
3835#[no_mangle]
3836pub unsafe extern "C" fn xmlLoadExternalEntity(
3837    URL: *const c_char,
3838    ID: *const c_char,
3839    ctxt: *mut _xmlParserCtxt,
3840) -> *mut _xmlParserInput {
3841    // R-000177: the loader xmlSetExternalEntityLoader registers binds to the
3842    // CORE DSO, so a load performed by a whole-archive facade's private copy
3843    // must consult the process-visible registration first (upstream: one
3844    // core instance, one loader). Single-DSO links resolve their own export.
3845    let loader = match foreign_external_entity_loader() {
3846        Some(f) => Some(f),
3847        None => *EXTERNAL_ENTITY_LOADER.lock(),
3848    };
3849    match loader {
3850        Some(f) => unsafe { f(URL, ID, ctxt) },
3851        None => unsafe { default_external_entity_loader(URL, ID, ctxt) },
3852    }
3853}
3854
3855/// Resolve the process-visible `xmlGetExternalEntityLoader` (the CORE DSO's
3856/// registration) via the dynamic symbol scope.
3857#[cfg(target_os = "linux")]
3858fn foreign_external_entity_loader() -> Option<xmlExternalEntityLoader> {
3859    use std::sync::OnceLock;
3860    type Getter = unsafe extern "C" fn() -> Option<xmlExternalEntityLoader>;
3861    static GETTER: OnceLock<Option<Getter>> = OnceLock::new();
3862    let getter = *GETTER.get_or_init(|| {
3863        // SAFETY: dlsym(RTLD_DEFAULT) returns the exported accessor address
3864        // or NULL; the transmute (pointer-sized) is sound.
3865        unsafe {
3866            let sym = libc::dlsym(libc::RTLD_DEFAULT, c"xmlGetExternalEntityLoader".as_ptr());
3867            if sym.is_null() {
3868                None
3869            } else {
3870                Some(std::mem::transmute::<*mut c_void, Getter>(sym))
3871            }
3872        }
3873    });
3874    getter.and_then(|g| unsafe { g() })
3875}
3876
3877#[cfg(not(target_os = "linux"))]
3878fn foreign_external_entity_loader() -> Option<xmlExternalEntityLoader> {
3879    None
3880}
3881
3882/// Check an input for HTTP access; with XML_PARSE_NONET set, HTTP inputs are
3883/// refused and freed.
3884///
3885/// # UPSTREAM-PARITY
3886///
3887/// ```c
3888/// xmlParserInputPtr xmlCheckHTTPInput(xmlParserCtxtPtr ctxt,
3889///                                     xmlParserInputPtr ret);
3890/// ```
3891///
3892/// # SAFETY
3893///
3894/// - `ctxt`, `ret` must be valid pointers (or NULL
3895///   where the upstream C contract allows), obtained from the
3896///   matching constructor/owner and not yet freed; the callee may
3897///   take or keep ownership exactly as the C API specifies.
3898///
3899/// The caller must not race this call with concurrent mutation of the
3900/// same objects from other threads (per-object state is not internally
3901/// synchronized). Violating any of the above is undefined behavior.
3902///
3903/// Exercised by the C-API differential courts
3904/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3905/// courts; those pass byte-for-byte against the upstream oracle.
3906#[no_mangle]
3907pub unsafe extern "C" fn xmlCheckHTTPInput(
3908    ctxt: *mut _xmlParserCtxt,
3909    ret: *mut _xmlParserInput,
3910) -> *mut _xmlParserInput {
3911    if ret.is_null() {
3912        return ptr::null_mut();
3913    }
3914    unsafe {
3915        if !ctxt.is_null() && (*ctxt).options & XML_PARSE_NONET != 0 {
3916            let filename = (*ret).filename;
3917            if !filename.is_null() {
3918                let len = libc::strlen(filename);
3919                if len >= 7
3920                    && libc::strncasecmp(filename, c"http://".as_ptr() as *const c_char, 7) == 0
3921                {
3922                    // free_parser_input now frees the owned buffer (upstream
3923                    // xmlFreeInputStream semantics); no separate buf free.
3924                    helpers::free_parser_input(ret);
3925                    return ptr::null_mut();
3926                }
3927            }
3928        }
3929        ret
3930    }
3931}
3932
3933// ═══════════════════════════════════════════════════════════════════════════════
3934// xmlFile* I/O callbacks (xmlIO.c)
3935// ═══════════════════════════════════════════════════════════════════════════════
3936
3937/// Match callback: the file I/O handlers accept every filename.
3938///
3939/// # UPSTREAM-PARITY
3940///
3941/// ```c
3942/// int xmlFileMatch(const char *filename);
3943/// ```
3944///
3945/// # SAFETY
3946///
3947///
3948/// - `_filename` must point to valid NUL-terminated
3949///   strings (or NULL where the C contract allows) for the lifetime
3950///   of the call.
3951///
3952/// The caller must not race this call with concurrent mutation of the
3953/// same objects from other threads (per-object state is not internally
3954/// synchronized). Violating any of the above is undefined behavior.
3955///
3956/// Exercised by the C-API differential courts
3957/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3958/// courts; those pass byte-for-byte against the upstream oracle.
3959#[no_mangle]
3960pub const unsafe extern "C" fn xmlFileMatch(_filename: *const c_char) -> c_int {
3961    1
3962}
3963
3964/// Open a file and return a `FILE *` I/O context (cast to `void *`).
3965///
3966/// # UPSTREAM-PARITY
3967///
3968/// ```c
3969/// void *xmlFileOpen(const char *filename);
3970/// ```
3971///
3972/// # SAFETY
3973///
3974///
3975/// - `filename` must point to valid NUL-terminated
3976///   strings (or NULL where the C contract allows) for the lifetime
3977///   of the call.
3978///
3979/// The caller must not race this call with concurrent mutation of the
3980/// same objects from other threads (per-object state is not internally
3981/// synchronized). Violating any of the above is undefined behavior.
3982///
3983/// Exercised by the C-API differential courts
3984/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3985/// courts; those pass byte-for-byte against the upstream oracle.
3986#[no_mangle]
3987pub unsafe extern "C" fn xmlFileOpen(filename: *const c_char) -> *mut c_void {
3988    if filename.is_null() {
3989        return ptr::null_mut();
3990    }
3991    unsafe { libc::fopen(filename, c"rb".as_ptr() as *const c_char) as *mut c_void }
3992}
3993
3994/// Read up to `len` bytes from a `FILE *` I/O context.
3995///
3996/// # UPSTREAM-PARITY
3997///
3998/// ```c
3999/// int xmlFileRead(void *context, char *buffer, int len);
4000/// ```
4001///
4002/// # SAFETY
4003///
4004/// - `context`, `buffer` must be valid pointers (or NULL
4005///   where the upstream C contract allows), obtained from the
4006///   matching constructor/owner and not yet freed; the callee may
4007///   take or keep ownership exactly as the C API specifies.
4008///
4009/// The caller must not race this call with concurrent mutation of the
4010/// same objects from other threads (per-object state is not internally
4011/// synchronized). Violating any of the above is undefined behavior.
4012///
4013/// Exercised by the C-API differential courts
4014/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4015/// courts; those pass byte-for-byte against the upstream oracle.
4016#[no_mangle]
4017pub unsafe extern "C" fn xmlFileRead(
4018    context: *mut c_void,
4019    buffer: *mut c_char,
4020    len: c_int,
4021) -> c_int {
4022    if context.is_null() || buffer.is_null() || len <= 0 {
4023        return -1;
4024    }
4025    unsafe {
4026        let n = libc::fread(
4027            buffer as *mut c_void,
4028            1,
4029            len as usize,
4030            context as *mut libc::FILE,
4031        );
4032        if n < len as usize && libc::ferror(context as *mut libc::FILE) != 0 {
4033            return -1;
4034        }
4035        n as c_int
4036    }
4037}
4038
4039/// Close a `FILE *` I/O context.
4040///
4041/// # UPSTREAM-PARITY
4042///
4043/// ```c
4044/// int xmlFileClose(void *context);
4045/// ```
4046///
4047/// # SAFETY
4048///
4049/// - `context` must be valid pointers (or NULL
4050///   where the upstream C contract allows), obtained from the
4051///   matching constructor/owner and not yet freed; the callee may
4052///   take or keep ownership exactly as the C API specifies.
4053///
4054/// The caller must not race this call with concurrent mutation of the
4055/// same objects from other threads (per-object state is not internally
4056/// synchronized). Violating any of the above is undefined behavior.
4057///
4058/// Exercised by the C-API differential courts
4059/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4060/// courts; those pass byte-for-byte against the upstream oracle.
4061#[no_mangle]
4062pub unsafe extern "C" fn xmlFileClose(context: *mut c_void) -> c_int {
4063    if context.is_null() {
4064        return -1;
4065    }
4066    unsafe {
4067        let file = context as *mut libc::FILE;
4068        let fd = libc::fileno(file);
4069        if fd == 0 {
4070            // stdin must not be closed.
4071            return 0;
4072        }
4073        if fd == 1 || fd == 2 {
4074            // stdout/stderr are only flushed.
4075            return if libc::fflush(file) == 0 { 0 } else { -1 };
4076        }
4077        libc::fclose(file)
4078    }
4079}
4080
4081// ═══════════════════════════════════════════════════════════════════════════════
4082// Low-level character scanning (parserInternals.c)
4083// ═══════════════════════════════════════════════════════════════════════════════
4084
4085/// Return the current character (UTF-8 decoded, EOL normalised) and its byte
4086/// length in `*len`. Does not advance the input pointer.
4087///
4088/// # UPSTREAM-PARITY
4089///
4090/// ```c
4091/// int xmlCurrentChar(xmlParserCtxtPtr ctxt, int *len);
4092/// ```
4093///
4094/// # SAFETY
4095///
4096/// - `ctxt`, `len` must be valid pointers (or NULL
4097///   where the upstream C contract allows), obtained from the
4098///   matching constructor/owner and not yet freed; the callee may
4099///   take or keep ownership exactly as the C API specifies.
4100///
4101/// The caller must not race this call with concurrent mutation of the
4102/// same objects from other threads (per-object state is not internally
4103/// synchronized). Violating any of the above is undefined behavior.
4104///
4105/// Exercised by the C-API differential courts
4106/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4107/// courts; those pass byte-for-byte against the upstream oracle.
4108#[no_mangle]
4109pub unsafe extern "C" fn xmlCurrentChar(ctxt: *mut _xmlParserCtxt, len: *mut c_int) -> c_int {
4110    if ctxt.is_null() || len.is_null() || (*ctxt).input.is_null() {
4111        return 0;
4112    }
4113    unsafe {
4114        let pi = &*((*ctxt).input);
4115        let cur = pi.cur;
4116        if cur.is_null() {
4117            *len = 0;
4118            return 0;
4119        }
4120        let avail = (pi.end as usize).saturating_sub(cur as usize);
4121        let c = *cur;
4122
4123        if c < 0x80 {
4124            if c == b'\r' {
4125                // EOL normalisation: CR (optionally CRLF) becomes LF.
4126                if avail >= 2 && *cur.add(1) == b'\n' {
4127                    (*(*ctxt).input).cur = cur.add(1);
4128                }
4129                *len = 1;
4130                return b'\n' as c_int;
4131            }
4132            if c == 0 {
4133                if avail == 0 {
4134                    *len = 0;
4135                } else {
4136                    *len = 1;
4137                }
4138                return 0;
4139            }
4140            *len = 1;
4141            return c as c_int;
4142        }
4143
4144        // Multi-byte UTF-8.
4145        if avail < 2 || (*cur.add(1) & 0xc0) != 0x80 {
4146            *len = 1;
4147            return XML_INVALID_CHAR;
4148        }
4149        if c < 0xe0 {
4150            if c < 0xc2 {
4151                *len = 1;
4152                return XML_INVALID_CHAR;
4153            }
4154            let val = (((c & 0x1f) as c_int) << 6) | ((*cur.add(1) & 0x3f) as c_int);
4155            *len = 2;
4156            return val;
4157        }
4158        if avail < 3 || (*cur.add(2) & 0xc0) != 0x80 {
4159            *len = 1;
4160            return XML_INVALID_CHAR;
4161        }
4162        if c < 0xf0 {
4163            let val = (((c & 0x0f) as c_int) << 12)
4164                | (((*cur.add(1) & 0x3f) as c_int) << 6)
4165                | ((*cur.add(2) & 0x3f) as c_int);
4166            if val < 0x800 || (0xd800..0xe000).contains(&val) {
4167                *len = 1;
4168                return XML_INVALID_CHAR;
4169            }
4170            *len = 3;
4171            return val;
4172        }
4173        if avail < 4 || (*cur.add(3) & 0xc0) != 0x80 {
4174            *len = 1;
4175            return XML_INVALID_CHAR;
4176        }
4177        let val = (((c & 0x07) as c_int) << 18)
4178            | (((*cur.add(1) & 0x3f) as c_int) << 12)
4179            | (((*cur.add(2) & 0x3f) as c_int) << 6)
4180            | ((*cur.add(3) & 0x3f) as c_int);
4181        if !(0x10000..0x110000).contains(&val) {
4182            *len = 1;
4183            return XML_INVALID_CHAR;
4184        }
4185        *len = 4;
4186        val
4187    }
4188}
4189
4190/// Advance to the next character, updating line/column accounting.
4191///
4192/// # UPSTREAM-PARITY
4193///
4194/// ```c
4195/// void xmlNextChar(xmlParserCtxtPtr ctxt);
4196/// ```
4197///
4198/// # SAFETY
4199///
4200/// - `ctxt` must be valid pointers (or NULL
4201///   where the upstream C contract allows), obtained from the
4202///   matching constructor/owner and not yet freed; the callee may
4203///   take or keep ownership exactly as the C API specifies.
4204///
4205/// The caller must not race this call with concurrent mutation of the
4206/// same objects from other threads (per-object state is not internally
4207/// synchronized). Violating any of the above is undefined behavior.
4208///
4209/// Exercised by the C-API differential courts
4210/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4211/// courts; those pass byte-for-byte against the upstream oracle.
4212#[no_mangle]
4213pub unsafe extern "C" fn xmlNextChar(ctxt: *mut _xmlParserCtxt) {
4214    if ctxt.is_null() || (*ctxt).input.is_null() {
4215        return;
4216    }
4217    unsafe {
4218        let pi = &mut *((*ctxt).input);
4219        let cur = pi.cur;
4220        if cur.is_null() {
4221            return;
4222        }
4223        let avail = (pi.end as usize).saturating_sub(cur as usize);
4224        if avail == 0 {
4225            return;
4226        }
4227        let c = *cur;
4228
4229        if c < 0x80 {
4230            if c == b'\n' {
4231                pi.cur = cur.add(1);
4232                pi.line += 1;
4233                pi.col = 1;
4234            } else if c == b'\r' {
4235                // CRLF is a single line break.
4236                pi.cur = cur.add(if avail >= 2 && *cur.add(1) == b'\n' {
4237                    2
4238                } else {
4239                    1
4240                });
4241                pi.line += 1;
4242                pi.col = 1;
4243            } else {
4244                pi.cur = cur.add(1);
4245                pi.col += 1;
4246            }
4247            return;
4248        }
4249
4250        pi.col += 1;
4251
4252        if avail < 2 || (*cur.add(1) & 0xc0) != 0x80 {
4253            pi.cur = cur.add(1);
4254            return;
4255        }
4256        if c < 0xe0 {
4257            if c < 0xc2 {
4258                pi.cur = cur.add(1);
4259                return;
4260            }
4261            pi.cur = cur.add(2);
4262            return;
4263        }
4264        if avail < 3 || (*cur.add(2) & 0xc0) != 0x80 {
4265            pi.cur = cur.add(1);
4266            return;
4267        }
4268        if c < 0xf0 {
4269            let val = (((c as c_int) << 8) as u32) | (*cur.add(1) as u32);
4270            if (val < 0xe0a0) || (0xeda0..0xee00).contains(&val) {
4271                pi.cur = cur.add(1);
4272                return;
4273            }
4274            pi.cur = cur.add(3);
4275            return;
4276        }
4277        if avail < 4 || (*cur.add(3) & 0xc0) != 0x80 {
4278            pi.cur = cur.add(1);
4279            return;
4280        }
4281        let val = (((c as c_int) << 8) as u32) | (*cur.add(1) as u32);
4282        if !(0xf090..0xf490).contains(&val) {
4283            pi.cur = cur.add(1);
4284            return;
4285        }
4286        pi.cur = cur.add(4);
4287    }
4288}
4289
4290/// Skip blank characters (space, tab, LF, CR), updating line/column.
4291/// Returns the number of blanks skipped.
4292///
4293/// # UPSTREAM-PARITY
4294///
4295/// ```c
4296/// int xmlSkipBlankChars(xmlParserCtxtPtr ctxt);
4297/// ```
4298///
4299/// # SAFETY
4300///
4301/// - `ctxt` must be valid pointers (or NULL
4302///   where the upstream C contract allows), obtained from the
4303///   matching constructor/owner and not yet freed; the callee may
4304///   take or keep ownership exactly as the C API specifies.
4305///
4306/// The caller must not race this call with concurrent mutation of the
4307/// same objects from other threads (per-object state is not internally
4308/// synchronized). Violating any of the above is undefined behavior.
4309///
4310/// Exercised by the C-API differential courts
4311/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4312/// courts; those pass byte-for-byte against the upstream oracle.
4313#[no_mangle]
4314pub unsafe extern "C" fn xmlSkipBlankChars(ctxt: *mut _xmlParserCtxt) -> c_int {
4315    if ctxt.is_null() || (*ctxt).input.is_null() {
4316        return 0;
4317    }
4318    unsafe {
4319        let pi = &mut *((*ctxt).input);
4320        let mut cur = pi.cur;
4321        if cur.is_null() {
4322            return 0;
4323        }
4324        let end = pi.end;
4325        let mut res = 0;
4326        while cur < end && (*cur == 0x20 || *cur == 0x09 || *cur == 0x0a || *cur == 0x0d) {
4327            if *cur == b'\n' {
4328                pi.line += 1;
4329                pi.col = 1;
4330            } else {
4331                pi.col += 1;
4332            }
4333            cur = cur.add(1);
4334            res += 1;
4335        }
4336        pi.cur = cur;
4337        res
4338    }
4339}
4340
4341/// XML 1.0 5th-edition NameStartChar predicate (upstream `xmlIsNameStartCharNew`).
4342const fn is_name_start_char_new(c: c_int) -> bool {
4343    if c == b' ' as c_int || c == b'>' as c_int || c == b'/' as c_int {
4344        return false;
4345    }
4346    (c >= b'a' as c_int && c <= b'z' as c_int)
4347        || (c >= b'A' as c_int && c <= b'Z' as c_int)
4348        || c == b'_' as c_int
4349        || c == b':' as c_int
4350        || (c >= 0xC0 && c <= 0xD6)
4351        || (c >= 0xD8 && c <= 0xF6)
4352        || (c >= 0xF8 && c <= 0x2FF)
4353        || (c >= 0x370 && c <= 0x37D)
4354        || (c >= 0x37F && c <= 0x1FFF)
4355        || (c >= 0x200C && c <= 0x200D)
4356        || (c >= 0x2070 && c <= 0x218F)
4357        || (c >= 0x2C00 && c <= 0x2FEF)
4358        || (c >= 0x3001 && c <= 0xD7FF)
4359        || (c >= 0xF900 && c <= 0xFDCF)
4360        || (c >= 0xFDF0 && c <= 0xFFFD)
4361        || (c >= 0x10000 && c <= 0xEFFFF)
4362}
4363
4364/// XML 1.0 5th-edition NameChar predicate (upstream `xmlIsNameCharNew`).
4365const fn is_name_char_new(c: c_int) -> bool {
4366    if c == b' ' as c_int || c == b'>' as c_int || c == b'/' as c_int {
4367        return false;
4368    }
4369    (c >= b'a' as c_int && c <= b'z' as c_int)
4370        || (c >= b'A' as c_int && c <= b'Z' as c_int)
4371        || (c >= b'0' as c_int && c <= b'9' as c_int)
4372        || c == b'_' as c_int
4373        || c == b':' as c_int
4374        || c == b'-' as c_int
4375        || c == b'.' as c_int
4376        || c == 0xB7
4377        || (c >= 0xC0 && c <= 0xD6)
4378        || (c >= 0xD8 && c <= 0xF6)
4379        || (c >= 0xF8 && c <= 0x2FF)
4380        || (c >= 0x300 && c <= 0x36F)
4381        || (c >= 0x370 && c <= 0x37D)
4382        || (c >= 0x37F && c <= 0x1FFF)
4383        || (c >= 0x200C && c <= 0x200D)
4384        || (c >= 0x203F && c <= 0x2040)
4385        || (c >= 0x2070 && c <= 0x218F)
4386        || (c >= 0x2C00 && c <= 0x2FEF)
4387        || (c >= 0x3001 && c <= 0xD7FF)
4388        || (c >= 0xF900 && c <= 0xFDCF)
4389        || (c >= 0xFDF0 && c <= 0xFFFD)
4390        || (c >= 0x10000 && c <= 0xEFFFF)
4391}
4392
4393/// Scan an XML Name (or NCName/Nmtoken) at `ctxt->input->cur`, advancing the
4394/// input pointer. Returns a pointer to the end of the name, or NULL when the
4395/// name exceeds `max` bytes.
4396///
4397/// # UPSTREAM-PARITY
4398///
4399/// ```c
4400/// const xmlChar *xmlScanName(xmlParserCtxtPtr ctxt, int max, int flags);
4401/// ```
4402///
4403/// # SAFETY
4404///
4405/// - `ctxt` must be valid pointers (or NULL
4406///   where the upstream C contract allows), obtained from the
4407///   matching constructor/owner and not yet freed; the callee may
4408///   take or keep ownership exactly as the C API specifies.
4409///
4410/// The caller must not race this call with concurrent mutation of the
4411/// same objects from other threads (per-object state is not internally
4412/// synchronized). Violating any of the above is undefined behavior.
4413///
4414/// Exercised by the C-API differential courts
4415/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4416/// courts; those pass byte-for-byte against the upstream oracle.
4417#[no_mangle]
4418pub unsafe extern "C" fn xmlScanName(
4419    ctxt: *mut _xmlParserCtxt,
4420    max: c_int,
4421    flags: c_int,
4422) -> *const xmlChar {
4423    if ctxt.is_null() || (*ctxt).input.is_null() || max <= 0 {
4424        return ptr::null();
4425    }
4426    unsafe {
4427        let pi = &mut *((*ctxt).input);
4428        let mut ptr = pi.cur;
4429        if ptr.is_null() {
4430            return ptr::null();
4431        }
4432        let end = pi.end;
4433        let mut remaining = max as usize;
4434        let stop: u8 = if flags & XML_SCAN_NC != 0 { b':' } else { 0 };
4435        let old10 = flags & XML_SCAN_OLD10 != 0;
4436        let mut f = flags;
4437
4438        loop {
4439            if ptr >= end {
4440                break;
4441            }
4442            let c = *ptr;
4443            let (cp, len) = if c < 0x80 {
4444                if stop != 0 && c == stop {
4445                    break;
4446                }
4447                (c as c_int, 1usize)
4448            } else {
4449                // Decode a multi-byte UTF-8 character.
4450                let avail = (end as usize).saturating_sub(ptr as usize);
4451                let mut l = 4usize;
4452                let cp = decode_utf8_char(ptr, avail, &mut l);
4453                if cp < 0 {
4454                    break;
4455                }
4456                (cp, l)
4457            };
4458
4459            let ok = if f & XML_SCAN_NMTOKEN != 0 {
4460                if old10 {
4461                    is_name_char_old10(cp)
4462                } else {
4463                    is_name_char_new(cp)
4464                }
4465            } else if old10 {
4466                is_name_start_char_old10(cp)
4467            } else {
4468                is_name_start_char_new(cp)
4469            };
4470            if !ok {
4471                break;
4472            }
4473            if len > remaining {
4474                return ptr::null();
4475            }
4476            ptr = ptr.add(len);
4477            remaining -= len;
4478            f |= XML_SCAN_NMTOKEN;
4479        }
4480
4481        pi.cur = ptr;
4482        ptr
4483    }
4484}
4485
4486/// Decode a UTF-8 character at `ptr` with `avail` bytes available; returns the
4487/// codepoint (or -1 on invalid/truncated input) and sets `*len` to its length.
4488const unsafe fn decode_utf8_char(ptr: *const u8, avail: usize, len: &mut usize) -> c_int {
4489    unsafe {
4490        let c = *ptr;
4491        if avail < 2 || (*ptr.add(1) & 0xc0) != 0x80 {
4492            return -1;
4493        }
4494        if c < 0xe0 {
4495            if c < 0xc2 {
4496                return -1;
4497            }
4498            *len = 2;
4499            return (((c & 0x1f) as c_int) << 6) | ((*ptr.add(1) & 0x3f) as c_int);
4500        }
4501        if avail < 3 || (*ptr.add(2) & 0xc0) != 0x80 {
4502            return -1;
4503        }
4504        if c < 0xf0 {
4505            let val = (((c & 0x0f) as c_int) << 12)
4506                | (((*ptr.add(1) & 0x3f) as c_int) << 6)
4507                | ((*ptr.add(2) & 0x3f) as c_int);
4508            if val < 0x800 || (val >= 0xd800 && val < 0xe000) {
4509                return -1;
4510            }
4511            *len = 3;
4512            return val;
4513        }
4514        if avail < 4 || (*ptr.add(3) & 0xc0) != 0x80 {
4515            return -1;
4516        }
4517        let val = (((c & 0x07) as c_int) << 18)
4518            | (((*ptr.add(1) & 0x3f) as c_int) << 12)
4519            | (((*ptr.add(2) & 0x3f) as c_int) << 6)
4520            | ((*ptr.add(3) & 0x3f) as c_int);
4521        if val < 0x10000 || val >= 0x110000 {
4522            return -1;
4523        }
4524        *len = 4;
4525        val
4526    }
4527}
4528
4529/// XML 1.0 (pre-revision-5) NameStartChar predicate: Letter, '_' or ':'.
4530const fn is_name_start_char_old10(c: c_int) -> bool {
4531    (c >= b'a' as c_int && c <= b'z' as c_int)
4532        || (c >= b'A' as c_int && c <= b'Z' as c_int)
4533        || c == b'_' as c_int
4534        || c == b':' as c_int
4535        || (c >= 0xC0 && c <= 0xD6)
4536        || (c >= 0xD8 && c <= 0xF6)
4537        || (c >= 0xF8 && c <= 0x2FF)
4538        || (c >= 0x370 && c <= 0x37D)
4539        || (c >= 0x37F && c <= 0x1FFF)
4540        || (c >= 0x200C && c <= 0x200D)
4541        || (c >= 0x2070 && c <= 0x218F)
4542        || (c >= 0x2C00 && c <= 0x2FEF)
4543        || (c >= 0x3001 && c <= 0xD7FF)
4544        || (c >= 0xF900 && c <= 0xFDCF)
4545        || (c >= 0xFDF0 && c <= 0xFFFD)
4546        || (c >= 0x10000 && c <= 0xEFFFF)
4547}
4548
4549/// XML 1.0 (pre-revision-5) NameChar predicate: NameStartChar, digits, '.',
4550/// '-', combining chars and extenders.
4551const fn is_name_char_old10(c: c_int) -> bool {
4552    is_name_start_char_old10(c)
4553        || (c >= b'0' as c_int && c <= b'9' as c_int)
4554        || c == b'.' as c_int
4555        || c == b'-' as c_int
4556        || c == 0xB7
4557        || (c >= 0x300 && c <= 0x36F)
4558        || c == 0x02D0
4559        || c == 0x02D1
4560        || c == 0x0387
4561        || c == 0x0640
4562        || c == 0x0E46
4563        || c == 0x0EC6
4564        || c == 0x3005
4565        || (c >= 0x3031 && c <= 0x3035)
4566        || (c >= 0x309D && c <= 0x309E)
4567        || (c >= 0x30FC && c <= 0x30FE)
4568}
4569
4570/// Decode entities from the current input position: char references and
4571/// (predefined and DTD-declared) entity references are substituted. Stops at
4572/// the first of `end`/`end2`/`end3`, or after `len` bytes (`len < 0` = rest).
4573///
4574/// # UPSTREAM-PARITY
4575///
4576/// ```c
4577/// xmlChar *xmlDecodeEntities(xmlParserCtxtPtr ctxt, int len, xmlChar end,
4578///                            xmlChar end2, xmlChar end3);
4579/// ```
4580///
4581/// # SAFETY
4582///
4583/// - `ctxt` must be valid pointers (or NULL
4584///   where the upstream C contract allows), obtained from the
4585///   matching constructor/owner and not yet freed; the callee may
4586///   take or keep ownership exactly as the C API specifies.
4587///
4588/// The caller must not race this call with concurrent mutation of the
4589/// same objects from other threads (per-object state is not internally
4590/// synchronized). Violating any of the above is undefined behavior.
4591///
4592/// Exercised by the C-API differential courts
4593/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4594/// courts; those pass byte-for-byte against the upstream oracle.
4595#[no_mangle]
4596pub unsafe extern "C" fn xmlDecodeEntities(
4597    ctxt: *mut _xmlParserCtxt,
4598    len: c_int,
4599    end: xmlChar,
4600    end2: xmlChar,
4601    end3: xmlChar,
4602) -> *mut xmlChar {
4603    if ctxt.is_null() || (*ctxt).input.is_null() {
4604        return ptr::null_mut();
4605    }
4606    unsafe {
4607        let pi = &*((*ctxt).input);
4608        let cur = pi.cur;
4609        if cur.is_null() {
4610            return ptr::null_mut();
4611        }
4612        let avail = (pi.end as usize).saturating_sub(cur as usize);
4613        let n = if len < 0 {
4614            avail
4615        } else {
4616            (len as usize).min(avail)
4617        };
4618
4619        let mut out: Vec<u8> = Vec::new();
4620        let mut i = 0usize;
4621
4622        while i < n {
4623            let c = *cur.add(i);
4624            if c == end || c == end2 || c == end3 {
4625                break;
4626            }
4627            if c != b'&' {
4628                out.push(c);
4629                i += 1;
4630                continue;
4631            }
4632
4633            // Character reference: &#...; or &#x...;
4634            if i + 1 < n && *cur.add(i + 1) == b'#' {
4635                let (value, consumed) = parse_char_ref(cur.add(i), n - i);
4636                if consumed == 0 {
4637                    out.push(b'&');
4638                    i += 1;
4639                    continue;
4640                }
4641                let mut buf = [0u8; 4];
4642                let blen = copy_char_utf8(&mut buf, value);
4643                out.extend_from_slice(&buf[..blen]);
4644                i += consumed;
4645                continue;
4646            }
4647
4648            // Entity reference: &name;
4649            let mut j = i + 1;
4650            while j < n
4651                && ((*cur.add(j)).is_ascii_alphanumeric()
4652                    || *cur.add(j) == b'_'
4653                    || *cur.add(j) == b'-'
4654                    || *cur.add(j) == b'.'
4655                    || *cur.add(j) == b':')
4656            {
4657                j += 1;
4658            }
4659            if j < n && *cur.add(j) == b';' {
4660                let name = core::slice::from_raw_parts(cur.add(i + 1), j - i - 1);
4661                let mut replaced = false;
4662                // Predefined entities.
4663                let content: Option<&[u8]> = match name {
4664                    b"amp" => Some(b"&"),
4665                    b"lt" => Some(b"<"),
4666                    b"gt" => Some(b">"),
4667                    b"quot" => Some(b"\""),
4668                    b"apos" => Some(b"'"),
4669                    _ => None,
4670                };
4671                if let Some(c) = content {
4672                    out.extend_from_slice(c);
4673                    replaced = true;
4674                } else {
4675                    // DTD-declared entity.
4676                    let mut name_nul = name.to_vec();
4677                    name_nul.push(0);
4678                    let ent = entities::get_entity((*ctxt).myDoc, name_nul.as_ptr());
4679                    if !ent.is_null() && !(*ent).content.is_null() {
4680                        let clen = string::xml_strlen((*ent).content);
4681                        out.extend_from_slice(core::slice::from_raw_parts((*ent).content, clen));
4682                        replaced = true;
4683                    }
4684                }
4685                if replaced {
4686                    i = j + 1;
4687                    continue;
4688                }
4689            }
4690            out.push(b'&');
4691            i += 1;
4692        }
4693
4694        out.push(0);
4695        let result = xmlMallocImpl(out.len()) as *mut xmlChar;
4696        if result.is_null() {
4697            return ptr::null_mut();
4698        }
4699        ptr::copy_nonoverlapping(out.as_ptr(), result, out.len());
4700        result
4701    }
4702}
4703
4704/// Parse a numeric character reference at `ptr` (starting at '&#'); returns
4705/// the value and total bytes consumed, or (0, 0) when malformed.
4706const unsafe fn parse_char_ref(ptr: *const u8, avail: usize) -> (c_int, usize) {
4707    unsafe {
4708        if avail < 3 || *ptr != b'&' || *ptr.add(1) != b'#' {
4709            return (0, 0);
4710        }
4711        let mut i = 2usize;
4712        let hex = i < avail && (*ptr.add(i) == b'x' || *ptr.add(i) == b'X');
4713        if hex {
4714            i += 1;
4715        }
4716        let start = i;
4717        let mut value: u32 = 0;
4718        while i < avail && *ptr.add(i) != b';' {
4719            let d = (*ptr.add(i) as char).to_digit(if hex { 16 } else { 10 });
4720            match d {
4721                Some(d) => {
4722                    value = value
4723                        .saturating_mul(if hex { 16 } else { 10 })
4724                        .saturating_add(d);
4725                    i += 1;
4726                }
4727                None => return (0, 0),
4728            }
4729        }
4730        if i == start || i >= avail || *ptr.add(i) != b';' {
4731            return (0, 0);
4732        }
4733        (value as c_int, i + 1)
4734    }
4735}
4736
4737/// Encode a codepoint into a UTF-8 byte sequence; returns the byte count.
4738const fn copy_char_utf8(out: &mut [u8; 4], val: c_int) -> usize {
4739    if val < 0x80 {
4740        out[0] = val as u8;
4741        1
4742    } else if val < 0x800 {
4743        out[0] = 0xC0 | ((val >> 6) as u8);
4744        out[1] = 0x80 | ((val & 0x3F) as u8);
4745        2
4746    } else if val < 0x10000 {
4747        out[0] = 0xE0 | ((val >> 12) as u8);
4748        out[1] = 0x80 | (((val >> 6) & 0x3F) as u8);
4749        out[2] = 0x80 | ((val & 0x3F) as u8);
4750        3
4751    } else if val < 0x110000 {
4752        out[0] = 0xF0 | ((val >> 18) as u8);
4753        out[1] = 0x80 | (((val >> 12) & 0x3F) as u8);
4754        out[2] = 0x80 | (((val >> 6) & 0x3F) as u8);
4755        out[3] = 0x80 | ((val & 0x3F) as u8);
4756        4
4757    } else {
4758        out[0] = 0;
4759        1
4760    }
4761}
4762
4763/// Detect the character encoding of a buffer from its initial bytes.
4764///
4765/// # UPSTREAM-PARITY
4766///
4767/// ```c
4768/// xmlCharEncoding xmlDetectCharEncoding(const unsigned char *in, int len);
4769/// ```
4770///
4771/// # SAFETY
4772///
4773/// - `in_` must be valid pointers (or NULL
4774///   where the upstream C contract allows), obtained from the
4775///   matching constructor/owner and not yet freed; the callee may
4776///   take or keep ownership exactly as the C API specifies.
4777///
4778/// The caller must not race this call with concurrent mutation of the
4779/// same objects from other threads (per-object state is not internally
4780/// synchronized). Violating any of the above is undefined behavior.
4781///
4782/// Exercised by the C-API differential courts
4783/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4784/// courts; those pass byte-for-byte against the upstream oracle.
4785#[no_mangle]
4786pub const unsafe extern "C" fn xmlDetectCharEncoding(in_: *const c_uchar, len: c_int) -> c_int {
4787    if in_.is_null() {
4788        return xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int;
4789    }
4790    unsafe {
4791        if len >= 4 {
4792            if *in_ == 0x00 && *in_.add(1) == 0x00 && *in_.add(2) == 0x00 && *in_.add(3) == 0x3C {
4793                return xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE as c_int;
4794            }
4795            if *in_ == 0x3C && *in_.add(1) == 0x00 && *in_.add(2) == 0x00 && *in_.add(3) == 0x00 {
4796                return xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE as c_int;
4797            }
4798            if *in_ == 0x4C && *in_.add(1) == 0x6F && *in_.add(2) == 0xA7 && *in_.add(3) == 0x94 {
4799                return xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC as c_int;
4800            }
4801            if *in_ == 0x3C && *in_.add(1) == 0x3F && *in_.add(2) == 0x78 && *in_.add(3) == 0x6D {
4802                return xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int;
4803            }
4804            if *in_ == 0x3C && *in_.add(1) == 0x00 && *in_.add(2) == 0x3F && *in_.add(3) == 0x00 {
4805                return xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE as c_int;
4806            }
4807            if *in_ == 0x00 && *in_.add(1) == 0x3C && *in_.add(2) == 0x00 && *in_.add(3) == 0x3F {
4808                return xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE as c_int;
4809            }
4810        }
4811        if len >= 3 && *in_ == 0xEF && *in_.add(1) == 0xBB && *in_.add(2) == 0xBF {
4812            return xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int;
4813        }
4814        if len >= 2 {
4815            if *in_ == 0xFE && *in_.add(1) == 0xFF {
4816                return xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE as c_int;
4817            }
4818            if *in_ == 0xFF && *in_.add(1) == 0xFE {
4819                return xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE as c_int;
4820            }
4821        }
4822    }
4823    xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int
4824}
4825
4826/// Convert the first line of `in` using the encoding handler, appending the
4827/// result to `out`.
4828///
4829/// # UPSTREAM-PARITY
4830///
4831/// ```c
4832/// int xmlCharEncFirstLine(xmlCharEncodingHandlerPtr handler,
4833///                         struct _xmlBuffer *out, struct _xmlBuffer *in);
4834/// ```
4835///
4836/// # SAFETY
4837///
4838/// - `handler`, `out`, `in_` must be valid pointers (or NULL
4839///   where the upstream C contract allows), obtained from the
4840///   matching constructor/owner and not yet freed; the callee may
4841///   take or keep ownership exactly as the C API specifies.
4842///
4843/// The caller must not race this call with concurrent mutation of the
4844/// same objects from other threads (per-object state is not internally
4845/// synchronized). Violating any of the above is undefined behavior.
4846///
4847/// Exercised by the C-API differential courts
4848/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4849/// courts; those pass byte-for-byte against the upstream oracle.
4850#[no_mangle]
4851pub unsafe extern "C" fn xmlCharEncFirstLine(
4852    handler: *mut _xmlCharEncodingHandler,
4853    out: *mut _xmlBuffer,
4854    in_: *mut _xmlBuffer,
4855) -> c_int {
4856    encoding::xmlCharEncInFunc(handler, out, in_)
4857}
4858
4859/// Check whether the current thread is the main thread.
4860///
4861/// # UPSTREAM-PARITY
4862///
4863/// ```c
4864/// int xmlIsMainThread(void);
4865/// ```
4866///
4867/// # SAFETY
4868///
4869/// The function touches crate-global state only; it is safe
4870/// as long as the caller respects the library's global
4871/// initialization/cleanup ordering (xmlInitParser before use,
4872/// xmlCleanupParser only after all users are done).
4873///
4874/// Violating the global lifecycle ordering, or calling this after
4875/// teardown or from a signal handler, is undefined behavior.
4876#[no_mangle]
4877pub const unsafe extern "C" fn xmlIsMainThread() -> c_int {
4878    1
4879}
4880
4881// ═══════════════════════════════════════════════════════════════════════════════
4882// Error reporting helpers (xmlerror.h)
4883// ═══════════════════════════════════════════════════════════════════════════════
4884
4885/// Print file and line information for a parser input to the generic error
4886/// channel.
4887///
4888/// # UPSTREAM-PARITY
4889///
4890/// ```c
4891/// void xmlParserPrintFileInfo(struct _xmlParserInput *input);
4892/// ```
4893///
4894/// # SAFETY
4895///
4896/// - `input` must be valid pointers (or NULL
4897///   where the upstream C contract allows), obtained from the
4898///   matching constructor/owner and not yet freed; the callee may
4899///   take or keep ownership exactly as the C API specifies.
4900///
4901/// The caller must not race this call with concurrent mutation of the
4902/// same objects from other threads (per-object state is not internally
4903/// synchronized). Violating any of the above is undefined behavior.
4904///
4905/// Exercised by the C-API differential courts
4906/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4907/// courts; those pass byte-for-byte against the upstream oracle.
4908#[no_mangle]
4909pub unsafe extern "C" fn xmlParserPrintFileInfo(input: *mut _xmlParserInput) {
4910    if input.is_null() {
4911        return;
4912    }
4913    unsafe {
4914        let channel: Option<xmlGenericErrorFunc> = globals::get_generic_error_func();
4915        let data = globals::get_generic_error_ctx();
4916        let Some(ch) = channel else { return };
4917
4918        let msg = if !(*input).filename.is_null() {
4919            let file = CStr::from_ptr((*input).filename);
4920            let s = format!("{}:{}: ", file.to_string_lossy(), (*input).line);
4921            std::ffi::CString::new(s).unwrap_or_default()
4922        } else {
4923            let s = format!("Entity: line {}: ", (*input).line);
4924            std::ffi::CString::new(s).unwrap_or_default()
4925        };
4926        ch(data, msg.as_ptr());
4927    }
4928}
4929
4930/// Print the input context around the current error position to the generic
4931/// error channel.
4932///
4933/// # UPSTREAM-PARITY
4934///
4935/// ```c
4936/// void xmlParserPrintFileContext(struct _xmlParserInput *input);
4937/// ```
4938///
4939/// # SAFETY
4940///
4941/// - `input` must be valid pointers (or NULL
4942///   where the upstream C contract allows), obtained from the
4943///   matching constructor/owner and not yet freed; the callee may
4944///   take or keep ownership exactly as the C API specifies.
4945///
4946/// The caller must not race this call with concurrent mutation of the
4947/// same objects from other threads (per-object state is not internally
4948/// synchronized). Violating any of the above is undefined behavior.
4949///
4950/// Exercised by the C-API differential courts
4951/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4952/// courts; those pass byte-for-byte against the upstream oracle.
4953#[no_mangle]
4954pub unsafe extern "C" fn xmlParserPrintFileContext(input: *mut _xmlParserInput) {
4955    if input.is_null() || (*input).cur.is_null() {
4956        return;
4957    }
4958    unsafe {
4959        let channel: Option<xmlGenericErrorFunc> = globals::get_generic_error_func();
4960        let data = globals::get_generic_error_ctx();
4961        let Some(ch) = channel else { return };
4962
4963        let pi = &*input;
4964        let cur = pi.cur;
4965        let base = pi.base;
4966        let end = pi.end;
4967
4968        // Build a window of up to 80 bytes ending at `cur`.
4969        let before = if base.is_null() {
4970            0
4971        } else {
4972            (cur as usize).saturating_sub(base as usize)
4973        };
4974        let take = before.min(LINE_LEN);
4975        let start = cur.sub(take);
4976        let n = (end as usize).saturating_sub(start as usize).min(LINE_LEN);
4977
4978        let mut content = vec![0u8; n];
4979        if n > 0 {
4980            ptr::copy_nonoverlapping(start, content.as_mut_ptr(), n);
4981        }
4982        let line = std::ffi::CString::new(content.clone()).unwrap_or_default();
4983        ch(data, line.as_ptr());
4984
4985        // Caret line pointing at the current character.
4986        let mut caret = vec![b' '; take];
4987        if take < LINE_LEN + 1 {
4988            caret.push(b'^');
4989        }
4990        let caret_c = std::ffi::CString::new(caret).unwrap_or_default();
4991        ch(data, caret_c.as_ptr());
4992    }
4993}
4994
4995// ═══════════════════════════════════════════════════════════════════════════════
4996// SAX/DTD parse front-ends
4997// ═══════════════════════════════════════════════════════════════════════════════
4998
4999/// Handle an entity reference by pushing the entity's content as a new input
5000/// stream (deprecated internal API).
5001///
5002/// # UPSTREAM-PARITY
5003///
5004/// ```c
5005/// void xmlHandleEntity(xmlParserCtxtPtr ctxt, xmlEntityPtr entity);
5006/// ```
5007///
5008/// # SAFETY
5009///
5010/// - `ctxt`, `entity` must be valid pointers (or NULL
5011///   where the upstream C contract allows), obtained from the
5012///   matching constructor/owner and not yet freed; the callee may
5013///   take or keep ownership exactly as the C API specifies.
5014///
5015/// The caller must not race this call with concurrent mutation of the
5016/// same objects from other threads (per-object state is not internally
5017/// synchronized). Violating any of the above is undefined behavior.
5018///
5019/// Exercised by the C-API differential courts
5020/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5021/// courts; those pass byte-for-byte against the upstream oracle.
5022#[no_mangle]
5023pub unsafe extern "C" fn xmlHandleEntity(ctxt: *mut _xmlParserCtxt, entity: *mut c_void) {
5024    if ctxt.is_null() {
5025        return;
5026    }
5027    unsafe {
5028        let ent = entity as *mut _xmlEntity;
5029        if ent.is_null() {
5030            return;
5031        }
5032        // Unparsed entities cannot be included by reference.
5033        if (*ent).etype == xmlEntityType::XML_EXTERNAL_GENERAL_UNPARSED_ENTITY as c_int {
5034            return;
5035        }
5036
5037        let mut input = ptr::null_mut();
5038        if !(*ent).content.is_null() {
5039            // Internal entity: push its replacement text as a new stream.
5040            let content = (*ent).content;
5041            let pi = xmlNewInputStream(ctxt);
5042            if pi.is_null() {
5043                return;
5044            }
5045            let len = string::xml_strlen(content);
5046            (*pi).base = content;
5047            (*pi).cur = content;
5048            (*pi).end = content.add(len);
5049            (*pi).length = len as c_int;
5050            (*pi).entity = ent;
5051            input = pi;
5052        } else if !(*ent).URI.is_null() {
5053            // External parsed entity: load it through the entity loader.
5054            input = xmlLoadExternalEntity(
5055                (*ent).URI as *const c_char,
5056                (*ent).ExternalID as *const c_char,
5057                ctxt,
5058            );
5059            if !input.is_null() {
5060                (*input).entity = ent;
5061            }
5062        }
5063
5064        if input.is_null() {
5065            return;
5066        }
5067        xmlPushInput(ctxt, input);
5068    }
5069}
5070
5071/// Load and parse a DTD, returning the resulting `xmlDtd` (detached from any
5072/// document).
5073///
5074/// # UPSTREAM-PARITY
5075///
5076/// ```c
5077/// xmlDtdPtr xmlSAXParseDTD(xmlSAXHandlerPtr sax, const xmlChar *publicId,
5078///                          const xmlChar *systemId);
5079/// ```
5080///
5081/// # SAFETY
5082///
5083/// - `sax` must be valid pointers (or NULL
5084///   where the upstream C contract allows), obtained from the
5085///   matching constructor/owner and not yet freed; the callee may
5086///   take or keep ownership exactly as the C API specifies.
5087///
5088/// - `publicId`, `systemId` must point to valid NUL-terminated
5089///   strings (or NULL where the C contract allows) for the lifetime
5090///   of the call.
5091///
5092/// The caller must not race this call with concurrent mutation of the
5093/// same objects from other threads (per-object state is not internally
5094/// synchronized). Violating any of the above is undefined behavior.
5095///
5096/// Exercised by the C-API differential courts
5097/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5098/// courts; those pass byte-for-byte against the upstream oracle.
5099#[no_mangle]
5100pub unsafe extern "C" fn xmlSAXParseDTD(
5101    sax: *mut _xmlSAXHandler,
5102    publicId: *const xmlChar,
5103    systemId: *const xmlChar,
5104) -> *mut _xmlDtd {
5105    if publicId.is_null() && systemId.is_null() {
5106        return ptr::null_mut();
5107    }
5108    unsafe {
5109        let ctxt = xmlNewSAXParserCtxt(sax, ptr::null_mut());
5110        if ctxt.is_null() {
5111            return ptr::null_mut();
5112        }
5113        apply_options(ctxt, XML_PARSE_DTDLOAD);
5114
5115        // Resolve via the SAX resolveEntity callback when available, else
5116        // load the system ID directly.
5117        let mut input = ptr::null_mut();
5118        if !sax.is_null() {
5119            if let Some(resolve) = (*sax).resolveEntity {
5120                input = resolve((*ctxt).userData, publicId, systemId);
5121            }
5122        }
5123        if input.is_null() {
5124            if systemId.is_null() {
5125                helpers::free_parser_ctxt(ctxt);
5126                return ptr::null_mut();
5127            }
5128            input = xmlLoadExternalEntity(systemId as *const c_char, ptr::null(), ctxt);
5129        }
5130        if input.is_null() {
5131            helpers::free_parser_ctxt(ctxt);
5132            return ptr::null_mut();
5133        }
5134
5135        // Materialise the DTD text before freeing the input struct.
5136        let data: Vec<u8> = {
5137            let pi = &*input;
5138            if !pi.base.is_null() && !pi.end.is_null() && pi.end >= pi.base {
5139                let len = (pi.end as usize).saturating_sub(pi.base as usize);
5140                core::slice::from_raw_parts(pi.base, len).to_vec()
5141            } else if !pi.buf.is_null() {
5142                input_buffer_data(pi.buf)
5143            } else {
5144                Vec::new()
5145            }
5146        };
5147        helpers::free_parser_input(input);
5148
5149        let dtd = parse_dtd_text(ctxt, &data, publicId, systemId);
5150        helpers::free_parser_ctxt(ctxt);
5151        dtd
5152    }
5153}
5154
5155/// Load and parse a DTD from an input buffer.
5156///
5157/// # UPSTREAM-PARITY
5158///
5159/// ```c
5160/// xmlDtdPtr xmlIOParseDTD(xmlSAXHandlerPtr sax, xmlParserInputBufferPtr input,
5161///                         xmlCharEncoding enc);
5162/// ```
5163///
5164/// # SAFETY
5165///
5166/// - `sax`, `input` must be valid pointers (or NULL
5167///   where the upstream C contract allows), obtained from the
5168///   matching constructor/owner and not yet freed; the callee may
5169///   take or keep ownership exactly as the C API specifies.
5170///
5171/// The caller must not race this call with concurrent mutation of the
5172/// same objects from other threads (per-object state is not internally
5173/// synchronized). Violating any of the above is undefined behavior.
5174///
5175/// Exercised by the C-API differential courts
5176/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5177/// courts; those pass byte-for-byte against the upstream oracle.
5178#[no_mangle]
5179pub unsafe extern "C" fn xmlIOParseDTD(
5180    sax: *mut _xmlSAXHandler,
5181    input: *mut _xmlParserInputBuffer,
5182    enc: c_int,
5183) -> *mut _xmlDtd {
5184    if input.is_null() {
5185        return ptr::null_mut();
5186    }
5187    unsafe {
5188        let ctxt = xmlNewSAXParserCtxt(sax, ptr::null_mut());
5189        if ctxt.is_null() {
5190            io::input_buffer_free(input);
5191            return ptr::null_mut();
5192        }
5193        apply_options(ctxt, XML_PARSE_DTDLOAD);
5194        if enc != xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int {
5195            (*ctxt).charset = enc;
5196        }
5197
5198        // Materialise the data from the input buffer.
5199        let data: Vec<u8> = input_buffer_data(input);
5200        io::input_buffer_free(input);
5201
5202        let dtd = parse_dtd_text(ctxt, &data, ptr::null(), ptr::null());
5203        helpers::free_parser_ctxt(ctxt);
5204        dtd
5205    }
5206}
5207
5208/// Extract the buffered data of an input buffer as an owned byte vector.
5209unsafe fn input_buffer_data(buf: *mut _xmlParserInputBuffer) -> Vec<u8> {
5210    unsafe {
5211        if buf.is_null() {
5212            return Vec::new();
5213        }
5214        let b = &*buf;
5215        if let Some(read) = b.readcallback {
5216            let mut out = Vec::new();
5217            let mut tmp = [0u8; 4096];
5218            loop {
5219                let n = read(
5220                    b.context,
5221                    tmp.as_mut_ptr() as *mut c_char,
5222                    tmp.len() as c_int,
5223                );
5224                if n <= 0 {
5225                    break;
5226                }
5227                out.extend_from_slice(&tmp[..n as usize]);
5228            }
5229            return out;
5230        }
5231        if !b.buffer.is_null() {
5232            let xbuf = &*(b.buffer as *mut _xmlBuffer);
5233            if !xbuf.content.is_null() && xbuf.use_ > 0 {
5234                return core::slice::from_raw_parts(xbuf.content, xbuf.use_ as usize).to_vec();
5235            }
5236        }
5237        Vec::new()
5238    }
5239}
5240
5241/// Parse an external general entity and build a tree.
5242///
5243/// # UPSTREAM-PARITY
5244///
5245/// ```c
5246/// xmlDocPtr xmlSAXParseEntity(xmlSAXHandlerPtr sax, const char *filename);
5247/// ```
5248///
5249/// # SAFETY
5250///
5251/// - `sax` must be valid pointers (or NULL
5252///   where the upstream C contract allows), obtained from the
5253///   matching constructor/owner and not yet freed; the callee may
5254///   take or keep ownership exactly as the C API specifies.
5255///
5256/// - `filename` must point to valid NUL-terminated
5257///   strings (or NULL where the C contract allows) for the lifetime
5258///   of the call.
5259///
5260/// The caller must not race this call with concurrent mutation of the
5261/// same objects from other threads (per-object state is not internally
5262/// synchronized). Violating any of the above is undefined behavior.
5263///
5264/// Exercised by the C-API differential courts
5265/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5266/// courts; those pass byte-for-byte against the upstream oracle.
5267#[no_mangle]
5268pub unsafe extern "C" fn xmlSAXParseEntity(
5269    sax: *mut _xmlSAXHandler,
5270    filename: *const c_char,
5271) -> *mut _xmlDoc {
5272    if filename.is_null() {
5273        return ptr::null_mut();
5274    }
5275    unsafe {
5276        let ctxt = xmlNewSAXParserCtxt(sax, ptr::null_mut());
5277        if ctxt.is_null() {
5278            return ptr::null_mut();
5279        }
5280        let input = match open_filename_routed(filename, ctxt) {
5281            RoutedFileOpen::Loaded(i) => i,
5282            RoutedFileOpen::Failed => {
5283                emit_io_warning(ctxt, io_load_failure_message(filename));
5284                helpers::free_parser_ctxt(ctxt);
5285                return ptr::null_mut();
5286            }
5287            RoutedFileOpen::EntityLoaderFailed => {
5288                helpers::free_parser_ctxt(ctxt);
5289                return ptr::null_mut();
5290            }
5291            RoutedFileOpen::Builtin => match helpers::input_from_file(filename) {
5292                Ok(i) => i,
5293                Err(_) => {
5294                    helpers::free_parser_ctxt(ctxt);
5295                    return ptr::null_mut();
5296                }
5297            },
5298        };
5299        helpers::setup_parser_input(ctxt, input);
5300        let rc = helpers::parse_document(ctxt);
5301        let doc = (*ctxt).myDoc;
5302        (*ctxt).myDoc = ptr::null_mut();
5303        if rc != 0 || (*ctxt).wellFormed == 0 {
5304            if !doc.is_null() {
5305                tree::free_doc(doc);
5306            }
5307            helpers::free_parser_ctxt(ctxt);
5308            return ptr::null_mut();
5309        }
5310        helpers::free_parser_ctxt(ctxt);
5311        doc
5312    }
5313}
5314
5315// ═══════════════════════════════════════════════════════════════════════════════
5316// C14N: xmlC14NDocSave
5317// ═══════════════════════════════════════════════════════════════════════════════
5318
5319/// Canonicalise a document (or node set) and save it to a file.
5320///
5321/// # UPSTREAM-PARITY
5322///
5323/// ```c
5324/// int xmlC14NDocSave(xmlDocPtr doc, xmlNodeSetPtr nodes, int mode,
5325///                    xmlChar **inclusive_ns_prefixes, int with_comments,
5326///                    const char *filename, int compression);
5327/// ```
5328///
5329/// # SAFETY
5330///
5331/// - `doc`, `nodes`, `inclusive_ns_prefixes` must be valid pointers (or NULL
5332///   where the upstream C contract allows), obtained from the
5333///   matching constructor/owner and not yet freed; the callee may
5334///   take or keep ownership exactly as the C API specifies.
5335///
5336/// - `filename` must point to valid NUL-terminated
5337///   strings (or NULL where the C contract allows) for the lifetime
5338///   of the call.
5339///
5340/// The caller must not race this call with concurrent mutation of the
5341/// same objects from other threads (per-object state is not internally
5342/// synchronized). Violating any of the above is undefined behavior.
5343///
5344/// Exercised by the C-API differential courts
5345/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5346/// courts; those pass byte-for-byte against the upstream oracle.
5347#[no_mangle]
5348pub unsafe extern "C" fn xmlC14NDocSave(
5349    doc: *mut _xmlDoc,
5350    nodes: *mut _xmlNodeSet,
5351    mode: c_int,
5352    inclusive_ns_prefixes: *mut *mut xmlChar,
5353    with_comments: c_int,
5354    filename: *const c_char,
5355    compression: c_int,
5356) -> c_int {
5357    if filename.is_null() {
5358        return -1;
5359    }
5360    unsafe {
5361        let output = io::output_buffer_create_filename(filename, ptr::null_mut(), compression);
5362        if output.is_null() {
5363            return -1;
5364        }
5365        let ret = crate::xml::c14n::xmlC14NDocSaveTo(
5366            doc,
5367            nodes,
5368            mode,
5369            inclusive_ns_prefixes,
5370            with_comments,
5371            output,
5372        );
5373        if ret < 0 {
5374            io::output_buffer_close(output);
5375            return -1;
5376        }
5377        let close_ret = io::output_buffer_close(output);
5378        if close_ret < 0 {
5379            -1
5380        } else {
5381            ret
5382        }
5383    }
5384}
5385
5386#[cfg(test)]
5387mod tests {
5388    use super::*;
5389
5390    /// A read-callback state pair mirroring PHP's streams IO loader: the
5391    /// registered `xmlParserInputBufferCreateFilenameDefault` serves bytes
5392    /// through an `xmlParserInputBufferCreateIO` buffer (php builds exactly
5393    /// this shape with php_libxml_streams_IO_read/close over a php_stream).
5394    /// The loader reaches the state through a thread-local pointer (the
5395    /// loader slot itself is per-thread TLS, so there is no cross-thread
5396    /// aliasing).
5397    struct ServeState {
5398        data: &'static [u8],
5399        pos: usize,
5400        closed: bool,
5401    }
5402
5403    thread_local! {
5404        static SERVE_STATE: std::cell::Cell<*mut ServeState> =
5405            std::cell::Cell::new(std::ptr::null_mut());
5406    }
5407
5408    unsafe extern "C" fn serve_read(ctx: *mut c_void, buffer: *mut c_char, len: c_int) -> c_int {
5409        // SAFETY: ctx is the ServeState set up by the test; buffer is a
5410        // writable len-byte region per the xmlInputReadCallback contract.
5411        let st = unsafe { &mut *(ctx as *mut ServeState) };
5412        if st.pos >= st.data.len() {
5413            return 0;
5414        }
5415        let n = (len as usize).min(st.data.len() - st.pos);
5416        unsafe {
5417            core::ptr::copy_nonoverlapping(st.data.as_ptr().add(st.pos), buffer as *mut u8, n);
5418        }
5419        st.pos += n;
5420        n as c_int
5421    }
5422
5423    unsafe extern "C" fn serve_close(ctx: *mut c_void) -> c_int {
5424        // SAFETY: ctx is the ServeState set up by the test.
5425        let st = unsafe { &mut *(ctx as *mut ServeState) };
5426        st.closed = true;
5427        0
5428    }
5429
5430    /// The php-shaped loader: build an IO buffer over the thread-local serve
5431    /// state. `uri` is deliberately ignored — php's loader opens whatever the
5432    /// php streams layer resolves, so a "file://" URI or a non-existent path
5433    /// both reach the stream; this guard proves the ENGINE consults the
5434    /// loader instead of the built-in path (which would fail on the bogus
5435    /// URI used here).
5436    unsafe extern "C" fn serving_loader(
5437        _uri: *const c_char,
5438        _enc: c_int,
5439    ) -> *mut _xmlParserInputBuffer {
5440        SERVE_STATE.with(|cell| {
5441            let st = cell.get();
5442            if st.is_null() {
5443                return ptr::null_mut();
5444            }
5445            crate::abi::exports_xml2::xmlParserInputBufferCreateIO(
5446                Some(serve_read as xmlInputReadCallback),
5447                Some(serve_close as xmlInputCloseCallback),
5448                st as *mut c_void,
5449                xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int,
5450            )
5451        })
5452    }
5453
5454    unsafe extern "C" fn record_message(ctx: *mut c_void, err: *const _xmlError) {
5455        if err.is_null() {
5456            return;
5457        }
5458        // SAFETY: ctx is the recording Vec set up by the test; the error is
5459        // live for the call and its message is NUL-terminated.
5460        let out = unsafe { &mut *(ctx as *mut Vec<u8>) };
5461        let msg = unsafe { (*err).message };
5462        if !msg.is_null() {
5463            // SAFETY: message is a NUL-terminated C string for the call.
5464            let bytes = unsafe { std::ffi::CStr::from_ptr(msg) }.to_bytes();
5465            out.extend_from_slice(bytes);
5466        }
5467    }
5468
5469    /// SP-14.3.2 S8 / dom-L2 (bug79971_1): a registered
5470    /// `xmlParserInputBufferCreateFilenameDefault` (PHP's streams loader) is
5471    /// consulted by the main-document file open (`xmlReadFile`/
5472    /// `xmlCtxtReadFile` -> xmlNewInputFromFile -> xmlNewInputFromUrl): its
5473    /// bytes are parsed even when the URI is not a real file, and a NULL
5474    /// loader result reports the xmlCtxtErrIO "failed to load" warning with
5475    /// NO built-in fallback.
5476    ///
5477    /// # Safety
5478    ///
5479    /// - the callbacks and stack state are valid for the duration of each
5480    ///   call; the loader/generic-handler TLS slots are restored before the
5481    ///   test ends (serialized via the error-handler test lock).
5482    #[test]
5483    fn test_main_doc_open_consults_registered_input_loader() {
5484        use crate::xml::globals::ERROR_HANDLER_TEST_LOCK;
5485
5486        // Serialize against the handler-slot tests (the generic func slot is
5487        // shared global state); the loader slot is this thread's TLS but it
5488        // is restored so later engine state stays pristine.
5489        let _guard = ERROR_HANDLER_TEST_LOCK.lock();
5490        let old_loader = globals::get_parser_input_buffer_create_filename_value();
5491        let old_struct = globals::get_structured_error_func();
5492        let old_struct_ctx = globals::get_structured_error_ctx();
5493
5494        let mut captured: Vec<u8> = Vec::new();
5495        let captured_ptr = &mut captured as *mut Vec<u8> as *mut c_void;
5496        // SAFETY: set/restore of the handler slots is serialized under
5497        // ERROR_HANDLER_TEST_LOCK for the test's duration.
5498        unsafe {
5499            globals::set_structured_error_func(
5500                captured_ptr,
5501                Some(record_message as xmlStructuredErrorFunc),
5502            );
5503        }
5504
5505        unsafe {
5506            let mut serve = ServeState {
5507                data: b"<root><a>1</a></root>",
5508                pos: 0,
5509                closed: false,
5510            };
5511            SERVE_STATE.with(|cell| cell.set(&mut serve as *mut ServeState));
5512            globals::set_parser_input_buffer_create_filename_value(Some(serving_loader));
5513
5514            // The URI names no real file — only the loader can satisfy it.
5515            let ctxt = helpers::create_parser_ctxt();
5516            assert!(!ctxt.is_null());
5517            let doc = xmlCtxtReadFile(
5518                ctxt,
5519                c"file:///definitely-not-a-file.xml".as_ptr(),
5520                ptr::null(),
5521                0,
5522            );
5523            assert!(
5524                !doc.is_null(),
5525                "registered loader must be consulted for the main document open"
5526            );
5527            let root = (*doc).children;
5528            assert!(
5529                !root.is_null() && !(*root).name.is_null(),
5530                "served document must produce a root element"
5531            );
5532            assert_eq!(
5533                crate::xml::string::xmlstr_to_bytes((*root).name as *const u8),
5534                b"root",
5535                "document served by the loader must be parsed"
5536            );
5537            assert!(serve.closed, "loader stream must be closed exactly once");
5538            tree::free_doc(doc);
5539            helpers::free_parser_ctxt(ctxt);
5540
5541            // A loader result of NULL is XML_IO_ENOENT: the built-in open is
5542            // NOT attempted and the xmlCtxtErrIO ENOENT report ("failed to
5543            // load") reaches the structured handler.
5544            globals::set_parser_input_buffer_create_filename_value(None);
5545            SERVE_STATE.with(|cell| cell.set(ptr::null_mut()));
5546            unsafe extern "C" fn null_loader(
5547                _uri: *const c_char,
5548                _enc: c_int,
5549            ) -> *mut _xmlParserInputBuffer {
5550                ptr::null_mut()
5551            }
5552            globals::set_parser_input_buffer_create_filename_value(Some(null_loader));
5553            let ctxt2 = helpers::create_parser_ctxt();
5554            assert!(!ctxt2.is_null());
5555            let doc2 = xmlCtxtReadFile(
5556                ctxt2,
5557                c"file:///definitely-not-a-file.xml".as_ptr(),
5558                ptr::null(),
5559                0,
5560            );
5561            assert!(doc2.is_null(), "NULL loader result must fail the open");
5562            let got = String::from_utf8_lossy(&captured);
5563            assert!(
5564                got.contains("failed to load"),
5565                "xmlCtxtErrIO report must reach the error channel: {got:?}"
5566            );
5567            helpers::free_parser_ctxt(ctxt2);
5568        }
5569
5570        // Restore both slots.
5571        unsafe {
5572            globals::set_parser_input_buffer_create_filename_value(old_loader);
5573            globals::set_structured_error_func(old_struct_ctx, old_struct);
5574        }
5575    }
5576}