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