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. The
522    // slot is the process-visible (core DSO's) one when it resolves, so
523    // whole-archive facades allocate through the same hooks as the core
524    // (R-000177 allocator coherence).
525    let hook = allocator_slot(foreign_malloc_slot(), unsafe { xmlMalloc });
526    unsafe { hook(size) }
527}
528
529/// Default `xmlMalloc` body: plain `libc::malloc` (upstream default `malloc`).
530///
531/// This is the initial value of the exported `xmlMalloc` variable and never
532/// reads the variable (no recursion). Since 11.1-Z.3 (R-000178) it performs
533/// NO accounting: upstream's counters are maintained only by the debug
534/// allocator, so with the default installed `xmlMemUsed`/`xmlMemBlocks` are 0
535/// and `xmlMemSize` is 0 — byte-identical with the oracle.
536///
537/// # Safety
538///
539/// - `size` is a byte count passed straight to `libc::malloc`; 0 is handled
540///   by the platform contract (glibc returns a unique non-NULL pointer).
541/// - The returned pointer (NULL on failure) is a libc-owned allocation that
542///   must be freed exactly once with the matching free path and never
543///   dereferenced after freeing.
544/// - This wrapper never reads the exported allocator variables, so it cannot
545///   recurse and is safe to install as a hook.
546unsafe extern "C" fn xmlMallocDefault(size: usize) -> *mut c_void {
547    unsafe { default_malloc(size) }
548}
549
550/// Allocate through the exported `xmlMallocAtomic` variable.
551///
552/// # UPSTREAM-PARITY
553///
554/// ```c
555/// void *xmlMallocAtomic(size_t size);
556/// ```
557///
558/// Identical to `xmlMalloc` but hints to the GC that the memory does not
559/// contain pointers. In modern libxml2 this is equivalent to `xmlMalloc`;
560/// `xmlMemSetup` aliases it to the same hook (upstream xmlmemory.c).
561///
562/// # SAFETY
563///
564/// - The returned pointer must be freed with `xmlFree`
565/// - `size` may be 0 (returns a valid non-NULL pointer or NULL)
566pub unsafe extern "C" fn xmlMallocAtomicImpl(size: usize) -> *mut c_void {
567    // SAFETY: see xmlMallocImpl.
568    let hook = allocator_slot(foreign_malloc_atomic_slot(), unsafe { xmlMallocAtomic });
569    unsafe { hook(size) }
570}
571
572/// Default `xmlMallocAtomic` body (initial exported-variable value).
573///
574/// Atomic allocations share the malloc accounting body; `xmlGcMemSetup`
575/// installs a dedicated atomic hook via the variable (upstream xmlmemory.c).
576unsafe extern "C" fn xmlMallocAtomicDefault(size: usize) -> *mut c_void {
577    unsafe { xmlMallocDefault(size) }
578}
579
580/// Reallocate through the exported `xmlRealloc` variable.
581///
582/// # UPSTREAM-PARITY
583///
584/// ```c
585/// void *xmlRealloc(void *ptr, size_t size);
586/// ```
587///
588/// Changes the size of the memory block pointed to by `ptr`.
589/// If `ptr` is NULL, behaves like `xmlMalloc`.
590/// If `size` is 0, may return NULL (like C realloc).
591///
592/// # SAFETY
593///
594/// - `ptr` must be a valid pointer from `xmlMalloc`, `xmlMallocAtomic`, or `xmlRealloc`,
595///   or NULL
596/// - The returned pointer must be freed with `xmlFree`
597pub unsafe extern "C" fn xmlReallocImpl(ptr: *mut c_void, size: usize) -> *mut c_void {
598    // SAFETY: see xmlMallocImpl.
599    let hook = allocator_slot(foreign_realloc_slot(), unsafe { xmlRealloc });
600    unsafe { hook(ptr, size) }
601}
602
603/// Default `xmlRealloc` body: plain `libc::realloc` (upstream default `realloc`).
604///
605/// No accounting (R-000178) — see `xmlMallocDefault`. C semantics: `realloc(NULL,
606/// n)` allocates, `realloc(p, 0)` follows the platform contract (glibc frees and
607/// returns NULL), failure leaves the old block intact.
608unsafe extern "C" fn xmlReallocDefault(ptr: *mut c_void, size: usize) -> *mut c_void {
609    unsafe { default_realloc(ptr, size) }
610}
611
612/// Free through the exported `xmlFree` variable.
613///
614/// # UPSTREAM-PARITY
615///
616/// ```c
617/// void xmlFree(void *ptr);
618/// ```
619///
620/// Frees memory previously allocated with `xmlMalloc`, `xmlMallocAtomic`,
621/// or `xmlRealloc`. If `ptr` is NULL, no operation is performed.
622///
623/// # SAFETY
624///
625/// - `ptr` must be a valid pointer from `xmlMalloc`/`xmlMallocAtomic`/`xmlRealloc`,
626///   or NULL
627/// - After this call, `ptr` must not be dereferenced
628pub unsafe extern "C" fn xmlFreeImpl(ptr: *mut c_void) {
629    // SAFETY: see xmlMallocImpl.
630    let hook = allocator_slot(foreign_free_slot(), unsafe { xmlFree });
631    unsafe { hook(ptr) }
632}
633
634/// Default `xmlFree` body: plain `libc::free` (upstream default `free`).
635///
636/// No accounting (R-000178) — see `xmlMallocDefault`.
637unsafe extern "C" fn xmlFreeDefault(ptr: *mut c_void) {
638    unsafe { default_free(ptr) };
639}
640
641/// Duplicate a C string through the exported `xmlMemStrdup` variable.
642///
643/// # UPSTREAM-PARITY
644///
645/// ```c
646/// void *xmlMemStrdup(const char *str);
647/// ```
648///
649/// Returns a pointer to the newly allocated copy, or NULL on failure.
650///
651/// # SAFETY
652///
653/// - `str` must be a valid null-terminated C string or NULL
654/// - The returned pointer must be freed with `xmlFree`
655pub unsafe extern "C" fn xmlMemStrdupImpl(str: *const c_char) -> *mut c_void {
656    // SAFETY: see xmlMallocImpl.
657    let hook = allocator_slot(foreign_mem_strdup_slot(), unsafe { xmlMemStrdup });
658    unsafe { hook(str) }
659}
660
661/// Default `xmlMemStrdup` body: plain libc strdup (upstream default
662/// `xmlPosixStrdup`). No accounting (R-000178) — see `xmlMallocDefault`.
663unsafe extern "C" fn xmlMemStrdupDefault(str: *const c_char) -> *mut c_void {
664    unsafe { default_strdup(str) }
665}
666
667// ═══════════════════════════════════════════════════════════════════════════════
668// R-000177 allocator-slot bridge
669// ═══════════════════════════════════════════════════════════════════════════════
670// The allocator hooks (xmlMemSetup / direct xmlMalloc= assignments) bind to
671// the CORE DSO's exported slots; the whole-archive facades' private copies
672// of those statics are never written, so every *Impl indirection reads the
673// process-visible (core DSO's) slot via its __xml* accessor and falls back
674// to the local exported variable when the accessor is not exported
675// (single-DSO links resolve their own export — identical storage).
676
677#[cfg(target_os = "linux")]
678macro_rules! alloc_slot_reader {
679    ($reader:ident, $cname:expr, $t:ty) => {
680        /// §16.5.10 allocator-bridge hot path: cache the ADDRESS of the
681        /// process-visible (core DSO's) exported allocator slot — NOT the
682        /// function value it currently holds. The slot's contents are
683        /// reassigned by `xmlMemSetup` / direct `xmlMalloc = ...`
684        /// assignments and every allocation must observe the CURRENT hook,
685        /// but the slot's address is fixed for the process lifetime, so
686        /// reading through the cached address preserves mutation semantics
687        /// (a fresh value per call) while dropping the per-allocation
688        /// dlsym'd accessor invocation.
689        fn $reader() -> Option<*mut $t> {
690            use std::sync::OnceLock;
691            type Acc = unsafe extern "C" fn() -> *mut $t;
692            // usize storage keeps the static Sync/Send (raw fn pointers are
693            // not); the value is a plain pointer-sized slot address.
694            static ACC: OnceLock<Option<usize>> = OnceLock::new();
695            let slot = *ACC.get_or_init(|| {
696                // SAFETY: dlsym(RTLD_DEFAULT) returns the exported accessor
697                // address or NULL; the transmute (pointer-sized) is sound.
698                // The accessor runs exactly ONCE, lazily, to learn the slot
699                // address; the slot itself is process-lifetime storage.
700                unsafe {
701                    let sym = libc::dlsym(libc::RTLD_DEFAULT, ($cname).as_ptr());
702                    if sym.is_null() {
703                        None
704                    } else {
705                        let acc = std::mem::transmute::<*mut c_void, Acc>(sym);
706                        Some(acc() as usize)
707                    }
708                }
709            });
710            slot.map(|a| a as *mut $t)
711        }
712    };
713}
714
715#[cfg(not(target_os = "linux"))]
716macro_rules! alloc_slot_reader {
717    ($reader:ident, $cname:expr, $t:ty) => {
718        fn $reader() -> Option<*mut $t> {
719            None
720        }
721    };
722}
723
724alloc_slot_reader!(foreign_malloc_slot, c"__xmlMalloc", xmlMallocFunc);
725alloc_slot_reader!(
726    foreign_malloc_atomic_slot,
727    c"__xmlMallocAtomic",
728    xmlMallocFunc
729);
730alloc_slot_reader!(foreign_realloc_slot, c"__xmlRealloc", xmlReallocFunc);
731alloc_slot_reader!(foreign_free_slot, c"__xmlFree", xmlFreeFunc);
732alloc_slot_reader!(foreign_mem_strdup_slot, c"__xmlMemStrdup", xmlStrdupFunc);
733
734/// Resolve the active allocator hook: the process-visible (core) slot when
735/// its accessor resolves, else the local exported variable via `local`.
736#[inline]
737fn allocator_slot<T: Copy>(foreign: Option<*mut T>, local: T) -> T {
738    match foreign {
739        Some(slot) => {
740            // SAFETY: the accessor returned a pointer to the core DSO's
741            // exported allocator slot (valid for the process lifetime); the
742            // stored value is a valid C fn pointer under the upstream
743            // setup-ordering contract.
744            unsafe { *slot }
745        }
746        None => local,
747    }
748}
749
750/// `xmlMallocFunc xmlMalloc` — the malloc hook (default: `xmlMallocDefault`).
751#[no_mangle]
752pub static mut xmlMalloc: xmlMallocFunc = xmlMallocDefault;
753
754/// `xmlMallocFunc xmlMallocAtomic` — the atomic-malloc hook.
755#[no_mangle]
756pub static mut xmlMallocAtomic: xmlMallocFunc = xmlMallocAtomicDefault;
757
758/// `xmlReallocFunc xmlRealloc` — the realloc hook.
759#[no_mangle]
760pub static mut xmlRealloc: xmlReallocFunc = xmlReallocDefault;
761
762/// `xmlFreeFunc xmlFree` — the free hook.
763#[no_mangle]
764pub static mut xmlFree: xmlFreeFunc = xmlFreeDefault;
765
766/// `xmlStrdupFunc xmlMemStrdup` — the strdup hook.
767#[no_mangle]
768pub static mut xmlMemStrdup: xmlStrdupFunc = xmlMemStrdupDefault;
769
770// ── LIBXML_THREAD_ALLOC_ENABLED accessors (upstream globals.c) ─────────────
771//
772// Upstream source builds with --with-thread-alloc export accessor FUNCTIONS
773// `xmlMallocFunc *__xmlMalloc(void)` etc. (globals.c, gated by
774// LIBXML_THREAD_ALLOC_ENABLED); its xmlmemory.h then redefines `xmlMalloc`
775// as `(*__xmlMalloc())`, so consumers compiled against the thread-alloc
776// profile reference these accessors instead of the variables. The candidate
777// implements them per upstream semantics: each returns a pointer to the
778// allocator slot the library uses — which is exactly the corresponding
779// exported variable above (single source of truth, R-000176) — so
780// `(*__xmlMalloc())(size)` is the candidate's `xmlMalloc` variable.
781//
782// The executed distro oracle (system 2.15.3) hides these five accessors
783// (built without thread-alloc / with hidden visibility), so they are
784// CUSTODIAN_EXTENSION exports in the disposition ledger: upstream-ABI-valid,
785// required by source-built consumers (e.g. the canonical source oracle in
786// the Phase-12 DOCKER-SUBSTITUTION court, whose libxslt references
787// __xmlFree/__xmlMalloc/__xmlRealloc).
788
789/// Upstream `xmlMallocFunc *__xmlMalloc(void)`.
790///
791/// # Safety
792///
793/// - The returned pointer is the address of the exported `xmlMalloc`
794///   variable; it stays valid for the process lifetime and may be read or
795///   written through exactly like upstream's thread-local allocator slot.
796#[no_mangle]
797pub unsafe extern "C" fn __xmlMalloc() -> *mut xmlMallocFunc {
798    core::ptr::addr_of_mut!(xmlMalloc)
799}
800
801/// Upstream `xmlMallocFunc *__xmlMallocAtomic(void)`.
802///
803/// # Safety
804///
805/// - The returned pointer is the address of the exported `xmlMallocAtomic`
806///   variable; it stays valid for the process lifetime.
807#[no_mangle]
808pub unsafe extern "C" fn __xmlMallocAtomic() -> *mut xmlMallocFunc {
809    core::ptr::addr_of_mut!(xmlMallocAtomic)
810}
811
812/// Upstream `xmlReallocFunc *__xmlRealloc(void)`.
813///
814/// # Safety
815///
816/// - The returned pointer is the address of the exported `xmlRealloc`
817///   variable; it stays valid for the process lifetime.
818#[no_mangle]
819pub unsafe extern "C" fn __xmlRealloc() -> *mut xmlReallocFunc {
820    core::ptr::addr_of_mut!(xmlRealloc)
821}
822
823/// Upstream `xmlFreeFunc *__xmlFree(void)`.
824///
825/// # Safety
826///
827/// - The returned pointer is the address of the exported `xmlFree`
828///   variable; it stays valid for the process lifetime.
829#[no_mangle]
830pub unsafe extern "C" fn __xmlFree() -> *mut xmlFreeFunc {
831    core::ptr::addr_of_mut!(xmlFree)
832}
833
834/// Upstream `xmlStrdupFunc *__xmlMemStrdup(void)`.
835///
836/// # Safety
837///
838/// - The returned pointer is the address of the exported `xmlMemStrdup`
839///   variable; it stays valid for the process lifetime.
840#[no_mangle]
841pub unsafe extern "C" fn __xmlMemStrdup() -> *mut xmlStrdupFunc {
842    core::ptr::addr_of_mut!(xmlMemStrdup)
843}
844
845// ═══════════════════════════════════════════════════════════════════════════════
846// Memory Debugging / Statistics
847// ═══════════════════════════════════════════════════════════════════════════════
848
849/// Return the total amount of memory currently allocated (approximate).
850///
851/// # UPSTREAM-PARITY
852///
853/// ```c
854/// int xmlMemUsed(void);
855/// ```
856///
857/// Returns upstream's `debugMemSize` counter, which is maintained ONLY by the
858/// debug allocator (`xmlMemMalloc`/`*Loc` surface). With the default
859/// allocator installed the counter stays 0 — byte-identical with the oracle
860/// (R-000178, verified by ALLOCATOR-DEFAULT-001). Custom allocator hooks
861/// never touch it (upstream contract).
862#[no_mangle]
863pub extern "C" fn xmlMemUsed() -> c_int {
864    MEM_USED.load(Ordering::Relaxed) as c_int
865}
866
867/// Return the current number of allocated blocks (approximate).
868///
869/// # UPSTREAM-PARITY
870///
871/// ```c
872/// int xmlMemBlocks(void);
873/// ```
874///
875/// Returns upstream's `debugMemBlocks` counter (debug allocator only; 0 with
876/// the default allocator — R-000178, byte-identical with the oracle).
877#[no_mangle]
878pub extern "C" fn xmlMemBlocks() -> c_int {
879    MEM_BLOCKS.load(Ordering::Relaxed) as c_int
880}
881
882/// Display memory allocation information to a file.
883///
884/// # UPSTREAM-PARITY
885///
886/// ```c
887/// void xmlMemDisplay(FILE *fp);
888/// ```
889///
890/// No-op — upstream 2.15.0 removed this feature (`@deprecated This feature
891/// was removed.`). The pre-Z.3 candidate printed aggregate counters; that was
892/// a divergence from the executed oracle and is removed (R-000131 sealed).
893///
894/// # SAFETY
895///
896/// - `fp` must be a valid FILE* pointer or NULL (unused)
897#[no_mangle]
898pub const unsafe extern "C" fn xmlMemDisplay(_fp: *mut c_void) {}
899
900/// Show memory allocation information.
901///
902/// # UPSTREAM-PARITY
903///
904/// ```c
905/// void xmlMemShow(FILE *fp, int nr);
906/// ```
907///
908/// No-op — upstream 2.15.0 removed this feature (`@deprecated This feature
909/// was removed.`); the candidate previously dumped the registry with a
910/// non-upstream ordering, a documented divergence that is now removed
911/// (R-000131 sealed).
912///
913/// # SAFETY
914///
915/// - `fp` must be a valid FILE* pointer or NULL (unused)
916#[no_mangle]
917pub const unsafe extern "C" fn xmlMemShow(_fp: *mut c_void, _nr: c_int) {}
918
919// ═══════════════════════════════════════════════════════════════════════════════
920// Convenience Functions (used internally)
921// ═══════════════════════════════════════════════════════════════════════════════
922
923/// Allocate zero-initialized memory.
924///
925/// # UPSTREAM-PARITY
926///
927/// This is like `xmlMalloc` followed by `memset(0)`, but some allocator
928/// hooks provide it directly.
929///
930/// # SAFETY
931///
932/// Same as `xmlMalloc`. The returned memory is zero-initialized.
933#[no_mangle]
934pub unsafe extern "C" fn xmlMallocZero(size: usize) -> *mut c_void {
935    // SAFETY: Delegates to xmlMallocImpl and zeroes the memory.
936    let ptr = unsafe { xmlMallocImpl(size) };
937    if !ptr.is_null() {
938        unsafe { ptr::write_bytes(ptr, 0, size) };
939    }
940    ptr
941}
942
943/// Allocate zero-initialized memory (atomic variant).
944///
945/// # UPSTREAM-PARITY
946///
947/// Like `xmlMallocAtomic` followed by zero-initialization.
948///
949/// # SAFETY
950///
951/// The function touches crate-global state only; it is safe
952/// as long as the caller respects the library's global
953/// initialization/cleanup ordering (xmlInitParser before use,
954/// xmlCleanupParser only after all users are done).
955///
956/// Violating the global lifecycle ordering, or calling this after
957/// teardown or from a signal handler, is undefined behavior.
958#[no_mangle]
959pub unsafe extern "C" fn xmlMallocAtomicZero(size: usize) -> *mut c_void {
960    // SAFETY: Delegates to xmlMallocAtomicImpl and zeroes the memory.
961    let ptr = unsafe { xmlMallocAtomicImpl(size) };
962    if !ptr.is_null() {
963        unsafe { ptr::write_bytes(ptr, 0, size) };
964    }
965    ptr
966}
967
968/// Reallocate and zero-initialize the new portion.
969///
970/// # UPSTREAM-PARITY
971///
972/// Like `xmlRealloc`, but zeroes any newly allocated bytes.
973///
974/// # SAFETY
975///
976/// Same as `xmlRealloc`.
977#[no_mangle]
978pub unsafe extern "C" fn xmlReallocZero(
979    ptr: *mut c_void,
980    old_size: usize,
981    new_size: usize,
982) -> *mut c_void {
983    // SAFETY: Delegates to xmlRealloc and zeroes the new portion.
984    let new_ptr = unsafe { xmlReallocImpl(ptr, new_size) };
985    if !new_ptr.is_null() && new_size > old_size {
986        unsafe {
987            ptr::write_bytes(new_ptr.add(old_size), 0, new_size - old_size);
988        }
989    }
990    new_ptr
991}
992
993// ═══════════════════════════════════════════════════════════════════════════════
994// Debug Allocator (Optional)
995// ═══════════════════════════════════════════════════════════════════════════════
996
997/// Initialize the memory layer with debugging support.
998///
999/// # UPSTREAM-PARITY
1000///
1001/// ```c
1002/// int xmlInitMemory(void);
1003/// ```
1004///
1005/// Initializes the memory subsystem. Returns 0 on success.
1006/// This is called automatically by `xmlInitParser`.
1007#[no_mangle]
1008pub const extern "C" fn xmlInitMemory() -> c_int {
1009    0
1010}
1011
1012/// Clean up the memory layer.
1013///
1014/// # UPSTREAM-PARITY
1015///
1016/// ```c
1017/// void xmlCleanupMemory(void);
1018/// ```
1019#[no_mangle]
1020pub const extern "C" fn xmlCleanupMemory() {
1021    // Phase 1: no cleanup needed for the default allocator.
1022}
1023
1024// ═══════════════════════════════════════════════════════════════════════════════
1025// Legacy-named allocator API (upstream xmlmemory.h)
1026// ═══════════════════════════════════════════════════════════════════════════════
1027// Upstream xmlmemory.h historically exported `xmlMemMalloc`, `xmlMemFree`,
1028// `xmlMemRealloc`, `xmlMemoryStrdup`, the `*Loc` location-tracking variants,
1029// `xmlMemSize`, `xmlMemDisplayLast` and `xmlMemoryDump` alongside the modern
1030// names. Downstream code (older consumers, some language bindings) links
1031// against these names, so the candidate exports them with identical
1032// semantics. In upstream 2.15.0 these ARE the debug allocator: always
1033// libc-backed, independent of the hook variables, and tracked by
1034// `debugMemSize`/`debugMemBlocks` (with the MEMHDR tag enabling
1035// `xmlMemSize`). The candidate mirrors that exactly: the debug-named
1036// functions and the `*Loc` variants are always libc-backed + registry- and
1037// counter-tracked, do NOT route through the exported variables, and the
1038// `*Loc` location arguments are accepted and ignored — exactly like
1039// upstream's `ATTRIBUTE_UNUSED` parameters (R-000131 sealed). The candidate
1040// returns plain libc pointers (no MEMHDR prefix), so a debug-surface block
1041// can also be freed with `xmlFree` — a safe superset of the upstream
1042// contract (upstream requires `xmlMemFree` for such blocks).
1043
1044/// Debug-surface malloc: libc + counters + registry (upstream xmlMemMalloc).
1045///
1046/// # Safety
1047///
1048/// - `size` must be a valid allocation size; the underlying `default_malloc`
1049///   follows the libc contract (0 handled by the platform).
1050/// - `file` must be NULL or a valid pointer to a NUL-terminated C string that
1051///   stays valid for the duration of the call (it is stored by address only,
1052///   never dereferenced here).
1053/// - The returned pointer (NULL on failure, with counters untouched) is owned
1054///   by the caller and must be freed exactly once via `debug_free` or
1055///   `xmlMemFree`; never dereferenced after freeing.
1056unsafe fn debug_malloc(size: usize, file: *const c_char, line: c_int) -> *mut c_void {
1057    let ptr = unsafe { default_malloc(size) };
1058    if !ptr.is_null() {
1059        MEM_USED.fetch_add(size, Ordering::Relaxed);
1060        MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
1061        unsafe { block_record(ptr, size, file, line) };
1062    }
1063    ptr
1064}
1065
1066/// Debug-surface realloc: libc + counters + registry (upstream xmlMemRealloc).
1067///
1068/// # Safety
1069///
1070/// - `ptr` must be NULL or a valid pointer previously returned by the debug
1071///   surface (`debug_malloc`/`debug_realloc`) or by a matching libc
1072///   allocation, and not yet freed; NULL behaves like malloc.
1073/// - On failure the old block is left intact and stays recorded; on success
1074///   the old pointer is invalidated and the returned pointer must be freed
1075///   exactly once via `debug_free`/`xmlMemFree`.
1076/// - `file` must be NULL or a valid NUL-terminated C string valid for the
1077///   duration of the call (stored by address only).
1078unsafe fn debug_realloc(
1079    ptr: *mut c_void,
1080    size: usize,
1081    file: *const c_char,
1082    line: c_int,
1083) -> *mut c_void {
1084    let new_ptr = unsafe { default_realloc(ptr, size) };
1085    if !new_ptr.is_null() {
1086        let old_size = unsafe { block_forget(ptr) };
1087        if let Some(old) = old_size {
1088            MEM_USED.fetch_add(size.saturating_sub(old), Ordering::Relaxed);
1089        } else if ptr.is_null() {
1090            MEM_USED.fetch_add(size, Ordering::Relaxed);
1091            MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
1092        }
1093        unsafe { block_record(new_ptr, size, file, line) };
1094    }
1095    new_ptr
1096}
1097
1098/// Debug-surface strdup: libc + counters + registry (upstream xmlMemoryStrdup).
1099///
1100/// # Safety
1101///
1102/// - `str` must be NULL or a valid pointer to a NUL-terminated C string
1103///   readable through its full length (including the terminator) for the
1104///   duration of the call; NULL yields NULL.
1105/// - `file` must be NULL or a valid NUL-terminated C string valid for the
1106///   duration of the call (stored by address only).
1107/// - The returned pointer (NULL on failure) must be freed exactly once via
1108///   `debug_free`/`xmlMemFree`; never dereferenced after freeing.
1109unsafe fn debug_strdup(str: *const c_char, file: *const c_char, line: c_int) -> *mut c_void {
1110    if str.is_null() {
1111        return ptr::null_mut();
1112    }
1113    let ptr = unsafe { default_strdup(str) };
1114    if !ptr.is_null() {
1115        let len = unsafe { libc::strlen(str) } + 1;
1116        MEM_USED.fetch_add(len, Ordering::Relaxed);
1117        MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
1118        unsafe { block_record(ptr, len, file, line) };
1119    }
1120    ptr
1121}
1122
1123/// Debug-surface free: registry/counter removal + libc free (upstream xmlMemFree).
1124/// A foreign pointer (not in the registry) is freed without touching the
1125/// counters — a safe divergence from upstream's tag-error print (which would
1126/// pollute stderr).
1127///
1128/// # Safety
1129///
1130/// - `ptr` must be NULL (a no-op) or a valid pointer previously returned by
1131///   the debug surface (`debug_malloc`/`debug_realloc`/`debug_strdup`) or by
1132///   a matching libc allocation; it must not be freed twice and must not be
1133///   dereferenced after this call.
1134/// - The registry and counters are only touched when the pointer was recorded;
1135///   a foreign pointer is still freed via libc.
1136unsafe fn debug_free(ptr: *mut c_void) {
1137    if ptr.is_null() {
1138        return;
1139    }
1140    let old_size = unsafe { block_forget(ptr) };
1141    unsafe { default_free(ptr) };
1142    if let Some(old) = old_size {
1143        MEM_USED.fetch_sub(old, Ordering::Relaxed);
1144        MEM_BLOCKS.fetch_sub(1, Ordering::Relaxed);
1145    }
1146}
1147
1148/// Allocate memory through the debug allocator (upstream xmlmemory.h).
1149///
1150/// ```c
1151/// void *xmlMemMalloc(size_t size);
1152/// ```
1153///
1154/// Always libc-backed and tracked (upstream debug allocator contract): the
1155/// block is recorded so `xmlMemSize`/`xmlMemUsed`/`xmlMemBlocks` observe it.
1156///
1157/// # SAFETY
1158///
1159/// - The returned pointer must be freed with `xmlMemFree` (or `xmlFree` —
1160///   plain libc pointer, candidate superset)
1161#[no_mangle]
1162pub unsafe extern "C" fn xmlMemMalloc(size: usize) -> *mut c_void {
1163    unsafe { debug_malloc(size, ptr::null(), 0) }
1164}
1165
1166/// Free memory through the debug allocator (upstream xmlmemory.h).
1167///
1168/// ```c
1169/// void xmlMemFree(void *ptr);
1170/// ```
1171///
1172/// Un-records the block and frees it. NULL is a no-op.
1173///
1174/// # SAFETY
1175///
1176/// - `ptr` must be valid pointers (or NULL
1177///   where the upstream C contract allows), obtained from the
1178///   matching constructor/owner and not yet freed; the callee may
1179///   take or keep ownership exactly as the C API specifies.
1180#[no_mangle]
1181pub unsafe extern "C" fn xmlMemFree(ptr: *mut c_void) {
1182    unsafe { debug_free(ptr) };
1183}
1184
1185/// Reallocate memory through the debug allocator (upstream xmlmemory.h).
1186///
1187/// ```c
1188/// void *xmlMemRealloc(void *ptr, size_t size);
1189/// ```
1190///
1191/// C realloc semantics + registry maintenance (upstream xmlMemRealloc:
1192/// NULL ptr allocates, failure leaves the old block recorded and intact).
1193///
1194/// # SAFETY
1195///
1196/// - `ptr` must be valid pointers (or NULL
1197///   where the upstream C contract allows), obtained from the
1198///   matching constructor/owner and not yet freed; the callee may
1199///   take or keep ownership exactly as the C API specifies.
1200#[no_mangle]
1201pub unsafe extern "C" fn xmlMemRealloc(ptr: *mut c_void, size: usize) -> *mut c_void {
1202    unsafe { debug_realloc(ptr, size, ptr::null(), 0) }
1203}
1204
1205/// Duplicate a string through the debug allocator (upstream xmlmemory.h).
1206///
1207/// ```c
1208/// void *xmlMemoryStrdup(const char *str);
1209/// ```
1210///
1211/// NULL returns NULL (upstream xmlPosixStrdup/xmlCharStrdup contract).
1212///
1213/// # SAFETY
1214///
1215/// - `str` must point to a valid NUL-terminated string, or NULL
1216/// - The returned pointer must be freed with `xmlMemFree` (or `xmlFree`)
1217#[no_mangle]
1218pub unsafe extern "C" fn xmlMemoryStrdup(str: *const c_char) -> *mut c_void {
1219    unsafe { debug_strdup(str, ptr::null(), 0) }
1220}
1221
1222/// Allocate memory, recording the allocation site (upstream xmlmemory.h).
1223///
1224/// ```c
1225/// void *xmlMallocLoc(size_t size, const char *file, int line);
1226/// ```
1227///
1228/// Debug-allocator contract (upstream xmlMallocLoc -> xmlMemMalloc): always
1229/// libc-backed + tracked, independent of the hook variables. Upstream 2.15.0
1230/// ignores the file/line arguments (`ATTRIBUTE_UNUSED`); the candidate
1231/// records them in the registry so `xmlMemSize` works (R-000131 sealed).
1232///
1233/// # SAFETY
1234///
1235///
1236/// - `file` must point to valid NUL-terminated
1237///   strings (or NULL where the C contract allows) for the lifetime
1238///   of the call.
1239///
1240/// The caller must not race this call with concurrent mutation of the
1241/// same objects from other threads (per-object state is not internally
1242/// synchronized). Violating any of the above is undefined behavior.
1243///
1244/// Exercised by the C-API differential courts
1245/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1246/// courts; those pass byte-for-byte against the upstream oracle.
1247#[no_mangle]
1248pub unsafe extern "C" fn xmlMallocLoc(
1249    size: usize,
1250    file: *const c_char,
1251    line: c_int,
1252) -> *mut c_void {
1253    unsafe { debug_malloc(size, file, line) }
1254}
1255
1256/// Allocate memory, recording the allocation site (upstream xmlmemory.h).
1257///
1258/// ```c
1259/// void *xmlMallocAtomicLoc(size_t size, const char *file, int line);
1260/// ```
1261///
1262/// Upstream xmlMallocAtomicLoc -> xmlMemMalloc: a plain (non-zeroed) tracked
1263/// allocation. The pre-Z.3 candidate zeroed it via `xmlMallocZero` — a real
1264/// divergence, fixed in 11.1-Z.3 (R-000178 narrative).
1265///
1266/// # SAFETY
1267///
1268///
1269/// - `file` must point to valid NUL-terminated
1270///   strings (or NULL where the C contract allows) for the lifetime
1271///   of the call.
1272///
1273/// The caller must not race this call with concurrent mutation of the
1274/// same objects from other threads (per-object state is not internally
1275/// synchronized). Violating any of the above is undefined behavior.
1276///
1277/// Exercised by the C-API differential courts
1278/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1279/// courts; those pass byte-for-byte against the upstream oracle.
1280#[no_mangle]
1281pub unsafe extern "C" fn xmlMallocAtomicLoc(
1282    size: usize,
1283    file: *const c_char,
1284    line: c_int,
1285) -> *mut c_void {
1286    unsafe { debug_malloc(size, file, line) }
1287}
1288
1289/// Reallocate memory, recording the allocation site (upstream xmlmemory.h).
1290///
1291/// ```c
1292/// void *xmlReallocLoc(void *ptr, size_t size, const char *file, int line);
1293/// ```
1294///
1295/// Debug-allocator contract (upstream xmlReallocLoc -> xmlMemRealloc): the
1296/// old record is superseded on success, kept on failure.
1297///
1298/// # SAFETY
1299///
1300/// - `ptr` must be valid pointers (or NULL
1301///   where the upstream C contract allows), obtained from the
1302///   matching constructor/owner and not yet freed; the callee may
1303///   take or keep ownership exactly as the C API specifies.
1304///
1305/// - `file` must point to valid NUL-terminated
1306///   strings (or NULL where the C contract allows) for the lifetime
1307///   of the call.
1308///
1309/// The caller must not race this call with concurrent mutation of the
1310/// same objects from other threads (per-object state is not internally
1311/// synchronized). Violating any of the above is undefined behavior.
1312///
1313/// Exercised by the C-API differential courts
1314/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1315/// courts; those pass byte-for-byte against the upstream oracle.
1316#[no_mangle]
1317pub unsafe extern "C" fn xmlReallocLoc(
1318    ptr: *mut c_void,
1319    size: usize,
1320    file: *const c_char,
1321    line: c_int,
1322) -> *mut c_void {
1323    unsafe { debug_realloc(ptr, size, file, line) }
1324}
1325
1326/// Duplicate a string, recording the allocation site (upstream xmlmemory.h).
1327///
1328/// ```c
1329/// void *xmlMemStrdupLoc(const char *str, const char *file, int line);
1330/// ```
1331///
1332/// Debug-allocator contract (upstream xmlMemStrdupLoc -> xmlMemoryStrdup).
1333///
1334/// # SAFETY
1335///
1336///
1337/// - `str`, `file` must point to valid NUL-terminated
1338///   strings (or NULL where the C contract allows) for the lifetime
1339///   of the call.
1340///
1341/// The caller must not race this call with concurrent mutation of the
1342/// same objects from other threads (per-object state is not internally
1343/// synchronized). Violating any of the above is undefined behavior.
1344///
1345/// Exercised by the C-API differential courts
1346/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1347/// courts; those pass byte-for-byte against the upstream oracle.
1348#[no_mangle]
1349pub unsafe extern "C" fn xmlMemStrdupLoc(
1350    str: *const c_char,
1351    file: *const c_char,
1352    line: c_int,
1353) -> *mut c_void {
1354    unsafe { debug_strdup(str, file, line) }
1355}
1356
1357/// Return the size of an allocated block (upstream xmlmemory.h).
1358///
1359/// ```c
1360/// size_t xmlMemSize(void *ptr);
1361/// ```
1362///
1363/// Returns the recorded size for debug-surface blocks (`xmlMemMalloc`/`*Loc`
1364/// surface) and 0 for everything else — matching upstream's MEMHDR tag
1365/// lookup, which misses on plain-malloc blocks (default allocator) and on
1366/// foreign pointers (R-000178; byte-identical with the oracle).
1367///
1368/// # SAFETY
1369///
1370/// - `ptr` must be valid pointers (or NULL
1371///   where the upstream C contract allows), obtained from the
1372///   matching constructor/owner and not yet freed; the callee may
1373///   take or keep ownership exactly as the C API specifies.
1374#[no_mangle]
1375pub unsafe extern "C" fn xmlMemSize(ptr: *mut c_void) -> usize {
1376    if ptr.is_null() {
1377        return 0;
1378    }
1379    BLOCKS
1380        .lock()
1381        .get(&(ptr as usize))
1382        .map(|m| m.size)
1383        .unwrap_or(0)
1384}
1385
1386/// Display a limited amount of memory debug information (upstream xmlmemory.h).
1387///
1388/// ```c
1389/// void xmlMemDisplayLast(FILE *fp, long nbBytes);
1390/// ```
1391///
1392/// No-op — upstream 2.15.0 removed this feature (`@deprecated This feature
1393/// was removed.`); the pre-Z.3 candidate dumped the registry with a
1394/// non-upstream format, a divergence now removed (R-000131 sealed).
1395///
1396/// # SAFETY
1397///
1398/// - `fp` must be a valid FILE* pointer or NULL (unused)
1399#[no_mangle]
1400pub const unsafe extern "C" fn xmlMemDisplayLast(_fp: *mut c_void, _nb_bytes: c_long) {}
1401
1402/// Dump memory allocation statistics (upstream xmlmemory.h).
1403///
1404/// ```c
1405/// void xmlMemoryDump(void);
1406/// ```
1407///
1408/// No-op — upstream 2.15.0 removed this feature (`@deprecated This feature
1409/// was removed.`).
1410///
1411/// # SAFETY
1412///
1413/// The function touches crate-global state only; it is safe
1414/// as long as the caller respects the library's global
1415/// initialization/cleanup ordering (xmlInitParser before use,
1416/// xmlCleanupParser only after all users are done).
1417#[no_mangle]
1418pub const unsafe extern "C" fn xmlMemoryDump() {
1419    // Upstream: empty body (feature removed in 2.15.0).
1420}
1421
1422// ═══════════════════════════════════════════════════════════════════════════════
1423// Tests
1424// ═══════════════════════════════════════════════════════════════════════════════
1425
1426#[cfg(test)]
1427mod tests {
1428    use super::*;
1429    use core::ptr;
1430
1431    /// Test basic allocation and deallocation.
1432    ///
1433    /// # Safety
1434    ///
1435    /// - `ptr` is the non-NULL result of `xmlMalloc(100)` (asserted): it is a
1436    ///   valid heap allocation owned by the caller, freed exactly once by
1437    ///   `xmlFree`, and not used afterwards.
1438    #[test]
1439    fn test_malloc_free() {
1440        unsafe {
1441            let ptr = xmlMalloc(100);
1442            assert!(!ptr.is_null(), "xmlMalloc(100) returned NULL");
1443            xmlFree(ptr);
1444        }
1445    }
1446
1447    /// Test that xmlMalloc(0) returns a valid pointer.
1448    ///
1449    /// # Safety
1450    ///
1451    /// - `xmlMalloc(0)` may return NULL or a unique non-NULL pointer per the
1452    ///   platform contract; a non-NULL result is a valid allocation freed
1453    ///   exactly once by `xmlFree` and not used afterwards.
1454    #[test]
1455    fn test_malloc_zero() {
1456        unsafe {
1457            let ptr = xmlMalloc(0);
1458            // malloc(0) may return NULL or a valid pointer.
1459            // If non-NULL, it must be freeable.
1460            if !ptr.is_null() {
1461                xmlFree(ptr);
1462            }
1463        }
1464    }
1465
1466    /// Test xmlFree(NULL) is a no-op.
1467    ///
1468    /// # Safety
1469    ///
1470    /// - Passing NULL to `xmlFree` is accepted and performs no operation; no
1471    ///   pointer is dereferenced.
1472    #[test]
1473    fn test_free_null() {
1474        unsafe {
1475            xmlFree(ptr::null_mut());
1476            // Should not crash
1477        }
1478    }
1479
1480    /// Test xmlRealloc.
1481    ///
1482    /// # Safety
1483    ///
1484    /// - `ptr` must be a valid non-NULL allocation from `xmlMalloc`; on the
1485    ///   successful `xmlRealloc` here the old pointer is invalidated and only
1486    ///   the returned `new_ptr` (non-NULL, asserted) is freed exactly once by
1487    ///   `xmlFree` and not used afterwards.
1488    #[test]
1489    fn test_realloc() {
1490        unsafe {
1491            let ptr = xmlMalloc(50);
1492            assert!(!ptr.is_null());
1493            let new_ptr = xmlRealloc(ptr, 100);
1494            assert!(!new_ptr.is_null());
1495            xmlFree(new_ptr);
1496        }
1497    }
1498
1499    /// Test xmlMemStrdup.
1500    ///
1501    /// # Safety
1502    ///
1503    /// - `s` points to a valid NUL-terminated byte string of 6 bytes (the
1504    ///   literal `hello` plus terminator) that stays alive for the call.
1505    /// - `dup` is the non-NULL result of `xmlMemStrdup`, a fresh allocation of
1506    ///   at least 6 bytes readable as a slice; it is freed exactly once by
1507    ///   `xmlFree` and not used afterwards.
1508    #[test]
1509    fn test_mem_strdup() {
1510        unsafe {
1511            let s = b"hello\0" as *const u8 as *const c_char;
1512            let dup = xmlMemStrdup(s);
1513            assert!(!dup.is_null());
1514            // Compare the strings
1515            let orig_slice = std::slice::from_raw_parts(s as *const u8, 6);
1516            let dup_slice = std::slice::from_raw_parts(dup as *const u8, 6);
1517            assert_eq!(orig_slice, dup_slice);
1518            xmlFree(dup);
1519        }
1520    }
1521
1522    /// Test xmlMallocZero returns zeroed memory.
1523    ///
1524    /// # Safety
1525    ///
1526    /// - `ptr` is the non-NULL result of `xmlMallocZero(100)`: a valid
1527    ///   allocation of at least 100 bytes, readable as a 100-byte slice
1528    ///   while alive, and freed exactly once by `xmlFree` afterwards.
1529    #[test]
1530    fn test_malloc_zero_init() {
1531        unsafe {
1532            let ptr = xmlMallocZero(100) as *mut u8;
1533            assert!(!ptr.is_null());
1534            let slice = std::slice::from_raw_parts(ptr, 100);
1535            assert!(slice.iter().all(|&b| b == 0));
1536            xmlFree(ptr as *mut c_void);
1537        }
1538    }
1539
1540    /// Test custom allocator setup/get (R-000176: int returns, single source
1541    /// of truth — `xmlMemSetup` writes the exported variables and `xmlMemGet`
1542    /// reads them back).
1543    ///
1544    /// # Safety
1545    ///
1546    /// - Each output pointer passed to `xmlMemGet` is derived from a live
1547    ///   stack local of matching type, so every non-NULL output points to
1548    ///   valid, aligned, writable storage for the duration of the call; NULL
1549    ///   outputs are tolerated and skipped.
1550    /// - The function pointers written back must be treated as valid
1551    ///   C-compatible hooks (they are the currently installed allocator
1552    ///   functions).
1553    #[test]
1554    fn test_mem_setup_get() {
1555        unsafe {
1556            let mut free_func: Option<xmlFreeFunc> = None;
1557            let mut malloc_func: Option<xmlMallocFunc> = None;
1558            let mut realloc_func: Option<xmlReallocFunc> = None;
1559            let mut strdup_func: Option<xmlStrdupFunc> = None;
1560
1561            let ret = xmlMemGet(
1562                &mut free_func as *mut _,
1563                &mut malloc_func as *mut _,
1564                &mut realloc_func as *mut _,
1565                &mut strdup_func as *mut _,
1566            );
1567            assert_eq!(ret, 0, "xmlMemGet must return 0");
1568            assert!(malloc_func.is_some());
1569            assert!(free_func.is_some());
1570            assert!(realloc_func.is_some());
1571            assert!(strdup_func.is_some());
1572
1573            // NULL output pointers are tolerated (upstream xmlmemory.c).
1574            let ret = xmlMemGet(
1575                ptr::null_mut(),
1576                &mut malloc_func as *mut _,
1577                ptr::null_mut(),
1578                ptr::null_mut(),
1579            );
1580            assert_eq!(ret, 0);
1581            assert!(malloc_func.is_some());
1582        }
1583    }
1584
1585    /// Test the GC allocator hooks: 5-argument Setup/Get with the dedicated
1586    /// `mallocAtomicFunc` slot (R-000176 — previously 4-arg and void).
1587    ///
1588    /// # Safety
1589    ///
1590    /// - Each output pointer passed to `xmlGcMemGet` is derived from a live
1591    ///   stack local of matching type, so every non-NULL output points to
1592    ///   valid, aligned, writable storage for the duration of the call; NULL
1593    ///   outputs are tolerated and skipped.
1594    #[test]
1595    fn test_gc_mem_setup_get() {
1596        unsafe {
1597            let mut free_func: Option<xmlFreeFunc> = None;
1598            let mut malloc_func: Option<xmlMallocFunc> = None;
1599            let mut malloc_atomic_func: Option<xmlMallocFunc> = None;
1600            let mut realloc_func: Option<xmlReallocFunc> = None;
1601            let mut strdup_func: Option<xmlStrdupFunc> = None;
1602
1603            let ret = xmlGcMemGet(
1604                &mut free_func as *mut _,
1605                &mut malloc_func as *mut _,
1606                &mut malloc_atomic_func as *mut _,
1607                &mut realloc_func as *mut _,
1608                &mut strdup_func as *mut _,
1609            );
1610            assert_eq!(ret, 0, "xmlGcMemGet must return 0");
1611            assert!(free_func.is_some());
1612            assert!(malloc_func.is_some());
1613            assert!(malloc_atomic_func.is_some());
1614            assert!(realloc_func.is_some());
1615            assert!(strdup_func.is_some());
1616        }
1617    }
1618
1619    /// Test `xmlMemSetup` NULL validation returns -1 (upstream xmlmemory.c)
1620    /// and that a NULL `mallocAtomicFunc` makes `xmlGcMemSetup` fail.
1621    ///
1622    /// # Safety
1623    ///
1624    /// - `default_malloc` cast to `xmlMallocFunc` is a valid C-compatible
1625    ///   malloc-shaped function pointer; it is only passed as an argument here
1626    ///   and never called by this test.
1627    /// - The NULL hooks are rejected with -1 before any write, so no
1628    ///   allocator state is modified by this test.
1629    #[test]
1630    fn test_mem_setup_null_rejected() {
1631        unsafe {
1632            let ret = xmlMemSetup(None, Some(default_malloc as xmlMallocFunc), None, None);
1633            assert_eq!(ret, -1, "NULL hook must be rejected with -1");
1634            let ret = xmlGcMemSetup(
1635                None,
1636                Some(default_malloc as xmlMallocFunc),
1637                Some(default_malloc as xmlMallocFunc),
1638                None,
1639                None,
1640            );
1641            assert_eq!(ret, -1);
1642        }
1643    }
1644
1645    /// Test the single-source-of-truth model (R-000176): a direct assignment
1646    /// to the exported `xmlMalloc` variable is observed by `xmlMemGet` AND by
1647    /// actual internal allocations through `xmlMallocImpl`.
1648    ///
1649    /// # Safety
1650    ///
1651    /// - The exported `static mut` allocator variables are read and written
1652    ///   directly, which is only valid under the upstream single-threaded
1653    ///   setup ordering: no other thread may allocate or read the variables
1654    ///   concurrently with these assignments.
1655    /// - The values installed (`xmlMallocDefault`/`xmlFreeDefault`) are valid
1656    ///   malloc/free-shaped functions; `p` from `xmlMallocImpl` is a valid
1657    ///   allocation freed exactly once by `xmlFreeImpl`; the prior hook values
1658    ///   are restored before the block ends.
1659    #[test]
1660    fn test_direct_assignment_coherence() {
1661        unsafe {
1662            let saved = xmlMalloc;
1663            let saved_free = xmlFree;
1664            // Install a counting hook via direct variable assignment.
1665            xmlMalloc = xmlMallocDefault;
1666            xmlFree = xmlFreeDefault;
1667
1668            // xmlMemGet reads the variable.
1669            let mut malloc_func: Option<xmlMallocFunc> = None;
1670            let ret = xmlMemGet(
1671                ptr::null_mut(),
1672                &mut malloc_func as *mut _,
1673                ptr::null_mut(),
1674                ptr::null_mut(),
1675            );
1676            assert_eq!(ret, 0);
1677            assert!(
1678                core::ptr::fn_addr_eq(malloc_func.unwrap(), xmlMallocDefault as xmlMallocFunc,),
1679                "xmlMemGet must return the directly-assigned variable"
1680            );
1681
1682            // Internal allocation routes through the variable.
1683            let p = xmlMallocImpl(64);
1684            assert!(!p.is_null());
1685            xmlFreeImpl(p);
1686
1687            // Restore the defaults so other tests are unaffected.
1688            xmlMalloc = saved;
1689            xmlFree = saved_free;
1690        }
1691    }
1692
1693    /// Test xmlMemUsed and xmlMemBlocks return 0 with the default allocator
1694    /// and the debug surface is tracked (R-000178: byte-identical with the
1695    /// oracle — default malloc untracked, xmlMemMalloc/*Loc tracked).
1696    ///
1697    /// # Safety
1698    ///
1699    /// - `ptr` from `xmlMalloc` is a valid libc allocation freed exactly once
1700    ///   by `xmlFree`; `xmlMemSize` reads the registry by address only and is
1701    ///   safe on freed default-allocator pointers (returns 0).
1702    /// - `dptr` from `xmlMallocLoc` and `rptr` from `xmlReallocLoc` are
1703    ///   debug-surface allocations: on success the old pointer is invalidated
1704    ///   and only `rptr` is freed, exactly once, by `xmlMemFree`.
1705    #[test]
1706    fn test_mem_stats() {
1707        unsafe {
1708            // Default allocator: plain libc, NOT tracked (oracle contract).
1709            let ptr = xmlMalloc(100);
1710            assert!(!ptr.is_null());
1711            assert_eq!(xmlMemSize(ptr), 0, "default-allocator blocks are untracked");
1712            xmlFree(ptr);
1713            assert_eq!(xmlMemSize(ptr), 0);
1714
1715            // Debug surface: tracked, xmlMemSize returns the recorded size.
1716            let dptr = xmlMallocLoc(100, c"mem.c".as_ptr(), 42);
1717            assert!(!dptr.is_null());
1718            assert_eq!(xmlMemSize(dptr), 100, "debug-surface blocks are tracked");
1719            let rptr = xmlReallocLoc(dptr, 200, c"mem.c".as_ptr(), 43);
1720            assert!(!rptr.is_null());
1721            // The realloc record supersedes the old one. glibc may grow the
1722            // block in place (rptr == dptr; the block at that address is then
1723            // recorded at 200) or MOVE it (the old address is released and
1724            // xmlMemSize(old) == 0 while rptr is the live 200-byte block). Both
1725            // are valid libc behaviors — assert per case so the test is
1726            // environment-independent (CI glibc moves; some hosts grow in
1727            // place).
1728            assert_eq!(xmlMemSize(rptr), 200);
1729            if rptr == dptr {
1730                assert_eq!(xmlMemSize(dptr), 200);
1731            } else {
1732                assert_eq!(
1733                    xmlMemSize(dptr),
1734                    0,
1735                    "a moved realloc releases the old address"
1736                );
1737            }
1738            xmlMemFree(rptr);
1739            assert_eq!(xmlMemSize(rptr), 0);
1740
1741            // xmlMemSize(NULL) is 0.
1742            assert_eq!(xmlMemSize(ptr::null_mut()), 0);
1743        }
1744    }
1745
1746    /// Test the default allocator follows the C/libc contract exactly
1747    /// (R-000178): malloc(0) non-NULL, realloc(p,0) NULL after freeing,
1748    /// realloc(NULL,n) == malloc, realloc failure leaves the old block,
1749    /// malloc(SIZE_MAX) NULL, strdup(NULL) NULL.
1750    ///
1751    /// # Safety
1752    ///
1753    /// - Each non-NULL allocation is freed exactly once by `xmlFree`, and
1754    ///   pointers are not used after freeing; in particular `q` is freed by
1755    ///   the `realloc(p, 0)` call that returns NULL (glibc contract), so it
1756    ///   must not be used afterwards, and `r` is freed explicitly after the
1757    ///   failed huge realloc leaves it intact.
1758    /// - NULL arguments to `xmlRealloc`, `xmlMemStrdup` and `xmlFree` are
1759    ///   accepted per the C contract.
1760    #[test]
1761    fn test_default_libc_semantics() {
1762        unsafe {
1763            // malloc(0): glibc returns a unique non-NULL pointer.
1764            let p0 = xmlMalloc(0);
1765            assert!(!p0.is_null(), "malloc(0) must be non-NULL on glibc");
1766            xmlFree(p0);
1767
1768            // realloc(NULL, n) allocates.
1769            let p = xmlRealloc(ptr::null_mut(), 16);
1770            assert!(!p.is_null());
1771            // grow: content preserved.
1772            let q = xmlRealloc(p, 256);
1773            assert!(!q.is_null());
1774            // realloc(p, 0): glibc frees and returns NULL.
1775            let z = xmlRealloc(q, 0);
1776            assert!(z.is_null(), "realloc(p, 0) returns NULL on glibc");
1777
1778            // realloc failure: old block intact.
1779            let r = xmlMalloc(8);
1780            assert!(!r.is_null());
1781            let huge = xmlRealloc(r, usize::MAX);
1782            assert!(huge.is_null(), "realloc to SIZE_MAX must fail");
1783            xmlFree(r);
1784
1785            // malloc failure.
1786            let m = xmlMalloc(usize::MAX);
1787            assert!(m.is_null(), "malloc(SIZE_MAX) must fail");
1788
1789            // strdup(NULL) returns NULL (upstream xmlPosixStrdup).
1790            let d = xmlMemStrdup(ptr::null());
1791            assert!(d.is_null());
1792
1793            // free(NULL) no-op.
1794            xmlFree(ptr::null_mut());
1795        }
1796    }
1797
1798    /// Test the debug-named surface is independent of the hook variables and
1799    /// always libc-backed + tracked (upstream debug-allocator contract).
1800    ///
1801    /// # Safety
1802    ///
1803    /// - `p`, `q` and `s` are non-NULL debug-surface allocations; after the
1804    ///   successful `xmlMemRealloc` only `q` remains valid, and `q` and `s`
1805    ///   are each freed exactly once by `xmlMemFree` and not used afterwards.
1806    /// - `xmlMemFree(NULL)` is a no-op; `xmlMemSize` reads the registry by
1807    ///   address only.
1808    #[test]
1809    fn test_debug_surface_tracked() {
1810        unsafe {
1811            let p = xmlMemMalloc(64);
1812            assert!(!p.is_null());
1813            assert_eq!(xmlMemSize(p), 64);
1814            let q = xmlMemRealloc(p, 128);
1815            assert!(!q.is_null());
1816            assert_eq!(xmlMemSize(q), 128);
1817            let s = xmlMemoryStrdup(c"dbg".as_ptr());
1818            assert!(!s.is_null());
1819            assert_eq!(xmlMemSize(s), 4);
1820            xmlMemFree(q);
1821            xmlMemFree(s);
1822            // xmlMemFree(NULL) no-op.
1823            xmlMemFree(ptr::null_mut());
1824        }
1825    }
1826}