Skip to main content

libxml_rs/abi/
allocator.rs

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