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