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}