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