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