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