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#[no_mangle]
331pub unsafe extern "C" fn xmlMalloc(size: usize) -> *mut c_void {
332    // SAFETY: We call the stored malloc function pointer.
333    // The function pointer must be valid (set by xmlMemSetup or default).
334    let alloc = ALLOCATOR.read();
335    let malloc_func = alloc.malloc_func.unwrap_or(default_malloc as xmlMallocFunc);
336    let ptr = unsafe { malloc_func(size) };
337    if !ptr.is_null() {
338        MEM_USED.fetch_add(size, Ordering::Relaxed);
339        MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
340        unsafe { block_record(ptr, size, ptr::null(), 0) };
341    }
342    ptr
343}
344
345/// Allocate memory that will never contain pointers to other memory.
346///
347/// # UPSTREAM-PARITY
348///
349/// ```c
350/// void *xmlMallocAtomic(size_t size);
351/// ```
352///
353/// Identical to `xmlMalloc` but hints to the GC that the memory does not
354/// contain pointers. In modern libxml2, this is equivalent to `xmlMalloc`.
355#[no_mangle]
356pub unsafe extern "C" fn xmlMallocAtomic(size: usize) -> *mut c_void {
357    // SAFETY: Same as xmlMalloc.
358    unsafe { xmlMalloc(size) }
359}
360
361/// Reallocate memory.
362///
363/// # UPSTREAM-PARITY
364///
365/// ```c
366/// void *xmlRealloc(void *ptr, size_t size);
367/// ```
368///
369/// Changes the size of the memory block pointed to by `ptr`.
370/// If `ptr` is NULL, behaves like `xmlMalloc`.
371/// If `size` is 0, may return NULL (like C realloc).
372///
373/// # SAFETY
374///
375/// - `ptr` must be a valid pointer from `xmlMalloc`, `xmlMallocAtomic`, or `xmlRealloc`,
376///   or NULL
377/// - The returned pointer must be freed with `xmlFree`
378#[no_mangle]
379pub unsafe extern "C" fn xmlRealloc(ptr: *mut c_void, size: usize) -> *mut c_void {
380    // SAFETY: We call the stored realloc function pointer.
381    let alloc = ALLOCATOR.read();
382    let realloc_func = alloc
383        .realloc_func
384        .unwrap_or(default_realloc as xmlReallocFunc);
385    let new_ptr = unsafe { realloc_func(ptr, size) };
386    if !new_ptr.is_null() {
387        // Exact accounting via the block registry.
388        let old_size = unsafe { block_forget(ptr) };
389        MEM_USED.fetch_add(size.saturating_sub(old_size), Ordering::Relaxed);
390        if ptr.is_null() {
391            MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
392        }
393        unsafe { block_record(new_ptr, size, ptr::null(), 0) };
394    } else if !ptr.is_null() {
395        // realloc failure: the old block is still alive.
396        unsafe { block_record(ptr, block_forget(ptr), ptr::null(), 0) };
397    }
398    new_ptr
399}
400
401/// Free allocated memory.
402///
403/// # UPSTREAM-PARITY
404///
405/// ```c
406/// void xmlFree(void *ptr);
407/// ```
408///
409/// Frees memory previously allocated with `xmlMalloc`, `xmlMallocAtomic`,
410/// or `xmlRealloc`. If `ptr` is NULL, no operation is performed.
411///
412/// # SAFETY
413///
414/// - `ptr` must be a valid pointer from `xmlMalloc`/`xmlMallocAtomic`/`xmlRealloc`,
415///   or NULL
416/// - After this call, `ptr` must not be dereferenced
417#[no_mangle]
418pub unsafe extern "C" fn xmlFree(ptr: *mut c_void) {
419    if ptr.is_null() {
420        return;
421    }
422    // SAFETY: We call the stored free function pointer.
423    let alloc = ALLOCATOR.read();
424    let free_func = alloc.free_func.unwrap_or(default_free as xmlFreeFunc);
425    let old_size = unsafe { block_forget(ptr) };
426    unsafe { free_func(ptr) };
427    MEM_BLOCKS.fetch_sub(1, Ordering::Relaxed);
428    if old_size > 0 {
429        MEM_USED.fetch_sub(old_size, Ordering::Relaxed);
430    }
431}
432
433/// Duplicate a C string using the configured allocator.
434///
435/// # UPSTREAM-PARITY
436///
437/// ```c
438/// void *xmlMemStrdup(const char *str);
439/// ```
440///
441/// Returns a pointer to the newly allocated copy, or NULL on failure.
442///
443/// # SAFETY
444///
445/// - `str` must be a valid null-terminated C string or NULL
446/// - The returned pointer must be freed with `xmlFree`
447#[no_mangle]
448pub unsafe extern "C" fn xmlMemStrdup(str: *const c_char) -> *mut c_void {
449    if str.is_null() {
450        return ptr::null_mut();
451    }
452    // SAFETY: We call the stored strdup function pointer.
453    let alloc = ALLOCATOR.read();
454    let strdup_func = alloc.strdup_func.unwrap_or(default_strdup as xmlStrdupFunc);
455    let ptr = unsafe { strdup_func(str) };
456    if !ptr.is_null() {
457        let len = unsafe { libc::strlen(str) } + 1;
458        MEM_USED.fetch_add(len, Ordering::Relaxed);
459        MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
460        unsafe { block_record(ptr, len, ptr::null(), 0) };
461    }
462    ptr
463}
464
465// ═══════════════════════════════════════════════════════════════════════════════
466// Memory Debugging / Statistics
467// ═══════════════════════════════════════════════════════════════════════════════
468
469/// Return the total amount of memory currently allocated (approximate).
470///
471/// # UPSTREAM-PARITY
472///
473/// ```c
474/// int xmlMemUsed(void);
475/// ```
476///
477/// Returns an approximate count of allocated bytes.
478/// With the default allocator, this tracks allocations but is not
479/// byte-exact for realloc (since we don't track old sizes).
480/// Custom allocator hooks can provide exact counts.
481#[no_mangle]
482pub extern "C" fn xmlMemUsed() -> c_int {
483    MEM_USED.load(Ordering::Relaxed) as c_int
484}
485
486/// Return the current number of allocated blocks (approximate).
487///
488/// # UPSTREAM-PARITY
489///
490/// ```c
491/// int xmlMemBlocks(void);
492/// ```
493///
494/// Returns an approximate count of live allocations.
495#[no_mangle]
496pub extern "C" fn xmlMemBlocks() -> c_int {
497    MEM_BLOCKS.load(Ordering::Relaxed) as c_int
498}
499
500/// Display memory allocation information to a file.
501///
502/// # UPSTREAM-PARITY
503///
504/// ```c
505/// void xmlMemDisplay(FILE *fp);
506/// ```
507///
508/// Prints debug memory information. With the default allocator,
509/// this prints a summary message. Custom allocator hooks may
510/// provide more detailed output.
511///
512/// # SAFETY
513///
514/// - `fp` must be a valid FILE* pointer or NULL (in which case stderr is used)
515#[no_mangle]
516pub unsafe extern "C" fn xmlMemDisplay(fp: *mut c_void) {
517    // SAFETY: Caller guarantees fp is valid FILE* or NULL.
518    unsafe {
519        let out = if fp.is_null() {
520            libc::fdopen(2, b"w\0" as *const u8 as *const c_char) as *mut c_void
521        } else {
522            fp
523        };
524        libc::fprintf(
525            out as *mut _,
526            b"Memory: used=%d blocks=%d\n\0" as *const u8 as *const c_char,
527            xmlMemUsed(),
528            xmlMemBlocks(),
529        );
530    }
531}
532
533/// Show memory allocation information.
534///
535/// # UPSTREAM-PARITY
536///
537/// ```c
538/// void xmlMemShow(FILE *fp, int nr);
539/// ```
540///
541/// Prints debug memory information for the last `nr` allocations.
542/// With the default allocator, this is a no-op (we don't track allocation history).
543#[no_mangle]
544pub unsafe extern "C" fn xmlMemShow(fp: *mut c_void, nr: c_int) {
545    // Upstream xmlMemShow(fp, nr) prints the nr most recently allocated
546    // blocks from the debug allocator's history. The candidate's block
547    // registry is unordered; print the live blocks (bounded by nr), which
548    // preserves the observable purpose (per-block debugging output).
549    unsafe {
550        let out = if fp.is_null() {
551            libc::fdopen(2, b"w\0" as *const u8 as *const c_char) as *mut c_void
552        } else {
553            fp
554        };
555        if out.is_null() {
556            return;
557        }
558        let mut msg = String::from("Recent blocks\n");
559        let map = BLOCKS.lock();
560        let mut entries: Vec<(usize, &BlockMeta)> = map.iter().map(|(k, v)| (*k, v)).collect();
561        entries.sort_by_key(|(k, _)| *k);
562        let mut shown = 0;
563        for (addr, meta) in entries {
564            if nr > 0 && shown >= nr {
565                break;
566            }
567            msg.push_str(&format!(
568                "  {:018p} : {:>7} bytes\n",
569                addr as *const c_void, meta.size
570            ));
571            shown += 1;
572        }
573        let bytes = msg.as_bytes();
574        libc::fwrite(
575            bytes.as_ptr() as *const c_void,
576            1,
577            bytes.len(),
578            out as *mut libc::FILE,
579        );
580    }
581}
582
583// ═══════════════════════════════════════════════════════════════════════════════
584// Convenience Functions (used internally)
585// ═══════════════════════════════════════════════════════════════════════════════
586
587/// Allocate zero-initialized memory.
588///
589/// # UPSTREAM-PARITY
590///
591/// This is like `xmlMalloc` followed by `memset(0)`, but some allocator
592/// hooks provide it directly.
593///
594/// # SAFETY
595///
596/// Same as `xmlMalloc`. The returned memory is zero-initialized.
597#[no_mangle]
598pub unsafe extern "C" fn xmlMallocZero(size: usize) -> *mut c_void {
599    // SAFETY: Delegates to xmlMalloc and zeroes the memory.
600    let ptr = unsafe { xmlMalloc(size) };
601    if !ptr.is_null() {
602        unsafe { ptr::write_bytes(ptr, 0, size) };
603    }
604    ptr
605}
606
607/// Allocate zero-initialized memory (atomic variant).
608///
609/// # UPSTREAM-PARITY
610///
611/// Like `xmlMallocAtomic` followed by zero-initialization.
612#[no_mangle]
613pub unsafe extern "C" fn xmlMallocAtomicZero(size: usize) -> *mut c_void {
614    // SAFETY: Delegates to xmlMallocAtomic and zeroes the memory.
615    let ptr = unsafe { xmlMallocAtomic(size) };
616    if !ptr.is_null() {
617        unsafe { ptr::write_bytes(ptr, 0, size) };
618    }
619    ptr
620}
621
622/// Reallocate and zero-initialize the new portion.
623///
624/// # UPSTREAM-PARITY
625///
626/// Like `xmlRealloc`, but zeroes any newly allocated bytes.
627///
628/// # SAFETY
629///
630/// Same as `xmlRealloc`.
631#[no_mangle]
632pub unsafe extern "C" fn xmlReallocZero(
633    ptr: *mut c_void,
634    old_size: usize,
635    new_size: usize,
636) -> *mut c_void {
637    // SAFETY: Delegates to xmlRealloc and zeroes the new portion.
638    let new_ptr = unsafe { xmlRealloc(ptr, new_size) };
639    if !new_ptr.is_null() && new_size > old_size {
640        unsafe {
641            ptr::write_bytes(new_ptr.add(old_size), 0, new_size - old_size);
642        }
643    }
644    new_ptr
645}
646
647// ═══════════════════════════════════════════════════════════════════════════════
648// Debug Allocator (Optional)
649// ═══════════════════════════════════════════════════════════════════════════════
650
651/// Initialize the memory layer with debugging support.
652///
653/// # UPSTREAM-PARITY
654///
655/// ```c
656/// int xmlInitMemory(void);
657/// ```
658///
659/// Initializes the memory subsystem. Returns 0 on success.
660/// This is called automatically by `xmlInitParser`.
661#[no_mangle]
662pub extern "C" fn xmlInitMemory() -> c_int {
663    0
664}
665
666/// Clean up the memory layer.
667///
668/// # UPSTREAM-PARITY
669///
670/// ```c
671/// void xmlCleanupMemory(void);
672/// ```
673#[no_mangle]
674pub extern "C" fn xmlCleanupMemory() {
675    // Phase 1: no cleanup needed for the default allocator.
676}
677
678// ═══════════════════════════════════════════════════════════════════════════════
679// Legacy-named allocator API (upstream xmlmemory.h)
680// ═══════════════════════════════════════════════════════════════════════════════
681// Upstream xmlmemory.h historically exported `xmlMemMalloc`, `xmlMemFree`,
682// `xmlMemRealloc`, `xmlMemoryStrdup`, the `*Loc` location-tracking variants,
683// `xmlMemSize`, `xmlMemDisplayLast` and `xmlMemoryDump` alongside the modern
684// names. Downstream code (older consumers, some language bindings) links
685// against these names, so the candidate exports them with identical
686// semantics. The `*Loc` variants take a source file/line that the default
687// allocator does not track (upstream uses it for leak reports only); the
688// location arguments are accepted and ignored — a documented safe divergence
689// (residual R-000131).
690
691/// Allocate memory (legacy name; same contract as `xmlMalloc`).
692///
693/// ```c
694/// void *xmlMemMalloc(size_t size);
695/// ```
696#[no_mangle]
697pub unsafe extern "C" fn xmlMemMalloc(size: usize) -> *mut c_void {
698    // SAFETY: identical contract to xmlMalloc.
699    unsafe { xmlMalloc(size) }
700}
701
702/// Free memory (legacy name; same contract as `xmlFree`).
703///
704/// ```c
705/// void xmlMemFree(void *ptr);
706/// ```
707#[no_mangle]
708pub unsafe extern "C" fn xmlMemFree(ptr: *mut c_void) {
709    // SAFETY: identical contract to xmlFree.
710    unsafe { xmlFree(ptr) }
711}
712
713/// Reallocate memory (legacy name; same contract as `xmlRealloc`).
714///
715/// ```c
716/// void *xmlMemRealloc(void *ptr, size_t size);
717/// ```
718#[no_mangle]
719pub unsafe extern "C" fn xmlMemRealloc(ptr: *mut c_void, size: usize) -> *mut c_void {
720    // SAFETY: identical contract to xmlRealloc.
721    unsafe { xmlRealloc(ptr, size) }
722}
723
724/// Duplicate a string (legacy name; same contract as `xmlMemStrdup`).
725///
726/// ```c
727/// void *xmlMemoryStrdup(const char *str);
728/// ```
729#[no_mangle]
730pub unsafe extern "C" fn xmlMemoryStrdup(str: *const c_char) -> *mut c_void {
731    // SAFETY: identical contract to xmlMemStrdup.
732    unsafe { xmlMemStrdup(str) }
733}
734
735/// Allocate memory, recording the allocation site (upstream xmlmemory.h).
736///
737/// ```c
738/// void *xmlMallocLoc(size_t size, const char *file, int line);
739/// ```
740///
741/// The default candidate allocator does not track allocation sites (see
742/// residual R-000131); the location arguments are accepted for ABI
743/// compatibility and ignored.
744#[no_mangle]
745pub unsafe extern "C" fn xmlMallocLoc(
746    size: usize,
747    file: *const c_char,
748    line: c_int,
749) -> *mut c_void {
750    let ptr = unsafe { xmlMalloc(size) };
751    if !ptr.is_null() {
752        unsafe { block_record(ptr, size, file, line) };
753    }
754    ptr
755}
756
757/// Allocate zeroed memory, recording the allocation site (upstream xmlmemory.h).
758///
759/// ```c
760/// void *xmlMallocAtomicLoc(size_t size, const char *file, int line);
761/// ```
762#[no_mangle]
763pub unsafe extern "C" fn xmlMallocAtomicLoc(
764    size: usize,
765    file: *const c_char,
766    line: c_int,
767) -> *mut c_void {
768    let ptr = unsafe { xmlMallocZero(size) };
769    if !ptr.is_null() {
770        unsafe { block_record(ptr, size, file, line) };
771    }
772    ptr
773}
774
775/// Reallocate memory, recording the allocation site (upstream xmlmemory.h).
776///
777/// ```c
778/// void *xmlReallocLoc(void *ptr, size_t size, const char *file, int line);
779/// ```
780#[no_mangle]
781pub unsafe extern "C" fn xmlReallocLoc(
782    ptr: *mut c_void,
783    size: usize,
784    file: *const c_char,
785    line: c_int,
786) -> *mut c_void {
787    let new_ptr = unsafe { xmlRealloc(ptr, size) };
788    if !new_ptr.is_null() {
789        unsafe { block_record(new_ptr, size, file, line) };
790    } else if !ptr.is_null() {
791        // Keep the old site on failure.
792        let meta = BLOCKS.lock().get(&(ptr as usize)).copied();
793        if let Some(m) = meta {
794            unsafe { block_record(ptr, m.size, m.file as *const c_char, m.line) };
795        }
796    }
797    new_ptr
798}
799
800/// Duplicate a string, recording the allocation site (upstream xmlmemory.h).
801///
802/// ```c
803/// void *xmlMemStrdupLoc(const char *str, const char *file, int line);
804/// ```
805#[no_mangle]
806pub unsafe extern "C" fn xmlMemStrdupLoc(
807    str: *const c_char,
808    file: *const c_char,
809    line: c_int,
810) -> *mut c_void {
811    let ptr = unsafe { xmlMemStrdup(str) };
812    if !ptr.is_null() {
813        let len = unsafe { libc::strlen(str) } + 1;
814        unsafe { block_record(ptr, len, file, line) };
815    }
816    ptr
817}
818
819/// Return the size of an allocated block (upstream xmlmemory.h).
820///
821/// ```c
822/// size_t xmlMemSize(void *ptr);
823/// ```
824///
825/// Returns the recorded size from the block registry (0 for unknown or
826/// foreign pointers, matching upstream's lookup-miss behavior).
827#[no_mangle]
828pub unsafe extern "C" fn xmlMemSize(ptr: *mut c_void) -> usize {
829    if ptr.is_null() {
830        return 0;
831    }
832    BLOCKS
833        .lock()
834        .get(&(ptr as usize))
835        .map(|m| m.size)
836        .unwrap_or(0)
837}
838
839/// Display a limited amount of memory debug information (upstream xmlmemory.h).
840///
841/// ```c
842/// void xmlMemDisplayLast(FILE *fp, long nbBytes);
843/// ```
844///
845/// Dumps the per-block registry (upstream xmlMemDisplayLast block listing):
846/// one line per live block with its address, size and recorded allocation
847/// site, bounded by `nb_bytes` when positive. The aggregate footer matches
848/// upstream's counters.
849#[no_mangle]
850pub unsafe extern "C" fn xmlMemDisplayLast(fp: *mut c_void, nb_bytes: c_long) {
851    // SAFETY: fp must be a valid FILE* or NULL (stderr used).
852    unsafe {
853        let out = if fp.is_null() {
854            libc::fdopen(2, b"w\0" as *const u8 as *const c_char) as *mut c_void
855        } else {
856            fp
857        };
858        if out.is_null() {
859            return;
860        }
861        let mut total: usize = 0;
862        let mut msg = String::new();
863        msg.push_str("MEMORY ALLOCATED : 0, MAX : 0, BLOCKS : ");
864        let blocks = MEM_BLOCKS.load(Ordering::Relaxed);
865        msg.push_str(&blocks.to_string());
866        msg.push('\n');
867        let map = BLOCKS.lock();
868        let mut entries: Vec<(usize, &BlockMeta)> = map.iter().map(|(k, v)| (*k, v)).collect();
869        entries.sort_by_key(|(k, _)| *k);
870        for (addr, meta) in entries {
871            if nb_bytes > 0 && (total as c_long) >= nb_bytes {
872                break;
873            }
874            total += meta.size;
875            msg.push_str(&format!(
876                "  {:018p} : {:>7} bytes",
877                addr as *const c_void, meta.size
878            ));
879            if meta.file != 0 {
880                let file = CStr::from_ptr(meta.file as *const c_char).to_string_lossy();
881                msg.push_str(&format!(" @ {}:{}", file, meta.line));
882            }
883            msg.push('\n');
884        }
885        drop(map);
886        let used = MEM_USED.load(Ordering::Relaxed);
887        msg.push_str(&format!(
888            "TOTAL MEMORY ALLOCATED : {} bytes, TOTAL BLOCKS : {}\n",
889            used, blocks
890        ));
891        let bytes = msg.as_bytes();
892        libc::fwrite(
893            bytes.as_ptr() as *const c_void,
894            1,
895            bytes.len(),
896            out as *mut libc::FILE,
897        );
898    }
899}
900
901/// Dump memory allocation statistics (upstream xmlmemory.h).
902///
903/// ```c
904/// int xmlMemoryDump(void);
905/// ```
906///
907/// Prints the global counters to stderr and returns 0 (no leak detector is
908/// active in the default allocator).
909#[no_mangle]
910pub unsafe extern "C" fn xmlMemoryDump() -> c_int {
911    unsafe {
912        xmlMemDisplayLast(ptr::null_mut(), -1);
913    }
914    0
915}
916
917// ═══════════════════════════════════════════════════════════════════════════════
918// Tests
919// ═══════════════════════════════════════════════════════════════════════════════
920
921#[cfg(test)]
922mod tests {
923    use super::*;
924    use core::ptr;
925
926    /// Test basic allocation and deallocation.
927    #[test]
928    fn test_malloc_free() {
929        unsafe {
930            let ptr = xmlMalloc(100);
931            assert!(!ptr.is_null(), "xmlMalloc(100) returned NULL");
932            xmlFree(ptr);
933        }
934    }
935
936    /// Test that xmlMalloc(0) returns a valid pointer.
937    #[test]
938    fn test_malloc_zero() {
939        unsafe {
940            let ptr = xmlMalloc(0);
941            // malloc(0) may return NULL or a valid pointer.
942            // If non-NULL, it must be freeable.
943            if !ptr.is_null() {
944                xmlFree(ptr);
945            }
946        }
947    }
948
949    /// Test xmlFree(NULL) is a no-op.
950    #[test]
951    fn test_free_null() {
952        unsafe {
953            xmlFree(ptr::null_mut());
954            // Should not crash
955        }
956    }
957
958    /// Test xmlRealloc.
959    #[test]
960    fn test_realloc() {
961        unsafe {
962            let ptr = xmlMalloc(50);
963            assert!(!ptr.is_null());
964            let new_ptr = xmlRealloc(ptr, 100);
965            assert!(!new_ptr.is_null());
966            xmlFree(new_ptr);
967        }
968    }
969
970    /// Test xmlMemStrdup.
971    #[test]
972    fn test_mem_strdup() {
973        unsafe {
974            let s = b"hello\0" as *const u8 as *const c_char;
975            let dup = xmlMemStrdup(s);
976            assert!(!dup.is_null());
977            // Compare the strings
978            let orig_slice = std::slice::from_raw_parts(s as *const u8, 6);
979            let dup_slice = std::slice::from_raw_parts(dup as *const u8, 6);
980            assert_eq!(orig_slice, dup_slice);
981            xmlFree(dup);
982        }
983    }
984
985    /// Test xmlMallocZero returns zeroed memory.
986    #[test]
987    fn test_malloc_zero_init() {
988        unsafe {
989            let ptr = xmlMallocZero(100) as *mut u8;
990            assert!(!ptr.is_null());
991            let slice = std::slice::from_raw_parts(ptr, 100);
992            assert!(slice.iter().all(|&b| b == 0));
993            xmlFree(ptr as *mut c_void);
994        }
995    }
996
997    /// Test custom allocator setup/get.
998    #[test]
999    fn test_mem_setup_get() {
1000        unsafe {
1001            let mut free_func: Option<xmlFreeFunc> = None;
1002            let mut malloc_func: Option<xmlMallocFunc> = None;
1003            let mut realloc_func: Option<xmlReallocFunc> = None;
1004            let mut strdup_func: Option<xmlStrdupFunc> = None;
1005
1006            xmlMemGet(
1007                &mut free_func as *mut _,
1008                &mut malloc_func as *mut _,
1009                &mut realloc_func as *mut _,
1010                &mut strdup_func as *mut _,
1011            );
1012
1013            assert!(malloc_func.is_some());
1014            assert!(free_func.is_some());
1015            assert!(realloc_func.is_some());
1016            assert!(strdup_func.is_some());
1017        }
1018    }
1019
1020    /// Test xmlMemUsed and xmlMemBlocks return reasonable values.
1021    #[test]
1022    fn test_mem_stats() {
1023        unsafe {
1024            let ptr = xmlMalloc(100);
1025            assert!(!ptr.is_null());
1026
1027            // The block registry records the exact size (deterministic,
1028            // unlike the process-wide counters which other test threads
1029            // mutate concurrently).
1030            assert_eq!(xmlMemSize(ptr), 100);
1031
1032            xmlFree(ptr);
1033            // Freed blocks leave the registry.
1034            assert_eq!(xmlMemSize(ptr), 0);
1035        }
1036    }
1037}