Skip to main content

libxml_rs/abi/
allocator.rs

1//! C ABI allocator compatibility — xmlMemSetup, xmlMemGet, xmlMalloc, xmlFree, etc. (§58).
2//!
3//! This module implements the complete memory hook system exposed by libxml2:
4//! - Global allocator function pointers (malloc, realloc, free, strdup)
5//! - `xmlMemSetup()` / `xmlMemGet()` — set/get allocator hooks
6//! - `xmlGcMemSetup()` / `xmlGcMemGet()` — GC-aware allocator hooks (wrappers)
7//! - `xmlMalloc()` / `xmlMallocAtomic()` / `xmlRealloc()` / `xmlFree()` / `xmlMemStrdup()`
8//! - `xmlMemUsed()` / `xmlMemBlocks()` — allocation tracking
9//! - `xmlMemDisplay()` / `xmlMemShow()` — debugging output
10//!
11//! # Phase 1 status
12//!
13//! Complete — all allocator APIs are implemented; state lives in the five
14//! exported function-pointer variables exactly as upstream (globals.c),
15//! giving `xmlMemSetup` and direct `xmlMalloc = custom` assignment one
16//! shared override mechanism (R-000176). Since 11.1-Z.3 (R-000178) the
17//! default bodies are plain libc `malloc`/`realloc`/`free`/`strdup`
18//! wrappers — no Rust `std::alloc` layout fabrication (that was UB) and no
19//! accounting, byte-identical with upstream's `globals.c` defaults
20//! (`xmlMalloc = malloc` etc.).
21//!
22//! # Safety
23//!
24//! Allocator hooks are `unsafe` because they operate on raw pointers and are called
25//! from C code. Every public function documents its safety contract.
26//!
27//! # Upstream contract
28//!
29//! The parity target is libxml2 2.15.3 (`SRC-LIBXML2-2.15.0-XMLMEMORY-C`:
30//! `oracle/historical/src/libxml2-2.15.0/xmlmemory.c`) plus the allocator globals
31//! of `globals.c`. The 5 allocator entry points (`xmlMalloc`, `xmlMallocAtomic`,
32//! `xmlRealloc`, `xmlFree`, `xmlMemStrdup`) are exported as DATA function-pointer
33//! globals matching the upstream `XMLPUBVAR` declarations (R-000162). Upstream
34//! initializes them to the C runtime functions (`xmlFree = free`, `xmlMalloc =
35//! malloc`, `xmlMallocAtomic = malloc`, `xmlRealloc = realloc`, `xmlMemStrdup =
36//! xmlPosixStrdup`); the candidate initializes them to the `*Default` bodies,
37//! which are libc wrappers with identical observable behavior.
38//!
39//! # Conceptual behavior
40//!
41//! This module implements the complete libxml2 memory-hook system: swappable
42//! allocator hooks via `xmlMemSetup`/`xmlMemGet` (and the GC aliases), plus the
43//! deprecated debug-named surface (`xmlMemMalloc`/`xmlMemFree`/`xmlMemRealloc`/
44//! `xmlMemoryStrdup` and the `*Loc` variants) which upstream keeps as a
45//! separately-tagged debug allocator. There are therefore two allocation
46//! planes, exactly as in upstream 2.15.0:
47//!
48//!   - the five exported variables (the hook system): default = libc, and
49//!     `xmlMemSetup`/direct assignment re-route them. Untracked — upstream's
50//!     `debugMemSize`/`debugMemBlocks` counters are only maintained by the
51//!     debug allocator, so with the default installed `xmlMemUsed()` == 0,
52//!     `xmlMemBlocks()` == 0 and `xmlMemSize()` == 0 (verified against the
53//!     oracle);
54//!   - the debug-named surface (deprecated, exported for legacy consumers):
55//!     always libc-backed and tracked by the per-block registry, mirroring
56//!     upstream `xmlMemMalloc` et al. `xmlMemSize` returns the recorded size
57//!     for these blocks and `xmlMemUsed`/`xmlMemBlocks` count them.
58//!
59//! The display entry points (`xmlMemDisplay`, `xmlMemDisplayLast`, `xmlMemShow`,
60//! `xmlMemoryDump`) are no-ops matching upstream 2.15.0, which removed that
61//! feature.
62//!
63//! # Ownership & safety invariants
64//!
65//! Every pointer returned by an xml* allocator must be freed with `xmlFree`
66//! (OWNERSHIP_ATLAS section 1). The block registry records
67//! ptr -> (size, file, line) for the debug-named surface only, so
68//! `xmlMemSize` is exact for debug-surface blocks and 0 for default-allocator
69//! blocks (upstream's MEMHDR tag lookup behaves identically: a plain `malloc`
70//! block carries no tag). `xmlMemSetup` custom allocators bypass the registry
71//! entirely, matching upstream's debug-allocator-only contract.
72//!
73//! # Historical quirks & epochs
74//!
75//! R-000178 (11.1-Z.3): the pre-Z.3 default allocator routed through Rust's
76//! global allocator with fabricated `Layout`s — `default_free` deallocated
77//! every pointer with a 1-byte layout and `default_realloc` passed the
78//! requested new size as the old allocation layout; both are invalid-layout
79//! UB under the Rust allocator contract. Replaced with libc
80//! `malloc`/`realloc`/`free` (C allocation semantics; no layout exists), and
81//! the default no longer maintains the accounting registry so `xmlMemUsed`/
82//! `xmlMemBlocks`/`xmlMemSize` match the oracle's 0s; the registry now backs
83//! only the debug-named surface. R-000131 (11.1-J) sealed: `xmlMemSize`
84//! returns the recorded size for debug-surface blocks, the `*Loc` variants
85//! accept-and-ignore file/line exactly like upstream 2.15.0's `ATTRIBUTE_UNUSED`
86//! parameters, and the display functions are upstream-faithful no-ops.
87//! R-000133 (11.1-H): the legacy names
88//! (`xmlMemMalloc`/`xmlMemFree`/`xmlMemRealloc`/`xmlMemoryStrdup`) were
89//! declared-but-unexported and had to be implemented for the honest-header
90//! rule.
91//!
92//! # Deliberate oddities
93//!
94//! `xmlMemSetup`/direct variable assignment bypass the accounting registry
95//! (deliberate: upstream's block table exists only in the debug allocator).
96//! The five exported variables (`xmlMalloc`, `xmlMallocAtomic`, `xmlRealloc`,
97//! `xmlFree`, `xmlMemStrdup`) are the single source of truth (R-000176,
98//! 11.1-Z.2): `xmlMemSetup` assigns them and every internal allocation reads
99//! them through the `*Impl` indirection, exactly like upstream internal
100//! `xmlMalloc(...)` calls. The debug-named functions deliberately do NOT
101//! route through the variables (upstream's debug allocator is independent of
102//! the hooks); they are always libc-backed + registry-tracked.
103//!
104//! # Proving courts
105//!
106//! ABI-DATA, ALLOCATOR, ALLOCATOR-DEFAULT, GLOBAL-STATE and THREADING court
107//! families; the allocator probes (`tools/abi/*_probe.py` +
108//! `courts/suites/data-abi/*`) compile the same C probe against the oracle DSO
109//! and the candidate and require byte-identical output; ALLOCATOR-DEFAULT-001
110//! proves the default-allocator contract (many sizes, zero-size, grow/shrink
111//! realloc, realloc-to-zero, realloc/malloc failure, strdup, direct
112//! exported-variable calls, long churn, `xmlMemSize`/`xmlMemUsed`/`xmlMemBlocks`
113//! exactness — all byte-identical with the oracle, R-000178); the DSO-LOADER
114//! court resolves every exported symbol from the built DSO.
115//!
116//! # Tempting simplifications that would break parity
117//!
118//! A tempting simplification is to keep the pre-Z.3 default allocator — Rust
119//! `std::alloc` with fabricated layouts is invalid-layout UB (R-000178) and
120//! returning nonzero `xmlMemUsed`/`xmlMemBlocks` under the default diverges
121//! from the oracle's 0s. Another tempting shortcut is exporting the allocator
122//! entry points as plain functions — upstream exports them as data function
123//! pointers, so the allocator-override mechanism (`xmlMalloc` = custom) could
124//! not link (R-000162 lesson).
125
126use core::ffi::c_void;
127use core::ptr;
128use core::sync::atomic::AtomicUsize;
129use core::sync::atomic::Ordering;
130use std::os::raw::{c_char, c_int, c_long};
131
132use crate::abi::callbacks::*;
133
134// ═══════════════════════════════════════════════════════════════════════════════
135// Default Allocator (libc — upstream 2.15.0 globals.c defaults, R-000178)
136// ═══════════════════════════════════════════════════════════════════════════════
137//
138// Upstream initializes the five exported allocator variables to the C runtime
139// functions (`xmlFree = free`, `xmlMalloc = malloc`, `xmlMallocAtomic = malloc`,
140// `xmlRealloc = realloc`, `xmlMemStrdup = xmlPosixStrdup`). The candidate's
141// `*Default` bodies are libc wrappers with identical observable behavior:
142//
143//   - malloc(0)      -> glibc returns a unique non-NULL pointer (C semantics);
144//   - realloc(p, 0)  -> glibc frees p and returns NULL (C semantics);
145//   - realloc(NULL,n)-> malloc(n) (C semantics);
146//   - realloc failure-> NULL with the old block left intact (C semantics);
147//   - free(NULL)     -> no-op (C semantics).
148//
149// All of these are byte-identical with the oracle (verified by the
150// ALLOCATOR-DEFAULT-001 differential court). The pre-Z.3 implementation used
151// Rust's `std::alloc` with fabricated `Layout`s: `default_free` deallocated
152// every pointer with a 1-byte layout and `default_realloc` passed the
153// requested NEW size as the OLD allocation layout. Rust's allocator API
154// requires the deallocation/reallocation layout to correspond to the original
155// allocation, so both were invalid-layout UB — the defect R-000178. libc
156// allocation has no layout parameter, so the C contract is reproduced exactly
157// and the UB class is eliminated.
158
159/// Default malloc: `libc::malloc`.
160///
161/// # SAFETY
162///
163/// - `size` must be a valid allocation size (0 is handled by the platform
164///   `malloc` contract, matching upstream which calls `malloc` directly)
165/// - Returns NULL on allocation failure
166unsafe extern "C" fn default_malloc(size: usize) -> *mut c_void {
167    unsafe { libc::malloc(size) }
168}
169
170/// Default realloc: `libc::realloc`.
171///
172/// # SAFETY
173///
174/// - `ptr` must be a valid pointer from a previous `default_malloc` or `default_realloc`,
175///   or NULL (in which case this behaves like malloc)
176/// - `size` must be a valid allocation size; `realloc(p, 0)` follows the C
177///   contract (glibc frees `p` and returns NULL), matching upstream which
178///   calls `realloc` directly
179/// - On failure the old block is left intact (C contract)
180unsafe extern "C" fn default_realloc(ptr: *mut c_void, size: usize) -> *mut c_void {
181    unsafe { libc::realloc(ptr, size) }
182}
183
184/// Default free: `libc::free`.
185///
186/// # SAFETY
187///
188/// - `ptr` may be NULL (free(NULL) is a no-op)
189/// - If non-NULL, `ptr` must be from a previous `default_malloc` or `default_realloc`
190unsafe extern "C" fn default_free(ptr: *mut c_void) {
191    if ptr.is_null() {
192        return;
193    }
194    unsafe { libc::free(ptr) };
195}
196
197/// Default strdup: `libc::malloc` + copy (upstream `xmlPosixStrdup` ->
198/// `xmlCharStrdup`, which is a NULL-checked malloc+copy).
199///
200/// # SAFETY
201///
202/// - `str` must be a valid null-terminated C string or NULL (NULL returns NULL,
203///   matching upstream `xmlCharStrdup`)
204unsafe extern "C" fn default_strdup(str: *const c_char) -> *mut c_void {
205    if str.is_null() {
206        return ptr::null_mut();
207    }
208    let len = unsafe { libc::strlen(str) };
209    let size = len + 1; // include null terminator
210    let new_ptr = unsafe { libc::malloc(size) };
211    if new_ptr.is_null() {
212        return ptr::null_mut();
213    }
214    unsafe { ptr::copy_nonoverlapping(str as *const u8, new_ptr as *mut u8, size) };
215    new_ptr
216}
217
218// ═══════════════════════════════════════════════════════════════════════════════
219// Global Allocator State — single source of truth (11.1-Z.2, R-000176)
220// ═══════════════════════════════════════════════════════════════════════════════
221//
222// Upstream (xmlmemory.c xmlMemSetup/xmlMemGet, globals.c) keeps exactly ONE
223// allocator state: the five exported DATA variables `xmlFree`, `xmlMalloc`,
224// `xmlMallocAtomic`, `xmlRealloc`, `xmlMemStrdup`. `xmlMemSetup` assigns them,
225// `xmlMemGet` reads them, and EVERY internal allocation calls through the
226// variables — so assigning `xmlMalloc = custom;` directly is equivalent to
227// `xmlMemSetup(...)`. The candidate previously kept a separate `ALLOCATOR`
228// RwLock consulted by the `*Impl` bodies, so a direct public-variable
229// assignment changed the exported symbol but not internal allocations, and
230// `xmlMemSetup` changed internal allocations but not the exported symbols:
231// two sources of truth. This was the xmlGcMemSetup-class defect R-000176
232// (11.1-Z.2). The merged model below restores the upstream single source:
233//
234//   - the five exported `static mut` fn-pointer variables ARE the state;
235//   - the `*Default` functions are the initial values (Rust global allocator
236//     + the accounting registry) and never read the variables;
237//   - the `*Impl` functions are the indirection every internal call site
238//     uses: they read the current variable, so custom hooks installed via
239//     `xmlMemSetup` OR direct assignment are observed everywhere.
240//
241// The write/read contract matches upstream: `xmlMemSetup`/assignment must
242// happen before concurrent use (upstream: "This has to be called before any
243// other libxml routines !"). Rust `static mut` access is `unsafe` and the
244// crate upholds the upstream single-threaded-setup ordering.
245
246/// Global allocation counters (for xmlMemUsed/xmlMemBlocks).
247///
248/// These use relaxed ordering since they are approximate debugging counters.
249/// Since 11.1-Z.3 (R-000178) they are maintained ONLY by the debug-named
250/// surface (`xmlMemMalloc`/`xmlMemFree`/`xmlMemRealloc`/`xmlMemoryStrdup` and
251/// the `*Loc` variants) — exactly like upstream's `debugMemSize`/
252/// `debugMemBlocks`, which the plain-malloc default never touches. With the
253/// default allocator installed, `xmlMemUsed()` and `xmlMemBlocks()` return 0
254/// (byte-identical with the oracle).
255static MEM_USED: AtomicUsize = AtomicUsize::new(0);
256static MEM_BLOCKS: AtomicUsize = AtomicUsize::new(0);
257
258/// Per-block metadata (upstream xmlmemory.c block table): enables
259/// xmlMemSize and the counters for the debug-named surface.
260/// The file pointer is stored as a raw address (usize) so the registry stays
261/// Send + Sync. The `file`/`line` fields mirror upstream's allocation-site
262/// record (populated by the `*Loc` variants); they are write-only until a
263/// future dump surface reads them, hence `allow(dead_code)`.
264#[derive(Clone, Copy)]
265#[allow(dead_code)]
266struct BlockMeta {
267    size: usize,
268    file: usize,
269    line: c_int,
270}
271
272static BLOCKS: once_cell::sync::Lazy<
273    parking_lot::Mutex<std::collections::HashMap<usize, BlockMeta>>,
274> = once_cell::sync::Lazy::new(|| parking_lot::Mutex::new(std::collections::HashMap::new()));
275
276/// Record a block in the registry (no-op for NULL).
277unsafe fn block_record(ptr: *mut c_void, size: usize, file: *const c_char, line: c_int) {
278    if ptr.is_null() {
279        return;
280    }
281    BLOCKS.lock().insert(
282        ptr as usize,
283        BlockMeta {
284            size,
285            file: file as usize,
286            line,
287        },
288    );
289}
290
291/// Drop a block from the registry; returns its recorded size (None if the
292/// block was unknown or NULL).
293unsafe fn block_forget(ptr: *mut c_void) -> Option<usize> {
294    if ptr.is_null() {
295        return None;
296    }
297    BLOCKS.lock().remove(&(ptr as usize)).map(|m| m.size)
298}
299
300// ═══════════════════════════════════════════════════════════════════════════════
301// Public Allocator API
302// ═══════════════════════════════════════════════════════════════════════════════
303
304/// Set custom memory allocator functions (upstream xmlmemory.h).
305///
306/// # UPSTREAM-PARITY
307///
308/// ```c
309/// int xmlMemSetup(xmlFreeFunc freeFunc,
310///                 xmlMallocFunc mallocFunc,
311///                 xmlReallocFunc reallocFunc,
312///                 xmlStrdupFunc strdupFunc);
313/// ```
314///
315/// Returns -1 if any function is NULL (upstream xmlmemory.c: "Returns 0 on
316/// success"); otherwise assigns the five exported allocator variables
317/// (xmlFree, xmlMalloc, xmlMallocAtomic = mallocFunc, xmlRealloc,
318/// xmlMemStrdup) and returns 0. The exported variables are the single source
319/// of truth: every internal allocation reads them through the `*Impl`
320/// indirection, so this call — or a direct `xmlMalloc = custom` assignment —
321/// re-routes all allocations immediately (R-000176 fix).
322///
323/// # SAFETY
324///
325/// - All function pointers must be valid (non-null) and thread-safe
326/// - The functions must follow the C malloc/realloc/free/strdup contract
327/// - Once set, the functions remain in effect until the next `xmlMemSetup` call
328/// - The caller is responsible for ensuring the functions remain valid for the
329///   entire time they are installed
330/// - Must not race with concurrent allocation (upstream ordering contract:
331///   "This has to be called before any other libxml routines !")
332#[no_mangle]
333pub unsafe extern "C" fn xmlMemSetup(
334    freeFunc: Option<xmlFreeFunc>,
335    mallocFunc: Option<xmlMallocFunc>,
336    reallocFunc: Option<xmlReallocFunc>,
337    strdupFunc: Option<xmlStrdupFunc>,
338) -> c_int {
339    if freeFunc.is_none() || mallocFunc.is_none() || reallocFunc.is_none() || strdupFunc.is_none() {
340        return -1;
341    }
342    // SAFETY: callers must uphold the upstream single-threaded-setup
343    // ordering; each value is a non-NULL C function pointer (checked above).
344    unsafe {
345        xmlFree = freeFunc.unwrap();
346        xmlMalloc = mallocFunc.unwrap();
347        xmlMallocAtomic = mallocFunc.unwrap();
348        xmlRealloc = reallocFunc.unwrap();
349        xmlMemStrdup = strdupFunc.unwrap();
350    }
351    0
352}
353
354/// Get the current memory allocator functions (upstream xmlmemory.h).
355///
356/// # UPSTREAM-PARITY
357///
358/// ```c
359/// int xmlMemGet(xmlFreeFunc *freeFunc,
360///               xmlMallocFunc *mallocFunc,
361///               xmlReallocFunc *reallocFunc,
362///               xmlStrdupFunc *strdupFunc);
363/// ```
364///
365/// Writes the current exported allocator variables through NULL-tolerant
366/// output pointers (upstream xmlmemory.c: NULL outputs are skipped) and
367/// returns 0.
368///
369/// # SAFETY
370///
371/// - All non-NULL output pointers must be valid and writable
372#[no_mangle]
373pub unsafe extern "C" fn xmlMemGet(
374    freeFunc: *mut Option<xmlFreeFunc>,
375    mallocFunc: *mut Option<xmlMallocFunc>,
376    reallocFunc: *mut Option<xmlReallocFunc>,
377    strdupFunc: *mut Option<xmlStrdupFunc>,
378) -> c_int {
379    // SAFETY: callers must pass NULL or valid writable pointers; reads of
380    // the exported variables are safe under the upstream setup ordering.
381    unsafe {
382        if !freeFunc.is_null() {
383            ptr::write(freeFunc, Some(xmlFree));
384        }
385        if !mallocFunc.is_null() {
386            ptr::write(mallocFunc, Some(xmlMalloc));
387        }
388        if !reallocFunc.is_null() {
389            ptr::write(reallocFunc, Some(xmlRealloc));
390        }
391        if !strdupFunc.is_null() {
392            ptr::write(strdupFunc, Some(xmlMemStrdup));
393        }
394    }
395    0
396}
397
398/// Set GC-aware memory allocator functions (upstream xmlmemory.h).
399///
400/// # UPSTREAM-PARITY
401///
402/// ```c
403/// int xmlGcMemSetup(xmlFreeFunc freeFunc,
404///                   xmlMallocFunc mallocFunc,
405///                   xmlMallocFunc mallocAtomicFunc,
406///                   xmlReallocFunc reallocFunc,
407///                   xmlStrdupFunc strdupFunc);
408/// ```
409///
410/// Same contract as `xmlMemSetup` with a dedicated `mallocAtomicFunc` for
411/// atomic allocations (upstream xmlmemory.c: `xmlMallocAtomic =
412/// mallocAtomicFunc`). Returns -1 if any function is NULL, else 0.
413///
414/// # SAFETY
415///
416/// - All function pointers must be valid (non-null) and thread-safe
417/// - The functions must follow the C malloc/realloc/free/strdup contract
418/// - Must not race with concurrent allocation (upstream ordering contract)
419/// - The caller is responsible for ensuring the functions remain valid for
420///   the entire time they are installed
421#[no_mangle]
422pub unsafe extern "C" fn xmlGcMemSetup(
423    freeFunc: Option<xmlFreeFunc>,
424    mallocFunc: Option<xmlMallocFunc>,
425    mallocAtomicFunc: Option<xmlMallocFunc>,
426    reallocFunc: Option<xmlReallocFunc>,
427    strdupFunc: Option<xmlStrdupFunc>,
428) -> c_int {
429    if freeFunc.is_none()
430        || mallocFunc.is_none()
431        || mallocAtomicFunc.is_none()
432        || reallocFunc.is_none()
433        || strdupFunc.is_none()
434    {
435        return -1;
436    }
437    // SAFETY: callers must uphold the upstream single-threaded-setup
438    // ordering; each value is a non-NULL C function pointer (checked above).
439    unsafe {
440        xmlFree = freeFunc.unwrap();
441        xmlMalloc = mallocFunc.unwrap();
442        xmlMallocAtomic = mallocAtomicFunc.unwrap();
443        xmlRealloc = reallocFunc.unwrap();
444        xmlMemStrdup = strdupFunc.unwrap();
445    }
446    0
447}
448
449/// Get GC-aware memory allocator functions (upstream xmlmemory.h).
450///
451/// # UPSTREAM-PARITY
452///
453/// ```c
454/// int xmlGcMemGet(xmlFreeFunc *freeFunc,
455///                 xmlMallocFunc *mallocFunc,
456///                 xmlMallocFunc *mallocAtomicFunc,
457///                 xmlReallocFunc *reallocFunc,
458///                 xmlStrdupFunc *strdupFunc);
459/// ```
460///
461/// Same contract as `xmlMemGet` with the `mallocAtomicFunc` output.
462/// Writes through NULL-tolerant output pointers and returns 0.
463///
464/// # SAFETY
465///
466/// - All non-NULL output pointers must be valid and writable
467#[no_mangle]
468pub unsafe extern "C" fn xmlGcMemGet(
469    freeFunc: *mut Option<xmlFreeFunc>,
470    mallocFunc: *mut Option<xmlMallocFunc>,
471    mallocAtomicFunc: *mut Option<xmlMallocFunc>,
472    reallocFunc: *mut Option<xmlReallocFunc>,
473    strdupFunc: *mut Option<xmlStrdupFunc>,
474) -> c_int {
475    // SAFETY: callers must pass NULL or valid writable pointers.
476    unsafe {
477        if !freeFunc.is_null() {
478            ptr::write(freeFunc, Some(xmlFree));
479        }
480        if !mallocFunc.is_null() {
481            ptr::write(mallocFunc, Some(xmlMalloc));
482        }
483        if !mallocAtomicFunc.is_null() {
484            ptr::write(mallocAtomicFunc, Some(xmlMallocAtomic));
485        }
486        if !reallocFunc.is_null() {
487            ptr::write(reallocFunc, Some(xmlRealloc));
488        }
489        if !strdupFunc.is_null() {
490            ptr::write(strdupFunc, Some(xmlMemStrdup));
491        }
492    }
493    0
494}
495
496// ═══════════════════════════════════════════════════════════════════════════════
497// Allocation Functions
498// ═══════════════════════════════════════════════════════════════════════════════
499
500/// Allocate memory through the exported `xmlMalloc` variable.
501///
502/// # UPSTREAM-PARITY
503///
504/// ```c
505/// void *xmlMalloc(size_t size);
506/// ```
507///
508/// Every internal allocation site calls this indirection, which reads the
509/// current exported `xmlMalloc` variable — exactly how upstream internal
510/// code calls `xmlMalloc(...)` (the variable, globals.c). With no override
511/// the variable holds `xmlMallocDefault`; after `xmlMemSetup` or a direct
512/// `xmlMalloc = custom` assignment it holds the custom hook, so all internal
513/// allocations observe the override (R-000176 fix, single source of truth).
514///
515/// # SAFETY
516///
517/// - The returned pointer must be freed with `xmlFree`
518/// - `size` may be 0 (returns a valid non-NULL pointer or NULL)
519pub unsafe extern "C" fn xmlMallocImpl(size: usize) -> *mut c_void {
520    // SAFETY: reading the exported static mut is safe under the upstream
521    // setup-ordering contract; the stored value is a valid C fn pointer.
522    unsafe { (xmlMalloc)(size) }
523}
524
525/// Default `xmlMalloc` body: plain `libc::malloc` (upstream default `malloc`).
526///
527/// This is the initial value of the exported `xmlMalloc` variable and never
528/// reads the variable (no recursion). Since 11.1-Z.3 (R-000178) it performs
529/// NO accounting: upstream's counters are maintained only by the debug
530/// allocator, so with the default installed `xmlMemUsed`/`xmlMemBlocks` are 0
531/// and `xmlMemSize` is 0 — byte-identical with the oracle.
532///
533/// # Safety
534///
535/// - `size` is a byte count passed straight to `libc::malloc`; 0 is handled
536///   by the platform contract (glibc returns a unique non-NULL pointer).
537/// - The returned pointer (NULL on failure) is a libc-owned allocation that
538///   must be freed exactly once with the matching free path and never
539///   dereferenced after freeing.
540/// - This wrapper never reads the exported allocator variables, so it cannot
541///   recurse and is safe to install as a hook.
542unsafe extern "C" fn xmlMallocDefault(size: usize) -> *mut c_void {
543    unsafe { default_malloc(size) }
544}
545
546/// Allocate through the exported `xmlMallocAtomic` variable.
547///
548/// # UPSTREAM-PARITY
549///
550/// ```c
551/// void *xmlMallocAtomic(size_t size);
552/// ```
553///
554/// Identical to `xmlMalloc` but hints to the GC that the memory does not
555/// contain pointers. In modern libxml2 this is equivalent to `xmlMalloc`;
556/// `xmlMemSetup` aliases it to the same hook (upstream xmlmemory.c).
557///
558/// # SAFETY
559///
560/// - The returned pointer must be freed with `xmlFree`
561/// - `size` may be 0 (returns a valid non-NULL pointer or NULL)
562pub unsafe extern "C" fn xmlMallocAtomicImpl(size: usize) -> *mut c_void {
563    // SAFETY: see xmlMallocImpl.
564    unsafe { (xmlMallocAtomic)(size) }
565}
566
567/// Default `xmlMallocAtomic` body (initial exported-variable value).
568///
569/// Atomic allocations share the malloc accounting body; `xmlGcMemSetup`
570/// installs a dedicated atomic hook via the variable (upstream xmlmemory.c).
571unsafe extern "C" fn xmlMallocAtomicDefault(size: usize) -> *mut c_void {
572    unsafe { xmlMallocDefault(size) }
573}
574
575/// Reallocate through the exported `xmlRealloc` variable.
576///
577/// # UPSTREAM-PARITY
578///
579/// ```c
580/// void *xmlRealloc(void *ptr, size_t size);
581/// ```
582///
583/// Changes the size of the memory block pointed to by `ptr`.
584/// If `ptr` is NULL, behaves like `xmlMalloc`.
585/// If `size` is 0, may return NULL (like C realloc).
586///
587/// # SAFETY
588///
589/// - `ptr` must be a valid pointer from `xmlMalloc`, `xmlMallocAtomic`, or `xmlRealloc`,
590///   or NULL
591/// - The returned pointer must be freed with `xmlFree`
592pub unsafe extern "C" fn xmlReallocImpl(ptr: *mut c_void, size: usize) -> *mut c_void {
593    // SAFETY: see xmlMallocImpl.
594    unsafe { (xmlRealloc)(ptr, size) }
595}
596
597/// Default `xmlRealloc` body: plain `libc::realloc` (upstream default `realloc`).
598///
599/// No accounting (R-000178) — see `xmlMallocDefault`. C semantics: `realloc(NULL,
600/// n)` allocates, `realloc(p, 0)` follows the platform contract (glibc frees and
601/// returns NULL), failure leaves the old block intact.
602unsafe extern "C" fn xmlReallocDefault(ptr: *mut c_void, size: usize) -> *mut c_void {
603    unsafe { default_realloc(ptr, size) }
604}
605
606/// Free through the exported `xmlFree` variable.
607///
608/// # UPSTREAM-PARITY
609///
610/// ```c
611/// void xmlFree(void *ptr);
612/// ```
613///
614/// Frees memory previously allocated with `xmlMalloc`, `xmlMallocAtomic`,
615/// or `xmlRealloc`. If `ptr` is NULL, no operation is performed.
616///
617/// # SAFETY
618///
619/// - `ptr` must be a valid pointer from `xmlMalloc`/`xmlMallocAtomic`/`xmlRealloc`,
620///   or NULL
621/// - After this call, `ptr` must not be dereferenced
622pub unsafe extern "C" fn xmlFreeImpl(ptr: *mut c_void) {
623    // SAFETY: see xmlMallocImpl.
624    unsafe { (xmlFree)(ptr) }
625}
626
627/// Default `xmlFree` body: plain `libc::free` (upstream default `free`).
628///
629/// No accounting (R-000178) — see `xmlMallocDefault`.
630unsafe extern "C" fn xmlFreeDefault(ptr: *mut c_void) {
631    unsafe { default_free(ptr) };
632}
633
634/// Duplicate a C string through the exported `xmlMemStrdup` variable.
635///
636/// # UPSTREAM-PARITY
637///
638/// ```c
639/// void *xmlMemStrdup(const char *str);
640/// ```
641///
642/// Returns a pointer to the newly allocated copy, or NULL on failure.
643///
644/// # SAFETY
645///
646/// - `str` must be a valid null-terminated C string or NULL
647/// - The returned pointer must be freed with `xmlFree`
648pub unsafe extern "C" fn xmlMemStrdupImpl(str: *const c_char) -> *mut c_void {
649    // SAFETY: see xmlMallocImpl.
650    unsafe { (xmlMemStrdup)(str) }
651}
652
653/// Default `xmlMemStrdup` body: plain libc strdup (upstream default
654/// `xmlPosixStrdup`). No accounting (R-000178) — see `xmlMallocDefault`.
655unsafe extern "C" fn xmlMemStrdupDefault(str: *const c_char) -> *mut c_void {
656    unsafe { default_strdup(str) }
657}
658
659// ═══════════════════════════════════════════════════════════════════════════════
660// Exported allocator globals (upstream xmlmemory.h)
661// ═══════════════════════════════════════════════════════════════════════════════
662//
663// Upstream exports the allocator entry points as DATA: `XMLPUBVAR
664// xmlMallocFunc xmlMalloc;` etc. — function-pointer variables that downstream
665// code can read AND assign (the documented allocator-override mechanism).
666// The candidate mirrors that ABI: these five variables ARE the allocator
667// state (single source of truth, R-000176). Their initial values are the
668// `*Default` bodies; every internal allocation routes through the variables
669// via the `*Impl` indirection, so `xmlMemSetup` and direct assignment are
670// equivalent override mechanisms, exactly as upstream.
671
672/// `xmlMallocFunc xmlMalloc` — the malloc hook (default: `xmlMallocDefault`).
673#[no_mangle]
674pub static mut xmlMalloc: xmlMallocFunc = xmlMallocDefault;
675
676/// `xmlMallocFunc xmlMallocAtomic` — the atomic-malloc hook.
677#[no_mangle]
678pub static mut xmlMallocAtomic: xmlMallocFunc = xmlMallocAtomicDefault;
679
680/// `xmlReallocFunc xmlRealloc` — the realloc hook.
681#[no_mangle]
682pub static mut xmlRealloc: xmlReallocFunc = xmlReallocDefault;
683
684/// `xmlFreeFunc xmlFree` — the free hook.
685#[no_mangle]
686pub static mut xmlFree: xmlFreeFunc = xmlFreeDefault;
687
688/// `xmlStrdupFunc xmlMemStrdup` — the strdup hook.
689#[no_mangle]
690pub static mut xmlMemStrdup: xmlStrdupFunc = xmlMemStrdupDefault;
691
692// ── LIBXML_THREAD_ALLOC_ENABLED accessors (upstream globals.c) ─────────────
693//
694// Upstream source builds with --with-thread-alloc export accessor FUNCTIONS
695// `xmlMallocFunc *__xmlMalloc(void)` etc. (globals.c, gated by
696// LIBXML_THREAD_ALLOC_ENABLED); its xmlmemory.h then redefines `xmlMalloc`
697// as `(*__xmlMalloc())`, so consumers compiled against the thread-alloc
698// profile reference these accessors instead of the variables. The candidate
699// implements them per upstream semantics: each returns a pointer to the
700// allocator slot the library uses — which is exactly the corresponding
701// exported variable above (single source of truth, R-000176) — so
702// `(*__xmlMalloc())(size)` is the candidate's `xmlMalloc` variable.
703//
704// The executed distro oracle (system 2.15.3) hides these five accessors
705// (built without thread-alloc / with hidden visibility), so they are
706// CUSTODIAN_EXTENSION exports in the disposition ledger: upstream-ABI-valid,
707// required by source-built consumers (e.g. the canonical source oracle in
708// the Phase-12 DOCKER-SUBSTITUTION court, whose libxslt references
709// __xmlFree/__xmlMalloc/__xmlRealloc).
710
711/// Upstream `xmlMallocFunc *__xmlMalloc(void)`.
712///
713/// # Safety
714///
715/// - The returned pointer is the address of the exported `xmlMalloc`
716///   variable; it stays valid for the process lifetime and may be read or
717///   written through exactly like upstream's thread-local allocator slot.
718#[no_mangle]
719pub unsafe extern "C" fn __xmlMalloc() -> *mut xmlMallocFunc {
720    core::ptr::addr_of_mut!(xmlMalloc)
721}
722
723/// Upstream `xmlMallocFunc *__xmlMallocAtomic(void)`.
724///
725/// # Safety
726///
727/// - The returned pointer is the address of the exported `xmlMallocAtomic`
728///   variable; it stays valid for the process lifetime.
729#[no_mangle]
730pub unsafe extern "C" fn __xmlMallocAtomic() -> *mut xmlMallocFunc {
731    core::ptr::addr_of_mut!(xmlMallocAtomic)
732}
733
734/// Upstream `xmlReallocFunc *__xmlRealloc(void)`.
735///
736/// # Safety
737///
738/// - The returned pointer is the address of the exported `xmlRealloc`
739///   variable; it stays valid for the process lifetime.
740#[no_mangle]
741pub unsafe extern "C" fn __xmlRealloc() -> *mut xmlReallocFunc {
742    core::ptr::addr_of_mut!(xmlRealloc)
743}
744
745/// Upstream `xmlFreeFunc *__xmlFree(void)`.
746///
747/// # Safety
748///
749/// - The returned pointer is the address of the exported `xmlFree`
750///   variable; it stays valid for the process lifetime.
751#[no_mangle]
752pub unsafe extern "C" fn __xmlFree() -> *mut xmlFreeFunc {
753    core::ptr::addr_of_mut!(xmlFree)
754}
755
756/// Upstream `xmlStrdupFunc *__xmlMemStrdup(void)`.
757///
758/// # Safety
759///
760/// - The returned pointer is the address of the exported `xmlMemStrdup`
761///   variable; it stays valid for the process lifetime.
762#[no_mangle]
763pub unsafe extern "C" fn __xmlMemStrdup() -> *mut xmlStrdupFunc {
764    core::ptr::addr_of_mut!(xmlMemStrdup)
765}
766
767// ═══════════════════════════════════════════════════════════════════════════════
768// Memory Debugging / Statistics
769// ═══════════════════════════════════════════════════════════════════════════════
770
771/// Return the total amount of memory currently allocated (approximate).
772///
773/// # UPSTREAM-PARITY
774///
775/// ```c
776/// int xmlMemUsed(void);
777/// ```
778///
779/// Returns upstream's `debugMemSize` counter, which is maintained ONLY by the
780/// debug allocator (`xmlMemMalloc`/`*Loc` surface). With the default
781/// allocator installed the counter stays 0 — byte-identical with the oracle
782/// (R-000178, verified by ALLOCATOR-DEFAULT-001). Custom allocator hooks
783/// never touch it (upstream contract).
784#[no_mangle]
785pub extern "C" fn xmlMemUsed() -> c_int {
786    MEM_USED.load(Ordering::Relaxed) as c_int
787}
788
789/// Return the current number of allocated blocks (approximate).
790///
791/// # UPSTREAM-PARITY
792///
793/// ```c
794/// int xmlMemBlocks(void);
795/// ```
796///
797/// Returns upstream's `debugMemBlocks` counter (debug allocator only; 0 with
798/// the default allocator — R-000178, byte-identical with the oracle).
799#[no_mangle]
800pub extern "C" fn xmlMemBlocks() -> c_int {
801    MEM_BLOCKS.load(Ordering::Relaxed) as c_int
802}
803
804/// Display memory allocation information to a file.
805///
806/// # UPSTREAM-PARITY
807///
808/// ```c
809/// void xmlMemDisplay(FILE *fp);
810/// ```
811///
812/// No-op — upstream 2.15.0 removed this feature (`@deprecated This feature
813/// was removed.`). The pre-Z.3 candidate printed aggregate counters; that was
814/// a divergence from the executed oracle and is removed (R-000131 sealed).
815///
816/// # SAFETY
817///
818/// - `fp` must be a valid FILE* pointer or NULL (unused)
819#[no_mangle]
820pub const unsafe extern "C" fn xmlMemDisplay(_fp: *mut c_void) {}
821
822/// Show memory allocation information.
823///
824/// # UPSTREAM-PARITY
825///
826/// ```c
827/// void xmlMemShow(FILE *fp, int nr);
828/// ```
829///
830/// No-op — upstream 2.15.0 removed this feature (`@deprecated This feature
831/// was removed.`); the candidate previously dumped the registry with a
832/// non-upstream ordering, a documented divergence that is now removed
833/// (R-000131 sealed).
834///
835/// # SAFETY
836///
837/// - `fp` must be a valid FILE* pointer or NULL (unused)
838#[no_mangle]
839pub const unsafe extern "C" fn xmlMemShow(_fp: *mut c_void, _nr: c_int) {}
840
841// ═══════════════════════════════════════════════════════════════════════════════
842// Convenience Functions (used internally)
843// ═══════════════════════════════════════════════════════════════════════════════
844
845/// Allocate zero-initialized memory.
846///
847/// # UPSTREAM-PARITY
848///
849/// This is like `xmlMalloc` followed by `memset(0)`, but some allocator
850/// hooks provide it directly.
851///
852/// # SAFETY
853///
854/// Same as `xmlMalloc`. The returned memory is zero-initialized.
855#[no_mangle]
856pub unsafe extern "C" fn xmlMallocZero(size: usize) -> *mut c_void {
857    // SAFETY: Delegates to xmlMallocImpl and zeroes the memory.
858    let ptr = unsafe { xmlMallocImpl(size) };
859    if !ptr.is_null() {
860        unsafe { ptr::write_bytes(ptr, 0, size) };
861    }
862    ptr
863}
864
865/// Allocate zero-initialized memory (atomic variant).
866///
867/// # UPSTREAM-PARITY
868///
869/// Like `xmlMallocAtomic` followed by zero-initialization.
870///
871/// # SAFETY
872///
873/// The function touches crate-global state only; it is safe
874/// as long as the caller respects the library's global
875/// initialization/cleanup ordering (xmlInitParser before use,
876/// xmlCleanupParser only after all users are done).
877///
878/// Violating the global lifecycle ordering, or calling this after
879/// teardown or from a signal handler, is undefined behavior.
880#[no_mangle]
881pub unsafe extern "C" fn xmlMallocAtomicZero(size: usize) -> *mut c_void {
882    // SAFETY: Delegates to xmlMallocAtomicImpl and zeroes the memory.
883    let ptr = unsafe { xmlMallocAtomicImpl(size) };
884    if !ptr.is_null() {
885        unsafe { ptr::write_bytes(ptr, 0, size) };
886    }
887    ptr
888}
889
890/// Reallocate and zero-initialize the new portion.
891///
892/// # UPSTREAM-PARITY
893///
894/// Like `xmlRealloc`, but zeroes any newly allocated bytes.
895///
896/// # SAFETY
897///
898/// Same as `xmlRealloc`.
899#[no_mangle]
900pub unsafe extern "C" fn xmlReallocZero(
901    ptr: *mut c_void,
902    old_size: usize,
903    new_size: usize,
904) -> *mut c_void {
905    // SAFETY: Delegates to xmlRealloc and zeroes the new portion.
906    let new_ptr = unsafe { xmlReallocImpl(ptr, new_size) };
907    if !new_ptr.is_null() && new_size > old_size {
908        unsafe {
909            ptr::write_bytes(new_ptr.add(old_size), 0, new_size - old_size);
910        }
911    }
912    new_ptr
913}
914
915// ═══════════════════════════════════════════════════════════════════════════════
916// Debug Allocator (Optional)
917// ═══════════════════════════════════════════════════════════════════════════════
918
919/// Initialize the memory layer with debugging support.
920///
921/// # UPSTREAM-PARITY
922///
923/// ```c
924/// int xmlInitMemory(void);
925/// ```
926///
927/// Initializes the memory subsystem. Returns 0 on success.
928/// This is called automatically by `xmlInitParser`.
929#[no_mangle]
930pub const extern "C" fn xmlInitMemory() -> c_int {
931    0
932}
933
934/// Clean up the memory layer.
935///
936/// # UPSTREAM-PARITY
937///
938/// ```c
939/// void xmlCleanupMemory(void);
940/// ```
941#[no_mangle]
942pub const extern "C" fn xmlCleanupMemory() {
943    // Phase 1: no cleanup needed for the default allocator.
944}
945
946// ═══════════════════════════════════════════════════════════════════════════════
947// Legacy-named allocator API (upstream xmlmemory.h)
948// ═══════════════════════════════════════════════════════════════════════════════
949// Upstream xmlmemory.h historically exported `xmlMemMalloc`, `xmlMemFree`,
950// `xmlMemRealloc`, `xmlMemoryStrdup`, the `*Loc` location-tracking variants,
951// `xmlMemSize`, `xmlMemDisplayLast` and `xmlMemoryDump` alongside the modern
952// names. Downstream code (older consumers, some language bindings) links
953// against these names, so the candidate exports them with identical
954// semantics. In upstream 2.15.0 these ARE the debug allocator: always
955// libc-backed, independent of the hook variables, and tracked by
956// `debugMemSize`/`debugMemBlocks` (with the MEMHDR tag enabling
957// `xmlMemSize`). The candidate mirrors that exactly: the debug-named
958// functions and the `*Loc` variants are always libc-backed + registry- and
959// counter-tracked, do NOT route through the exported variables, and the
960// `*Loc` location arguments are accepted and ignored — exactly like
961// upstream's `ATTRIBUTE_UNUSED` parameters (R-000131 sealed). The candidate
962// returns plain libc pointers (no MEMHDR prefix), so a debug-surface block
963// can also be freed with `xmlFree` — a safe superset of the upstream
964// contract (upstream requires `xmlMemFree` for such blocks).
965
966/// Debug-surface malloc: libc + counters + registry (upstream xmlMemMalloc).
967///
968/// # Safety
969///
970/// - `size` must be a valid allocation size; the underlying `default_malloc`
971///   follows the libc contract (0 handled by the platform).
972/// - `file` must be NULL or a valid pointer to a NUL-terminated C string that
973///   stays valid for the duration of the call (it is stored by address only,
974///   never dereferenced here).
975/// - The returned pointer (NULL on failure, with counters untouched) is owned
976///   by the caller and must be freed exactly once via `debug_free` or
977///   `xmlMemFree`; never dereferenced after freeing.
978unsafe fn debug_malloc(size: usize, file: *const c_char, line: c_int) -> *mut c_void {
979    let ptr = unsafe { default_malloc(size) };
980    if !ptr.is_null() {
981        MEM_USED.fetch_add(size, Ordering::Relaxed);
982        MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
983        unsafe { block_record(ptr, size, file, line) };
984    }
985    ptr
986}
987
988/// Debug-surface realloc: libc + counters + registry (upstream xmlMemRealloc).
989///
990/// # Safety
991///
992/// - `ptr` must be NULL or a valid pointer previously returned by the debug
993///   surface (`debug_malloc`/`debug_realloc`) or by a matching libc
994///   allocation, and not yet freed; NULL behaves like malloc.
995/// - On failure the old block is left intact and stays recorded; on success
996///   the old pointer is invalidated and the returned pointer must be freed
997///   exactly once via `debug_free`/`xmlMemFree`.
998/// - `file` must be NULL or a valid NUL-terminated C string valid for the
999///   duration of the call (stored by address only).
1000unsafe fn debug_realloc(
1001    ptr: *mut c_void,
1002    size: usize,
1003    file: *const c_char,
1004    line: c_int,
1005) -> *mut c_void {
1006    let new_ptr = unsafe { default_realloc(ptr, size) };
1007    if !new_ptr.is_null() {
1008        let old_size = unsafe { block_forget(ptr) };
1009        if let Some(old) = old_size {
1010            MEM_USED.fetch_add(size.saturating_sub(old), Ordering::Relaxed);
1011        } else if ptr.is_null() {
1012            MEM_USED.fetch_add(size, Ordering::Relaxed);
1013            MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
1014        }
1015        unsafe { block_record(new_ptr, size, file, line) };
1016    }
1017    new_ptr
1018}
1019
1020/// Debug-surface strdup: libc + counters + registry (upstream xmlMemoryStrdup).
1021///
1022/// # Safety
1023///
1024/// - `str` must be NULL or a valid pointer to a NUL-terminated C string
1025///   readable through its full length (including the terminator) for the
1026///   duration of the call; NULL yields NULL.
1027/// - `file` must be NULL or a valid NUL-terminated C string valid for the
1028///   duration of the call (stored by address only).
1029/// - The returned pointer (NULL on failure) must be freed exactly once via
1030///   `debug_free`/`xmlMemFree`; never dereferenced after freeing.
1031unsafe fn debug_strdup(str: *const c_char, file: *const c_char, line: c_int) -> *mut c_void {
1032    if str.is_null() {
1033        return ptr::null_mut();
1034    }
1035    let ptr = unsafe { default_strdup(str) };
1036    if !ptr.is_null() {
1037        let len = unsafe { libc::strlen(str) } + 1;
1038        MEM_USED.fetch_add(len, Ordering::Relaxed);
1039        MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
1040        unsafe { block_record(ptr, len, file, line) };
1041    }
1042    ptr
1043}
1044
1045/// Debug-surface free: registry/counter removal + libc free (upstream xmlMemFree).
1046/// A foreign pointer (not in the registry) is freed without touching the
1047/// counters — a safe divergence from upstream's tag-error print (which would
1048/// pollute stderr).
1049///
1050/// # Safety
1051///
1052/// - `ptr` must be NULL (a no-op) or a valid pointer previously returned by
1053///   the debug surface (`debug_malloc`/`debug_realloc`/`debug_strdup`) or by
1054///   a matching libc allocation; it must not be freed twice and must not be
1055///   dereferenced after this call.
1056/// - The registry and counters are only touched when the pointer was recorded;
1057///   a foreign pointer is still freed via libc.
1058unsafe fn debug_free(ptr: *mut c_void) {
1059    if ptr.is_null() {
1060        return;
1061    }
1062    let old_size = unsafe { block_forget(ptr) };
1063    unsafe { default_free(ptr) };
1064    if let Some(old) = old_size {
1065        MEM_USED.fetch_sub(old, Ordering::Relaxed);
1066        MEM_BLOCKS.fetch_sub(1, Ordering::Relaxed);
1067    }
1068}
1069
1070/// Allocate memory through the debug allocator (upstream xmlmemory.h).
1071///
1072/// ```c
1073/// void *xmlMemMalloc(size_t size);
1074/// ```
1075///
1076/// Always libc-backed and tracked (upstream debug allocator contract): the
1077/// block is recorded so `xmlMemSize`/`xmlMemUsed`/`xmlMemBlocks` observe it.
1078///
1079/// # SAFETY
1080///
1081/// - The returned pointer must be freed with `xmlMemFree` (or `xmlFree` —
1082///   plain libc pointer, candidate superset)
1083#[no_mangle]
1084pub unsafe extern "C" fn xmlMemMalloc(size: usize) -> *mut c_void {
1085    unsafe { debug_malloc(size, ptr::null(), 0) }
1086}
1087
1088/// Free memory through the debug allocator (upstream xmlmemory.h).
1089///
1090/// ```c
1091/// void xmlMemFree(void *ptr);
1092/// ```
1093///
1094/// Un-records the block and frees it. NULL is a no-op.
1095///
1096/// # SAFETY
1097///
1098/// - `ptr` must be valid pointers (or NULL
1099///   where the upstream C contract allows), obtained from the
1100///   matching constructor/owner and not yet freed; the callee may
1101///   take or keep ownership exactly as the C API specifies.
1102#[no_mangle]
1103pub unsafe extern "C" fn xmlMemFree(ptr: *mut c_void) {
1104    unsafe { debug_free(ptr) };
1105}
1106
1107/// Reallocate memory through the debug allocator (upstream xmlmemory.h).
1108///
1109/// ```c
1110/// void *xmlMemRealloc(void *ptr, size_t size);
1111/// ```
1112///
1113/// C realloc semantics + registry maintenance (upstream xmlMemRealloc:
1114/// NULL ptr allocates, failure leaves the old block recorded and intact).
1115///
1116/// # SAFETY
1117///
1118/// - `ptr` must be valid pointers (or NULL
1119///   where the upstream C contract allows), obtained from the
1120///   matching constructor/owner and not yet freed; the callee may
1121///   take or keep ownership exactly as the C API specifies.
1122#[no_mangle]
1123pub unsafe extern "C" fn xmlMemRealloc(ptr: *mut c_void, size: usize) -> *mut c_void {
1124    unsafe { debug_realloc(ptr, size, ptr::null(), 0) }
1125}
1126
1127/// Duplicate a string through the debug allocator (upstream xmlmemory.h).
1128///
1129/// ```c
1130/// void *xmlMemoryStrdup(const char *str);
1131/// ```
1132///
1133/// NULL returns NULL (upstream xmlPosixStrdup/xmlCharStrdup contract).
1134///
1135/// # SAFETY
1136///
1137/// - `str` must point to a valid NUL-terminated string, or NULL
1138/// - The returned pointer must be freed with `xmlMemFree` (or `xmlFree`)
1139#[no_mangle]
1140pub unsafe extern "C" fn xmlMemoryStrdup(str: *const c_char) -> *mut c_void {
1141    unsafe { debug_strdup(str, ptr::null(), 0) }
1142}
1143
1144/// Allocate memory, recording the allocation site (upstream xmlmemory.h).
1145///
1146/// ```c
1147/// void *xmlMallocLoc(size_t size, const char *file, int line);
1148/// ```
1149///
1150/// Debug-allocator contract (upstream xmlMallocLoc -> xmlMemMalloc): always
1151/// libc-backed + tracked, independent of the hook variables. Upstream 2.15.0
1152/// ignores the file/line arguments (`ATTRIBUTE_UNUSED`); the candidate
1153/// records them in the registry so `xmlMemSize` works (R-000131 sealed).
1154///
1155/// # SAFETY
1156///
1157///
1158/// - `file` must point to valid NUL-terminated
1159///   strings (or NULL where the C contract allows) for the lifetime
1160///   of the call.
1161///
1162/// The caller must not race this call with concurrent mutation of the
1163/// same objects from other threads (per-object state is not internally
1164/// synchronized). Violating any of the above is undefined behavior.
1165///
1166/// Exercised by the C-API differential courts
1167/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1168/// courts; those pass byte-for-byte against the upstream oracle.
1169#[no_mangle]
1170pub unsafe extern "C" fn xmlMallocLoc(
1171    size: usize,
1172    file: *const c_char,
1173    line: c_int,
1174) -> *mut c_void {
1175    unsafe { debug_malloc(size, file, line) }
1176}
1177
1178/// Allocate memory, recording the allocation site (upstream xmlmemory.h).
1179///
1180/// ```c
1181/// void *xmlMallocAtomicLoc(size_t size, const char *file, int line);
1182/// ```
1183///
1184/// Upstream xmlMallocAtomicLoc -> xmlMemMalloc: a plain (non-zeroed) tracked
1185/// allocation. The pre-Z.3 candidate zeroed it via `xmlMallocZero` — a real
1186/// divergence, fixed in 11.1-Z.3 (R-000178 narrative).
1187///
1188/// # SAFETY
1189///
1190///
1191/// - `file` must point to valid NUL-terminated
1192///   strings (or NULL where the C contract allows) for the lifetime
1193///   of the call.
1194///
1195/// The caller must not race this call with concurrent mutation of the
1196/// same objects from other threads (per-object state is not internally
1197/// synchronized). Violating any of the above is undefined behavior.
1198///
1199/// Exercised by the C-API differential courts
1200/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1201/// courts; those pass byte-for-byte against the upstream oracle.
1202#[no_mangle]
1203pub unsafe extern "C" fn xmlMallocAtomicLoc(
1204    size: usize,
1205    file: *const c_char,
1206    line: c_int,
1207) -> *mut c_void {
1208    unsafe { debug_malloc(size, file, line) }
1209}
1210
1211/// Reallocate memory, recording the allocation site (upstream xmlmemory.h).
1212///
1213/// ```c
1214/// void *xmlReallocLoc(void *ptr, size_t size, const char *file, int line);
1215/// ```
1216///
1217/// Debug-allocator contract (upstream xmlReallocLoc -> xmlMemRealloc): the
1218/// old record is superseded on success, kept on failure.
1219///
1220/// # SAFETY
1221///
1222/// - `ptr` must be valid pointers (or NULL
1223///   where the upstream C contract allows), obtained from the
1224///   matching constructor/owner and not yet freed; the callee may
1225///   take or keep ownership exactly as the C API specifies.
1226///
1227/// - `file` must point to valid NUL-terminated
1228///   strings (or NULL where the C contract allows) for the lifetime
1229///   of the call.
1230///
1231/// The caller must not race this call with concurrent mutation of the
1232/// same objects from other threads (per-object state is not internally
1233/// synchronized). Violating any of the above is undefined behavior.
1234///
1235/// Exercised by the C-API differential courts
1236/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1237/// courts; those pass byte-for-byte against the upstream oracle.
1238#[no_mangle]
1239pub unsafe extern "C" fn xmlReallocLoc(
1240    ptr: *mut c_void,
1241    size: usize,
1242    file: *const c_char,
1243    line: c_int,
1244) -> *mut c_void {
1245    unsafe { debug_realloc(ptr, size, file, line) }
1246}
1247
1248/// Duplicate a string, recording the allocation site (upstream xmlmemory.h).
1249///
1250/// ```c
1251/// void *xmlMemStrdupLoc(const char *str, const char *file, int line);
1252/// ```
1253///
1254/// Debug-allocator contract (upstream xmlMemStrdupLoc -> xmlMemoryStrdup).
1255///
1256/// # SAFETY
1257///
1258///
1259/// - `str`, `file` must point to valid NUL-terminated
1260///   strings (or NULL where the C contract allows) for the lifetime
1261///   of the call.
1262///
1263/// The caller must not race this call with concurrent mutation of the
1264/// same objects from other threads (per-object state is not internally
1265/// synchronized). Violating any of the above is undefined behavior.
1266///
1267/// Exercised by the C-API differential courts
1268/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1269/// courts; those pass byte-for-byte against the upstream oracle.
1270#[no_mangle]
1271pub unsafe extern "C" fn xmlMemStrdupLoc(
1272    str: *const c_char,
1273    file: *const c_char,
1274    line: c_int,
1275) -> *mut c_void {
1276    unsafe { debug_strdup(str, file, line) }
1277}
1278
1279/// Return the size of an allocated block (upstream xmlmemory.h).
1280///
1281/// ```c
1282/// size_t xmlMemSize(void *ptr);
1283/// ```
1284///
1285/// Returns the recorded size for debug-surface blocks (`xmlMemMalloc`/`*Loc`
1286/// surface) and 0 for everything else — matching upstream's MEMHDR tag
1287/// lookup, which misses on plain-malloc blocks (default allocator) and on
1288/// foreign pointers (R-000178; byte-identical with the oracle).
1289///
1290/// # SAFETY
1291///
1292/// - `ptr` must be valid pointers (or NULL
1293///   where the upstream C contract allows), obtained from the
1294///   matching constructor/owner and not yet freed; the callee may
1295///   take or keep ownership exactly as the C API specifies.
1296#[no_mangle]
1297pub unsafe extern "C" fn xmlMemSize(ptr: *mut c_void) -> usize {
1298    if ptr.is_null() {
1299        return 0;
1300    }
1301    BLOCKS
1302        .lock()
1303        .get(&(ptr as usize))
1304        .map(|m| m.size)
1305        .unwrap_or(0)
1306}
1307
1308/// Display a limited amount of memory debug information (upstream xmlmemory.h).
1309///
1310/// ```c
1311/// void xmlMemDisplayLast(FILE *fp, long nbBytes);
1312/// ```
1313///
1314/// No-op — upstream 2.15.0 removed this feature (`@deprecated This feature
1315/// was removed.`); the pre-Z.3 candidate dumped the registry with a
1316/// non-upstream format, a divergence now removed (R-000131 sealed).
1317///
1318/// # SAFETY
1319///
1320/// - `fp` must be a valid FILE* pointer or NULL (unused)
1321#[no_mangle]
1322pub const unsafe extern "C" fn xmlMemDisplayLast(_fp: *mut c_void, _nb_bytes: c_long) {}
1323
1324/// Dump memory allocation statistics (upstream xmlmemory.h).
1325///
1326/// ```c
1327/// void xmlMemoryDump(void);
1328/// ```
1329///
1330/// No-op — upstream 2.15.0 removed this feature (`@deprecated This feature
1331/// was removed.`).
1332///
1333/// # SAFETY
1334///
1335/// The function touches crate-global state only; it is safe
1336/// as long as the caller respects the library's global
1337/// initialization/cleanup ordering (xmlInitParser before use,
1338/// xmlCleanupParser only after all users are done).
1339#[no_mangle]
1340pub const unsafe extern "C" fn xmlMemoryDump() {
1341    // Upstream: empty body (feature removed in 2.15.0).
1342}
1343
1344// ═══════════════════════════════════════════════════════════════════════════════
1345// Tests
1346// ═══════════════════════════════════════════════════════════════════════════════
1347
1348#[cfg(test)]
1349mod tests {
1350    use super::*;
1351    use core::ptr;
1352
1353    /// Test basic allocation and deallocation.
1354    ///
1355    /// # Safety
1356    ///
1357    /// - `ptr` is the non-NULL result of `xmlMalloc(100)` (asserted): it is a
1358    ///   valid heap allocation owned by the caller, freed exactly once by
1359    ///   `xmlFree`, and not used afterwards.
1360    #[test]
1361    fn test_malloc_free() {
1362        unsafe {
1363            let ptr = xmlMalloc(100);
1364            assert!(!ptr.is_null(), "xmlMalloc(100) returned NULL");
1365            xmlFree(ptr);
1366        }
1367    }
1368
1369    /// Test that xmlMalloc(0) returns a valid pointer.
1370    ///
1371    /// # Safety
1372    ///
1373    /// - `xmlMalloc(0)` may return NULL or a unique non-NULL pointer per the
1374    ///   platform contract; a non-NULL result is a valid allocation freed
1375    ///   exactly once by `xmlFree` and not used afterwards.
1376    #[test]
1377    fn test_malloc_zero() {
1378        unsafe {
1379            let ptr = xmlMalloc(0);
1380            // malloc(0) may return NULL or a valid pointer.
1381            // If non-NULL, it must be freeable.
1382            if !ptr.is_null() {
1383                xmlFree(ptr);
1384            }
1385        }
1386    }
1387
1388    /// Test xmlFree(NULL) is a no-op.
1389    ///
1390    /// # Safety
1391    ///
1392    /// - Passing NULL to `xmlFree` is accepted and performs no operation; no
1393    ///   pointer is dereferenced.
1394    #[test]
1395    fn test_free_null() {
1396        unsafe {
1397            xmlFree(ptr::null_mut());
1398            // Should not crash
1399        }
1400    }
1401
1402    /// Test xmlRealloc.
1403    ///
1404    /// # Safety
1405    ///
1406    /// - `ptr` must be a valid non-NULL allocation from `xmlMalloc`; on the
1407    ///   successful `xmlRealloc` here the old pointer is invalidated and only
1408    ///   the returned `new_ptr` (non-NULL, asserted) is freed exactly once by
1409    ///   `xmlFree` and not used afterwards.
1410    #[test]
1411    fn test_realloc() {
1412        unsafe {
1413            let ptr = xmlMalloc(50);
1414            assert!(!ptr.is_null());
1415            let new_ptr = xmlRealloc(ptr, 100);
1416            assert!(!new_ptr.is_null());
1417            xmlFree(new_ptr);
1418        }
1419    }
1420
1421    /// Test xmlMemStrdup.
1422    ///
1423    /// # Safety
1424    ///
1425    /// - `s` points to a valid NUL-terminated byte string of 6 bytes (the
1426    ///   literal `hello` plus terminator) that stays alive for the call.
1427    /// - `dup` is the non-NULL result of `xmlMemStrdup`, a fresh allocation of
1428    ///   at least 6 bytes readable as a slice; it is freed exactly once by
1429    ///   `xmlFree` and not used afterwards.
1430    #[test]
1431    fn test_mem_strdup() {
1432        unsafe {
1433            let s = b"hello\0" as *const u8 as *const c_char;
1434            let dup = xmlMemStrdup(s);
1435            assert!(!dup.is_null());
1436            // Compare the strings
1437            let orig_slice = std::slice::from_raw_parts(s as *const u8, 6);
1438            let dup_slice = std::slice::from_raw_parts(dup as *const u8, 6);
1439            assert_eq!(orig_slice, dup_slice);
1440            xmlFree(dup);
1441        }
1442    }
1443
1444    /// Test xmlMallocZero returns zeroed memory.
1445    ///
1446    /// # Safety
1447    ///
1448    /// - `ptr` is the non-NULL result of `xmlMallocZero(100)`: a valid
1449    ///   allocation of at least 100 bytes, readable as a 100-byte slice
1450    ///   while alive, and freed exactly once by `xmlFree` afterwards.
1451    #[test]
1452    fn test_malloc_zero_init() {
1453        unsafe {
1454            let ptr = xmlMallocZero(100) as *mut u8;
1455            assert!(!ptr.is_null());
1456            let slice = std::slice::from_raw_parts(ptr, 100);
1457            assert!(slice.iter().all(|&b| b == 0));
1458            xmlFree(ptr as *mut c_void);
1459        }
1460    }
1461
1462    /// Test custom allocator setup/get (R-000176: int returns, single source
1463    /// of truth — `xmlMemSetup` writes the exported variables and `xmlMemGet`
1464    /// reads them back).
1465    ///
1466    /// # Safety
1467    ///
1468    /// - Each output pointer passed to `xmlMemGet` is derived from a live
1469    ///   stack local of matching type, so every non-NULL output points to
1470    ///   valid, aligned, writable storage for the duration of the call; NULL
1471    ///   outputs are tolerated and skipped.
1472    /// - The function pointers written back must be treated as valid
1473    ///   C-compatible hooks (they are the currently installed allocator
1474    ///   functions).
1475    #[test]
1476    fn test_mem_setup_get() {
1477        unsafe {
1478            let mut free_func: Option<xmlFreeFunc> = None;
1479            let mut malloc_func: Option<xmlMallocFunc> = None;
1480            let mut realloc_func: Option<xmlReallocFunc> = None;
1481            let mut strdup_func: Option<xmlStrdupFunc> = None;
1482
1483            let ret = xmlMemGet(
1484                &mut free_func as *mut _,
1485                &mut malloc_func as *mut _,
1486                &mut realloc_func as *mut _,
1487                &mut strdup_func as *mut _,
1488            );
1489            assert_eq!(ret, 0, "xmlMemGet must return 0");
1490            assert!(malloc_func.is_some());
1491            assert!(free_func.is_some());
1492            assert!(realloc_func.is_some());
1493            assert!(strdup_func.is_some());
1494
1495            // NULL output pointers are tolerated (upstream xmlmemory.c).
1496            let ret = xmlMemGet(
1497                ptr::null_mut(),
1498                &mut malloc_func as *mut _,
1499                ptr::null_mut(),
1500                ptr::null_mut(),
1501            );
1502            assert_eq!(ret, 0);
1503            assert!(malloc_func.is_some());
1504        }
1505    }
1506
1507    /// Test the GC allocator hooks: 5-argument Setup/Get with the dedicated
1508    /// `mallocAtomicFunc` slot (R-000176 — previously 4-arg and void).
1509    ///
1510    /// # Safety
1511    ///
1512    /// - Each output pointer passed to `xmlGcMemGet` is derived from a live
1513    ///   stack local of matching type, so every non-NULL output points to
1514    ///   valid, aligned, writable storage for the duration of the call; NULL
1515    ///   outputs are tolerated and skipped.
1516    #[test]
1517    fn test_gc_mem_setup_get() {
1518        unsafe {
1519            let mut free_func: Option<xmlFreeFunc> = None;
1520            let mut malloc_func: Option<xmlMallocFunc> = None;
1521            let mut malloc_atomic_func: Option<xmlMallocFunc> = None;
1522            let mut realloc_func: Option<xmlReallocFunc> = None;
1523            let mut strdup_func: Option<xmlStrdupFunc> = None;
1524
1525            let ret = xmlGcMemGet(
1526                &mut free_func as *mut _,
1527                &mut malloc_func as *mut _,
1528                &mut malloc_atomic_func as *mut _,
1529                &mut realloc_func as *mut _,
1530                &mut strdup_func as *mut _,
1531            );
1532            assert_eq!(ret, 0, "xmlGcMemGet must return 0");
1533            assert!(free_func.is_some());
1534            assert!(malloc_func.is_some());
1535            assert!(malloc_atomic_func.is_some());
1536            assert!(realloc_func.is_some());
1537            assert!(strdup_func.is_some());
1538        }
1539    }
1540
1541    /// Test `xmlMemSetup` NULL validation returns -1 (upstream xmlmemory.c)
1542    /// and that a NULL `mallocAtomicFunc` makes `xmlGcMemSetup` fail.
1543    ///
1544    /// # Safety
1545    ///
1546    /// - `default_malloc` cast to `xmlMallocFunc` is a valid C-compatible
1547    ///   malloc-shaped function pointer; it is only passed as an argument here
1548    ///   and never called by this test.
1549    /// - The NULL hooks are rejected with -1 before any write, so no
1550    ///   allocator state is modified by this test.
1551    #[test]
1552    fn test_mem_setup_null_rejected() {
1553        unsafe {
1554            let ret = xmlMemSetup(None, Some(default_malloc as xmlMallocFunc), None, None);
1555            assert_eq!(ret, -1, "NULL hook must be rejected with -1");
1556            let ret = xmlGcMemSetup(
1557                None,
1558                Some(default_malloc as xmlMallocFunc),
1559                Some(default_malloc as xmlMallocFunc),
1560                None,
1561                None,
1562            );
1563            assert_eq!(ret, -1);
1564        }
1565    }
1566
1567    /// Test the single-source-of-truth model (R-000176): a direct assignment
1568    /// to the exported `xmlMalloc` variable is observed by `xmlMemGet` AND by
1569    /// actual internal allocations through `xmlMallocImpl`.
1570    ///
1571    /// # Safety
1572    ///
1573    /// - The exported `static mut` allocator variables are read and written
1574    ///   directly, which is only valid under the upstream single-threaded
1575    ///   setup ordering: no other thread may allocate or read the variables
1576    ///   concurrently with these assignments.
1577    /// - The values installed (`xmlMallocDefault`/`xmlFreeDefault`) are valid
1578    ///   malloc/free-shaped functions; `p` from `xmlMallocImpl` is a valid
1579    ///   allocation freed exactly once by `xmlFreeImpl`; the prior hook values
1580    ///   are restored before the block ends.
1581    #[test]
1582    fn test_direct_assignment_coherence() {
1583        unsafe {
1584            let saved = xmlMalloc;
1585            let saved_free = xmlFree;
1586            // Install a counting hook via direct variable assignment.
1587            xmlMalloc = xmlMallocDefault;
1588            xmlFree = xmlFreeDefault;
1589
1590            // xmlMemGet reads the variable.
1591            let mut malloc_func: Option<xmlMallocFunc> = None;
1592            let ret = xmlMemGet(
1593                ptr::null_mut(),
1594                &mut malloc_func as *mut _,
1595                ptr::null_mut(),
1596                ptr::null_mut(),
1597            );
1598            assert_eq!(ret, 0);
1599            assert!(
1600                core::ptr::fn_addr_eq(malloc_func.unwrap(), xmlMallocDefault as xmlMallocFunc,),
1601                "xmlMemGet must return the directly-assigned variable"
1602            );
1603
1604            // Internal allocation routes through the variable.
1605            let p = xmlMallocImpl(64);
1606            assert!(!p.is_null());
1607            xmlFreeImpl(p);
1608
1609            // Restore the defaults so other tests are unaffected.
1610            xmlMalloc = saved;
1611            xmlFree = saved_free;
1612        }
1613    }
1614
1615    /// Test xmlMemUsed and xmlMemBlocks return 0 with the default allocator
1616    /// and the debug surface is tracked (R-000178: byte-identical with the
1617    /// oracle — default malloc untracked, xmlMemMalloc/*Loc tracked).
1618    ///
1619    /// # Safety
1620    ///
1621    /// - `ptr` from `xmlMalloc` is a valid libc allocation freed exactly once
1622    ///   by `xmlFree`; `xmlMemSize` reads the registry by address only and is
1623    ///   safe on freed default-allocator pointers (returns 0).
1624    /// - `dptr` from `xmlMallocLoc` and `rptr` from `xmlReallocLoc` are
1625    ///   debug-surface allocations: on success the old pointer is invalidated
1626    ///   and only `rptr` is freed, exactly once, by `xmlMemFree`.
1627    #[test]
1628    fn test_mem_stats() {
1629        unsafe {
1630            // Default allocator: plain libc, NOT tracked (oracle contract).
1631            let ptr = xmlMalloc(100);
1632            assert!(!ptr.is_null());
1633            assert_eq!(xmlMemSize(ptr), 0, "default-allocator blocks are untracked");
1634            xmlFree(ptr);
1635            assert_eq!(xmlMemSize(ptr), 0);
1636
1637            // Debug surface: tracked, xmlMemSize returns the recorded size.
1638            let dptr = xmlMallocLoc(100, c"mem.c".as_ptr(), 42);
1639            assert!(!dptr.is_null());
1640            assert_eq!(xmlMemSize(dptr), 100, "debug-surface blocks are tracked");
1641            let rptr = xmlReallocLoc(dptr, 200, c"mem.c".as_ptr(), 43);
1642            assert!(!rptr.is_null());
1643            // The realloc record supersedes the old one (same address when
1644            // glibc grows in place — then the block AT that address is 200).
1645            assert_eq!(xmlMemSize(rptr), 200);
1646            assert_eq!(xmlMemSize(dptr), xmlMemSize(rptr));
1647            xmlMemFree(rptr);
1648            assert_eq!(xmlMemSize(rptr), 0);
1649
1650            // xmlMemSize(NULL) is 0.
1651            assert_eq!(xmlMemSize(ptr::null_mut()), 0);
1652        }
1653    }
1654
1655    /// Test the default allocator follows the C/libc contract exactly
1656    /// (R-000178): malloc(0) non-NULL, realloc(p,0) NULL after freeing,
1657    /// realloc(NULL,n) == malloc, realloc failure leaves the old block,
1658    /// malloc(SIZE_MAX) NULL, strdup(NULL) NULL.
1659    ///
1660    /// # Safety
1661    ///
1662    /// - Each non-NULL allocation is freed exactly once by `xmlFree`, and
1663    ///   pointers are not used after freeing; in particular `q` is freed by
1664    ///   the `realloc(p, 0)` call that returns NULL (glibc contract), so it
1665    ///   must not be used afterwards, and `r` is freed explicitly after the
1666    ///   failed huge realloc leaves it intact.
1667    /// - NULL arguments to `xmlRealloc`, `xmlMemStrdup` and `xmlFree` are
1668    ///   accepted per the C contract.
1669    #[test]
1670    fn test_default_libc_semantics() {
1671        unsafe {
1672            // malloc(0): glibc returns a unique non-NULL pointer.
1673            let p0 = xmlMalloc(0);
1674            assert!(!p0.is_null(), "malloc(0) must be non-NULL on glibc");
1675            xmlFree(p0);
1676
1677            // realloc(NULL, n) allocates.
1678            let p = xmlRealloc(ptr::null_mut(), 16);
1679            assert!(!p.is_null());
1680            // grow: content preserved.
1681            let q = xmlRealloc(p, 256);
1682            assert!(!q.is_null());
1683            // realloc(p, 0): glibc frees and returns NULL.
1684            let z = xmlRealloc(q, 0);
1685            assert!(z.is_null(), "realloc(p, 0) returns NULL on glibc");
1686
1687            // realloc failure: old block intact.
1688            let r = xmlMalloc(8);
1689            assert!(!r.is_null());
1690            let huge = xmlRealloc(r, usize::MAX);
1691            assert!(huge.is_null(), "realloc to SIZE_MAX must fail");
1692            xmlFree(r);
1693
1694            // malloc failure.
1695            let m = xmlMalloc(usize::MAX);
1696            assert!(m.is_null(), "malloc(SIZE_MAX) must fail");
1697
1698            // strdup(NULL) returns NULL (upstream xmlPosixStrdup).
1699            let d = xmlMemStrdup(ptr::null());
1700            assert!(d.is_null());
1701
1702            // free(NULL) no-op.
1703            xmlFree(ptr::null_mut());
1704        }
1705    }
1706
1707    /// Test the debug-named surface is independent of the hook variables and
1708    /// always libc-backed + tracked (upstream debug-allocator contract).
1709    ///
1710    /// # Safety
1711    ///
1712    /// - `p`, `q` and `s` are non-NULL debug-surface allocations; after the
1713    ///   successful `xmlMemRealloc` only `q` remains valid, and `q` and `s`
1714    ///   are each freed exactly once by `xmlMemFree` and not used afterwards.
1715    /// - `xmlMemFree(NULL)` is a no-op; `xmlMemSize` reads the registry by
1716    ///   address only.
1717    #[test]
1718    fn test_debug_surface_tracked() {
1719        unsafe {
1720            let p = xmlMemMalloc(64);
1721            assert!(!p.is_null());
1722            assert_eq!(xmlMemSize(p), 64);
1723            let q = xmlMemRealloc(p, 128);
1724            assert!(!q.is_null());
1725            assert_eq!(xmlMemSize(q), 128);
1726            let s = xmlMemoryStrdup(c"dbg".as_ptr());
1727            assert!(!s.is_null());
1728            assert_eq!(xmlMemSize(s), 4);
1729            xmlMemFree(q);
1730            xmlMemFree(s);
1731            // xmlMemFree(NULL) no-op.
1732            xmlMemFree(ptr::null_mut());
1733        }
1734    }
1735}