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}