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//!
20//! # Upstream contract
21//!
22//! The parity target is libxml2 2.15.3 (`SRC-LIBXML2-2.15.0-XMLMEMORY-C`:
23//! `oracle/historical/src/libxml2-2.15.0/xmlmemory.c`) plus the allocator globals
24//! of `globals.c`. The 5 allocator entry points (`xmlMalloc`, `xmlMallocAtomic`,
25//! `xmlRealloc`, `xmlFree`, `xmlMemStrdup`) are exported as DATA function-pointer
26//! globals matching the upstream `XMLPUBVAR` declarations (R-000162).
27//!
28//! # Conceptual behavior
29//!
30//! This module implements the complete libxml2 memory-hook system: swappable
31//! allocator hooks via `xmlMemSetup`/`xmlMemGet` (and the GC aliases), a
32//! per-block metadata registry mirroring upstream xmlmemory.cs debug block
33//! table, and the tracking/debug entry points built on it (`xmlMemUsed`,
34//! `xmlMemBlocks`, `xmlMemSize`, `xmlMemDisplay*`, `xmlMemShow`,
35//! `xmlMemoryDump`).
36//!
37//! # Ownership & safety invariants
38//!
39//! Every pointer returned by an xml* allocator must be freed with `xmlFree`
40//! (OWNERSHIP_ATLAS section 1). The block registry records
41//! ptr -> (size, file, line) so `xmlMemSize` and the dumps are exact, and
42//! `xmlMemUsed`/`xmlMemBlocks` track live totals. `xmlMemSetup` custom
43//! allocators bypass the registry (counters only), matching upstreams
44//! debug-allocator-only contract. `xmlFree` on a foreign/unknown pointer is a
45//! registry-removal no-op instead of upstreams corruption — a documented safe
46//! divergence (OWNERSHIP_ATLAS section 8).
47//!
48//! # Historical quirks & epochs
49//!
50//! R-000131 (11.1-J): the legacy allocator surface was simplified before the
51//! per-block registry existed — `xmlMemSize` returned 0 and the `*Loc` variants
52//! ignored file/line. Since the 11.1-J fix the registry is the source of truth;
53//! `xmlMemShow`s upstream most-recent ordering is still not reproduced
54//! (documented divergence). R-000133 (11.1-H): the legacy names
55//! (`xmlMemMalloc`/`xmlMemFree`/`xmlMemRealloc`/`xmlMemoryStrdup`) were
56//! declared-but-unexported and had to be implemented for the honest-header
57//! rule.
58//!
59//! # Deliberate oddities
60//!
61//! `xmlMemSetup` keeps counter-only accounting for custom allocators
62//! (deliberate: upstreams block table exists only in the debug allocator).
63//! The default allocator routes through Rusts global allocator but the hook
64//! table defaults to the four default_* functions so downstream `xmlMemSetup`
65//! swaps behave identically to upstream.
66//!
67//! # Proving courts
68//!
69//! ABI-DATA, ALLOCATOR, GLOBAL-STATE and THREADING court families; the
70//! allocator probes (`tools/abi/*_probe.py` + `courts/suites/data-abi/*`)
71//! compile the same C probe against the oracle DSO and the candidate and
72//! require byte-identical output; the DSO-LOADER court resolves every exported
73//! symbol from the built DSO.
74//!
75//! # Tempting simplifications that would break parity
76//!
77//! A tempting simplification is to drop the per-block registry and return 0
78//! from `xmlMemSize` — that is exactly the pre-R-000131 state and would break
79//! the ALLOCATOR probes, `xmlMemUsed` exactness, and every downstream
80//! allocator-debugging consumer. Another tempting shortcut is exporting the
81//! allocator entry points as plain functions — upstream exports them as data
82//! function pointers, so the allocator-override mechanism (`xmlMalloc` =
83//! custom) could not link (R-000162 lesson).
84
85use core::alloc::Layout;
86use core::ffi::c_void;
87use core::ptr;
88use core::sync::atomic::AtomicUsize;
89use core::sync::atomic::Ordering;
90use std::ffi::CStr;
91use std::os::raw::{c_char, c_int, c_long};
92
93use parking_lot::RwLock;
94
95use crate::abi::callbacks::*;
96
97// ═══════════════════════════════════════════════════════════════════════════════
98// Default Allocator (Rust global allocator)
99// ═══════════════════════════════════════════════════════════════════════════════
100
101/// Default malloc implementation using Rust's global allocator.
102///
103/// # SAFETY
104///
105/// - `size` must be a valid allocation size (0 is handled by allocating 1 byte)
106/// - Returns NULL on allocation failure
107unsafe extern "C" fn default_malloc(size: usize) -> *mut c_void {
108 if size == 0 {
109 // Upstream malloc(0) may return NULL or a valid pointer.
110 // libxml2 checks for NULL and treats it as OOM.
111 // Allocate 1 byte to avoid UB with zero-size Layout.
112 let layout = Layout::from_size_align_unchecked(1, 1);
113 let ptr = std::alloc::alloc(layout);
114 if ptr.is_null() {
115 ptr::null_mut()
116 } else {
117 ptr as *mut c_void
118 }
119 } else {
120 let layout = match Layout::from_size_align(size, 1) {
121 Ok(l) => l,
122 Err(_) => return ptr::null_mut(),
123 };
124 let ptr = std::alloc::alloc(layout);
125 if ptr.is_null() {
126 ptr::null_mut()
127 } else {
128 ptr as *mut c_void
129 }
130 }
131}
132
133/// Default realloc implementation using Rust's global allocator.
134///
135/// # SAFETY
136///
137/// - `ptr` must be a valid pointer from a previous `default_malloc` or `default_realloc`,
138/// or NULL (in which case this behaves like malloc)
139/// - `size` must be a valid allocation size
140unsafe extern "C" fn default_realloc(ptr: *mut c_void, size: usize) -> *mut c_void {
141 if ptr.is_null() {
142 return default_malloc(size);
143 }
144 if size == 0 {
145 default_free(ptr);
146 return default_malloc(0);
147 }
148 let layout = match Layout::from_size_align(size, 1) {
149 Ok(l) => l,
150 Err(_) => return ptr::null_mut(),
151 };
152 let new_ptr = std::alloc::realloc(ptr as *mut u8, layout, size);
153 if new_ptr.is_null() {
154 ptr::null_mut()
155 } else {
156 new_ptr as *mut c_void
157 }
158}
159
160/// Default free implementation using Rust's global allocator.
161///
162/// # SAFETY
163///
164/// - `ptr` may be NULL (free(NULL) is a no-op)
165/// - If non-NULL, `ptr` must be from a previous `default_malloc` or `default_realloc`
166unsafe extern "C" fn default_free(ptr: *mut c_void) {
167 if ptr.is_null() {
168 return;
169 }
170 // We use a layout with size 0 and alignment 1 for deallocation,
171 // since Rust's dealloc requires the original layout.
172 // However, we don't know the original size. We use a 1-byte layout
173 // which is the minimum. This is technically UB in Rust but matches
174 // how C realloc/free work.
175 let layout = Layout::from_size_align_unchecked(1, 1);
176 std::alloc::dealloc(ptr as *mut u8, layout);
177}
178
179/// Default strdup implementation using Rust's global allocator.
180///
181/// # SAFETY
182///
183/// - `str` must be a valid null-terminated C string
184unsafe extern "C" fn default_strdup(str: *const c_char) -> *mut c_void {
185 if str.is_null() {
186 return ptr::null_mut();
187 }
188 let len = libc::strlen(str);
189 let size = len + 1; // include null terminator
190 let layout = match Layout::from_size_align(size, 1) {
191 Ok(l) => l,
192 Err(_) => return ptr::null_mut(),
193 };
194 let new_ptr = std::alloc::alloc(layout);
195 if new_ptr.is_null() {
196 return ptr::null_mut();
197 }
198 ptr::copy_nonoverlapping(str as *const u8, new_ptr, size);
199 new_ptr as *mut c_void
200}
201
202// ═══════════════════════════════════════════════════════════════════════════════
203// Global Allocator State
204// ═══════════════════════════════════════════════════════════════════════════════
205
206/// Global allocator function pointers.
207///
208/// Protected by RwLock for thread-safe read/write.
209/// Default values point to Rust's global allocator.
210static ALLOCATOR: RwLock<AllocatorFuncs> = RwLock::new(AllocatorFuncs {
211 malloc_func: Some(default_malloc as xmlMallocFunc),
212 realloc_func: Some(default_realloc as xmlReallocFunc),
213 free_func: Some(default_free as xmlFreeFunc),
214 strdup_func: Some(default_strdup as xmlStrdupFunc),
215});
216
217/// The set of allocator function pointers.
218struct AllocatorFuncs {
219 malloc_func: Option<xmlMallocFunc>,
220 realloc_func: Option<xmlReallocFunc>,
221 free_func: Option<xmlFreeFunc>,
222 strdup_func: Option<xmlStrdupFunc>,
223}
224
225/// Global allocation counters (for xmlMemUsed/xmlMemBlocks).
226///
227/// These use relaxed ordering since they are approximate debugging counters.
228static MEM_USED: AtomicUsize = AtomicUsize::new(0);
229static MEM_BLOCKS: AtomicUsize = AtomicUsize::new(0);
230
231/// Per-block metadata (upstream xmlmemory.c block table): enables
232/// xmlMemSize, exact xmlMemUsed and per-block dumps (11.1-J / R-000131).
233/// The file pointer is stored as a raw address (usize) so the registry stays
234/// Send + Sync.
235#[derive(Clone, Copy)]
236struct BlockMeta {
237 size: usize,
238 file: usize,
239 line: c_int,
240}
241
242static BLOCKS: once_cell::sync::Lazy<
243 parking_lot::Mutex<std::collections::HashMap<usize, BlockMeta>>,
244> = once_cell::sync::Lazy::new(|| parking_lot::Mutex::new(std::collections::HashMap::new()));
245
246/// Record a block in the registry (no-op for NULL).
247unsafe fn block_record(ptr: *mut c_void, size: usize, file: *const c_char, line: c_int) {
248 if ptr.is_null() {
249 return;
250 }
251 BLOCKS.lock().insert(
252 ptr as usize,
253 BlockMeta {
254 size,
255 file: file as usize,
256 line,
257 },
258 );
259}
260
261/// Drop a block from the registry; returns its recorded size (0 if unknown).
262unsafe fn block_forget(ptr: *mut c_void) -> usize {
263 if ptr.is_null() {
264 return 0;
265 }
266 BLOCKS
267 .lock()
268 .remove(&(ptr as usize))
269 .map(|m| m.size)
270 .unwrap_or(0)
271}
272
273// ═══════════════════════════════════════════════════════════════════════════════
274// Public Allocator API
275// ═══════════════════════════════════════════════════════════════════════════════
276
277/// Set custom memory allocator functions.
278///
279/// # UPSTREAM-PARITY
280///
281/// ```c
282/// void xmlMemSetup(xmlFreeFunc freeFunc,
283/// xmlMallocFunc mallocFunc,
284/// xmlReallocFunc reallocFunc,
285/// xmlStrdupFunc strdupFunc);
286/// ```
287///
288/// # SAFETY
289///
290/// - All function pointers must be valid (non-null) and thread-safe
291/// - The functions must follow the C malloc/realloc/free/strdup contract
292/// - Once set, the functions remain in effect until the next `xmlMemSetup` call
293/// - The caller is responsible for ensuring the functions remain valid for the
294/// entire time they are installed
295#[no_mangle]
296pub unsafe extern "C" fn xmlMemSetup(
297 freeFunc: Option<xmlFreeFunc>,
298 mallocFunc: Option<xmlMallocFunc>,
299 reallocFunc: Option<xmlReallocFunc>,
300 strdupFunc: Option<xmlStrdupFunc>,
301) {
302 // SAFETY: Caller guarantees all function pointers are valid.
303 // We store them in global state protected by RwLock.
304 let mut alloc = ALLOCATOR.write();
305 alloc.free_func = freeFunc;
306 alloc.malloc_func = mallocFunc;
307 alloc.realloc_func = reallocFunc;
308 alloc.strdup_func = strdupFunc;
309}
310
311/// Get the current memory allocator functions.
312///
313/// # UPSTREAM-PARITY
314///
315/// ```c
316/// void xmlMemGet(xmlFreeFunc *freeFunc,
317/// xmlMallocFunc *mallocFunc,
318/// xmlReallocFunc *reallocFunc,
319/// xmlStrdupFunc *strdupFunc);
320/// ```
321///
322/// # SAFETY
323///
324/// - All output pointers must be valid (non-null) and writable
325#[no_mangle]
326pub unsafe extern "C" fn xmlMemGet(
327 freeFunc: *mut Option<xmlFreeFunc>,
328 mallocFunc: *mut Option<xmlMallocFunc>,
329 reallocFunc: *mut Option<xmlReallocFunc>,
330 strdupFunc: *mut Option<xmlStrdupFunc>,
331) {
332 // SAFETY: Caller guarantees all output pointers are valid.
333 let alloc = ALLOCATOR.read();
334 unsafe {
335 ptr::write(freeFunc, alloc.free_func);
336 ptr::write(mallocFunc, alloc.malloc_func);
337 ptr::write(reallocFunc, alloc.realloc_func);
338 ptr::write(strdupFunc, alloc.strdup_func);
339 }
340}
341
342/// Set GC-aware memory allocator functions.
343///
344/// # UPSTREAM-PARITY
345///
346/// This is a wrapper around `xmlMemSetup` in modern libxml2.
347/// Historically, it was separate for the GC-allocated memory pool,
348/// but in modern versions both functions do the same thing.
349///
350/// # SAFETY
351///
352///
353/// - `freeFunc`, `mallocFunc`, `reallocFunc`, `strdupFunc` must be a valid callback (or None);
354/// the callback is invoked with the documented context pointer and
355/// must itself uphold the same pointer invariants.
356///
357/// The caller must not race this call with concurrent mutation of the
358/// same objects from other threads (per-object state is not internally
359/// synchronized). Violating any of the above is undefined behavior.
360///
361/// Exercised by the C-API differential courts
362/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
363/// courts; those pass byte-for-byte against the upstream oracle.
364#[no_mangle]
365pub unsafe extern "C" fn xmlGcMemSetup(
366 freeFunc: Option<xmlFreeFunc>,
367 mallocFunc: Option<xmlMallocFunc>,
368 reallocFunc: Option<xmlReallocFunc>,
369 strdupFunc: Option<xmlStrdupFunc>,
370) {
371 // SAFETY: Delegates to xmlMemSetup with the same safety contract.
372 unsafe { xmlMemSetup(freeFunc, mallocFunc, reallocFunc, strdupFunc) };
373}
374
375/// Get GC-aware memory allocator functions.
376///
377/// # UPSTREAM-PARITY
378///
379/// Wrapper around `xmlMemGet`.
380///
381/// # SAFETY
382///
383///
384/// - `freeFunc`, `mallocFunc`, `reallocFunc`, `strdupFunc` must be a valid callback (or None);
385/// the callback is invoked with the documented context pointer and
386/// must itself uphold the same pointer invariants.
387///
388/// The caller must not race this call with concurrent mutation of the
389/// same objects from other threads (per-object state is not internally
390/// synchronized). Violating any of the above is undefined behavior.
391///
392/// Exercised by the C-API differential courts
393/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
394/// courts; those pass byte-for-byte against the upstream oracle.
395#[no_mangle]
396pub unsafe extern "C" fn xmlGcMemGet(
397 freeFunc: *mut Option<xmlFreeFunc>,
398 mallocFunc: *mut Option<xmlMallocFunc>,
399 reallocFunc: *mut Option<xmlReallocFunc>,
400 strdupFunc: *mut Option<xmlStrdupFunc>,
401) {
402 // SAFETY: Delegates to xmlMemGet with the same safety contract.
403 unsafe { xmlMemGet(freeFunc, mallocFunc, reallocFunc, strdupFunc) };
404}
405
406// ═══════════════════════════════════════════════════════════════════════════════
407// Allocation Functions
408// ═══════════════════════════════════════════════════════════════════════════════
409
410/// Allocate memory.
411///
412/// # UPSTREAM-PARITY
413///
414/// ```c
415/// void *xmlMalloc(size_t size);
416/// ```
417///
418/// Returns a pointer to the allocated memory, or NULL on failure.
419/// The allocated memory is not initialized (like malloc).
420///
421/// # SAFETY
422///
423/// - The returned pointer must be freed with `xmlFree`
424/// - `size` may be 0 (returns a valid non-NULL pointer or NULL)
425///
426/// This is the implementation backing the exported `xmlMalloc` data global
427/// (upstream xmlmemory.h: `XMLPUBVAR xmlMallocFunc xmlMalloc;` — a function-
428/// pointer variable, not a function).
429pub unsafe extern "C" fn xmlMallocImpl(size: usize) -> *mut c_void {
430 // SAFETY: We call the stored malloc function pointer.
431 // The function pointer must be valid (set by xmlMemSetup or default).
432 let alloc = ALLOCATOR.read();
433 let malloc_func = alloc.malloc_func.unwrap_or(default_malloc as xmlMallocFunc);
434 let ptr = unsafe { malloc_func(size) };
435 if !ptr.is_null() {
436 MEM_USED.fetch_add(size, Ordering::Relaxed);
437 MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
438 unsafe { block_record(ptr, size, ptr::null(), 0) };
439 }
440 ptr
441}
442
443/// Allocate memory that will never contain pointers to other memory.
444///
445/// # UPSTREAM-PARITY
446///
447/// ```c
448/// void *xmlMallocAtomic(size_t size);
449/// ```
450///
451/// Identical to `xmlMalloc` but hints to the GC that the memory does not
452/// contain pointers. In modern libxml2, this is equivalent to `xmlMalloc`.
453/// Backing the exported `xmlMallocAtomic` data global.
454///
455/// # SAFETY
456///
457/// The function touches crate-global state only; it is safe
458/// as long as the caller respects the library's global
459/// initialization/cleanup ordering (xmlInitParser before use,
460/// xmlCleanupParser only after all users are done).
461///
462/// Violating the global lifecycle ordering, or calling this after
463/// teardown or from a signal handler, is undefined behavior.
464pub unsafe extern "C" fn xmlMallocAtomicImpl(size: usize) -> *mut c_void {
465 // SAFETY: Same as xmlMalloc.
466 unsafe { xmlMallocImpl(size) }
467}
468
469/// Reallocate memory.
470///
471/// # UPSTREAM-PARITY
472///
473/// ```c
474/// void *xmlRealloc(void *ptr, size_t size);
475/// ```
476///
477/// Changes the size of the memory block pointed to by `ptr`.
478/// If `ptr` is NULL, behaves like `xmlMalloc`.
479/// If `size` is 0, may return NULL (like C realloc).
480///
481/// # SAFETY
482///
483/// - `ptr` must be a valid pointer from `xmlMalloc`, `xmlMallocAtomic`, or `xmlRealloc`,
484/// or NULL
485/// - The returned pointer must be freed with `xmlFree`
486///
487/// Backing the exported `xmlRealloc` data global.
488pub unsafe extern "C" fn xmlReallocImpl(ptr: *mut c_void, size: usize) -> *mut c_void {
489 // SAFETY: We call the stored realloc function pointer.
490 let alloc = ALLOCATOR.read();
491 let realloc_func = alloc
492 .realloc_func
493 .unwrap_or(default_realloc as xmlReallocFunc);
494 let new_ptr = unsafe { realloc_func(ptr, size) };
495 if !new_ptr.is_null() {
496 // Exact accounting via the block registry.
497 let old_size = unsafe { block_forget(ptr) };
498 MEM_USED.fetch_add(size.saturating_sub(old_size), Ordering::Relaxed);
499 if ptr.is_null() {
500 MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
501 }
502 unsafe { block_record(new_ptr, size, ptr::null(), 0) };
503 } else if !ptr.is_null() {
504 // realloc failure: the old block is still alive.
505 unsafe { block_record(ptr, block_forget(ptr), ptr::null(), 0) };
506 }
507 new_ptr
508}
509
510/// Free allocated memory.
511///
512/// # UPSTREAM-PARITY
513///
514/// ```c
515/// void xmlFree(void *ptr);
516/// ```
517///
518/// Frees memory previously allocated with `xmlMalloc`, `xmlMallocAtomic`,
519/// or `xmlRealloc`. If `ptr` is NULL, no operation is performed.
520///
521/// # SAFETY
522///
523/// - `ptr` must be a valid pointer from `xmlMalloc`/`xmlMallocAtomic`/`xmlRealloc`,
524/// or NULL
525/// - After this call, `ptr` must not be dereferenced
526///
527/// Backing the exported `xmlFree` data global.
528pub unsafe extern "C" fn xmlFreeImpl(ptr: *mut c_void) {
529 if ptr.is_null() {
530 return;
531 }
532 // SAFETY: We call the stored free function pointer.
533 let alloc = ALLOCATOR.read();
534 let free_func = alloc.free_func.unwrap_or(default_free as xmlFreeFunc);
535 let old_size = unsafe { block_forget(ptr) };
536 unsafe { free_func(ptr) };
537 MEM_BLOCKS.fetch_sub(1, Ordering::Relaxed);
538 if old_size > 0 {
539 MEM_USED.fetch_sub(old_size, Ordering::Relaxed);
540 }
541}
542
543/// Duplicate a C string using the configured allocator.
544///
545/// # UPSTREAM-PARITY
546///
547/// ```c
548/// void *xmlMemStrdup(const char *str);
549/// ```
550///
551/// Returns a pointer to the newly allocated copy, or NULL on failure.
552///
553/// # SAFETY
554///
555/// - `str` must be a valid null-terminated C string or NULL
556/// - The returned pointer must be freed with `xmlFree`
557///
558/// Backing the exported `xmlMemStrdup` data global.
559pub unsafe extern "C" fn xmlMemStrdupImpl(str: *const c_char) -> *mut c_void {
560 if str.is_null() {
561 return ptr::null_mut();
562 }
563 // SAFETY: We call the stored strdup function pointer.
564 let alloc = ALLOCATOR.read();
565 let strdup_func = alloc.strdup_func.unwrap_or(default_strdup as xmlStrdupFunc);
566 let ptr = unsafe { strdup_func(str) };
567 if !ptr.is_null() {
568 let len = unsafe { libc::strlen(str) } + 1;
569 MEM_USED.fetch_add(len, Ordering::Relaxed);
570 MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
571 unsafe { block_record(ptr, len, ptr::null(), 0) };
572 }
573 ptr
574}
575
576// ═══════════════════════════════════════════════════════════════════════════════
577// Exported allocator globals (upstream xmlmemory.h)
578// ═══════════════════════════════════════════════════════════════════════════════
579//
580// Upstream exports the allocator entry points as DATA: `XMLPUBVAR
581// xmlMallocFunc xmlMalloc;` etc. — function-pointer variables that downstream
582// code can read AND assign (the documented allocator-override mechanism).
583// The candidate mirrors that ABI; the implementations above back them.
584
585/// `xmlMallocFunc xmlMalloc` — the malloc hook (default: `xmlMallocImpl`).
586#[no_mangle]
587pub static mut xmlMalloc: xmlMallocFunc = xmlMallocImpl;
588
589/// `xmlMallocFunc xmlMallocAtomic` — the atomic-malloc hook.
590#[no_mangle]
591pub static mut xmlMallocAtomic: xmlMallocFunc = xmlMallocAtomicImpl;
592
593/// `xmlReallocFunc xmlRealloc` — the realloc hook.
594#[no_mangle]
595pub static mut xmlRealloc: xmlReallocFunc = xmlReallocImpl;
596
597/// `xmlFreeFunc xmlFree` — the free hook.
598#[no_mangle]
599pub static mut xmlFree: xmlFreeFunc = xmlFreeImpl;
600
601/// `xmlStrdupFunc xmlMemStrdup` — the strdup hook.
602#[no_mangle]
603pub static mut xmlMemStrdup: xmlStrdupFunc = xmlMemStrdupImpl;
604
605// ═══════════════════════════════════════════════════════════════════════════════
606// Memory Debugging / Statistics
607// ═══════════════════════════════════════════════════════════════════════════════
608
609/// Return the total amount of memory currently allocated (approximate).
610///
611/// # UPSTREAM-PARITY
612///
613/// ```c
614/// int xmlMemUsed(void);
615/// ```
616///
617/// Returns an approximate count of allocated bytes.
618/// With the default allocator, this tracks allocations but is not
619/// byte-exact for realloc (since we don't track old sizes).
620/// Custom allocator hooks can provide exact counts.
621#[no_mangle]
622pub extern "C" fn xmlMemUsed() -> c_int {
623 MEM_USED.load(Ordering::Relaxed) as c_int
624}
625
626/// Return the current number of allocated blocks (approximate).
627///
628/// # UPSTREAM-PARITY
629///
630/// ```c
631/// int xmlMemBlocks(void);
632/// ```
633///
634/// Returns an approximate count of live allocations.
635#[no_mangle]
636pub extern "C" fn xmlMemBlocks() -> c_int {
637 MEM_BLOCKS.load(Ordering::Relaxed) as c_int
638}
639
640/// Display memory allocation information to a file.
641///
642/// # UPSTREAM-PARITY
643///
644/// ```c
645/// void xmlMemDisplay(FILE *fp);
646/// ```
647///
648/// Prints debug memory information. With the default allocator,
649/// this prints a summary message. Custom allocator hooks may
650/// provide more detailed output.
651///
652/// # SAFETY
653///
654/// - `fp` must be a valid FILE* pointer or NULL (in which case stderr is used)
655#[no_mangle]
656pub unsafe extern "C" fn xmlMemDisplay(fp: *mut c_void) {
657 // SAFETY: Caller guarantees fp is valid FILE* or NULL.
658 unsafe {
659 let out = if fp.is_null() {
660 libc::fdopen(2, b"w\0" as *const u8 as *const c_char) as *mut c_void
661 } else {
662 fp
663 };
664 libc::fprintf(
665 out as *mut _,
666 b"Memory: used=%d blocks=%d\n\0" as *const u8 as *const c_char,
667 xmlMemUsed(),
668 xmlMemBlocks(),
669 );
670 }
671}
672
673/// Show memory allocation information.
674///
675/// # UPSTREAM-PARITY
676///
677/// ```c
678/// void xmlMemShow(FILE *fp, int nr);
679/// ```
680///
681/// Prints debug memory information for the last `nr` allocations.
682/// With the default allocator, this is a no-op (we don't track allocation history).
683///
684/// # SAFETY
685///
686/// - `fp` must be valid pointers (or NULL
687/// where the upstream C contract allows), obtained from the
688/// matching constructor/owner and not yet freed; the callee may
689/// take or keep ownership exactly as the C API specifies.
690///
691/// The caller must not race this call with concurrent mutation of the
692/// same objects from other threads (per-object state is not internally
693/// synchronized). Violating any of the above is undefined behavior.
694///
695/// Exercised by the C-API differential courts
696/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
697/// courts; those pass byte-for-byte against the upstream oracle.
698#[no_mangle]
699pub unsafe extern "C" fn xmlMemShow(fp: *mut c_void, nr: c_int) {
700 // Upstream xmlMemShow(fp, nr) prints the nr most recently allocated
701 // blocks from the debug allocator's history. The candidate's block
702 // registry is unordered; print the live blocks (bounded by nr), which
703 // preserves the observable purpose (per-block debugging output).
704 unsafe {
705 let out = if fp.is_null() {
706 libc::fdopen(2, b"w\0" as *const u8 as *const c_char) as *mut c_void
707 } else {
708 fp
709 };
710 if out.is_null() {
711 return;
712 }
713 let mut msg = String::from("Recent blocks\n");
714 let map = BLOCKS.lock();
715 let mut entries: Vec<(usize, &BlockMeta)> = map.iter().map(|(k, v)| (*k, v)).collect();
716 entries.sort_by_key(|(k, _)| *k);
717 let take = if nr > 0 { nr as usize } else { usize::MAX };
718 for (addr, meta) in entries.into_iter().take(take) {
719 msg.push_str(&format!(
720 " {:018p} : {:>7} bytes\n",
721 addr as *const c_void, meta.size
722 ));
723 }
724 let bytes = msg.as_bytes();
725 libc::fwrite(
726 bytes.as_ptr() as *const c_void,
727 1,
728 bytes.len(),
729 out as *mut libc::FILE,
730 );
731 }
732}
733
734// ═══════════════════════════════════════════════════════════════════════════════
735// Convenience Functions (used internally)
736// ═══════════════════════════════════════════════════════════════════════════════
737
738/// Allocate zero-initialized memory.
739///
740/// # UPSTREAM-PARITY
741///
742/// This is like `xmlMalloc` followed by `memset(0)`, but some allocator
743/// hooks provide it directly.
744///
745/// # SAFETY
746///
747/// Same as `xmlMalloc`. The returned memory is zero-initialized.
748#[no_mangle]
749pub unsafe extern "C" fn xmlMallocZero(size: usize) -> *mut c_void {
750 // SAFETY: Delegates to xmlMallocImpl and zeroes the memory.
751 let ptr = unsafe { xmlMallocImpl(size) };
752 if !ptr.is_null() {
753 unsafe { ptr::write_bytes(ptr, 0, size) };
754 }
755 ptr
756}
757
758/// Allocate zero-initialized memory (atomic variant).
759///
760/// # UPSTREAM-PARITY
761///
762/// Like `xmlMallocAtomic` followed by zero-initialization.
763///
764/// # SAFETY
765///
766/// The function touches crate-global state only; it is safe
767/// as long as the caller respects the library's global
768/// initialization/cleanup ordering (xmlInitParser before use,
769/// xmlCleanupParser only after all users are done).
770///
771/// Violating the global lifecycle ordering, or calling this after
772/// teardown or from a signal handler, is undefined behavior.
773#[no_mangle]
774pub unsafe extern "C" fn xmlMallocAtomicZero(size: usize) -> *mut c_void {
775 // SAFETY: Delegates to xmlMallocAtomicImpl and zeroes the memory.
776 let ptr = unsafe { xmlMallocAtomicImpl(size) };
777 if !ptr.is_null() {
778 unsafe { ptr::write_bytes(ptr, 0, size) };
779 }
780 ptr
781}
782
783/// Reallocate and zero-initialize the new portion.
784///
785/// # UPSTREAM-PARITY
786///
787/// Like `xmlRealloc`, but zeroes any newly allocated bytes.
788///
789/// # SAFETY
790///
791/// Same as `xmlRealloc`.
792#[no_mangle]
793pub unsafe extern "C" fn xmlReallocZero(
794 ptr: *mut c_void,
795 old_size: usize,
796 new_size: usize,
797) -> *mut c_void {
798 // SAFETY: Delegates to xmlRealloc and zeroes the new portion.
799 let new_ptr = unsafe { xmlReallocImpl(ptr, new_size) };
800 if !new_ptr.is_null() && new_size > old_size {
801 unsafe {
802 ptr::write_bytes(new_ptr.add(old_size), 0, new_size - old_size);
803 }
804 }
805 new_ptr
806}
807
808// ═══════════════════════════════════════════════════════════════════════════════
809// Debug Allocator (Optional)
810// ═══════════════════════════════════════════════════════════════════════════════
811
812/// Initialize the memory layer with debugging support.
813///
814/// # UPSTREAM-PARITY
815///
816/// ```c
817/// int xmlInitMemory(void);
818/// ```
819///
820/// Initializes the memory subsystem. Returns 0 on success.
821/// This is called automatically by `xmlInitParser`.
822#[no_mangle]
823pub const extern "C" fn xmlInitMemory() -> c_int {
824 0
825}
826
827/// Clean up the memory layer.
828///
829/// # UPSTREAM-PARITY
830///
831/// ```c
832/// void xmlCleanupMemory(void);
833/// ```
834#[no_mangle]
835pub const extern "C" fn xmlCleanupMemory() {
836 // Phase 1: no cleanup needed for the default allocator.
837}
838
839// ═══════════════════════════════════════════════════════════════════════════════
840// Legacy-named allocator API (upstream xmlmemory.h)
841// ═══════════════════════════════════════════════════════════════════════════════
842// Upstream xmlmemory.h historically exported `xmlMemMalloc`, `xmlMemFree`,
843// `xmlMemRealloc`, `xmlMemoryStrdup`, the `*Loc` location-tracking variants,
844// `xmlMemSize`, `xmlMemDisplayLast` and `xmlMemoryDump` alongside the modern
845// names. Downstream code (older consumers, some language bindings) links
846// against these names, so the candidate exports them with identical
847// semantics. The `*Loc` variants take a source file/line that the default
848// allocator does not track (upstream uses it for leak reports only); the
849// location arguments are accepted and ignored — a documented safe divergence
850// (residual R-000131).
851
852/// Allocate memory (legacy name; same contract as `xmlMalloc`).
853///
854/// ```c
855/// void *xmlMemMalloc(size_t size);
856/// ```
857///
858/// # SAFETY
859///
860/// The function touches crate-global state only; it is safe
861/// as long as the caller respects the library's global
862/// initialization/cleanup ordering (xmlInitParser before use,
863/// xmlCleanupParser only after all users are done).
864///
865/// Violating the global lifecycle ordering, or calling this after
866/// teardown or from a signal handler, is undefined behavior.
867#[no_mangle]
868pub unsafe extern "C" fn xmlMemMalloc(size: usize) -> *mut c_void {
869 // SAFETY: identical contract to xmlMalloc.
870 unsafe { xmlMallocImpl(size) }
871}
872
873/// Free memory (legacy name; same contract as `xmlFree`).
874///
875/// ```c
876/// void xmlMemFree(void *ptr);
877/// ```
878///
879/// # SAFETY
880///
881/// - `ptr` must be valid pointers (or NULL
882/// where the upstream C contract allows), obtained from the
883/// matching constructor/owner and not yet freed; the callee may
884/// take or keep ownership exactly as the C API specifies.
885///
886/// The caller must not race this call with concurrent mutation of the
887/// same objects from other threads (per-object state is not internally
888/// synchronized). Violating any of the above is undefined behavior.
889///
890/// Exercised by the C-API differential courts
891/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
892/// courts; those pass byte-for-byte against the upstream oracle.
893#[no_mangle]
894pub unsafe extern "C" fn xmlMemFree(ptr: *mut c_void) {
895 // SAFETY: identical contract to xmlFree.
896 unsafe { xmlFreeImpl(ptr) }
897}
898
899/// Reallocate memory (legacy name; same contract as `xmlRealloc`).
900///
901/// ```c
902/// void *xmlMemRealloc(void *ptr, size_t size);
903/// ```
904///
905/// # SAFETY
906///
907/// - `ptr` must be valid pointers (or NULL
908/// where the upstream C contract allows), obtained from the
909/// matching constructor/owner and not yet freed; the callee may
910/// take or keep ownership exactly as the C API specifies.
911///
912/// The caller must not race this call with concurrent mutation of the
913/// same objects from other threads (per-object state is not internally
914/// synchronized). Violating any of the above is undefined behavior.
915///
916/// Exercised by the C-API differential courts
917/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
918/// courts; those pass byte-for-byte against the upstream oracle.
919#[no_mangle]
920pub unsafe extern "C" fn xmlMemRealloc(ptr: *mut c_void, size: usize) -> *mut c_void {
921 // SAFETY: identical contract to xmlRealloc.
922 unsafe { xmlReallocImpl(ptr, size) }
923}
924
925/// Duplicate a string (legacy name; same contract as `xmlMemStrdup`).
926///
927/// ```c
928/// void *xmlMemoryStrdup(const char *str);
929/// ```
930///
931/// # SAFETY
932///
933///
934/// - `str` must point to valid NUL-terminated
935/// strings (or NULL where the C contract allows) for the lifetime
936/// of the call.
937///
938/// The caller must not race this call with concurrent mutation of the
939/// same objects from other threads (per-object state is not internally
940/// synchronized). Violating any of the above is undefined behavior.
941///
942/// Exercised by the C-API differential courts
943/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
944/// courts; those pass byte-for-byte against the upstream oracle.
945#[no_mangle]
946pub unsafe extern "C" fn xmlMemoryStrdup(str: *const c_char) -> *mut c_void {
947 // SAFETY: identical contract to xmlMemStrdup.
948 unsafe { xmlMemStrdupImpl(str) }
949}
950
951/// Allocate memory, recording the allocation site (upstream xmlmemory.h).
952///
953/// ```c
954/// void *xmlMallocLoc(size_t size, const char *file, int line);
955/// ```
956///
957/// The default candidate allocator does not track allocation sites (see
958/// residual R-000131); the location arguments are accepted for ABI
959/// compatibility and ignored.
960///
961/// # SAFETY
962///
963///
964/// - `file` must point to valid NUL-terminated
965/// strings (or NULL where the C contract allows) for the lifetime
966/// of the call.
967///
968/// The caller must not race this call with concurrent mutation of the
969/// same objects from other threads (per-object state is not internally
970/// synchronized). Violating any of the above is undefined behavior.
971///
972/// Exercised by the C-API differential courts
973/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
974/// courts; those pass byte-for-byte against the upstream oracle.
975#[no_mangle]
976pub unsafe extern "C" fn xmlMallocLoc(
977 size: usize,
978 file: *const c_char,
979 line: c_int,
980) -> *mut c_void {
981 let ptr = unsafe { xmlMallocImpl(size) };
982 if !ptr.is_null() {
983 unsafe { block_record(ptr, size, file, line) };
984 }
985 ptr
986}
987
988/// Allocate zeroed memory, recording the allocation site (upstream xmlmemory.h).
989///
990/// ```c
991/// void *xmlMallocAtomicLoc(size_t size, const char *file, int line);
992/// ```
993///
994/// # SAFETY
995///
996///
997/// - `file` must point to valid NUL-terminated
998/// strings (or NULL where the C contract allows) for the lifetime
999/// of the call.
1000///
1001/// The caller must not race this call with concurrent mutation of the
1002/// same objects from other threads (per-object state is not internally
1003/// synchronized). Violating any of the above is undefined behavior.
1004///
1005/// Exercised by the C-API differential courts
1006/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1007/// courts; those pass byte-for-byte against the upstream oracle.
1008#[no_mangle]
1009pub unsafe extern "C" fn xmlMallocAtomicLoc(
1010 size: usize,
1011 file: *const c_char,
1012 line: c_int,
1013) -> *mut c_void {
1014 let ptr = unsafe { xmlMallocZero(size) };
1015 if !ptr.is_null() {
1016 unsafe { block_record(ptr, size, file, line) };
1017 }
1018 ptr
1019}
1020
1021/// Reallocate memory, recording the allocation site (upstream xmlmemory.h).
1022///
1023/// ```c
1024/// void *xmlReallocLoc(void *ptr, size_t size, const char *file, int line);
1025/// ```
1026///
1027/// # SAFETY
1028///
1029/// - `ptr` must be valid pointers (or NULL
1030/// where the upstream C contract allows), obtained from the
1031/// matching constructor/owner and not yet freed; the callee may
1032/// take or keep ownership exactly as the C API specifies.
1033///
1034/// - `file` must point to valid NUL-terminated
1035/// strings (or NULL where the C contract allows) for the lifetime
1036/// of the call.
1037///
1038/// The caller must not race this call with concurrent mutation of the
1039/// same objects from other threads (per-object state is not internally
1040/// synchronized). Violating any of the above is undefined behavior.
1041///
1042/// Exercised by the C-API differential courts
1043/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1044/// courts; those pass byte-for-byte against the upstream oracle.
1045#[no_mangle]
1046pub unsafe extern "C" fn xmlReallocLoc(
1047 ptr: *mut c_void,
1048 size: usize,
1049 file: *const c_char,
1050 line: c_int,
1051) -> *mut c_void {
1052 let new_ptr = unsafe { xmlReallocImpl(ptr, size) };
1053 if !new_ptr.is_null() {
1054 unsafe { block_record(new_ptr, size, file, line) };
1055 } else if !ptr.is_null() {
1056 // Keep the old site on failure.
1057 let meta = BLOCKS.lock().get(&(ptr as usize)).copied();
1058 if let Some(m) = meta {
1059 unsafe { block_record(ptr, m.size, m.file as *const c_char, m.line) };
1060 }
1061 }
1062 new_ptr
1063}
1064
1065/// Duplicate a string, recording the allocation site (upstream xmlmemory.h).
1066///
1067/// ```c
1068/// void *xmlMemStrdupLoc(const char *str, const char *file, int line);
1069/// ```
1070///
1071/// # SAFETY
1072///
1073///
1074/// - `str`, `file` must point to valid NUL-terminated
1075/// strings (or NULL where the C contract allows) for the lifetime
1076/// of the call.
1077///
1078/// The caller must not race this call with concurrent mutation of the
1079/// same objects from other threads (per-object state is not internally
1080/// synchronized). Violating any of the above is undefined behavior.
1081///
1082/// Exercised by the C-API differential courts
1083/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1084/// courts; those pass byte-for-byte against the upstream oracle.
1085#[no_mangle]
1086pub unsafe extern "C" fn xmlMemStrdupLoc(
1087 str: *const c_char,
1088 file: *const c_char,
1089 line: c_int,
1090) -> *mut c_void {
1091 let ptr = unsafe { xmlMemStrdupImpl(str) };
1092 if !ptr.is_null() {
1093 let len = unsafe { libc::strlen(str) } + 1;
1094 unsafe { block_record(ptr, len, file, line) };
1095 }
1096 ptr
1097}
1098
1099/// Return the size of an allocated block (upstream xmlmemory.h).
1100///
1101/// ```c
1102/// size_t xmlMemSize(void *ptr);
1103/// ```
1104///
1105/// Returns the recorded size from the block registry (0 for unknown or
1106/// foreign pointers, matching upstream's lookup-miss behavior).
1107///
1108/// # SAFETY
1109///
1110/// - `ptr` must be valid pointers (or NULL
1111/// where the upstream C contract allows), obtained from the
1112/// matching constructor/owner and not yet freed; the callee may
1113/// take or keep ownership exactly as the C API specifies.
1114///
1115/// The caller must not race this call with concurrent mutation of the
1116/// same objects from other threads (per-object state is not internally
1117/// synchronized). Violating any of the above is undefined behavior.
1118///
1119/// Exercised by the C-API differential courts
1120/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1121/// courts; those pass byte-for-byte against the upstream oracle.
1122#[no_mangle]
1123pub unsafe extern "C" fn xmlMemSize(ptr: *mut c_void) -> usize {
1124 if ptr.is_null() {
1125 return 0;
1126 }
1127 BLOCKS
1128 .lock()
1129 .get(&(ptr as usize))
1130 .map(|m| m.size)
1131 .unwrap_or(0)
1132}
1133
1134/// Display a limited amount of memory debug information (upstream xmlmemory.h).
1135///
1136/// ```c
1137/// void xmlMemDisplayLast(FILE *fp, long nbBytes);
1138/// ```
1139///
1140/// Dumps the per-block registry (upstream xmlMemDisplayLast block listing):
1141/// one line per live block with its address, size and recorded allocation
1142/// site, bounded by `nb_bytes` when positive. The aggregate footer matches
1143/// upstream's counters.
1144///
1145/// # SAFETY
1146///
1147/// - `fp` must be valid pointers (or NULL
1148/// where the upstream C contract allows), obtained from the
1149/// matching constructor/owner and not yet freed; the callee may
1150/// take or keep ownership exactly as the C API specifies.
1151///
1152/// The caller must not race this call with concurrent mutation of the
1153/// same objects from other threads (per-object state is not internally
1154/// synchronized). Violating any of the above is undefined behavior.
1155///
1156/// Exercised by the C-API differential courts
1157/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1158/// courts; those pass byte-for-byte against the upstream oracle.
1159#[no_mangle]
1160pub unsafe extern "C" fn xmlMemDisplayLast(fp: *mut c_void, nb_bytes: c_long) {
1161 // SAFETY: fp must be a valid FILE* or NULL (stderr used).
1162 unsafe {
1163 let out = if fp.is_null() {
1164 libc::fdopen(2, b"w\0" as *const u8 as *const c_char) as *mut c_void
1165 } else {
1166 fp
1167 };
1168 if out.is_null() {
1169 return;
1170 }
1171 let mut total: usize = 0;
1172 let mut msg = String::new();
1173 msg.push_str("MEMORY ALLOCATED : 0, MAX : 0, BLOCKS : ");
1174 let blocks = MEM_BLOCKS.load(Ordering::Relaxed);
1175 msg.push_str(&blocks.to_string());
1176 msg.push('\n');
1177 let map = BLOCKS.lock();
1178 let mut entries: Vec<(usize, &BlockMeta)> = map.iter().map(|(k, v)| (*k, v)).collect();
1179 entries.sort_by_key(|(k, _)| *k);
1180 for (addr, meta) in entries {
1181 if nb_bytes > 0 && (total as c_long) >= nb_bytes {
1182 break;
1183 }
1184 total += meta.size;
1185 msg.push_str(&format!(
1186 " {:018p} : {:>7} bytes",
1187 addr as *const c_void, meta.size
1188 ));
1189 if meta.file != 0 {
1190 let file = CStr::from_ptr(meta.file as *const c_char).to_string_lossy();
1191 msg.push_str(&format!(" @ {}:{}", file, meta.line));
1192 }
1193 msg.push('\n');
1194 }
1195 drop(map);
1196 let used = MEM_USED.load(Ordering::Relaxed);
1197 msg.push_str(&format!(
1198 "TOTAL MEMORY ALLOCATED : {} bytes, TOTAL BLOCKS : {}\n",
1199 used, blocks
1200 ));
1201 let bytes = msg.as_bytes();
1202 libc::fwrite(
1203 bytes.as_ptr() as *const c_void,
1204 1,
1205 bytes.len(),
1206 out as *mut libc::FILE,
1207 );
1208 }
1209}
1210
1211/// Dump memory allocation statistics (upstream xmlmemory.h).
1212///
1213/// ```c
1214/// int xmlMemoryDump(void);
1215/// ```
1216///
1217/// Prints the global counters to stderr and returns 0 (no leak detector is
1218/// active in the default allocator).
1219///
1220/// # SAFETY
1221///
1222/// The function touches crate-global state only; it is safe
1223/// as long as the caller respects the library's global
1224/// initialization/cleanup ordering (xmlInitParser before use,
1225/// xmlCleanupParser only after all users are done).
1226///
1227/// Violating the global lifecycle ordering, or calling this after
1228/// teardown or from a signal handler, is undefined behavior.
1229#[no_mangle]
1230pub unsafe extern "C" fn xmlMemoryDump() -> c_int {
1231 unsafe {
1232 xmlMemDisplayLast(ptr::null_mut(), -1);
1233 }
1234 0
1235}
1236
1237// ═══════════════════════════════════════════════════════════════════════════════
1238// Tests
1239// ═══════════════════════════════════════════════════════════════════════════════
1240
1241#[cfg(test)]
1242mod tests {
1243 use super::*;
1244 use core::ptr;
1245
1246 /// Test basic allocation and deallocation.
1247 #[test]
1248 fn test_malloc_free() {
1249 unsafe {
1250 let ptr = xmlMalloc(100);
1251 assert!(!ptr.is_null(), "xmlMalloc(100) returned NULL");
1252 xmlFree(ptr);
1253 }
1254 }
1255
1256 /// Test that xmlMalloc(0) returns a valid pointer.
1257 #[test]
1258 fn test_malloc_zero() {
1259 unsafe {
1260 let ptr = xmlMalloc(0);
1261 // malloc(0) may return NULL or a valid pointer.
1262 // If non-NULL, it must be freeable.
1263 if !ptr.is_null() {
1264 xmlFree(ptr);
1265 }
1266 }
1267 }
1268
1269 /// Test xmlFree(NULL) is a no-op.
1270 #[test]
1271 fn test_free_null() {
1272 unsafe {
1273 xmlFree(ptr::null_mut());
1274 // Should not crash
1275 }
1276 }
1277
1278 /// Test xmlRealloc.
1279 #[test]
1280 fn test_realloc() {
1281 unsafe {
1282 let ptr = xmlMalloc(50);
1283 assert!(!ptr.is_null());
1284 let new_ptr = xmlRealloc(ptr, 100);
1285 assert!(!new_ptr.is_null());
1286 xmlFree(new_ptr);
1287 }
1288 }
1289
1290 /// Test xmlMemStrdup.
1291 #[test]
1292 fn test_mem_strdup() {
1293 unsafe {
1294 let s = b"hello\0" as *const u8 as *const c_char;
1295 let dup = xmlMemStrdup(s);
1296 assert!(!dup.is_null());
1297 // Compare the strings
1298 let orig_slice = std::slice::from_raw_parts(s as *const u8, 6);
1299 let dup_slice = std::slice::from_raw_parts(dup as *const u8, 6);
1300 assert_eq!(orig_slice, dup_slice);
1301 xmlFree(dup);
1302 }
1303 }
1304
1305 /// Test xmlMallocZero returns zeroed memory.
1306 #[test]
1307 fn test_malloc_zero_init() {
1308 unsafe {
1309 let ptr = xmlMallocZero(100) as *mut u8;
1310 assert!(!ptr.is_null());
1311 let slice = std::slice::from_raw_parts(ptr, 100);
1312 assert!(slice.iter().all(|&b| b == 0));
1313 xmlFree(ptr as *mut c_void);
1314 }
1315 }
1316
1317 /// Test custom allocator setup/get.
1318 #[test]
1319 fn test_mem_setup_get() {
1320 unsafe {
1321 let mut free_func: Option<xmlFreeFunc> = None;
1322 let mut malloc_func: Option<xmlMallocFunc> = None;
1323 let mut realloc_func: Option<xmlReallocFunc> = None;
1324 let mut strdup_func: Option<xmlStrdupFunc> = None;
1325
1326 xmlMemGet(
1327 &mut free_func as *mut _,
1328 &mut malloc_func as *mut _,
1329 &mut realloc_func as *mut _,
1330 &mut strdup_func as *mut _,
1331 );
1332
1333 assert!(malloc_func.is_some());
1334 assert!(free_func.is_some());
1335 assert!(realloc_func.is_some());
1336 assert!(strdup_func.is_some());
1337 }
1338 }
1339
1340 /// Test xmlMemUsed and xmlMemBlocks return reasonable values.
1341 #[test]
1342 fn test_mem_stats() {
1343 unsafe {
1344 let ptr = xmlMalloc(100);
1345 assert!(!ptr.is_null());
1346
1347 // The block registry records the exact size (deterministic,
1348 // unlike the process-wide counters which other test threads
1349 // mutate concurrently).
1350 assert_eq!(xmlMemSize(ptr), 100);
1351
1352 xmlFree(ptr);
1353 // Freed blocks leave the registry.
1354 assert_eq!(xmlMemSize(ptr), 0);
1355 }
1356 }
1357}