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
20use core::alloc::Layout;
21use core::ffi::c_void;
22use core::ptr;
23use core::sync::atomic::AtomicUsize;
24use core::sync::atomic::Ordering;
25use std::os::raw::{c_char, c_int, c_long};
26
27use parking_lot::RwLock;
28
29use crate::abi::callbacks::*;
30
31// ═══════════════════════════════════════════════════════════════════════════════
32// Default Allocator (Rust global allocator)
33// ═══════════════════════════════════════════════════════════════════════════════
34
35/// Default malloc implementation using Rust's global allocator.
36///
37/// # SAFETY
38///
39/// - `size` must be a valid allocation size (0 is handled by allocating 1 byte)
40/// - Returns NULL on allocation failure
41unsafe extern "C" fn default_malloc(size: usize) -> *mut c_void {
42    if size == 0 {
43        // Upstream malloc(0) may return NULL or a valid pointer.
44        // libxml2 checks for NULL and treats it as OOM.
45        // Allocate 1 byte to avoid UB with zero-size Layout.
46        let layout = Layout::from_size_align_unchecked(1, 1);
47        let ptr = std::alloc::alloc(layout);
48        if ptr.is_null() {
49            ptr::null_mut()
50        } else {
51            ptr as *mut c_void
52        }
53    } else {
54        let layout = match Layout::from_size_align(size, 1) {
55            Ok(l) => l,
56            Err(_) => return ptr::null_mut(),
57        };
58        let ptr = std::alloc::alloc(layout);
59        if ptr.is_null() {
60            ptr::null_mut()
61        } else {
62            ptr as *mut c_void
63        }
64    }
65}
66
67/// Default realloc implementation using Rust's global allocator.
68///
69/// # SAFETY
70///
71/// - `ptr` must be a valid pointer from a previous `default_malloc` or `default_realloc`,
72///   or NULL (in which case this behaves like malloc)
73/// - `size` must be a valid allocation size
74unsafe extern "C" fn default_realloc(ptr: *mut c_void, size: usize) -> *mut c_void {
75    if ptr.is_null() {
76        return default_malloc(size);
77    }
78    if size == 0 {
79        default_free(ptr);
80        return default_malloc(0);
81    }
82    let layout = match Layout::from_size_align(size, 1) {
83        Ok(l) => l,
84        Err(_) => return ptr::null_mut(),
85    };
86    let new_ptr = std::alloc::realloc(ptr as *mut u8, layout, size);
87    if new_ptr.is_null() {
88        ptr::null_mut()
89    } else {
90        new_ptr as *mut c_void
91    }
92}
93
94/// Default free implementation using Rust's global allocator.
95///
96/// # SAFETY
97///
98/// - `ptr` may be NULL (free(NULL) is a no-op)
99/// - If non-NULL, `ptr` must be from a previous `default_malloc` or `default_realloc`
100unsafe extern "C" fn default_free(ptr: *mut c_void) {
101    if ptr.is_null() {
102        return;
103    }
104    // We use a layout with size 0 and alignment 1 for deallocation,
105    // since Rust's dealloc requires the original layout.
106    // However, we don't know the original size. We use a 1-byte layout
107    // which is the minimum. This is technically UB in Rust but matches
108    // how C realloc/free work.
109    let layout = Layout::from_size_align_unchecked(1, 1);
110    std::alloc::dealloc(ptr as *mut u8, layout);
111}
112
113/// Default strdup implementation using Rust's global allocator.
114///
115/// # SAFETY
116///
117/// - `str` must be a valid null-terminated C string
118unsafe extern "C" fn default_strdup(str: *const c_char) -> *mut c_void {
119    if str.is_null() {
120        return ptr::null_mut();
121    }
122    let len = libc::strlen(str);
123    let size = len + 1; // include null terminator
124    let layout = match Layout::from_size_align(size, 1) {
125        Ok(l) => l,
126        Err(_) => return ptr::null_mut(),
127    };
128    let new_ptr = std::alloc::alloc(layout);
129    if new_ptr.is_null() {
130        return ptr::null_mut();
131    }
132    ptr::copy_nonoverlapping(str as *const u8, new_ptr, size);
133    new_ptr as *mut c_void
134}
135
136// ═══════════════════════════════════════════════════════════════════════════════
137// Global Allocator State
138// ═══════════════════════════════════════════════════════════════════════════════
139
140/// Global allocator function pointers.
141///
142/// Protected by RwLock for thread-safe read/write.
143/// Default values point to Rust's global allocator.
144static ALLOCATOR: RwLock<AllocatorFuncs> = RwLock::new(AllocatorFuncs {
145    malloc_func: Some(default_malloc as xmlMallocFunc),
146    realloc_func: Some(default_realloc as xmlReallocFunc),
147    free_func: Some(default_free as xmlFreeFunc),
148    strdup_func: Some(default_strdup as xmlStrdupFunc),
149});
150
151/// The set of allocator function pointers.
152struct AllocatorFuncs {
153    malloc_func: Option<xmlMallocFunc>,
154    realloc_func: Option<xmlReallocFunc>,
155    free_func: Option<xmlFreeFunc>,
156    strdup_func: Option<xmlStrdupFunc>,
157}
158
159/// Global allocation counters (for xmlMemUsed/xmlMemBlocks).
160///
161/// These use relaxed ordering since they are approximate debugging counters.
162static MEM_USED: AtomicUsize = AtomicUsize::new(0);
163static MEM_BLOCKS: AtomicUsize = AtomicUsize::new(0);
164
165// ═══════════════════════════════════════════════════════════════════════════════
166// Public Allocator API
167// ═══════════════════════════════════════════════════════════════════════════════
168
169/// Set custom memory allocator functions.
170///
171/// # UPSTREAM-PARITY
172///
173/// ```c
174/// void xmlMemSetup(xmlFreeFunc freeFunc,
175///                  xmlMallocFunc mallocFunc,
176///                  xmlReallocFunc reallocFunc,
177///                  xmlStrdupFunc strdupFunc);
178/// ```
179///
180/// # SAFETY
181///
182/// - All function pointers must be valid (non-null) and thread-safe
183/// - The functions must follow the C malloc/realloc/free/strdup contract
184/// - Once set, the functions remain in effect until the next `xmlMemSetup` call
185/// - The caller is responsible for ensuring the functions remain valid for the
186///   entire time they are installed
187#[no_mangle]
188pub unsafe extern "C" fn xmlMemSetup(
189    freeFunc: Option<xmlFreeFunc>,
190    mallocFunc: Option<xmlMallocFunc>,
191    reallocFunc: Option<xmlReallocFunc>,
192    strdupFunc: Option<xmlStrdupFunc>,
193) {
194    // SAFETY: Caller guarantees all function pointers are valid.
195    // We store them in global state protected by RwLock.
196    let mut alloc = ALLOCATOR.write();
197    alloc.free_func = freeFunc;
198    alloc.malloc_func = mallocFunc;
199    alloc.realloc_func = reallocFunc;
200    alloc.strdup_func = strdupFunc;
201}
202
203/// Get the current memory allocator functions.
204///
205/// # UPSTREAM-PARITY
206///
207/// ```c
208/// void xmlMemGet(xmlFreeFunc *freeFunc,
209///                xmlMallocFunc *mallocFunc,
210///                xmlReallocFunc *reallocFunc,
211///                xmlStrdupFunc *strdupFunc);
212/// ```
213///
214/// # SAFETY
215///
216/// - All output pointers must be valid (non-null) and writable
217#[no_mangle]
218pub unsafe extern "C" fn xmlMemGet(
219    freeFunc: *mut Option<xmlFreeFunc>,
220    mallocFunc: *mut Option<xmlMallocFunc>,
221    reallocFunc: *mut Option<xmlReallocFunc>,
222    strdupFunc: *mut Option<xmlStrdupFunc>,
223) {
224    // SAFETY: Caller guarantees all output pointers are valid.
225    let alloc = ALLOCATOR.read();
226    unsafe {
227        ptr::write(freeFunc, alloc.free_func);
228        ptr::write(mallocFunc, alloc.malloc_func);
229        ptr::write(reallocFunc, alloc.realloc_func);
230        ptr::write(strdupFunc, alloc.strdup_func);
231    }
232}
233
234/// Set GC-aware memory allocator functions.
235///
236/// # UPSTREAM-PARITY
237///
238/// This is a wrapper around `xmlMemSetup` in modern libxml2.
239/// Historically, it was separate for the GC-allocated memory pool,
240/// but in modern versions both functions do the same thing.
241#[no_mangle]
242pub unsafe extern "C" fn xmlGcMemSetup(
243    freeFunc: Option<xmlFreeFunc>,
244    mallocFunc: Option<xmlMallocFunc>,
245    reallocFunc: Option<xmlReallocFunc>,
246    strdupFunc: Option<xmlStrdupFunc>,
247) {
248    // SAFETY: Delegates to xmlMemSetup with the same safety contract.
249    unsafe { xmlMemSetup(freeFunc, mallocFunc, reallocFunc, strdupFunc) };
250}
251
252/// Get GC-aware memory allocator functions.
253///
254/// # UPSTREAM-PARITY
255///
256/// Wrapper around `xmlMemGet`.
257#[no_mangle]
258pub unsafe extern "C" fn xmlGcMemGet(
259    freeFunc: *mut Option<xmlFreeFunc>,
260    mallocFunc: *mut Option<xmlMallocFunc>,
261    reallocFunc: *mut Option<xmlReallocFunc>,
262    strdupFunc: *mut Option<xmlStrdupFunc>,
263) {
264    // SAFETY: Delegates to xmlMemGet with the same safety contract.
265    unsafe { xmlMemGet(freeFunc, mallocFunc, reallocFunc, strdupFunc) };
266}
267
268// ═══════════════════════════════════════════════════════════════════════════════
269// Allocation Functions
270// ═══════════════════════════════════════════════════════════════════════════════
271
272/// Allocate memory.
273///
274/// # UPSTREAM-PARITY
275///
276/// ```c
277/// void *xmlMalloc(size_t size);
278/// ```
279///
280/// Returns a pointer to the allocated memory, or NULL on failure.
281/// The allocated memory is not initialized (like malloc).
282///
283/// # SAFETY
284///
285/// - The returned pointer must be freed with `xmlFree`
286/// - `size` may be 0 (returns a valid non-NULL pointer or NULL)
287#[no_mangle]
288pub unsafe extern "C" fn xmlMalloc(size: usize) -> *mut c_void {
289    // SAFETY: We call the stored malloc function pointer.
290    // The function pointer must be valid (set by xmlMemSetup or default).
291    let alloc = ALLOCATOR.read();
292    let malloc_func = alloc.malloc_func.unwrap_or(default_malloc as xmlMallocFunc);
293    let ptr = unsafe { malloc_func(size) };
294    if !ptr.is_null() {
295        MEM_USED.fetch_add(size, Ordering::Relaxed);
296        MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
297    }
298    ptr
299}
300
301/// Allocate memory that will never contain pointers to other memory.
302///
303/// # UPSTREAM-PARITY
304///
305/// ```c
306/// void *xmlMallocAtomic(size_t size);
307/// ```
308///
309/// Identical to `xmlMalloc` but hints to the GC that the memory does not
310/// contain pointers. In modern libxml2, this is equivalent to `xmlMalloc`.
311#[no_mangle]
312pub unsafe extern "C" fn xmlMallocAtomic(size: usize) -> *mut c_void {
313    // SAFETY: Same as xmlMalloc.
314    unsafe { xmlMalloc(size) }
315}
316
317/// Reallocate memory.
318///
319/// # UPSTREAM-PARITY
320///
321/// ```c
322/// void *xmlRealloc(void *ptr, size_t size);
323/// ```
324///
325/// Changes the size of the memory block pointed to by `ptr`.
326/// If `ptr` is NULL, behaves like `xmlMalloc`.
327/// If `size` is 0, may return NULL (like C realloc).
328///
329/// # SAFETY
330///
331/// - `ptr` must be a valid pointer from `xmlMalloc`, `xmlMallocAtomic`, or `xmlRealloc`,
332///   or NULL
333/// - The returned pointer must be freed with `xmlFree`
334#[no_mangle]
335pub unsafe extern "C" fn xmlRealloc(ptr: *mut c_void, size: usize) -> *mut c_void {
336    // SAFETY: We call the stored realloc function pointer.
337    let alloc = ALLOCATOR.read();
338    let realloc_func = alloc
339        .realloc_func
340        .unwrap_or(default_realloc as xmlReallocFunc);
341    let new_ptr = unsafe { realloc_func(ptr, size) };
342    if !new_ptr.is_null() {
343        // Update counters (approximate — we don't know the old size)
344        MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
345        if ptr.is_null() {
346            MEM_USED.fetch_add(size, Ordering::Relaxed);
347        }
348        // Note: we don't subtract old size because we don't track it.
349        // This makes counters approximate, matching upstream behavior
350        // where the debugging allocator tracks sizes but the default doesn't.
351    }
352    new_ptr
353}
354
355/// Free allocated memory.
356///
357/// # UPSTREAM-PARITY
358///
359/// ```c
360/// void xmlFree(void *ptr);
361/// ```
362///
363/// Frees memory previously allocated with `xmlMalloc`, `xmlMallocAtomic`,
364/// or `xmlRealloc`. If `ptr` is NULL, no operation is performed.
365///
366/// # SAFETY
367///
368/// - `ptr` must be a valid pointer from `xmlMalloc`/`xmlMallocAtomic`/`xmlRealloc`,
369///   or NULL
370/// - After this call, `ptr` must not be dereferenced
371#[no_mangle]
372pub unsafe extern "C" fn xmlFree(ptr: *mut c_void) {
373    if ptr.is_null() {
374        return;
375    }
376    // SAFETY: We call the stored free function pointer.
377    let alloc = ALLOCATOR.read();
378    let free_func = alloc.free_func.unwrap_or(default_free as xmlFreeFunc);
379    unsafe { free_func(ptr) };
380    MEM_BLOCKS.fetch_sub(1, Ordering::Relaxed);
381}
382
383/// Duplicate a C string using the configured allocator.
384///
385/// # UPSTREAM-PARITY
386///
387/// ```c
388/// void *xmlMemStrdup(const char *str);
389/// ```
390///
391/// Returns a pointer to the newly allocated copy, or NULL on failure.
392///
393/// # SAFETY
394///
395/// - `str` must be a valid null-terminated C string or NULL
396/// - The returned pointer must be freed with `xmlFree`
397#[no_mangle]
398pub unsafe extern "C" fn xmlMemStrdup(str: *const c_char) -> *mut c_void {
399    if str.is_null() {
400        return ptr::null_mut();
401    }
402    // SAFETY: We call the stored strdup function pointer.
403    let alloc = ALLOCATOR.read();
404    let strdup_func = alloc.strdup_func.unwrap_or(default_strdup as xmlStrdupFunc);
405    let ptr = unsafe { strdup_func(str) };
406    if !ptr.is_null() {
407        let len = unsafe { libc::strlen(str) } + 1;
408        MEM_USED.fetch_add(len, Ordering::Relaxed);
409        MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
410    }
411    ptr
412}
413
414// ═══════════════════════════════════════════════════════════════════════════════
415// Memory Debugging / Statistics
416// ═══════════════════════════════════════════════════════════════════════════════
417
418/// Return the total amount of memory currently allocated (approximate).
419///
420/// # UPSTREAM-PARITY
421///
422/// ```c
423/// int xmlMemUsed(void);
424/// ```
425///
426/// Returns an approximate count of allocated bytes.
427/// With the default allocator, this tracks allocations but is not
428/// byte-exact for realloc (since we don't track old sizes).
429/// Custom allocator hooks can provide exact counts.
430#[no_mangle]
431pub extern "C" fn xmlMemUsed() -> c_int {
432    MEM_USED.load(Ordering::Relaxed) as c_int
433}
434
435/// Return the current number of allocated blocks (approximate).
436///
437/// # UPSTREAM-PARITY
438///
439/// ```c
440/// int xmlMemBlocks(void);
441/// ```
442///
443/// Returns an approximate count of live allocations.
444#[no_mangle]
445pub extern "C" fn xmlMemBlocks() -> c_int {
446    MEM_BLOCKS.load(Ordering::Relaxed) as c_int
447}
448
449/// Display memory allocation information to a file.
450///
451/// # UPSTREAM-PARITY
452///
453/// ```c
454/// void xmlMemDisplay(FILE *fp);
455/// ```
456///
457/// Prints debug memory information. With the default allocator,
458/// this prints a summary message. Custom allocator hooks may
459/// provide more detailed output.
460///
461/// # SAFETY
462///
463/// - `fp` must be a valid FILE* pointer or NULL (in which case stderr is used)
464#[no_mangle]
465pub unsafe extern "C" fn xmlMemDisplay(fp: *mut c_void) {
466    // SAFETY: Caller guarantees fp is valid FILE* or NULL.
467    unsafe {
468        let out = if fp.is_null() {
469            libc::fdopen(2, b"w\0" as *const u8 as *const c_char) as *mut c_void
470        } else {
471            fp
472        };
473        libc::fprintf(
474            out as *mut _,
475            b"Memory: used=%d blocks=%d\n\0" as *const u8 as *const c_char,
476            xmlMemUsed(),
477            xmlMemBlocks(),
478        );
479    }
480}
481
482/// Show memory allocation information.
483///
484/// # UPSTREAM-PARITY
485///
486/// ```c
487/// void xmlMemShow(FILE *fp, int nr);
488/// ```
489///
490/// Prints debug memory information for the last `nr` allocations.
491/// With the default allocator, this is a no-op (we don't track allocation history).
492#[no_mangle]
493pub unsafe extern "C" fn xmlMemShow(_fp: *mut c_void, _nr: c_int) {
494    // Phase 1: no-op with the default allocator.
495    // A future debugging allocator could track allocation history.
496}
497
498// ═══════════════════════════════════════════════════════════════════════════════
499// Convenience Functions (used internally)
500// ═══════════════════════════════════════════════════════════════════════════════
501
502/// Allocate zero-initialized memory.
503///
504/// # UPSTREAM-PARITY
505///
506/// This is like `xmlMalloc` followed by `memset(0)`, but some allocator
507/// hooks provide it directly.
508///
509/// # SAFETY
510///
511/// Same as `xmlMalloc`. The returned memory is zero-initialized.
512#[no_mangle]
513pub unsafe extern "C" fn xmlMallocZero(size: usize) -> *mut c_void {
514    // SAFETY: Delegates to xmlMalloc and zeroes the memory.
515    let ptr = unsafe { xmlMalloc(size) };
516    if !ptr.is_null() {
517        unsafe { ptr::write_bytes(ptr, 0, size) };
518    }
519    ptr
520}
521
522/// Allocate zero-initialized memory (atomic variant).
523///
524/// # UPSTREAM-PARITY
525///
526/// Like `xmlMallocAtomic` followed by zero-initialization.
527#[no_mangle]
528pub unsafe extern "C" fn xmlMallocAtomicZero(size: usize) -> *mut c_void {
529    // SAFETY: Delegates to xmlMallocAtomic and zeroes the memory.
530    let ptr = unsafe { xmlMallocAtomic(size) };
531    if !ptr.is_null() {
532        unsafe { ptr::write_bytes(ptr, 0, size) };
533    }
534    ptr
535}
536
537/// Reallocate and zero-initialize the new portion.
538///
539/// # UPSTREAM-PARITY
540///
541/// Like `xmlRealloc`, but zeroes any newly allocated bytes.
542///
543/// # SAFETY
544///
545/// Same as `xmlRealloc`.
546#[no_mangle]
547pub unsafe extern "C" fn xmlReallocZero(
548    ptr: *mut c_void,
549    old_size: usize,
550    new_size: usize,
551) -> *mut c_void {
552    // SAFETY: Delegates to xmlRealloc and zeroes the new portion.
553    let new_ptr = unsafe { xmlRealloc(ptr, new_size) };
554    if !new_ptr.is_null() && new_size > old_size {
555        unsafe {
556            ptr::write_bytes(new_ptr.add(old_size), 0, new_size - old_size);
557        }
558    }
559    new_ptr
560}
561
562// ═══════════════════════════════════════════════════════════════════════════════
563// Debug Allocator (Optional)
564// ═══════════════════════════════════════════════════════════════════════════════
565
566/// Initialize the memory layer with debugging support.
567///
568/// # UPSTREAM-PARITY
569///
570/// ```c
571/// int xmlInitMemory(void);
572/// ```
573///
574/// Initializes the memory subsystem. Returns 0 on success.
575/// This is called automatically by `xmlInitParser`.
576#[no_mangle]
577pub extern "C" fn xmlInitMemory() -> c_int {
578    0
579}
580
581/// Clean up the memory layer.
582///
583/// # UPSTREAM-PARITY
584///
585/// ```c
586/// void xmlCleanupMemory(void);
587/// ```
588#[no_mangle]
589pub extern "C" fn xmlCleanupMemory() {
590    // Phase 1: no cleanup needed for the default allocator.
591}
592
593// ═══════════════════════════════════════════════════════════════════════════════
594// Legacy-named allocator API (upstream xmlmemory.h)
595// ═══════════════════════════════════════════════════════════════════════════════
596// Upstream xmlmemory.h historically exported `xmlMemMalloc`, `xmlMemFree`,
597// `xmlMemRealloc`, `xmlMemoryStrdup`, the `*Loc` location-tracking variants,
598// `xmlMemSize`, `xmlMemDisplayLast` and `xmlMemoryDump` alongside the modern
599// names. Downstream code (older consumers, some language bindings) links
600// against these names, so the candidate exports them with identical
601// semantics. The `*Loc` variants take a source file/line that the default
602// allocator does not track (upstream uses it for leak reports only); the
603// location arguments are accepted and ignored — a documented safe divergence
604// (residual R-000131).
605
606/// Allocate memory (legacy name; same contract as `xmlMalloc`).
607///
608/// ```c
609/// void *xmlMemMalloc(size_t size);
610/// ```
611#[no_mangle]
612pub unsafe extern "C" fn xmlMemMalloc(size: usize) -> *mut c_void {
613    // SAFETY: identical contract to xmlMalloc.
614    unsafe { xmlMalloc(size) }
615}
616
617/// Free memory (legacy name; same contract as `xmlFree`).
618///
619/// ```c
620/// void xmlMemFree(void *ptr);
621/// ```
622#[no_mangle]
623pub unsafe extern "C" fn xmlMemFree(ptr: *mut c_void) {
624    // SAFETY: identical contract to xmlFree.
625    unsafe { xmlFree(ptr) }
626}
627
628/// Reallocate memory (legacy name; same contract as `xmlRealloc`).
629///
630/// ```c
631/// void *xmlMemRealloc(void *ptr, size_t size);
632/// ```
633#[no_mangle]
634pub unsafe extern "C" fn xmlMemRealloc(ptr: *mut c_void, size: usize) -> *mut c_void {
635    // SAFETY: identical contract to xmlRealloc.
636    unsafe { xmlRealloc(ptr, size) }
637}
638
639/// Duplicate a string (legacy name; same contract as `xmlMemStrdup`).
640///
641/// ```c
642/// void *xmlMemoryStrdup(const char *str);
643/// ```
644#[no_mangle]
645pub unsafe extern "C" fn xmlMemoryStrdup(str: *const c_char) -> *mut c_void {
646    // SAFETY: identical contract to xmlMemStrdup.
647    unsafe { xmlMemStrdup(str) }
648}
649
650/// Allocate memory, recording the allocation site (upstream xmlmemory.h).
651///
652/// ```c
653/// void *xmlMallocLoc(size_t size, const char *file, int line);
654/// ```
655///
656/// The default candidate allocator does not track allocation sites (see
657/// residual R-000131); the location arguments are accepted for ABI
658/// compatibility and ignored.
659#[no_mangle]
660pub unsafe extern "C" fn xmlMallocLoc(
661    size: usize,
662    _file: *const c_char,
663    _line: c_int,
664) -> *mut c_void {
665    // SAFETY: identical contract to xmlMalloc.
666    unsafe { xmlMalloc(size) }
667}
668
669/// Allocate zeroed memory, recording the allocation site (upstream xmlmemory.h).
670///
671/// ```c
672/// void *xmlMallocAtomicLoc(size_t size, const char *file, int line);
673/// ```
674#[no_mangle]
675pub unsafe extern "C" fn xmlMallocAtomicLoc(
676    size: usize,
677    _file: *const c_char,
678    _line: c_int,
679) -> *mut c_void {
680    // SAFETY: identical contract to xmlMallocZero.
681    unsafe { xmlMallocZero(size) }
682}
683
684/// Reallocate memory, recording the allocation site (upstream xmlmemory.h).
685///
686/// ```c
687/// void *xmlReallocLoc(void *ptr, size_t size, const char *file, int line);
688/// ```
689#[no_mangle]
690pub unsafe extern "C" fn xmlReallocLoc(
691    ptr: *mut c_void,
692    size: usize,
693    _file: *const c_char,
694    _line: c_int,
695) -> *mut c_void {
696    // SAFETY: identical contract to xmlRealloc.
697    unsafe { xmlRealloc(ptr, size) }
698}
699
700/// Duplicate a string, recording the allocation site (upstream xmlmemory.h).
701///
702/// ```c
703/// void *xmlMemStrdupLoc(const char *str, const char *file, int line);
704/// ```
705#[no_mangle]
706pub unsafe extern "C" fn xmlMemStrdupLoc(
707    str: *const c_char,
708    _file: *const c_char,
709    _line: c_int,
710) -> *mut c_void {
711    // SAFETY: identical contract to xmlMemStrdup.
712    unsafe { xmlMemStrdup(str) }
713}
714
715/// Return the size of an allocated block (upstream xmlmemory.h).
716///
717/// ```c
718/// size_t xmlMemSize(void *ptr);
719/// ```
720///
721/// The default candidate allocator does not maintain a per-block size table
722/// (that is the upstream debug-allocator block list); it therefore returns 0
723/// for all blocks — a documented safe divergence (residual R-000131) until
724/// the allocator instrumentation court (11.1-J) adds block metadata.
725#[no_mangle]
726pub unsafe extern "C" fn xmlMemSize(_ptr: *mut c_void) -> usize {
727    0
728}
729
730/// Display a limited amount of memory debug information (upstream xmlmemory.h).
731///
732/// ```c
733/// void xmlMemDisplayLast(FILE *fp, long nbBytes);
734/// ```
735///
736/// The default candidate allocator prints the global counters (it has no
737/// per-block list); the output format is intentionally simpler than
738/// upstream's block dump — a documented safe divergence (residual R-000131).
739#[no_mangle]
740pub unsafe extern "C" fn xmlMemDisplayLast(fp: *mut c_void, _nb_bytes: c_long) {
741    // SAFETY: fp must be a valid FILE* or NULL (stderr used).
742    unsafe {
743        let used = MEM_USED.load(Ordering::Relaxed);
744        let blocks = MEM_BLOCKS.load(Ordering::Relaxed);
745        let out = if fp.is_null() {
746            libc::fdopen(2, b"w\0" as *const u8 as *const c_char) as *mut c_void
747        } else {
748            fp
749        };
750        if !out.is_null() {
751            let msg = format!(
752                "libxml-rs allocator: {} blocks, {} bytes in use\n",
753                blocks, used
754            );
755            let bytes = msg.as_bytes();
756            libc::fwrite(
757                bytes.as_ptr() as *const c_void,
758                1,
759                bytes.len(),
760                out as *mut libc::FILE,
761            );
762        }
763    }
764}
765
766/// Dump memory allocation statistics (upstream xmlmemory.h).
767///
768/// ```c
769/// int xmlMemoryDump(void);
770/// ```
771///
772/// Prints the global counters to stderr and returns 0 (no leak detector is
773/// active in the default allocator).
774#[no_mangle]
775pub unsafe extern "C" fn xmlMemoryDump() -> c_int {
776    unsafe {
777        xmlMemDisplayLast(ptr::null_mut(), -1);
778    }
779    0
780}
781
782// ═══════════════════════════════════════════════════════════════════════════════
783// Tests
784// ═══════════════════════════════════════════════════════════════════════════════
785
786#[cfg(test)]
787mod tests {
788    use super::*;
789    use core::ptr;
790
791    /// Test basic allocation and deallocation.
792    #[test]
793    fn test_malloc_free() {
794        unsafe {
795            let ptr = xmlMalloc(100);
796            assert!(!ptr.is_null(), "xmlMalloc(100) returned NULL");
797            xmlFree(ptr);
798        }
799    }
800
801    /// Test that xmlMalloc(0) returns a valid pointer.
802    #[test]
803    fn test_malloc_zero() {
804        unsafe {
805            let ptr = xmlMalloc(0);
806            // malloc(0) may return NULL or a valid pointer.
807            // If non-NULL, it must be freeable.
808            if !ptr.is_null() {
809                xmlFree(ptr);
810            }
811        }
812    }
813
814    /// Test xmlFree(NULL) is a no-op.
815    #[test]
816    fn test_free_null() {
817        unsafe {
818            xmlFree(ptr::null_mut());
819            // Should not crash
820        }
821    }
822
823    /// Test xmlRealloc.
824    #[test]
825    fn test_realloc() {
826        unsafe {
827            let ptr = xmlMalloc(50);
828            assert!(!ptr.is_null());
829            let new_ptr = xmlRealloc(ptr, 100);
830            assert!(!new_ptr.is_null());
831            xmlFree(new_ptr);
832        }
833    }
834
835    /// Test xmlMemStrdup.
836    #[test]
837    fn test_mem_strdup() {
838        unsafe {
839            let s = b"hello\0" as *const u8 as *const c_char;
840            let dup = xmlMemStrdup(s);
841            assert!(!dup.is_null());
842            // Compare the strings
843            let orig_slice = std::slice::from_raw_parts(s as *const u8, 6);
844            let dup_slice = std::slice::from_raw_parts(dup as *const u8, 6);
845            assert_eq!(orig_slice, dup_slice);
846            xmlFree(dup);
847        }
848    }
849
850    /// Test xmlMallocZero returns zeroed memory.
851    #[test]
852    fn test_malloc_zero_init() {
853        unsafe {
854            let ptr = xmlMallocZero(100) as *mut u8;
855            assert!(!ptr.is_null());
856            let slice = std::slice::from_raw_parts(ptr, 100);
857            assert!(slice.iter().all(|&b| b == 0));
858            xmlFree(ptr as *mut c_void);
859        }
860    }
861
862    /// Test custom allocator setup/get.
863    #[test]
864    fn test_mem_setup_get() {
865        unsafe {
866            let mut free_func: Option<xmlFreeFunc> = None;
867            let mut malloc_func: Option<xmlMallocFunc> = None;
868            let mut realloc_func: Option<xmlReallocFunc> = None;
869            let mut strdup_func: Option<xmlStrdupFunc> = None;
870
871            xmlMemGet(
872                &mut free_func as *mut _,
873                &mut malloc_func as *mut _,
874                &mut realloc_func as *mut _,
875                &mut strdup_func as *mut _,
876            );
877
878            assert!(malloc_func.is_some());
879            assert!(free_func.is_some());
880            assert!(realloc_func.is_some());
881            assert!(strdup_func.is_some());
882        }
883    }
884
885    /// Test xmlMemUsed and xmlMemBlocks return reasonable values.
886    #[test]
887    fn test_mem_stats() {
888        unsafe {
889            let before_used = xmlMemUsed();
890            let before_blocks = xmlMemBlocks();
891
892            let ptr = xmlMalloc(100);
893            assert!(!ptr.is_null());
894
895            // After allocation, used and blocks should be higher
896            assert!(xmlMemBlocks() >= before_blocks + 1);
897
898            xmlFree(ptr);
899        }
900    }
901}