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