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///
285/// # SAFETY
286///
287///
288/// - `freeFunc`, `mallocFunc`, `reallocFunc`, `strdupFunc` must be a valid callback (or None);
289///   the callback is invoked with the documented context pointer and
290///   must itself uphold the same pointer invariants.
291///
292/// The caller must not race this call with concurrent mutation of the
293/// same objects from other threads (per-object state is not internally
294/// synchronized). Violating any of the above is undefined behavior.
295///
296/// Exercised by the C-API differential courts
297/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
298/// courts; those pass byte-for-byte against the upstream oracle.
299#[no_mangle]
300pub unsafe extern "C" fn xmlGcMemSetup(
301    freeFunc: Option<xmlFreeFunc>,
302    mallocFunc: Option<xmlMallocFunc>,
303    reallocFunc: Option<xmlReallocFunc>,
304    strdupFunc: Option<xmlStrdupFunc>,
305) {
306    // SAFETY: Delegates to xmlMemSetup with the same safety contract.
307    unsafe { xmlMemSetup(freeFunc, mallocFunc, reallocFunc, strdupFunc) };
308}
309
310/// Get GC-aware memory allocator functions.
311///
312/// # UPSTREAM-PARITY
313///
314/// Wrapper around `xmlMemGet`.
315///
316/// # SAFETY
317///
318///
319/// - `freeFunc`, `mallocFunc`, `reallocFunc`, `strdupFunc` must be a valid callback (or None);
320///   the callback is invoked with the documented context pointer and
321///   must itself uphold the same pointer invariants.
322///
323/// The caller must not race this call with concurrent mutation of the
324/// same objects from other threads (per-object state is not internally
325/// synchronized). Violating any of the above is undefined behavior.
326///
327/// Exercised by the C-API differential courts
328/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
329/// courts; those pass byte-for-byte against the upstream oracle.
330#[no_mangle]
331pub unsafe extern "C" fn xmlGcMemGet(
332    freeFunc: *mut Option<xmlFreeFunc>,
333    mallocFunc: *mut Option<xmlMallocFunc>,
334    reallocFunc: *mut Option<xmlReallocFunc>,
335    strdupFunc: *mut Option<xmlStrdupFunc>,
336) {
337    // SAFETY: Delegates to xmlMemGet with the same safety contract.
338    unsafe { xmlMemGet(freeFunc, mallocFunc, reallocFunc, strdupFunc) };
339}
340
341// ═══════════════════════════════════════════════════════════════════════════════
342// Allocation Functions
343// ═══════════════════════════════════════════════════════════════════════════════
344
345/// Allocate memory.
346///
347/// # UPSTREAM-PARITY
348///
349/// ```c
350/// void *xmlMalloc(size_t size);
351/// ```
352///
353/// Returns a pointer to the allocated memory, or NULL on failure.
354/// The allocated memory is not initialized (like malloc).
355///
356/// # SAFETY
357///
358/// - The returned pointer must be freed with `xmlFree`
359/// - `size` may be 0 (returns a valid non-NULL pointer or NULL)
360///
361/// This is the implementation backing the exported `xmlMalloc` data global
362/// (upstream xmlmemory.h: `XMLPUBVAR xmlMallocFunc xmlMalloc;` — a function-
363/// pointer variable, not a function).
364pub unsafe extern "C" fn xmlMallocImpl(size: usize) -> *mut c_void {
365    // SAFETY: We call the stored malloc function pointer.
366    // The function pointer must be valid (set by xmlMemSetup or default).
367    let alloc = ALLOCATOR.read();
368    let malloc_func = alloc.malloc_func.unwrap_or(default_malloc as xmlMallocFunc);
369    let ptr = unsafe { malloc_func(size) };
370    if !ptr.is_null() {
371        MEM_USED.fetch_add(size, Ordering::Relaxed);
372        MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
373        unsafe { block_record(ptr, size, ptr::null(), 0) };
374    }
375    ptr
376}
377
378/// Allocate memory that will never contain pointers to other memory.
379///
380/// # UPSTREAM-PARITY
381///
382/// ```c
383/// void *xmlMallocAtomic(size_t size);
384/// ```
385///
386/// Identical to `xmlMalloc` but hints to the GC that the memory does not
387/// contain pointers. In modern libxml2, this is equivalent to `xmlMalloc`.
388/// Backing the exported `xmlMallocAtomic` data global.
389///
390/// # SAFETY
391///
392/// The function touches crate-global state only; it is safe
393/// as long as the caller respects the library's global
394/// initialization/cleanup ordering (xmlInitParser before use,
395/// xmlCleanupParser only after all users are done).
396///
397/// Violating the global lifecycle ordering, or calling this after
398/// teardown or from a signal handler, is undefined behavior.
399pub unsafe extern "C" fn xmlMallocAtomicImpl(size: usize) -> *mut c_void {
400    // SAFETY: Same as xmlMalloc.
401    unsafe { xmlMallocImpl(size) }
402}
403
404/// Reallocate memory.
405///
406/// # UPSTREAM-PARITY
407///
408/// ```c
409/// void *xmlRealloc(void *ptr, size_t size);
410/// ```
411///
412/// Changes the size of the memory block pointed to by `ptr`.
413/// If `ptr` is NULL, behaves like `xmlMalloc`.
414/// If `size` is 0, may return NULL (like C realloc).
415///
416/// # SAFETY
417///
418/// - `ptr` must be a valid pointer from `xmlMalloc`, `xmlMallocAtomic`, or `xmlRealloc`,
419///   or NULL
420/// - The returned pointer must be freed with `xmlFree`
421///
422/// Backing the exported `xmlRealloc` data global.
423pub unsafe extern "C" fn xmlReallocImpl(ptr: *mut c_void, size: usize) -> *mut c_void {
424    // SAFETY: We call the stored realloc function pointer.
425    let alloc = ALLOCATOR.read();
426    let realloc_func = alloc
427        .realloc_func
428        .unwrap_or(default_realloc as xmlReallocFunc);
429    let new_ptr = unsafe { realloc_func(ptr, size) };
430    if !new_ptr.is_null() {
431        // Exact accounting via the block registry.
432        let old_size = unsafe { block_forget(ptr) };
433        MEM_USED.fetch_add(size.saturating_sub(old_size), Ordering::Relaxed);
434        if ptr.is_null() {
435            MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
436        }
437        unsafe { block_record(new_ptr, size, ptr::null(), 0) };
438    } else if !ptr.is_null() {
439        // realloc failure: the old block is still alive.
440        unsafe { block_record(ptr, block_forget(ptr), ptr::null(), 0) };
441    }
442    new_ptr
443}
444
445/// Free allocated memory.
446///
447/// # UPSTREAM-PARITY
448///
449/// ```c
450/// void xmlFree(void *ptr);
451/// ```
452///
453/// Frees memory previously allocated with `xmlMalloc`, `xmlMallocAtomic`,
454/// or `xmlRealloc`. If `ptr` is NULL, no operation is performed.
455///
456/// # SAFETY
457///
458/// - `ptr` must be a valid pointer from `xmlMalloc`/`xmlMallocAtomic`/`xmlRealloc`,
459///   or NULL
460/// - After this call, `ptr` must not be dereferenced
461///
462/// Backing the exported `xmlFree` data global.
463pub unsafe extern "C" fn xmlFreeImpl(ptr: *mut c_void) {
464    if ptr.is_null() {
465        return;
466    }
467    // SAFETY: We call the stored free function pointer.
468    let alloc = ALLOCATOR.read();
469    let free_func = alloc.free_func.unwrap_or(default_free as xmlFreeFunc);
470    let old_size = unsafe { block_forget(ptr) };
471    unsafe { free_func(ptr) };
472    MEM_BLOCKS.fetch_sub(1, Ordering::Relaxed);
473    if old_size > 0 {
474        MEM_USED.fetch_sub(old_size, Ordering::Relaxed);
475    }
476}
477
478/// Duplicate a C string using the configured allocator.
479///
480/// # UPSTREAM-PARITY
481///
482/// ```c
483/// void *xmlMemStrdup(const char *str);
484/// ```
485///
486/// Returns a pointer to the newly allocated copy, or NULL on failure.
487///
488/// # SAFETY
489///
490/// - `str` must be a valid null-terminated C string or NULL
491/// - The returned pointer must be freed with `xmlFree`
492///
493/// Backing the exported `xmlMemStrdup` data global.
494pub unsafe extern "C" fn xmlMemStrdupImpl(str: *const c_char) -> *mut c_void {
495    if str.is_null() {
496        return ptr::null_mut();
497    }
498    // SAFETY: We call the stored strdup function pointer.
499    let alloc = ALLOCATOR.read();
500    let strdup_func = alloc.strdup_func.unwrap_or(default_strdup as xmlStrdupFunc);
501    let ptr = unsafe { strdup_func(str) };
502    if !ptr.is_null() {
503        let len = unsafe { libc::strlen(str) } + 1;
504        MEM_USED.fetch_add(len, Ordering::Relaxed);
505        MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
506        unsafe { block_record(ptr, len, ptr::null(), 0) };
507    }
508    ptr
509}
510
511// ═══════════════════════════════════════════════════════════════════════════════
512// Exported allocator globals (upstream xmlmemory.h)
513// ═══════════════════════════════════════════════════════════════════════════════
514//
515// Upstream exports the allocator entry points as DATA: `XMLPUBVAR
516// xmlMallocFunc xmlMalloc;` etc. — function-pointer variables that downstream
517// code can read AND assign (the documented allocator-override mechanism).
518// The candidate mirrors that ABI; the implementations above back them.
519
520/// `xmlMallocFunc xmlMalloc` — the malloc hook (default: `xmlMallocImpl`).
521#[no_mangle]
522pub static mut xmlMalloc: xmlMallocFunc = xmlMallocImpl;
523
524/// `xmlMallocFunc xmlMallocAtomic` — the atomic-malloc hook.
525#[no_mangle]
526pub static mut xmlMallocAtomic: xmlMallocFunc = xmlMallocAtomicImpl;
527
528/// `xmlReallocFunc xmlRealloc` — the realloc hook.
529#[no_mangle]
530pub static mut xmlRealloc: xmlReallocFunc = xmlReallocImpl;
531
532/// `xmlFreeFunc xmlFree` — the free hook.
533#[no_mangle]
534pub static mut xmlFree: xmlFreeFunc = xmlFreeImpl;
535
536/// `xmlStrdupFunc xmlMemStrdup` — the strdup hook.
537#[no_mangle]
538pub static mut xmlMemStrdup: xmlStrdupFunc = xmlMemStrdupImpl;
539
540// ═══════════════════════════════════════════════════════════════════════════════
541// Memory Debugging / Statistics
542// ═══════════════════════════════════════════════════════════════════════════════
543
544/// Return the total amount of memory currently allocated (approximate).
545///
546/// # UPSTREAM-PARITY
547///
548/// ```c
549/// int xmlMemUsed(void);
550/// ```
551///
552/// Returns an approximate count of allocated bytes.
553/// With the default allocator, this tracks allocations but is not
554/// byte-exact for realloc (since we don't track old sizes).
555/// Custom allocator hooks can provide exact counts.
556#[no_mangle]
557pub extern "C" fn xmlMemUsed() -> c_int {
558    MEM_USED.load(Ordering::Relaxed) as c_int
559}
560
561/// Return the current number of allocated blocks (approximate).
562///
563/// # UPSTREAM-PARITY
564///
565/// ```c
566/// int xmlMemBlocks(void);
567/// ```
568///
569/// Returns an approximate count of live allocations.
570#[no_mangle]
571pub extern "C" fn xmlMemBlocks() -> c_int {
572    MEM_BLOCKS.load(Ordering::Relaxed) as c_int
573}
574
575/// Display memory allocation information to a file.
576///
577/// # UPSTREAM-PARITY
578///
579/// ```c
580/// void xmlMemDisplay(FILE *fp);
581/// ```
582///
583/// Prints debug memory information. With the default allocator,
584/// this prints a summary message. Custom allocator hooks may
585/// provide more detailed output.
586///
587/// # SAFETY
588///
589/// - `fp` must be a valid FILE* pointer or NULL (in which case stderr is used)
590#[no_mangle]
591pub unsafe extern "C" fn xmlMemDisplay(fp: *mut c_void) {
592    // SAFETY: Caller guarantees fp is valid FILE* or NULL.
593    unsafe {
594        let out = if fp.is_null() {
595            libc::fdopen(2, b"w\0" as *const u8 as *const c_char) as *mut c_void
596        } else {
597            fp
598        };
599        libc::fprintf(
600            out as *mut _,
601            b"Memory: used=%d blocks=%d\n\0" as *const u8 as *const c_char,
602            xmlMemUsed(),
603            xmlMemBlocks(),
604        );
605    }
606}
607
608/// Show memory allocation information.
609///
610/// # UPSTREAM-PARITY
611///
612/// ```c
613/// void xmlMemShow(FILE *fp, int nr);
614/// ```
615///
616/// Prints debug memory information for the last `nr` allocations.
617/// With the default allocator, this is a no-op (we don't track allocation history).
618///
619/// # SAFETY
620///
621/// - `fp` must be valid pointers (or NULL
622///   where the upstream C contract allows), obtained from the
623///   matching constructor/owner and not yet freed; the callee may
624///   take or keep ownership exactly as the C API specifies.
625///
626/// The caller must not race this call with concurrent mutation of the
627/// same objects from other threads (per-object state is not internally
628/// synchronized). Violating any of the above is undefined behavior.
629///
630/// Exercised by the C-API differential courts
631/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
632/// courts; those pass byte-for-byte against the upstream oracle.
633#[no_mangle]
634pub unsafe extern "C" fn xmlMemShow(fp: *mut c_void, nr: c_int) {
635    // Upstream xmlMemShow(fp, nr) prints the nr most recently allocated
636    // blocks from the debug allocator's history. The candidate's block
637    // registry is unordered; print the live blocks (bounded by nr), which
638    // preserves the observable purpose (per-block debugging output).
639    unsafe {
640        let out = if fp.is_null() {
641            libc::fdopen(2, b"w\0" as *const u8 as *const c_char) as *mut c_void
642        } else {
643            fp
644        };
645        if out.is_null() {
646            return;
647        }
648        let mut msg = String::from("Recent blocks\n");
649        let map = BLOCKS.lock();
650        let mut entries: Vec<(usize, &BlockMeta)> = map.iter().map(|(k, v)| (*k, v)).collect();
651        entries.sort_by_key(|(k, _)| *k);
652        let take = if nr > 0 { nr as usize } else { usize::MAX };
653        for (addr, meta) in entries.into_iter().take(take) {
654            msg.push_str(&format!(
655                "  {:018p} : {:>7} bytes\n",
656                addr as *const c_void, meta.size
657            ));
658        }
659        let bytes = msg.as_bytes();
660        libc::fwrite(
661            bytes.as_ptr() as *const c_void,
662            1,
663            bytes.len(),
664            out as *mut libc::FILE,
665        );
666    }
667}
668
669// ═══════════════════════════════════════════════════════════════════════════════
670// Convenience Functions (used internally)
671// ═══════════════════════════════════════════════════════════════════════════════
672
673/// Allocate zero-initialized memory.
674///
675/// # UPSTREAM-PARITY
676///
677/// This is like `xmlMalloc` followed by `memset(0)`, but some allocator
678/// hooks provide it directly.
679///
680/// # SAFETY
681///
682/// Same as `xmlMalloc`. The returned memory is zero-initialized.
683#[no_mangle]
684pub unsafe extern "C" fn xmlMallocZero(size: usize) -> *mut c_void {
685    // SAFETY: Delegates to xmlMallocImpl and zeroes the memory.
686    let ptr = unsafe { xmlMallocImpl(size) };
687    if !ptr.is_null() {
688        unsafe { ptr::write_bytes(ptr, 0, size) };
689    }
690    ptr
691}
692
693/// Allocate zero-initialized memory (atomic variant).
694///
695/// # UPSTREAM-PARITY
696///
697/// Like `xmlMallocAtomic` followed by zero-initialization.
698///
699/// # SAFETY
700///
701/// The function touches crate-global state only; it is safe
702/// as long as the caller respects the library's global
703/// initialization/cleanup ordering (xmlInitParser before use,
704/// xmlCleanupParser only after all users are done).
705///
706/// Violating the global lifecycle ordering, or calling this after
707/// teardown or from a signal handler, is undefined behavior.
708#[no_mangle]
709pub unsafe extern "C" fn xmlMallocAtomicZero(size: usize) -> *mut c_void {
710    // SAFETY: Delegates to xmlMallocAtomicImpl and zeroes the memory.
711    let ptr = unsafe { xmlMallocAtomicImpl(size) };
712    if !ptr.is_null() {
713        unsafe { ptr::write_bytes(ptr, 0, size) };
714    }
715    ptr
716}
717
718/// Reallocate and zero-initialize the new portion.
719///
720/// # UPSTREAM-PARITY
721///
722/// Like `xmlRealloc`, but zeroes any newly allocated bytes.
723///
724/// # SAFETY
725///
726/// Same as `xmlRealloc`.
727#[no_mangle]
728pub unsafe extern "C" fn xmlReallocZero(
729    ptr: *mut c_void,
730    old_size: usize,
731    new_size: usize,
732) -> *mut c_void {
733    // SAFETY: Delegates to xmlRealloc and zeroes the new portion.
734    let new_ptr = unsafe { xmlReallocImpl(ptr, new_size) };
735    if !new_ptr.is_null() && new_size > old_size {
736        unsafe {
737            ptr::write_bytes(new_ptr.add(old_size), 0, new_size - old_size);
738        }
739    }
740    new_ptr
741}
742
743// ═══════════════════════════════════════════════════════════════════════════════
744// Debug Allocator (Optional)
745// ═══════════════════════════════════════════════════════════════════════════════
746
747/// Initialize the memory layer with debugging support.
748///
749/// # UPSTREAM-PARITY
750///
751/// ```c
752/// int xmlInitMemory(void);
753/// ```
754///
755/// Initializes the memory subsystem. Returns 0 on success.
756/// This is called automatically by `xmlInitParser`.
757#[no_mangle]
758pub const extern "C" fn xmlInitMemory() -> c_int {
759    0
760}
761
762/// Clean up the memory layer.
763///
764/// # UPSTREAM-PARITY
765///
766/// ```c
767/// void xmlCleanupMemory(void);
768/// ```
769#[no_mangle]
770pub const extern "C" fn xmlCleanupMemory() {
771    // Phase 1: no cleanup needed for the default allocator.
772}
773
774// ═══════════════════════════════════════════════════════════════════════════════
775// Legacy-named allocator API (upstream xmlmemory.h)
776// ═══════════════════════════════════════════════════════════════════════════════
777// Upstream xmlmemory.h historically exported `xmlMemMalloc`, `xmlMemFree`,
778// `xmlMemRealloc`, `xmlMemoryStrdup`, the `*Loc` location-tracking variants,
779// `xmlMemSize`, `xmlMemDisplayLast` and `xmlMemoryDump` alongside the modern
780// names. Downstream code (older consumers, some language bindings) links
781// against these names, so the candidate exports them with identical
782// semantics. The `*Loc` variants take a source file/line that the default
783// allocator does not track (upstream uses it for leak reports only); the
784// location arguments are accepted and ignored — a documented safe divergence
785// (residual R-000131).
786
787/// Allocate memory (legacy name; same contract as `xmlMalloc`).
788///
789/// ```c
790/// void *xmlMemMalloc(size_t size);
791/// ```
792///
793/// # SAFETY
794///
795/// The function touches crate-global state only; it is safe
796/// as long as the caller respects the library's global
797/// initialization/cleanup ordering (xmlInitParser before use,
798/// xmlCleanupParser only after all users are done).
799///
800/// Violating the global lifecycle ordering, or calling this after
801/// teardown or from a signal handler, is undefined behavior.
802#[no_mangle]
803pub unsafe extern "C" fn xmlMemMalloc(size: usize) -> *mut c_void {
804    // SAFETY: identical contract to xmlMalloc.
805    unsafe { xmlMallocImpl(size) }
806}
807
808/// Free memory (legacy name; same contract as `xmlFree`).
809///
810/// ```c
811/// void xmlMemFree(void *ptr);
812/// ```
813///
814/// # SAFETY
815///
816/// - `ptr` must be valid pointers (or NULL
817///   where the upstream C contract allows), obtained from the
818///   matching constructor/owner and not yet freed; the callee may
819///   take or keep ownership exactly as the C API specifies.
820///
821/// The caller must not race this call with concurrent mutation of the
822/// same objects from other threads (per-object state is not internally
823/// synchronized). Violating any of the above is undefined behavior.
824///
825/// Exercised by the C-API differential courts
826/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
827/// courts; those pass byte-for-byte against the upstream oracle.
828#[no_mangle]
829pub unsafe extern "C" fn xmlMemFree(ptr: *mut c_void) {
830    // SAFETY: identical contract to xmlFree.
831    unsafe { xmlFreeImpl(ptr) }
832}
833
834/// Reallocate memory (legacy name; same contract as `xmlRealloc`).
835///
836/// ```c
837/// void *xmlMemRealloc(void *ptr, size_t size);
838/// ```
839///
840/// # SAFETY
841///
842/// - `ptr` must be valid pointers (or NULL
843///   where the upstream C contract allows), obtained from the
844///   matching constructor/owner and not yet freed; the callee may
845///   take or keep ownership exactly as the C API specifies.
846///
847/// The caller must not race this call with concurrent mutation of the
848/// same objects from other threads (per-object state is not internally
849/// synchronized). Violating any of the above is undefined behavior.
850///
851/// Exercised by the C-API differential courts
852/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
853/// courts; those pass byte-for-byte against the upstream oracle.
854#[no_mangle]
855pub unsafe extern "C" fn xmlMemRealloc(ptr: *mut c_void, size: usize) -> *mut c_void {
856    // SAFETY: identical contract to xmlRealloc.
857    unsafe { xmlReallocImpl(ptr, size) }
858}
859
860/// Duplicate a string (legacy name; same contract as `xmlMemStrdup`).
861///
862/// ```c
863/// void *xmlMemoryStrdup(const char *str);
864/// ```
865///
866/// # SAFETY
867///
868///
869/// - `str` must point to valid NUL-terminated
870///   strings (or NULL where the C contract allows) for the lifetime
871///   of the call.
872///
873/// The caller must not race this call with concurrent mutation of the
874/// same objects from other threads (per-object state is not internally
875/// synchronized). Violating any of the above is undefined behavior.
876///
877/// Exercised by the C-API differential courts
878/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
879/// courts; those pass byte-for-byte against the upstream oracle.
880#[no_mangle]
881pub unsafe extern "C" fn xmlMemoryStrdup(str: *const c_char) -> *mut c_void {
882    // SAFETY: identical contract to xmlMemStrdup.
883    unsafe { xmlMemStrdupImpl(str) }
884}
885
886/// Allocate memory, recording the allocation site (upstream xmlmemory.h).
887///
888/// ```c
889/// void *xmlMallocLoc(size_t size, const char *file, int line);
890/// ```
891///
892/// The default candidate allocator does not track allocation sites (see
893/// residual R-000131); the location arguments are accepted for ABI
894/// compatibility and ignored.
895///
896/// # SAFETY
897///
898///
899/// - `file` must point to valid NUL-terminated
900///   strings (or NULL where the C contract allows) for the lifetime
901///   of the call.
902///
903/// The caller must not race this call with concurrent mutation of the
904/// same objects from other threads (per-object state is not internally
905/// synchronized). Violating any of the above is undefined behavior.
906///
907/// Exercised by the C-API differential courts
908/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
909/// courts; those pass byte-for-byte against the upstream oracle.
910#[no_mangle]
911pub unsafe extern "C" fn xmlMallocLoc(
912    size: usize,
913    file: *const c_char,
914    line: c_int,
915) -> *mut c_void {
916    let ptr = unsafe { xmlMallocImpl(size) };
917    if !ptr.is_null() {
918        unsafe { block_record(ptr, size, file, line) };
919    }
920    ptr
921}
922
923/// Allocate zeroed memory, recording the allocation site (upstream xmlmemory.h).
924///
925/// ```c
926/// void *xmlMallocAtomicLoc(size_t size, const char *file, int line);
927/// ```
928///
929/// # SAFETY
930///
931///
932/// - `file` must point to valid NUL-terminated
933///   strings (or NULL where the C contract allows) for the lifetime
934///   of the call.
935///
936/// The caller must not race this call with concurrent mutation of the
937/// same objects from other threads (per-object state is not internally
938/// synchronized). Violating any of the above is undefined behavior.
939///
940/// Exercised by the C-API differential courts
941/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
942/// courts; those pass byte-for-byte against the upstream oracle.
943#[no_mangle]
944pub unsafe extern "C" fn xmlMallocAtomicLoc(
945    size: usize,
946    file: *const c_char,
947    line: c_int,
948) -> *mut c_void {
949    let ptr = unsafe { xmlMallocZero(size) };
950    if !ptr.is_null() {
951        unsafe { block_record(ptr, size, file, line) };
952    }
953    ptr
954}
955
956/// Reallocate memory, recording the allocation site (upstream xmlmemory.h).
957///
958/// ```c
959/// void *xmlReallocLoc(void *ptr, size_t size, const char *file, int line);
960/// ```
961///
962/// # SAFETY
963///
964/// - `ptr` must be valid pointers (or NULL
965///   where the upstream C contract allows), obtained from the
966///   matching constructor/owner and not yet freed; the callee may
967///   take or keep ownership exactly as the C API specifies.
968///
969/// - `file` must point to valid NUL-terminated
970///   strings (or NULL where the C contract allows) for the lifetime
971///   of the call.
972///
973/// The caller must not race this call with concurrent mutation of the
974/// same objects from other threads (per-object state is not internally
975/// synchronized). Violating any of the above is undefined behavior.
976///
977/// Exercised by the C-API differential courts
978/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
979/// courts; those pass byte-for-byte against the upstream oracle.
980#[no_mangle]
981pub unsafe extern "C" fn xmlReallocLoc(
982    ptr: *mut c_void,
983    size: usize,
984    file: *const c_char,
985    line: c_int,
986) -> *mut c_void {
987    let new_ptr = unsafe { xmlReallocImpl(ptr, size) };
988    if !new_ptr.is_null() {
989        unsafe { block_record(new_ptr, size, file, line) };
990    } else if !ptr.is_null() {
991        // Keep the old site on failure.
992        let meta = BLOCKS.lock().get(&(ptr as usize)).copied();
993        if let Some(m) = meta {
994            unsafe { block_record(ptr, m.size, m.file as *const c_char, m.line) };
995        }
996    }
997    new_ptr
998}
999
1000/// Duplicate a string, recording the allocation site (upstream xmlmemory.h).
1001///
1002/// ```c
1003/// void *xmlMemStrdupLoc(const char *str, const char *file, int line);
1004/// ```
1005///
1006/// # SAFETY
1007///
1008///
1009/// - `str`, `file` must point to valid NUL-terminated
1010///   strings (or NULL where the C contract allows) for the lifetime
1011///   of the call.
1012///
1013/// The caller must not race this call with concurrent mutation of the
1014/// same objects from other threads (per-object state is not internally
1015/// synchronized). Violating any of the above is undefined behavior.
1016///
1017/// Exercised by the C-API differential courts
1018/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1019/// courts; those pass byte-for-byte against the upstream oracle.
1020#[no_mangle]
1021pub unsafe extern "C" fn xmlMemStrdupLoc(
1022    str: *const c_char,
1023    file: *const c_char,
1024    line: c_int,
1025) -> *mut c_void {
1026    let ptr = unsafe { xmlMemStrdupImpl(str) };
1027    if !ptr.is_null() {
1028        let len = unsafe { libc::strlen(str) } + 1;
1029        unsafe { block_record(ptr, len, file, line) };
1030    }
1031    ptr
1032}
1033
1034/// Return the size of an allocated block (upstream xmlmemory.h).
1035///
1036/// ```c
1037/// size_t xmlMemSize(void *ptr);
1038/// ```
1039///
1040/// Returns the recorded size from the block registry (0 for unknown or
1041/// foreign pointers, matching upstream's lookup-miss behavior).
1042///
1043/// # SAFETY
1044///
1045/// - `ptr` must be valid pointers (or NULL
1046///   where the upstream C contract allows), obtained from the
1047///   matching constructor/owner and not yet freed; the callee may
1048///   take or keep ownership exactly as the C API specifies.
1049///
1050/// The caller must not race this call with concurrent mutation of the
1051/// same objects from other threads (per-object state is not internally
1052/// synchronized). Violating any of the above is undefined behavior.
1053///
1054/// Exercised by the C-API differential courts
1055/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1056/// courts; those pass byte-for-byte against the upstream oracle.
1057#[no_mangle]
1058pub unsafe extern "C" fn xmlMemSize(ptr: *mut c_void) -> usize {
1059    if ptr.is_null() {
1060        return 0;
1061    }
1062    BLOCKS
1063        .lock()
1064        .get(&(ptr as usize))
1065        .map(|m| m.size)
1066        .unwrap_or(0)
1067}
1068
1069/// Display a limited amount of memory debug information (upstream xmlmemory.h).
1070///
1071/// ```c
1072/// void xmlMemDisplayLast(FILE *fp, long nbBytes);
1073/// ```
1074///
1075/// Dumps the per-block registry (upstream xmlMemDisplayLast block listing):
1076/// one line per live block with its address, size and recorded allocation
1077/// site, bounded by `nb_bytes` when positive. The aggregate footer matches
1078/// upstream's counters.
1079///
1080/// # SAFETY
1081///
1082/// - `fp` must be valid pointers (or NULL
1083///   where the upstream C contract allows), obtained from the
1084///   matching constructor/owner and not yet freed; the callee may
1085///   take or keep ownership exactly as the C API specifies.
1086///
1087/// The caller must not race this call with concurrent mutation of the
1088/// same objects from other threads (per-object state is not internally
1089/// synchronized). Violating any of the above is undefined behavior.
1090///
1091/// Exercised by the C-API differential courts
1092/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1093/// courts; those pass byte-for-byte against the upstream oracle.
1094#[no_mangle]
1095pub unsafe extern "C" fn xmlMemDisplayLast(fp: *mut c_void, nb_bytes: c_long) {
1096    // SAFETY: fp must be a valid FILE* or NULL (stderr used).
1097    unsafe {
1098        let out = if fp.is_null() {
1099            libc::fdopen(2, b"w\0" as *const u8 as *const c_char) as *mut c_void
1100        } else {
1101            fp
1102        };
1103        if out.is_null() {
1104            return;
1105        }
1106        let mut total: usize = 0;
1107        let mut msg = String::new();
1108        msg.push_str("MEMORY ALLOCATED : 0, MAX : 0, BLOCKS : ");
1109        let blocks = MEM_BLOCKS.load(Ordering::Relaxed);
1110        msg.push_str(&blocks.to_string());
1111        msg.push('\n');
1112        let map = BLOCKS.lock();
1113        let mut entries: Vec<(usize, &BlockMeta)> = map.iter().map(|(k, v)| (*k, v)).collect();
1114        entries.sort_by_key(|(k, _)| *k);
1115        for (addr, meta) in entries {
1116            if nb_bytes > 0 && (total as c_long) >= nb_bytes {
1117                break;
1118            }
1119            total += meta.size;
1120            msg.push_str(&format!(
1121                "  {:018p} : {:>7} bytes",
1122                addr as *const c_void, meta.size
1123            ));
1124            if meta.file != 0 {
1125                let file = CStr::from_ptr(meta.file as *const c_char).to_string_lossy();
1126                msg.push_str(&format!(" @ {}:{}", file, meta.line));
1127            }
1128            msg.push('\n');
1129        }
1130        drop(map);
1131        let used = MEM_USED.load(Ordering::Relaxed);
1132        msg.push_str(&format!(
1133            "TOTAL MEMORY ALLOCATED : {} bytes, TOTAL BLOCKS : {}\n",
1134            used, blocks
1135        ));
1136        let bytes = msg.as_bytes();
1137        libc::fwrite(
1138            bytes.as_ptr() as *const c_void,
1139            1,
1140            bytes.len(),
1141            out as *mut libc::FILE,
1142        );
1143    }
1144}
1145
1146/// Dump memory allocation statistics (upstream xmlmemory.h).
1147///
1148/// ```c
1149/// int xmlMemoryDump(void);
1150/// ```
1151///
1152/// Prints the global counters to stderr and returns 0 (no leak detector is
1153/// active in the default allocator).
1154///
1155/// # SAFETY
1156///
1157/// The function touches crate-global state only; it is safe
1158/// as long as the caller respects the library's global
1159/// initialization/cleanup ordering (xmlInitParser before use,
1160/// xmlCleanupParser only after all users are done).
1161///
1162/// Violating the global lifecycle ordering, or calling this after
1163/// teardown or from a signal handler, is undefined behavior.
1164#[no_mangle]
1165pub unsafe extern "C" fn xmlMemoryDump() -> c_int {
1166    unsafe {
1167        xmlMemDisplayLast(ptr::null_mut(), -1);
1168    }
1169    0
1170}
1171
1172// ═══════════════════════════════════════════════════════════════════════════════
1173// Tests
1174// ═══════════════════════════════════════════════════════════════════════════════
1175
1176#[cfg(test)]
1177mod tests {
1178    use super::*;
1179    use core::ptr;
1180
1181    /// Test basic allocation and deallocation.
1182    #[test]
1183    fn test_malloc_free() {
1184        unsafe {
1185            let ptr = xmlMalloc(100);
1186            assert!(!ptr.is_null(), "xmlMalloc(100) returned NULL");
1187            xmlFree(ptr);
1188        }
1189    }
1190
1191    /// Test that xmlMalloc(0) returns a valid pointer.
1192    #[test]
1193    fn test_malloc_zero() {
1194        unsafe {
1195            let ptr = xmlMalloc(0);
1196            // malloc(0) may return NULL or a valid pointer.
1197            // If non-NULL, it must be freeable.
1198            if !ptr.is_null() {
1199                xmlFree(ptr);
1200            }
1201        }
1202    }
1203
1204    /// Test xmlFree(NULL) is a no-op.
1205    #[test]
1206    fn test_free_null() {
1207        unsafe {
1208            xmlFree(ptr::null_mut());
1209            // Should not crash
1210        }
1211    }
1212
1213    /// Test xmlRealloc.
1214    #[test]
1215    fn test_realloc() {
1216        unsafe {
1217            let ptr = xmlMalloc(50);
1218            assert!(!ptr.is_null());
1219            let new_ptr = xmlRealloc(ptr, 100);
1220            assert!(!new_ptr.is_null());
1221            xmlFree(new_ptr);
1222        }
1223    }
1224
1225    /// Test xmlMemStrdup.
1226    #[test]
1227    fn test_mem_strdup() {
1228        unsafe {
1229            let s = b"hello\0" as *const u8 as *const c_char;
1230            let dup = xmlMemStrdup(s);
1231            assert!(!dup.is_null());
1232            // Compare the strings
1233            let orig_slice = std::slice::from_raw_parts(s as *const u8, 6);
1234            let dup_slice = std::slice::from_raw_parts(dup as *const u8, 6);
1235            assert_eq!(orig_slice, dup_slice);
1236            xmlFree(dup);
1237        }
1238    }
1239
1240    /// Test xmlMallocZero returns zeroed memory.
1241    #[test]
1242    fn test_malloc_zero_init() {
1243        unsafe {
1244            let ptr = xmlMallocZero(100) as *mut u8;
1245            assert!(!ptr.is_null());
1246            let slice = std::slice::from_raw_parts(ptr, 100);
1247            assert!(slice.iter().all(|&b| b == 0));
1248            xmlFree(ptr as *mut c_void);
1249        }
1250    }
1251
1252    /// Test custom allocator setup/get.
1253    #[test]
1254    fn test_mem_setup_get() {
1255        unsafe {
1256            let mut free_func: Option<xmlFreeFunc> = None;
1257            let mut malloc_func: Option<xmlMallocFunc> = None;
1258            let mut realloc_func: Option<xmlReallocFunc> = None;
1259            let mut strdup_func: Option<xmlStrdupFunc> = None;
1260
1261            xmlMemGet(
1262                &mut free_func as *mut _,
1263                &mut malloc_func as *mut _,
1264                &mut realloc_func as *mut _,
1265                &mut strdup_func as *mut _,
1266            );
1267
1268            assert!(malloc_func.is_some());
1269            assert!(free_func.is_some());
1270            assert!(realloc_func.is_some());
1271            assert!(strdup_func.is_some());
1272        }
1273    }
1274
1275    /// Test xmlMemUsed and xmlMemBlocks return reasonable values.
1276    #[test]
1277    fn test_mem_stats() {
1278        unsafe {
1279            let ptr = xmlMalloc(100);
1280            assert!(!ptr.is_null());
1281
1282            // The block registry records the exact size (deterministic,
1283            // unlike the process-wide counters which other test threads
1284            // mutate concurrently).
1285            assert_eq!(xmlMemSize(ptr), 100);
1286
1287            xmlFree(ptr);
1288            // Freed blocks leave the registry.
1289            assert_eq!(xmlMemSize(ptr), 0);
1290        }
1291    }
1292}