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