Skip to main content

libxml_rs/abi/
exports_xml2.rs

1//! C ABI exports for libxml2.so.2 — no_mangle extern "C" functions (§1, §16).
2//!
3//! This module contains all `#[no_mangle] pub extern "C"` function definitions
4//! that form the public ABI of libxml2.so.2. Every function here corresponds to
5//! a function in the upstream libxml2 headers.
6//!
7//! # Phase 1 status
8//!
9//! Complete — all major ABI entry points are implemented. Functions that require
10//! modules not yet implemented (tree, parser, etc.) call into those modules,
11//! which will be filled in as Phase 1 continues.
12//!
13//! # Organization
14//!
15//! Exports are grouped by subsystem in the order they appear in upstream headers:
16//!
17//! 1. Initialization / Cleanup
18//! 2. Version
19//! 3. Memory / Allocator
20//! 4. Error handling
21//! 5. String utilities
22//! 6. Tree (document, node, attribute, namespace, DTD, entity)
23//! 7. Parser (SAX, DOM, push, reader)
24//! 8. I/O
25//! 9. Dictionary
26//! 10. Hash table
27//! 11. List
28//! 12. Buffer
29//! 13. Encoding
30//! 14. XPath
31//! 15. XInclude
32//! 16. Catalog
33//! 17. HTML
34//! 18. Debug/misc
35//!
36//! # Upstream contract
37//!
38//! The parity target is the complete libxml2.so.2 export surface of the oracle
39//! DSO (libxml2 2.15.3): `globals.c`, `parser.c`, `threads.c`, `encoding.c`,
40//! `xmlmemory.c` and `xmlstring.c` entry points with upstream header
41//! signatures. Residuals R-000116, R-000117, R-000132, R-000133, R-000135,
42//! R-000136, R-000138, R-000157, R-000161, R-000162, R-000163, R-000164 and
43//! R-000165 all touch this module.
44//!
45//! # Conceptual behavior
46//!
47//! This module implements the core libxml2 ABI: init/cleanup, version, memory,
48//! error handling, string utilities, tree/document construction, parser entry
49//! points, I/O, dict/hash/list, buffer, encoding, XPath and catalog — grouped
50//! by subsystem in upstream header order. Functions needing the engine call
51//! into `src/xml/*`; the rest are faithful ports.
52//!
53//! # Ownership & safety invariants
54//!
55//! Ownership follows OWNERSHIP_ATLAS: docs/nodes/strings returned to C are
56//! caller-owned (freed with the documented xmlFree-family); `xmlReadMemory`/
57//! `xmlCtxtRead*` propagate the URL into the owned input filename (R-000161);
58//! the error channel routes through the exported handler slots
59//! (R-000161/R-000163). The deprecated init/cleanup no-ops are deliberate:
60//! upstream bodies are empty (R-000138).
61//!
62//! # Historical quirks & epochs
63//!
64//! QUIRK-0001: 2.9.0 default parser limits (commit `52d8ade7`); E-002/E-005:
65//! parse-error diagnostics and exit codes across 2.9.10-2.13.0; R-000138: the
66//! deprecated no-op entry points dispositioned as the oracles own behavior;
67//! R-000161: the five wrong default values fixed (e.g. `xmlParserVersion` =
68//! `21503-GITv2.15.3`); R-000162: allocator entry points exported as DATA;
69//! R-000157: iconv-only encodings report XML_ERR_UNSUPPORTED_ENCODING (no
70//! iconv backend — OPEN residual, UNRESOLVED since 11.1-Z.1: the executed
71//! Iconv+ICU-enabled oracle serves those encodings, so closing requires an
72//! iconv/ICU backend rather than a waiver).
73//!
74//! # Deliberate oddities
75//!
76//! The no-op init/cleanup exports (R-000138), the `xmlGenericError`/
77//! `xsltGenericError` variadic asm stderr printers (R-000161), and the
78//! XML_ERR_UNSUPPORTED_ENCODING divergence for iconv/ICU-only encodings
79//! (R-000157, OPEN/UNRESOLVED) are the deliberate oddities of this module.
80//!
81//! # Proving courts
82//!
83//! ABI-DATA, ALLOCATOR, GLOBAL-STATE, PARSER and THREADING court families;
84//! the data-ABI probes (CALLBACK-001, ERROR-001, TREE-001, ENCODING-001, ...)
85//! require byte-identical output vs the oracle DSO; DSO-LOADER and
86//! HEADER-COMPILE close the surface.
87//!
88//! # Tempting simplifications that would break parity
89//!
90//! A tempting simplification is to delete the deprecated no-op exports as dead
91//! code — R-000138 records they ARE the upstream observable behavior and
92//! downstream linking depends on them. Another shortcut, defaulting
93//! `xmlGenericError` to NULL instead of the variadic stderr printer, is the
94//! pre-R-000161 state that broke counting handlers (err-count 1 vs oracle 6).
95//! Both must not be simplified.
96
97#![allow(non_snake_case)]
98#![allow(unused_variables)]
99#![allow(clippy::missing_safety_doc)]
100#![allow(clippy::not_unsafe_ptr_arg_deref)]
101
102// SAFETY-SCOPE: EXPORT-XML2-MECHANICAL-001
103// (11.1-Z.3 proof scope, classified-generated) — this module is the
104// mechanical extern-"C" export surface: every `unsafe` block in it is
105// the documented indirection/registry-access pattern whose validity
106// rests on the upstream C contract, and the exported signatures are
107// machine-measured by the ABI-FUNCTION-SIGNATURE and DSO-LOADER
108// courts and the C-API differential probes. The safety contract of
109// each export is stated in its own doc comment; this scope covers the
110// mechanical wrappers' unsafe blocks.
111
112use core::ffi::c_void;
113use core::ptr;
114use once_cell::sync::Lazy;
115use parking_lot::Mutex;
116use std::collections::HashMap;
117use std::ffi::{CStr, CString};
118use std::mem::size_of;
119use std::os::raw::{c_char, c_int, c_long, c_uint, c_ulong};
120
121use crate::xml::xpath::ast::CompiledExpr;
122use crate::xml::xpath::context::{BoxedXPathFunction, XPathContext};
123use crate::xml::xpath::types::{NodeSet, XPathValue};
124
125use crate::abi::allocator::*;
126use crate::abi::callbacks::*;
127use crate::abi::structs::*;
128use crate::abi::types::*;
129
130// ═══════════════════════════════════════════════════════════════════════════════
131// 1. Initialization / Cleanup
132// ═══════════════════════════════════════════════════════════════════════════════
133
134/// Initialize the parser library.
135///
136/// Must be called before any other libxml2 functions.
137/// Safe to call multiple times (reference-counted in modern libxml2).
138///
139/// # UPSTREAM-PARITY
140///
141/// ```c
142/// void xmlInitParser(void);
143/// ```
144#[no_mangle]
145pub unsafe extern "C" fn xmlInitParser() {
146    crate::internal::globals::init_parser();
147}
148
149/// Initialize the global variables module (upstream globals.h).
150///
151/// # UPSTREAM-PARITY
152///
153/// ```c
154/// void xmlInitGlobals(void);
155/// ```
156///
157/// Upstream `xmlInitGlobals` (globals.c) is called once to initialize the
158/// global variable defaults. The candidate's globals are initialized
159/// statically/on first use, so this is a no-op that exists for ABI
160/// compatibility.
161#[no_mangle]
162pub const unsafe extern "C" fn xmlInitGlobals() {
163    // Globals are statically initialized in the candidate.
164}
165
166/// Upstream `xmlInitializeGlobalState` (globals.c) — initializes a
167/// `xmlGlobalState` struct; the candidate keeps no global-state struct, so
168/// this is a no-op for ABI compatibility.
169///
170/// # UPSTREAM-PARITY
171///
172/// ```c
173/// void xmlInitializeGlobalState(xmlGlobalStatePtr gs);
174/// ```
175#[no_mangle]
176pub const unsafe extern "C" fn xmlInitializeGlobalState(_gs: *mut c_void) {
177    // No-op: the candidate's globals are statically initialized.
178    // R-000138: upstream globals.c body is empty (lazy init); the no-op
179    // IS the oracle behavior, so this must never become a real initializer.
180}
181
182/// Upstream `xmlInitializeDict` (dict.c) — ensures the dictionary
183/// subsystem is initialized; no-op in the candidate (lazy init).
184///
185/// # UPSTREAM-PARITY
186///
187/// ```c
188/// int xmlInitializeDict(void);
189/// ```
190#[no_mangle]
191pub const extern "C" fn xmlInitializeDict() -> c_int {
192    // R-000138: upstream dict.c xmlInitializeDict is an empty body after lazy
193    // init; returning 0 is the oracle observable behavior.
194    0
195}
196
197/// Upstream `xmlInitializePredefinedEntities` (entities.c) — the
198/// predefined entities (& < > " ') are built lazily by
199/// the candidate; no-op.
200///
201/// # UPSTREAM-PARITY
202///
203/// ```c
204/// void xmlInitializePredefinedEntities(void);
205/// ```
206#[no_mangle]
207pub const extern "C" fn xmlInitializePredefinedEntities() {
208    // No-op: predefined entities are resolved on demand.
209}
210
211/// Upstream `xmlCleanupPredefinedEntities` (entities.c) — no-op in the
212/// candidate (no global entity table to release).
213///
214/// # UPSTREAM-PARITY
215///
216/// ```c
217/// void xmlCleanupPredefinedEntities(void);
218/// ```
219#[no_mangle]
220pub const extern "C" fn xmlCleanupPredefinedEntities() {
221    // No-op.
222}
223
224/// Upstream `xmlDefaultSAXHandlerInit` (SAX2.c) — fills the
225/// `xmlDefaultSAXHandler` global. The candidate's default handler is built
226/// on demand; this initializes the exported default-handler global when it
227/// is added (currently tracked in R-000135). No-op for now.
228///
229/// # UPSTREAM-PARITY
230///
231/// ```c
232/// void xmlDefaultSAXHandlerInit(void);
233/// ```
234#[no_mangle]
235pub const extern "C" fn xmlDefaultSAXHandlerInit() {
236    // The candidate builds default handlers on demand; the exported
237    // xmlDefaultSAXHandler global is part of the R-000135 data closure.
238}
239
240/// The current default SAX version (2), stored as an atomic so callers can
241/// query it without taking a lock.
242static SAX2_DEFAULT_VERSION: core::sync::atomic::AtomicI32 = core::sync::atomic::AtomicI32::new(2);
243/// Set the default SAX version (upstream SAX2.c `xmlSAXDefaultVersion`):
244/// returns the previous default; -1 when the version is not 1 or 2.
245///
246/// # UPSTREAM-PARITY
247///
248/// ```c
249/// int xmlSAXDefaultVersion(int version);
250/// ```
251#[no_mangle]
252pub extern "C" fn xmlSAXDefaultVersion(version: c_int) -> c_int {
253    use core::sync::atomic::Ordering;
254    let ret = SAX2_DEFAULT_VERSION.load(Ordering::Relaxed);
255    if version != 1 && version != 2 {
256        return -1;
257    }
258    SAX2_DEFAULT_VERSION.store(version, Ordering::Relaxed);
259    ret
260}
261
262/// Initialize a SAX handler for a given SAX version (upstream SAX2.c
263/// `xmlSAXVersion`): fills the handler with the default callbacks and sets
264/// `initialized` (XML_SAX2_MAGIC for version 2, 1 for version 1).
265///
266/// # UPSTREAM-PARITY
267///
268/// ```c
269/// int xmlSAXVersion(xmlSAXHandler *hdlr, int version);
270/// ```
271#[no_mangle]
272pub unsafe extern "C" fn xmlSAXVersion(
273    hdlr: *mut crate::abi::structs::_xmlSAXHandler,
274    version: c_int,
275) -> c_int {
276    if hdlr.is_null() {
277        return -1;
278    }
279    if version != 1 && version != 2 {
280        return -1;
281    }
282    // SAFETY: hdlr is non-NULL and writable.
283    unsafe {
284        crate::xml::sax::dispatch::xmlSAX2InitDefaultSAXHandler(hdlr);
285        let h = &mut *hdlr;
286        if version == 2 {
287            h.initialized = crate::abi::constants::XML_SAX2_MAGIC as c_uint;
288        } else {
289            h.initialized = 1;
290        }
291    }
292    0
293}
294
295/// Upstream `xmlHasFeature` (parser.c): returns 1 when the library was
296/// compiled with the requested feature. The candidate enables the full
297/// feature set (see include/libxml/xmlversion.h), so every known feature
298/// reports 1; unknown features report 0.
299///
300/// # UPSTREAM-PARITY
301///
302/// ```c
303/// int xmlHasFeature(xmlFeature feature);
304/// ```
305#[no_mangle]
306pub extern "C" fn xmlHasFeature(feature: c_int) -> c_int {
307    // xmlFeature enum values (upstream xmlversion.h): XML_WITH_* run 1..24
308    // (XML_WITH_THREAD=1 ... XML_WITH_MODULES=24).
309    if (1..=24).contains(&feature) {
310        1
311    } else {
312        0
313    }
314}
315
316/// Clean up the global variables module (upstream globals.h).
317///
318/// # UPSTREAM-PARITY
319///
320/// ```c
321/// void xmlCleanupGlobals(void);
322/// ```
323///
324/// Upstream `xmlCleanupGlobals` frees the global defaults. The candidate
325/// keeps globals alive for the process lifetime (repeated init/cleanup is
326/// reference-counted); no-op for ABI compatibility.
327#[no_mangle]
328pub const unsafe extern "C" fn xmlCleanupGlobals() {
329    // The candidate's globals are process-lifetime statics.
330}
331
332/// Clean up the parser library.
333///
334/// Should be called when the library is no longer needed.
335///
336/// # UPSTREAM-PARITY
337///
338/// ```c
339/// void xmlCleanupParser(void);
340/// ```
341#[no_mangle]
342pub unsafe extern "C" fn xmlCleanupParser() {
343    crate::internal::globals::cleanup_parser();
344}
345
346/// Create a simple mutex (upstream threads.h).
347///
348/// # UPSTREAM-PARITY
349///
350/// ```c
351/// xmlMutexPtr xmlNewMutex(void);
352/// ```
353#[no_mangle]
354pub extern "C" fn xmlNewMutex() -> *mut c_void {
355    crate::xml::threads::new_mutex()
356}
357
358/// Free a simple mutex (upstream threads.h).
359///
360/// # UPSTREAM-PARITY
361///
362/// ```c
363/// void xmlFreeMutex(xmlMutexPtr tok);
364/// ```
365#[no_mangle]
366pub unsafe extern "C" fn xmlFreeMutex(tok: *mut c_void) {
367    crate::xml::threads::free_mutex(tok);
368}
369
370/// Lock a simple mutex (upstream threads.h).
371///
372/// # UPSTREAM-PARITY
373///
374/// ```c
375/// void xmlMutexLock(xmlMutexPtr tok);
376/// ```
377#[no_mangle]
378pub unsafe extern "C" fn xmlMutexLock(tok: *mut c_void) {
379    crate::xml::threads::mutex_lock(tok);
380}
381
382/// Unlock a simple mutex (upstream threads.h).
383///
384/// # UPSTREAM-PARITY
385///
386/// ```c
387/// void xmlMutexUnlock(xmlMutexPtr tok);
388/// ```
389#[no_mangle]
390pub unsafe extern "C" fn xmlMutexUnlock(tok: *mut c_void) {
391    crate::xml::threads::mutex_unlock(tok);
392}
393
394/// Create a recursive mutex (upstream threads.h).
395///
396/// # UPSTREAM-PARITY
397///
398/// ```c
399/// xmlRMutexPtr xmlNewRMutex(void);
400/// ```
401#[no_mangle]
402pub extern "C" fn xmlNewRMutex() -> *mut c_void {
403    crate::xml::threads::new_rmutex()
404}
405
406/// Free a recursive mutex (upstream threads.h).
407///
408/// # UPSTREAM-PARITY
409///
410/// ```c
411/// void xmlFreeRMutex(xmlRMutexPtr tok);
412/// ```
413#[no_mangle]
414pub unsafe extern "C" fn xmlFreeRMutex(tok: *mut c_void) {
415    crate::xml::threads::free_rmutex(tok);
416}
417
418/// Lock a recursive mutex (upstream threads.h).
419///
420/// # UPSTREAM-PARITY
421///
422/// ```c
423/// void xmlRMutexLock(xmlRMutexPtr tok);
424/// ```
425#[no_mangle]
426pub unsafe extern "C" fn xmlRMutexLock(tok: *mut c_void) {
427    crate::xml::threads::rmutex_lock(tok);
428}
429
430/// Unlock a recursive mutex (upstream threads.h).
431///
432/// # UPSTREAM-PARITY
433///
434/// ```c
435/// void xmlRMutexUnlock(xmlRMutexPtr tok);
436/// ```
437#[no_mangle]
438pub unsafe extern "C" fn xmlRMutexUnlock(tok: *mut c_void) {
439    crate::xml::threads::rmutex_unlock(tok);
440}
441
442/// Check the thread-local storage (upstream threads.h `xmlCheckThreadLocalStorage`):
443/// returns 0 when TLS is functional, -1 otherwise. The candidate uses Rust
444/// thread-locals which are always functional.
445///
446/// # UPSTREAM-PARITY
447///
448/// ```c
449/// int xmlCheckThreadLocalStorage(void);
450/// ```
451#[no_mangle]
452pub const extern "C" fn xmlCheckThreadLocalStorage() -> c_int {
453    // R-000138: upstream threads.c body is empty (TLS always works); 0 is the
454    // oracle observable behavior, not a stub.
455    0
456}
457
458/// Initialize threading support.
459///
460/// # UPSTREAM-PARITY
461///
462/// ```c
463/// void xmlInitThreads(void);
464/// ```
465///
466/// Upstream threads.h declares `void xmlInitThreads(void)` (11.1-Z.3
467/// signature court: the pre-Z.3 candidate returned `int`).
468#[no_mangle]
469pub unsafe extern "C" fn xmlInitThreads() {
470    let _ = unsafe { crate::internal::globals::init_threads() };
471}
472
473/// Clean up threading support.
474///
475/// # UPSTREAM-PARITY
476///
477/// ```c
478/// void xmlCleanupThreads(void);
479/// ```
480#[no_mangle]
481pub unsafe extern "C" fn xmlCleanupThreads() {
482    crate::xml::threads::cleanup_threads();
483}
484
485/// Check whether the library has been initialized.
486///
487/// # UPSTREAM-PARITY
488///
489/// ```c
490/// int xmlIsInitialized(void);
491/// ```
492#[no_mangle]
493pub extern "C" fn xmlIsInitialized() -> c_int {
494    if crate::abi::versioning::is_initialized() {
495        1
496    } else {
497        0
498    }
499}
500
501/// Initialize a set of threads (libxml2 compat).
502///
503/// # UPSTREAM-PARITY
504///
505/// ```c
506/// int xmlInitThreads(void);
507/// ```
508/// This is an alias.
509#[no_mangle]
510pub const unsafe extern "C" fn xmlLockLibrary() {
511    crate::xml::threads::lock_library();
512}
513
514/// Unlock the library (libxml2 compat).
515///
516/// # UPSTREAM-PARITY
517///
518/// ```c
519/// void xmlUnlockLibrary(void);
520/// ```
521#[no_mangle]
522pub const unsafe extern "C" fn xmlUnlockLibrary() {
523    crate::xml::threads::unlock_library();
524}
525
526// ═══════════════════════════════════════════════════════════════════════════════
527// 4. Error Handling
528// ═══════════════════════════════════════════════════════════════════════════════
529
530/// Set the generic error handler.
531///
532/// # UPSTREAM-PARITY
533///
534/// ```c
535/// void xmlSetGenericErrorFunc(void *ctx, xmlGenericErrorFunc handler);
536/// ```
537///
538/// # SAFETY
539///
540/// - `handler` must be a valid function pointer or NULL (to reset to default).
541/// - If non-NULL, the handler may be called at any time with `ctx`.
542#[no_mangle]
543pub unsafe extern "C" fn xmlSetGenericErrorFunc(
544    ctx: *mut c_void,
545    handler: Option<crate::abi::callbacks::xmlGenericErrorFunc>,
546) {
547    // SAFETY: Delegates to xml::errors with same safety contract.
548    unsafe { crate::xml::errors::set_generic_error_func(ctx, handler) };
549}
550
551/// Set the structured error handler.
552///
553/// # UPSTREAM-PARITY
554///
555/// ```c
556/// void xmlSetStructuredErrorFunc(void *ctx, xmlStructuredErrorFunc handler);
557/// ```
558///
559/// # SAFETY
560///
561/// - `handler` must be a valid function pointer or NULL.
562#[no_mangle]
563pub unsafe extern "C" fn xmlSetStructuredErrorFunc(
564    ctx: *mut c_void,
565    handler: Option<xmlStructuredErrorFunc>,
566) {
567    // SAFETY: Delegates to xml::errors with same safety contract.
568    unsafe { crate::xml::errors::set_structured_error_func(ctx, handler) };
569}
570
571/// Get the last error for the current thread.
572///
573/// # UPSTREAM-PARITY
574///
575/// ```c
576/// xmlErrorPtr xmlGetLastError(void);
577/// ```
578///
579/// Returns a pointer to the last error, or NULL if no error occurred.
580/// The returned pointer is valid until the next libxml2 call in this thread.
581#[no_mangle]
582pub extern "C" fn xmlGetLastError() -> *mut _xmlError {
583    crate::xml::errors::get_last_error()
584}
585
586/// Get a copy of the last error for the current thread.
587///
588/// # UPSTREAM-PARITY
589///
590/// ```c
591/// xmlErrorPtr xmlCopyError(xmlErrorPtr from, xmlErrorPtr to);
592/// ```
593///
594/// Copies `from` into `to`. Returns 0 on success, -1 on error.
595///
596/// # SAFETY
597///
598/// - `from` and `to` must be valid pointers to `_xmlError` structs, or NULL.
599#[no_mangle]
600pub unsafe extern "C" fn xmlCopyError(from: *const _xmlError, to: *mut _xmlError) -> c_int {
601    // SAFETY: Delegates to xml::errors with same safety contract.
602    unsafe { crate::xml::errors::copy_error(from, to) }
603}
604
605/// Reset an error structure.
606///
607/// # UPSTREAM-PARITY
608///
609/// ```c
610/// void xmlResetError(xmlErrorPtr err);
611/// ```
612///
613/// # SAFETY
614///
615/// - `err` must be a valid pointer to `_xmlError`, or NULL.
616#[no_mangle]
617pub const unsafe extern "C" fn xmlResetError(err: *mut _xmlError) {
618    // SAFETY: Delegates to xml::errors with same safety contract.
619    unsafe { crate::xml::errors::reset_error(err) };
620}
621
622/// Raise a structured error.
623///
624/// This is called internally when an error occurs. It updates the last error
625/// and invokes the structured error handler if one is set.
626///
627/// # SAFETY
628///
629/// - `ctxt` may be NULL (context of the error).
630/// - `domain`, `code`, `level`: valid error codes.
631/// - `msg` must be a valid C string or NULL.
632/// - `file` must be a valid C string or NULL.
633/// - `str1`, `str2`, `str3`: error-related strings (may be NULL).
634#[no_mangle]
635pub unsafe extern "C" fn xmlRaiseError(
636    ctxt: *mut c_void,
637    ctxt2: *mut c_void,
638    ctxt3: *mut c_void,
639    ctxt4: *mut c_void,
640    ctxt5: *mut c_void,
641    domain: c_int,
642    code: c_int,
643    level: c_int,
644    file: *const c_char,
645    line: c_int,
646    str1: *const c_char,
647    str2: *const c_char,
648    str3: *const c_char,
649    int1: c_int,
650    int2: c_int,
651    msg: *const c_char,
652) {
653    // SAFETY: Delegates to xml::errors with same safety contract.
654    unsafe {
655        crate::xml::errors::raise_error(
656            ctxt, ctxt2, ctxt3, ctxt4, ctxt5, domain, code, level, file, line, str1, str2, str3,
657            int1, int2, msg,
658        );
659    }
660}
661
662/// Remove any error from the last error stack.
663///
664/// # UPSTREAM-PARITY
665///
666/// ```c
667/// void xmlResetLastError(void);
668/// ```
669#[no_mangle]
670pub extern "C" fn xmlResetLastError() {
671    crate::xml::errors::reset_last_error();
672}
673
674// ═══════════════════════════════════════════════════════════════════════════════
675// 5. String Utilities
676// ═══════════════════════════════════════════════════════════════════════════════
677
678/// Duplicate a string using xmlChar.
679///
680/// # UPSTREAM-PARITY
681///
682/// ```c
683/// xmlChar *xmlStrdup(const xmlChar *cur);
684/// ```
685///
686/// # SAFETY
687///
688/// - `cur` must be a valid null-terminated xmlChar string or NULL.
689#[no_mangle]
690pub unsafe extern "C" fn xmlStrdup(cur: *const xmlChar) -> *mut xmlChar {
691    if cur.is_null() {
692        return ptr::null_mut();
693    }
694    let len = unsafe { xmlStrlen(cur) };
695    let size = len + 1;
696    let new_ptr = unsafe { xmlMallocImpl(size as usize) };
697    if new_ptr.is_null() {
698        return ptr::null_mut();
699    }
700    unsafe {
701        ptr::copy_nonoverlapping(cur, new_ptr as *mut u8, size as usize);
702    }
703    new_ptr as *mut xmlChar
704}
705
706/// Duplicate a substring.
707///
708/// # UPSTREAM-PARITY
709///
710/// ```c
711/// xmlChar *xmlStrndup(const xmlChar *cur, int len);
712/// ```
713///
714/// # SAFETY
715///
716/// - `cur` must be a valid pointer or NULL.
717#[no_mangle]
718pub unsafe extern "C" fn xmlStrndup(cur: *const xmlChar, len: c_int) -> *mut xmlChar {
719    // UPSTREAM-PARITY (xmlstring.c xmlStrndup): a NEGATIVE length means
720    // strlen; length 0 still allocates a 1-byte NUL string (never NULL) —
721    // xmlNewTextLen("", 0) depends on it so an empty text node keeps its
722    // content and survives xmlAddChild (php textContent = NULL writes an
723    // empty text child; serialization then prints <e></e>, not <e/>).
724    if cur.is_null() || len < 0 {
725        return ptr::null_mut();
726    }
727    let size = len as usize + 1;
728    let new_ptr = unsafe { xmlMallocImpl(size) };
729    if new_ptr.is_null() {
730        return ptr::null_mut();
731    }
732    unsafe {
733        ptr::copy_nonoverlapping(cur, new_ptr as *mut u8, len as usize);
734        *(new_ptr.add(len as usize) as *mut u8) = 0;
735    }
736    new_ptr as *mut xmlChar
737}
738
739/// Get the length of an xmlChar string.
740///
741/// # UPSTREAM-PARITY
742///
743/// ```c
744/// int xmlStrlen(const xmlChar *str);
745/// ```
746///
747/// # SAFETY
748///
749/// - `str` must be a valid null-terminated string or NULL (returns 0).
750#[no_mangle]
751pub unsafe extern "C" fn xmlStrlen(str: *const xmlChar) -> c_int {
752    if str.is_null() {
753        return 0;
754    }
755    unsafe { libc::strlen(str as *const c_char) as c_int }
756}
757
758/// Find the first occurrence of a character in a string (upstream tree.c
759/// `xmlStrchr`): returns a pointer to the first occurrence or NULL.
760///
761/// # UPSTREAM-PARITY
762///
763/// ```c
764/// const xmlChar *xmlStrchr(const xmlChar *str, xmlChar val);
765/// ```
766///
767/// # SAFETY
768///
769/// - `str` must be a valid null-terminated string or NULL.
770#[no_mangle]
771pub const unsafe extern "C" fn xmlStrchr(str: *const xmlChar, val: xmlChar) -> *const xmlChar {
772    if str.is_null() {
773        return ptr::null();
774    }
775    unsafe {
776        let mut cur = str;
777        while *cur != 0 {
778            if *cur == val {
779                return cur;
780            }
781            cur = cur.add(1);
782        }
783        ptr::null()
784    }
785}
786
787/// Compare two xmlChar strings.
788///
789/// # UPSTREAM-PARITY
790///
791/// ```c
792/// int xmlStrcmp(const xmlChar *str1, const xmlChar *str2);
793/// ```
794///
795/// Returns 0 if equal, <0 if str1 < str2, >0 if str1 > str2.
796/// NULL-safe: NULL sorts before any non-NULL string.
797#[no_mangle]
798pub unsafe extern "C" fn xmlStrcmp(str1: *const xmlChar, str2: *const xmlChar) -> c_int {
799    if str1.is_null() && str2.is_null() {
800        return 0;
801    }
802    if str1.is_null() {
803        return -1;
804    }
805    if str2.is_null() {
806        return 1;
807    }
808    unsafe { libc::strcmp(str1 as *const c_char, str2 as *const c_char) as c_int }
809}
810
811/// Compare two xmlChar strings up to a given length.
812///
813/// # UPSTREAM-PARITY
814///
815/// ```c
816/// int xmlStrncmp(const xmlChar *str1, const xmlChar *str2, int len);
817/// ```
818#[no_mangle]
819pub unsafe extern "C" fn xmlStrncmp(
820    str1: *const xmlChar,
821    str2: *const xmlChar,
822    len: c_int,
823) -> c_int {
824    if len <= 0 {
825        return 0;
826    }
827    if str1.is_null() && str2.is_null() {
828        return 0;
829    }
830    if str1.is_null() {
831        return -1;
832    }
833    if str2.is_null() {
834        return 1;
835    }
836    unsafe { libc::strncmp(str1 as *const c_char, str2 as *const c_char, len as usize) as c_int }
837}
838
839/// Case-insensitive comparison of two xmlChar strings.
840///
841/// # UPSTREAM-PARITY
842///
843/// ```c
844/// int xmlStrcasecmp(const xmlChar *str1, const xmlChar *str2);
845/// ```
846#[no_mangle]
847pub unsafe extern "C" fn xmlStrcasecmp(str1: *const xmlChar, str2: *const xmlChar) -> c_int {
848    if str1.is_null() && str2.is_null() {
849        return 0;
850    }
851    if str1.is_null() {
852        return -1;
853    }
854    if str2.is_null() {
855        return 1;
856    }
857    unsafe { libc::strcasecmp(str1 as *const c_char, str2 as *const c_char) as c_int }
858}
859
860/// Case-insensitive comparison with length limit.
861///
862/// # UPSTREAM-PARITY
863///
864/// ```c
865/// int xmlStrncasecmp(const xmlChar *str1, const xmlChar *str2, int len);
866/// ```
867#[no_mangle]
868pub unsafe extern "C" fn xmlStrncasecmp(
869    str1: *const xmlChar,
870    str2: *const xmlChar,
871    len: c_int,
872) -> c_int {
873    if len <= 0 {
874        return 0;
875    }
876    if str1.is_null() && str2.is_null() {
877        return 0;
878    }
879    if str1.is_null() {
880        return -1;
881    }
882    if str2.is_null() {
883        return 1;
884    }
885    unsafe {
886        libc::strncasecmp(str1 as *const c_char, str2 as *const c_char, len as usize) as c_int
887    }
888}
889
890/// Check if two xmlChar strings are equal.
891///
892/// # UPSTREAM-PARITY
893///
894/// ```c
895/// int xmlStrEqual(const xmlChar *str1, const xmlChar *str2);
896/// ```
897///
898/// Returns 1 if equal, 0 if not. NULL-safe.
899#[no_mangle]
900pub unsafe extern "C" fn xmlStrEqual(str1: *const xmlChar, str2: *const xmlChar) -> c_int {
901    if str1.is_null() && str2.is_null() {
902        return 1;
903    }
904    if str1.is_null() || str2.is_null() {
905        return 0;
906    }
907    unsafe { (libc::strcmp(str1 as *const c_char, str2 as *const c_char) == 0) as c_int }
908}
909
910/// Build a QName (upstream tree.h).
911///
912/// # UPSTREAM-PARITY
913///
914/// ```c
915/// xmlChar *xmlBuildQName(const xmlChar *ncname, const xmlChar *prefix,
916///                        xmlChar *memory, int len);
917/// ```
918#[no_mangle]
919pub unsafe extern "C" fn xmlBuildQName(
920    ncname: *const xmlChar,
921    prefix: *const xmlChar,
922    memory: *mut xmlChar,
923    len: c_int,
924) -> *mut xmlChar {
925    crate::xml::string::build_qname(ncname, prefix, memory, len)
926}
927
928/// Split a QName into prefix + local part (upstream tree.h).
929///
930/// # UPSTREAM-PARITY
931///
932/// ```c
933/// xmlChar *xmlSplitQName2(const xmlChar *name, xmlChar **prefix);
934/// ```
935#[no_mangle]
936pub unsafe extern "C" fn xmlSplitQName2(
937    name: *const xmlChar,
938    prefix: *mut *mut xmlChar,
939) -> *mut xmlChar {
940    crate::xml::string::split_qname2(name, prefix)
941}
942
943/// Return a pointer to the local part of a QName, filling `*len` with the
944/// prefix length (upstream tree.h — R-000176, the candidate previously
945/// returned the prefix length as an int).
946///
947/// # UPSTREAM-PARITY
948///
949/// ```c
950/// const xmlChar *xmlSplitQName3(const xmlChar *name, int *len);
951/// ```
952#[no_mangle]
953pub unsafe extern "C" fn xmlSplitQName3(name: *const xmlChar, len: *mut c_int) -> *mut xmlChar {
954    crate::xml::string::split_qname3(name, len)
955}
956
957/// Split a QName into prefix + local part (upstream parserInternals.h
958/// `xmlSplitQName(ctxt, name, prefix)` — the `ctxt` first argument is part
959/// of the ABI; R-000176, the candidate previously exported the 2-argument
960/// tree.h `xmlSplitQName2` form under this name).
961///
962/// # UPSTREAM-PARITY
963///
964/// ```c
965/// xmlChar *xmlSplitQName(xmlParserCtxt *ctxt, const xmlChar *name,
966///                        xmlChar **prefix);
967/// ```
968///
969/// `ctxt` is accepted for ABI parity; upstream uses it only for the name-
970/// length limit and error reporting (candidate: parser-side error model).
971#[no_mangle]
972pub unsafe extern "C" fn xmlSplitQName(
973    _ctxt: *mut _xmlParserCtxt,
974    name: *const xmlChar,
975    prefix: *mut *mut xmlChar,
976) -> *mut xmlChar {
977    crate::xml::string::split_qname2(name, prefix)
978}
979
980/// Count UTF-8 characters (upstream xmlstring.h).
981///
982/// # UPSTREAM-PARITY
983///
984/// ```c
985/// int xmlUTF8Strlen(const xmlChar *utf);
986/// ```
987#[no_mangle]
988pub const unsafe extern "C" fn xmlUTF8Strlen(utf: *const xmlChar) -> c_int {
989    crate::xml::string::utf8_strlen(utf)
990}
991
992/// Size in bytes of a UTF-8 sequence (upstream xmlstring.h).
993///
994/// # UPSTREAM-PARITY
995///
996/// ```c
997/// int xmlUTF8Size(const xmlChar *utf);
998/// ```
999#[no_mangle]
1000pub const unsafe extern "C" fn xmlUTF8Size(utf: *const xmlChar) -> c_int {
1001    crate::xml::string::utf8_size(utf)
1002}
1003
1004/// Check UTF-8 validity (upstream xmlstring.h).
1005///
1006/// # UPSTREAM-PARITY
1007///
1008/// ```c
1009/// int xmlCheckUTF8(const unsigned char *utf);
1010/// ```
1011#[no_mangle]
1012pub const unsafe extern "C" fn xmlCheckUTF8(utf: *const xmlChar) -> c_int {
1013    crate::xml::string::check_utf8(utf)
1014}
1015
1016/// Check if an xmlChar string equals a qualified name.
1017///
1018/// # UPSTREAM-PARITY
1019///
1020/// ```c
1021/// int xmlStrQEqual(const xmlChar *pref, const xmlChar *name, const xmlChar *str);
1022/// ```
1023///
1024/// Returns 1 if `pref:name` equals `str`, 0 otherwise.
1025/// `pref` may be NULL (compares only name).
1026#[no_mangle]
1027pub unsafe extern "C" fn xmlStrQEqual(
1028    pref: *const xmlChar,
1029    name: *const xmlChar,
1030    str: *const xmlChar,
1031) -> c_int {
1032    if name.is_null() || str.is_null() {
1033        return 0;
1034    }
1035    if pref.is_null() {
1036        return unsafe { xmlStrEqual(name, str) };
1037    }
1038    // Compare "pref:name" with str
1039    let pref_len = unsafe { xmlStrlen(pref) };
1040    let name_len = unsafe { xmlStrlen(name) };
1041    let total_len = pref_len + 1 + name_len;
1042    let str_len = unsafe { xmlStrlen(str) };
1043    if total_len != str_len {
1044        return 0;
1045    }
1046    // Compare prefix part
1047    if unsafe {
1048        libc::strncmp(
1049            pref as *const c_char,
1050            str as *const c_char,
1051            pref_len as usize,
1052        )
1053    } != 0
1054    {
1055        return 0;
1056    }
1057    // Check colon
1058    if unsafe { *str.add(pref_len as usize) } != b':' as xmlChar {
1059        return 0;
1060    }
1061    // Compare name part
1062    (unsafe {
1063        libc::strncmp(
1064            name as *const c_char,
1065            str.add((pref_len + 1) as usize) as *const c_char,
1066            name_len as usize,
1067        ) == 0
1068    }) as c_int
1069}
1070
1071/// Concatenate two strings.
1072///
1073/// # UPSTREAM-PARITY
1074///
1075/// ```c
1076/// xmlChar *xmlStrcat(xmlChar *cur, const xmlChar *add);
1077/// ```
1078///
1079/// # SAFETY
1080///
1081/// - `cur` must be a valid xmlMalloc'd string or NULL.
1082/// - `add` must be a valid string or NULL.
1083/// - If `cur` is NULL, behaves like xmlStrdup(add).
1084#[no_mangle]
1085pub unsafe extern "C" fn xmlStrcat(cur: *mut xmlChar, add: *const xmlChar) -> *mut xmlChar {
1086    if add.is_null() {
1087        return cur;
1088    }
1089    if cur.is_null() {
1090        return unsafe { xmlStrdup(add) };
1091    }
1092    let cur_len = unsafe { xmlStrlen(cur) } as usize;
1093    let add_len = unsafe { xmlStrlen(add) } as usize;
1094    let new_size = cur_len + add_len + 1;
1095    let new_ptr = unsafe { xmlReallocImpl(cur as *mut c_void, new_size) };
1096    if new_ptr.is_null() {
1097        // UPSTREAM-PARITY (xmlstring.c xmlStrcat -> xmlStrncat): on realloc
1098        // failure upstream frees `cur` and returns NULL — the caller must
1099        // not touch `cur` afterwards (HOSTILE-ALLOCATOR H4).
1100        unsafe { xmlFreeImpl(cur as *mut c_void) };
1101        return ptr::null_mut();
1102    }
1103    unsafe {
1104        ptr::copy_nonoverlapping(add, (new_ptr as *mut u8).add(cur_len), add_len);
1105        *((new_ptr as *mut u8).add(cur_len + add_len)) = 0;
1106    }
1107    new_ptr as *mut xmlChar
1108}
1109
1110/// Concatenate up to `len` characters.
1111///
1112/// # UPSTREAM-PARITY
1113///
1114/// ```c
1115/// xmlChar *xmlStrncat(xmlChar *cur, const xmlChar *add, int len);
1116/// ```
1117///
1118/// # SAFETY
1119///
1120/// Same as xmlStrcat, but only copies up to `len` characters from `add`.
1121#[no_mangle]
1122pub unsafe extern "C" fn xmlStrncat(
1123    cur: *mut xmlChar,
1124    add: *const xmlChar,
1125    len: c_int,
1126) -> *mut xmlChar {
1127    if add.is_null() || len <= 0 {
1128        return cur;
1129    }
1130    let len = len as usize;
1131    if cur.is_null() {
1132        return unsafe { xmlStrndup(add, len as c_int) };
1133    }
1134    let cur_len = unsafe { xmlStrlen(cur) } as usize;
1135    let new_size = cur_len + len + 1;
1136    let new_ptr = unsafe { xmlReallocImpl(cur as *mut c_void, new_size) };
1137    if new_ptr.is_null() {
1138        // UPSTREAM-PARITY (xmlstring.c xmlStrncat): on realloc failure
1139        // upstream frees `cur` and returns NULL — the caller must not touch
1140        // `cur` afterwards (HOSTILE-ALLOCATOR H4).
1141        unsafe { xmlFreeImpl(cur as *mut c_void) };
1142        return ptr::null_mut();
1143    }
1144    unsafe {
1145        ptr::copy_nonoverlapping(add, (new_ptr as *mut u8).add(cur_len), len);
1146        *((new_ptr as *mut u8).add(cur_len + len)) = 0;
1147    }
1148    new_ptr as *mut xmlChar
1149}
1150
1151/// Create a new string by concatenating up to `len` characters.
1152///
1153/// # UPSTREAM-PARITY
1154///
1155/// ```c
1156/// xmlChar *xmlStrncatNew(const xmlChar *str1, const xmlChar *str2, int len);
1157/// ```
1158#[no_mangle]
1159pub unsafe extern "C" fn xmlStrncatNew(
1160    str1: *const xmlChar,
1161    str2: *const xmlChar,
1162    len: c_int,
1163) -> *mut xmlChar {
1164    let mut result: *mut xmlChar = ptr::null_mut();
1165    if !str1.is_null() {
1166        result = unsafe { xmlStrdup(str1) };
1167    }
1168    if !str2.is_null() && len > 0 {
1169        result = unsafe { xmlStrncat(result, str2, len) };
1170    }
1171    result
1172}
1173
1174/// Copy a string.
1175///
1176/// # UPSTREAM-PARITY
1177///
1178/// ```c
1179/// xmlChar *xmlStrcpy(xmlChar *dst, const xmlChar *src);
1180/// ```
1181///
1182/// # SAFETY
1183///
1184/// - `dst` must be a valid xmlMalloc'd buffer large enough to hold `src`.
1185/// - `src` must be a valid string.
1186#[no_mangle]
1187pub unsafe extern "C" fn xmlStrcpy(dst: *mut xmlChar, src: *const xmlChar) -> *mut xmlChar {
1188    if dst.is_null() || src.is_null() {
1189        return dst;
1190    }
1191    let len = unsafe { xmlStrlen(src) } as usize + 1;
1192    unsafe {
1193        ptr::copy_nonoverlapping(src, dst, len);
1194    }
1195    dst
1196}
1197
1198/// Copy up to `len` characters.
1199///
1200/// # UPSTREAM-PARITY
1201///
1202/// ```c
1203/// xmlChar *xmlStrncpy(xmlChar *dst, const xmlChar *src, int len);
1204/// ```
1205#[no_mangle]
1206pub unsafe extern "C" fn xmlStrncpy(
1207    dst: *mut xmlChar,
1208    src: *const xmlChar,
1209    len: c_int,
1210) -> *mut xmlChar {
1211    if dst.is_null() || src.is_null() || len <= 0 {
1212        return dst;
1213    }
1214    let len = len as usize;
1215    let src_len = unsafe { xmlStrlen(src) } as usize;
1216    let copy_len = if src_len < len { src_len } else { len - 1 };
1217    unsafe {
1218        ptr::copy_nonoverlapping(src, dst, copy_len);
1219        *dst.add(copy_len) = 0;
1220    }
1221    dst
1222}
1223
1224/// Extract a substring.
1225///
1226/// # UPSTREAM-PARITY
1227///
1228/// ```c
1229/// xmlChar *xmlStrsub(const xmlChar *str, int start, int len);
1230/// ```
1231///
1232/// Returns a newly allocated substring, or NULL on error.
1233#[no_mangle]
1234pub unsafe extern "C" fn xmlStrsub(str: *const xmlChar, start: c_int, len: c_int) -> *mut xmlChar {
1235    if str.is_null() || start < 0 || len < 0 {
1236        return ptr::null_mut();
1237    }
1238    let str_len = unsafe { xmlStrlen(str) };
1239    if start >= str_len {
1240        return unsafe { xmlStrdup(b"\0" as *const u8 as *const xmlChar) };
1241    }
1242    let actual_len = if start + len > str_len {
1243        str_len - start
1244    } else {
1245        len
1246    };
1247    unsafe { xmlStrndup(str.add(start as usize), actual_len) }
1248}
1249
1250// ═══════════════════════════════════════════════════════════════════════════════
1251// 6. Tree — Document, Node, Attribute, Namespace, DTD, Entity
1252// ═══════════════════════════════════════════════════════════════════════════════
1253
1254/// Create a new document.
1255///
1256/// # UPSTREAM-PARITY
1257///
1258/// ```c
1259/// xmlDocPtr xmlNewDoc(const xmlChar *version);
1260/// ```
1261///
1262/// # SAFETY
1263///
1264/// - `version` must be a valid string or NULL (defaults to "1.0").
1265/// - Returns a newly allocated document. Caller must free with `xmlFreeDoc`.
1266#[no_mangle]
1267pub unsafe extern "C" fn xmlNewDoc(version: *const xmlChar) -> *mut _xmlDoc {
1268    crate::xml::tree::new_doc(version)
1269}
1270
1271/// Free a document.
1272///
1273/// # UPSTREAM-PARITY
1274///
1275/// ```c
1276/// void xmlFreeDoc(xmlDocPtr doc);
1277/// ```
1278///
1279/// # SAFETY
1280///
1281/// - `doc` must be a valid document pointer or NULL.
1282#[no_mangle]
1283pub unsafe extern "C" fn xmlFreeDoc(doc: *mut _xmlDoc) {
1284    crate::xml::tree::free_doc(doc);
1285}
1286
1287/// Get the compression mode of a document (upstream tree.h).
1288///
1289/// # UPSTREAM-PARITY
1290///
1291/// ```c
1292/// int xmlGetDocCompressMode(const xmlDoc *doc);
1293/// ```
1294///
1295/// Returns the compression level (0-9) or -1 if `doc` is NULL.
1296#[no_mangle]
1297pub unsafe extern "C" fn xmlGetDocCompressMode(doc: *mut _xmlDoc) -> c_int {
1298    crate::xml::tree::xmlGetDocCompressMode(doc)
1299}
1300
1301/// Set the compression mode of a document (upstream tree.h).
1302///
1303/// # UPSTREAM-PARITY
1304///
1305/// ```c
1306/// void xmlSetDocCompressMode(xmlDocPtr doc, int mode);
1307/// ```
1308#[no_mangle]
1309pub unsafe extern "C" fn xmlSetDocCompressMode(doc: *mut _xmlDoc, mode: c_int) {
1310    crate::xml::tree::xmlSetDocCompressMode(doc, mode);
1311}
1312
1313/// Create a new node.
1314///
1315/// # UPSTREAM-PARITY
1316///
1317/// ```c
1318/// xmlNodePtr xmlNewNode(xmlNsPtr ns, const xmlChar *name);
1319/// ```
1320///
1321/// # SAFETY
1322///
1323/// - `ns` may be NULL.
1324/// - `name` must be a valid string.
1325/// - Returns a newly allocated node. Caller must free with `xmlFreeNode`.
1326#[no_mangle]
1327pub unsafe extern "C" fn xmlNewNode(ns: *mut _xmlNs, name: *const xmlChar) -> *mut _xmlNode {
1328    crate::xml::tree::new_node(ns, name)
1329}
1330
1331/// Free a node.
1332///
1333/// # UPSTREAM-PARITY
1334///
1335/// ```c
1336/// void xmlFreeNode(xmlNodePtr node);
1337/// ```
1338///
1339/// # SAFETY
1340///
1341/// - `node` must be a valid node pointer or NULL.
1342/// - The node must NOT be part of a document tree (must be unlinked first).
1343#[no_mangle]
1344pub unsafe extern "C" fn xmlFreeNode(node: *mut _xmlNode) {
1345    crate::xml::tree::free_node(node);
1346}
1347
1348/// Free a linked list of nodes (upstream tree.h).
1349///
1350/// # UPSTREAM-PARITY
1351///
1352/// ```c
1353/// void xmlFreeNodeList(xmlNodePtr node);
1354/// ```
1355///
1356/// Frees the node and all its siblings (following `next` pointers),
1357/// recursively freeing children. Matches upstream `xmlFreeNodeList`
1358/// (tree.c): a NULL argument is a no-op.
1359///
1360/// # SAFETY
1361///
1362/// - `node` must be a valid node pointer or NULL.
1363/// - The list must NOT be part of a document tree (must be unlinked first).
1364#[no_mangle]
1365pub unsafe extern "C" fn xmlFreeNodeList(node: *mut _xmlNode) {
1366    crate::xml::tree::free_node_list(node);
1367}
1368
1369/// Unlink a node from its tree.
1370///
1371/// # UPSTREAM-PARITY
1372///
1373/// ```c
1374/// void xmlUnlinkNode(xmlNodePtr node);
1375/// ```
1376///
1377/// # SAFETY
1378///
1379/// - `node` must be a valid node pointer or NULL.
1380#[no_mangle]
1381pub unsafe extern "C" fn xmlUnlinkNode(node: *mut _xmlNode) {
1382    crate::xml::tree::unlink_node(node);
1383}
1384
1385/// Initialize a SAX handler with the default SAX2 callbacks (upstream SAX2.h).
1386///
1387/// # UPSTREAM-PARITY
1388///
1389/// ```c
1390/// void xmlSAX2InitDefaultSAXHandler(xmlSAXHandler *hdlr, int warning);
1391/// ```
1392///
1393/// The `warning` parameter controls whether the warning callback is set in
1394/// upstream; the candidate always sets it (the parser dispatches warnings
1395/// identically) — a documented safe divergence tracked in the parity ledger.
1396///
1397/// # SAFETY
1398///
1399/// - `handler` must be a valid writable `_xmlSAXHandler` or NULL.
1400#[no_mangle]
1401pub unsafe extern "C" fn xmlSAX2InitDefaultSAXHandler(
1402    handler: *mut crate::abi::structs::_xmlSAXHandler,
1403    _warning: c_int,
1404) {
1405    crate::xml::sax::dispatch::xmlSAX2InitDefaultSAXHandler(handler);
1406}
1407
1408/// Initialize a SAX handler with the default HTML callbacks (upstream SAX2.h).
1409///
1410/// # UPSTREAM-PARITY
1411///
1412/// ```c
1413/// void xmlSAX2InitHtmlDefaultSAXHandler(xmlSAXHandler *hdlr);
1414/// ```
1415///
1416/// Upstream (SAX2.c `xmlSAX2InitHtmlDefaultSAXHandler`) fills the handler
1417/// with the SAX2 defaults minus the DTD-declaration callbacks
1418/// (resolveEntity/getParameterEntity/entityDecl/attributeDecl/elementDecl/
1419/// notationDecl/unparsedEntityDecl/reference/externalSubset are NULL) and
1420/// sets `initialized = 1` (not XML_SAX2_MAGIC). The candidate mirrors that
1421/// exactly.
1422///
1423/// # SAFETY
1424///
1425/// - `handler` must be a valid writable `_xmlSAXHandler` or NULL.
1426#[no_mangle]
1427pub unsafe extern "C" fn xmlSAX2InitHtmlDefaultSAXHandler(
1428    handler: *mut crate::abi::structs::_xmlSAXHandler,
1429) {
1430    if handler.is_null() {
1431        return;
1432    }
1433    // SAFETY: handler is non-NULL and writable.
1434    unsafe {
1435        // The DTD-ish callbacks are not part of the HTML default set.
1436        if (*handler).initialized != 0 {
1437            return;
1438        }
1439        crate::xml::sax::dispatch::xmlSAX2InitDefaultSAXHandler(handler);
1440        let h = &mut *handler;
1441        h.resolveEntity = None;
1442        h.getParameterEntity = None;
1443        h.entityDecl = None;
1444        h.attributeDecl = None;
1445        h.elementDecl = None;
1446        h.notationDecl = None;
1447        h.unparsedEntityDecl = None;
1448        h.reference = None;
1449        h.externalSubset = None;
1450        h.initialized = 1;
1451    }
1452}
1453
1454/// Add a child node.
1455///
1456/// # UPSTREAM-PARITY
1457///
1458/// ```c
1459/// xmlNodePtr xmlAddChild(xmlNodePtr parent, xmlNodePtr cur);
1460/// ```
1461///
1462/// # SAFETY
1463///
1464/// - `parent` must be a valid node.
1465/// - `cur` must be a valid node (ownership transfers to parent).
1466/// - Returns pointer to the added child (borrowed).
1467#[no_mangle]
1468pub unsafe extern "C" fn xmlAddChild(parent: *mut _xmlNode, cur: *mut _xmlNode) -> *mut _xmlNode {
1469    crate::xml::tree::add_child(parent, cur)
1470}
1471
1472/// Add a sibling node.
1473///
1474/// # UPSTREAM-PARITY
1475///
1476/// ```c
1477/// xmlNodePtr xmlAddSibling(xmlNodePtr cur, xmlNodePtr sibling);
1478/// ```
1479///
1480/// # SAFETY
1481///
1482/// Same as xmlAddChild, but adds after `cur` instead of as a child.
1483#[no_mangle]
1484pub unsafe extern "C" fn xmlAddSibling(
1485    cur: *mut _xmlNode,
1486    sibling: *mut _xmlNode,
1487) -> *mut _xmlNode {
1488    crate::xml::tree::add_sibling(cur, sibling)
1489}
1490
1491/// Create a new child element.
1492///
1493/// # UPSTREAM-PARITY
1494///
1495/// ```c
1496/// xmlNodePtr xmlNewChild(xmlNodePtr parent, xmlNsPtr ns,
1497///                        const xmlChar *name, const xmlChar *content);
1498/// ```
1499///
1500/// Creates a new element node, adds it as a child of `parent`, and
1501/// sets its content if `content` is non-NULL.
1502///
1503/// # SAFETY
1504///
1505/// - `parent` must be a valid node (may be NULL).
1506/// - `ns` may be NULL.
1507/// - `name` must be a valid string.
1508/// - Returns a newly allocated node (owned by parent).
1509#[no_mangle]
1510pub unsafe extern "C" fn xmlNewChild(
1511    parent: *mut _xmlNode,
1512    ns: *mut _xmlNs,
1513    name: *const xmlChar,
1514    content: *const xmlChar,
1515) -> *mut _xmlNode {
1516    // UPSTREAM-PARITY (tree.c 2.15 xmlNewChild -> xmlNewDocNode -> xmlNewElem):
1517    // the content is parsed as an ATTRIBUTE VALUE (xmlNodeParseAttValue) —
1518    // character references are decoded (`a &#38; b` becomes `a & b`),
1519    // declared general entities become entity-ref children, and an EMPTY
1520    // content adds NO text child (SimpleXML addChild: bug44478 decode,
1521    // bug76712 `<bar/>` for addChild('bar','')). The old raw `new_text`
1522    // storage kept the reference text verbatim and appended an empty text
1523    // node for "".
1524    if parent.is_null() || name.is_null() {
1525        return ptr::null_mut();
1526    }
1527    let ptype = unsafe { (*parent).type_ };
1528    use crate::abi::types::xmlElementType::*;
1529    if ptype != XML_DOCUMENT_NODE as c_int
1530        && ptype != XML_HTML_DOCUMENT_NODE as c_int
1531        && ptype != XML_DOCUMENT_FRAG_NODE as c_int
1532        && ptype != XML_ELEMENT_NODE as c_int
1533    {
1534        return ptr::null_mut();
1535    }
1536    let mut ns = ns;
1537    if ptype == XML_ELEMENT_NODE as c_int && ns.is_null() {
1538        ns = unsafe { (*parent).ns };
1539    }
1540    let node = crate::abi::exports_tree::xmlNewDocNode(unsafe { (*parent).doc }, ns, name, content);
1541    if node.is_null() {
1542        return ptr::null_mut();
1543    }
1544    // Add the new element at the end of the parent's children (upstream
1545    // xmlNewChild links it directly after xmlNewDocNode).
1546    unsafe {
1547        (*node).parent = parent;
1548        if (*parent).children.is_null() {
1549            (*parent).children = node;
1550            (*parent).last = node;
1551        } else {
1552            let prev = (*parent).last;
1553            (*prev).next = node;
1554            (*node).prev = prev;
1555            (*parent).last = node;
1556        }
1557    }
1558    node
1559}
1560
1561/// Set the root element of a document.
1562///
1563/// # UPSTREAM-PARITY
1564///
1565/// ```c
1566/// xmlNodePtr xmlDocSetRootElement(xmlDocPtr doc, xmlNodePtr root);
1567/// ```
1568///
1569/// Returns the old root element (if any), which the caller must free.
1570///
1571/// # SAFETY
1572///
1573/// - `doc` must be a valid document.
1574/// - `root` must be a valid node (ownership transfers to doc).
1575#[no_mangle]
1576pub unsafe extern "C" fn xmlDocSetRootElement(
1577    doc: *mut _xmlDoc,
1578    root: *mut _xmlNode,
1579) -> *mut _xmlNode {
1580    crate::xml::tree::doc_set_root_element(doc, root)
1581}
1582
1583/// Get the root element of a document.
1584///
1585/// # UPSTREAM-PARITY
1586///
1587/// ```c
1588/// xmlNodePtr xmlDocGetRootElement(const xmlDoc *doc);
1589/// ```
1590///
1591/// Returns a borrowed pointer (do not free).
1592#[no_mangle]
1593pub extern "C" fn xmlDocGetRootElement(doc: *const _xmlDoc) -> *mut _xmlNode {
1594    crate::xml::tree::doc_get_root_element(doc as *mut _xmlDoc)
1595}
1596
1597/// Copy a node.
1598///
1599/// # UPSTREAM-PARITY
1600///
1601/// ```c
1602/// xmlNodePtr xmlCopyNode(const xmlNodePtr node, int extended);
1603/// ```
1604///
1605/// If `extended` is 1, copies recursively (deep copy).
1606/// If `extended` is 0, copies only the node itself (shallow copy).
1607///
1608/// Returns a newly allocated copy. Caller must free with `xmlFreeNode`.
1609#[no_mangle]
1610pub unsafe extern "C" fn xmlCopyNode(node: *const _xmlNode, extended: c_int) -> *mut _xmlNode {
1611    crate::xml::tree::copy_node(node, extended)
1612}
1613
1614/// Copy a document.
1615///
1616/// # UPSTREAM-PARITY
1617///
1618/// ```c
1619/// xmlDocPtr xmlCopyDoc(const xmlDocPtr doc, int recursive);
1620/// ```
1621///
1622/// Returns a newly allocated copy. Caller must free with `xmlFreeDoc`.
1623#[no_mangle]
1624pub unsafe extern "C" fn xmlCopyDoc(doc: *const _xmlDoc, recursive: c_int) -> *mut _xmlDoc {
1625    crate::xml::tree::copy_doc(doc, recursive)
1626}
1627
1628/// Create a text node.
1629///
1630/// # UPSTREAM-PARITY
1631///
1632/// ```c
1633/// xmlNodePtr xmlNewText(const xmlChar *content);
1634/// ```
1635///
1636/// Creates a new text node with the given content.
1637/// If `content` is NULL, creates an empty text node.
1638#[no_mangle]
1639pub unsafe extern "C" fn xmlNewText(content: *const xmlChar) -> *mut _xmlNode {
1640    crate::xml::tree::new_text(content)
1641}
1642
1643/// Create a new comment node.
1644///
1645/// # UPSTREAM-PARITY
1646///
1647/// ```c
1648/// xmlNodePtr xmlNewComment(const xmlChar *content);
1649/// ```
1650#[no_mangle]
1651pub unsafe extern "C" fn xmlNewComment(content: *const xmlChar) -> *mut _xmlNode {
1652    crate::xml::tree::new_comment(content)
1653}
1654
1655/// Create a new PI node.
1656///
1657/// # UPSTREAM-PARITY
1658///
1659/// ```c
1660/// xmlNodePtr xmlNewPI(const xmlChar *name, const xmlChar *content);
1661/// ```
1662#[no_mangle]
1663pub unsafe extern "C" fn xmlNewPI(name: *const xmlChar, content: *const xmlChar) -> *mut _xmlNode {
1664    crate::xml::tree::new_pi(name, content)
1665}
1666
1667/// Create a new CDATA node.
1668///
1669/// # UPSTREAM-PARITY
1670///
1671/// ```c
1672/// xmlNodePtr xmlNewCDataBlock(xmlDocPtr doc, const xmlChar *content, int len);
1673/// ```
1674#[no_mangle]
1675pub unsafe extern "C" fn xmlNewCDataBlock(
1676    doc: *mut _xmlDoc,
1677    content: *const xmlChar,
1678    len: c_int,
1679) -> *mut _xmlNode {
1680    crate::xml::tree::new_cdata_block(doc, content, len)
1681}
1682
1683/// Create a new namespace definition.
1684///
1685/// # UPSTREAM-PARITY
1686///
1687/// ```c
1688/// xmlNsPtr xmlNewNs(xmlNodePtr node, const xmlChar *href, const xmlChar *prefix);
1689/// ```
1690///
1691/// # SAFETY
1692///
1693/// - `node` may be NULL.
1694/// - `href` and `prefix` are copied.
1695/// - Returns a borrowed pointer (namespace is owned by the node).
1696#[no_mangle]
1697pub unsafe extern "C" fn xmlNewNs(
1698    node: *mut _xmlNode,
1699    href: *const xmlChar,
1700    prefix: *const xmlChar,
1701) -> *mut _xmlNs {
1702    crate::xml::tree::new_ns(node, href, prefix)
1703}
1704
1705/// Set the namespace of a node.
1706///
1707/// # UPSTREAM-PARITY
1708///
1709/// ```c
1710/// void xmlSetNs(xmlNodePtr node, xmlNsPtr ns);
1711/// ```
1712#[no_mangle]
1713pub unsafe extern "C" fn xmlSetNs(node: *mut _xmlNode, ns: *mut _xmlNs) {
1714    crate::xml::tree::set_ns(node, ns);
1715}
1716
1717/// Get the namespace of a node.
1718///
1719/// # UPSTREAM-PARITY
1720///
1721/// ```c
1722/// xmlNsPtr xmlGetNsList(xmlDocPtr doc, const xmlNode *node);
1723/// ```
1724#[no_mangle]
1725pub unsafe extern "C" fn xmlGetNsList(
1726    doc: *mut _xmlDoc,
1727    node: *const _xmlNode,
1728) -> *mut *mut _xmlNs {
1729    crate::xml::tree::get_ns_list(doc, node as *mut _xmlNode)
1730}
1731
1732/// Search for a namespace by href.
1733///
1734/// # UPSTREAM-PARITY
1735///
1736/// ```c
1737/// xmlNsPtr xmlSearchNs(xmlDocPtr doc, xmlNodePtr node, const xmlChar *nameSpace);
1738/// ```
1739#[no_mangle]
1740pub unsafe extern "C" fn xmlSearchNs(
1741    doc: *mut _xmlDoc,
1742    node: *mut _xmlNode,
1743    nameSpace: *const xmlChar,
1744) -> *mut _xmlNs {
1745    crate::xml::tree::search_ns(doc, node, nameSpace)
1746}
1747
1748/// Search for a namespace by href, using the full in-scope chain.
1749///
1750/// # UPSTREAM-PARITY
1751///
1752/// ```c
1753/// xmlNsPtr xmlSearchNsByHref(xmlDocPtr doc, xmlNodePtr node, const xmlChar *href);
1754/// ```
1755#[no_mangle]
1756pub unsafe extern "C" fn xmlSearchNsByHref(
1757    doc: *mut _xmlDoc,
1758    node: *mut _xmlNode,
1759    href: *const xmlChar,
1760) -> *mut _xmlNs {
1761    crate::xml::tree::search_ns_by_href(doc, node, href)
1762}
1763
1764/// Set a property (attribute) on a node.
1765///
1766/// # UPSTREAM-PARITY
1767///
1768/// ```c
1769/// xmlAttrPtr xmlSetProp(xmlNodePtr node, const xmlChar *name, const xmlChar *value);
1770/// ```
1771///
1772/// If the attribute already exists, its value is updated.
1773/// Returns a borrowed pointer to the attribute.
1774///
1775/// # SAFETY
1776///
1777/// - `node` must be a valid element node.
1778/// - `name` must be a valid string.
1779/// - `value` may be NULL.
1780#[no_mangle]
1781pub unsafe extern "C" fn xmlSetProp(
1782    node: *mut _xmlNode,
1783    name: *const xmlChar,
1784    value: *const xmlChar,
1785) -> *mut _xmlAttr {
1786    crate::xml::tree::set_prop(node, name, value)
1787}
1788
1789/// Get a property value by name.
1790///
1791/// # UPSTREAM-PARITY
1792///
1793/// ```c
1794/// xmlChar *xmlGetProp(const xmlNode *node, const xmlChar *name);
1795/// ```
1796///
1797/// Returns a newly allocated string. Caller must free with `xmlFree`.
1798#[no_mangle]
1799pub unsafe extern "C" fn xmlGetProp(node: *const _xmlNode, name: *const xmlChar) -> *mut xmlChar {
1800    crate::xml::tree::get_prop(node as *mut _xmlNode, name)
1801}
1802
1803/// Get a namespaced property value.
1804///
1805/// # UPSTREAM-PARITY
1806///
1807/// ```c
1808/// xmlChar *xmlGetNsProp(const xmlNode *node, const xmlChar *name, const xmlChar *nameSpace);
1809/// ```
1810#[no_mangle]
1811pub unsafe extern "C" fn xmlGetNsProp(
1812    node: *const _xmlNode,
1813    name: *const xmlChar,
1814    nameSpace: *const xmlChar,
1815) -> *mut xmlChar {
1816    crate::xml::tree::get_ns_prop(node as *mut _xmlNode, name, nameSpace)
1817}
1818
1819/// Set a namespaced property.
1820///
1821/// # UPSTREAM-PARITY
1822///
1823/// ```c
1824/// xmlAttrPtr xmlSetNsProp(xmlNodePtr node, xmlNsPtr ns,
1825///                         const xmlChar *name, const xmlChar *value);
1826/// ```
1827#[no_mangle]
1828pub unsafe extern "C" fn xmlSetNsProp(
1829    node: *mut _xmlNode,
1830    ns: *mut _xmlNs,
1831    name: *const xmlChar,
1832    value: *const xmlChar,
1833) -> *mut _xmlAttr {
1834    crate::xml::tree::set_ns_prop(node, ns, name, value)
1835}
1836
1837/// Remove a property by name.
1838///
1839/// # UPSTREAM-PARITY
1840///
1841/// ```c
1842/// int xmlRemoveProp(xmlAttrPtr attr);
1843/// ```
1844///
1845/// Returns 0 on success, -1 on error.
1846#[no_mangle]
1847pub unsafe extern "C" fn xmlRemoveProp(attr: *mut _xmlAttr) -> c_int {
1848    crate::xml::tree::remove_prop(attr)
1849}
1850
1851/// Check whether a node has a property (upstream tree.h).
1852///
1853/// # UPSTREAM-PARITY
1854///
1855/// ```c
1856/// xmlAttrPtr xmlHasProp(const xmlNode *node, const xmlChar *name);
1857/// ```
1858///
1859/// Returns the attribute pointer or NULL.
1860#[no_mangle]
1861pub unsafe extern "C" fn xmlHasProp(node: *const _xmlNode, name: *const xmlChar) -> *mut _xmlAttr {
1862    crate::xml::tree::has_prop(node as *mut _xmlNode, name)
1863}
1864
1865/// Check whether a node has a namespaced property (upstream tree.h).
1866///
1867/// # UPSTREAM-PARITY
1868///
1869/// ```c
1870/// xmlAttrPtr xmlHasNsProp(const xmlNode *node, const xmlChar *name,
1871///                         const xmlChar *nameSpace);
1872/// ```
1873#[no_mangle]
1874pub unsafe extern "C" fn xmlHasNsProp(
1875    node: *const _xmlNode,
1876    name: *const xmlChar,
1877    nameSpace: *const xmlChar,
1878) -> *mut _xmlAttr {
1879    crate::xml::tree::has_ns_prop(node as *mut _xmlNode, name, nameSpace)
1880}
1881
1882/// Remove a property by name (upstream tree.h).
1883///
1884/// # UPSTREAM-PARITY
1885///
1886/// ```c
1887/// int xmlUnsetProp(xmlNodePtr node, const xmlChar *name);
1888/// ```
1889///
1890/// Returns 0 on success, -1 if not found.
1891#[no_mangle]
1892pub unsafe extern "C" fn xmlUnsetProp(node: *mut _xmlNode, name: *const xmlChar) -> c_int {
1893    crate::xml::tree::unset_prop(node, name)
1894}
1895
1896/// Remove a namespaced property by name (upstream tree.h).
1897///
1898/// # UPSTREAM-PARITY
1899///
1900/// ```c
1901/// int xmlUnsetNsProp(xmlNodePtr node, const xmlChar *name,
1902///                    const xmlChar *nameSpace);
1903/// ```
1904#[no_mangle]
1905pub unsafe extern "C" fn xmlUnsetNsProp(
1906    node: *mut _xmlNode,
1907    name: *const xmlChar,
1908    nameSpace: *const xmlChar,
1909) -> c_int {
1910    crate::xml::tree::unset_ns_prop(node, name, nameSpace)
1911}
1912
1913/// Return the first child element (upstream tree.h).
1914///
1915/// # UPSTREAM-PARITY
1916///
1917/// ```c
1918/// xmlNodePtr xmlFirstElementChild(xmlNodePtr parent);
1919/// ```
1920#[no_mangle]
1921pub unsafe extern "C" fn xmlFirstElementChild(parent: *mut _xmlNode) -> *mut _xmlNode {
1922    crate::xml::tree::first_element_child(parent)
1923}
1924
1925/// Return the last child element (upstream tree.h).
1926#[no_mangle]
1927pub unsafe extern "C" fn xmlLastElementChild(parent: *mut _xmlNode) -> *mut _xmlNode {
1928    crate::xml::tree::last_element_child(parent)
1929}
1930
1931/// Return the next element sibling (upstream tree.h).
1932#[no_mangle]
1933pub unsafe extern "C" fn xmlNextElementSibling(node: *mut _xmlNode) -> *mut _xmlNode {
1934    crate::xml::tree::next_element_sibling(node)
1935}
1936
1937/// Return the previous element sibling (upstream tree.h).
1938#[no_mangle]
1939pub unsafe extern "C" fn xmlPreviousElementSibling(node: *mut _xmlNode) -> *mut _xmlNode {
1940    crate::xml::tree::previous_element_sibling(node)
1941}
1942
1943/// Count the child elements (upstream tree.h).
1944///
1945/// # UPSTREAM-PARITY
1946///
1947/// ```c
1948/// unsigned long xmlChildElementCount(xmlNodePtr parent);
1949/// ```
1950#[no_mangle]
1951pub unsafe extern "C" fn xmlChildElementCount(parent: *mut _xmlNode) -> c_ulong {
1952    crate::xml::tree::child_element_count(parent)
1953}
1954
1955/// Concatenate text to a node (upstream tree.h).
1956///
1957/// # UPSTREAM-PARITY
1958///
1959/// ```c
1960/// int xmlTextConcat(xmlNodePtr node, const xmlChar *content, int len);
1961/// ```
1962#[no_mangle]
1963pub unsafe extern "C" fn xmlTextConcat(
1964    node: *mut _xmlNode,
1965    content: *const xmlChar,
1966    len: c_int,
1967) -> c_int {
1968    crate::xml::tree::text_concat(node, content, len)
1969}
1970
1971/// Merge two text nodes (upstream tree.h).
1972///
1973/// # UPSTREAM-PARITY
1974///
1975/// ```c
1976/// xmlNodePtr xmlTextMerge(xmlNodePtr first, xmlNodePtr second);
1977/// ```
1978#[no_mangle]
1979pub unsafe extern "C" fn xmlTextMerge(
1980    first: *mut _xmlNode,
1981    second: *mut _xmlNode,
1982) -> *mut _xmlNode {
1983    crate::xml::tree::text_merge(first, second)
1984}
1985
1986/// Get a DTD from a document, creating one if needed.
1987///
1988/// # UPSTREAM-PARITY
1989///
1990/// ```c
1991/// xmlDtdPtr xmlGetIntSubset(const xmlDoc *doc);
1992/// ```
1993#[no_mangle]
1994pub const extern "C" fn xmlGetIntSubset(doc: *const _xmlDoc) -> *mut _xmlDtd {
1995    crate::xml::tree::get_int_subset(doc)
1996}
1997
1998/// Create a new DTD.
1999///
2000/// # UPSTREAM-PARITY
2001///
2002/// ```c
2003/// xmlDtdPtr xmlNewDtd(xmlDocPtr doc, const xmlChar *name,
2004///                     const xmlChar *ExternalID, const xmlChar *SystemID);
2005/// ```
2006#[no_mangle]
2007pub unsafe extern "C" fn xmlNewDtd(
2008    doc: *mut _xmlDoc,
2009    name: *const xmlChar,
2010    ExternalID: *const xmlChar,
2011    SystemID: *const xmlChar,
2012) -> *mut _xmlDtd {
2013    crate::xml::tree::new_dtd(doc, name, ExternalID, SystemID)
2014}
2015
2016/// Create a new entity.
2017///
2018/// # UPSTREAM-PARITY
2019///
2020/// ```c
2021/// xmlEntityPtr xmlNewEntity(xmlDocPtr doc, const xmlChar *name, int type,
2022///                           const xmlChar *ExternalID, const xmlChar *SystemID,
2023///                           const xmlChar *content);
2024/// ```
2025#[no_mangle]
2026pub unsafe extern "C" fn xmlNewEntity(
2027    doc: *mut _xmlDoc,
2028    name: *const xmlChar,
2029    type_: c_int,
2030    ExternalID: *const xmlChar,
2031    SystemID: *const xmlChar,
2032    content: *const xmlChar,
2033) -> *mut _xmlEntity {
2034    crate::xml::tree::new_entity(doc, name, type_, ExternalID, SystemID, content)
2035}
2036
2037/// Get an entity by name.
2038///
2039/// # UPSTREAM-PARITY
2040///
2041/// ```c
2042/// xmlEntityPtr xmlGetDocEntity(const xmlDoc *doc, const xmlChar *name);
2043/// ```
2044#[no_mangle]
2045pub unsafe extern "C" fn xmlGetDocEntity(
2046    doc: *const _xmlDoc,
2047    name: *const xmlChar,
2048) -> *mut _xmlEntity {
2049    crate::xml::tree::get_doc_entity(doc, name)
2050}
2051
2052/// Get a parameter entity by name.
2053///
2054/// # UPSTREAM-PARITY
2055///
2056/// ```c
2057/// xmlEntityPtr xmlGetParameterEntity(const xmlDoc *doc, const xmlChar *name);
2058/// ```
2059#[no_mangle]
2060pub unsafe extern "C" fn xmlGetParameterEntity(
2061    doc: *const _xmlDoc,
2062    name: *const xmlChar,
2063) -> *mut _xmlEntity {
2064    crate::xml::tree::get_parameter_entity(doc, name)
2065}
2066
2067// ── DTD Declaration Exports ────────────────────────────────────────────
2068
2069/// Create an internal subset (DTD).
2070///
2071/// # UPSTREAM-PARITY
2072///
2073/// ```c
2074/// xmlDtdPtr xmlCreateIntSubset(xmlDocPtr doc, const xmlChar *name,
2075///                              const xmlChar *ExternalID, const xmlChar *SystemID);
2076/// ```
2077#[no_mangle]
2078pub unsafe extern "C" fn xmlCreateIntSubset(
2079    doc: *mut _xmlDoc,
2080    name: *const xmlChar,
2081    ExternalID: *const xmlChar,
2082    SystemID: *const xmlChar,
2083) -> *mut _xmlDtd {
2084    crate::xml::dtd::create_int_subset(doc, name, ExternalID, SystemID)
2085}
2086
2087/// Free a DTD.
2088///
2089/// # UPSTREAM-PARITY
2090///
2091/// ```c
2092/// void xmlFreeDtd(xmlDtdPtr dtd);
2093/// ```
2094#[no_mangle]
2095pub unsafe extern "C" fn xmlFreeDtd(dtd: *mut _xmlDtd) {
2096    crate::xml::dtd::free_dtd(dtd);
2097}
2098
2099/// Add a notation declaration (upstream valid.h 2.15: the `ctxt` first
2100/// argument is part of the ABI; R-000176).
2101///
2102/// # UPSTREAM-PARITY
2103///
2104/// ```c
2105/// xmlNotationPtr xmlAddNotationDecl(xmlValidCtxt *ctxt, xmlDtd *dtd,
2106///                                   const xmlChar *name,
2107///                                   const xmlChar *publicId,
2108///                                   const xmlChar *systemId);
2109/// ```
2110///
2111/// `ctxt` is accepted for ABI parity; upstream uses it only for validation
2112/// error reporting, which the candidate performs in `xml/validation` (the
2113/// parser-side error model), so the argument is currently unused.
2114#[no_mangle]
2115pub unsafe extern "C" fn xmlAddNotationDecl(
2116    _ctxt: *mut _xmlValidCtxt,
2117    dtd: *mut _xmlDtd,
2118    name: *const xmlChar,
2119    PublicID: *const xmlChar,
2120    SystemID: *const xmlChar,
2121) -> *mut _xmlNotation {
2122    crate::xml::dtd::add_notation_decl(dtd, name, PublicID, SystemID)
2123}
2124
2125/// Look up a notation declaration.
2126///
2127/// # UPSTREAM-PARITY
2128///
2129/// ```c
2130/// xmlNotationPtr xmlGetNotationDecl(xmlDtdPtr dtd, const xmlChar *name);
2131/// ```
2132#[no_mangle]
2133pub unsafe extern "C" fn xmlGetNotationDecl(
2134    dtd: *mut _xmlDtd,
2135    name: *const xmlChar,
2136) -> *mut _xmlNotation {
2137    crate::xml::dtd::get_notation_decl(dtd, name)
2138}
2139
2140/// Copy a notation declaration.
2141///
2142/// # UPSTREAM-PARITY
2143///
2144/// ```c
2145/// xmlNotationPtr xmlCopyNotation(xmlNotationPtr notation);
2146/// ```
2147#[no_mangle]
2148pub unsafe extern "C" fn xmlCopyNotation(notation: *mut _xmlNotation) -> *mut _xmlNotation {
2149    crate::xml::dtd::copy_notation(notation)
2150}
2151
2152/// Free a notation declaration.
2153///
2154/// # UPSTREAM-PARITY
2155///
2156/// ```c
2157/// void xmlFreeNotation(xmlNotationPtr notation);
2158/// ```
2159#[no_mangle]
2160pub unsafe extern "C" fn xmlFreeNotation(notation: *mut _xmlNotation) {
2161    crate::xml::dtd::free_notation(notation);
2162}
2163
2164/// Add an element declaration (upstream valid.h 2.15: `ctxt` first arg;
2165/// R-000176).
2166///
2167/// # UPSTREAM-PARITY
2168///
2169/// ```c
2170/// xmlElementPtr xmlAddElementDecl(xmlValidCtxt *ctxt, xmlDtd *dtd,
2171///                                 const xmlChar *name, xmlElementTypeVal type,
2172///                                 xmlElementContent *content);
2173/// ```
2174///
2175/// `ctxt` is accepted for ABI parity; upstream uses it only for validation
2176/// error reporting (candidate: parser-side error model in `xml/validation`).
2177#[no_mangle]
2178pub unsafe extern "C" fn xmlAddElementDecl(
2179    _ctxt: *mut _xmlValidCtxt,
2180    dtd: *mut _xmlDtd,
2181    name: *const xmlChar,
2182    type_: c_int,
2183    content: *mut _xmlElementContent,
2184) -> *mut _xmlElement {
2185    crate::xml::dtd::add_element_decl(dtd, name, type_, content)
2186}
2187
2188/// Look up an element declaration.
2189///
2190/// # UPSTREAM-PARITY
2191///
2192/// ```c
2193/// xmlElementPtr xmlGetElementDecl(xmlDtdPtr dtd, const xmlChar *name);
2194/// ```
2195#[no_mangle]
2196pub unsafe extern "C" fn xmlGetElementDecl(
2197    dtd: *mut _xmlDtd,
2198    name: *const xmlChar,
2199) -> *mut _xmlElement {
2200    crate::xml::dtd::get_element_decl(dtd, name)
2201}
2202
2203/// Copy an element declaration.
2204///
2205/// # UPSTREAM-PARITY
2206///
2207/// ```c
2208/// xmlElementPtr xmlCopyElement(xmlElementPtr elem);
2209/// ```
2210#[no_mangle]
2211pub unsafe extern "C" fn xmlCopyElement(elem: *mut _xmlElement) -> *mut _xmlElement {
2212    crate::xml::dtd::copy_element(elem)
2213}
2214
2215/// Free an element declaration.
2216///
2217/// # UPSTREAM-PARITY
2218///
2219/// ```c
2220/// void xmlFreeElement(xmlElementPtr elem);
2221/// ```
2222#[no_mangle]
2223pub unsafe extern "C" fn xmlFreeElement(elem: *mut _xmlElement) {
2224    crate::xml::dtd::free_element(elem);
2225}
2226
2227/// Add an attribute declaration (upstream valid.h 2.15: 9-arg contract with
2228/// `ctxt` and the namespace key; R-000176).
2229///
2230/// # UPSTREAM-PARITY
2231///
2232/// ```c
2233/// xmlAttributePtr xmlAddAttributeDecl(xmlValidCtxt *ctxt, xmlDtd *dtd,
2234///                                     const xmlChar *elem, const xmlChar *name,
2235///                                     const xmlChar *ns, xmlAttributeType type,
2236///                                     xmlAttributeDefault def,
2237///                                     const xmlChar *defaultValue,
2238///                                     xmlEnumeration *tree);
2239/// ```
2240///
2241/// `ctxt` is accepted for ABI parity; upstream uses it only for validation
2242/// error reporting (candidate: parser-side error model in `xml/validation`).
2243/// The `ns` namespace is threaded into the DTD attribute table as the
2244/// middle hash key and into `attr->prefix`, exactly as upstream valid.c.
2245#[no_mangle]
2246pub unsafe extern "C" fn xmlAddAttributeDecl(
2247    _ctxt: *mut _xmlValidCtxt,
2248    dtd: *mut _xmlDtd,
2249    elem: *mut _xmlElement,
2250    name: *const xmlChar,
2251    ns: *const xmlChar,
2252    type_: c_int,
2253    def: c_int,
2254    defaultValue: *const xmlChar,
2255    tree: *mut _xmlEnumeration,
2256) -> *mut _xmlAttribute {
2257    crate::xml::dtd::add_attribute_decl(dtd, elem, name, ns, type_, def, defaultValue, tree)
2258}
2259
2260/// Look up an attribute declaration.
2261///
2262/// # UPSTREAM-PARITY
2263///
2264/// ```c
2265/// xmlAttributePtr xmlGetAttributeDecl(xmlDtdPtr dtd, xmlElementPtr elem,
2266///                                     const xmlChar *name, int namePrefix);
2267/// ```
2268#[no_mangle]
2269pub unsafe extern "C" fn xmlGetAttributeDecl(
2270    dtd: *mut _xmlDtd,
2271    elem: *mut _xmlElement,
2272    name: *const xmlChar,
2273    namePrefix: c_int,
2274) -> *mut _xmlAttribute {
2275    crate::xml::dtd::get_attribute_decl(dtd, elem, name, namePrefix)
2276}
2277
2278/// Copy an attribute declaration.
2279///
2280/// # UPSTREAM-PARITY
2281///
2282/// ```c
2283/// xmlAttributePtr xmlCopyAttribute(xmlAttributePtr attr);
2284/// ```
2285#[no_mangle]
2286pub unsafe extern "C" fn xmlCopyAttribute(attr: *mut _xmlAttribute) -> *mut _xmlAttribute {
2287    crate::xml::dtd::copy_attribute_decl(attr)
2288}
2289
2290/// Free an attribute declaration.
2291///
2292/// # UPSTREAM-PARITY
2293///
2294/// ```c
2295/// void xmlFreeAttribute(xmlAttributePtr attr);
2296/// ```
2297#[no_mangle]
2298pub unsafe extern "C" fn xmlFreeAttribute(attr: *mut _xmlAttribute) {
2299    crate::xml::dtd::free_attribute(attr);
2300}
2301
2302/// Create a new element content model.
2303///
2304/// # UPSTREAM-PARITY
2305///
2306/// ```c
2307/// xmlElementContentPtr xmlNewElementContent(const xmlChar *name, int type);
2308/// ```
2309#[no_mangle]
2310pub unsafe extern "C" fn xmlNewElementContent(
2311    name: *const xmlChar,
2312    type_: c_int,
2313) -> *mut _xmlElementContent {
2314    crate::xml::dtd::create_content_model(name, type_)
2315}
2316
2317/// Copy an element content model.
2318///
2319/// # UPSTREAM-PARITY
2320///
2321/// ```c
2322/// xmlElementContentPtr xmlCopyElementContent(xmlElementContentPtr content);
2323/// ```
2324#[no_mangle]
2325pub unsafe extern "C" fn xmlCopyElementContent(
2326    content: *mut _xmlElementContent,
2327) -> *mut _xmlElementContent {
2328    crate::xml::dtd::copy_content_model(content)
2329}
2330
2331/// Free an element content model.
2332///
2333/// # UPSTREAM-PARITY
2334///
2335/// ```c
2336/// void xmlFreeElementContent(xmlElementContentPtr cur);
2337/// ```
2338#[no_mangle]
2339pub unsafe extern "C" fn xmlFreeElementContent(cur: *mut _xmlElementContent) {
2340    crate::xml::dtd::free_content_model(cur);
2341}
2342
2343// ── Entity Exports ─────────────────────────────────────────────────────
2344
2345/// Add an entity declaration to a document's DTD (upstream entities.h 2.15:
2346/// entities.c `xmlAddEntity` — a full int/error-code contract, R-000176).
2347///
2348/// # UPSTREAM-PARITY
2349///
2350/// ```c
2351/// int xmlAddEntity(xmlDoc *doc, int extSubset, const xmlChar *name, int type,
2352///                  const xmlChar *publicId, const xmlChar *systemId,
2353///                  const xmlChar *content, xmlEntity **out);
2354/// ```
2355///
2356/// Returns 0 on success (with the new entity in `*out`), `XML_ERR_ARGUMENT`
2357/// for NULL doc/name or an unknown type, `XML_DTD_NO_DTD` when the selected
2358/// subset does not exist, `XML_ERR_REDECL_PREDEF_ENTITY` for an invalid
2359/// predefined-entity redeclaration, `XML_ERR_NO_MEMORY` on allocation
2360/// failure and `XML_WAR_ENTITY_REDEFINED` when the name already exists.
2361/// `*out` is set to NULL on entry and on every failure.
2362#[no_mangle]
2363pub unsafe extern "C" fn xmlAddEntity(
2364    doc: *mut _xmlDoc,
2365    extSubset: c_int,
2366    name: *const xmlChar,
2367    type_: c_int,
2368    publicId: *const xmlChar,
2369    systemId: *const xmlChar,
2370    content: *const xmlChar,
2371    out: *mut *mut _xmlEntity,
2372) -> c_int {
2373    crate::xml::entities::add_entity_doc(
2374        doc, extSubset, name, type_, publicId, systemId, content, out,
2375    )
2376}
2377
2378/// Add an entity declaration to a document's internal subset (upstream
2379/// entities.c `xmlAddDocEntity`): if the document has no internal subset
2380/// one is created.
2381///
2382/// # UPSTREAM-PARITY
2383///
2384/// ```c
2385/// xmlEntityPtr xmlAddDocEntity(xmlDocPtr doc, const xmlChar *name, int type,
2386///                              const xmlChar *ExternalID, const xmlChar *SystemID,
2387///                              const xmlChar *content);
2388/// ```
2389#[no_mangle]
2390pub unsafe extern "C" fn xmlAddDocEntity(
2391    doc: *mut _xmlDoc,
2392    name: *const xmlChar,
2393    type_: c_int,
2394    ExternalID: *const xmlChar,
2395    SystemID: *const xmlChar,
2396    content: *const xmlChar,
2397) -> *mut _xmlEntity {
2398    crate::xml::tree::add_doc_entity(doc, name, type_, ExternalID, SystemID, content)
2399}
2400
2401/// Add an entity declaration to a document's external subset (upstream
2402/// entities.c `xmlAddDtdEntity`).
2403///
2404/// # UPSTREAM-PARITY
2405///
2406/// ```c
2407/// xmlEntityPtr xmlAddDtdEntity(xmlDocPtr doc, const xmlChar *name, int type,
2408///                              const xmlChar *ExternalID, const xmlChar *SystemID,
2409///                              const xmlChar *content);
2410/// ```
2411#[no_mangle]
2412pub unsafe extern "C" fn xmlAddDtdEntity(
2413    doc: *mut _xmlDoc,
2414    name: *const xmlChar,
2415    type_: c_int,
2416    ExternalID: *const xmlChar,
2417    SystemID: *const xmlChar,
2418    content: *const xmlChar,
2419) -> *mut _xmlEntity {
2420    crate::xml::tree::add_dtd_entity(doc, name, type_, ExternalID, SystemID, content)
2421}
2422
2423/// Get an entity declaration from a DTD (upstream entities.c
2424/// `xmlGetDtdEntity`): searches the internal then external subset.
2425///
2426/// # UPSTREAM-PARITY
2427///
2428/// ```c
2429/// xmlEntityPtr xmlGetDtdEntity(xmlDocPtr doc, const xmlChar *name);
2430/// ```
2431#[no_mangle]
2432pub unsafe extern "C" fn xmlGetDtdEntity(
2433    doc: *mut _xmlDoc,
2434    name: *const xmlChar,
2435) -> *mut _xmlEntity {
2436    crate::xml::tree::get_dtd_entity(doc, name)
2437}
2438
2439/// Get an entity by name.
2440///
2441/// # UPSTREAM-PARITY
2442///
2443/// ```c
2444/// xmlEntityPtr xmlGetEntity(xmlDocPtr doc, const xmlChar *name);
2445/// ```
2446#[no_mangle]
2447pub unsafe extern "C" fn xmlGetEntity(doc: *mut _xmlDoc, name: *const xmlChar) -> *mut _xmlEntity {
2448    crate::xml::entities::get_entity(doc, name)
2449}
2450
2451/// Copy an entity.
2452///
2453/// # UPSTREAM-PARITY
2454///
2455/// ```c
2456/// xmlEntityPtr xmlCopyEntity(xmlEntityPtr entity);
2457/// ```
2458#[no_mangle]
2459pub unsafe extern "C" fn xmlCopyEntity(entity: *mut _xmlEntity) -> *mut _xmlEntity {
2460    crate::xml::entities::copy_entity(entity)
2461}
2462
2463/// Free an entity.
2464///
2465/// # UPSTREAM-PARITY
2466///
2467/// ```c
2468/// void xmlFreeEntity(xmlEntityPtr entity);
2469/// ```
2470#[no_mangle]
2471pub unsafe extern "C" fn xmlFreeEntity(entity: *mut _xmlEntity) {
2472    crate::xml::entities::free_entity(entity);
2473}
2474
2475/// Encode entities for reentrant output.
2476///
2477/// # UPSTREAM-PARITY
2478///
2479/// ```c
2480/// xmlChar *xmlEncodeEntitiesReentrant(xmlDocPtr doc, const xmlChar *input);
2481/// ```
2482#[no_mangle]
2483pub unsafe extern "C" fn xmlEncodeEntitiesReentrant(
2484    doc: *mut _xmlDoc,
2485    input: *const xmlChar,
2486) -> *mut xmlChar {
2487    crate::xml::entities::encode_entities_reentrant(doc, input)
2488}
2489
2490/// Encode special characters in a string (upstream entities.c
2491/// `xmlEncodeSpecialChars`): escapes `<`, `>`, `&`, `"` and `\r`.
2492///
2493/// # UPSTREAM-PARITY
2494///
2495/// ```c
2496/// xmlChar *xmlEncodeSpecialChars(const xmlDoc *doc, const xmlChar *input);
2497/// ```
2498///
2499/// Returns a newly allocated string (free with `xmlFree`) or NULL.
2500#[no_mangle]
2501pub unsafe extern "C" fn xmlEncodeSpecialChars(
2502    _doc: *const _xmlDoc,
2503    input: *const xmlChar,
2504) -> *mut xmlChar {
2505    if input.is_null() {
2506        return ptr::null_mut();
2507    }
2508    unsafe {
2509        let len = crate::xml::string::xml_strlen(input);
2510        // Worst case: every byte becomes a 6-byte entity (&#13; is 5; &quot;
2511        // is 6).
2512        let cap = len * 6 + 1;
2513        let out = crate::abi::allocator::xmlMallocImpl(cap) as *mut xmlChar;
2514        if out.is_null() {
2515            return ptr::null_mut();
2516        }
2517        let mut o = 0usize;
2518        let mut i = 0usize;
2519        while i < len {
2520            let c = *input.add(i);
2521            let rep: &[u8] = match c {
2522                b'<' => b"&lt;",
2523                b'>' => b"&gt;",
2524                b'&' => b"&amp;",
2525                b'"' => b"&quot;",
2526                b'\r' => b"&#13;",
2527                _ => {
2528                    *out.add(o) = c;
2529                    o += 1;
2530                    i += 1;
2531                    continue;
2532                }
2533            };
2534            core::ptr::copy_nonoverlapping(rep.as_ptr(), out.add(o), rep.len());
2535            o += rep.len();
2536            i += 1;
2537        }
2538        *out.add(o) = 0;
2539        out
2540    }
2541}
2542
2543/// Deprecated entity encoder (upstream 2.15 `xmlEncodeEntities`): the
2544/// symbol still exists for ABI compatibility but emits a one-time
2545/// deprecation warning and returns NULL (verified against the oracle DSO
2546/// disassembly — the 2.15 implementation returns NULL after warning).
2547///
2548/// # UPSTREAM-PARITY
2549///
2550/// ```c
2551/// xmlChar *xmlEncodeEntities(xmlDocPtr doc, const xmlChar *input);
2552/// ```
2553#[no_mangle]
2554pub unsafe extern "C" fn xmlEncodeEntities(
2555    _doc: *mut _xmlDoc,
2556    _input: *const xmlChar,
2557) -> *mut xmlChar {
2558    use core::sync::atomic::{AtomicBool, Ordering};
2559    static WARNED: AtomicBool = AtomicBool::new(false);
2560    if !WARNED.swap(true, Ordering::Relaxed) {
2561        // Match the oracle: one-time "deprecated" diagnostic on stderr.
2562        let msg = b"xmlEncodeEntities is deprecated, use xmlEncodeSpecialChars or xmlEncodeEntitiesReentrant\n";
2563        unsafe {
2564            libc::fwrite(
2565                msg.as_ptr() as *const c_void,
2566                1,
2567                msg.len(),
2568                libc::fdopen(2, b"w\0" as *const u8 as *const c_char),
2569            );
2570        }
2571    }
2572    ptr::null_mut()
2573}
2574
2575/// Get the line number of a node.
2576///
2577/// # UPSTREAM-PARITY
2578///
2579/// ```c
2580/// long xmlGetLineNo(const xmlNode *node);
2581/// ```
2582#[no_mangle]
2583pub extern "C" fn xmlGetLineNo(node: *const _xmlNode) -> c_long {
2584    crate::xml::tree::get_line_no(node)
2585}
2586
2587// ═══════════════════════════════════════════════════════════════════════════════
2588// Serialization — xmlNodeDump, xmlDocDump, xmlSaveFile, etc.
2589// ═══════════════════════════════════════════════════════════════════════════════
2590
2591/// Dump a node to a buffer.
2592///
2593/// # UPSTREAM-PARITY
2594///
2595/// ```c
2596/// int xmlNodeDump(xmlBufferPtr buf, xmlDocPtr doc, xmlNodePtr cur, int level, int format);
2597/// ```
2598#[no_mangle]
2599pub unsafe extern "C" fn xmlNodeDump(
2600    buf: *mut _xmlBuffer,
2601    doc: *mut _xmlDoc,
2602    cur: *mut _xmlNode,
2603    level: c_int,
2604    format: c_int,
2605) -> c_int {
2606    if buf.is_null() || cur.is_null() {
2607        return -1;
2608    }
2609    crate::xml::tree::xmlNodeDump(buf, doc, cur, level, format)
2610}
2611
2612/// Dump a document to a file pointer.
2613///
2614/// # UPSTREAM-PARITY
2615///
2616/// ```c
2617/// int xmlDocDump(FILE *f, xmlDocPtr doc);
2618/// ```
2619#[no_mangle]
2620pub unsafe extern "C" fn xmlDocDump(fp: *mut c_void, doc: *mut _xmlDoc) -> c_int {
2621    if fp.is_null() || doc.is_null() {
2622        return -1;
2623    }
2624    crate::xml::tree::xmlDocDump(fp, doc)
2625}
2626
2627/// Dump a document to memory with format.
2628///
2629/// # UPSTREAM-PARITY
2630///
2631/// ```c
2632/// void xmlDocDumpFormatMemory(xmlDocPtr doc, xmlChar **mem, int *size, int format);
2633/// ```
2634#[no_mangle]
2635pub unsafe extern "C" fn xmlDocDumpFormatMemory(
2636    doc: *mut _xmlDoc,
2637    mem: *mut *mut xmlChar,
2638    size: *mut c_int,
2639    format: c_int,
2640) {
2641    if doc.is_null() || mem.is_null() {
2642        return;
2643    }
2644    crate::xml::tree::xmlDocDumpFormatMemory(doc, mem, size, format)
2645}
2646
2647/// Dump a document to memory (unformatted).
2648///
2649/// # UPSTREAM-PARITY
2650///
2651/// ```c
2652/// void xmlDocDumpMemory(xmlDocPtr doc, xmlChar **mem, int *size);
2653/// ```
2654#[no_mangle]
2655pub unsafe extern "C" fn xmlDocDumpMemory(
2656    doc: *mut _xmlDoc,
2657    mem: *mut *mut xmlChar,
2658    size: *mut c_int,
2659) {
2660    if doc.is_null() || mem.is_null() {
2661        return;
2662    }
2663    crate::xml::tree::xmlDocDumpMemory(doc, mem, size)
2664}
2665
2666/// Save a document to a file.
2667///
2668/// # UPSTREAM-PARITY
2669///
2670/// ```c
2671/// int xmlSaveFile(const char *filename, xmlDocPtr cur);
2672/// ```
2673#[no_mangle]
2674pub unsafe extern "C" fn xmlSaveFile(filename: *const c_char, cur: *mut _xmlDoc) -> c_int {
2675    if filename.is_null() || cur.is_null() {
2676        return -1;
2677    }
2678    crate::xml::tree::xmlSaveFile(filename, cur)
2679}
2680
2681/// Save a document to a file with encoding.
2682///
2683/// # UPSTREAM-PARITY
2684///
2685/// ```c
2686/// int xmlSaveFileEnc(const char *filename, xmlDocPtr cur, const char *encoding);
2687/// ```
2688#[no_mangle]
2689pub unsafe extern "C" fn xmlSaveFileEnc(
2690    filename: *const c_char,
2691    cur: *mut _xmlDoc,
2692    encoding: *const c_char,
2693) -> c_int {
2694    if filename.is_null() || cur.is_null() {
2695        return -1;
2696    }
2697    crate::xml::tree::xmlSaveFileEnc(filename, cur, encoding)
2698}
2699
2700/// Save a document to a file with format.
2701///
2702/// # UPSTREAM-PARITY
2703///
2704/// ```c
2705/// int xmlSaveFormatFile(const char *filename, xmlDocPtr cur, int format);
2706/// ```
2707#[no_mangle]
2708pub unsafe extern "C" fn xmlSaveFormatFile(
2709    filename: *const c_char,
2710    cur: *mut _xmlDoc,
2711    format: c_int,
2712) -> c_int {
2713    if filename.is_null() || cur.is_null() {
2714        return -1;
2715    }
2716    crate::xml::tree::xmlSaveFormatFile(filename, cur, format)
2717}
2718
2719/// Save a document to a file with encoding and format.
2720///
2721/// # UPSTREAM-PARITY
2722///
2723/// ```c
2724/// int xmlSaveFormatFileEnc(const char *filename, xmlDocPtr cur, const char *encoding, int format);
2725/// ```
2726#[no_mangle]
2727pub unsafe extern "C" fn xmlSaveFormatFileEnc(
2728    filename: *const c_char,
2729    cur: *mut _xmlDoc,
2730    encoding: *const c_char,
2731    format: c_int,
2732) -> c_int {
2733    if filename.is_null() || cur.is_null() {
2734        return -1;
2735    }
2736    crate::xml::tree::xmlSaveFormatFileEnc(filename, cur, encoding, format)
2737}
2738
2739// ═══════════════════════════════════════════════════════════════════════════════
2740// 7. Parser — SAX, DOM, Push, Reader
2741// ═══════════════════════════════════════════════════════════════════════════════
2742
2743/// Read an XML document from a string.
2744///
2745/// # UPSTREAM-PARITY
2746///
2747/// ```c
2748/// xmlDocPtr xmlReadDoc(const xmlChar *cur, const char *URL,
2749///                      const char *encoding, int options);
2750/// ```
2751///
2752/// Returns a parsed document. Caller must free with `xmlFreeDoc`.
2753#[no_mangle]
2754pub unsafe extern "C" fn xmlReadDoc(
2755    cur: *const xmlChar,
2756    URL: *const c_char,
2757    encoding: *const c_char,
2758    options: c_int,
2759) -> *mut _xmlDoc {
2760    // SAFETY: cur must be a valid null-terminated xmlChar string if non-null.
2761    if cur.is_null() {
2762        return ptr::null_mut();
2763    }
2764    let ctxt = crate::xml::parser::helpers::create_parser_ctxt();
2765    if ctxt.is_null() {
2766        return ptr::null_mut();
2767    }
2768    let len = crate::xml::string::xml_strlen(cur);
2769    // §16.5.2: xmlReadDoc creates, parses and frees its own context inside
2770    // this call, so the input BORROWS the caller's string (zero copy); the
2771    // caller's buffer is only required to stay valid for the call.
2772    let input =
2773        crate::xml::parser::helpers::input_from_memory_borrowed(cur as *const c_char, len as c_int);
2774    crate::xml::parser::helpers::setup_parser_input(ctxt, input);
2775    (*ctxt).options = options;
2776    if crate::xml::parser::helpers::parse_document(ctxt) != 0 {
2777        let doc = (*ctxt).myDoc;
2778        crate::xml::parser::helpers::free_parser_ctxt(ctxt);
2779        return doc;
2780    }
2781    let doc = (*ctxt).myDoc;
2782    if !doc.is_null() {
2783        (*doc).URL = crate::xml::string::xml_strdup(URL as *const xmlChar);
2784    }
2785    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
2786    doc
2787}
2788
2789/// Read an XML document from a file.
2790///
2791/// # UPSTREAM-PARITY
2792///
2793/// ```c
2794/// xmlDocPtr xmlReadFile(const char *URL, const char *encoding, int options);
2795/// ```
2796#[no_mangle]
2797pub unsafe extern "C" fn xmlReadFile(
2798    URL: *const c_char,
2799    encoding: *const c_char,
2800    options: c_int,
2801) -> *mut _xmlDoc {
2802    // SAFETY: URL must be a valid C string or NULL.
2803    if URL.is_null() {
2804        return ptr::null_mut();
2805    }
2806    let ctxt = crate::xml::parser::helpers::create_parser_ctxt();
2807    if ctxt.is_null() {
2808        return ptr::null_mut();
2809    }
2810    // UPSTREAM-PARITY (parser.c xmlReadFile -> xmlCtxtReadFile): options are
2811    // applied before the input open (xmlCtxtUseOptions -> xmlCtxtNewInputFromUrl
2812    // -> xmlLoadResource), so a registered external entity loader observes
2813    // them; below it the xmlParserInputBufferCreateFilenameDefault (php
2814    // streams loader) is consulted — a NULL result raises xmlCtxtErrIO — "I/O
2815    // warning : failed to load \"%s\": %s\n" — and the load fails.
2816    (*ctxt).options = options;
2817    // UPSTREAM-PARITY (xmlCtxtUseOptions): replaceEntities is derived from
2818    // the options argument — a deprecated-global seed (create_parser_ctxt
2819    // snapshots xmlSubstituteEntitiesDefault, which PHP's ext/xsl sets at
2820    // request init) must not leak into a read whose options lack NOENT.
2821    (*ctxt).replaceEntities = (options & crate::abi::types::XML_PARSE_NOENT != 0) as c_int;
2822    let input = match crate::abi::exports_parser::open_filename_routed(URL, ctxt) {
2823        crate::abi::exports_parser::RoutedFileOpen::Loaded(i) => i,
2824        crate::abi::exports_parser::RoutedFileOpen::Failed => {
2825            crate::abi::exports_parser::emit_io_warning(
2826                ctxt,
2827                crate::abi::exports_parser::io_load_failure_message(URL),
2828            );
2829            crate::xml::parser::helpers::free_parser_ctxt(ctxt);
2830            return ptr::null_mut();
2831        }
2832        crate::abi::exports_parser::RoutedFileOpen::EntityLoaderFailed => {
2833            crate::xml::parser::helpers::free_parser_ctxt(ctxt);
2834            return ptr::null_mut();
2835        }
2836        crate::abi::exports_parser::RoutedFileOpen::Builtin => {
2837            match crate::xml::parser::helpers::input_from_file(URL) {
2838                Ok(input) => input,
2839                Err(_) => {
2840                    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
2841                    return ptr::null_mut();
2842                }
2843            }
2844        }
2845    };
2846    crate::xml::parser::helpers::setup_parser_input(ctxt, input);
2847    if crate::xml::parser::helpers::parse_document(ctxt) != 0 {
2848        let doc = (*ctxt).myDoc;
2849        crate::xml::parser::helpers::free_parser_ctxt(ctxt);
2850        // UPSTREAM-PARITY: on a hard (non-recoverable) parse error the
2851        // partially built document is discarded and NULL is returned; with
2852        // XML_PARSE_RECOVER the partial tree is kept.
2853        if options & 1 << 0 != 0 {
2854            return doc;
2855        }
2856        if !doc.is_null() {
2857            crate::xml::tree::free_doc(doc);
2858        }
2859        return ptr::null_mut();
2860    }
2861    let doc = (*ctxt).myDoc;
2862    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
2863    doc
2864}
2865
2866/// Recover-parse a document from a string (upstream parser.h): same as
2867/// `xmlReadDoc` with XML_PARSE_RECOVER forced.
2868///
2869/// # UPSTREAM-PARITY
2870///
2871/// ```c
2872/// xmlDocPtr xmlRecoverDoc(const xmlChar *cur);
2873/// ```
2874#[no_mangle]
2875pub unsafe extern "C" fn xmlRecoverDoc(cur: *const xmlChar) -> *mut _xmlDoc {
2876    unsafe { xmlReadDoc(cur, ptr::null(), ptr::null(), 1 << 0) }
2877}
2878
2879/// Recover-parse a document from a file (upstream parser.h).
2880///
2881/// # UPSTREAM-PARITY
2882///
2883/// ```c
2884/// xmlDocPtr xmlRecoverFile(const char *filename);
2885/// ```
2886#[no_mangle]
2887pub unsafe extern "C" fn xmlRecoverFile(filename: *const c_char) -> *mut _xmlDoc {
2888    unsafe { xmlReadFile(filename, ptr::null(), 1 << 0) }
2889}
2890
2891/// Recover-parse a document from memory (upstream parser.h).
2892///
2893/// # UPSTREAM-PARITY
2894///
2895/// ```c
2896/// xmlDocPtr xmlRecoverMemory(const char *buffer, int size);
2897/// ```
2898#[no_mangle]
2899pub unsafe extern "C" fn xmlRecoverMemory(buffer: *const c_char, size: c_int) -> *mut _xmlDoc {
2900    unsafe { xmlReadMemory(buffer, size, ptr::null(), ptr::null(), 1 << 0) }
2901}
2902
2903/// Read an XML document from a file (upstream parser.h).
2904/// Canonically record an explicit input `encoding` on `doc->encoding` when
2905/// the caller supplied one and the document has no encoding of its own.
2906///
2907/// Mirrors upstream: the encoding name is canonicalized via the encoding
2908/// handler table (so `utf-8`/`UTF8` become `UTF-8`, `latin1` becomes
2909/// `ISO-8859-1`, …) rather than stored verbatim.
2910///
2911/// # SAFETY
2912///
2913/// - `doc` must be a valid `_xmlDoc` with a NULL `encoding` field.
2914/// - `encoding` must be a valid NUL-terminated C string.
2915unsafe fn canonical_doc_encoding(doc: *mut _xmlDoc, encoding: *const c_char) {
2916    let handler = crate::xml::encoding::xmlFindCharEncodingHandler(encoding);
2917    if handler.is_null() {
2918        return;
2919    }
2920    // SAFETY: handler is non-NULL; name is a NUL-terminated owned buffer.
2921    let name = unsafe { (*handler).name }; // *mut c_char
2922    if name.is_null() {
2923        return;
2924    }
2925    // SAFETY: name is a valid NUL-terminated string; caller frees via xmlFree.
2926    unsafe {
2927        (*doc).encoding = crate::xml::string::xml_strdup(name as *const xmlChar);
2928    }
2929}
2930
2931/// Parse an XML document from a C memory buffer.
2932///
2933/// # UPSTREAM-PARITY
2934///
2935/// ```c
2936/// xmlDocPtr xmlReadMemory(const char *buffer, int size,
2937///                         const char *URL, const char *encoding, int options);
2938/// ```
2939#[no_mangle]
2940pub unsafe extern "C" fn xmlReadMemory(
2941    buffer: *const c_char,
2942    size: c_int,
2943    URL: *const c_char,
2944    encoding: *const c_char,
2945    options: c_int,
2946) -> *mut _xmlDoc {
2947    // SAFETY: buffer must be a valid pointer with at least `size` readable
2948    // bytes. An empty input (size 0) is still parsed — upstream reports
2949    // "Document is empty".
2950    if buffer.is_null() || size < 0 {
2951        return ptr::null_mut();
2952    }
2953    // UPSTREAM-PARITY + HOSTILE-ABI hardening (parser.c 2.15 xmlReadMemory):
2954    // upstream only rejects `size < 0` and then streams from the caller's
2955    // buffer, so a size at or beyond INT_MAX turns into an unsized wild read
2956    // (the oracle's own outcome for xmlReadMemory("<a/>", INT_MAX, ...) is a
2957    // NULL document — or a crash of the oracle itself depending on the heap
2958    // layout). The candidate rejects such sizes up front instead of copying
2959    // ~2 GiB from the caller's buffer; the observable result matches the
2960    // oracle's deterministic probe outcome (HOSTILE-ABI D1).
2961    if size == c_int::MAX {
2962        return ptr::null_mut();
2963    }
2964    let ctxt = crate::xml::parser::helpers::create_parser_ctxt();
2965    if ctxt.is_null() {
2966        return ptr::null_mut();
2967    }
2968    // UPSTREAM-PARITY: the URL becomes the input's filename (feeds the
2969    // `file:line:` error prefix and doc->URL). §16.5.2: xmlReadMemory owns
2970    // its context for the duration of this call, so the input BORROWS the
2971    // caller's buffer — zero copies on the ordinary UTF-8 path (the caller's
2972    // memory only has to stay valid for the call, exactly like upstream's
2973    // xmlReadMemory streaming contract). A caller-supplied `encoding` below
2974    // converts to an owned transcoded buffer when it applies.
2975    let mut input =
2976        crate::xml::parser::helpers::input_from_memory_named_borrowed(buffer, size, URL);
2977    // UPSTREAM-PARITY (xmlReadMemory -> xmlCtxtReadMemory): an explicit
2978    // `encoding` argument switches the input before parsing (lxml/KIND-2/4
2979    // python strings arrive as raw UTF-16/UCS-4). Best-effort: unknown
2980    // names leave the raw bytes for BOM/declaration detection.
2981    if !encoding.is_null() {
2982        let name = core::ffi::CStr::from_ptr(encoding).to_bytes();
2983        input.apply_explicit_input_encoding(name);
2984    }
2985    crate::xml::parser::helpers::setup_parser_input(ctxt, input);
2986    // UPSTREAM-PARITY (xmlReadMemory -> xmlCtxtReadMemory): the parse
2987    // options are mirrored into the context members (dictNames, keepBlanks,
2988    // recovery, ...) before parsing starts.
2989    crate::abi::exports_parser::apply_options(ctxt, options);
2990    let parsed = crate::xml::parser::helpers::parse_document(ctxt);
2991    let doc = (*ctxt).myDoc;
2992    // UPSTREAM-PARITY: the URL is attached to the document on success AND on
2993    // the recovery path (the partial tree keeps the document identity).
2994    if !doc.is_null() && !URL.is_null() && (*doc).URL.is_null() {
2995        (*doc).URL = crate::xml::string::xml_strdup(URL as *const xmlChar);
2996    }
2997    // UPSTREAM-PARITY (xmlCtxtReadMemory): an explicit `encoding` argument
2998    // is recorded on the document when the document carries no encoding
2999    // declaration of its own, so `doc->encoding` (nokogiri Document#encoding)
3000    // reflects what the caller requested.
3001    if !doc.is_null() && (*doc).encoding.is_null() && !encoding.is_null() {
3002        canonical_doc_encoding(doc, encoding);
3003    }
3004    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3005    if parsed != 0 {
3006        // UPSTREAM-PARITY: on a hard (non-recoverable) parse error the
3007        // partially built document is discarded and NULL is returned; with
3008        // XML_PARSE_RECOVER the partial tree is kept.
3009        if options & 1 << 0 != 0 {
3010            return doc;
3011        }
3012        if !doc.is_null() {
3013            crate::xml::tree::free_doc(doc);
3014        }
3015        return ptr::null_mut();
3016    }
3017    doc
3018}
3019
3020/// Load a list of catalogs (upstream `xmlLoadCatalogs`).
3021///
3022/// # SAFETY
3023///
3024/// - `catalogs` must be a valid NUL-terminated string or NULL.
3025#[no_mangle]
3026pub unsafe extern "C" fn xmlLoadCatalogs(catalogs: *const c_char) {
3027    if !catalogs.is_null() {
3028        crate::xml::catalog::load_catalog(catalogs);
3029    }
3030}
3031
3032/// Load a single catalog (upstream `xmlLoadCatalog`).
3033///
3034/// # SAFETY
3035///
3036/// - `catalogs` must be a valid NUL-terminated string or NULL.
3037#[no_mangle]
3038pub unsafe extern "C" fn xmlLoadCatalog(catalogs: *const c_char) -> c_int {
3039    // UPSTREAM-PARITY (catalog.c xmlLoadCatalog): returns 0 on success,
3040    // 1 on error (unlike xmlCatalogLoad which returns the catalog handle).
3041    let handle = crate::xml::catalog::load_catalog(catalogs);
3042    if handle.is_null() {
3043        1
3044    } else {
3045        0
3046    }
3047}
3048
3049/// Read an XML document from a file descriptor.
3050///
3051/// # UPSTREAM-PARITY
3052///
3053/// ```c
3054/// xmlDocPtr xmlReadFd(int fd, const char *URL, const char *encoding, int options);
3055/// ```
3056#[no_mangle]
3057pub unsafe extern "C" fn xmlReadFd(
3058    fd: c_int,
3059    URL: *const c_char,
3060    encoding: *const c_char,
3061    options: c_int,
3062) -> *mut _xmlDoc {
3063    // SAFETY: fd must be a valid open file descriptor.
3064    let ctxt = crate::xml::parser::helpers::create_parser_ctxt();
3065    if ctxt.is_null() {
3066        return ptr::null_mut();
3067    }
3068    // Read all data from the fd
3069    let mut buf = Vec::new();
3070    let mut tmp = [0u8; 4096];
3071    loop {
3072        let n = libc::read(fd, tmp.as_mut_ptr() as *mut c_void, tmp.len());
3073        if n <= 0 {
3074            break;
3075        }
3076        buf.extend_from_slice(&tmp[..n as usize]);
3077    }
3078    let input = crate::xml::parser::helpers::input_from_memory(
3079        buf.as_ptr() as *const c_char,
3080        buf.len() as c_int,
3081    );
3082    crate::xml::parser::helpers::setup_parser_input(ctxt, input);
3083    (*ctxt).options = options;
3084    if crate::xml::parser::helpers::parse_document(ctxt) != 0 {
3085        let doc = (*ctxt).myDoc;
3086        crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3087        return doc;
3088    }
3089    let doc = (*ctxt).myDoc;
3090    if !doc.is_null() && !URL.is_null() {
3091        (*doc).URL = crate::xml::string::xml_strdup(URL as *const xmlChar);
3092    }
3093    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3094    doc
3095}
3096
3097/// Read an XML document from I/O callbacks.
3098///
3099/// # UPSTREAM-PARITY
3100///
3101/// ```c
3102/// xmlDocPtr xmlReadIO(xmlInputReadCallback ioread, xmlInputCloseCallback ioclose,
3103///                     void *ioctx, const char *URL, const char *encoding, int options);
3104/// ```
3105#[no_mangle]
3106pub unsafe extern "C" fn xmlReadIO(
3107    ioread: Option<xmlInputReadCallback>,
3108    ioclose: Option<xmlInputCloseCallback>,
3109    ioctx: *mut c_void,
3110    URL: *const c_char,
3111    encoding: *const c_char,
3112    options: c_int,
3113) -> *mut _xmlDoc {
3114    // SAFETY: callbacks must be valid function pointers if non-NULL.
3115    let ctxt = crate::xml::parser::helpers::create_parser_ctxt();
3116    if ctxt.is_null() {
3117        return ptr::null_mut();
3118    }
3119    let input = crate::xml::parser::helpers::input_from_io(ioread, ioclose, ioctx);
3120    crate::xml::parser::helpers::setup_parser_input(ctxt, input);
3121    (*ctxt).options = options;
3122    let parsed = crate::xml::parser::helpers::parse_document(ctxt);
3123    let doc = (*ctxt).myDoc;
3124    // UPSTREAM-PARITY (xmlReadIO -> xmlCtxtReadIO): the URL is attached to the
3125    // document on success AND on the recovery path (the partial tree keeps the
3126    // document identity).
3127    if !doc.is_null() && !URL.is_null() && (*doc).URL.is_null() {
3128        (*doc).URL = crate::xml::string::xml_strdup(URL as *const xmlChar);
3129    }
3130    if !doc.is_null() && (*doc).encoding.is_null() && !encoding.is_null() {
3131        canonical_doc_encoding(doc, encoding);
3132    }
3133    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3134    if parsed != 0 {
3135        // UPSTREAM-PARITY: on a hard (non-recoverable) parse error the
3136        // partially built document is discarded and NULL is returned; only with
3137        // XML_PARSE_RECOVER is the partial tree kept. nokogiri's read_io/strict
3138        // path relies on NULL here to raise a SyntaxError.
3139        if options & 1 << 0 != 0 {
3140            return doc;
3141        }
3142        if !doc.is_null() {
3143            crate::xml::tree::free_doc(doc);
3144        }
3145        return ptr::null_mut();
3146    }
3147    doc
3148}
3149
3150/// Parse an XML document (SAX1).
3151///
3152/// # UPSTREAM-PARITY
3153///
3154/// ```c
3155/// xmlDocPtr xmlSAXParseDoc(xmlSAXHandlerPtr sax, const xmlChar *cur, int recovery);
3156/// ```
3157#[no_mangle]
3158pub unsafe extern "C" fn xmlSAXParseDoc(
3159    sax: *mut _xmlSAXHandler,
3160    cur: *const xmlChar,
3161    recovery: c_int,
3162) -> *mut _xmlDoc {
3163    // SAFETY: cur must be a valid null-terminated xmlChar string.
3164    if cur.is_null() {
3165        return ptr::null_mut();
3166    }
3167    let ctxt = crate::xml::parser::helpers::create_parser_ctxt();
3168    if ctxt.is_null() {
3169        return ptr::null_mut();
3170    }
3171    if !sax.is_null() {
3172        (*ctxt).sax = sax;
3173        (*ctxt).userData = (*ctxt).sax as *mut c_void;
3174    }
3175    if recovery != 0 {
3176        (*ctxt).recovery = 1;
3177        (*ctxt).options |= 1; // XML_PARSE_RECOVER
3178    }
3179    let len = crate::xml::string::xml_strlen(cur);
3180    // §16.5.2 zero-copy: xmlSAXParseDoc creates, parses and frees its own
3181    // context inside this call — the input borrows the caller's string.
3182    let input =
3183        crate::xml::parser::helpers::input_from_memory_borrowed(cur as *const c_char, len as c_int);
3184    crate::xml::parser::helpers::setup_parser_input(ctxt, input);
3185    crate::xml::parser::helpers::parse_document(ctxt);
3186    let doc = (*ctxt).myDoc;
3187    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3188    doc
3189}
3190
3191/// Parse an XML document (SAX1) with user data (upstream parser.h
3192/// `xmlSAXParseDocWithData`): `user_data` is passed to the SAX callbacks.
3193///
3194/// # UPSTREAM-PARITY
3195///
3196/// ```c
3197/// xmlDocPtr xmlSAXParseDocWithData(xmlSAXHandlerPtr sax, const xmlChar *cur,
3198///                                  int recovery, void *data);
3199/// ```
3200#[no_mangle]
3201pub unsafe extern "C" fn xmlSAXParseDocWithData(
3202    sax: *mut _xmlSAXHandler,
3203    cur: *const xmlChar,
3204    recovery: c_int,
3205    data: *mut c_void,
3206) -> *mut _xmlDoc {
3207    // SAFETY: cur must be a valid null-terminated xmlChar string.
3208    if cur.is_null() {
3209        return ptr::null_mut();
3210    }
3211    let ctxt = crate::xml::parser::helpers::create_parser_ctxt();
3212    if ctxt.is_null() {
3213        return ptr::null_mut();
3214    }
3215    if !sax.is_null() {
3216        (*ctxt).sax = sax;
3217    }
3218    (*ctxt).userData = data;
3219    if recovery != 0 {
3220        (*ctxt).recovery = 1;
3221        (*ctxt).options |= 1; // XML_PARSE_RECOVER
3222    }
3223    let len = crate::xml::string::xml_strlen(cur);
3224    // §16.5.2 zero-copy: xmlSAXParseDocWithData owns its context for this
3225    // call — the input borrows the caller's string.
3226    let input =
3227        crate::xml::parser::helpers::input_from_memory_borrowed(cur as *const c_char, len as c_int);
3228    crate::xml::parser::helpers::setup_parser_input(ctxt, input);
3229    crate::xml::parser::helpers::parse_document(ctxt);
3230    let doc = (*ctxt).myDoc;
3231    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3232    doc
3233}
3234///
3235/// # UPSTREAM-PARITY
3236///
3237/// ```c
3238/// xmlDocPtr xmlSAXParseFileWithData(xmlSAXHandlerPtr sax, const char *filename,
3239///                                   int recovery, void *data);
3240/// ```
3241#[no_mangle]
3242pub unsafe extern "C" fn xmlSAXParseFileWithData(
3243    sax: *mut _xmlSAXHandler,
3244    filename: *const c_char,
3245    recovery: c_int,
3246    data: *mut c_void,
3247) -> *mut _xmlDoc {
3248    // SAFETY: filename must be a valid C string or NULL.
3249    if filename.is_null() {
3250        return ptr::null_mut();
3251    }
3252    let ctxt = crate::xml::parser::helpers::create_parser_ctxt();
3253    if ctxt.is_null() {
3254        return ptr::null_mut();
3255    }
3256    if !sax.is_null() {
3257        (*ctxt).sax = sax;
3258    }
3259    (*ctxt).userData = data;
3260    if recovery != 0 {
3261        (*ctxt).recovery = 1;
3262        (*ctxt).options |= 1; // XML_PARSE_RECOVER
3263    }
3264    let input = match crate::abi::exports_parser::open_filename_routed(filename, ctxt) {
3265        crate::abi::exports_parser::RoutedFileOpen::Loaded(input) => input,
3266        crate::abi::exports_parser::RoutedFileOpen::Failed => {
3267            crate::abi::exports_parser::emit_io_warning(
3268                ctxt,
3269                crate::abi::exports_parser::io_load_failure_message(filename),
3270            );
3271            crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3272            return ptr::null_mut();
3273        }
3274        crate::abi::exports_parser::RoutedFileOpen::EntityLoaderFailed => {
3275            crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3276            return ptr::null_mut();
3277        }
3278        crate::abi::exports_parser::RoutedFileOpen::Builtin => {
3279            match crate::xml::parser::helpers::input_from_file(filename) {
3280                Ok(input) => input,
3281                Err(_) => {
3282                    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3283                    return ptr::null_mut();
3284                }
3285            }
3286        }
3287    };
3288    crate::xml::parser::helpers::setup_parser_input(ctxt, input);
3289    crate::xml::parser::helpers::parse_document(ctxt);
3290    let doc = (*ctxt).myDoc;
3291    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3292    doc
3293}
3294
3295/// Parse an XML document (SAX1) with user data from memory (upstream
3296/// parser.h `xmlSAXParseMemoryWithData`).
3297///
3298/// # UPSTREAM-PARITY
3299///
3300/// ```c
3301/// xmlDocPtr xmlSAXParseMemoryWithData(xmlSAXHandlerPtr sax, const char *buffer,
3302///                                     int size, int recovery, void *data);
3303/// ```
3304#[no_mangle]
3305pub unsafe extern "C" fn xmlSAXParseMemoryWithData(
3306    sax: *mut _xmlSAXHandler,
3307    buffer: *const c_char,
3308    size: c_int,
3309    recovery: c_int,
3310    data: *mut c_void,
3311) -> *mut _xmlDoc {
3312    // SAFETY: buffer must be a valid pointer with `size` readable bytes.
3313    if buffer.is_null() || size <= 0 {
3314        return ptr::null_mut();
3315    }
3316    let ctxt = crate::xml::parser::helpers::create_parser_ctxt();
3317    if ctxt.is_null() {
3318        return ptr::null_mut();
3319    }
3320    if !sax.is_null() {
3321        (*ctxt).sax = sax;
3322    }
3323    (*ctxt).userData = data;
3324    if recovery != 0 {
3325        (*ctxt).recovery = 1;
3326        (*ctxt).options |= 1; // XML_PARSE_RECOVER
3327    }
3328    // §16.5.2 zero-copy: xmlSAXParseMemoryWithData owns its context for this
3329    // call — the input borrows the caller's buffer.
3330    let input = crate::xml::parser::helpers::input_from_memory_borrowed(buffer, size);
3331    crate::xml::parser::helpers::setup_parser_input(ctxt, input);
3332    crate::xml::parser::helpers::parse_document(ctxt);
3333    let doc = (*ctxt).myDoc;
3334    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3335    doc
3336}
3337
3338/// Parse an XML file (SAX1).
3339///
3340/// # UPSTREAM-PARITY
3341///
3342/// ```c
3343/// xmlDocPtr xmlSAXParseFile(xmlSAXHandlerPtr sax, const char *filename, int recovery);
3344/// ```
3345#[no_mangle]
3346pub unsafe extern "C" fn xmlSAXParseFile(
3347    sax: *mut _xmlSAXHandler,
3348    filename: *const c_char,
3349    recovery: c_int,
3350) -> *mut _xmlDoc {
3351    // SAFETY: filename must be a valid C string.
3352    if filename.is_null() {
3353        return ptr::null_mut();
3354    }
3355    let ctxt = crate::xml::parser::helpers::create_parser_ctxt();
3356    if ctxt.is_null() {
3357        return ptr::null_mut();
3358    }
3359    if !sax.is_null() {
3360        (*ctxt).sax = sax;
3361        (*ctxt).userData = (*ctxt).sax as *mut c_void;
3362    }
3363    if recovery != 0 {
3364        (*ctxt).recovery = 1;
3365        (*ctxt).options |= 1;
3366    }
3367    let input = match crate::abi::exports_parser::open_filename_routed(filename, ctxt) {
3368        crate::abi::exports_parser::RoutedFileOpen::Loaded(input) => input,
3369        crate::abi::exports_parser::RoutedFileOpen::Failed => {
3370            crate::abi::exports_parser::emit_io_warning(
3371                ctxt,
3372                crate::abi::exports_parser::io_load_failure_message(filename),
3373            );
3374            crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3375            return ptr::null_mut();
3376        }
3377        crate::abi::exports_parser::RoutedFileOpen::EntityLoaderFailed => {
3378            crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3379            return ptr::null_mut();
3380        }
3381        crate::abi::exports_parser::RoutedFileOpen::Builtin => {
3382            match crate::xml::parser::helpers::input_from_file(filename) {
3383                Ok(input) => input,
3384                Err(_) => {
3385                    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3386                    return ptr::null_mut();
3387                }
3388            }
3389        }
3390    };
3391    crate::xml::parser::helpers::setup_parser_input(ctxt, input);
3392    crate::xml::parser::helpers::parse_document(ctxt);
3393    let doc = (*ctxt).myDoc;
3394    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3395    doc
3396}
3397
3398/// Parse an XML document from memory (SAX1).
3399///
3400/// # UPSTREAM-PARITY
3401///
3402/// ```c
3403/// xmlDocPtr xmlSAXParseMemory(xmlSAXHandlerPtr sax,
3404///                             const char *buffer, int size, int recovery);
3405/// ```
3406#[no_mangle]
3407pub unsafe extern "C" fn xmlSAXParseMemory(
3408    sax: *mut _xmlSAXHandler,
3409    buffer: *const c_char,
3410    size: c_int,
3411    recovery: c_int,
3412) -> *mut _xmlDoc {
3413    // SAFETY: buffer must be valid with at least `size` bytes.
3414    if buffer.is_null() || size <= 0 {
3415        return ptr::null_mut();
3416    }
3417    let ctxt = crate::xml::parser::helpers::create_parser_ctxt();
3418    if ctxt.is_null() {
3419        return ptr::null_mut();
3420    }
3421    if !sax.is_null() {
3422        (*ctxt).sax = sax;
3423        (*ctxt).userData = (*ctxt).sax as *mut c_void;
3424    }
3425    if recovery != 0 {
3426        (*ctxt).recovery = 1;
3427        (*ctxt).options |= 1;
3428    }
3429    // §16.5.2 zero-copy: xmlSAXParseMemory owns its context for this call —
3430    // the input borrows the caller's buffer.
3431    let input = crate::xml::parser::helpers::input_from_memory_borrowed(buffer, size);
3432    crate::xml::parser::helpers::setup_parser_input(ctxt, input);
3433    crate::xml::parser::helpers::parse_document(ctxt);
3434    let doc = (*ctxt).myDoc;
3435    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3436    doc
3437}
3438
3439/// SAX user parse file.
3440///
3441/// # UPSTREAM-PARITY
3442///
3443/// ```c
3444/// int xmlSAXUserParseFile(xmlSAXHandlerPtr sax, void *user_data,
3445///                         const char *filename);
3446/// ```
3447#[no_mangle]
3448pub unsafe extern "C" fn xmlSAXUserParseFile(
3449    sax: *mut _xmlSAXHandler,
3450    user_data: *mut c_void,
3451    filename: *const c_char,
3452) -> c_int {
3453    // SAFETY: filename must be a valid C string. sax and user_data may be NULL.
3454    if filename.is_null() {
3455        return -1;
3456    }
3457    let ctxt = crate::xml::parser::helpers::create_parser_ctxt();
3458    if ctxt.is_null() {
3459        return -1;
3460    }
3461    if !sax.is_null() {
3462        // UPSTREAM-PARITY (parser.c xmlSAXUserParseFile): same copy-into-
3463        //-own-storage contract as xmlSAXUserParseMemory (HOSTILE-CALLBACKS
3464        // C10 class).
3465        if unsafe { (*sax).initialized } == XML_SAX2_MAGIC as c_uint {
3466            unsafe {
3467                ptr::copy_nonoverlapping(sax as *const _xmlSAXHandler, (*ctxt).sax, 1);
3468            }
3469        } else {
3470            unsafe {
3471                ptr::copy_nonoverlapping(
3472                    sax as *const u8,
3473                    (*ctxt).sax as *mut u8,
3474                    size_of::<crate::abi::structs::_xmlSAXHandlerV1>(),
3475                );
3476            }
3477        }
3478    }
3479    (*ctxt).userData = if !user_data.is_null() {
3480        user_data
3481    } else {
3482        ctxt as *mut c_void
3483    };
3484    let input = match crate::abi::exports_parser::open_filename_routed(filename, ctxt) {
3485        crate::abi::exports_parser::RoutedFileOpen::Loaded(input) => input,
3486        crate::abi::exports_parser::RoutedFileOpen::Failed => {
3487            crate::abi::exports_parser::emit_io_warning(
3488                ctxt,
3489                crate::abi::exports_parser::io_load_failure_message(filename),
3490            );
3491            crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3492            return -1;
3493        }
3494        crate::abi::exports_parser::RoutedFileOpen::EntityLoaderFailed => {
3495            crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3496            return -1;
3497        }
3498        crate::abi::exports_parser::RoutedFileOpen::Builtin => {
3499            match crate::xml::parser::helpers::input_from_file(filename) {
3500                Ok(input) => input,
3501                Err(_) => {
3502                    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3503                    return -1;
3504                }
3505            }
3506        }
3507    };
3508    crate::xml::parser::helpers::setup_parser_input(ctxt, input);
3509    let ret = crate::xml::parser::helpers::parse_document(ctxt);
3510    // UPSTREAM-PARITY (parser.c xmlSAXUserParseFile): 0 when well-formed,
3511    // otherwise the recorded errNo (or -1).
3512    let ret = if ret == 0 {
3513        0
3514    } else {
3515        let err = unsafe { (*ctxt).errNo };
3516        if err != crate::abi::types::XML_ERR_OK {
3517            err
3518        } else {
3519            -1
3520        }
3521    };
3522    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3523    ret
3524}
3525
3526/// SAX user parse memory.
3527///
3528/// # UPSTREAM-PARITY
3529///
3530/// ```c
3531/// int xmlSAXUserParseMemory(xmlSAXHandlerPtr sax, void *user_data,
3532///                           const char *buffer, int size);
3533/// ```
3534#[no_mangle]
3535pub unsafe extern "C" fn xmlSAXUserParseMemory(
3536    sax: *mut _xmlSAXHandler,
3537    user_data: *mut c_void,
3538    buffer: *const c_char,
3539    size: c_int,
3540) -> c_int {
3541    // SAFETY: buffer must be valid with at least `size` bytes.
3542    if buffer.is_null() || size <= 0 {
3543        return -1;
3544    }
3545    let ctxt = crate::xml::parser::helpers::create_parser_ctxt();
3546    if ctxt.is_null() {
3547        return -1;
3548    }
3549    if !sax.is_null() {
3550        // UPSTREAM-PARITY (parser.c xmlSAXUserParseMemory): the caller's
3551        // handler is COPIED into the context's own SAX struct — never
3552        // borrowed — so xmlFreeParserCtxt frees only the copy. SAX2-magic
3553        // handlers are copied in full; legacy handlers expose only the V1
3554        // prefix (HOSTILE-CALLBACKS C10: borrowing the caller's struct made
3555        // xmlFreeParserCtxt free a stack object).
3556        if unsafe { (*sax).initialized } == XML_SAX2_MAGIC as c_uint {
3557            unsafe {
3558                ptr::copy_nonoverlapping(sax as *const _xmlSAXHandler, (*ctxt).sax, 1);
3559            }
3560        } else {
3561            unsafe {
3562                ptr::copy_nonoverlapping(
3563                    sax as *const u8,
3564                    (*ctxt).sax as *mut u8,
3565                    size_of::<crate::abi::structs::_xmlSAXHandlerV1>(),
3566                );
3567            }
3568        }
3569    }
3570    (*ctxt).userData = if !user_data.is_null() {
3571        user_data
3572    } else {
3573        ctxt as *mut c_void
3574    };
3575    let input = crate::xml::parser::helpers::input_from_memory(buffer, size);
3576    crate::xml::parser::helpers::setup_parser_input(ctxt, input);
3577    let ret = crate::xml::parser::helpers::parse_document(ctxt);
3578    // UPSTREAM-PARITY (parser.c xmlSAXUserParseMemory): the return value is
3579    // 0 when well-formed, otherwise the recorded errNo (or -1).
3580    let ret = if ret == 0 {
3581        0
3582    } else {
3583        let err = unsafe { (*ctxt).errNo };
3584        if err != crate::abi::types::XML_ERR_OK {
3585            err
3586        } else {
3587            -1
3588        }
3589    };
3590    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3591    ret
3592}
3593
3594/// Parse an XML document from a string (DOM).
3595///
3596/// # UPSTREAM-PARITY
3597///
3598/// ```c
3599/// xmlDocPtr xmlParseDoc(const xmlChar *cur);
3600/// ```
3601#[no_mangle]
3602pub unsafe extern "C" fn xmlParseDoc(cur: *const xmlChar) -> *mut _xmlDoc {
3603    // SAFETY: cur must be a valid null-terminated xmlChar string.
3604    if cur.is_null() {
3605        return ptr::null_mut();
3606    }
3607    xmlReadDoc(cur, ptr::null(), ptr::null(), 0)
3608}
3609
3610/// Parse an XML file (DOM).
3611///
3612/// # UPSTREAM-PARITY
3613///
3614/// ```c
3615/// xmlDocPtr xmlParseFile(const char *filename);
3616/// ```
3617#[no_mangle]
3618pub unsafe extern "C" fn xmlParseFile(filename: *const c_char) -> *mut _xmlDoc {
3619    // SAFETY: filename must be a valid C string.
3620    if filename.is_null() {
3621        return ptr::null_mut();
3622    }
3623    xmlReadFile(filename, ptr::null(), 0)
3624}
3625
3626/// Parse an XML document from memory (DOM).
3627///
3628/// # UPSTREAM-PARITY
3629///
3630/// ```c
3631/// xmlDocPtr xmlParseMemory(const char *buffer, int size);
3632/// ```
3633#[no_mangle]
3634pub unsafe extern "C" fn xmlParseMemory(buffer: *const c_char, size: c_int) -> *mut _xmlDoc {
3635    // SAFETY: buffer must be valid with at least `size` bytes.
3636    if buffer.is_null() || size <= 0 {
3637        return ptr::null_mut();
3638    }
3639    xmlReadMemory(buffer, size, ptr::null(), ptr::null(), 0)
3640}
3641
3642/// Create a file parser context.
3643///
3644/// # UPSTREAM-PARITY
3645///
3646/// ```c
3647/// xmlParserCtxtPtr xmlCreateFileParserCtxt(const char *filename);
3648/// ```
3649#[no_mangle]
3650pub unsafe extern "C" fn xmlCreateFileParserCtxt(filename: *const c_char) -> *mut _xmlParserCtxt {
3651    // SAFETY: filename must be a valid C string.
3652    if filename.is_null() {
3653        return ptr::null_mut();
3654    }
3655    let ctxt = crate::xml::parser::helpers::create_parser_ctxt();
3656    if ctxt.is_null() {
3657        return ptr::null_mut();
3658    }
3659    let input = match crate::abi::exports_parser::open_filename_routed(filename, ctxt) {
3660        crate::abi::exports_parser::RoutedFileOpen::Loaded(input) => input,
3661        crate::abi::exports_parser::RoutedFileOpen::Failed => {
3662            // UPSTREAM-PARITY (parser.c xmlCreateFileParserCtxt ->
3663            // xmlNewInputFromFile): a failed open raises the xmlCtxtErrIO
3664            // warning through the context channel.
3665            crate::abi::exports_parser::emit_io_warning(
3666                ctxt,
3667                crate::abi::exports_parser::io_load_failure_message(filename),
3668            );
3669            crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3670            return ptr::null_mut();
3671        }
3672        crate::abi::exports_parser::RoutedFileOpen::EntityLoaderFailed => {
3673            crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3674            return ptr::null_mut();
3675        }
3676        crate::abi::exports_parser::RoutedFileOpen::Builtin => {
3677            match crate::xml::parser::helpers::input_from_file(filename) {
3678                Ok(input) => input,
3679                Err(_) => {
3680                    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3681                    return ptr::null_mut();
3682                }
3683            }
3684        }
3685    };
3686    crate::xml::parser::helpers::setup_parser_input(ctxt, input);
3687    ctxt
3688}
3689
3690/// Create a document parser context.
3691///
3692/// # UPSTREAM-PARITY
3693///
3694/// ```c
3695/// xmlParserCtxtPtr xmlCreateDocParserCtxt(const xmlChar *cur);
3696/// ```
3697#[no_mangle]
3698pub unsafe extern "C" fn xmlCreateDocParserCtxt(cur: *const xmlChar) -> *mut _xmlParserCtxt {
3699    // SAFETY: cur must be a valid null-terminated xmlChar string.
3700    if cur.is_null() {
3701        return ptr::null_mut();
3702    }
3703    let ctxt = crate::xml::parser::helpers::create_parser_ctxt();
3704    if ctxt.is_null() {
3705        return ptr::null_mut();
3706    }
3707    let len = crate::xml::string::xml_strlen(cur);
3708    let input = crate::xml::parser::helpers::input_from_memory(cur as *const c_char, len as c_int);
3709    crate::xml::parser::helpers::setup_parser_input(ctxt, input);
3710    ctxt
3711}
3712
3713/// Parse a document using an existing parser context.
3714///
3715/// # UPSTREAM-PARITY
3716///
3717/// ```c
3718/// int xmlParseDocument(xmlParserCtxtPtr ctxt);
3719/// ```
3720#[no_mangle]
3721pub unsafe extern "C" fn xmlParseDocument(ctxt: *mut _xmlParserCtxt) -> c_int {
3722    // SAFETY: ctxt must be a valid parser context.
3723    if ctxt.is_null() {
3724        return -1;
3725    }
3726    crate::xml::parser::helpers::parse_document(ctxt)
3727}
3728
3729/// Free a parser context.
3730///
3731/// # UPSTREAM-PARITY
3732///
3733/// ```c
3734/// void xmlFreeParserCtxt(xmlParserCtxtPtr ctxt);
3735/// ```
3736#[no_mangle]
3737pub unsafe extern "C" fn xmlFreeParserCtxt(ctxt: *mut _xmlParserCtxt) {
3738    if ctxt.is_null() {
3739        return;
3740    }
3741    crate::xml::parser::helpers::free_parser_ctxt(ctxt);
3742}
3743
3744/// Set parser options.
3745///
3746/// # UPSTREAM-PARITY
3747///
3748/// ```c
3749/// int xmlCtxtUseOptions(xmlParserCtxtPtr ctxt, int options);
3750/// ```
3751#[no_mangle]
3752pub unsafe extern "C" fn xmlCtxtUseOptions(ctxt: *mut _xmlParserCtxt, options: c_int) -> c_int {
3753    if ctxt.is_null() {
3754        return -1;
3755    }
3756    // UPSTREAM-PARITY (parser.c 2.15 xmlCtxtUseOptions ->
3757    // xmlCtxtSetOptionsInternal): options in the keep mask can only ever be
3758    // enabled (historic never-clear bits); the remaining handled bits are
3759    // taken from the caller's `options`. The historical struct members
3760    // (recovery / replaceEntities / loadsubset / validate / pedantic /
3761    // keepBlanks / dictNames) are derived from the option bits exactly like
3762    // upstream, because deprecated APIs and consumers (e.g. PHP's expat
3763    // compat layer, which sanitizes then calls xmlCtxtUseOptions with
3764    // XML_PARSE_OLDSAX | XML_PARSE_NOENT) read those members directly.
3765    const KEEP_MASK: c_int = XML_PARSE_NOERROR
3766        | XML_PARSE_NOWARNING
3767        | XML_PARSE_NONET
3768        | XML_PARSE_NSCLEAN
3769        | XML_PARSE_NOCDATA
3770        | XML_PARSE_COMPACT
3771        | XML_PARSE_OLD10
3772        | XML_PARSE_HUGE
3773        | XML_PARSE_OLDSAX
3774        | XML_PARSE_IGNORE_ENC
3775        | XML_PARSE_BIG_LINES;
3776    const ALL_MASK: c_int = XML_PARSE_RECOVER
3777        | XML_PARSE_NOENT
3778        | XML_PARSE_DTDLOAD
3779        | XML_PARSE_DTDATTR
3780        | XML_PARSE_DTDVALID
3781        | XML_PARSE_NOERROR
3782        | XML_PARSE_NOWARNING
3783        | XML_PARSE_PEDANTIC
3784        | XML_PARSE_NOBLANKS
3785        | XML_PARSE_SAX1
3786        | XML_PARSE_NONET
3787        | XML_PARSE_NODICT
3788        | XML_PARSE_NSCLEAN
3789        | XML_PARSE_NOCDATA
3790        | XML_PARSE_COMPACT
3791        | XML_PARSE_OLD10
3792        | XML_PARSE_HUGE
3793        | XML_PARSE_OLDSAX
3794        | XML_PARSE_IGNORE_ENC
3795        | XML_PARSE_BIG_LINES
3796        | XML_PARSE_NO_XXE;
3797    unsafe {
3798        let merged = ((*ctxt).options & KEEP_MASK) | (options & ALL_MASK);
3799        crate::abi::exports_parser::apply_options(ctxt, merged);
3800    }
3801    options & !ALL_MASK
3802}
3803
3804/// Parse a well-balanced chunk (for push parsing).
3805///
3806/// # UPSTREAM-PARITY
3807///
3808/// ```c
3809/// xmlParserErrors xmlParseChunk(xmlParserCtxtPtr ctxt,
3810///                               const char *chunk, int size, int terminate);
3811/// ```
3812#[no_mangle]
3813pub unsafe extern "C" fn xmlParseChunk(
3814    ctxt: *mut _xmlParserCtxt,
3815    chunk: *const c_char,
3816    size: c_int,
3817    terminate: c_int,
3818) -> c_int {
3819    // SAFETY: ctxt must be a valid parser context.
3820    // chunk may be NULL if terminate is set (finalize without data).
3821    //
3822    // UPSTREAM-PARITY (parser.c 2.15 xmlParseChunk): NULL context, negative
3823    // sizes and NULL chunk with positive size all return XML_ERR_ARGUMENT
3824    // (115), never -1 — HOSTILE-ABI B10/B13/D2.
3825    if ctxt.is_null() || size < 0 || (chunk.is_null() && size > 0) {
3826        return XML_ERR_ARGUMENT;
3827    }
3828    crate::xml::parser::helpers::parse_chunk(ctxt, chunk, size, terminate)
3829}
3830
3831/// Create a memory parser input buffer.
3832///
3833/// # UPSTREAM-PARITY
3834///
3835/// ```c
3836/// xmlParserInputBufferPtr xmlParserInputBufferCreateMem(const char *buffer, int size, int enc);
3837/// ```
3838#[no_mangle]
3839pub unsafe extern "C" fn xmlParserInputBufferCreateMem(
3840    buffer: *const c_char,
3841    size: c_int,
3842    enc: c_int,
3843) -> *mut _xmlParserInputBuffer {
3844    // SAFETY: buffer must be valid with at least `size` bytes.
3845    if buffer.is_null() || size <= 0 {
3846        return ptr::null_mut();
3847    }
3848    // UPSTREAM-PARITY (xmlIO.c xmlParserInputBufferCreateMem): the content
3849    // is copied into the buffer (readcallback stays NULL — PHP's
3850    // XMLReader::XML()/fromString() parse from the buffer's own bytes).
3851    // The enc argument selects an input converter upstream; the candidate
3852    // parser handles declared encodings itself, so it is not stored.
3853    let _ = enc;
3854    crate::xml::parser::helpers::alloc_parser_input_buffer_with_mem(buffer, size)
3855}
3856
3857/// Create a file parser input buffer.
3858///
3859/// # UPSTREAM-PARITY
3860///
3861/// ```c
3862/// xmlParserInputBufferPtr xmlParserInputBufferCreateFilename(const char *URI, int enc);
3863/// ```
3864#[no_mangle]
3865pub unsafe extern "C" fn xmlParserInputBufferCreateFilename(
3866    URI: *const c_char,
3867    enc: c_int,
3868) -> *mut _xmlParserInputBuffer {
3869    // SAFETY: URI must be a valid C string or NULL.
3870    if URI.is_null() {
3871        return ptr::null_mut();
3872    }
3873    crate::xml::parser::helpers::alloc_parser_input_buffer()
3874}
3875
3876/// Create an I/O parser input buffer.
3877///
3878/// # UPSTREAM-PARITY
3879///
3880/// ```c
3881/// xmlParserInputBufferPtr xmlParserInputBufferCreateIO(
3882///     xmlInputReadCallback ioread, xmlInputCloseCallback ioclose,
3883///     void *ioctx, int enc);
3884/// ```
3885#[no_mangle]
3886pub unsafe extern "C" fn xmlParserInputBufferCreateIO(
3887    ioread: Option<xmlInputReadCallback>,
3888    ioclose: Option<xmlInputCloseCallback>,
3889    ioctx: *mut c_void,
3890    enc: c_int,
3891) -> *mut _xmlParserInputBuffer {
3892    // SAFETY: ioread must be a valid callback if Some. ioctx may be NULL.
3893    let buf = crate::xml::parser::helpers::alloc_parser_input_buffer();
3894    if !buf.is_null() {
3895        (*buf).readcallback = ioread;
3896        (*buf).closecallback = ioclose;
3897        (*buf).context = ioctx;
3898    }
3899    buf
3900}
3901
3902/// Free a parser input buffer.
3903///
3904/// # UPSTREAM-PARITY
3905///
3906/// ```c
3907/// void xmlFreeParserInputBuffer(xmlParserInputBufferPtr buf);
3908/// ```
3909#[no_mangle]
3910pub unsafe extern "C" fn xmlFreeParserInputBuffer(buf: *mut _xmlParserInputBuffer) {
3911    if buf.is_null() {
3912        return;
3913    }
3914    crate::xml::parser::helpers::free_parser_input_buffer(buf);
3915}
3916
3917/// Create a new parser input.
3918///
3919/// # UPSTREAM-PARITY
3920///
3921/// ```c
3922/// xmlParserInputPtr xmlNewInputFromFile(xmlParserCtxtPtr ctxt, const char *filename);
3923/// ```
3924#[no_mangle]
3925pub unsafe extern "C" fn xmlNewInputFromFile(
3926    ctxt: *mut _xmlParserCtxt,
3927    filename: *const c_char,
3928) -> *mut _xmlParserInput {
3929    // SAFETY: filename must be a valid C string. ctxt may be NULL.
3930    // This function allocates a _xmlParserInput. The caller owns it.
3931    // Note: The InputBuffer backing data is NOT leaked here (no ctxt._private
3932    // to store it). Use xmlCreateFileParserCtxt + xmlParseDocument instead.
3933    if filename.is_null() {
3934        return ptr::null_mut();
3935    }
3936    crate::xml::parser::helpers::alloc_parser_input_buffer() as *mut _xmlParserInput
3937}
3938
3939/// Free a parser input.
3940///
3941/// # UPSTREAM-PARITY
3942///
3943/// ```c
3944/// void xmlFreeInputStream(xmlParserInputPtr input);
3945/// ```
3946#[no_mangle]
3947pub unsafe extern "C" fn xmlFreeInputStream(input: *mut _xmlParserInput) {
3948    if input.is_null() {
3949        return;
3950    }
3951    crate::xml::parser::helpers::free_parser_input(input);
3952}
3953
3954// ═══════════════════════════════════════════════════════════════════════════════
3955// 8. I/O
3956// ═══════════════════════════════════════════════════════════════════════════════
3957
3958/// Create an output buffer for a file.
3959///
3960/// # UPSTREAM-PARITY
3961///
3962/// ```c
3963/// xmlOutputBufferPtr xmlOutputBufferCreateFilename(const char *URI,
3964///                                                  xmlCharEncodingHandlerPtr encoder,
3965///                                                  int compression);
3966/// ```
3967#[no_mangle]
3968pub unsafe extern "C" fn xmlOutputBufferCreateFilename(
3969    URI: *const c_char,
3970    encoder: *mut c_void,
3971    compression: c_int,
3972) -> *mut _xmlOutputBuffer {
3973    if URI.is_null() {
3974        return ptr::null_mut();
3975    }
3976    // UPSTREAM-PARITY (xmlIO.c xmlOutputBufferCreateFilename): a default
3977    // create-filename callback registered via xmlOutputBufferCreateFilenameDefault
3978    // is consulted first (PHP installs php_libxml_output_buffer_create_filename at
3979    // request init, so every filename open routes through the PHP streams layer);
3980    // otherwise the builtin file open runs.
3981    crate::xml::io::output_buffer_create_filename_routed(
3982        URI,
3983        encoder as *mut crate::abi::structs::_xmlCharEncodingHandler,
3984        compression,
3985    )
3986}
3987
3988/// Create an output buffer for a file descriptor.
3989///
3990/// # UPSTREAM-PARITY
3991///
3992/// ```c
3993/// xmlOutputBufferPtr xmlOutputBufferCreateFd(int fd,
3994///                                            xmlCharEncodingHandlerPtr encoder);
3995/// ```
3996#[no_mangle]
3997pub unsafe extern "C" fn xmlOutputBufferCreateFd(
3998    fd: c_int,
3999    encoder: *mut c_void,
4000) -> *mut _xmlOutputBuffer {
4001    if fd < 0 {
4002        return ptr::null_mut();
4003    }
4004    crate::xml::io::output_buffer_create_fd(
4005        fd,
4006        encoder as *mut crate::abi::structs::_xmlCharEncodingHandler,
4007    )
4008}
4009
4010/// Create an output buffer from I/O callbacks.
4011///
4012/// # UPSTREAM-PARITY
4013///
4014/// ```c
4015/// xmlOutputBufferPtr xmlOutputBufferCreateIO(
4016///     xmlOutputWriteCallback iowrite, xmlOutputCloseCallback ioclose,
4017///     void *ioctx, xmlCharEncodingHandlerPtr encoder);
4018/// ```
4019#[no_mangle]
4020pub unsafe extern "C" fn xmlOutputBufferCreateIO(
4021    iowrite: Option<xmlOutputWriteCallback>,
4022    ioclose: Option<xmlOutputCloseCallback>,
4023    ioctx: *mut c_void,
4024    encoder: *mut c_void,
4025) -> *mut _xmlOutputBuffer {
4026    crate::xml::io::output_buffer_create_io(
4027        iowrite,
4028        ioclose,
4029        ioctx,
4030        encoder as *mut crate::abi::structs::_xmlCharEncodingHandler,
4031    )
4032}
4033
4034/// Free an output buffer.
4035///
4036/// # UPSTREAM-PARITY
4037///
4038/// ```c
4039/// void xmlOutputBufferClose(xmlOutputBufferPtr out);
4040/// ```
4041#[no_mangle]
4042pub unsafe extern "C" fn xmlOutputBufferClose(out: *mut _xmlOutputBuffer) -> c_int {
4043    if out.is_null() {
4044        return -1;
4045    }
4046    crate::xml::io::output_buffer_close(out)
4047}
4048
4049/// Flush an output buffer.
4050///
4051/// # UPSTREAM-PARITY
4052///
4053/// ```c
4054/// int xmlOutputBufferFlush(xmlOutputBufferPtr out);
4055/// ```
4056#[no_mangle]
4057pub unsafe extern "C" fn xmlOutputBufferFlush(out: *mut _xmlOutputBuffer) -> c_int {
4058    if out.is_null() {
4059        return -1;
4060    }
4061    crate::xml::io::output_buffer_flush(out)
4062}
4063
4064/// Write to an output buffer.
4065///
4066/// # UPSTREAM-PARITY
4067///
4068/// ```c
4069/// int xmlOutputBufferWrite(xmlOutputBufferPtr out, int len, const char *data);
4070/// ```
4071#[no_mangle]
4072pub unsafe extern "C" fn xmlOutputBufferWrite(
4073    out: *mut _xmlOutputBuffer,
4074    len: c_int,
4075    data: *const c_char,
4076) -> c_int {
4077    // UPSTREAM-PARITY (xmlIO.c xmlOutputBufferWrite): a zero-length write
4078    // is a legal no-op returning 0 — PHP's W3C DOM-Parsing serializer
4079    // (ext/dom xml_serializer.c dom_xml_common_text_serialization) issues
4080    // xmlOutputBufferWrite(out, 0, p) when a text/attribute run starts with
4081    // a character that needs escaping; -1 there aborts the whole save with
4082    // "Could not save document".
4083    if out.is_null() || data.is_null() || len < 0 {
4084        return -1;
4085    }
4086    if len == 0 {
4087        return 0;
4088    }
4089    crate::xml::io::output_buffer_write(out, len, data)
4090}
4091
4092/// Write a string to an output buffer.
4093///
4094/// # UPSTREAM-PARITY
4095///
4096/// ```c
4097/// int xmlOutputBufferWriteString(xmlOutputBufferPtr out, const char *str);
4098/// ```
4099#[no_mangle]
4100pub unsafe extern "C" fn xmlOutputBufferWriteString(
4101    out: *mut _xmlOutputBuffer,
4102    str: *const c_char,
4103) -> c_int {
4104    if str.is_null() {
4105        return 0;
4106    }
4107    unsafe { xmlOutputBufferWrite(out, xmlStrlen(str as *const xmlChar), str) }
4108}
4109
4110/// Allocate an output buffer with no I/O target (upstream xmlAllocOutputBuffer).
4111///
4112/// # UPSTREAM-PARITY
4113///
4114/// ```c
4115/// xmlOutputBufferPtr xmlAllocOutputBuffer(xmlCharEncodingHandlerPtr encoder);
4116/// ```
4117#[no_mangle]
4118pub unsafe extern "C" fn xmlAllocOutputBuffer(encoder: *mut c_void) -> *mut _xmlOutputBuffer {
4119    crate::xml::io::output_buffer_create(
4120        encoder as *mut crate::abi::structs::_xmlCharEncodingHandler,
4121    )
4122}
4123
4124/// Create an output buffer that writes into a `_xmlBuffer` (upstream
4125/// xmlOutputBufferCreateBuffer).
4126///
4127/// # UPSTREAM-PARITY
4128///
4129/// ```c
4130/// xmlOutputBufferPtr xmlOutputBufferCreateBuffer(xmlBufferPtr buffer,
4131///                                                xmlCharEncodingHandlerPtr encoder);
4132/// ```
4133///
4134/// # SAFETY
4135///
4136/// - `buffer` must be a valid `_xmlBuffer`.
4137#[no_mangle]
4138pub unsafe extern "C" fn xmlOutputBufferCreateBuffer(
4139    buffer: *mut crate::abi::structs::_xmlBuffer,
4140    encoder: *mut c_void,
4141) -> *mut _xmlOutputBuffer {
4142    crate::xml::io::output_buffer_create_buffer(
4143        buffer,
4144        encoder as *mut crate::abi::structs::_xmlCharEncodingHandler,
4145    )
4146}
4147
4148/// Create an output buffer writing to a `FILE *` (upstream
4149/// xmlOutputBufferCreateFile): the FILE is the I/O context with a
4150/// write callback wrapping `fwrite` and a close callback wrapping `fflush`.
4151///
4152/// # SAFETY
4153///
4154/// - `file` must be a valid `FILE *` or NULL.
4155#[no_mangle]
4156pub unsafe extern "C" fn xmlOutputBufferCreateFile(
4157    file: *mut libc::FILE,
4158    encoder: *mut c_void,
4159) -> *mut _xmlOutputBuffer {
4160    crate::xml::io::output_buffer_create_file(
4161        file,
4162        encoder as *mut crate::abi::structs::_xmlCharEncodingHandler,
4163    )
4164}
4165
4166/// Get the current content of an output buffer (upstream xmlOutputBufferGetContent).
4167///
4168/// # SAFETY
4169///
4170/// - `out` must be a valid output buffer.
4171#[no_mangle]
4172pub unsafe extern "C" fn xmlOutputBufferGetContent(out: *mut _xmlOutputBuffer) -> *const c_char {
4173    crate::xml::io::output_buffer_get_content(out) as *const c_char
4174}
4175
4176/// Get the number of bytes currently in the output buffer (upstream
4177/// xmlOutputBufferGetSize: `size_t`, 0 on NULL/error — 11.1-Z.3 signature
4178/// court: the pre-Z.3 candidate returned `int` with -1 on error).
4179///
4180/// # SAFETY
4181///
4182/// - `out` must be a valid output buffer.
4183#[no_mangle]
4184pub unsafe extern "C" fn xmlOutputBufferGetSize(out: *mut _xmlOutputBuffer) -> usize {
4185    crate::xml::io::output_buffer_get_size(out)
4186}
4187
4188/// Write to an output buffer, escaping special characters with the given
4189/// escape function (upstream xmlOutputBufferWriteEscape).
4190///
4191/// # SAFETY
4192///
4193/// - `out` must be a valid output buffer; `str` a NUL-terminated string;
4194///   `escaping` a valid escape callback or NULL.
4195#[no_mangle]
4196pub unsafe extern "C" fn xmlOutputBufferWriteEscape(
4197    out: *mut _xmlOutputBuffer,
4198    str: *const xmlChar,
4199    escaping: Option<xmlCharEncodingOutputFunc>,
4200) -> c_int {
4201    if out.is_null() || str.is_null() {
4202        return -1;
4203    }
4204    crate::xml::io::output_buffer_write_escape(out, str, escaping)
4205}
4206
4207/// Set/query the default output-buffer filename callback
4208/// (upstream xmlOutputBufferCreateFilenameDefault).
4209///
4210/// Stored per-thread in the same cell the I/O layer consults
4211/// (`xml::globals::OUTPUT_CREATE_FILENAME`, upstream's
4212/// `xmlOutputBufferCreateFilenameValue`); the deprecated plain-global
4213/// accessor `__xmlOutputBufferCreateFilename` returns a pointer to it.
4214///
4215/// # SAFETY
4216///
4217/// - `func` must be a valid function pointer or NULL.
4218/// - The previous value (NULL when none was registered) is returned.
4219#[no_mangle]
4220pub unsafe extern "C" fn xmlOutputBufferCreateFilenameDefault(
4221    func: Option<
4222        unsafe extern "C" fn(
4223            *const c_char,
4224            *mut crate::abi::structs::_xmlCharEncodingHandler,
4225            c_int,
4226        ) -> *mut _xmlOutputBuffer,
4227    >,
4228) -> Option<
4229    unsafe extern "C" fn(
4230        *const c_char,
4231        *mut crate::abi::structs::_xmlCharEncodingHandler,
4232        c_int,
4233    ) -> *mut _xmlOutputBuffer,
4234> {
4235    // UPSTREAM-PARITY (xmlIO.c): set only when func is non-NULL, return the
4236    // previously registered value (NULL when none). Same per-thread slot the
4237    // thrDef variant and the I/O routing consult.
4238    let old = crate::xml::globals::get_output_buffer_create_filename_value();
4239    if func.is_some() {
4240        crate::xml::globals::set_output_buffer_create_filename_value(func);
4241    }
4242    old
4243}
4244
4245/// `__xmlOutputBufferCreateFilename` — accessor returning a pointer to the
4246/// default callback (upstream xmlIO.c).
4247#[no_mangle]
4248pub unsafe extern "C" fn __xmlOutputBufferCreateFilename() -> *mut Option<
4249    unsafe extern "C" fn(
4250        *const c_char,
4251        *mut crate::abi::structs::_xmlCharEncodingHandler,
4252        c_int,
4253    ) -> *mut _xmlOutputBuffer,
4254> {
4255    crate::xml::globals::output_create_filename_ptr()
4256}
4257
4258// ═══════════════════════════════════════════════════════════════════════════════
4259// 9. Dictionary
4260// ═══════════════════════════════════════════════════════════════════════════════
4261
4262/// Create a new dictionary.
4263///
4264/// # UPSTREAM-PARITY
4265///
4266/// ```c
4267/// xmlDictPtr xmlDictCreate(void);
4268/// ```
4269#[no_mangle]
4270pub extern "C" fn xmlDictCreate() -> *mut c_void {
4271    // The creator holds the base reference (Dict.ref_count = 1, upstream
4272    // ref_counter); xmlDictReference adds to it and xmlDictFree decrements,
4273    // freeing the dictionary when it reaches zero. The count lives in the
4274    // shared dict memory (R-000177: cross-DSO coherent).
4275    crate::xml::dictionary::dict_create() as *mut c_void
4276}
4277
4278/// Create a sub-dictionary.
4279///
4280/// # UPSTREAM-PARITY
4281///
4282/// ```c
4283/// xmlDictPtr xmlDictCreateSub(xmlDictPtr sub);
4284/// ```
4285#[no_mangle]
4286pub extern "C" fn xmlDictCreateSub(sub: *mut c_void) -> *mut c_void {
4287    // Base reference lives in the shared dict memory (Dict.ref_count).
4288    unsafe {
4289        crate::xml::dictionary::dict_create_sub(sub as *mut crate::xml::dictionary::Dict)
4290            as *mut c_void
4291    }
4292}
4293
4294/// Look up a string in the dictionary.
4295///
4296/// # UPSTREAM-PARITY
4297///
4298/// ```c
4299/// const xmlChar *xmlDictLookup(xmlDictPtr dict, const xmlChar *name, int len);
4300/// ```
4301///
4302/// Returns an interned string pointer (valid as long as the dictionary exists).
4303/// - If `len` < 0, `name` must be null-terminated.
4304/// - If `len` >= 0, exactly `len` bytes are used.
4305#[no_mangle]
4306pub unsafe extern "C" fn xmlDictLookup(
4307    dict: *mut c_void,
4308    name: *const xmlChar,
4309    len: c_int,
4310) -> *const xmlChar {
4311    unsafe {
4312        crate::xml::dictionary::dict_lookup(dict as *mut crate::xml::dictionary::Dict, name, len)
4313    }
4314}
4315
4316/// Check if a string exists in the dictionary.
4317///
4318/// # UPSTREAM-PARITY
4319///
4320/// ```c
4321/// const xmlChar *xmlDictExists(xmlDictPtr dict, const xmlChar *name, int len);
4322/// ```
4323#[no_mangle]
4324pub unsafe extern "C" fn xmlDictExists(
4325    dict: *mut c_void,
4326    name: *const xmlChar,
4327    len: c_int,
4328) -> *const xmlChar {
4329    unsafe {
4330        crate::xml::dictionary::dict_exists(dict as *mut crate::xml::dictionary::Dict, name, len)
4331    }
4332}
4333
4334/// Query dictionary size.
4335///
4336/// # UPSTREAM-PARITY
4337///
4338/// ```c
4339/// int xmlDictSize(const xmlDictPtr dict);
4340/// ```
4341#[no_mangle]
4342pub const extern "C" fn xmlDictSize(dict: *const c_void) -> c_int {
4343    {
4344        crate::xml::dictionary::dict_size(dict as *const crate::xml::dictionary::Dict)
4345    }
4346}
4347
4348/// Free a dictionary.
4349///
4350/// # UPSTREAM-PARITY
4351///
4352/// ```c
4353/// void xmlDictFree(xmlDictPtr dict);
4354/// ```
4355///
4356/// The reference counter added by `xmlDictReference` is honored: the
4357/// underlying dictionary is destroyed only when the last reference is
4358/// released (the base owner counts as one implicit reference).
4359#[no_mangle]
4360pub extern "C" fn xmlDictFree(dict: *mut c_void) {
4361    if dict.is_null() {
4362        return;
4363    }
4364    // The reference count lives IN the shared dict memory (R-000177): a
4365    // decrement through one DSO observes references made through every
4366    // other DSO, so a dict created by libxml2.so.16 is never freed early by
4367    // libxslt.so.1's teardown. The pre-fix per-DSO side table partitioned
4368    // the count and unconditionally freed unknown dicts.
4369    let d = dict as *mut crate::xml::dictionary::Dict;
4370    let prev = unsafe {
4371        (*d).ref_count
4372            .fetch_sub(1, core::sync::atomic::Ordering::Relaxed)
4373    };
4374    if prev == 1 {
4375        unsafe { crate::xml::dictionary::dict_free(dict as *mut crate::xml::dictionary::Dict) };
4376    }
4377}
4378
4379/// Set the dictionary size limit.
4380///
4381/// # UPSTREAM-PARITY
4382///
4383/// ```c
4384/// size_t xmlDictSetLimit(xmlDictPtr dict, size_t limit);
4385/// ```
4386#[no_mangle]
4387pub extern "C" fn xmlDictSetLimit(dict: *mut c_void, limit: usize) -> usize {
4388    {
4389        crate::xml::dictionary::dict_set_limit(dict as *mut crate::xml::dictionary::Dict, limit)
4390    }
4391}
4392
4393/// Get current dictionary usage.
4394///
4395/// # UPSTREAM-PARITY
4396///
4397/// ```c
4398/// size_t xmlDictGetUsage(const xmlDictPtr dict);
4399/// ```
4400#[no_mangle]
4401pub extern "C" fn xmlDictGetUsage(dict: *const c_void) -> usize {
4402    {
4403        crate::xml::dictionary::dict_get_usage(dict as *mut crate::xml::dictionary::Dict)
4404    }
4405}
4406
4407// ═══════════════════════════════════════════════════════════════════════════════
4408// 10. Hash Table
4409// ═══════════════════════════════════════════════════════════════════════════════
4410
4411/// Create a new hash table.
4412///
4413/// # UPSTREAM-PARITY
4414///
4415/// ```c
4416/// xmlHashTablePtr xmlHashCreate(int size);
4417/// ```
4418#[no_mangle]
4419pub extern "C" fn xmlHashCreate(size: c_int) -> *mut c_void {
4420    crate::xml::hash::hash_create(size) as *mut c_void
4421}
4422
4423/// Create a new hash table with a dictionary.
4424///
4425/// # UPSTREAM-PARITY
4426///
4427/// ```c
4428/// xmlHashTablePtr xmlHashCreateDict(int size, xmlDictPtr dict);
4429/// ```
4430#[no_mangle]
4431pub extern "C" fn xmlHashCreateDict(size: c_int, dict: *mut c_void) -> *mut c_void {
4432    crate::xml::hash::hash_create_dict(size, dict) as *mut c_void
4433}
4434
4435/// Free a hash table.
4436///
4437/// # UPSTREAM-PARITY
4438///
4439/// ```c
4440/// void xmlHashFree(xmlHashTablePtr table, xmlHashDeallocator f);
4441/// ```
4442#[no_mangle]
4443pub extern "C" fn xmlHashFree(
4444    table: *mut c_void,
4445    f: Option<unsafe extern "C" fn(*mut c_void, *mut xmlChar)>,
4446) {
4447    unsafe { crate::xml::hash::hash_free(table as *mut crate::xml::hash::HashTable, f) }
4448}
4449
4450/// Add an entry to a hash table.
4451///
4452/// # UPSTREAM-PARITY
4453///
4454/// ```c
4455/// int xmlHashAddEntry(xmlHashTablePtr table, const xmlChar *name, void *userdata);
4456/// ```
4457#[no_mangle]
4458pub unsafe extern "C" fn xmlHashAddEntry(
4459    table: *mut c_void,
4460    name: *const xmlChar,
4461    userdata: *mut c_void,
4462) -> c_int {
4463    unsafe {
4464        crate::xml::hash::hash_add_entry(table as *mut crate::xml::hash::HashTable, name, userdata)
4465    }
4466}
4467
4468/// Add a 2-key entry.
4469///
4470/// # UPSTREAM-PARITY
4471///
4472/// ```c
4473/// int xmlHashAddEntry2(xmlHashTablePtr table, const xmlChar *name,
4474///                      const xmlChar *name2, void *userdata);
4475/// ```
4476#[no_mangle]
4477pub unsafe extern "C" fn xmlHashAddEntry2(
4478    table: *mut c_void,
4479    name: *const xmlChar,
4480    name2: *const xmlChar,
4481    userdata: *mut c_void,
4482) -> c_int {
4483    unsafe {
4484        crate::xml::hash::hash_add_entry2(
4485            table as *mut crate::xml::hash::HashTable,
4486            name,
4487            name2,
4488            userdata,
4489        )
4490    }
4491}
4492
4493/// Add a 3-key entry.
4494///
4495/// # UPSTREAM-PARITY
4496///
4497/// ```c
4498/// int xmlHashAddEntry3(xmlHashTablePtr table, const xmlChar *name,
4499///                      const xmlChar *name2, const xmlChar *name3, void *userdata);
4500/// ```
4501#[no_mangle]
4502pub unsafe extern "C" fn xmlHashAddEntry3(
4503    table: *mut c_void,
4504    name: *const xmlChar,
4505    name2: *const xmlChar,
4506    name3: *const xmlChar,
4507    userdata: *mut c_void,
4508) -> c_int {
4509    unsafe {
4510        crate::xml::hash::hash_add_entry3(
4511            table as *mut crate::xml::hash::HashTable,
4512            name,
4513            name2,
4514            name3,
4515            userdata,
4516        )
4517    }
4518}
4519
4520/// Update or add an entry.
4521///
4522/// # UPSTREAM-PARITY
4523///
4524/// ```c
4525/// int xmlHashUpdateEntry(xmlHashTablePtr table, const xmlChar *name,
4526///                        void *userdata, xmlHashDeallocator f);
4527/// ```
4528#[no_mangle]
4529pub unsafe extern "C" fn xmlHashUpdateEntry(
4530    table: *mut c_void,
4531    name: *const xmlChar,
4532    userdata: *mut c_void,
4533    f: Option<unsafe extern "C" fn(*mut c_void, *mut xmlChar)>,
4534) -> c_int {
4535    unsafe {
4536        crate::xml::hash::hash_update_entry(
4537            table as *mut crate::xml::hash::HashTable,
4538            name,
4539            userdata,
4540            f,
4541        )
4542    }
4543}
4544
4545/// Update or add a 2-key entry.
4546#[no_mangle]
4547pub unsafe extern "C" fn xmlHashUpdateEntry2(
4548    table: *mut c_void,
4549    name: *const xmlChar,
4550    name2: *const xmlChar,
4551    userdata: *mut c_void,
4552    f: Option<unsafe extern "C" fn(*mut c_void, *mut xmlChar)>,
4553) -> c_int {
4554    unsafe {
4555        crate::xml::hash::hash_update_entry2(
4556            table as *mut crate::xml::hash::HashTable,
4557            name,
4558            name2,
4559            userdata,
4560            f,
4561        )
4562    }
4563}
4564
4565/// Update or add a 3-key entry.
4566#[no_mangle]
4567pub unsafe extern "C" fn xmlHashUpdateEntry3(
4568    table: *mut c_void,
4569    name: *const xmlChar,
4570    name2: *const xmlChar,
4571    name3: *const xmlChar,
4572    userdata: *mut c_void,
4573    f: Option<unsafe extern "C" fn(*mut c_void, *mut xmlChar)>,
4574) -> c_int {
4575    unsafe {
4576        crate::xml::hash::hash_update_entry3(
4577            table as *mut crate::xml::hash::HashTable,
4578            name,
4579            name2,
4580            name3,
4581            userdata,
4582            f,
4583        )
4584    }
4585}
4586
4587/// Look up an entry.
4588///
4589/// # UPSTREAM-PARITY
4590///
4591/// ```c
4592/// void *xmlHashLookup(xmlHashTablePtr table, const xmlChar *name);
4593/// ```
4594#[no_mangle]
4595pub unsafe extern "C" fn xmlHashLookup(table: *mut c_void, name: *const xmlChar) -> *mut c_void {
4596    unsafe { crate::xml::hash::hash_lookup(table as *mut crate::xml::hash::HashTable, name) }
4597}
4598
4599/// Look up a 2-key entry.
4600#[no_mangle]
4601pub unsafe extern "C" fn xmlHashLookup2(
4602    table: *mut c_void,
4603    name: *const xmlChar,
4604    name2: *const xmlChar,
4605) -> *mut c_void {
4606    unsafe {
4607        crate::xml::hash::hash_lookup2(table as *mut crate::xml::hash::HashTable, name, name2)
4608    }
4609}
4610
4611/// Look up a 3-key entry.
4612#[no_mangle]
4613pub unsafe extern "C" fn xmlHashLookup3(
4614    table: *mut c_void,
4615    name: *const xmlChar,
4616    name2: *const xmlChar,
4617    name3: *const xmlChar,
4618) -> *mut c_void {
4619    unsafe {
4620        crate::xml::hash::hash_lookup3(
4621            table as *mut crate::xml::hash::HashTable,
4622            name,
4623            name2,
4624            name3,
4625        )
4626    }
4627}
4628
4629/// Get the size of a hash table.
4630///
4631/// # UPSTREAM-PARITY
4632///
4633/// ```c
4634/// int xmlHashSize(xmlHashTablePtr table);
4635/// ```
4636#[no_mangle]
4637pub extern "C" fn xmlHashSize(table: *mut c_void) -> c_int {
4638    crate::xml::hash::hash_size(table as *mut crate::xml::hash::HashTable)
4639}
4640
4641/// Remove an entry.
4642///
4643/// # UPSTREAM-PARITY
4644///
4645/// ```c
4646/// int xmlHashRemoveEntry(xmlHashTablePtr table, const xmlChar *name,
4647///                        xmlHashDeallocator f);
4648/// ```
4649#[no_mangle]
4650pub unsafe extern "C" fn xmlHashRemoveEntry(
4651    table: *mut c_void,
4652    name: *const xmlChar,
4653    f: Option<unsafe extern "C" fn(*mut c_void, *mut xmlChar)>,
4654) -> c_int {
4655    unsafe {
4656        crate::xml::hash::hash_remove_entry(table as *mut crate::xml::hash::HashTable, name, f)
4657    }
4658}
4659
4660/// Remove a 2-key entry.
4661#[no_mangle]
4662pub unsafe extern "C" fn xmlHashRemoveEntry2(
4663    table: *mut c_void,
4664    name: *const xmlChar,
4665    name2: *const xmlChar,
4666    f: Option<unsafe extern "C" fn(*mut c_void, *mut xmlChar)>,
4667) -> c_int {
4668    unsafe {
4669        crate::xml::hash::hash_remove_entry2(
4670            table as *mut crate::xml::hash::HashTable,
4671            name,
4672            name2,
4673            f,
4674        )
4675    }
4676}
4677
4678/// Remove a 3-key entry.
4679#[no_mangle]
4680pub unsafe extern "C" fn xmlHashRemoveEntry3(
4681    table: *mut c_void,
4682    name: *const xmlChar,
4683    name2: *const xmlChar,
4684    name3: *const xmlChar,
4685    f: Option<unsafe extern "C" fn(*mut c_void, *mut xmlChar)>,
4686) -> c_int {
4687    unsafe {
4688        crate::xml::hash::hash_remove_entry3(
4689            table as *mut crate::xml::hash::HashTable,
4690            name,
4691            name2,
4692            name3,
4693            f,
4694        )
4695    }
4696}
4697
4698/// Scan a hash table with a scanner function.
4699///
4700/// # UPSTREAM-PARITY
4701///
4702/// ```c
4703/// void xmlHashScan(xmlHashTablePtr table, xmlHashScanner f, void *data);
4704/// ```
4705#[no_mangle]
4706pub extern "C" fn xmlHashScan(table: *mut c_void, f: Option<xmlHashScanner>, data: *mut c_void) {
4707    unsafe { crate::xml::hash::hash_scan(table as *mut crate::xml::hash::HashTable, f, data) }
4708}
4709
4710/// Scan a hash table with a full scanner function.
4711#[no_mangle]
4712pub extern "C" fn xmlHashScanFull(
4713    table: *mut c_void,
4714    f: Option<xmlHashScannerFull>,
4715    data: *mut c_void,
4716) {
4717    unsafe { crate::xml::hash::hash_scan_full(table as *mut crate::xml::hash::HashTable, f, data) }
4718}
4719
4720/// Copy a hash table.
4721///
4722/// # UPSTREAM-PARITY
4723///
4724/// ```c
4725/// xmlHashTablePtr xmlHashCopy(xmlHashTablePtr table, xmlHashCopier f);
4726/// ```
4727#[no_mangle]
4728pub extern "C" fn xmlHashCopy(
4729    table: *mut c_void,
4730    f: Option<unsafe extern "C" fn(*mut c_void, *const xmlChar) -> *mut c_void>,
4731) -> *mut c_void {
4732    unsafe {
4733        crate::xml::hash::hash_copy(table as *mut crate::xml::hash::HashTable, f) as *mut c_void
4734    }
4735}
4736
4737// ═══════════════════════════════════════════════════════════════════════════════
4738// 11. List
4739// ═══════════════════════════════════════════════════════════════════════════════
4740
4741/// Create a new list.
4742///
4743/// # UPSTREAM-PARITY
4744///
4745/// ```c
4746/// xmlListPtr xmlListCreate(xmlListDeallocator deallocator,
4747///                          xmlListDataCompare compare);
4748/// ```
4749#[no_mangle]
4750pub extern "C" fn xmlListCreate(
4751    deallocator: Option<unsafe extern "C" fn(*mut c_void)>,
4752    compare: Option<unsafe extern "C" fn(*const c_void, *const c_void) -> c_int>,
4753) -> *mut c_void {
4754    crate::xml::list::list_create(deallocator, compare) as *mut c_void
4755}
4756
4757/// Delete a list.
4758///
4759/// # UPSTREAM-PARITY
4760///
4761/// ```c
4762/// void xmlListDelete(xmlListPtr list);
4763/// ```
4764#[no_mangle]
4765pub extern "C" fn xmlListDelete(list: *mut c_void) {
4766    unsafe { crate::xml::list::list_delete(list as *mut crate::xml::list::List) }
4767}
4768
4769/// Search a list.
4770///
4771/// # UPSTREAM-PARITY
4772///
4773/// ```c
4774/// void *xmlListSearch(xmlListPtr list, void *data);
4775/// ```
4776#[no_mangle]
4777pub extern "C" fn xmlListSearch(list: *mut c_void, data: *mut c_void) -> *mut c_void {
4778    unsafe { crate::xml::list::list_search(list as *mut crate::xml::list::List, data) }
4779}
4780
4781/// Walk a list.
4782///
4783/// # UPSTREAM-PARITY
4784///
4785/// ```c
4786/// void xmlListWalk(xmlListPtr list, xmlListWalker walker, void *data);
4787/// ```
4788#[no_mangle]
4789pub extern "C" fn xmlListWalk(
4790    list: *mut c_void,
4791    walker: Option<unsafe extern "C" fn(*mut c_void, *mut c_void) -> c_int>,
4792    data: *mut c_void,
4793) {
4794    unsafe { crate::xml::list::list_walk(list as *mut crate::xml::list::List, walker, data) }
4795}
4796
4797/// Push to back.
4798///
4799/// # UPSTREAM-PARITY
4800///
4801/// ```c
4802/// int xmlListPushBack(xmlListPtr list, void *data);
4803/// ```
4804#[no_mangle]
4805pub extern "C" fn xmlListPushBack(list: *mut c_void, data: *mut c_void) -> c_int {
4806    unsafe { crate::xml::list::list_push_back(list as *mut crate::xml::list::List, data) }
4807}
4808
4809/// Push to front.
4810///
4811/// # UPSTREAM-PARITY
4812///
4813/// ```c
4814/// int xmlListPushFront(xmlListPtr list, void *data);
4815/// ```
4816#[no_mangle]
4817pub extern "C" fn xmlListPushFront(list: *mut c_void, data: *mut c_void) -> c_int {
4818    unsafe { crate::xml::list::list_push_front(list as *mut crate::xml::list::List, data) }
4819}
4820
4821/// Pop from back.
4822#[no_mangle]
4823pub extern "C" fn xmlListPopBack(list: *mut c_void) {
4824    unsafe { crate::xml::list::list_pop_back(list as *mut crate::xml::list::List) }
4825}
4826
4827/// Pop from front.
4828#[no_mangle]
4829pub extern "C" fn xmlListPopFront(list: *mut c_void) {
4830    unsafe { crate::xml::list::list_pop_front(list as *mut crate::xml::list::List) }
4831}
4832
4833/// Insert into sorted list.
4834///
4835/// # UPSTREAM-PARITY
4836///
4837/// ```c
4838/// int xmlListInsert(xmlListPtr list, void *data);
4839/// ```
4840#[no_mangle]
4841pub extern "C" fn xmlListInsert(list: *mut c_void, data: *mut c_void) -> c_int {
4842    unsafe { crate::xml::list::list_insert(list as *mut crate::xml::list::List, data) }
4843}
4844
4845/// Append to list.
4846#[no_mangle]
4847pub extern "C" fn xmlListAppend(list: *mut c_void, data: *mut c_void) -> c_int {
4848    unsafe { crate::xml::list::list_append(list as *mut crate::xml::list::List, data) }
4849}
4850
4851/// Remove first matching element.
4852#[no_mangle]
4853pub extern "C" fn xmlListRemoveFirst(list: *mut c_void, data: *mut c_void) -> c_int {
4854    unsafe { crate::xml::list::list_remove_first(list as *mut crate::xml::list::List, data) }
4855}
4856
4857/// Remove last matching element.
4858#[no_mangle]
4859pub extern "C" fn xmlListRemoveLast(list: *mut c_void, data: *mut c_void) -> c_int {
4860    unsafe { crate::xml::list::list_remove_last(list as *mut crate::xml::list::List, data) }
4861}
4862
4863/// Remove all matching elements.
4864#[no_mangle]
4865pub extern "C" fn xmlListRemoveAll(list: *mut c_void, data: *mut c_void) -> c_int {
4866    unsafe { crate::xml::list::list_remove_all(list as *mut crate::xml::list::List, data) }
4867}
4868
4869/// Clear a list.
4870#[no_mangle]
4871pub extern "C" fn xmlListClear(list: *mut c_void) {
4872    unsafe { crate::xml::list::list_clear(list as *mut crate::xml::list::List) }
4873}
4874
4875/// Check if list is empty.
4876///
4877/// # UPSTREAM-PARITY
4878///
4879/// ```c
4880/// int xmlListEmpty(xmlListPtr list);
4881/// ```
4882#[no_mangle]
4883pub extern "C" fn xmlListEmpty(list: *mut c_void) -> c_int {
4884    crate::xml::list::list_empty(list as *mut crate::xml::list::List)
4885}
4886
4887/// Get front element.
4888///
4889/// # UPSTREAM-PARITY
4890///
4891/// ```c
4892/// void *xmlListFront(xmlListPtr list);
4893/// ```
4894#[no_mangle]
4895pub extern "C" fn xmlListFront(list: *mut c_void) -> *mut c_void {
4896    crate::xml::list::list_front(list as *mut crate::xml::list::List)
4897}
4898
4899/// Get back element.
4900///
4901/// # UPSTREAM-PARITY
4902///
4903/// ```c
4904/// void *xmlListBack(xmlListPtr list);
4905/// ```
4906#[no_mangle]
4907pub extern "C" fn xmlListBack(list: *mut c_void) -> *mut c_void {
4908    crate::xml::list::list_back(list as *mut crate::xml::list::List)
4909}
4910
4911/// Get list size.
4912///
4913/// # UPSTREAM-PARITY
4914///
4915/// ```c
4916/// int xmlListSize(xmlListPtr list);
4917/// ```
4918#[no_mangle]
4919pub extern "C" fn xmlListSize(list: *mut c_void) -> c_int {
4920    crate::xml::list::list_size(list as *mut crate::xml::list::List)
4921}
4922
4923/// Sort a list.
4924#[no_mangle]
4925pub extern "C" fn xmlListSort(list: *mut c_void) {
4926    unsafe { crate::xml::list::list_sort(list as *mut crate::xml::list::List) }
4927}
4928
4929/// Reverse a list.
4930#[no_mangle]
4931pub extern "C" fn xmlListReverse(list: *mut c_void) {
4932    unsafe { crate::xml::list::list_reverse(list as *mut crate::xml::list::List) }
4933}
4934
4935/// Reverse a list in-place.
4936#[no_mangle]
4937pub extern "C" fn xmlListReverseSplice(list: *mut c_void, list2: *mut c_void) {
4938    unsafe {
4939        crate::xml::list::list_reverse_splice(
4940            list as *mut crate::xml::list::List,
4941            list2 as *mut crate::xml::list::List,
4942        )
4943    }
4944}
4945
4946/// Merge two sorted lists.
4947#[no_mangle]
4948pub extern "C" fn xmlListMerge(list: *mut c_void, list2: *mut c_void) {
4949    unsafe {
4950        crate::xml::list::list_merge(
4951            list as *mut crate::xml::list::List,
4952            list2 as *mut crate::xml::list::List,
4953        )
4954    }
4955}
4956/// Return the last element of a list (upstream list.h).
4957///
4958/// # UPSTREAM-PARITY
4959///
4960/// ```c
4961/// void *xmlListEnd(xmlListPtr l);
4962/// ```
4963#[no_mangle]
4964pub unsafe extern "C" fn xmlListEnd(l: *mut c_void) -> *mut c_void {
4965    crate::xml::list::list_end(l as *mut crate::xml::list::List)
4966}
4967
4968/// Reverse-search a list (upstream list.h).
4969///
4970/// # UPSTREAM-PARITY
4971///
4972/// ```c
4973/// void *xmlListReverseSearch(xmlListPtr l, void *data);
4974/// ```
4975#[no_mangle]
4976pub unsafe extern "C" fn xmlListReverseSearch(l: *mut c_void, data: *mut c_void) -> *mut c_void {
4977    crate::xml::list::list_reverse_search(l as *mut crate::xml::list::List, data)
4978}
4979
4980/// Walk a list in reverse (upstream list.h).
4981///
4982/// # UPSTREAM-PARITY
4983///
4984/// ```c
4985/// void xmlListReverseWalk(xmlListPtr l, xmlListWalker walker, void *data);
4986/// ```
4987#[no_mangle]
4988pub unsafe extern "C" fn xmlListReverseWalk(
4989    l: *mut c_void,
4990    walker: Option<unsafe extern "C" fn(*mut c_void, *mut c_void) -> c_int>,
4991    data: *mut c_void,
4992) {
4993    crate::xml::list::list_reverse_walk(l as *mut crate::xml::list::List, walker, data)
4994}
4995
4996/// Duplicate a list (upstream list.h).
4997///
4998/// # UPSTREAM-PARITY
4999///
5000/// ```c
5001/// xmlListPtr xmlListDup(xmlListPtr l);
5002/// ```
5003#[no_mangle]
5004pub unsafe extern "C" fn xmlListDup(l: *mut c_void) -> *mut c_void {
5005    crate::xml::list::list_dup(l as *mut crate::xml::list::List) as *mut c_void
5006}
5007
5008/// Copy the contents of `old` into the existing list `cur` (upstream
5009/// list.h — R-000176, the candidate previously passed a copier callback).
5010///
5011/// # UPSTREAM-PARITY
5012///
5013/// ```c
5014/// int xmlListCopy(xmlListPtr cur, const xmlListPtr old);
5015/// ```
5016///
5017/// Returns 0 on success, 1 on error (upstream list.c).
5018#[no_mangle]
5019pub unsafe extern "C" fn xmlListCopy(cur: *mut c_void, old: *mut c_void) -> c_int {
5020    crate::xml::list::list_copy(
5021        cur as *mut crate::xml::list::List,
5022        old as *mut crate::xml::list::List,
5023    )
5024}
5025
5026/// Return the data of a link (upstream list.h).
5027///
5028/// # UPSTREAM-PARITY
5029///
5030/// ```c
5031/// void *xmlLinkGetData(xmlLinkPtr lk);
5032/// ```
5033#[no_mangle]
5034pub unsafe extern "C" fn xmlLinkGetData(lk: *mut c_void) -> *mut c_void {
5035    crate::xml::list::link_get_data(lk)
5036}
5037
5038// ═══════════════════════════════════════════════════════════════════════════════
5039// 12. Buffer
5040// ═══════════════════════════════════════════════════════════════════════════════
5041
5042/// Create a new buffer.
5043///
5044/// # UPSTREAM-PARITY
5045///
5046/// ```c
5047/// xmlBufferPtr xmlBufferCreate(void);
5048/// ```
5049#[no_mangle]
5050pub extern "C" fn xmlBufferCreate() -> *mut _xmlBuffer {
5051    // UPSTREAM-PARITY (buf.c 2.15): xmlBufferCreate = size 256, alloc scheme
5052    // XML_BUFFER_ALLOC_IO, content[0] = 0. (The pre-Phase-13 path used the
5053    // internal default-size DOUBLEIT helper, which diverged from the oracle
5054    // for negative/huge CreateSize arguments — HOSTILE-ABI finding.)
5055    unsafe { xml_buffer_create_upstream(256) }
5056}
5057
5058/// Create a new buffer of a given size.
5059///
5060/// # UPSTREAM-PARITY
5061///
5062/// ```c
5063/// xmlBufferPtr xmlBufferCreateSize(size_t size);
5064/// ```
5065///
5066/// Upstream buf.c 2.15: `size >= INT_MAX` returns NULL; `size == 0` returns
5067/// a buffer with a NULL content; otherwise the content is `size + 1` bytes
5068/// (the extra byte is the NUL terminator). The alloc scheme is
5069/// XML_BUFFER_ALLOC_IO.
5070#[no_mangle]
5071pub extern "C" fn xmlBufferCreateSize(size: usize) -> *mut _xmlBuffer {
5072    // UPSTREAM-PARITY (buf.c 2.15 xmlBufferCreateSize): sizes at or beyond
5073    // INT_MAX are rejected up front — a C caller passing -1 (huge size_t)
5074    // gets NULL exactly like the oracle (HOSTILE-ABI finding).
5075    if size >= c_int::MAX as usize {
5076        return ptr::null_mut();
5077    }
5078    unsafe { xml_buffer_create_upstream(size) }
5079}
5080
5081/// Upstream `xmlBufferCreateSize` body (buf.c 2.15): allocate the struct,
5082/// then `size + 1` content bytes when size != 0 (zero-size buffers have a
5083/// NULL content), alloc scheme XML_BUFFER_ALLOC_IO, content[0] = 0.
5084///
5085/// # Safety
5086///
5087/// - No caller-provided pointers; every allocation is checked for NULL and
5088///   the struct is freed on content-allocation failure.
5089unsafe fn xml_buffer_create_upstream(size: usize) -> *mut _xmlBuffer {
5090    let ret = unsafe { xmlMallocImpl(size_of::<_xmlBuffer>()) as *mut _xmlBuffer };
5091    if ret.is_null() {
5092        return ptr::null_mut();
5093    }
5094    let sz = if size != 0 { size + 1 } else { 0 };
5095    unsafe {
5096        if sz != 0 {
5097            let content = xmlMallocImpl(sz) as *mut xmlChar;
5098            if content.is_null() {
5099                xmlFreeImpl(ret as *mut c_void);
5100                return ptr::null_mut();
5101            }
5102            *content = 0;
5103            (*ret).content = content;
5104            (*ret).contentIO = content;
5105        } else {
5106            (*ret).content = ptr::null_mut();
5107            (*ret).contentIO = ptr::null_mut();
5108        }
5109        (*ret).use_ = 0;
5110        (*ret).size = sz as c_uint;
5111        (*ret).alloc = crate::abi::types::xmlBufferAllocationScheme::XML_BUFFER_ALLOC_IO as c_int;
5112    }
5113    ret
5114}
5115
5116/// Create a buffer from a static string.
5117///
5118/// # UPSTREAM-PARITY
5119///
5120/// ```c
5121/// xmlBufferPtr xmlBufferCreateStatic(void *mem, size_t size);
5122/// ```
5123#[no_mangle]
5124pub extern "C" fn xmlBufferCreateStatic(mem: *mut c_void, size: usize) -> *mut _xmlBuffer {
5125    if mem.is_null() || size == 0 {
5126        return ptr::null_mut();
5127    }
5128    crate::xml::io::buf_create_static(mem as *const xmlChar, size as c_int)
5129}
5130
5131/// Free a buffer.
5132///
5133/// # UPSTREAM-PARITY
5134///
5135/// ```c
5136/// void xmlBufferFree(xmlBufferPtr buf);
5137/// ```
5138#[no_mangle]
5139pub extern "C" fn xmlBufferFree(buf: *mut _xmlBuffer) {
5140    crate::xml::io::buf_free(buf)
5141}
5142
5143/// Empty a buffer.
5144///
5145/// # UPSTREAM-PARITY
5146///
5147/// ```c
5148/// void xmlBufferEmpty(xmlBufferPtr buf);
5149/// ```
5150#[no_mangle]
5151pub extern "C" fn xmlBufferEmpty(buf: *mut _xmlBuffer) {
5152    if buf.is_null() {
5153        return;
5154    }
5155    unsafe {
5156        if !(*buf).content.is_null() {
5157            *(*buf).content = 0;
5158        }
5159        (*buf).use_ = 0;
5160    }
5161}
5162
5163/// Get buffer content.
5164///
5165/// # UPSTREAM-PARITY
5166///
5167/// ```c
5168/// xmlChar *xmlBufferContent(const xmlBuffer *buf);
5169/// ```
5170#[no_mangle]
5171pub extern "C" fn xmlBufferContent(buf: *const _xmlBuffer) -> *mut xmlChar {
5172    crate::xml::io::buf_content(buf as *mut _xmlBuffer)
5173}
5174
5175/// Get buffer length.
5176///
5177/// # UPSTREAM-PARITY
5178///
5179/// ```c
5180/// int xmlBufferLength(const xmlBuffer *buf);
5181/// ```
5182#[no_mangle]
5183pub extern "C" fn xmlBufferLength(buf: *const _xmlBuffer) -> c_int {
5184    crate::xml::io::buf_length(buf as *mut _xmlBuffer)
5185}
5186
5187/// Write to a buffer.
5188///
5189/// # UPSTREAM-PARITY
5190///
5191/// ```c
5192/// int xmlBufferAdd(xmlBufferPtr buf, const xmlChar *str, int len);
5193/// ```
5194#[no_mangle]
5195pub unsafe extern "C" fn xmlBufferAdd(
5196    buf: *mut _xmlBuffer,
5197    str: *const xmlChar,
5198    len: c_int,
5199) -> c_int {
5200    crate::xml::io::buf_add(buf, str, len)
5201}
5202
5203/// Write to a buffer at a position.
5204///
5205/// # UPSTREAM-PARITY
5206///
5207/// ```c
5208/// int xmlBufferAddHead(xmlBufferPtr buf, const xmlChar *str, int len);
5209/// ```
5210#[no_mangle]
5211pub unsafe extern "C" fn xmlBufferAddHead(
5212    buf: *mut _xmlBuffer,
5213    str: *const xmlChar,
5214    len: c_int,
5215) -> c_int {
5216    crate::xml::io::buf_add_head(buf, str, len)
5217}
5218
5219/// Write a C string to a buffer.
5220///
5221/// # UPSTREAM-PARITY
5222///
5223/// ```c
5224/// int xmlBufferCat(xmlBufferPtr buf, const xmlChar *str);
5225/// ```
5226#[no_mangle]
5227pub unsafe extern "C" fn xmlBufferCat(buf: *mut _xmlBuffer, str: *const xmlChar) -> c_int {
5228    if str.is_null() {
5229        return -1;
5230    }
5231    let len = crate::xml::string::xml_strlen(str) as c_int;
5232    crate::xml::io::buf_add(buf, str, len)
5233}
5234
5235/// Set buffer allocation scheme.
5236///
5237/// # UPSTREAM-PARITY
5238///
5239/// ```c
5240/// void xmlBufferSetAllocationScheme(xmlBufferPtr buf,
5241///                                    xmlBufferAllocationScheme scheme);
5242/// ```
5243#[no_mangle]
5244pub extern "C" fn xmlBufferSetAllocationScheme(buf: *mut _xmlBuffer, scheme: c_int) {
5245    if buf.is_null() {
5246        return;
5247    }
5248    unsafe {
5249        (*buf).alloc = scheme;
5250    }
5251}
5252
5253/// Shrink buffer.
5254///
5255/// # UPSTREAM-PARITY
5256///
5257/// ```c
5258/// int xmlBufferShrink(xmlBufferPtr buf, unsigned int len);
5259/// ```
5260///
5261/// Oracle buf.c semantics (11.1-Z.3 alignment): -1 on NULL or `len` larger
5262/// than the buffer content, 0 for `len == 0`, else the removed byte count.
5263#[no_mangle]
5264pub extern "C" fn xmlBufferShrink(buf: *mut _xmlBuffer, len: c_uint) -> c_int {
5265    if buf.is_null() {
5266        return -1;
5267    }
5268    if len == 0 {
5269        return 0;
5270    }
5271    unsafe {
5272        let b = &mut *buf;
5273        if len > b.use_ {
5274            return -1;
5275        }
5276        let remaining = b.use_ - len;
5277        if remaining > 0 {
5278            core::ptr::copy(b.content.add(len as usize), b.content, remaining as usize);
5279        }
5280        *b.content.add(remaining as usize) = 0;
5281        b.use_ = remaining;
5282    }
5283    len as c_int
5284}
5285
5286/// Grow buffer.
5287///
5288/// # UPSTREAM-PARITY
5289///
5290/// ```c
5291/// int xmlBufferGrow(xmlBufferPtr buf, unsigned int len);
5292/// ```
5293#[no_mangle]
5294pub extern "C" fn xmlBufferGrow(buf: *mut _xmlBuffer, len: c_uint) -> c_int {
5295    if buf.is_null() || len == 0 {
5296        return 0;
5297    }
5298    let cur_use = unsafe { (*buf).use_ };
5299    let new_size = cur_use + len + 1;
5300    crate::xml::io::buf_grow(buf, new_size)
5301}
5302
5303/// Reserve buffer space.
5304///
5305/// # UPSTREAM-PARITY
5306///
5307/// ```c
5308/// int xmlBufferReserve(xmlBufferPtr buf, int len);
5309/// ```
5310#[no_mangle]
5311pub extern "C" fn xmlBufferReserve(buf: *mut _xmlBuffer, len: c_int) -> c_int {
5312    xmlBufferGrow(buf, len as c_uint)
5313}
5314
5315/// Detach buffer content.
5316///
5317/// # UPSTREAM-PARITY
5318///
5319/// ```c
5320/// xmlChar *xmlBufferDetach(xmlBufferPtr buf);
5321/// ```
5322#[no_mangle]
5323pub extern "C" fn xmlBufferDetach(buf: *mut _xmlBuffer) -> *mut xmlChar {
5324    if buf.is_null() {
5325        return ptr::null_mut();
5326    }
5327    unsafe {
5328        let content = (*buf).content;
5329        (*buf).content = ptr::null_mut();
5330        (*buf).use_ = 0;
5331        (*buf).size = 0;
5332        content
5333    }
5334}
5335
5336// ═══════════════════════════════════════════════════════════════════════════════
5337// 13. Encoding
5338// ═══════════════════════════════════════════════════════════════════════════════
5339
5340/// Get encoding from a name string.
5341///
5342/// # UPSTREAM-PARITY
5343///
5344/// ```c
5345/// xmlCharEncoding xmlGetCharEncoding(const char *name);
5346/// ```
5347#[no_mangle]
5348pub extern "C" fn xmlGetCharEncoding(name: *const c_char) -> c_int {
5349    if name.is_null() {
5350        return 0; // XML_CHAR_ENCODING_NONE
5351    }
5352    let name_bytes = unsafe {
5353        let len = libc::strlen(name);
5354        core::slice::from_raw_parts(name as *const u8, len)
5355    };
5356    crate::xml::encoding::encoding_from_name(name_bytes) as c_int
5357}
5358
5359/// Find an encoding handler.
5360///
5361/// # UPSTREAM-PARITY
5362///
5363/// ```c
5364/// xmlCharEncodingHandlerPtr xmlFindCharEncodingHandler(const char *name);
5365/// ```
5366#[no_mangle]
5367pub extern "C" fn xmlFindCharEncodingHandler(name: *const c_char) -> *mut c_void {
5368    if name.is_null() {
5369        return ptr::null_mut();
5370    }
5371    // Upstream hands the caller an OWNED handler it must release with
5372    // xmlCharEncCloseFunc (PHP's dom_document_encoding_write closes it after
5373    // every $dom->encoding= write) — except UTF-8, where the handler is static
5374    // and close is a no-op. Returning the persistent registry pointer directly
5375    // would let a non-UTF-8 caller's close free a still-registered handler (the
5376    // use-after-free behind DOMDocument::$encoding='UTF-16' crashing the next
5377    // registry lookup). See xmlFindCharEncodingHandler_owned.
5378    crate::xml::encoding::xmlFindCharEncodingHandler_owned(
5379        name as *const crate::abi::types::xmlChar,
5380    ) as *mut c_void
5381}
5382
5383/// Close an encoding handler.
5384///
5385/// # UPSTREAM-PARITY
5386///
5387/// ```c
5388/// int xmlCharEncCloseFunc(xmlCharEncodingHandlerPtr handler);
5389/// ```
5390#[no_mangle]
5391pub extern "C" fn xmlCharEncCloseFunc(handler: *mut c_void) -> c_int {
5392    if handler.is_null() {
5393        return -1;
5394    }
5395    // Upstream xmlCharEncCloseFunc (encoding.c): a handler flagged
5396    // XML_HANDLER_STATIC (UTF-8's default handler, or any static table
5397    // handler) is not owned by the caller and must not be released.
5398    unsafe {
5399        let h = handler as *mut crate::abi::structs::_xmlCharEncodingHandler;
5400        if (*h).flags & crate::xml::encoding::XML_HANDLER_STATIC != 0 {
5401            return 0;
5402        }
5403        if !(*h).name.is_null() {
5404            crate::abi::allocator::xmlFreeImpl((*h).name as *mut c_void);
5405        }
5406        // Run any conversion-context destructor before freeing the struct,
5407        // matching upstream's ordering (name, then ctxtDtor, then struct).
5408        if let Some(dtor) = (*h).ctxtDtor {
5409            if !(*h).inputCtxt.is_null() {
5410                dtor((*h).inputCtxt);
5411            }
5412            if !(*h).outputCtxt.is_null() {
5413                dtor((*h).outputCtxt);
5414            }
5415        }
5416        crate::abi::allocator::xmlFreeImpl(handler);
5417    }
5418    0
5419}
5420
5421/// Convert a block of ISO-8859-1 bytes to UTF-8 (upstream encoding.c
5422/// `xmlIsolat1ToUTF8`; R-000165 closure).
5423///
5424/// `*outlen`/`*inlen` are updated with the bytes produced/consumed; returns
5425/// the number of bytes written or an xmlCharEncError code.
5426///
5427/// # SAFETY
5428///
5429/// `out`/`in` must be valid buffers for `*outlen`/`*inlen` bytes.
5430#[no_mangle]
5431pub unsafe extern "C" fn xmlIsolat1ToUTF8(
5432    out: *mut u8,
5433    outlen: *mut c_int,
5434    input: *const u8,
5435    inlen: *mut c_int,
5436) -> c_int {
5437    // xmlCharEncError (encoding.h): SUCCESS 0, INTERNAL -1, SPACE -2.
5438    const XML_ENC_ERR_SPACE: c_int = -2;
5439    const XML_ENC_ERR_INTERNAL: c_int = -1;
5440    unsafe {
5441        if out.is_null() || input.is_null() || outlen.is_null() || inlen.is_null() {
5442            return XML_ENC_ERR_INTERNAL;
5443        }
5444        let outstart = out;
5445        let instart = input;
5446        let outend = out.add(*outlen as usize);
5447        let inend = input.add(*inlen as usize);
5448        let mut cur = input;
5449        let mut o = out;
5450        while cur < inend {
5451            let c = *cur;
5452            if c < 0x80 {
5453                if o >= outend {
5454                    break;
5455                }
5456                *o = c;
5457                o = o.add(1);
5458            } else {
5459                if (outend as usize) - (o as usize) < 2 {
5460                    break;
5461                }
5462                *o = (c >> 6) | 0xC0;
5463                *o.add(1) = (c & 0x3F) | 0x80;
5464                o = o.add(2);
5465            }
5466            cur = cur.add(1);
5467        }
5468        let mut ret = XML_ENC_ERR_SPACE;
5469        if cur == inend {
5470            ret = (o as usize - outstart as usize) as c_int;
5471        }
5472        *outlen = (o as usize - outstart as usize) as c_int;
5473        *inlen = (cur as usize - instart as usize) as c_int;
5474        ret
5475    }
5476}
5477
5478/// Convert a block of UTF-8 to ISO-8859-1 (upstream encoding.c
5479/// `xmlUTF8ToIsolat1`; R-000165 closure).
5480///
5481/// # SAFETY
5482///
5483/// `out`/`in` must be valid buffers for `*outlen`/`*inlen` bytes.
5484#[no_mangle]
5485pub unsafe extern "C" fn xmlUTF8ToIsolat1(
5486    out: *mut u8,
5487    outlen: *mut c_int,
5488    input: *const u8,
5489    inlen: *mut c_int,
5490) -> c_int {
5491    const XML_ENC_ERR_SPACE: c_int = -2;
5492    const XML_ENC_ERR_INTERNAL: c_int = -1;
5493    const XML_ENC_ERR_INPUT: c_int = -3;
5494    const XML_ENC_ERR_SUCCESS: c_int = 0;
5495    unsafe {
5496        if out.is_null() || outlen.is_null() || inlen.is_null() {
5497            return XML_ENC_ERR_INTERNAL;
5498        }
5499        if input.is_null() {
5500            *inlen = 0;
5501            *outlen = 0;
5502            return XML_ENC_ERR_SUCCESS;
5503        }
5504        let outstart = out;
5505        let instart = input;
5506        let outend = out.add(*outlen as usize);
5507        let inend = input.add(*inlen as usize);
5508        let mut cur = input;
5509        let mut o = out;
5510        let mut ret = XML_ENC_ERR_SPACE;
5511        while cur < inend {
5512            if o >= outend {
5513                break;
5514            }
5515            let c = *cur;
5516            if c < 0x80 {
5517                *o = c;
5518                o = o.add(1);
5519            } else if (0xC2..=0xC3).contains(&c) {
5520                if (inend as usize) - (cur as usize) < 2 {
5521                    break;
5522                }
5523                cur = cur.add(1);
5524                *o = (c << 6) | (*cur & 0x3F);
5525                o = o.add(1);
5526            } else {
5527                ret = XML_ENC_ERR_INPUT;
5528                break;
5529            }
5530            cur = cur.add(1);
5531        }
5532        if ret != XML_ENC_ERR_INPUT {
5533            ret = (o as usize - outstart as usize) as c_int;
5534        }
5535        *outlen = (o as usize - outstart as usize) as c_int;
5536        *inlen = (cur as usize - instart as usize) as c_int;
5537        ret
5538    }
5539}
5540
5541/// Return the name of a character encoding (upstream encoding.h).
5542///
5543/// # UPSTREAM-PARITY
5544///
5545/// ```c
5546/// const char *xmlGetCharEncodingName(xmlCharEncoding enc);
5547/// ```
5548#[no_mangle]
5549pub extern "C" fn xmlGetCharEncodingName(enc: c_int) -> *const c_char {
5550    /* Values outside the local enum resolve against the upstream
5551     * defaultHandlers table (XML_CHAR_ENCODING_UTF16=23, HTML=24,
5552     * WINDOWS_1252=31); anything else is unknown (NULL). */
5553    if !(-1..=22).contains(&enc) {
5554        return match enc {
5555            23 => c"UTF-16".as_ptr(),
5556            24 => c"HTML".as_ptr(),
5557            31 => c"windows-1252".as_ptr(),
5558            _ => ptr::null(),
5559        };
5560    }
5561    let e: crate::abi::types::xmlCharEncoding = unsafe { core::mem::transmute(enc) };
5562    crate::xml::encoding::xmlGetCharEncodingName(e)
5563}
5564
5565/// Parse an encoding name into an xmlCharEncoding value (upstream encoding.h).
5566///
5567/// # UPSTREAM-PARITY
5568///
5569/// ```c
5570/// xmlCharEncoding xmlParseCharEncoding(const char *name);
5571/// ```
5572///
5573/// Returns the encoding value or XML_CHAR_ENCODING_ERROR (-1).
5574#[no_mangle]
5575pub extern "C" fn xmlParseCharEncoding(name: *const c_char) -> c_int {
5576    crate::xml::encoding::xmlParseCharEncoding(name)
5577}
5578
5579/// Add an encoding alias (upstream encoding.h).
5580///
5581/// # UPSTREAM-PARITY
5582///
5583/// ```c
5584/// int xmlAddEncodingAlias(const char *name, const char *alias);
5585/// ```
5586#[no_mangle]
5587pub extern "C" fn xmlAddEncodingAlias(name: *const c_char, alias: *const c_char) -> c_int {
5588    crate::xml::encoding::add_encoding_alias(name, alias)
5589}
5590
5591/// Delete an encoding alias (upstream encoding.h).
5592///
5593/// # UPSTREAM-PARITY
5594///
5595/// ```c
5596/// int xmlDelEncodingAlias(const char *alias);
5597/// ```
5598#[no_mangle]
5599pub extern "C" fn xmlDelEncodingAlias(alias: *const c_char) -> c_int {
5600    crate::xml::encoding::del_encoding_alias(alias)
5601}
5602
5603/// Look up an encoding alias (upstream encoding.h).
5604///
5605/// # UPSTREAM-PARITY
5606///
5607/// ```c
5608/// const char *xmlGetEncodingAlias(const char *alias);
5609/// ```
5610#[no_mangle]
5611pub extern "C" fn xmlGetEncodingAlias(alias: *const c_char) -> *const c_char {
5612    crate::xml::encoding::get_encoding_alias(alias)
5613}
5614
5615/// Clean up the encoding alias table (upstream encoding.h).
5616///
5617/// # UPSTREAM-PARITY
5618///
5619/// ```c
5620/// void xmlCleanupEncodingAliases(void);
5621/// ```
5622#[no_mangle]
5623pub extern "C" fn xmlCleanupEncodingAliases() {
5624    crate::xml::encoding::cleanup_encoding_aliases();
5625}
5626
5627/// Convert the input buffer using an encoding handler (upstream encoding.h).
5628///
5629/// # UPSTREAM-PARITY
5630///
5631/// ```c
5632/// int xmlCharEncInFunc(xmlCharEncodingHandler *handler,
5633///                      xmlBufferPtr out, xmlBufferPtr in);
5634/// ```
5635#[no_mangle]
5636pub extern "C" fn xmlCharEncInFunc(
5637    handler: *mut c_void,
5638    out: *mut c_void,
5639    in_: *mut c_void,
5640) -> c_int {
5641    crate::xml::encoding::xmlCharEncInFunc(
5642        handler as *mut crate::abi::structs::_xmlCharEncodingHandler,
5643        out as *mut crate::abi::structs::_xmlBuffer,
5644        in_ as *mut crate::abi::structs::_xmlBuffer,
5645    )
5646}
5647
5648/// Convert the output buffer using an encoding handler (upstream encoding.h).
5649///
5650/// # UPSTREAM-PARITY
5651///
5652/// ```c
5653/// int xmlCharEncOutFunc(xmlCharEncodingHandler *handler,
5654///                       xmlBufferPtr out, xmlBufferPtr in);
5655/// ```
5656#[no_mangle]
5657pub extern "C" fn xmlCharEncOutFunc(
5658    handler: *mut c_void,
5659    out: *mut c_void,
5660    in_: *mut c_void,
5661) -> c_int {
5662    crate::xml::encoding::xmlCharEncOutFunc(
5663        handler as *mut crate::abi::structs::_xmlCharEncodingHandler,
5664        out as *mut crate::abi::structs::_xmlBuffer,
5665        in_ as *mut crate::abi::structs::_xmlBuffer,
5666    )
5667}
5668
5669/// Create a new encoding handler (upstream encoding.h).
5670///
5671/// # UPSTREAM-PARITY
5672///
5673/// ```c
5674/// xmlCharEncodingHandlerPtr xmlNewCharEncodingHandler(
5675///     const char *name, xmlCharEncodingInputFunc input,
5676///     xmlCharEncodingOutputFunc output);
5677/// ```
5678#[no_mangle]
5679pub extern "C" fn xmlNewCharEncodingHandler(
5680    name: *const c_char,
5681    input: crate::abi::callbacks::xmlCharEncodingInputFunc,
5682    output: crate::abi::callbacks::xmlCharEncodingOutputFunc,
5683) -> *mut c_void {
5684    crate::xml::encoding::xmlNewCharEncodingHandler(name, input, output) as *mut c_void
5685}
5686
5687/// Initialize the built-in encoding handlers (upstream encoding.h).
5688///
5689/// # UPSTREAM-PARITY
5690///
5691/// ```c
5692/// void xmlInitCharEncodingHandlers(void);
5693/// ```
5694#[no_mangle]
5695pub extern "C" fn xmlInitCharEncodingHandlers() {
5696    crate::xml::encoding::xmlInitCharEncodingHandlers();
5697}
5698
5699/// Clean up the encoding handlers (upstream encoding.h).
5700///
5701/// # UPSTREAM-PARITY
5702///
5703/// ```c
5704/// void xmlCleanupCharEncodingHandlers(void);
5705/// ```
5706#[no_mangle]
5707pub extern "C" fn xmlCleanupCharEncodingHandlers() {
5708    crate::xml::encoding::xmlCleanupCharEncodingHandlers();
5709}
5710
5711/// Look up a built-in encoding handler by `xmlCharEncoding` value.
5712///
5713/// Returns an `xmlParserErrors` code; on success `*out` receives the static
5714/// handler (NULL for UTF-8, which needs no conversion).
5715///
5716/// # UPSTREAM-PARITY
5717///
5718/// ```c
5719/// xmlParserErrors xmlLookupCharEncodingHandler(xmlCharEncoding enc,
5720///                                              xmlCharEncodingHandler **out);
5721/// ```
5722#[no_mangle]
5723pub extern "C" fn xmlLookupCharEncodingHandler(enc: c_int, out: *mut *mut c_void) -> c_int {
5724    crate::xml::encoding::xmlLookupCharEncodingHandler(enc, out)
5725}
5726
5727/// Get the encoding handler for an `xmlCharEncoding` value (deprecated).
5728///
5729/// # UPSTREAM-PARITY
5730///
5731/// ```c
5732/// xmlCharEncodingHandler *xmlGetCharEncodingHandler(xmlCharEncoding enc);
5733/// ```
5734#[no_mangle]
5735pub extern "C" fn xmlGetCharEncodingHandler(enc: c_int) -> *mut c_void {
5736    crate::xml::encoding::xmlGetCharEncodingHandler(enc)
5737}
5738
5739/// Find or create an encoding handler by name for one conversion direction.
5740///
5741/// # UPSTREAM-PARITY
5742///
5743/// ```c
5744/// xmlParserErrors xmlOpenCharEncodingHandler(const char *name, int output,
5745///                                            xmlCharEncodingHandler **out);
5746/// ```
5747#[no_mangle]
5748pub extern "C" fn xmlOpenCharEncodingHandler(
5749    name: *const c_char,
5750    output: c_int,
5751    out: *mut *mut c_void,
5752) -> c_int {
5753    crate::xml::encoding::xmlOpenCharEncodingHandler(name, output, out)
5754}
5755
5756/// Find or create an encoding handler by name with flags and an optional
5757/// custom conversion implementation.
5758///
5759/// # UPSTREAM-PARITY
5760///
5761/// ```c
5762/// xmlParserErrors xmlCreateCharEncodingHandler(
5763///     const char *name, xmlCharEncFlags flags, xmlCharEncConvImpl impl,
5764///     void *implCtxt, xmlCharEncodingHandler **out);
5765/// ```
5766#[no_mangle]
5767pub extern "C" fn xmlCreateCharEncodingHandler(
5768    name: *const c_char,
5769    flags: c_int,
5770    impl_: Option<crate::abi::callbacks::xmlCharEncConvImpl>,
5771    implCtxt: *mut c_void,
5772    out: *mut *mut c_void,
5773) -> c_int {
5774    crate::xml::encoding::xmlCreateCharEncodingHandler(name, flags, impl_, implCtxt, out)
5775}
5776
5777/// Create an encoding handler backed by modern conversion callbacks.
5778///
5779/// # UPSTREAM-PARITY
5780///
5781/// ```c
5782/// xmlParserErrors xmlCharEncNewCustomHandler(
5783///     const char *name, xmlCharEncConvFunc input, xmlCharEncConvFunc output,
5784///     xmlCharEncConvCtxtDtor ctxtDtor, void *inputCtxt, void *outputCtxt,
5785///     xmlCharEncodingHandler **out);
5786/// ```
5787#[no_mangle]
5788pub extern "C" fn xmlCharEncNewCustomHandler(
5789    name: *const c_char,
5790    input: crate::abi::callbacks::xmlCharEncConvFunc,
5791    output: crate::abi::callbacks::xmlCharEncConvFunc,
5792    ctxtDtor: Option<crate::abi::callbacks::xmlCharEncConvCtxtDtor>,
5793    inputCtxt: *mut c_void,
5794    outputCtxt: *mut c_void,
5795    out: *mut *mut c_void,
5796) -> c_int {
5797    crate::xml::encoding::xmlCharEncNewCustomHandler(
5798        name, input, output, ctxtDtor, inputCtxt, outputCtxt, out,
5799    )
5800}
5801
5802/// Convert an input buffer's encoding.
5803///
5804/// # UPSTREAM-PARITY
5805///
5806/// ```c
5807/// int xmlCharEncInput(xmlParserInputBufferPtr input, int to);
5808/// ```
5809#[no_mangle]
5810pub unsafe extern "C" fn xmlCharEncInput(input: *mut _xmlParserInputBuffer, _to: c_int) -> c_int {
5811    if input.is_null() {
5812        return -1;
5813    }
5814    let handler = (*input).encoder as *mut crate::abi::structs::_xmlCharEncodingHandler;
5815    if handler.is_null() {
5816        return -1;
5817    }
5818    let raw = (*input).raw as *mut crate::abi::structs::_xmlBuffer;
5819    let buf = (*input).buffer as *mut crate::abi::structs::_xmlBuffer;
5820    if raw.is_null() || buf.is_null() {
5821        return -1;
5822    }
5823    crate::xml::encoding::char_enc_in(handler, buf, raw)
5824}
5825
5826/// Convert an output buffer's encoding.
5827///
5828/// # UPSTREAM-PARITY
5829///
5830/// ```c
5831/// int xmlCharEncOutput(xmlOutputBufferPtr output, int to);
5832/// ```
5833#[no_mangle]
5834pub unsafe extern "C" fn xmlCharEncOutput(output: *mut _xmlOutputBuffer, _to: c_int) -> c_int {
5835    if output.is_null() {
5836        return -1;
5837    }
5838    let handler = (*output).encoder as *mut crate::abi::structs::_xmlCharEncodingHandler;
5839    if handler.is_null() {
5840        return -1;
5841    }
5842    let buf = (*output).buffer as *mut crate::abi::structs::_xmlBuffer;
5843    let conv = (*output).conv as *mut crate::abi::structs::_xmlBuffer;
5844    if buf.is_null() || conv.is_null() {
5845        return -1;
5846    }
5847    crate::xml::encoding::char_enc_out(handler, conv, buf)
5848}
5849
5850// ═══════════════════════════════════════════════════════════════════════════════
5851// URI
5852// ═══════════════════════════════════════════════════════════════════════════════
5853
5854/// Parse a URI string.
5855///
5856/// # UPSTREAM-PARITY
5857///
5858/// ```c
5859/// xmlURIPtr xmlParseURI(const char *str);
5860/// ```
5861#[no_mangle]
5862pub unsafe extern "C" fn xmlParseURI(str: *const c_char) -> *mut c_void {
5863    crate::xml::uri::xmlParseURI(str)
5864}
5865
5866/// Parse a URI string (raw version).
5867///
5868/// # UPSTREAM-PARITY
5869///
5870/// ```c
5871/// xmlURIPtr xmlParseURIRaw(const char *str, int raw);
5872/// ```
5873#[no_mangle]
5874pub unsafe extern "C" fn xmlParseURIRaw(str: *const c_char, raw: c_int) -> *mut c_void {
5875    let _ = raw;
5876    crate::xml::uri::xmlParseURI(str)
5877}
5878
5879/// Free a URI structure.
5880///
5881/// # UPSTREAM-PARITY
5882///
5883/// ```c
5884/// void xmlFreeURI(xmlURIPtr uri);
5885/// ```
5886#[no_mangle]
5887pub unsafe extern "C" fn xmlFreeURI(uri: *mut c_void) {
5888    crate::xml::uri::xmlFreeURI(uri)
5889}
5890
5891/// Create an empty URI.
5892///
5893/// # UPSTREAM-PARITY
5894///
5895/// ```c
5896/// xmlURIPtr xmlCreateURI(void);
5897/// ```
5898#[no_mangle]
5899pub extern "C" fn xmlCreateURI() -> *mut c_void {
5900    crate::xml::uri::xmlCreateURI()
5901}
5902
5903/// Save a URI structure to a string.
5904///
5905/// # UPSTREAM-PARITY
5906///
5907/// ```c
5908/// xmlChar *xmlSaveUri(xmlURIPtr uri);
5909/// ```
5910#[no_mangle]
5911pub unsafe extern "C" fn xmlSaveUri(uri: *mut c_void) -> *mut xmlChar {
5912    crate::xml::uri::xmlSaveUri(uri)
5913}
5914
5915/// Parse a URI string into an existing URI structure (upstream uri.h).
5916///
5917/// # UPSTREAM-PARITY
5918///
5919/// ```c
5920/// int xmlParseURIReference(xmlURIPtr uri, const char *str);
5921/// ```
5922///
5923/// Returns 0 on success, -1 on failure (the URI structure is left
5924/// untouched on failure).
5925///
5926/// # Safety
5927///
5928/// - `uri` must be a valid pointer from `xmlParseURI`/`xmlCreateURI`.
5929/// - `str` must be a valid null-terminated C string.
5930#[no_mangle]
5931pub unsafe extern "C" fn xmlParseURIReference(uri: *mut c_void, str: *const c_char) -> c_int {
5932    crate::xml::uri::xmlParseURIReference(uri, str)
5933}
5934
5935/// Normalize a URI path in place (upstream uri.h).
5936///
5937/// # UPSTREAM-PARITY
5938///
5939/// ```c
5940/// int xmlNormalizeURIPath(char *path);
5941/// ```
5942///
5943/// Returns 0 on success, -1 if the path is NULL, not absolute, or contains
5944/// `..` segments that climb above the root.
5945///
5946/// # Safety
5947///
5948/// `path` must be a valid writable null-terminated C string buffer.
5949#[no_mangle]
5950pub unsafe extern "C" fn xmlNormalizeURIPath(path: *mut c_char) -> c_int {
5951    crate::xml::uri::xmlNormalizeURIPath(path)
5952}
5953
5954/// Escape a URI string.
5955///
5956/// # UPSTREAM-PARITY
5957///
5958/// ```c
5959/// xmlChar *xmlURIEscapeStr(const xmlChar *str, const xmlChar *list);
5960/// ```
5961#[no_mangle]
5962pub unsafe extern "C" fn xmlURIEscapeStr(
5963    str: *const xmlChar,
5964    list: *const xmlChar,
5965) -> *mut xmlChar {
5966    crate::xml::uri::xmlURIEscapeStr(str, list)
5967}
5968
5969/// Unescape a URI string.
5970///
5971/// # UPSTREAM-PARITY
5972///
5973/// ```c
5974/// char *xmlURIUnescapeString(const char *str, int len, char *target);
5975/// ```
5976#[no_mangle]
5977pub unsafe extern "C" fn xmlURIUnescapeString(
5978    str: *const c_char,
5979    len: c_int,
5980    target: *mut c_char,
5981) -> *mut c_char {
5982    crate::xml::uri::xmlURIUnescapeString(str, len, target)
5983}
5984
5985// ═══════════════════════════════════════════════════════════════════════════════
5986// 14. XPath
5987// ═══════════════════════════════════════════════════════════════════════════════
5988
5989// ── Helper functions ────────────────────────────────────────────────────
5990
5991/// Convert an internal `XPathValue` to a C ABI `_xmlXPathObject`.
5992///
5993/// The returned pointer is heap-allocated via `xmlMallocZero` and must be
5994/// freed with `xmlXPathFreeObject`.
5995///
5996/// # Safety
5997///
5998/// Must be called from a context where `xmlMalloc` is safe to call.
5999unsafe fn xpath_to_object(val: XPathValue) -> *mut _xmlXPathObject {
6000    let obj = xmlMallocZero(size_of::<_xmlXPathObject>()) as *mut _xmlXPathObject;
6001    if obj.is_null() {
6002        return ptr::null_mut();
6003    }
6004    match val {
6005        XPathValue::NodeSet(ns) => {
6006            (*obj).type_ = xmlXPathObjectType::XPATH_NODESET as c_int;
6007            (*obj).nodesetval = ns.to_raw() as *mut c_void;
6008        }
6009        XPathValue::Boolean(b) => {
6010            (*obj).type_ = xmlXPathObjectType::XPATH_BOOLEAN as c_int;
6011            (*obj).boolval = if b { 1 } else { 0 };
6012        }
6013        XPathValue::Number(n) => {
6014            (*obj).type_ = xmlXPathObjectType::XPATH_NUMBER as c_int;
6015            (*obj).floatval = n;
6016        }
6017        XPathValue::String(s) => {
6018            (*obj).type_ = xmlXPathObjectType::XPATH_STRING as c_int;
6019            let bytes = s.as_bytes();
6020            let len = bytes.len();
6021            let buf = xmlMallocImpl(len + 1) as *mut xmlChar;
6022            if !buf.is_null() {
6023                ptr::copy_nonoverlapping(bytes.as_ptr(), buf, len);
6024                *buf.add(len) = 0; // null terminator
6025            }
6026            (*obj).stringval = buf;
6027        }
6028    }
6029    obj
6030}
6031
6032/// Extract an internal `XPathValue` from a C ABI `_xmlXPathObject`.
6033///
6034/// # Safety
6035///
6036/// `obj` must be a valid, non-null pointer to a properly initialised
6037/// `_xmlXPathObject`.
6038unsafe fn object_to_xpathvalue(obj: *mut _xmlXPathObject) -> XPathValue {
6039    let typ = (*obj).type_;
6040    if typ == xmlXPathObjectType::XPATH_NODESET as c_int {
6041        let ns_ptr = (*obj).nodesetval as *mut _xmlNodeSet;
6042        if ns_ptr.is_null() {
6043            return XPathValue::NodeSet(NodeSet::new());
6044        }
6045        let node_nr = (*ns_ptr).nodeNr;
6046        let node_tab = (*ns_ptr).nodeTab;
6047        let mut ns = NodeSet::new();
6048        if !node_tab.is_null() {
6049            for i in 0..node_nr as isize {
6050                let node = *node_tab.add(i as usize);
6051                ns.push(node);
6052            }
6053        }
6054        XPathValue::NodeSet(ns)
6055    } else if typ == xmlXPathObjectType::XPATH_BOOLEAN as c_int {
6056        XPathValue::Boolean((*obj).boolval != 0)
6057    } else if typ == xmlXPathObjectType::XPATH_NUMBER as c_int {
6058        XPathValue::Number((*obj).floatval)
6059    } else if typ == xmlXPathObjectType::XPATH_STRING as c_int {
6060        let s_ptr = (*obj).stringval;
6061        if s_ptr.is_null() {
6062            XPathValue::String(String::new())
6063        } else {
6064            let s = CStr::from_ptr(s_ptr as *const c_char)
6065                .to_string_lossy()
6066                .into_owned();
6067            XPathValue::String(s)
6068        }
6069    } else if typ == xmlXPathObjectType::XPATH_XSLT_TREE as c_int {
6070        // A result tree fragment: node-set containing the fragment's
6071        // document node (matching how global RTF variables are bound), so
6072        // local RTF variables stringify to their text and remain navigable
6073        // via exsl:node-set.
6074        let frag_doc = (*obj).nodesetval as *mut _xmlDoc;
6075        if frag_doc.is_null() {
6076            XPathValue::NodeSet(NodeSet::new())
6077        } else {
6078            let mut ns = NodeSet::new();
6079            ns.push(frag_doc as *mut _xmlNode);
6080            XPathValue::NodeSet(ns)
6081        }
6082    } else {
6083        // Undefined / unknown type — return boolean false as a safe default.
6084        XPathValue::Boolean(false)
6085    }
6086}
6087
6088/// Public wrapper for `xpath_to_object` (used by the XPath export bridge).
6089///
6090/// # Safety
6091///
6092/// - `val` is consumed and converted into a heap-allocated `_xmlXPathObject`.
6093pub unsafe fn xpath_to_object_pub(val: XPathValue) -> *mut _xmlXPathObject {
6094    xpath_to_object(val)
6095}
6096
6097/// Public wrapper for `object_to_xpathvalue` (used by the XSLT engine).
6098///
6099/// # Safety
6100///
6101/// `obj` must be a valid, non-null pointer to a properly initialised
6102/// `_xmlXPathObject`.
6103pub unsafe fn object_to_xpathvalue_pub(obj: *mut _xmlXPathObject) -> XPathValue {
6104    object_to_xpathvalue(obj)
6105}
6106
6107// ── Compiled expression registry ────────────────────────────────────────
6108//
6109// Compiled XPath expressions are opaque pointers returned by xmlXPathCompile.
6110// We store them in a global registry keyed by a monotonically increasing ID.
6111
6112static COMPILED_EXPRS: Lazy<Mutex<HashMap<u64, Box<CompiledExpr>>>> =
6113    Lazy::new(|| Mutex::new(HashMap::new()));
6114static NEXT_COMPILED_KEY: Lazy<Mutex<u64>> = Lazy::new(|| Mutex::new(1));
6115
6116/// Accessor for the compiled-expression registry (used by the XPath export
6117/// bridge for `xmlXPathCompiledEval` / `xmlXPathCompiledEvalToBoolean`).
6118pub(crate) fn xpath_compiled_registry() -> &'static Mutex<HashMap<u64, Box<CompiledExpr>>> {
6119    &COMPILED_EXPRS
6120}
6121
6122// ── C extension-function registry ──────────────────────────────────────
6123//
6124// C extension functions registered via xmlXPathRegisterFunc / RegisterFuncNS
6125// are stored here because the Rust XPathFunction signature is incompatible
6126// with the C xmlXPathFunction calling convention (the C function expects a
6127// parser context, not pre-evaluated argument slices). The registration is
6128// stored faithfully; invoking registered C functions from within the Rust
6129// evaluator requires a bridge that is not yet implemented.
6130
6131type CXPathFunc = unsafe extern "C" fn(*mut c_void, c_int);
6132
6133/// Wrapper around `*mut c_void` that implements `Send` + `Sync` so it can
6134/// be used as a key in a `Mutex`-protected global `HashMap`.
6135#[derive(Clone, Copy, PartialEq, Eq, Hash)]
6136struct SendSyncPtr(*mut c_void);
6137unsafe impl Send for SendSyncPtr {}
6138unsafe impl Sync for SendSyncPtr {}
6139
6140static C_FUNCTIONS: Lazy<Mutex<HashMap<(SendSyncPtr, String), CXPathFunc>>> =
6141    Lazy::new(|| Mutex::new(HashMap::new()));
6142
6143/// Look up a C-registered extension function for the context identified by
6144/// `extra` (the internal XPathContext pointer). Used by
6145/// `xmlXPathFunctionLookupNS`.
6146pub(crate) fn xpath_cfunc_lookup(extra: *mut c_void, qualified: &str) -> Option<CXPathFunc> {
6147    C_FUNCTIONS
6148        .lock()
6149        .get(&(SendSyncPtr(extra), qualified.to_string()))
6150        .copied()
6151}
6152
6153/// Drop every C extension-function registration belonging to the context
6154/// identified by `extra` (upstream `xmlXPathRegisteredFuncsCleanup`).
6155pub(crate) fn xpath_cfunc_cleanup(extra: *mut c_void) {
6156    C_FUNCTIONS.lock().retain(|(k, _), _| k.0 != extra);
6157}
6158
6159/// Build the Rust-side closure that bridges a C-registered XPath function
6160/// into the Rust evaluator (see `c_func_call_bridge`). Returns a
6161/// `BoxedXPathFunction` so the closure is coerced with the higher-ranked
6162/// signature the evaluator requires.
6163fn c_func_bridge_closure(c_ctxt: SendSyncPtr, qualified: String) -> BoxedXPathFunction {
6164    let (local_name, ns_uri) = split_qualified(&qualified);
6165    Box::new(move |_ctx: &mut XPathContext, args: &[XPathValue]| {
6166        let cc = c_ctxt;
6167        unsafe {
6168            c_func_call_bridge(
6169                cc.0 as *mut _xmlXPathContext,
6170                &qualified,
6171                &local_name,
6172                ns_uri.as_deref(),
6173                args,
6174            )
6175        }
6176    })
6177}
6178
6179/// Split a registered function key (`{uri}name` — the Clark notation used by
6180/// `xmlXPathRegisterFuncNS` — or a bare `name`) into the LOCAL function name
6181/// and the optional namespace URI. Upstream sets exactly these two on
6182/// `ctxt->context->function` / `functionURI` before invoking a registered C
6183/// function (xpath.c xmlXPathCompOpEval), and PHP's dom/xsl trampolines read
6184/// them back to dispatch to the registered PHP closure.
6185fn split_qualified(qualified: &str) -> (String, Option<String>) {
6186    if let Some(rest) = qualified.strip_prefix('{') {
6187        if let Some(end) = rest.find('}') {
6188            let uri = rest[..end].to_string();
6189            let local = rest[end + 1..].to_string();
6190            return (local, Some(uri));
6191        }
6192    }
6193    (qualified.to_string(), None)
6194}
6195
6196/// Call a C-ABI `xmlXPathFunction` through a synthesized
6197/// `xmlXPathParserContext`: push the evaluated arguments as XPath objects,
6198/// invoke the function, pop and convert the result — the upstream
6199/// `xmlXPathCompOpEval` function-call sequence (xpath.c).
6200///
6201/// # UPSTREAM-PARITY
6202///
6203/// xpath.c xmlXPathCompOpEval (XPATH_OP_FUNCTION) sets the in-context
6204/// function identity before the call and restores it after:
6205///
6206/// ```c
6207/// oldFunc = ctxt->context->function;
6208/// oldFuncURI = ctxt->context->functionURI;
6209/// ctxt->context->function = op->value4;       /* local name */
6210/// ctxt->context->functionURI = op->cacheURI;  /* resolved ns URI or NULL */
6211/// func(ctxt, op->value);
6212/// ctxt->context->function = oldFunc;
6213/// ctxt->context->functionURI = oldFuncURI;
6214/// ```
6215///
6216/// PHP registers ONE trampoline for every custom-namespace XPath function
6217/// and dispatches to the PHP closure by reading `ctxt->context->function` /
6218/// `functionURI`, so without these fields it dereferences garbage
6219/// (SP-14.3.6-dom O1: return_dom_node_from_xpath / registerPhpFunctionNS
6220/// segv).
6221///
6222/// # SAFETY
6223///
6224/// - `fnptr` must be a valid C callback (or None).
6225/// - `c_ctxt` must be the live C XPath context the callback belongs to.
6226/// - `name`/`ns_uri` must be the function's local name / namespace being
6227///   invoked, valid for the duration of the call.
6228pub(crate) unsafe fn call_c_xpath_function(
6229    fnptr: Option<unsafe extern "C" fn(*mut c_void, c_int)>,
6230    c_ctxt: *mut _xmlXPathContext,
6231    name: &str,
6232    ns_uri: Option<&str>,
6233    args: &[XPathValue],
6234) -> Result<XPathValue, String> {
6235    let func = match fnptr {
6236        Some(f) => f,
6237        None => return Err("XPath: missing C function pointer".to_string()),
6238    };
6239    let pc = crate::xml::xpath::parser_context::new_parser_context(ptr::null(), c_ctxt);
6240    if pc.is_null() {
6241        return Err("XPath: parser-context allocation failure".to_string());
6242    }
6243    let mut push_ok = true;
6244    for v in args {
6245        let obj = xpath_to_object(v.clone());
6246        if obj.is_null() || crate::xml::xpath::parser_context::value_push(pc, obj).is_null() {
6247            push_ok = false;
6248            break;
6249        }
6250    }
6251    let result = if push_ok {
6252        // NUL-terminated buffers naming the invoked function, live for the
6253        // callback duration (upstream op->value4 / op->cacheURI).
6254        let mut name_nul: Vec<xmlChar> = name.as_bytes().to_vec();
6255        name_nul.push(0);
6256        let uri_nul: Option<Vec<xmlChar>> = ns_uri.map(|u| {
6257            let mut v = u.as_bytes().to_vec();
6258            v.push(0);
6259            v
6260        });
6261        let saved_function = (*c_ctxt).function;
6262        let saved_function_uri = (*c_ctxt).functionURI;
6263        (*c_ctxt).function = name_nul.as_ptr() as *const xmlChar;
6264        (*c_ctxt).functionURI = uri_nul
6265            .as_ref()
6266            .map_or(ptr::null(), |v| v.as_ptr() as *const xmlChar);
6267        // SAFETY: `func` is a valid C callback; the arguments are on the
6268        // parser-context value stack exactly as upstream would leave them
6269        // and the context names the invoked function.
6270        unsafe { func(pc as *mut c_void, args.len() as c_int) };
6271        (*c_ctxt).function = saved_function;
6272        (*c_ctxt).functionURI = saved_function_uri;
6273        let ret = crate::xml::xpath::parser_context::value_pop(pc);
6274        if ret.is_null() {
6275            Err("XPath: C function returned no value".to_string())
6276        } else {
6277            let v = object_to_xpathvalue(ret);
6278            // The popped object is heap-allocated; free it after converting.
6279            unsafe { xmlXPathFreeObject(ret) };
6280            Ok(v)
6281        }
6282    } else {
6283        Err("XPath: failed to push arguments to C function".to_string())
6284    };
6285    // Free any objects the C function left on the stack, then the context.
6286    unsafe {
6287        loop {
6288            let leftover = crate::xml::xpath::parser_context::value_pop(pc);
6289            if leftover.is_null() {
6290                break;
6291            }
6292            xmlXPathFreeObject(leftover);
6293        }
6294        crate::xml::xpath::parser_context::free_parser_context(pc);
6295    }
6296    result
6297}
6298
6299/// Invoke an XSLT extension / module function through the upstream
6300/// parser-context protocol, with an upstream-layout context.
6301///
6302/// Upstream libxslt stores the transform context in `xmlXPathContext.extra`
6303/// (XSLT_REGISTER_VARIABLE_LOOKUP, variables.h), and PHP's xsl callbacks
6304/// read `parser_ctxt->context->extra` DIRECTLY as the
6305/// `xsltTransformContextPtr` (xsltprocessor.c xsl_proxy_factory). The
6306/// candidate reserves `extra` for the internal Rust `XPathContext`, so this
6307/// bridge synthesises a shallow MIRROR of the C context (same doc/node/…,
6308/// `extra` = transform context) that lives only for the callback duration.
6309/// The real transform XPath context is left untouched; the function name
6310/// is set on the mirror (php's call_custom_ns reads `context->function` /
6311/// `functionURI`), and arguments are pushed in evaluation order (last arg
6312/// on top) exactly as upstream `xmlXPathCompOpEval` leaves them.
6313///
6314/// Returns `Err` when the callback produced no value.
6315///
6316/// # Safety
6317///
6318/// - `fnptr` must be a C `xmlXPathFunction`-compatible callback.
6319/// - `tctxt` / `xpath_ctxt` must be the live transform context pair.
6320/// - `xpath_ctxt` must be the transform context's own `xpathCtxt`.
6321pub(crate) unsafe fn call_xslt_ext_function(
6322    fnptr: Option<unsafe extern "C" fn(*mut c_void, c_int)>,
6323    tctxt: *mut crate::abi::structs::_xsltTransformContext,
6324    xpath_ctxt: *mut _xmlXPathContext,
6325    name: &str,
6326    ns_uri: Option<&str>,
6327    args: &[XPathValue],
6328) -> Result<XPathValue, String> {
6329    let func = match fnptr {
6330        Some(f) => f,
6331        None => return Err("XPath: missing C function pointer".to_string()),
6332    };
6333    if tctxt.is_null() || xpath_ctxt.is_null() {
6334        return Err("XPath: null XSLT context in extension-function bridge".to_string());
6335    }
6336    // Mirror: shallow copy of the C-visible context with `extra` = tctxt
6337    // (upstream layout that php's proxy code dereferences).
6338    let mirror = libc::calloc(1, core::mem::size_of::<_xmlXPathContext>()) as *mut _xmlXPathContext;
6339    if mirror.is_null() {
6340        return Err("XPath: mirror-context allocation failure".to_string());
6341    }
6342    unsafe {
6343        libc::memcpy(
6344            mirror as *mut libc::c_void,
6345            xpath_ctxt as *const libc::c_void,
6346            core::mem::size_of::<_xmlXPathContext>(),
6347        );
6348        (*mirror).extra = tctxt as *mut c_void;
6349        let pc = crate::xml::xpath::parser_context::new_parser_context(ptr::null(), mirror);
6350        if pc.is_null() {
6351            libc::free(mirror as *mut libc::c_void);
6352            return Err("XPath: parser-context allocation failure".to_string());
6353        }
6354        let mut push_ok = true;
6355        for v in args {
6356            let obj = xpath_to_object(v.clone());
6357            if obj.is_null() || crate::xml::xpath::parser_context::value_push(pc, obj).is_null() {
6358                push_ok = false;
6359                break;
6360            }
6361        }
6362        let result = if push_ok {
6363            // NUL-terminated buffers naming the invoked function, live for
6364            // the callback duration (upstream op->value4 / op->cacheURI on
6365            // the eval context).
6366            let mut name_nul: Vec<xmlChar> = name.as_bytes().to_vec();
6367            name_nul.push(0);
6368            let uri_nul: Option<Vec<xmlChar>> = ns_uri.map(|u| {
6369                let mut v = u.as_bytes().to_vec();
6370                v.push(0);
6371                v
6372            });
6373            (*mirror).function = name_nul.as_ptr() as *const xmlChar;
6374            (*mirror).functionURI = uri_nul
6375                .as_ref()
6376                .map_or(ptr::null(), |v| v.as_ptr() as *const xmlChar);
6377            // SAFETY: `func` is a valid C callback; the arguments are on the
6378            // parser-context value stack exactly as upstream would leave them
6379            // and the context names the invoked function.
6380            func(pc as *mut c_void, args.len() as c_int);
6381            let ret = crate::xml::xpath::parser_context::value_pop(pc);
6382            if ret.is_null() {
6383                Err("XPath: C function returned no value".to_string())
6384            } else {
6385                let v = object_to_xpathvalue(ret);
6386                // The popped object is heap-allocated; free it after converting.
6387                xmlXPathFreeObject(ret);
6388                Ok(v)
6389            }
6390        } else {
6391            Err("XPath: failed to push arguments to C function".to_string())
6392        };
6393        // Free any objects the C function left on the stack, then the
6394        // parser context and the mirror.
6395        loop {
6396            let leftover = crate::xml::xpath::parser_context::value_pop(pc);
6397            if leftover.is_null() {
6398                break;
6399            }
6400            xmlXPathFreeObject(leftover);
6401        }
6402        crate::xml::xpath::parser_context::free_parser_context(pc);
6403        libc::free(mirror as *mut libc::c_void);
6404        result
6405    }
6406}
6407
6408/// Rust-side wrapper registered in the internal XPathContext when a C
6409/// extension function is registered (`xmlXPathRegisterFunc[NS]`). This is the
6410/// parser-context bridge: it synthesises the upstream `xmlXPathParserContext`
6411/// (value stack + context pointer), pushes the evaluated arguments as XPath
6412/// objects, invokes the C function, then pops and converts the result — the
6413/// upstream `xmlXPathCompOpEval` function-call sequence (xpath.c).
6414unsafe fn c_func_call_bridge(
6415    c_ctxt: *mut _xmlXPathContext,
6416    qualified: &str,
6417    name: &str,
6418    ns_uri: Option<&str>,
6419    args: &[XPathValue],
6420) -> Result<XPathValue, String> {
6421    if c_ctxt.is_null() {
6422        return Err("XPath: null context in C function bridge".to_string());
6423    }
6424    let func = xpath_cfunc_lookup((*c_ctxt).extra, qualified);
6425    if func.is_none() {
6426        return Err(format!("XPath: unknown C function '{}'", qualified));
6427    }
6428    unsafe { call_c_xpath_function(func, c_ctxt, name, ns_uri, args) }
6429}
6430
6431// ── Public API ─────────────────────────────────────────────────────────
6432
6433/// Create a new XPath context.
6434///
6435/// Allocates a `_xmlXPathContext` and an internal `XPathContext`, storing
6436/// the latter's pointer in the `extra` field.
6437///
6438/// # UPSTREAM-PARITY
6439///
6440/// ```c
6441/// xmlXPathContextPtr xmlXPathNewContext(xmlDocPtr doc);
6442/// ```
6443#[no_mangle]
6444pub unsafe extern "C" fn xmlXPathNewContext(doc: *mut _xmlDoc) -> *mut _xmlXPathContext {
6445    let ctxt = xmlMallocZero(size_of::<_xmlXPathContext>()) as *mut _xmlXPathContext;
6446    if ctxt.is_null() {
6447        return ptr::null_mut();
6448    }
6449
6450    // Initialise the C ABI context fields.
6451    (*ctxt).doc = doc;
6452    (*ctxt).node = ptr::null_mut();
6453    (*ctxt).contextSize = 1;
6454    (*ctxt).proximityPosition = 1;
6455
6456    // Create the internal XPathContext and store it in `extra`.
6457    let mut internal = Box::new(XPathContext::new(doc));
6458    // UPSTREAM-PARITY: the standard function library is implicitly available
6459    // in every context (upstream compiles it in; xmlXPathRegisterAllFunctions
6460    // is a no-op since 2.14.0). Core built-ins are served by the static
6461    // lookup_core_function table (Phase 16.5.9) — no per-context copy.
6462    internal.c_context = ctxt;
6463    (*ctxt).extra = Box::into_raw(internal) as *mut c_void;
6464
6465    ctxt
6466}
6467
6468/// Deallocator for `xmlXPathContext.nsHash` payloads (strdup'd namespace
6469/// URIs; upstream `xmlXPathFreeContext` / `xmlXPathRegisteredNsCleanup` pass
6470/// `xmlFree`).
6471pub(crate) unsafe extern "C" fn free_ns_uri_payload(payload: *mut c_void, _key: *mut xmlChar) {
6472    if !payload.is_null() {
6473        crate::abi::allocator::xmlFreeImpl(payload);
6474    }
6475}
6476
6477/// Free an XPath context.
6478///
6479/// # UPSTREAM-PARITY
6480///
6481/// ```c
6482/// void xmlXPathFreeContext(xmlXPathContextPtr ctxt);
6483/// ```
6484#[no_mangle]
6485pub unsafe extern "C" fn xmlXPathFreeContext(ctxt: *mut _xmlXPathContext) {
6486    if ctxt.is_null() {
6487        return;
6488    }
6489    // Drop the internal XPathContext.
6490    if !(*ctxt).extra.is_null() {
6491        let _ = Box::from_raw((*ctxt).extra as *mut XPathContext);
6492        (*ctxt).extra = ptr::null_mut();
6493    }
6494    // Free the registered-namespace hash (upstream xmlXPathFreeContext:
6495    // xmlHashFree(ctxt->nsHash, xmlFree) — the payloads are strdup'd URIs).
6496    if !(*ctxt).nsHash.is_null() {
6497        xmlHashFree((*ctxt).nsHash, Some(free_ns_uri_payload));
6498        (*ctxt).nsHash = ptr::null_mut();
6499    }
6500    // UPSTREAM-PARITY (xpath.c xmlXPathFreeContext): the lastError fields
6501    // are heap-owned by the context (raise_xpath_error strdups the
6502    // expression into str1 and later raises free the previous strings
6503    // before overwriting). Release them here or every failed evaluation
6504    // leaks (Phase 16 ASan fuzz finding: 1 B per error raise).
6505    crate::xml::globals::free_error_strings(&(*ctxt).lastError);
6506    // Free the C ABI context struct.
6507    xmlFreeImpl(ctxt as *mut c_void);
6508}
6509
6510/// Evaluate an XPath expression.
6511///
6512/// # UPSTREAM-PARITY
6513///
6514/// ```c
6515/// xmlXPathObjectPtr xmlXPathEvalExpression(const xmlChar *str,
6516///                                          xmlXPathContextPtr ctxt);
6517/// ```
6518#[no_mangle]
6519pub unsafe extern "C" fn xmlXPathEvalExpression(
6520    str_: *const xmlChar,
6521    ctxt: *mut _xmlXPathContext,
6522) -> *mut _xmlXPathObject {
6523    if str_.is_null() || ctxt.is_null() {
6524        return ptr::null_mut();
6525    }
6526    let expr_str = match CStr::from_ptr(str_ as *const c_char).to_str() {
6527        Ok(s) => s,
6528        Err(_) => return ptr::null_mut(),
6529    };
6530    let internal = (*ctxt).extra as *mut XPathContext;
6531    if internal.is_null() {
6532        return ptr::null_mut();
6533    }
6534    let internal = &mut *internal;
6535    // UPSTREAM-PARITY (xpath.c xmlXPathEvalExpression): the evaluation
6536    // context node and position come from the C context fields — consumers
6537    // (lxml XPathElementEvaluator) set xpathCtxt->node per evaluation — so
6538    // mirror them into the internal context before evaluating. The pre-fix
6539    // code never did, so relative paths ("a", "./a", "a/@i") evaluated
6540    // against a NULL context node and returned an empty node-set (Phase 14
6541    // lxml XPath court).
6542    internal.set_context_node((*ctxt).node);
6543    internal.context_position = (*ctxt).proximityPosition;
6544    internal.context_size = (*ctxt).contextSize;
6545    internal.proximity_position = (*ctxt).proximityPosition;
6546    // Mirror the C context's namespace array (upstream xmlXPathNsLookup
6547    // consults ctxt->namespaces/nsNr first). PHP's DOMXPath fills the array
6548    // with the CONTEXT NODE's in-scope namespaces before evaluating
6549    // (ext/dom xpath.c php_dom_get_in_scope_ns*), so prefixed tests resolve
6550    // without an explicit registerNamespace when the document declares them.
6551    crate::abi::exports_xml2::sync_xpath_context_namespaces(ctxt, internal);
6552    // Clear any stale error so a fresh evaluation either succeeds or records
6553    // its own failure message (the XSLT layer surfaces it verbatim).
6554    internal.clear_error();
6555
6556    match crate::xml::xpath::evaluate_str(expr_str, internal) {
6557        Some(val) => xpath_to_object(val),
6558        None => {
6559            // UPSTREAM-PARITY (xpath.c xmlXPathErrFmt): report the failure
6560            // through the C context (ctxt->lastError pre-fill + ctxt->error
6561            // handler with ctxt->userData) so consumers like lxml's
6562            // _receiveXPathError receive the specific message.
6563            raise_internal_xpath_error(ctxt, internal, expr_str);
6564            ptr::null_mut()
6565        }
6566    }
6567}
6568
6569/// Map an internal XPath error message to its upstream `xmlXPathError` code
6570/// (0-based, xpath.h) and raise it through the C context.
6571pub(crate) unsafe fn raise_internal_xpath_error(
6572    ctxt: *mut _xmlXPathContext,
6573    internal: &mut XPathContext,
6574    expr: &str,
6575) {
6576    let (xpath_code, message) = match internal.error.as_deref() {
6577        Some(m) if m.starts_with("Undefined namespace prefix") => (XPATH_UNDEF_PREFIX_ERROR, m),
6578        Some(m) if m.starts_with("Unregistered function") => (XPATH_UNKNOWN_FUNC_ERROR, m),
6579        Some(m) if m.starts_with("Undefined variable") => (XPATH_UNDEF_VARIABLE_ERROR, m),
6580        // UPSTREAM-PARITY (xpath.c XPATH_RECURSION_LIMIT_EXCEEDED — the
6581        // compile recursion budget, e.g. a 500th nested '(' group): the
6582        // oracle reports "XPath error : Recursion limit exceeded" (verified
6583        // against xmllint 2.15.3).
6584        Some(m) if m == "Recursion limit exceeded" => (XPATH_RECURSION_LIMIT_EXCEEDED, m),
6585        // UPSTREAM-PARITY (xpath.c XPATH_INVALID_TYPE — FilterExpr
6586        // '/'-paths and unions over non-node-sets, e.g. `1/b`): the oracle
6587        // reports "XPath error : Invalid type" (verified against xmllint
6588        // 2.15.3).
6589        Some(m) if m == "Invalid type" => (XPATH_INVALID_TYPE, m),
6590        Some(m) => (XPATH_EXPR_ERROR, m),
6591        None => {
6592            internal.set_error("Invalid expression");
6593            (XPATH_EXPR_ERROR, "Invalid expression")
6594        }
6595    };
6596    unsafe { raise_xpath_error(ctxt, xpath_code, message, expr) }
6597}
6598
6599/// Mirror the C-visible context's `namespaces[0..nsNr]` array into the
6600/// internal XPath context's prefix map (upstream xmlXPathNsLookup consults
6601/// the array before nsHash). PHP's DOMXPath fills the array with the context
6602/// node's in-scope namespaces (ext/dom xpath.c), which is how prefixed
6603/// location steps resolve against the document's declarations.
6604///
6605/// # Safety
6606///
6607/// - `ctxt` must be a valid `_xmlXPathContext`; `internal` its internal
6608///   Rust context (may be shared).
6609pub(crate) unsafe fn sync_xpath_context_namespaces(
6610    ctxt: *mut _xmlXPathContext,
6611    internal: &mut crate::xml::xpath::context::XPathContext,
6612) {
6613    if ctxt.is_null() {
6614        return;
6615    }
6616    unsafe {
6617        let tab = (*ctxt).namespaces;
6618        if tab.is_null() {
6619            return;
6620        }
6621        let count = (*ctxt).nsNr;
6622        if count <= 0 {
6623            return;
6624        }
6625        let mut i = 0;
6626        while i < count {
6627            let ns = *tab.add(i as usize);
6628            if !ns.is_null() && !(*ns).prefix.is_null() && !(*ns).href.is_null() {
6629                let prefix_len = libc::strlen((*ns).prefix as *const libc::c_char) as usize;
6630                let href_len = libc::strlen((*ns).href as *const libc::c_char) as usize;
6631                let prefix =
6632                    String::from_utf8_lossy(core::slice::from_raw_parts((*ns).prefix, prefix_len))
6633                        .into_owned();
6634                let href =
6635                    String::from_utf8_lossy(core::slice::from_raw_parts((*ns).href, href_len))
6636                        .into_owned();
6637                internal.register_namespace(&prefix, &href);
6638            }
6639            i += 1;
6640        }
6641    }
6642}
6643
6644/// UPSTREAM-PARITY (xpath.c `xmlXPathErrFmt` -> error.c `xmlVRaiseError`):
6645/// deliver an XPath compile/eval failure to the C context. Pre-fills
6646/// `ctxt->lastError` (domain/code/level, the expression as `str1`, `int1` =
6647/// 0) exactly like upstream's message-less pre-fill, then raises through the
6648/// shared streamed path: TLS-global storage + dispatch to `ctxt->error`
6649/// (with `ctxt->userData`), the global structured handler, or the generic
6650/// channel (`xmlFormatError` "XPath error : ..." fragments).
6651///
6652/// `code` is the 0-based `xmlXPathError` value (candidate `XPATH_*_ERROR`
6653/// constants); the delivered structured code is offset by
6654/// `XML_XPATH_EXPRESSION_OK` (1200) like upstream.
6655///
6656/// # SAFETY
6657///
6658/// - `ctxt` must be a valid `_xmlXPathContext`.
6659pub(crate) unsafe fn raise_xpath_error(
6660    ctxt: *mut _xmlXPathContext,
6661    code: c_int,
6662    message: &str,
6663    expr: &str,
6664) {
6665    unsafe {
6666        use crate::xml::errors::{raise_error_streamed, GenericDelivery};
6667        use crate::xml::globals;
6668
6669        // Upstream xmlXPathErr / xmlXPathErrFmt always format with a
6670        // trailing newline ("%s\n"); lxml's _LogEntry.message strips one.
6671        let msg_c = CString::new(format!("{}\n", message)).unwrap_or_default();
6672        let expr_c = CString::new(expr).unwrap_or_default();
6673        let xerr_code = code + XML_XPATH_EXPRESSION_OK;
6674
6675        // Upstream pre-fill of ctxt->lastError (xmlXPathErrFmt): domain,
6676        // code, level, str1 = strdup(base), int1 = cur - base. The message
6677        // stays NULL here; xmlVRaiseError writes it into the TLS global.
6678        globals::free_error_strings(&(*ctxt).lastError);
6679        let pre = _xmlError {
6680            domain: XML_FROM_XPATH,
6681            code: xerr_code,
6682            message: ptr::null_mut(),
6683            level: xmlErrorLevel::XML_ERR_ERROR as c_int,
6684            file: ptr::null_mut(),
6685            line: 0,
6686            str1: xmlMemStrdupImpl(expr_c.as_ptr()) as *mut c_char,
6687            str2: ptr::null_mut(),
6688            str3: ptr::null_mut(),
6689            int1: 0,
6690            int2: 0,
6691            ctxt: ctxt as *mut c_void,
6692            node: (*ctxt).debugNode as *mut c_void,
6693        };
6694        ptr::write(&mut (*ctxt).lastError, pre);
6695
6696        // Cross-DSO channel routing: the whole-archive facade layout embeds a
6697        // per-DSO copy of libxml2's TLS error slots, so PHP's
6698        // xmlSetGenericErrorFunc (registered in the core) is invisible to the
6699        // XSLT engine running inside the libxslt facade. Upstream consumers
6700        // register on the xsltGenericError channel for transform-time
6701        // messages (php's ext/xsl MINIT), and that static IS shared with the
6702        // engine — when a real (non-default) handler is installed there,
6703        // deliver the XPath diagnostic through it exactly like the XSLT error
6704        // channel does (xsltproc leaves the default installed and is
6705        // unaffected).
6706        let xslt_bound = {
6707            let extra = (*ctxt).extra;
6708            if extra.is_null() || !crate::xml::xpath::context::has_signature(extra) {
6709                false
6710            } else {
6711                let internal: *mut crate::xml::xpath::context::XPathContext =
6712                    extra as *mut crate::xml::xpath::context::XPathContext;
6713                !(*internal).func_lookup_data.is_null()
6714            }
6715        };
6716        let xslt_global = crate::abi::data_globals::xsltGenericError;
6717        let xslt_default = crate::abi::data_globals::xslt_default_generic_error_func()
6718            .map_or(ptr::null(), |d| d as *const ());
6719        if xslt_bound {
6720            if let Some(g) = xslt_global {
6721                if g as *const () != xslt_default {
6722                    // SAFETY: the channel is a variadic C callback
6723                    // (upstream xmlGenericErrorFunc ABI); the xslt handler
6724                    // special-cases the "%s" format (php xsl_libxslt_error
6725                    // _handler).
6726                    let hv: unsafe extern "C" fn(*mut c_void, *const c_char, ...) =
6727                        core::mem::transmute(g);
6728                    hv(
6729                        crate::abi::data_globals::xsltGenericErrorContext,
6730                        c"%s".as_ptr() as *const c_char,
6731                        msg_c.as_ptr() as *const c_char,
6732                    );
6733                    return;
6734                }
6735            }
6736        }
6737
6738        // UPSTREAM-PARITY (error.c xmlVRaiseError channel selection): the
6739        // XPath raise passes the message VERBATIM to the generic channel —
6740        // `channel(data, "%s", to->message)` — EXCEPT when that channel IS
6741        // the default handler (xmlGenericErrorDefaultFunc / xmlParserError &co),
6742        // which routes through xmlFormatError's fragment stream and prefixes
6743        // "XPath error : " (verified against the 2.15.3 oracle: no-handler
6744        // C caller sees "XPath error : Invalid type"). PHP installs a custom
6745        // generic handler and must keep receiving the text alone
6746        // (ext/simplexml 008: "Invalid expression", not "XPath error :
6747        // Invalid expression"). The pre-fix delivery classified the INSTALLED
6748        // default func as a custom channel, so handler-less callers (xmllint,
6749        // plain C) saw the bare message instead of the formatted stream.
6750        let delivery = match globals::get_generic_error_func() {
6751            Some(f)
6752                if crate::abi::data_globals::default_generic_error_func()
6753                    .map_or(false, |d| d as usize == f as usize) =>
6754            {
6755                GenericDelivery::Stream
6756            }
6757            Some(f) => GenericDelivery::Custom(f, globals::get_generic_error_ctx()),
6758            None => GenericDelivery::Stream,
6759        };
6760
6761        raise_error_streamed(
6762            ctxt as *mut c_void,
6763            XML_FROM_XPATH,
6764            xerr_code,
6765            xmlErrorLevel::XML_ERR_ERROR as c_int,
6766            ptr::null_mut(),
6767            0,
6768            0,
6769            expr_c.as_ptr(),
6770            ptr::null_mut(),
6771            ptr::null_mut(),
6772            0,
6773            msg_c.as_ptr(),
6774            None,
6775            None,
6776            delivery,
6777            None,
6778        );
6779    }
6780}
6781
6782/// Evaluate an XPath expression (simplified alias).
6783///
6784/// # UPSTREAM-PARITY
6785///
6786/// ```c
6787/// xmlXPathObjectPtr xmlXPathEval(const xmlChar *str, xmlXPathContextPtr ctxt);
6788/// ```
6789#[no_mangle]
6790pub unsafe extern "C" fn xmlXPathEval(
6791    str_: *const xmlChar,
6792    ctxt: *mut _xmlXPathContext,
6793) -> *mut _xmlXPathObject {
6794    xmlXPathEvalExpression(str_, ctxt)
6795}
6796
6797/// Free an XPath object.
6798///
6799/// Releases the internal members (string buffer or node-set) and then frees
6800/// the object struct itself.
6801///
6802/// # UPSTREAM-PARITY
6803///
6804/// ```c
6805/// void xmlXPathFreeObject(xmlXPathObjectPtr obj);
6806/// ```
6807#[no_mangle]
6808pub unsafe extern "C" fn xmlXPathFreeObject(obj: *mut _xmlXPathObject) {
6809    if obj.is_null() {
6810        return;
6811    }
6812    let typ = (*obj).type_;
6813    // Free string storage.
6814    if typ == xmlXPathObjectType::XPATH_STRING as c_int && !(*obj).stringval.is_null() {
6815        xmlFreeImpl((*obj).stringval as *mut c_void);
6816        (*obj).stringval = ptr::null_mut();
6817    }
6818    // Free node-set storage.
6819    if typ == xmlXPathObjectType::XPATH_NODESET as c_int {
6820        let ns = (*obj).nodesetval as *mut _xmlNodeSet;
6821        if !ns.is_null() {
6822            if !(*ns).nodeTab.is_null() {
6823                xmlFreeImpl((*ns).nodeTab as *mut c_void);
6824            }
6825            xmlFreeImpl(ns as *mut c_void);
6826        }
6827        (*obj).nodesetval = ptr::null_mut();
6828    }
6829    xmlFreeImpl(obj as *mut c_void);
6830}
6831
6832/// Copy an XPath object (deep copy).
6833///
6834/// # UPSTREAM-PARITY
6835///
6836/// ```c
6837/// xmlXPathObjectPtr xmlXPathObjectCopy(xmlXPathObjectPtr val);
6838/// ```
6839///
6840/// Oracle behavior: returns a newly allocated object with the same type
6841/// and value. Node-sets are copied element-by-element; strings are
6842/// duplicated; numbers and booleans are copied by value.
6843#[no_mangle]
6844pub unsafe extern "C" fn xmlXPathObjectCopy(val: *mut _xmlXPathObject) -> *mut _xmlXPathObject {
6845    if val.is_null() {
6846        return ptr::null_mut();
6847    }
6848    let typ = (*val).type_;
6849    let obj = xmlMallocZero(size_of::<_xmlXPathObject>()) as *mut _xmlXPathObject;
6850    if obj.is_null() {
6851        return ptr::null_mut();
6852    }
6853    (*obj).type_ = typ;
6854    if typ == xmlXPathObjectType::XPATH_NODESET as c_int {
6855        let src_ns = (*val).nodesetval as *mut _xmlNodeSet;
6856        if !src_ns.is_null() {
6857            let nr = (*src_ns).nodeNr;
6858            let ns = xmlMallocZero(size_of::<_xmlNodeSet>()) as *mut _xmlNodeSet;
6859            if ns.is_null() {
6860                xmlFreeImpl(obj as *mut c_void);
6861                return ptr::null_mut();
6862            }
6863            (*ns).nodeNr = nr;
6864            (*ns).nodeMax = nr;
6865            if nr > 0 && !(*src_ns).nodeTab.is_null() {
6866                let tab = xmlMallocImpl((nr as usize) * core::mem::size_of::<*mut _xmlNode>())
6867                    as *mut *mut _xmlNode;
6868                if tab.is_null() {
6869                    xmlFreeImpl(ns as *mut c_void);
6870                    xmlFreeImpl(obj as *mut c_void);
6871                    return ptr::null_mut();
6872                }
6873                ptr::copy_nonoverlapping((*src_ns).nodeTab, tab, nr as usize);
6874                (*ns).nodeTab = tab;
6875            } else {
6876                (*ns).nodeTab = ptr::null_mut();
6877            }
6878            (*obj).nodesetval = ns as *mut c_void;
6879        }
6880    } else if typ == xmlXPathObjectType::XPATH_BOOLEAN as c_int {
6881        (*obj).boolval = (*val).boolval;
6882    } else if typ == xmlXPathObjectType::XPATH_NUMBER as c_int {
6883        (*obj).floatval = (*val).floatval;
6884    } else if typ == xmlXPathObjectType::XPATH_STRING as c_int {
6885        let src = (*val).stringval;
6886        if !src.is_null() {
6887            let len = libc::strlen(src as *const libc::c_char);
6888            let buf = xmlMallocImpl(len + 1) as *mut xmlChar;
6889            if !buf.is_null() {
6890                ptr::copy_nonoverlapping(src, buf, len);
6891                *buf.add(len) = 0;
6892            }
6893            (*obj).stringval = buf;
6894        }
6895    }
6896    obj
6897}
6898
6899/// Cast an XPath object to its string value.
6900///
6901/// Returns a newly allocated string (caller frees with `xmlFree`).
6902///
6903/// # UPSTREAM-PARITY
6904///
6905/// ```c
6906/// xmlChar *xmlXPathCastToString(xmlXPathObjectPtr val);
6907/// ```
6908#[no_mangle]
6909pub unsafe extern "C" fn xmlXPathCastToString(val: *mut _xmlXPathObject) -> *mut xmlChar {
6910    if val.is_null() {
6911        return ptr::null_mut();
6912    }
6913    let typ = (*val).type_;
6914    let mut result: Vec<u8> = Vec::new();
6915    if typ == xmlXPathObjectType::XPATH_STRING as c_int {
6916        if !(*val).stringval.is_null() {
6917            let len = libc::strlen((*val).stringval as *const libc::c_char);
6918            result.extend_from_slice(core::slice::from_raw_parts((*val).stringval, len));
6919        }
6920    } else if typ == xmlXPathObjectType::XPATH_NUMBER as c_int {
6921        // Number → string conversion per XPath 1.0 §4.2:
6922        // - NaN → "NaN"
6923        // - +0/-0 → "0"
6924        // - infinity → "Infinity" / "-Infinity"
6925        // - integer → decimal representation without exponent
6926        let n = (*val).floatval;
6927        result.extend_from_slice(xml_number_to_string(n).as_bytes());
6928    } else if typ == xmlXPathObjectType::XPATH_BOOLEAN as c_int {
6929        result.extend_from_slice(if (*val).boolval != 0 {
6930            b"true"
6931        } else {
6932            b"false"
6933        });
6934    } else if typ == xmlXPathObjectType::XPATH_NODESET as c_int {
6935        // String value of a node-set is the string value of the first node
6936        // in document order (or empty if empty).
6937        let ns = (*val).nodesetval as *mut _xmlNodeSet;
6938        if !ns.is_null() && (*ns).nodeNr > 0 && !(*ns).nodeTab.is_null() {
6939            let node = *(*ns).nodeTab;
6940            if !node.is_null() {
6941                let content = crate::xml::tree::node_get_content(node);
6942                if !content.is_null() {
6943                    let len = libc::strlen(content as *const libc::c_char);
6944                    result.extend_from_slice(core::slice::from_raw_parts(content, len));
6945                    xmlFreeImpl(content as *mut c_void);
6946                }
6947            }
6948        }
6949    }
6950    // Allocate the C string.
6951    let buf = xmlMallocImpl(result.len() + 1) as *mut xmlChar;
6952    if buf.is_null() {
6953        return ptr::null_mut();
6954    }
6955    if !result.is_empty() {
6956        ptr::copy_nonoverlapping(result.as_ptr(), buf, result.len());
6957    }
6958    *buf.add(result.len()) = 0;
6959    buf
6960}
6961
6962/// Convert an XPath number to its string representation (XPath 1.0 §4.2).
6963///
6964/// Canonical implementation lives in `crate::xml::xpath::types::number_to_string`
6965/// (a port of upstream `xmlXPathCastNumberToString` / `xmlXPathFormatNumber`,
6966/// R-000166); this ABI helper delegates so every number→string conversion
6967/// shares exactly one oracle-verified code path.
6968pub fn xml_number_to_string(n: f64) -> String {
6969    crate::xml::xpath::types::number_to_string(n)
6970}
6971
6972/// Port of upstream xpath.c `xmlXPathStringEvalNumber` (R-000166): see
6973/// `crate::xml::xpath::types::string_bytes_to_number` — the oracle
6974/// accumulates digits directly, caps the fraction at MAX_FRAC=20 digits
6975/// after any leading zeros, applies the exponent with `pow(10.0, exp)`
6976/// (underflowing to 0 below the smallest subnormal), accepts XML whitespace
6977/// around the number, and returns NaN for anything else — including a
6978/// leading '+'.
6979fn xpath_string_eval_number(bytes: &[u8]) -> f64 {
6980    crate::xml::xpath::types::string_bytes_to_number(bytes)
6981}
6982
6983/// Cast a C string to a number per XPath 1.0 §4.2 conversion rules.
6984///
6985/// # UPSTREAM-PARITY
6986///
6987/// ```c
6988/// double xmlXPathCastStringToNumber(const xmlChar *val);
6989/// ```
6990#[no_mangle]
6991pub unsafe extern "C" fn xmlXPathCastStringToNumber(val: *const xmlChar) -> f64 {
6992    if val.is_null() {
6993        return f64::NAN;
6994    }
6995    let len = libc::strlen(val as *const libc::c_char);
6996    let bytes = core::slice::from_raw_parts(val, len);
6997    xpath_string_eval_number(bytes)
6998}
6999
7000/// Compare two nodes in document order.
7001///
7002/// UPSTREAM-PARITY (xpath.c `xmlXPathCmpNodes`): returns **1** when
7003/// `node1` precedes `node2` in document order, **-1** when `node1` follows
7004/// `node2`, 0 for the same node, and -2 for NULL or cross-document
7005/// comparisons. The sign convention was verified against the system oracle
7006/// (libxml2 2.15.3): `xmlXPathCmpNodes(book1, book2)` returns 1.
7007///
7008/// # UPSTREAM-PARITY
7009///
7010/// ```c
7011/// int xmlXPathCmpNodes(xmlNodePtr node1, xmlNodePtr node2);
7012/// ```
7013#[no_mangle]
7014pub unsafe extern "C" fn xmlXPathCmpNodes(node1: *mut _xmlNode, node2: *mut _xmlNode) -> c_int {
7015    if node1.is_null() || node2.is_null() {
7016        return -2;
7017    }
7018    if node1 == node2 {
7019        return 0;
7020    }
7021    // Build ancestor chains.
7022    let mut chain1: Vec<*mut _xmlNode> = Vec::new();
7023    let mut chain2: Vec<*mut _xmlNode> = Vec::new();
7024    let mut n = node1;
7025    while !n.is_null() {
7026        chain1.push(n);
7027        n = (*n).parent;
7028    }
7029    let mut n = node2;
7030    while !n.is_null() {
7031        chain2.push(n);
7032        n = (*n).parent;
7033    }
7034    // Distinct documents (or entities) case.
7035    if chain1[chain1.len() - 1] != chain2[chain2.len() - 1] {
7036        return -2;
7037    }
7038    // Find the nearest common ancestor.
7039    let mut i = chain1.len();
7040    let mut j = chain2.len();
7041    while i > 0 && j > 0 && chain1[i - 1] == chain2[j - 1] {
7042        i -= 1;
7043        j -= 1;
7044    }
7045    // node1 is an ancestor of node2 -> node1 precedes it -> 1.
7046    if i == 0 {
7047        return 1;
7048    }
7049    // node2 is an ancestor of node1 -> node1 follows it -> -1.
7050    if j == 0 {
7051        return -1;
7052    }
7053    // Compare sibling order at the divergence point.
7054    let mut a = chain1[i - 1];
7055    let mut b = chain2[j - 1];
7056    // Climb to the same level.
7057    while !a.is_null() && !b.is_null() {
7058        let pa = (*a).parent;
7059        let pb = (*b).parent;
7060        if pa == pb {
7061            break;
7062        }
7063        a = pa;
7064        b = pb;
7065    }
7066    // Walk forward from the first child of the common parent.
7067    let parent = (*a).parent;
7068    let mut child = if parent.is_null() {
7069        ptr::null_mut()
7070    } else {
7071        (*parent).children
7072    };
7073    while !child.is_null() {
7074        if child == a {
7075            return 1; // a precedes b
7076        }
7077        if child == b {
7078            return -1; // b precedes a
7079        }
7080        child = (*child).next;
7081    }
7082    0
7083}
7084
7085/// Create a node-set from a range of an existing node-set.
7086///
7087/// # UPSTREAM-PARITY
7088///
7089/// ```c
7090/// xmlNodeSetPtr xmlXPathNodeSetCreate(xmlNodePtr val);
7091/// ```
7092///
7093/// With a null `val`, creates an empty node-set.
7094#[no_mangle]
7095pub unsafe extern "C" fn xmlXPathNodeSetCreate(val: *mut _xmlNode) -> *mut _xmlNodeSet {
7096    let ns = xmlMallocZero(size_of::<_xmlNodeSet>()) as *mut _xmlNodeSet;
7097    if ns.is_null() {
7098        return ptr::null_mut();
7099    }
7100    if val.is_null() {
7101        return ns;
7102    }
7103    let tab = xmlMallocImpl(core::mem::size_of::<*mut _xmlNode>()) as *mut *mut _xmlNode;
7104    if tab.is_null() {
7105        xmlFreeImpl(ns as *mut c_void);
7106        return ptr::null_mut();
7107    }
7108    *tab = val;
7109    (*ns).nodeTab = tab;
7110    (*ns).nodeNr = 1;
7111    (*ns).nodeMax = 1;
7112    ns
7113}
7114
7115/// Free a node-set allocated by `xmlXPathNodeSetCreate` or a node-set
7116/// builder in this library.
7117///
7118/// Frees the node-set structure and its node table; the nodes themselves
7119/// are owned by their document and are not freed.
7120///
7121/// # UPSTREAM-PARITY
7122///
7123/// ```c
7124/// void xmlXPathFreeNodeSet(xmlNodeSetPtr ns);
7125/// ```
7126#[no_mangle]
7127pub unsafe extern "C" fn xmlXPathFreeNodeSet(ns: *mut _xmlNodeSet) {
7128    if ns.is_null() {
7129        return;
7130    }
7131    if !(*ns).nodeTab.is_null() {
7132        xmlFreeImpl((*ns).nodeTab as *mut c_void);
7133        (*ns).nodeTab = ptr::null_mut();
7134    }
7135    (*ns).nodeNr = 0;
7136    (*ns).nodeMax = 0;
7137    xmlFreeImpl(ns as *mut c_void);
7138}
7139
7140/// Compile `expr_str` and store it in the compiled-expression registry,
7141/// returning the opaque key (or the parse error without raising — the caller
7142/// decides delivery).
7143pub(crate) fn xpath_compile_store(
7144    expr_str: &str,
7145) -> Result<*mut c_void, crate::xml::xpath::parser::ParseError> {
7146    match crate::xml::xpath::compile_result(expr_str) {
7147        Ok(compiled) => {
7148            let mut map = COMPILED_EXPRS.lock();
7149            let mut counter = NEXT_COMPILED_KEY.lock();
7150            let key = *counter;
7151            *counter += 1;
7152            map.insert(key, Box::new(compiled));
7153            Ok(key as *mut c_void)
7154        }
7155        Err(e) => Err(e),
7156    }
7157}
7158
7159/// Compile an XPath expression.
7160///
7161/// # UPSTREAM-PARITY
7162///
7163/// ```c
7164/// xmlXPathCompExprPtr xmlXPathCompile(const xmlChar *str);
7165/// ```
7166#[no_mangle]
7167pub unsafe extern "C" fn xmlXPathCompile(str_: *const xmlChar) -> *mut c_void {
7168    if str_.is_null() {
7169        return ptr::null_mut();
7170    }
7171    let expr_str = match CStr::from_ptr(str_ as *const c_char).to_str() {
7172        Ok(s) => s,
7173        Err(_) => return ptr::null_mut(),
7174    };
7175
7176    match xpath_compile_store(expr_str) {
7177        Ok(key) => key,
7178        Err(e) => {
7179            // UPSTREAM-PARITY (xpath.c xmlXPathErrFmt): a failed compile
7180            // reports "XPath error : Invalid expression\n" plus the
7181            // expression and a caret at the error offset through the generic
7182            // channel (HOSTILE-FAILURE F3). The structured code is
7183            // 1200-based like upstream `code + XML_XPATH_EXPRESSION_OK`.
7184            let msg_cstr = std::ffi::CString::new("Invalid expression\n").unwrap_or_default();
7185            let expr_cstr = std::ffi::CString::new(expr_str).unwrap_or_default();
7186            let off = e.pos;
7187            let window = if off < 100 && off < expr_str.len() {
7188                Some((expr_str.as_bytes(), off))
7189            } else {
7190                None
7191            };
7192            unsafe {
7193                crate::xml::errors::raise_error_streamed(
7194                    ptr::null_mut(),
7195                    crate::abi::types::XML_FROM_XPATH,
7196                    crate::abi::types::XPATH_EXPR_ERROR
7197                        + crate::abi::types::XML_XPATH_EXPRESSION_OK,
7198                    crate::abi::types::xmlErrorLevel::XML_ERR_ERROR as c_int,
7199                    ptr::null(),
7200                    0,
7201                    0,
7202                    expr_cstr.as_ptr(),
7203                    ptr::null(),
7204                    ptr::null(),
7205                    off as c_int,
7206                    msg_cstr.as_ptr(),
7207                    window,
7208                    None,
7209                    crate::xml::errors::GenericDelivery::Stream,
7210                    None,
7211                );
7212            }
7213            ptr::null_mut()
7214        }
7215    }
7216}
7217
7218/// Free a compiled XPath expression.
7219///
7220/// # UPSTREAM-PARITY
7221///
7222/// ```c
7223/// void xmlXPathFreeCompExpr(xmlXPathCompExprPtr comp);
7224/// ```
7225#[no_mangle]
7226pub unsafe extern "C" fn xmlXPathFreeCompExpr(comp: *mut c_void) {
7227    if comp.is_null() {
7228        return;
7229    }
7230    let mut map = COMPILED_EXPRS.lock();
7231    map.remove(&(comp as u64));
7232}
7233
7234/// Register an XPath namespace.
7235///
7236/// # UPSTREAM-PARITY
7237///
7238/// ```c
7239/// int xmlXPathRegisterNs(xmlXPathContextPtr ctxt,
7240///                        const xmlChar *prefix, const xmlChar *ns_uri);
7241/// ```
7242#[no_mangle]
7243pub unsafe extern "C" fn xmlXPathRegisterNs(
7244    ctxt: *mut _xmlXPathContext,
7245    prefix: *const xmlChar,
7246    ns_uri: *const xmlChar,
7247) -> c_int {
7248    if ctxt.is_null() || prefix.is_null() || ns_uri.is_null() {
7249        return -1;
7250    }
7251    let internal = (*ctxt).extra as *mut XPathContext;
7252    if internal.is_null() {
7253        return -1;
7254    }
7255    let internal = &mut *internal;
7256
7257    let prefix_str = match CStr::from_ptr(prefix as *const c_char).to_str() {
7258        Ok(s) => s,
7259        Err(_) => return -1,
7260    };
7261    let uri_str = match CStr::from_ptr(ns_uri as *const c_char).to_str() {
7262        Ok(s) => s,
7263        Err(_) => return -1,
7264    };
7265
7266    internal.register_namespace(prefix_str, uri_str);
7267
7268    // UPSTREAM-PARITY (xpath.c xmlXPathRegisterNs): the C context's nsHash
7269    // is a REAL xmlHashTable keyed by prefix with a strdup'd URI payload —
7270    // C consumers (lxml registerExsltFunctions) call xmlHashScan and
7271    // xmlHashLookup on it, so it must be an xmlHashTable, not a Rust map.
7272    // (R-00019x: the pre-fix nsHash held a Box<HashMap<..>>; lxml's
7273    // xmlHashScan interpreted the HashMap as an xmlHashTable and crashed on
7274    // its internal layout.)
7275    if (*ctxt).nsHash.is_null() {
7276        (*ctxt).nsHash = xmlHashCreate(10);
7277    }
7278    if !(*ctxt).nsHash.is_null() {
7279        let uri_dup = crate::xml::string::xml_strdup(ns_uri);
7280        let rc = xmlHashAddEntry((*ctxt).nsHash, prefix, uri_dup as *mut c_void);
7281        if rc != 0 {
7282            // Duplicate prefix: upstream keeps the first mapping (and leaks
7283            // the new strdup'd URI); the candidate frees the unused copy.
7284            crate::abi::allocator::xmlFreeImpl(uri_dup as *mut c_void);
7285        }
7286    }
7287    0
7288}
7289
7290/// Register an XPath function.
7291///
7292/// The C function pointer is stored in a side table keyed by the context.
7293/// A Rust-side stub is registered in the internal context so that the Rust
7294/// evaluator is aware of the function; however, calling the C function
7295/// directly from the Rust evaluator is not yet supported.
7296///
7297/// # UPSTREAM-PARITY
7298///
7299/// ```c
7300/// int xmlXPathRegisterFunc(xmlXPathContextPtr ctxt,
7301///                          const xmlChar *name, xmlXPathFunction f);
7302/// ```
7303#[no_mangle]
7304pub unsafe extern "C" fn xmlXPathRegisterFunc(
7305    ctxt: *mut _xmlXPathContext,
7306    name: *const xmlChar,
7307    f: Option<unsafe extern "C" fn(*mut c_void, c_int)>,
7308) -> c_int {
7309    if ctxt.is_null() || name.is_null() {
7310        return -1;
7311    }
7312    let internal = (*ctxt).extra as *mut XPathContext;
7313    if internal.is_null() {
7314        return -1;
7315    }
7316    let internal = &mut *internal;
7317
7318    let name_str = match CStr::from_ptr(name as *const c_char).to_str() {
7319        Ok(s) => s,
7320        Err(_) => return -1,
7321    };
7322
7323    if let Some(func) = f {
7324        // Store the C function pointer in the side table.
7325        let key = (SendSyncPtr((*ctxt).extra), name_str.to_string());
7326        C_FUNCTIONS.lock().insert(key, func);
7327        // Register a Rust closure that bridges to the C function through a
7328        // synthesized xmlXPathParserContext (upstream function-call ABI).
7329        let c_ctxt = SendSyncPtr(ctxt as *mut c_void);
7330        let name_owned = name_str.to_string();
7331        internal.register_function(name_str, c_func_bridge_closure(c_ctxt, name_owned));
7332    }
7333    0
7334}
7335
7336/// Register an XPath function with namespace.
7337///
7338/// # UPSTREAM-PARITY
7339///
7340/// ```c
7341/// int xmlXPathRegisterFuncNS(xmlXPathContextPtr ctxt,
7342///                            const xmlChar *name, const xmlChar *ns_uri,
7343///                            xmlXPathFunction f);
7344/// ```
7345#[no_mangle]
7346pub unsafe extern "C" fn xmlXPathRegisterFuncNS(
7347    ctxt: *mut _xmlXPathContext,
7348    name: *const xmlChar,
7349    ns_uri: *const xmlChar,
7350    f: Option<unsafe extern "C" fn(*mut c_void, c_int)>,
7351) -> c_int {
7352    if ctxt.is_null() || name.is_null() {
7353        return -1;
7354    }
7355    let internal = (*ctxt).extra as *mut XPathContext;
7356    if internal.is_null() {
7357        return -1;
7358    }
7359    let internal = &mut *internal;
7360
7361    let name_str = match CStr::from_ptr(name as *const c_char).to_str() {
7362        Ok(s) => s,
7363        Err(_) => return -1,
7364    };
7365    let ns_str = if ns_uri.is_null() {
7366        String::new()
7367    } else {
7368        match CStr::from_ptr(ns_uri as *const c_char).to_str() {
7369            Ok(s) => s.to_string(),
7370            Err(_) => return -1,
7371        }
7372    };
7373
7374    // Use "{ns}:" prefix as part of the key to keep functions unique.
7375    let qualified = if ns_str.is_empty() {
7376        name_str.to_string()
7377    } else {
7378        format!("{{{}}}{}", ns_str, name_str)
7379    };
7380
7381    if let Some(func) = f {
7382        let key = (SendSyncPtr((*ctxt).extra), qualified.clone());
7383        C_FUNCTIONS.lock().insert(key, func);
7384        let c_ctxt = SendSyncPtr(ctxt as *mut c_void);
7385        let qualified_owned = qualified.clone();
7386        internal.register_function(&qualified, c_func_bridge_closure(c_ctxt, qualified_owned));
7387    }
7388    0
7389}
7390
7391/// Register an XPath variable.
7392///
7393/// # UPSTREAM-PARITY
7394///
7395/// ```c
7396/// int xmlXPathRegisterVariable(xmlXPathContextPtr ctxt,
7397///                              const xmlChar *name, xmlXPathObjectPtr value);
7398/// ```
7399#[no_mangle]
7400pub unsafe extern "C" fn xmlXPathRegisterVariable(
7401    ctxt: *mut _xmlXPathContext,
7402    name: *const xmlChar,
7403    value: *mut _xmlXPathObject,
7404) -> c_int {
7405    if ctxt.is_null() || name.is_null() || value.is_null() {
7406        return -1;
7407    }
7408    let internal = (*ctxt).extra as *mut XPathContext;
7409    if internal.is_null() {
7410        return -1;
7411    }
7412    let internal = &mut *internal;
7413
7414    let name_str = match CStr::from_ptr(name as *const c_char).to_str() {
7415        Ok(s) => s,
7416        Err(_) => return -1,
7417    };
7418
7419    let xpath_val = object_to_xpathvalue(value);
7420    internal.register_variable(name_str, xpath_val);
7421    0
7422}
7423
7424/// Create an XPath object wrapping a single node in a node-set.
7425///
7426/// # UPSTREAM-PARITY
7427///
7428/// ```c
7429/// xmlXPathObjectPtr xmlXPathNewNodeSet(xmlNodePtr val);
7430/// ```
7431#[no_mangle]
7432pub unsafe extern "C" fn xmlXPathNewNodeSet(val: *mut _xmlNode) -> *mut _xmlXPathObject {
7433    let ns = if val.is_null() {
7434        NodeSet::new()
7435    } else {
7436        NodeSet::singleton(val)
7437    };
7438    xpath_to_object(XPathValue::NodeSet(ns))
7439}
7440
7441/// Create an XPath object from a C string value.
7442///
7443/// # UPSTREAM-PARITY
7444///
7445/// ```c
7446/// xmlXPathObjectPtr xmlXPathNewCString(const xmlChar *val);
7447/// ```
7448#[no_mangle]
7449pub unsafe extern "C" fn xmlXPathNewCString(val: *const xmlChar) -> *mut _xmlXPathObject {
7450    if val.is_null() {
7451        return xpath_to_object(XPathValue::String(String::new()));
7452    }
7453    let s = match CStr::from_ptr(val as *const c_char).to_str() {
7454        Ok(s) => s.to_string(),
7455        Err(_) => return ptr::null_mut(),
7456    };
7457    xpath_to_object(XPathValue::String(s))
7458}
7459
7460/// Create an XPath number object.
7461///
7462/// # UPSTREAM-PARITY
7463///
7464/// ```c
7465/// xmlXPathObjectPtr xmlXPathNewFloat(double val);
7466/// ```
7467#[no_mangle]
7468pub extern "C" fn xmlXPathNewFloat(val: f64) -> *mut _xmlXPathObject {
7469    unsafe { xpath_to_object(XPathValue::Number(val)) }
7470}
7471
7472/// Create an XPath boolean object.
7473///
7474/// # UPSTREAM-PARITY
7475///
7476/// ```c
7477/// xmlXPathObjectPtr xmlXPathNewBoolean(int val);
7478/// ```
7479#[no_mangle]
7480pub extern "C" fn xmlXPathNewBoolean(val: c_int) -> *mut _xmlXPathObject {
7481    unsafe { xpath_to_object(XPathValue::Boolean(val != 0)) }
7482}
7483
7484// ═══════════════════════════════════════════════════════════════════════════════
7485// 14.5. XPointer
7486// ═══════════════════════════════════════════════════════════════════════════════
7487
7488/// Evaluate an XPointer expression.
7489///
7490/// Delegates to the xpointer module.
7491///
7492/// # UPSTREAM-PARITY
7493///
7494/// ```c
7495/// xmlNodePtr xmlXPtrEval(const xmlChar *expr, xmlDocPtr doc);
7496/// ```
7497#[no_mangle]
7498pub unsafe extern "C" fn xmlXPtrEval(expr: *const c_char, doc: *mut _xmlDoc) -> *mut _xmlNode {
7499    crate::xml::xpointer::xmlXPtrEval(expr, doc)
7500}
7501
7502// ═══════════════════════════════════════════════════════════════════════════════
7503// 15. XInclude
7504// ═══════════════════════════════════════════════════════════════════════════════
7505
7506/// Process XInclude nodes in a document.
7507///
7508/// # UPSTREAM-PARITY
7509///
7510/// ```c
7511/// int xmlXIncludeProcess(xmlDocPtr doc);
7512/// ```
7513#[no_mangle]
7514pub unsafe extern "C" fn xmlXIncludeProcess(doc: *mut _xmlDoc) -> c_int {
7515    crate::xml::xinclude::xinclude_process(doc)
7516}
7517
7518/// Process XInclude nodes with flags.
7519///
7520/// # UPSTREAM-PARITY
7521///
7522/// ```c
7523/// int xmlXIncludeProcessFlags(xmlDocPtr doc, int flags);
7524/// ```
7525#[no_mangle]
7526pub unsafe extern "C" fn xmlXIncludeProcessFlags(doc: *mut _xmlDoc, flags: c_int) -> c_int {
7527    crate::xml::xinclude::xinclude_process_flags(doc, flags)
7528}
7529
7530// ═══════════════════════════════════════════════════════════════════════════════
7531// 16. Catalog
7532// ═══════════════════════════════════════════════════════════════════════════════
7533
7534/// Load a catalog.
7535///
7536/// # UPSTREAM-PARITY
7537///
7538/// ```c
7539/// xmlCatalogPtr xmlCatalogLoad(const char *catalogs);
7540/// ```
7541#[no_mangle]
7542pub extern "C" fn xmlCatalogLoad(catalogs: *const c_char) -> *mut c_void {
7543    if catalogs.is_null() {
7544        return ptr::null_mut();
7545    }
7546    crate::xml::catalog::load_catalog(catalogs)
7547}
7548
7549/// Resolve a public ID.
7550///
7551/// # UPSTREAM-PARITY
7552///
7553/// ```c
7554/// xmlCharPtr xmlCatalogResolvePublic(const xmlChar *pubID);
7555/// ```
7556#[no_mangle]
7557pub unsafe extern "C" fn xmlCatalogResolvePublic(pubID: *const xmlChar) -> *mut xmlChar {
7558    if pubID.is_null() {
7559        return ptr::null_mut();
7560    }
7561    crate::xml::catalog::resolve_public(pubID)
7562}
7563
7564/// Resolve a system ID.
7565///
7566/// # UPSTREAM-PARITY
7567///
7568/// ```c
7569/// xmlCharPtr xmlCatalogResolveSystem(const xmlChar *sysID);
7570/// ```
7571#[no_mangle]
7572pub unsafe extern "C" fn xmlCatalogResolveSystem(sysID: *const xmlChar) -> *mut xmlChar {
7573    if sysID.is_null() {
7574        return ptr::null_mut();
7575    }
7576    crate::xml::catalog::resolve_system(sysID)
7577}
7578
7579/// Resolve a URI.
7580///
7581/// # UPSTREAM-PARITY
7582///
7583/// ```c
7584/// xmlCharPtr xmlCatalogResolveURI(const xmlChar *URI);
7585/// ```
7586#[no_mangle]
7587pub unsafe extern "C" fn xmlCatalogResolveURI(URI: *const xmlChar) -> *mut xmlChar {
7588    if URI.is_null() {
7589        return ptr::null_mut();
7590    }
7591    crate::xml::catalog::resolve_uri(URI)
7592}
7593
7594/// Set catalog defaults.
7595///
7596/// # UPSTREAM-PARITY
7597///
7598/// ```c
7599/// void xmlCatalogSetDefaults(xmlCatalogAllowValue allow);
7600/// ```
7601#[no_mangle]
7602pub extern "C" fn xmlCatalogSetDefaults(allow: c_int) {
7603    crate::xml::catalog::set_defaults(allow)
7604}
7605
7606/// Get catalog defaults.
7607///
7608/// # UPSTREAM-PARITY
7609///
7610/// ```c
7611/// xmlCatalogAllowValue xmlCatalogGetDefaults(void);
7612/// ```
7613#[no_mangle]
7614pub extern "C" fn xmlCatalogGetDefaults() -> c_int {
7615    crate::xml::catalog::get_defaults()
7616}
7617
7618/// Add a catalog.
7619///
7620/// # UPSTREAM-PARITY
7621///
7622/// ```c
7623/// int xmlCatalogAdd(const xmlChar *type, const xmlChar *orig, const xmlChar *replace);
7624/// ```
7625#[no_mangle]
7626pub unsafe extern "C" fn xmlCatalogAdd(
7627    type_: *const xmlChar,
7628    orig: *const xmlChar,
7629    replace: *const xmlChar,
7630) -> c_int {
7631    if type_.is_null() || orig.is_null() || replace.is_null() {
7632        return -1;
7633    }
7634    crate::xml::catalog::add(type_, orig, replace)
7635}
7636
7637/// Remove a catalog entry.
7638///
7639/// # UPSTREAM-PARITY
7640///
7641/// ```c
7642/// int xmlCatalogRemove(const xmlChar *value);
7643/// ```
7644#[no_mangle]
7645pub unsafe extern "C" fn xmlCatalogRemove(value: *const xmlChar) -> c_int {
7646    if value.is_null() {
7647        return 0;
7648    }
7649    crate::xml::catalog::remove(value)
7650}
7651
7652/// Dump the catalog in XML format to a FILE* (upstream catalog.h:
7653/// 1-argument form — R-000176, the candidate previously took a spurious
7654/// second `xmlCatalogPtr` argument).
7655///
7656/// # UPSTREAM-PARITY
7657///
7658/// ```c
7659/// void xmlCatalogDump(FILE *out);
7660/// ```
7661#[no_mangle]
7662pub unsafe extern "C" fn xmlCatalogDump(output: *mut c_void) {
7663    if output.is_null() {
7664        return;
7665    }
7666    let doc = crate::xml::catalog::dump_doc();
7667    if doc.is_null() {
7668        return;
7669    }
7670    let mut mem: *mut xmlChar = ptr::null_mut();
7671    let mut size: c_int = 0;
7672    crate::xml::tree::xmlDocDumpFormatMemory(doc, &mut mem, &mut size, 1);
7673    if !mem.is_null() {
7674        libc::fwrite(
7675            mem as *const c_void,
7676            1,
7677            size as usize,
7678            output as *mut libc::FILE,
7679        );
7680        xmlFreeImpl(mem as *mut c_void);
7681    }
7682    crate::xml::tree::free_doc(doc);
7683}
7684
7685/// Save the catalog to a file (upstream `xmlCatalogSave`).
7686///
7687/// Returns 0 on success, -1 on failure.
7688///
7689/// # UPSTREAM-PARITY
7690///
7691/// ```c
7692/// int xmlCatalogSave(const char *filename);
7693/// ```
7694#[no_mangle]
7695pub unsafe extern "C" fn xmlCatalogSave(filename: *const c_char) -> c_int {
7696    if filename.is_null() {
7697        return -1;
7698    }
7699    let doc = crate::xml::catalog::dump_doc();
7700    if doc.is_null() {
7701        return -1;
7702    }
7703    let mut mem: *mut xmlChar = ptr::null_mut();
7704    let mut size: c_int = 0;
7705    crate::xml::tree::xmlDocDumpFormatMemory(doc, &mut mem, &mut size, 1);
7706    let mut ret: c_int = -1;
7707    if !mem.is_null() {
7708        let fp = libc::fopen(filename, c"w".as_ptr() as *const c_char);
7709        if !fp.is_null() {
7710            let written = libc::fwrite(mem as *const c_void, 1, size as usize, fp);
7711            ret = if written == size as usize { 0 } else { -1 };
7712            libc::fclose(fp);
7713        }
7714        xmlFreeImpl(mem as *mut c_void);
7715    }
7716    crate::xml::tree::free_doc(doc);
7717    ret
7718}
7719
7720/// Clean up the catalog subsystem.
7721///
7722/// # UPSTREAM-PARITY
7723///
7724/// ```c
7725/// void xmlCatalogCleanup(void);
7726/// ```
7727#[no_mangle]
7728pub extern "C" fn xmlCatalogCleanup() {
7729    crate::xml::catalog::cleanup();
7730}
7731
7732/// Convert all the SGML catalog entries as XML ones (upstream catalog.c:
7733/// returns 0 on success, -1 otherwise — R-000176, the candidate previously
7734/// returned a dump document).
7735///
7736/// # UPSTREAM-PARITY
7737///
7738/// ```c
7739/// int xmlCatalogConvert(void);
7740/// ```
7741///
7742/// The candidate's catalog stores XML-flavored entries natively (there is no
7743/// separate SGML table to convert), so once the catalog is initialized the
7744/// conversion succeeds as a no-op, matching upstream's successful-return
7745/// contract.
7746#[no_mangle]
7747pub extern "C" fn xmlCatalogConvert() -> c_int {
7748    if !crate::xml::catalog::is_initialized() {
7749        return -1;
7750    }
7751    0
7752}
7753
7754// ═══════════════════════════════════════════════════════════════════════════════
7755// 17. HTML
7756// ═══════════════════════════════════════════════════════════════════════════════
7757
7758/// Parse an HTML document from a file.
7759///
7760/// # UPSTREAM-PARITY
7761///
7762/// ```c
7763/// htmlDocPtr htmlParseFile(const char *filename, const char *encoding);
7764/// ```
7765#[no_mangle]
7766pub const unsafe extern "C" fn htmlParseFile(
7767    _filename: *const c_char,
7768    _encoding: *const c_char,
7769) -> *mut _xmlDoc {
7770    // Phase 1: STUB
7771    ptr::null_mut()
7772}
7773
7774/// Parse an HTML document from memory.
7775///
7776/// # UPSTREAM-PARITY
7777///
7778/// ```c
7779/// htmlDocPtr htmlParseMemory(const char *buffer, int size);
7780/// ```
7781#[no_mangle]
7782pub const unsafe extern "C" fn htmlParseMemory(
7783    _buffer: *const c_char,
7784    _size: c_int,
7785) -> *mut _xmlDoc {
7786    // Phase 1: STUB
7787    ptr::null_mut()
7788}
7789
7790/// Parse an HTML document from a document string.
7791///
7792/// # UPSTREAM-PARITY
7793///
7794/// ```c
7795/// htmlDocPtr htmlParseDoc(const xmlChar *cur, const char *encoding);
7796/// ```
7797#[no_mangle]
7798pub const unsafe extern "C" fn htmlParseDoc(
7799    _cur: *const xmlChar,
7800    _encoding: *const c_char,
7801) -> *mut _xmlDoc {
7802    // Phase 1: STUB
7803    ptr::null_mut()
7804}
7805
7806/// Create an HTML parser context.
7807///
7808/// # UPSTREAM-PARITY
7809///
7810/// ```c
7811/// Free an HTML parser context.
7812///
7813/// # UPSTREAM-PARITY
7814///
7815/// ```c
7816/// void htmlFreeParserCtxt(htmlParserCtxtPtr ctxt);
7817/// ```
7818#[no_mangle]
7819pub extern "C" fn htmlFreeParserCtxt(ctxt: *mut c_void) {
7820    unsafe { crate::xml::html::free_parser_ctxt(ctxt) }
7821}
7822
7823/// Initialize the HTML parser.
7824///
7825/// # UPSTREAM-PARITY
7826///
7827/// ```c
7828/// void htmlInitParser(void);
7829/// ```
7830#[no_mangle]
7831pub const extern "C" fn htmlInitParser() {
7832    // Phase 1: STUB
7833}
7834
7835/// Clean up the HTML parser.
7836///
7837/// # UPSTREAM-PARITY
7838///
7839/// ```c
7840/// void htmlCleanupParser(void);
7841/// ```
7842#[no_mangle]
7843pub const extern "C" fn htmlCleanupParser() {
7844    // Phase 1: STUB
7845}
7846
7847// ═══════════════════════════════════════════════════════════════════════════════
7848// 17.5. Validation (DTD)
7849// ═══════════════════════════════════════════════════════════════════════════════
7850
7851/// Create a new validation context.
7852///
7853/// # UPSTREAM-PARITY
7854///
7855/// ```c
7856/// xmlValidCtxtPtr xmlNewValidCtxt(void);
7857/// ```
7858#[no_mangle]
7859pub unsafe extern "C" fn xmlNewValidCtxt() -> *mut _xmlValidCtxt {
7860    crate::xml::validation::new_valid_ctxt()
7861}
7862
7863/// Free a validation context.
7864///
7865/// # UPSTREAM-PARITY
7866///
7867/// ```c
7868/// void xmlFreeValidCtxt(xmlValidCtxtPtr ctxt);
7869/// ```
7870#[no_mangle]
7871pub unsafe extern "C" fn xmlFreeValidCtxt(ctxt: *mut _xmlValidCtxt) {
7872    crate::xml::validation::free_valid_ctxt(ctxt);
7873}
7874
7875/// Set error and warning callbacks on a validation context.
7876///
7877/// # UPSTREAM-PARITY
7878///
7879/// ```c
7880/// void xmlSetValidErrors(xmlValidCtxtPtr ctxt,
7881///                        xmlGenericErrorFunc err,
7882///                        xmlGenericErrorFunc warn,
7883///                        void *data);
7884/// ```
7885#[no_mangle]
7886pub unsafe extern "C" fn xmlSetValidErrors(
7887    ctxt: *mut _xmlValidCtxt,
7888    err: Option<xmlGenericErrorFunc>,
7889    warn: Option<xmlGenericErrorFunc>,
7890    data: *mut c_void,
7891) {
7892    crate::xml::validation::set_valid_errors(ctxt, err, warn, data);
7893}
7894
7895/// Validate a document against its DTD.
7896///
7897/// # UPSTREAM-PARITY
7898///
7899/// ```c
7900/// int xmlValidateDocument(xmlValidCtxtPtr ctxt, xmlDocPtr doc);
7901/// ```
7902#[no_mangle]
7903pub unsafe extern "C" fn xmlValidateDocument(ctxt: *mut _xmlValidCtxt, doc: *mut _xmlDoc) -> c_int {
7904    crate::xml::validation::validate_document(ctxt, doc)
7905}
7906
7907/// Final validation pass (check ID/IDREF consistency).
7908///
7909/// # UPSTREAM-PARITY
7910///
7911/// ```c
7912/// int xmlValidateDocumentFinal(xmlValidCtxtPtr ctxt, xmlDocPtr doc);
7913/// ```
7914#[no_mangle]
7915pub unsafe extern "C" fn xmlValidateDocumentFinal(
7916    ctxt: *mut _xmlValidCtxt,
7917    doc: *mut _xmlDoc,
7918) -> c_int {
7919    crate::xml::validation::validate_document_final(ctxt, doc)
7920}
7921
7922/// Validate an element node against its DTD declarations.
7923///
7924/// # UPSTREAM-PARITY
7925///
7926/// ```c
7927/// int xmlValidateElement(xmlValidCtxtPtr ctxt,
7928///                        xmlDocPtr doc,
7929///                        xmlNodePtr elem);
7930/// ```
7931#[no_mangle]
7932pub unsafe extern "C" fn xmlValidateElement(
7933    ctxt: *mut _xmlValidCtxt,
7934    doc: *mut _xmlDoc,
7935    elem: *mut _xmlNode,
7936) -> c_int {
7937    crate::xml::validation::validate_element(ctxt, doc, elem)
7938}
7939
7940/// Validate an attribute declaration.
7941///
7942/// # UPSTREAM-PARITY
7943///
7944/// ```c
7945/// int xmlValidateAttributeDecl(xmlValidCtxt *ctxt,
7946///                              xmlDoc *doc,
7947///                              xmlAttribute *attr);
7948/// ```
7949///
7950/// 11.1-Z.3 signature court: the pre-Z.3 candidate declared a fourth
7951/// `xmlNodePtr elem` argument that does not exist upstream (valid.h 2.15.3).
7952#[no_mangle]
7953pub unsafe extern "C" fn xmlValidateAttributeDecl(
7954    ctxt: *mut _xmlValidCtxt,
7955    doc: *mut _xmlDoc,
7956    attr: *mut _xmlAttribute,
7957) -> c_int {
7958    crate::xml::validation::validate_attribute_decl(ctxt, doc, attr)
7959}
7960
7961/// Validate an attribute value against its declared type.
7962///
7963/// # UPSTREAM-PARITY
7964///
7965/// ```c
7966/// int xmlValidateAttributeValue(int type, const xmlChar *value);
7967/// ```
7968#[no_mangle]
7969pub unsafe extern "C" fn xmlValidateAttributeValue(atype: c_int, value: *const xmlChar) -> c_int {
7970    crate::xml::validation::validate_attribute_value(atype, value)
7971}
7972
7973/// Validate a NOTATION reference.
7974///
7975/// # UPSTREAM-PARITY
7976///
7977/// ```c
7978/// int xmlValidateNotationUse(xmlValidCtxtPtr ctxt,
7979///                            xmlDocPtr doc,
7980///                            const xmlChar *notationName);
7981/// ```
7982#[no_mangle]
7983pub unsafe extern "C" fn xmlValidateNotationUse(
7984    ctxt: *mut _xmlValidCtxt,
7985    doc: *mut _xmlDoc,
7986    notation_name: *const xmlChar,
7987) -> c_int {
7988    crate::xml::validation::validate_notation_use(ctxt, doc, notation_name)
7989}
7990
7991/// Validate an ID value (check uniqueness).
7992///
7993/// # UPSTREAM-PARITY
7994///
7995/// ```c
7996/// int xmlValidateID(xmlValidCtxtPtr ctxt,
7997///                   xmlDocPtr doc,
7998///                   xmlNodePtr node,
7999///                   const xmlChar *value);
8000/// ```
8001#[no_mangle]
8002pub unsafe extern "C" fn xmlValidateID(
8003    ctxt: *mut _xmlValidCtxt,
8004    doc: *mut _xmlDoc,
8005    node: *mut _xmlNode,
8006    value: *const xmlChar,
8007) -> c_int {
8008    crate::xml::validation::validate_id(ctxt, doc, node, value)
8009}
8010
8011/// Validate an IDREF value (check it references a known ID; upstream
8012/// valid.h 2.15 has no `node` argument — R-000176).
8013///
8014/// # UPSTREAM-PARITY
8015///
8016/// ```c
8017/// int xmlValidateIDRef(xmlValidCtxt *ctxt, xmlDoc *doc,
8018///                      const xmlChar *value);
8019/// ```
8020#[no_mangle]
8021pub unsafe extern "C" fn xmlValidateIDRef(
8022    ctxt: *mut _xmlValidCtxt,
8023    doc: *mut _xmlDoc,
8024    value: *const xmlChar,
8025) -> c_int {
8026    crate::xml::validation::validate_id_ref(ctxt, doc, ptr::null_mut(), value)
8027}
8028
8029/// Validate IDREFS (whitespace-separated list of IDREFs; upstream valid.h
8030/// 2.15 has no `node` argument — R-000176).
8031///
8032/// # UPSTREAM-PARITY
8033///
8034/// ```c
8035/// int xmlValidateIDRefs(xmlValidCtxt *ctxt, xmlDoc *doc,
8036///                       const xmlChar *value);
8037/// ```
8038#[no_mangle]
8039pub unsafe extern "C" fn xmlValidateIDRefs(
8040    ctxt: *mut _xmlValidCtxt,
8041    doc: *mut _xmlDoc,
8042    value: *const xmlChar,
8043) -> c_int {
8044    crate::xml::validation::validate_id_refs(ctxt, doc, ptr::null_mut(), value)
8045}
8046
8047/// Validate an NCName value (modern 2-arg form, upstream tree.c).
8048///
8049/// # UPSTREAM-PARITY
8050///
8051/// ```c
8052/// int xmlValidateNCName(const xmlChar *value, int space);
8053/// ```
8054///
8055/// Returns -1 on NULL, 0 if valid, 1 if invalid.
8056#[no_mangle]
8057pub unsafe extern "C" fn xmlValidateNCName(value: *const xmlChar, space: c_int) -> c_int {
8058    crate::xml::validation::validate_ncname(value, space)
8059}
8060
8061/// Validate a QName value (modern 2-arg form, upstream tree.c).
8062///
8063/// # UPSTREAM-PARITY
8064///
8065/// ```c
8066/// int xmlValidateQName(const xmlChar *value, int space);
8067/// ```
8068#[no_mangle]
8069pub unsafe extern "C" fn xmlValidateQName(value: *const xmlChar, space: c_int) -> c_int {
8070    crate::xml::validation::validate_qname(value, space)
8071}
8072
8073/// Validate an XML Name value (modern 2-arg form, upstream tree.c).
8074///
8075/// # UPSTREAM-PARITY / HISTORICAL
8076///
8077/// Since libxml2 2.12 the DSO symbol carries a second `int space` parameter
8078/// with inverted return semantics (0 valid / 1 invalid / -1 NULL); the
8079/// pre-2.12 1-arg form no longer exists in the DSO. The candidate matches
8080/// the current oracle. (The 1-arg semantics live on as xmlValidateNameValue.)
8081///
8082/// ```c
8083/// int xmlValidateName(const xmlChar *value, int space);
8084/// ```
8085#[no_mangle]
8086pub unsafe extern "C" fn xmlValidateName(value: *const xmlChar, space: c_int) -> c_int {
8087    crate::xml::validation::validate_name_space(value, space)
8088}
8089
8090/// Validate an NMToken value (modern 2-arg form, upstream tree.c).
8091///
8092/// # UPSTREAM-PARITY
8093///
8094/// ```c
8095/// int xmlValidateNMToken(const xmlChar *value, int space);
8096/// ```
8097#[no_mangle]
8098pub unsafe extern "C" fn xmlValidateNMToken(value: *const xmlChar, space: c_int) -> c_int {
8099    crate::xml::validation::validate_nmtoken_space(value, space)
8100}
8101
8102/// Validate a Name value (1-arg form, upstream valid.c).
8103///
8104/// # UPSTREAM-PARITY
8105///
8106/// ```c
8107/// int xmlValidateNameValue(const xmlChar *value);
8108/// ```
8109///
8110/// Returns 1 if valid, 0 if not (NULL included).
8111#[no_mangle]
8112pub unsafe extern "C" fn xmlValidateNameValue(value: *const xmlChar) -> c_int {
8113    crate::xml::validation::validate_name_value(value)
8114}
8115
8116/// Validate a whitespace-separated list of Names (separator is exactly
8117/// 0x20, upstream erratum E20).
8118///
8119/// # UPSTREAM-PARITY
8120///
8121/// ```c
8122/// int xmlValidateNamesValue(const xmlChar *value);
8123/// ```
8124#[no_mangle]
8125pub unsafe extern "C" fn xmlValidateNamesValue(value: *const xmlChar) -> c_int {
8126    crate::xml::validation::validate_names_value(value)
8127}
8128
8129/// Validate an Nmtoken value (1-arg form, upstream valid.c).
8130///
8131/// # UPSTREAM-PARITY
8132///
8133/// ```c
8134/// int xmlValidateNmtokenValue(const xmlChar *value);
8135/// ```
8136#[no_mangle]
8137pub unsafe extern "C" fn xmlValidateNmtokenValue(value: *const xmlChar) -> c_int {
8138    crate::xml::validation::validate_nmtoken_value(value)
8139}
8140
8141/// Validate a whitespace-separated list of Nmtokens.
8142///
8143/// # UPSTREAM-PARITY
8144///
8145/// ```c
8146/// int xmlValidateNmtokensValue(const xmlChar *value);
8147/// ```
8148#[no_mangle]
8149pub unsafe extern "C" fn xmlValidateNmtokensValue(value: *const xmlChar) -> c_int {
8150    crate::xml::validation::validate_nmtokens_value(value)
8151}
8152
8153/// Validate a single element declaration (VC: Unique Element Type
8154/// Declaration, VC: No Duplicate Types).
8155///
8156/// # UPSTREAM-PARITY
8157///
8158/// ```c
8159/// int xmlValidateElementDecl(xmlValidCtxtPtr ctxt,
8160///                            xmlDocPtr doc,
8161///                            xmlElementPtr elem);
8162/// ```
8163#[no_mangle]
8164pub unsafe extern "C" fn xmlValidateElementDecl(
8165    ctxt: *mut _xmlValidCtxt,
8166    doc: *mut _xmlDoc,
8167    elem: *mut _xmlElement,
8168) -> c_int {
8169    crate::xml::validation::validate_element_decl(ctxt, doc, elem)
8170}
8171
8172/// Validate a notation declaration.
8173///
8174/// # UPSTREAM-PARITY
8175///
8176/// Modern libxml2 has no validity constraint on notation declarations; the
8177/// oracle returns 1 unconditionally (verified by DSO disassembly).
8178///
8179/// ```c
8180/// int xmlValidateNotationDecl(xmlValidCtxtPtr ctxt,
8181///                             xmlDocPtr doc,
8182///                             xmlNotationPtr nota);
8183/// ```
8184#[no_mangle]
8185pub const unsafe extern "C" fn xmlValidateNotationDecl(
8186    ctxt: *mut _xmlValidCtxt,
8187    doc: *mut _xmlDoc,
8188    nota: *mut _xmlNotation,
8189) -> c_int {
8190    crate::xml::validation::validate_notation_decl(ctxt, doc, nota)
8191}
8192
8193/// Validate a single attribute against its declaration.
8194///
8195/// # UPSTREAM-PARITY
8196///
8197/// ```c
8198/// int xmlValidateOneAttribute(xmlValidCtxtPtr ctxt,
8199///                             xmlDocPtr doc,
8200///                             xmlNodePtr elem,
8201///                             xmlAttrPtr attr,
8202///                             const xmlChar *value);
8203/// ```
8204#[no_mangle]
8205pub unsafe extern "C" fn xmlValidateOneAttribute(
8206    ctxt: *mut _xmlValidCtxt,
8207    doc: *mut _xmlDoc,
8208    elem: *mut _xmlNode,
8209    attr: *mut _xmlAttr,
8210    value: *const xmlChar,
8211) -> c_int {
8212    crate::xml::validation::validate_one_attribute(ctxt, doc, elem, attr, value)
8213}
8214
8215/// Validate a single element against its declaration (without recursing).
8216///
8217/// # UPSTREAM-PARITY
8218///
8219/// ```c
8220/// int xmlValidateOneElement(xmlValidCtxtPtr ctxt,
8221///                           xmlDocPtr doc,
8222///                           xmlNodePtr elem);
8223/// ```
8224#[no_mangle]
8225pub unsafe extern "C" fn xmlValidateOneElement(
8226    ctxt: *mut _xmlValidCtxt,
8227    doc: *mut _xmlDoc,
8228    elem: *mut _xmlNode,
8229) -> c_int {
8230    crate::xml::validation::validate_one_element(ctxt, doc, elem)
8231}
8232
8233/// Validate a namespace declaration attribute.
8234///
8235/// # UPSTREAM-PARITY
8236///
8237/// ```c
8238/// int xmlValidateOneNamespace(xmlValidCtxtPtr ctxt,
8239///                             xmlDocPtr doc,
8240///                             xmlNodePtr elem,
8241///                             const xmlChar *prefix,
8242///                             xmlNsPtr ns,
8243///                             const xmlChar *value);
8244/// ```
8245#[no_mangle]
8246pub unsafe extern "C" fn xmlValidateOneNamespace(
8247    ctxt: *mut _xmlValidCtxt,
8248    doc: *mut _xmlDoc,
8249    elem: *mut _xmlNode,
8250    prefix: *const xmlChar,
8251    ns: *mut _xmlNs,
8252    value: *const xmlChar,
8253) -> c_int {
8254    crate::xml::validation::validate_one_namespace(ctxt, doc, elem, prefix, ns, value)
8255}
8256
8257/// Push a new element start onto the validation stack (streaming DTD
8258/// validation).
8259///
8260/// # UPSTREAM-PARITY
8261///
8262/// ```c
8263/// int xmlValidatePushElement(xmlValidCtxtPtr ctxt,
8264///                            xmlDocPtr doc,
8265///                            xmlNodePtr elem,
8266///                            const xmlChar *qname);
8267/// ```
8268#[no_mangle]
8269pub unsafe extern "C" fn xmlValidatePushElement(
8270    ctxt: *mut _xmlValidCtxt,
8271    doc: *mut _xmlDoc,
8272    elem: *mut _xmlNode,
8273    qname: *const xmlChar,
8274) -> c_int {
8275    crate::xml::validation::validate_push_element(ctxt, doc, elem, qname)
8276}
8277
8278/// Push character data onto the validation stack.
8279///
8280/// # UPSTREAM-PARITY
8281///
8282/// ```c
8283/// int xmlValidatePushCData(xmlValidCtxtPtr ctxt,
8284///                          const xmlChar *data,
8285///                          int len);
8286/// ```
8287#[no_mangle]
8288pub unsafe extern "C" fn xmlValidatePushCData(
8289    ctxt: *mut _xmlValidCtxt,
8290    data: *const xmlChar,
8291    len: c_int,
8292) -> c_int {
8293    crate::xml::validation::validate_push_cdata(ctxt, data, len)
8294}
8295
8296/// Pop an element end from the validation stack.
8297///
8298/// # UPSTREAM-PARITY
8299///
8300/// ```c
8301/// int xmlValidatePopElement(xmlValidCtxtPtr ctxt,
8302///                           xmlDocPtr doc,
8303///                           xmlNodePtr elem,
8304///                           const xmlChar *qname);
8305/// ```
8306#[no_mangle]
8307pub unsafe extern "C" fn xmlValidatePopElement(
8308    ctxt: *mut _xmlValidCtxt,
8309    doc: *mut _xmlDoc,
8310    elem: *mut _xmlNode,
8311    qname: *const xmlChar,
8312) -> c_int {
8313    crate::xml::validation::validate_pop_element(ctxt, doc, elem, qname)
8314}
8315
8316/// Build the content-model automaton for an element declaration.
8317///
8318/// # UPSTREAM-PARITY
8319///
8320/// ```c
8321/// int xmlValidBuildContentModel(xmlValidCtxtPtr ctxt,
8322///                               xmlElementPtr elem);
8323/// ```
8324#[no_mangle]
8325pub unsafe extern "C" fn xmlValidBuildContentModel(
8326    ctxt: *mut _xmlValidCtxt,
8327    elem: *mut _xmlElement,
8328) -> c_int {
8329    crate::xml::validation::validate_build_content_model(ctxt, elem)
8330}
8331
8332/// Add an attribute to the document's ID table.
8333///
8334/// # UPSTREAM-PARITY
8335///
8336/// ```c
8337/// xmlIDPtr xmlAddID(xmlValidCtxtPtr ctxt,
8338///                   xmlDocPtr doc,
8339///                   const xmlChar *value,
8340///                   xmlAttrPtr attr);
8341/// ```
8342#[no_mangle]
8343pub unsafe extern "C" fn xmlAddID(
8344    ctxt: *mut _xmlValidCtxt,
8345    doc: *mut _xmlDoc,
8346    value: *const xmlChar,
8347    attr: *mut _xmlAttr,
8348) -> *mut _xmlID {
8349    crate::xml::validation::add_id(ctxt, doc, value, attr)
8350}
8351
8352/// Remove an attribute from the document's ID table.
8353///
8354/// # UPSTREAM-PARITY
8355///
8356/// ```c
8357/// int xmlRemoveID(xmlDocPtr doc, xmlAttrPtr attr);
8358/// ```
8359#[no_mangle]
8360pub unsafe extern "C" fn xmlRemoveID(doc: *mut _xmlDoc, attr: *mut _xmlAttr) -> c_int {
8361    crate::xml::validation::remove_id(doc, attr)
8362}
8363
8364/// Register an IDREF in the document's ref table.
8365///
8366/// # UPSTREAM-PARITY
8367///
8368/// ```c
8369/// xmlRefPtr xmlAddRef(xmlValidCtxtPtr ctxt,
8370///                     xmlDocPtr doc,
8371///                     const xmlChar *value,
8372///                     xmlAttrPtr attr);
8373/// ```
8374#[no_mangle]
8375pub unsafe extern "C" fn xmlAddRef(
8376    ctxt: *mut _xmlValidCtxt,
8377    doc: *mut _xmlDoc,
8378    value: *const xmlChar,
8379    attr: *mut _xmlAttr,
8380) -> *mut _xmlRef {
8381    crate::xml::validation::add_ref(ctxt, doc, value, attr)
8382}
8383
8384/// Remove an attribute's IDREF entries.
8385///
8386/// # UPSTREAM-PARITY
8387///
8388/// ```c
8389/// int xmlRemoveRef(xmlDocPtr doc, xmlAttrPtr attr);
8390/// ```
8391#[no_mangle]
8392pub unsafe extern "C" fn xmlRemoveRef(doc: *mut _xmlDoc, attr: *mut _xmlAttr) -> c_int {
8393    crate::xml::validation::remove_ref(doc, attr)
8394}
8395
8396/// Add an ID without a validation context (2.13+).
8397///
8398/// # UPSTREAM-PARITY
8399///
8400/// ```c
8401/// int xmlAddIDSafe(xmlAttrPtr attr, const xmlChar *value);
8402/// ```
8403#[no_mangle]
8404pub unsafe extern "C" fn xmlAddIDSafe(attr: *mut _xmlAttr, value: *const xmlChar) -> c_int {
8405    crate::xml::validation::add_id_safe(attr, value)
8406}
8407
8408/// Free an ID hash table.
8409///
8410/// # UPSTREAM-PARITY
8411///
8412/// ```c
8413/// void xmlFreeIDTable(xmlIDTablePtr table);
8414/// ```
8415#[no_mangle]
8416pub unsafe extern "C" fn xmlFreeIDTable(table: *mut c_void) {
8417    crate::xml::validation::free_id_table(table as *mut crate::xml::hash::HashTable);
8418}
8419
8420/// Free an IDREF hash table.
8421///
8422/// # UPSTREAM-PARITY
8423///
8424/// ```c
8425/// void xmlFreeRefTable(xmlRefTablePtr table);
8426/// ```
8427#[no_mangle]
8428pub unsafe extern "C" fn xmlFreeRefTable(table: *mut c_void) {
8429    crate::xml::validation::free_ref_table(table as *mut crate::xml::hash::HashTable);
8430}
8431
8432/// Look up the attribute holding an ID.
8433///
8434/// # UPSTREAM-PARITY
8435///
8436/// ```c
8437/// xmlAttrPtr xmlGetID(xmlDocPtr doc, const xmlChar *ID);
8438/// ```
8439#[no_mangle]
8440pub unsafe extern "C" fn xmlGetID(doc: *mut _xmlDoc, id: *const xmlChar) -> *mut _xmlAttr {
8441    crate::xml::validation::get_id(doc, id)
8442}
8443
8444/// Look up the list of references for an ID.
8445///
8446/// # UPSTREAM-PARITY
8447///
8448/// ```c
8449/// xmlListPtr xmlGetRefs(xmlDocPtr doc, const xmlChar *ID);
8450/// ```
8451#[no_mangle]
8452pub unsafe extern "C" fn xmlGetRefs(doc: *mut _xmlDoc, id: *const xmlChar) -> *mut c_void {
8453    crate::xml::validation::get_refs(doc, id) as *mut c_void
8454}
8455
8456/// Is this attribute an ID?
8457///
8458/// # UPSTREAM-PARITY
8459///
8460/// ```c
8461/// int xmlIsID(xmlDocPtr doc, xmlNodePtr elem, xmlAttrPtr attr);
8462/// ```
8463#[no_mangle]
8464pub unsafe extern "C" fn xmlIsID(
8465    doc: *mut _xmlDoc,
8466    elem: *mut _xmlNode,
8467    attr: *mut _xmlAttr,
8468) -> c_int {
8469    crate::xml::validation::is_id(doc, elem, attr)
8470}
8471
8472/// Is this attribute an IDREF?
8473///
8474/// # UPSTREAM-PARITY
8475///
8476/// ```c
8477/// int xmlIsRef(xmlDocPtr doc, xmlNodePtr elem, xmlAttrPtr attr);
8478/// ```
8479#[no_mangle]
8480pub unsafe extern "C" fn xmlIsRef(
8481    doc: *mut _xmlDoc,
8482    elem: *mut _xmlNode,
8483    attr: *mut _xmlAttr,
8484) -> c_int {
8485    crate::xml::validation::is_ref(doc, elem, attr)
8486}
8487
8488/// Search a DTD for an element declaration (with QName splitting).
8489///
8490/// # UPSTREAM-PARITY
8491///
8492/// ```c
8493/// xmlElementPtr xmlGetDtdElementDesc(xmlDtdPtr dtd, const xmlChar *name);
8494/// ```
8495#[no_mangle]
8496pub unsafe extern "C" fn xmlGetDtdElementDesc(
8497    dtd: *mut _xmlDtd,
8498    name: *const xmlChar,
8499) -> *mut _xmlElement {
8500    crate::xml::validation::get_dtd_element_desc(dtd, name)
8501}
8502
8503/// Search a DTD for an attribute declaration (with QName splitting).
8504///
8505/// # UPSTREAM-PARITY
8506///
8507/// ```c
8508/// xmlAttributePtr xmlGetDtdAttrDesc(xmlDtdPtr dtd,
8509///                                   const xmlChar *elem,
8510///                                   const xmlChar *name);
8511/// ```
8512#[no_mangle]
8513pub unsafe extern "C" fn xmlGetDtdAttrDesc(
8514    dtd: *mut _xmlDtd,
8515    elem: *const xmlChar,
8516    name: *const xmlChar,
8517) -> *mut _xmlAttribute {
8518    crate::xml::validation::get_dtd_attr_desc(dtd, elem, name)
8519}
8520
8521/// Search a DTD for a qualified element declaration.
8522///
8523/// # UPSTREAM-PARITY
8524///
8525/// ```c
8526/// xmlElementPtr xmlGetDtdQElementDesc(xmlDtdPtr dtd,
8527///                                     const xmlChar *name,
8528///                                     const xmlChar *prefix);
8529/// ```
8530#[no_mangle]
8531pub unsafe extern "C" fn xmlGetDtdQElementDesc(
8532    dtd: *mut _xmlDtd,
8533    name: *const xmlChar,
8534    prefix: *const xmlChar,
8535) -> *mut _xmlElement {
8536    crate::xml::validation::get_dtd_qelement_desc(dtd, name, prefix)
8537}
8538
8539/// Search a DTD for a qualified attribute declaration.
8540///
8541/// # UPSTREAM-PARITY
8542///
8543/// ```c
8544/// xmlAttributePtr xmlGetDtdQAttrDesc(xmlDtdPtr dtd,
8545///                                    const xmlChar *elem,
8546///                                    const xmlChar *name,
8547///                                    const xmlChar *prefix);
8548/// ```
8549#[no_mangle]
8550pub unsafe extern "C" fn xmlGetDtdQAttrDesc(
8551    dtd: *mut _xmlDtd,
8552    elem: *const xmlChar,
8553    name: *const xmlChar,
8554    prefix: *const xmlChar,
8555) -> *mut _xmlAttribute {
8556    crate::xml::validation::get_dtd_qattr_desc(dtd, elem, name, prefix)
8557}
8558
8559/// Search a DTD for a notation declaration.
8560///
8561/// # UPSTREAM-PARITY
8562///
8563/// ```c
8564/// xmlNotationPtr xmlGetDtdNotationDesc(xmlDtdPtr dtd, const xmlChar *name);
8565/// ```
8566#[no_mangle]
8567pub unsafe extern "C" fn xmlGetDtdNotationDesc(
8568    dtd: *mut _xmlDtd,
8569    name: *const xmlChar,
8570) -> *mut _xmlNotation {
8571    crate::xml::validation::get_dtd_notation_desc(dtd, name)
8572}
8573
8574/// Validate the root element of a document.
8575///
8576/// # UPSTREAM-PARITY
8577///
8578/// ```c
8579/// int xmlValidateRoot(xmlValidCtxtPtr ctxt, xmlDocPtr doc);
8580/// ```
8581#[no_mangle]
8582pub unsafe extern "C" fn xmlValidateRoot(ctxt: *mut _xmlValidCtxt, doc: *mut _xmlDoc) -> c_int {
8583    crate::xml::validation::validate_root(ctxt, doc)
8584}
8585
8586/// Validate element content against its content model.
8587///
8588/// # UPSTREAM-PARITY
8589///
8590/// ```c
8591/// int xmlValidateContent(xmlValidCtxtPtr ctxt,
8592///                        xmlNodePtr node,
8593///                        xmlDocPtr doc);
8594/// ```
8595#[no_mangle]
8596pub unsafe extern "C" fn xmlValidateContent(
8597    ctxt: *mut _xmlValidCtxt,
8598    node: *mut _xmlNode,
8599    doc: *mut _xmlDoc,
8600) -> c_int {
8601    crate::xml::validation::validate_content(ctxt, node, doc)
8602}
8603
8604/// Check if an element is declared as mixed content.
8605///
8606/// # UPSTREAM-PARITY
8607///
8608/// ```c
8609/// int xmlIsMixedElement(xmlDocPtr doc, const xmlChar *name);
8610/// ```
8611#[no_mangle]
8612pub unsafe extern "C" fn xmlIsMixedElement(doc: *mut _xmlDoc, name: *const xmlChar) -> c_int {
8613    crate::xml::validation::is_mixed_element(doc, name)
8614}
8615
8616/// Check if an element is declared as EMPTY.
8617///
8618/// # UPSTREAM-PARITY
8619///
8620/// ```c
8621/// int xmlIsEmptyElement(xmlDocPtr doc, const xmlChar *name);
8622/// ```
8623#[no_mangle]
8624pub unsafe extern "C" fn xmlIsEmptyElement(doc: *mut _xmlDoc, name: *const xmlChar) -> c_int {
8625    crate::xml::validation::is_empty_element(doc, name)
8626}
8627
8628/// Validate a DTD's declarations.
8629///
8630/// # UPSTREAM-PARITY
8631///
8632/// ```c
8633/// int xmlValidateDtd(xmlValidCtxtPtr ctxt,
8634///                    xmlDocPtr doc,
8635///                    xmlDtdPtr dtd);
8636/// ```
8637#[no_mangle]
8638pub unsafe extern "C" fn xmlValidateDtd(
8639    ctxt: *mut _xmlValidCtxt,
8640    doc: *mut _xmlDoc,
8641    dtd: *mut _xmlDtd,
8642) -> c_int {
8643    crate::xml::validation::validate_dtd(ctxt, doc, dtd)
8644}
8645
8646/// Final DTD validation (ID/IDREF consistency).
8647///
8648/// # UPSTREAM-PARITY
8649///
8650/// ```c
8651/// int xmlValidateDtdFinal(xmlValidCtxtPtr ctxt, xmlDocPtr doc);
8652/// ```
8653#[no_mangle]
8654pub unsafe extern "C" fn xmlValidateDtdFinal(ctxt: *mut _xmlValidCtxt, doc: *mut _xmlDoc) -> c_int {
8655    crate::xml::validation::validate_dtd_final(ctxt, doc)
8656}
8657
8658/// Validate that a value is in an enumeration.
8659///
8660/// # UPSTREAM-PARITY
8661///
8662/// ```c
8663/// int xmlValidateEnumeration(xmlValidCtxtPtr ctxt,
8664///                            const xmlChar *value,
8665///                            xmlEnumerationPtr tree);
8666/// ```
8667#[no_mangle]
8668pub unsafe extern "C" fn xmlValidateEnumeration(
8669    ctxt: *mut _xmlValidCtxt,
8670    value: *const xmlChar,
8671    tree: *mut _xmlEnumeration,
8672) -> c_int {
8673    crate::xml::validation::validate_enumeration(ctxt, value, tree)
8674}
8675
8676// ═══════════════════════════════════════════════════════════════════════════════
8677// 18. Debug / Miscellaneous
8678// ═══════════════════════════════════════════════════════════════════════════════
8679
8680/// Dump a document to a file for debugging.
8681/// Get the path to the current executable.
8682///
8683/// # UPSTREAM-PARITY
8684///
8685/// ```c
8686/// char *xmlGetBinaryPath(void);
8687/// ```
8688#[no_mangle]
8689pub const extern "C" fn xmlGetBinaryPath() -> *mut c_char {
8690    // Phase 1: STUB
8691    ptr::null_mut()
8692}
8693
8694/// Get the path to the current executable's home directory.
8695///
8696/// # UPSTREAM-PARITY
8697///
8698/// ```c
8699/// char *xmlGetHomeOfBinary(void);
8700/// ```
8701#[no_mangle]
8702pub const extern "C" fn xmlGetHomeOfBinary() -> *mut c_char {
8703    // Phase 1: STUB
8704    ptr::null_mut()
8705}
8706
8707// ═══════════════════════════════════════════════════════════════════════════════
8708// SAX2 default callback entry points (upstream SAX2.c)
8709// ═══════════════════════════════════════════════════════════════════════════════
8710//
8711// These are the public `xmlSAX2*` callback functions that downstream code
8712// installs into `xmlSAXHandler` structures. They are the same implementations
8713// the candidate's default SAX handler uses; exporting them under the
8714// upstream names is required for ABI parity (R-000136 closure).
8715
8716/// Upstream SAX2.c `xmlSAX2StartDocument` — public entry point of the default handler.
8717#[no_mangle]
8718pub unsafe extern "C" fn xmlSAX2StartDocument(ctx: *mut c_void) {
8719    crate::xml::sax::default::default_sax_handler::startDocument(ctx)
8720}
8721
8722/// Upstream SAX2.c `xmlSAX2EndDocument` — public entry point of the default handler.
8723#[no_mangle]
8724pub unsafe extern "C" fn xmlSAX2EndDocument(ctx: *mut c_void) {
8725    crate::xml::sax::default::default_sax_handler::endDocument(ctx)
8726}
8727
8728/// Upstream SAX2.c `xmlSAX2StartElementNs` — public entry point of the default handler.
8729#[no_mangle]
8730pub unsafe extern "C" fn xmlSAX2StartElementNs(
8731    ctx: *mut c_void,
8732    localname: *const xmlChar,
8733    prefix: *const xmlChar,
8734    URI: *const xmlChar,
8735    nb_namespaces: c_int,
8736    namespaces: *mut *const xmlChar,
8737    nb_attributes: c_int,
8738    nb_defaulted: c_int,
8739    attributes: *mut *const xmlChar,
8740) {
8741    crate::xml::sax::default::default_sax_handler::startElementNs(
8742        ctx,
8743        localname,
8744        prefix,
8745        URI,
8746        nb_namespaces,
8747        namespaces,
8748        nb_attributes,
8749        nb_defaulted,
8750        attributes,
8751    )
8752}
8753
8754/// Upstream SAX2.c `xmlSAX2EndElementNs` — public entry point of the default handler.
8755#[no_mangle]
8756pub unsafe extern "C" fn xmlSAX2EndElementNs(
8757    ctx: *mut c_void,
8758    localname: *const xmlChar,
8759    prefix: *const xmlChar,
8760    URI: *const xmlChar,
8761) {
8762    crate::xml::sax::default::default_sax_handler::endElementNs(ctx, localname, prefix, URI)
8763}
8764
8765/// Upstream SAX2.c `xmlSAX2Characters` — public entry point of the default handler.
8766#[no_mangle]
8767pub unsafe extern "C" fn xmlSAX2Characters(ctx: *mut c_void, ch: *const xmlChar, len: c_int) {
8768    crate::xml::sax::default::default_sax_handler::characters(ctx, ch, len)
8769}
8770
8771/// Upstream SAX2.c `xmlSAX2IgnorableWhitespace` — public entry point of the default handler.
8772#[no_mangle]
8773pub unsafe extern "C" fn xmlSAX2IgnorableWhitespace(
8774    ctx: *mut c_void,
8775    ch: *const xmlChar,
8776    len: c_int,
8777) {
8778    crate::xml::sax::default::default_sax_handler::ignorableWhitespace(ctx, ch, len)
8779}
8780
8781/// Upstream SAX2.c `xmlSAX2Comment` — public entry point of the default handler.
8782#[no_mangle]
8783pub unsafe extern "C" fn xmlSAX2Comment(ctx: *mut c_void, value: *const xmlChar) {
8784    crate::xml::sax::default::default_sax_handler::comment(ctx, value)
8785}
8786
8787/// Upstream SAX2.c `xmlSAX2ProcessingInstruction` — public entry point of the default handler.
8788#[no_mangle]
8789pub unsafe extern "C" fn xmlSAX2ProcessingInstruction(
8790    ctx: *mut c_void,
8791    target: *const xmlChar,
8792    data: *const xmlChar,
8793) {
8794    crate::xml::sax::default::default_sax_handler::processingInstruction(ctx, target, data)
8795}
8796
8797/// Upstream SAX2.c `xmlSAX2CDataBlock` — public entry point of the default handler.
8798#[no_mangle]
8799pub unsafe extern "C" fn xmlSAX2CDataBlock(ctx: *mut c_void, value: *const xmlChar, len: c_int) {
8800    crate::xml::sax::default::default_sax_handler::cdataBlock(ctx, value, len)
8801}
8802
8803/// Upstream SAX2.c `xmlSAX2InternalSubset` — public entry point of the default handler.
8804#[no_mangle]
8805pub unsafe extern "C" fn xmlSAX2InternalSubset(
8806    ctx: *mut c_void,
8807    name: *const xmlChar,
8808    ExternalID: *const xmlChar,
8809    SystemID: *const xmlChar,
8810) {
8811    crate::xml::sax::default::default_sax_handler::internalSubset(ctx, name, ExternalID, SystemID)
8812}
8813
8814/// Upstream SAX2.c `xmlSAX2ExternalSubset` — public entry point of the default handler.
8815#[no_mangle]
8816pub unsafe extern "C" fn xmlSAX2ExternalSubset(
8817    ctx: *mut c_void,
8818    name: *const xmlChar,
8819    ExternalID: *const xmlChar,
8820    SystemID: *const xmlChar,
8821) {
8822    crate::xml::sax::default::default_sax_handler::externalSubset(ctx, name, ExternalID, SystemID)
8823}
8824
8825/// Upstream SAX2.c `xmlSAX2EntityDecl` — public entry point of the default handler.
8826#[no_mangle]
8827pub unsafe extern "C" fn xmlSAX2EntityDecl(
8828    ctx: *mut c_void,
8829    name: *const xmlChar,
8830    type_: c_int,
8831    publicId: *const xmlChar,
8832    systemId: *const xmlChar,
8833    content: *mut xmlChar,
8834) {
8835    crate::xml::sax::default::default_sax_handler::entityDecl(
8836        ctx, name, type_, publicId, systemId, content,
8837    )
8838}
8839
8840/// Upstream SAX2.c `xmlSAX2AttributeDecl` — public entry point of the default handler.
8841#[no_mangle]
8842pub const unsafe extern "C" fn xmlSAX2AttributeDecl(
8843    ctx: *mut c_void,
8844    elem: *const xmlChar,
8845    fullname: *const xmlChar,
8846    type_: c_int,
8847    def: c_int,
8848    defaultValue: *const xmlChar,
8849    tree: *mut crate::abi::structs::_xmlEnumeration,
8850) {
8851    crate::xml::sax::default::default_sax_handler::attributeDecl(
8852        ctx,
8853        elem,
8854        fullname,
8855        type_,
8856        def,
8857        defaultValue,
8858        tree,
8859    )
8860}
8861
8862/// Upstream SAX2.c `xmlSAX2ElementDecl` — public entry point of the default handler.
8863#[no_mangle]
8864pub const unsafe extern "C" fn xmlSAX2ElementDecl(
8865    ctx: *mut c_void,
8866    name: *const xmlChar,
8867    type_: c_int,
8868    content: *mut crate::abi::structs::_xmlElementContent,
8869) {
8870    crate::xml::sax::default::default_sax_handler::elementDecl(ctx, name, type_, content)
8871}
8872
8873/// Upstream SAX2.c `xmlSAX2NotationDecl` — public entry point of the default handler.
8874#[no_mangle]
8875pub const unsafe extern "C" fn xmlSAX2NotationDecl(
8876    ctx: *mut c_void,
8877    name: *const xmlChar,
8878    publicId: *const xmlChar,
8879    systemId: *const xmlChar,
8880) {
8881    crate::xml::sax::default::default_sax_handler::notationDecl(ctx, name, publicId, systemId)
8882}
8883
8884/// Upstream SAX2.c `xmlSAX2UnparsedEntityDecl` — public entry point of the default handler.
8885#[no_mangle]
8886pub const unsafe extern "C" fn xmlSAX2UnparsedEntityDecl(
8887    ctx: *mut c_void,
8888    name: *const xmlChar,
8889    publicId: *const xmlChar,
8890    systemId: *const xmlChar,
8891    notationName: *const xmlChar,
8892) {
8893    crate::xml::sax::default::default_sax_handler::unparsedEntityDecl(
8894        ctx,
8895        name,
8896        publicId,
8897        systemId,
8898        notationName,
8899    )
8900}
8901
8902/// Upstream SAX2.c `xmlSAX2ResolveEntity` — public entry point of the default handler.
8903#[no_mangle]
8904pub const unsafe extern "C" fn xmlSAX2ResolveEntity(
8905    ctx: *mut c_void,
8906    publicId: *const xmlChar,
8907    systemId: *const xmlChar,
8908) -> *mut crate::abi::structs::_xmlParserInput {
8909    crate::xml::sax::default::default_sax_handler::resolveEntity(ctx, publicId, systemId)
8910}
8911
8912/// Upstream SAX2.c `xmlSAX2IsStandalone` — public entry point of the default handler.
8913#[no_mangle]
8914pub const unsafe extern "C" fn xmlSAX2IsStandalone(ctx: *mut c_void) -> c_int {
8915    crate::xml::sax::default::default_sax_handler::isStandalone(ctx)
8916}
8917
8918/// Upstream SAX2.c `xmlSAX2HasInternalSubset` — public entry point of the default handler.
8919#[no_mangle]
8920pub unsafe extern "C" fn xmlSAX2HasInternalSubset(ctx: *mut c_void) -> c_int {
8921    crate::xml::sax::default::default_sax_handler::hasInternalSubset(ctx)
8922}
8923
8924/// Upstream SAX2.c `xmlSAX2HasExternalSubset` — public entry point of the default handler.
8925#[no_mangle]
8926pub unsafe extern "C" fn xmlSAX2HasExternalSubset(ctx: *mut c_void) -> c_int {
8927    crate::xml::sax::default::default_sax_handler::hasExternalSubset(ctx)
8928}
8929
8930/// Upstream SAX2.c `xmlSAX2GetEntity` — public entry point of the default handler.
8931#[no_mangle]
8932pub unsafe extern "C" fn xmlSAX2GetEntity(
8933    ctx: *mut c_void,
8934    name: *const xmlChar,
8935) -> *mut crate::abi::structs::_xmlEntity {
8936    crate::xml::sax::default::default_sax_handler::getEntity(ctx, name)
8937}
8938
8939/// Upstream SAX2.c `xmlSAX2GetParameterEntity` — public entry point of the default handler.
8940#[no_mangle]
8941pub unsafe extern "C" fn xmlSAX2GetParameterEntity(
8942    ctx: *mut c_void,
8943    name: *const xmlChar,
8944) -> *mut crate::abi::structs::_xmlEntity {
8945    crate::xml::sax::default::default_sax_handler::getParameterEntity(ctx, name)
8946}
8947
8948/// Upstream SAX2.c `xmlSAX2GetLineNumber` — public entry point of the
8949/// default handler (SAX locator callback).
8950#[no_mangle]
8951pub unsafe extern "C" fn xmlSAX2GetLineNumber(ctx: *mut c_void) -> c_int {
8952    crate::xml::sax::default::default_sax_handler::getLineNumber(ctx)
8953}
8954
8955/// Upstream SAX2.c `xmlSAX2GetColumnNumber`.
8956#[no_mangle]
8957pub unsafe extern "C" fn xmlSAX2GetColumnNumber(ctx: *mut c_void) -> c_int {
8958    crate::xml::sax::default::default_sax_handler::getColumnNumber(ctx)
8959}
8960
8961/// Upstream SAX2.c `xmlSAX2GetPublicId`.
8962#[no_mangle]
8963pub const unsafe extern "C" fn xmlSAX2GetPublicId(ctx: *mut c_void) -> *const xmlChar {
8964    crate::xml::sax::default::default_sax_handler::getPublicId(ctx)
8965}
8966
8967/// Upstream SAX2.c `xmlSAX2GetSystemId`.
8968#[no_mangle]
8969pub unsafe extern "C" fn xmlSAX2GetSystemId(ctx: *mut c_void) -> *const xmlChar {
8970    crate::xml::sax::default::default_sax_handler::getSystemId(ctx)
8971}
8972
8973/// Upstream SAX2.c `xmlSAX2StartElement` — SAX1 start-element entry point.
8974/// The candidate parser dispatches through the SAX2 (namespaced) callbacks;
8975/// this wrapper maps to the SAX1 handler when installed.
8976#[no_mangle]
8977pub unsafe extern "C" fn xmlSAX2StartElement(
8978    ctx: *mut c_void,
8979    name: *const xmlChar,
8980    atts: *mut *const xmlChar,
8981) {
8982    // The parser core invokes startElementNs; the SAX1 shim is provided by
8983    // the dispatch layer. When this entry point is installed directly on a
8984    // handler, route through the internal SAX1 path.
8985    crate::xml::sax::dispatch::SaxDispatcher::sax1_start_element(ctx, name, atts);
8986}
8987
8988/// Upstream SAX2.c `xmlSAX2EndElement` — SAX1 end-element entry point.
8989#[no_mangle]
8990pub unsafe extern "C" fn xmlSAX2EndElement(ctx: *mut c_void, name: *const xmlChar) {
8991    crate::xml::sax::dispatch::SaxDispatcher::sax1_end_element(ctx, name);
8992}
8993
8994/// Upstream SAX2.c `xmlSAX2SetDocumentLocator` — public entry point of the default handler.
8995#[no_mangle]
8996pub const unsafe extern "C" fn xmlSAX2SetDocumentLocator(
8997    ctx: *mut c_void,
8998    loc: *mut crate::abi::callbacks::_xmlSAXLocator,
8999) {
9000    crate::xml::sax::default::default_sax_handler::setDocumentLocator(ctx, loc)
9001}
9002
9003/// Upstream SAX2.c `xmlSAX2Reference` — public entry point of the default handler.
9004#[no_mangle]
9005pub unsafe extern "C" fn xmlSAX2Reference(ctx: *mut c_void, name: *const xmlChar) {
9006    crate::xml::sax::default::default_sax_handler::reference(ctx, name)
9007}
9008
9009#[cfg(test)]
9010mod tests {
9011    use super::xml_number_to_string;
9012
9013    /// R-000166: number-to-string follows upstream xmlXPathFormatNumber —
9014    /// verified byte-identical against the oracle (xsltproc) on the t4/n3
9015    /// differential corpora. Cases here are exact doubles or
9016    /// rounding-robust formats (parser-dependent literals are covered by the
9017    /// differential corpora, not unit tests).
9018    #[allow(clippy::approx_constant)]
9019    #[test]
9020    fn test_xml_number_to_string_parity_cases() {
9021        let cases: &[(f64, &str)] = &[
9022            (1234567.891, "1234567.891"),
9023            (0.1 + 0.2, "0.3"),
9024            (1.0 / 3.0, "0.333333333333333"),
9025            (1e20, "1e+20"),
9026            (1e-5, "0.00001"),
9027            (123456789012345678901234567890.0, "1.23456789012346e+29"),
9028            (1e100, "1e+100"),
9029            (-1e100, "-1e+100"),
9030            (1.5e-100, "1.5e-100"),
9031            (1e9, "1000000000"),
9032            (0.00001, "0.00001"),
9033            (9.99e-6, "9.99e-06"),
9034            (2147483646.0, "2147483646"),
9035            (2147483648.0, "2.147483648e+09"),
9036            (-2147483647.0, "-2147483647"),
9037            (-2147483649.0, "-2.147483649e+09"),
9038            (0.5, "0.5"),
9039            (1.0 / 7.0, "0.142857142857143"),
9040            (2.675, "2.675"),
9041            (3.141592653589793, "3.141592653589793"),
9042            (-0.0, "0"),
9043            (0.0, "0"),
9044            (f64::INFINITY, "Infinity"),
9045            (f64::NEG_INFINITY, "-Infinity"),
9046            (f64::NAN, "NaN"),
9047            (0.30000000000000004, "0.3"),
9048            (2.2250738585072014e-308, "2.2250738585072e-308"),
9049            (5e-324, "4.94065645841247e-324"),
9050        ];
9051        for (n, expected) in cases {
9052            assert_eq!(&xml_number_to_string(*n), expected, "value: {}", n);
9053        }
9054    }
9055
9056    /// xmlNewChild with a non-NULL content creates the element and appends a
9057    /// text child (upstream tree.c xmlNewChild -> xmlNewDocNode ->
9058    /// xmlNewDocText + xmlAddChild; tree2.c relies on it — Phase-12
9059    /// EXTERNAL-CONSUMERS court).
9060    ///
9061    /// # Safety
9062    ///
9063    /// - The doc and nodes are created and freed exactly once within the
9064    ///   test; pointers are asserted non-NULL before dereference.
9065    #[test]
9066    fn test_xml_new_child_with_content() {
9067        unsafe {
9068            let doc =
9069                crate::xml::tree::new_doc(c"1.0".as_ptr() as *const crate::abi::types::xmlChar);
9070            assert!(!doc.is_null());
9071            let root = crate::xml::tree::new_node(
9072                core::ptr::null_mut(),
9073                c"root".as_ptr() as *const crate::abi::types::xmlChar,
9074            );
9075            assert!(!root.is_null());
9076            crate::xml::tree::doc_set_root_element(doc, root);
9077
9078            let child = super::xmlNewChild(
9079                root,
9080                core::ptr::null_mut(),
9081                c"node1".as_ptr() as *const crate::abi::types::xmlChar,
9082                c"content of node 1".as_ptr() as *const crate::abi::types::xmlChar,
9083            );
9084            assert!(!child.is_null());
9085            assert!(
9086                !(*child).children.is_null(),
9087                "content must become a text child"
9088            );
9089            assert_eq!(
9090                crate::abi::types::xmlElementType::XML_TEXT_NODE as i32,
9091                (*(*child).children).type_
9092            );
9093            let text = crate::xml::string::xmlstr_to_bytes((*(*child).children).content);
9094            assert_eq!(text, b"content of node 1");
9095
9096            // NULL content stays childless
9097            let empty = super::xmlNewChild(
9098                root,
9099                core::ptr::null_mut(),
9100                c"node2".as_ptr() as *const crate::abi::types::xmlChar,
9101                core::ptr::null(),
9102            );
9103            assert!(!empty.is_null());
9104            assert!((*empty).children.is_null());
9105
9106            crate::xml::tree::free_doc(doc);
9107        }
9108    }
9109
9110    /// xmlNewChild content is parsed as an ATTRIBUTE VALUE (upstream tree.c
9111    /// xmlNewChild -> xmlNewDocNode -> xmlNewElem -> xmlNodeParseAttValue):
9112    /// an EMPTY content adds NO text child (`<bar/>`, SimpleXML bug76712),
9113    /// character references are decoded (`a &#38; b` -> `a & b`, SimpleXML
9114    /// bug44478) and a declared general entity becomes an entity-ref child.
9115    /// The old raw text storage appended an empty text node for "" and kept
9116    /// the reference text verbatim.
9117    ///
9118    /// # Safety
9119    ///
9120    /// - doc/root/child are created and freed exactly once; the text child
9121    ///   content string is read while live.
9122    #[test]
9123    fn test_xml_new_child_parses_content_as_att_value() {
9124        unsafe {
9125            let doc =
9126                crate::xml::tree::new_doc(c"1.0".as_ptr() as *const crate::abi::types::xmlChar);
9127            assert!(!doc.is_null());
9128            let root = crate::xml::tree::new_node(
9129                core::ptr::null_mut(),
9130                c"root".as_ptr() as *const crate::abi::types::xmlChar,
9131            );
9132            assert!(!root.is_null());
9133            crate::xml::tree::doc_set_root_element(doc, root);
9134
9135            // Empty content -> NO text child (upstream value[0] == 0 early
9136            // out); serializes as `<empty/>`.
9137            let e = super::xmlNewChild(
9138                root,
9139                core::ptr::null_mut(),
9140                c"empty".as_ptr() as *const crate::abi::types::xmlChar,
9141                c"".as_ptr() as *const crate::abi::types::xmlChar,
9142            );
9143            assert!(!e.is_null());
9144            assert!(
9145                (*e).children.is_null(),
9146                "empty content must not add a text child"
9147            );
9148
9149            // Character reference content is DECODED into the text child.
9150            let r = super::xmlNewChild(
9151                root,
9152                core::ptr::null_mut(),
9153                c"ref".as_ptr() as *const crate::abi::types::xmlChar,
9154                c"a &#38; b".as_ptr() as *const crate::abi::types::xmlChar,
9155            );
9156            assert!(!r.is_null());
9157            assert!(!(*r).children.is_null());
9158            let txt = crate::xml::string::xmlstr_to_bytes((*(*r).children).content);
9159            assert_eq!(txt, b"a & b");
9160
9161            // A bare '&' (no terminating ';') consumes the '&' and keeps the
9162            // rest as text (upstream xmlNodeParseAttValue, "x & y" -> "x  y").
9163            let b = super::xmlNewChild(
9164                root,
9165                core::ptr::null_mut(),
9166                c"bare".as_ptr() as *const crate::abi::types::xmlChar,
9167                c"x & y".as_ptr() as *const crate::abi::types::xmlChar,
9168            );
9169            assert!(!b.is_null());
9170            assert!(!(*b).children.is_null());
9171            let txt = crate::xml::string::xmlstr_to_bytes((*(*b).children).content);
9172            assert_eq!(txt, b"x  y");
9173
9174            crate::xml::tree::free_doc(doc);
9175        }
9176    }
9177
9178    /// Phase 14.3 Bug-2 regression: `xmlFreeProp` must not free a
9179    /// dict-interned attribute name. PHP's SimpleXML unset path
9180    /// (`sxe_unlink_node` -> `php_libxml_node_free` -> `xmlFreeProp`) frees an
9181    /// attribute whose name the parser interned in the document dictionary;
9182    /// the pre-fix `free_prop_impl` freed the interned string, and
9183    /// `xmlDictFree` at doc teardown freed it again (double free). Mirrors the
9184    /// PHP sequence: unlink the attribute, free it via `xmlFreeProp`, then
9185    /// free the document.
9186    ///
9187    /// # Safety
9188    ///
9189    /// - doc/dict/root/attr are built and freed exactly once; a double free
9190    ///   (the bug) aborts the test process under glibc tcache detection.
9191    #[test]
9192    fn test_xml_free_prop_preserves_dict_interned_attr_name() {
9193        unsafe {
9194            use crate::abi::allocator::xmlMallocZero;
9195            use crate::abi::structs::{_xmlAttr, _xmlNode};
9196            use core::mem::size_of;
9197
9198            let dict = super::xmlDictCreate();
9199            assert!(!dict.is_null());
9200            let doc =
9201                crate::xml::tree::new_doc(c"1.0".as_ptr() as *const crate::abi::types::xmlChar);
9202            assert!(!doc.is_null());
9203            (*doc).dict = dict;
9204            let root = crate::xml::tree::new_node(
9205                core::ptr::null_mut(),
9206                c"root".as_ptr() as *const crate::abi::types::xmlChar,
9207            );
9208            assert!(!root.is_null());
9209            crate::xml::tree::doc_set_root_element(doc, root);
9210
9211            // Intern the attribute name exactly as the dictNames parser does.
9212            let iname = super::xmlDictLookup(
9213                dict,
9214                c"id".as_ptr() as *const crate::abi::types::xmlChar,
9215                -1,
9216            );
9217            assert!(!iname.is_null());
9218
9219            // Build the attribute node with the borrowed dict-interned name
9220            // (mirrors parser_set_prop attribute creation).
9221            let attr = xmlMallocZero(size_of::<_xmlAttr>()) as *mut _xmlAttr;
9222            assert!(!attr.is_null());
9223            (*attr).type_ = crate::abi::types::xmlElementType::XML_ATTRIBUTE_NODE as i32;
9224            (*attr).name = iname as *mut crate::abi::types::xmlChar;
9225            (*attr).parent = root;
9226            (*attr).doc = doc;
9227            (*root).properties = attr;
9228
9229            // PHP SimpleXML unset: xmlUnlinkNode(attr) then xmlFreeProp(attr).
9230            crate::xml::tree::unlink_node(attr as *mut _xmlNode);
9231            crate::abi::exports_tree::xmlFreeProp(attr);
9232
9233            // Teardown: xmlDictFree reaches refcount 0 and frees the interned
9234            // string once. A pre-fix double free aborts here.
9235            crate::xml::tree::free_doc(doc);
9236        }
9237    }
9238
9239    /// Phase 14.3 regression: `xmlNodeListGetString` over a non-text node
9240    /// list must return a NUL-terminated EMPTY string. The pre-fix
9241    /// `xml_strdup(b"")` idiom handed xml_strdup a dangling 0x1 pointer
9242    /// (Rust zero-length byte-string literal), crashing in xml_strlen when
9243    /// SimpleXML string-casts an element whose children produce no text
9244    /// (ext/simplexml 027/028).
9245    ///
9246    /// # Safety
9247    ///
9248    /// - doc/root/person are built and freed exactly once; returned strings
9249    ///   are freed with `xmlFreeImpl`.
9250    #[test]
9251    fn test_nodelist_getstring_empty_from_element_chain() {
9252        unsafe {
9253            let doc =
9254                crate::xml::tree::new_doc(c"1.0".as_ptr() as *const crate::abi::types::xmlChar);
9255            assert!(!doc.is_null());
9256            let root = crate::xml::tree::new_node(
9257                core::ptr::null_mut(),
9258                c"people".as_ptr() as *const crate::abi::types::xmlChar,
9259            );
9260            assert!(!root.is_null());
9261            crate::xml::tree::doc_set_root_element(doc, root);
9262            let person = crate::abi::exports_tree::xmlNewTextChild(
9263                root,
9264                core::ptr::null_mut(),
9265                c"person".as_ptr() as *const crate::abi::types::xmlChar,
9266                c"Joe".as_ptr() as *const crate::abi::types::xmlChar,
9267            );
9268            assert!(!person.is_null());
9269            super::xmlSetProp(
9270                person,
9271                c"gender".as_ptr() as *const crate::abi::types::xmlChar,
9272                c"male".as_ptr() as *const crate::abi::types::xmlChar,
9273            );
9274
9275            // Element list head yields an empty string (pre-fix: crash).
9276            let s1 = crate::abi::exports_treedump::xmlNodeListGetString(doc, (*root).children, 1);
9277            assert!(!s1.is_null());
9278            assert_eq!(*s1 as u8, 0);
9279            crate::abi::allocator::xmlFreeImpl(s1 as *mut core::ffi::c_void);
9280
9281            // Text child list yields the element text.
9282            let s2 = crate::abi::exports_treedump::xmlNodeListGetString(doc, (*person).children, 1);
9283            assert!(!s2.is_null());
9284            let b = crate::xml::string::xmlstr_to_bytes(s2);
9285            assert_eq!(b, b"Joe");
9286            crate::abi::allocator::xmlFreeImpl(s2 as *mut core::ffi::c_void);
9287
9288            crate::xml::tree::free_doc(doc);
9289        }
9290    }
9291
9292    /// Phase 14.3 (simplexml S3 / ext/simplexml 008): with no structured
9293    /// handler, a failed XPath compile delivers the message VERBATIM to the
9294    /// generic channel — upstream xpath.c xmlXPathErrFmt sets
9295    /// `channel = xmlGenericError; data = xmlGenericErrorContext`, and
9296    /// xmlVRaiseError calls `channel(data, "%s", to->message)` because the
9297    /// generic channel is NOT one of the parser channels that trigger
9298    /// xmlFormatError's fragment stream (which would prefix "XPath error :").
9299    /// PHP installs a generic handler at request start (php_libxml_issue_
9300    /// warning), so the raw text "Invalid expression\n" must arrive alone;
9301    /// a pre-fix `GenericDelivery::Stream` reached PHP's handler with the
9302    /// fragment prefix and ext/simplexml 008 warned "XPath error : Invalid
9303    /// expression".
9304    ///
9305    /// # Safety
9306    ///
9307    /// - doc/ctxt are created and freed exactly once; `captured` lives on
9308    ///   the stack for the duration of the call; the generic handler slot is
9309    ///   restored to the default printer before the test ends.
9310    #[test]
9311    fn test_xpath_compile_error_verbatim_to_generic_channel() {
9312        use crate::abi::callbacks::xmlGenericErrorFunc;
9313        use core::ffi::{c_char, c_void};
9314
9315        // Serialized against the handler-slot tests in xml::globals (11.1-X)
9316        // and xml::errors: the generic handler slot is shared global state.
9317        let _guard = crate::xml::globals::ERROR_HANDLER_TEST_LOCK.lock();
9318        unsafe {
9319            let mut captured: Vec<u8> = Vec::new();
9320            let captured_ptr = &mut captured as *mut Vec<u8> as *mut c_void;
9321
9322            unsafe extern "C" fn record(ctx: *mut c_void, msg: *const c_char) {
9323                if msg.is_null() {
9324                    return;
9325                }
9326                let out = &mut *(ctx as *mut Vec<u8>);
9327                let bytes = std::ffi::CStr::from_ptr(msg).to_bytes();
9328                out.extend_from_slice(bytes);
9329            }
9330
9331            super::xmlSetGenericErrorFunc(captured_ptr, Some(record as xmlGenericErrorFunc));
9332
9333            let doc =
9334                crate::xml::tree::new_doc(c"1.0".as_ptr() as *const crate::abi::types::xmlChar);
9335            assert!(!doc.is_null());
9336            let ctxt = super::xmlXPathNewContext(doc);
9337            assert!(!ctxt.is_null());
9338            // No structured handler on the context (ctxt->error stays NULL)
9339            // so the generic channel is the delivery target.
9340            (*ctxt).error = None;
9341
9342            let comp = crate::xml::xpath::exports::xmlXPathCtxtCompile(
9343                ctxt,
9344                c"**".as_ptr() as *const crate::abi::types::xmlChar,
9345            );
9346            assert!(comp.is_null(), "`**` must fail to compile");
9347
9348            assert_eq!(
9349                captured, b"Invalid expression\n",
9350                "generic channel must receive the raw message, no \"XPath error : \" prefix"
9351            );
9352
9353            // Restore the generic handler slot to the default printer.
9354            super::xmlSetGenericErrorFunc(core::ptr::null_mut(), None);
9355            super::xmlXPathFreeContext(ctxt);
9356            crate::xml::tree::free_doc(doc);
9357        }
9358    }
9359
9360    /// Phase 14.3 (simplexml S7 / bug63575): node/document copies must NOT
9361    /// carry the source `_private` — upstream xmlStaticCopyNode zeroes the
9362    /// new node and xmlCopyNamespaceList never copies _private. PHP keys its
9363    /// wrapper registrations on `_private` (php_libxml_node_ptr), so a
9364    /// copied subtree that inherits the original's registrations binds the
9365    /// clone to the ORIGINAL document: SimpleXML root-element clone
9366    /// (xmlCopyDoc) then resolved XPath and mutations into the original's
9367    /// tree. The copy must look UNREGISTERED.
9368    ///
9369    /// # Safety
9370    ///
9371    /// - doc/root/marker are created and freed exactly once within the test;
9372    ///   pointers are asserted non-NULL before dereference.
9373    #[test]
9374    fn test_copies_do_not_carry_private() {
9375        unsafe {
9376            let doc =
9377                crate::xml::tree::new_doc(c"1.0".as_ptr() as *const crate::abi::types::xmlChar);
9378            assert!(!doc.is_null());
9379            let root = crate::xml::tree::new_node(
9380                core::ptr::null_mut(),
9381                c"a".as_ptr() as *const crate::abi::types::xmlChar,
9382            );
9383            assert!(!root.is_null());
9384            crate::xml::tree::doc_set_root_element(doc, root);
9385
9386            // PHP-style registration marker on the SOURCE node.
9387            let marker: usize = 0x5A5A;
9388            (*root)._private = marker as *mut core::ffi::c_void;
9389
9390            // xmlCopyDoc: the new root must be unregistered.
9391            let cdoc = super::xmlCopyDoc(doc, 1);
9392            assert!(!cdoc.is_null());
9393            let croot = (*cdoc).children;
9394            assert!(!croot.is_null());
9395            assert!(
9396                (*croot)._private.is_null(),
9397                "xmlCopyDoc must not carry the source node _private"
9398            );
9399            assert_eq!(
9400                (*root)._private as usize,
9401                marker,
9402                "source _private is untouched"
9403            );
9404            crate::xml::tree::free_doc(cdoc);
9405
9406            // xmlDocCopyNode into the same doc: the copy must be unregistered.
9407            let copy = crate::abi::exports_treedump::xmlDocCopyNode(root, doc, 1);
9408            assert!(!copy.is_null());
9409            assert!(
9410                (*copy)._private.is_null(),
9411                "xmlDocCopyNode must not carry the source node's _private"
9412            );
9413            // The detached copy owns a duplicated name string: free it
9414            // directly, then the document.
9415            super::xmlFreeNode(copy);
9416            crate::xml::tree::free_doc(doc);
9417        }
9418    }
9419
9420    /// The C-extension-function bridge must expose the invoked function's
9421    /// LOCAL name and namespace URI on `ctxt->context->function` /
9422    /// `functionURI` for the duration of the call, and restore the previous
9423    /// values afterwards (upstream xpath.c xmlXPathCompOpEval, XPATH_OP_
9424    /// FUNCTION). PHP registers ONE trampoline for every custom-namespace
9425    /// XPath function and dispatches to the PHP closure from those two
9426    /// fields — without them the dom/xsl php:function callbacks dereference
9427    /// garbage (SP-14.3.6-dom O1: return_dom_node_from_xpath /
9428    /// registerPhpFunctionNS / gh22077 segv).
9429    ///
9430    /// # Safety
9431    ///
9432    /// - doc/ctxt/result are created and freed exactly once; the callback
9433    ///   reads only the two fields the bridge must set.
9434    #[test]
9435    fn test_c_xpath_function_bridge_exposes_function_and_uri() {
9436        unsafe {
9437            use crate::xml::xpath::exports::{xmlXPathNewString, xmlXPathValuePush};
9438            use crate::xml::xpath::parser_context::XmlXPathParserContext;
9439            use std::os::raw::{c_char, c_int};
9440
9441            /// Mirrors PHP's dom_xpath_ext_fetch_intern: read the function
9442            /// identity the invoker set on the context.
9443            unsafe extern "C" fn capture_identity(ctxt: *mut core::ffi::c_void, _nargs: c_int) {
9444                let pc = ctxt as *mut XmlXPathParserContext;
9445                let ctx = (*pc).context;
9446                assert!(!ctx.is_null());
9447                let name = (*ctx).function;
9448                let uri = (*ctx).functionURI;
9449                assert!(!name.is_null());
9450                let name_s = std::ffi::CStr::from_ptr(name as *const c_char)
9451                    .to_string_lossy()
9452                    .into_owned();
9453                let uri_s = if uri.is_null() {
9454                    "(null)".to_string()
9455                } else {
9456                    std::ffi::CStr::from_ptr(uri as *const c_char)
9457                        .to_string_lossy()
9458                        .into_owned()
9459                };
9460                let out = std::ffi::CString::new(format!("{}@{}", name_s, uri_s)).unwrap();
9461                let obj = xmlXPathNewString(out.as_ptr() as *const crate::abi::types::xmlChar);
9462                assert!(!obj.is_null());
9463                assert_eq!(xmlXPathValuePush(ctxt, obj), 0);
9464            }
9465
9466            let doc =
9467                crate::xml::tree::new_doc(c"1.0".as_ptr() as *const crate::abi::types::xmlChar);
9468            assert!(!doc.is_null());
9469            let ctxt = super::xmlXPathNewContext(doc);
9470            assert!(!ctxt.is_null());
9471
9472            super::xmlXPathRegisterNs(
9473                ctxt,
9474                c"t".as_ptr() as *const crate::abi::types::xmlChar,
9475                c"urn:t".as_ptr() as *const crate::abi::types::xmlChar,
9476            );
9477            super::xmlXPathRegisterFuncNS(
9478                ctxt,
9479                c"capture".as_ptr() as *const crate::abi::types::xmlChar,
9480                c"urn:t".as_ptr() as *const crate::abi::types::xmlChar,
9481                Some(capture_identity),
9482            );
9483
9484            let res = super::xmlXPathEvalExpression(
9485                c"t:capture()".as_ptr() as *const crate::abi::types::xmlChar,
9486                ctxt,
9487            );
9488            assert!(!res.is_null());
9489            let s = crate::xml::string::xmlstr_to_bytes((*res).stringval);
9490            assert_eq!(
9491                s, b"capture@urn:t",
9492                "bridge must set context->function (local name) and ->functionURI (ns)"
9493            );
9494            super::xmlXPathFreeObject(res);
9495            super::xmlXPathFreeContext(ctxt);
9496            crate::xml::tree::free_doc(doc);
9497        }
9498    }
9499}