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). Since 11.1-Z.3 (R-000178) the
17//! default bodies are plain libc `malloc`/`realloc`/`free`/`strdup`
18//! wrappers — no Rust `std::alloc` layout fabrication (that was UB) and no
19//! accounting, byte-identical with upstream's `globals.c` defaults
20//! (`xmlMalloc = malloc` etc.).
21//!
22//! # Safety
23//!
24//! Allocator hooks are `unsafe` because they operate on raw pointers and are called
25//! from C code. Every public function documents its safety contract.
26//!
27//! # Upstream contract
28//!
29//! The parity target is libxml2 2.15.3 (`SRC-LIBXML2-2.15.0-XMLMEMORY-C`:
30//! `oracle/historical/src/libxml2-2.15.0/xmlmemory.c`) plus the allocator globals
31//! of `globals.c`. The 5 allocator entry points (`xmlMalloc`, `xmlMallocAtomic`,
32//! `xmlRealloc`, `xmlFree`, `xmlMemStrdup`) are exported as DATA function-pointer
33//! globals matching the upstream `XMLPUBVAR` declarations (R-000162). Upstream
34//! initializes them to the C runtime functions (`xmlFree = free`, `xmlMalloc =
35//! malloc`, `xmlMallocAtomic = malloc`, `xmlRealloc = realloc`, `xmlMemStrdup =
36//! xmlPosixStrdup`); the candidate initializes them to the `*Default` bodies,
37//! which are libc wrappers with identical observable behavior.
38//!
39//! # Conceptual behavior
40//!
41//! This module implements the complete libxml2 memory-hook system: swappable
42//! allocator hooks via `xmlMemSetup`/`xmlMemGet` (and the GC aliases), plus the
43//! deprecated debug-named surface (`xmlMemMalloc`/`xmlMemFree`/`xmlMemRealloc`/
44//! `xmlMemoryStrdup` and the `*Loc` variants) which upstream keeps as a
45//! separately-tagged debug allocator. There are therefore two allocation
46//! planes, exactly as in upstream 2.15.0:
47//!
48//! - the five exported variables (the hook system): default = libc, and
49//! `xmlMemSetup`/direct assignment re-route them. Untracked — upstream's
50//! `debugMemSize`/`debugMemBlocks` counters are only maintained by the
51//! debug allocator, so with the default installed `xmlMemUsed()` == 0,
52//! `xmlMemBlocks()` == 0 and `xmlMemSize()` == 0 (verified against the
53//! oracle);
54//! - the debug-named surface (deprecated, exported for legacy consumers):
55//! always libc-backed and tracked by the per-block registry, mirroring
56//! upstream `xmlMemMalloc` et al. `xmlMemSize` returns the recorded size
57//! for these blocks and `xmlMemUsed`/`xmlMemBlocks` count them.
58//!
59//! The display entry points (`xmlMemDisplay`, `xmlMemDisplayLast`, `xmlMemShow`,
60//! `xmlMemoryDump`) are no-ops matching upstream 2.15.0, which removed that
61//! feature.
62//!
63//! # Ownership & safety invariants
64//!
65//! Every pointer returned by an xml* allocator must be freed with `xmlFree`
66//! (OWNERSHIP_ATLAS section 1). The block registry records
67//! ptr -> (size, file, line) for the debug-named surface only, so
68//! `xmlMemSize` is exact for debug-surface blocks and 0 for default-allocator
69//! blocks (upstream's MEMHDR tag lookup behaves identically: a plain `malloc`
70//! block carries no tag). `xmlMemSetup` custom allocators bypass the registry
71//! entirely, matching upstream's debug-allocator-only contract.
72//!
73//! # Historical quirks & epochs
74//!
75//! R-000178 (11.1-Z.3): the pre-Z.3 default allocator routed through Rust's
76//! global allocator with fabricated `Layout`s — `default_free` deallocated
77//! every pointer with a 1-byte layout and `default_realloc` passed the
78//! requested new size as the old allocation layout; both are invalid-layout
79//! UB under the Rust allocator contract. Replaced with libc
80//! `malloc`/`realloc`/`free` (C allocation semantics; no layout exists), and
81//! the default no longer maintains the accounting registry so `xmlMemUsed`/
82//! `xmlMemBlocks`/`xmlMemSize` match the oracle's 0s; the registry now backs
83//! only the debug-named surface. R-000131 (11.1-J) sealed: `xmlMemSize`
84//! returns the recorded size for debug-surface blocks, the `*Loc` variants
85//! accept-and-ignore file/line exactly like upstream 2.15.0's `ATTRIBUTE_UNUSED`
86//! parameters, and the display functions are upstream-faithful no-ops.
87//! R-000133 (11.1-H): the legacy names
88//! (`xmlMemMalloc`/`xmlMemFree`/`xmlMemRealloc`/`xmlMemoryStrdup`) were
89//! declared-but-unexported and had to be implemented for the honest-header
90//! rule.
91//!
92//! # Deliberate oddities
93//!
94//! `xmlMemSetup`/direct variable assignment bypass the accounting registry
95//! (deliberate: upstream's block table exists only in the debug allocator).
96//! The five exported variables (`xmlMalloc`, `xmlMallocAtomic`, `xmlRealloc`,
97//! `xmlFree`, `xmlMemStrdup`) are the single source of truth (R-000176,
98//! 11.1-Z.2): `xmlMemSetup` assigns them and every internal allocation reads
99//! them through the `*Impl` indirection, exactly like upstream internal
100//! `xmlMalloc(...)` calls. The debug-named functions deliberately do NOT
101//! route through the variables (upstream's debug allocator is independent of
102//! the hooks); they are always libc-backed + registry-tracked.
103//!
104//! # Proving courts
105//!
106//! ABI-DATA, ALLOCATOR, ALLOCATOR-DEFAULT, GLOBAL-STATE and THREADING court
107//! families; the allocator probes (`tools/abi/*_probe.py` +
108//! `courts/suites/data-abi/*`) compile the same C probe against the oracle DSO
109//! and the candidate and require byte-identical output; ALLOCATOR-DEFAULT-001
110//! proves the default-allocator contract (many sizes, zero-size, grow/shrink
111//! realloc, realloc-to-zero, realloc/malloc failure, strdup, direct
112//! exported-variable calls, long churn, `xmlMemSize`/`xmlMemUsed`/`xmlMemBlocks`
113//! exactness — all byte-identical with the oracle, R-000178); the DSO-LOADER
114//! court resolves every exported symbol from the built DSO.
115//!
116//! # Tempting simplifications that would break parity
117//!
118//! A tempting simplification is to keep the pre-Z.3 default allocator — Rust
119//! `std::alloc` with fabricated layouts is invalid-layout UB (R-000178) and
120//! returning nonzero `xmlMemUsed`/`xmlMemBlocks` under the default diverges
121//! from the oracle's 0s. Another tempting shortcut is exporting the allocator
122//! entry points as plain functions — upstream exports them as data function
123//! pointers, so the allocator-override mechanism (`xmlMalloc` = custom) could
124//! not link (R-000162 lesson).
125
126use core::ffi::c_void;
127use core::ptr;
128use core::sync::atomic::AtomicUsize;
129use core::sync::atomic::Ordering;
130use std::os::raw::{c_char, c_int, c_long};
131
132use crate::abi::callbacks::*;
133
134// ═══════════════════════════════════════════════════════════════════════════════
135// Default Allocator (libc — upstream 2.15.0 globals.c defaults, R-000178)
136// ═══════════════════════════════════════════════════════════════════════════════
137//
138// Upstream initializes the five exported allocator variables to the C runtime
139// functions (`xmlFree = free`, `xmlMalloc = malloc`, `xmlMallocAtomic = malloc`,
140// `xmlRealloc = realloc`, `xmlMemStrdup = xmlPosixStrdup`). The candidate's
141// `*Default` bodies are libc wrappers with identical observable behavior:
142//
143// - malloc(0) -> glibc returns a unique non-NULL pointer (C semantics);
144// - realloc(p, 0) -> glibc frees p and returns NULL (C semantics);
145// - realloc(NULL,n)-> malloc(n) (C semantics);
146// - realloc failure-> NULL with the old block left intact (C semantics);
147// - free(NULL) -> no-op (C semantics).
148//
149// All of these are byte-identical with the oracle (verified by the
150// ALLOCATOR-DEFAULT-001 differential court). The pre-Z.3 implementation used
151// Rust's `std::alloc` with fabricated `Layout`s: `default_free` deallocated
152// every pointer with a 1-byte layout and `default_realloc` passed the
153// requested NEW size as the OLD allocation layout. Rust's allocator API
154// requires the deallocation/reallocation layout to correspond to the original
155// allocation, so both were invalid-layout UB — the defect R-000178. libc
156// allocation has no layout parameter, so the C contract is reproduced exactly
157// and the UB class is eliminated.
158
159/// Default malloc: `libc::malloc`.
160///
161/// # SAFETY
162///
163/// - `size` must be a valid allocation size (0 is handled by the platform
164/// `malloc` contract, matching upstream which calls `malloc` directly)
165/// - Returns NULL on allocation failure
166unsafe extern "C" fn default_malloc(size: usize) -> *mut c_void {
167 unsafe { libc::malloc(size) }
168}
169
170/// Default realloc: `libc::realloc`.
171///
172/// # SAFETY
173///
174/// - `ptr` must be a valid pointer from a previous `default_malloc` or `default_realloc`,
175/// or NULL (in which case this behaves like malloc)
176/// - `size` must be a valid allocation size; `realloc(p, 0)` follows the C
177/// contract (glibc frees `p` and returns NULL), matching upstream which
178/// calls `realloc` directly
179/// - On failure the old block is left intact (C contract)
180unsafe extern "C" fn default_realloc(ptr: *mut c_void, size: usize) -> *mut c_void {
181 unsafe { libc::realloc(ptr, size) }
182}
183
184/// Default free: `libc::free`.
185///
186/// # SAFETY
187///
188/// - `ptr` may be NULL (free(NULL) is a no-op)
189/// - If non-NULL, `ptr` must be from a previous `default_malloc` or `default_realloc`
190unsafe extern "C" fn default_free(ptr: *mut c_void) {
191 if ptr.is_null() {
192 return;
193 }
194 unsafe { libc::free(ptr) };
195}
196
197/// Default strdup: `libc::malloc` + copy (upstream `xmlPosixStrdup` ->
198/// `xmlCharStrdup`, which is a NULL-checked malloc+copy).
199///
200/// # SAFETY
201///
202/// - `str` must be a valid null-terminated C string or NULL (NULL returns NULL,
203/// matching upstream `xmlCharStrdup`)
204unsafe extern "C" fn default_strdup(str: *const c_char) -> *mut c_void {
205 if str.is_null() {
206 return ptr::null_mut();
207 }
208 let len = unsafe { libc::strlen(str) };
209 let size = len + 1; // include null terminator
210 let new_ptr = unsafe { libc::malloc(size) };
211 if new_ptr.is_null() {
212 return ptr::null_mut();
213 }
214 unsafe { ptr::copy_nonoverlapping(str as *const u8, new_ptr as *mut u8, size) };
215 new_ptr
216}
217
218// ═══════════════════════════════════════════════════════════════════════════════
219// Global Allocator State — single source of truth (11.1-Z.2, R-000176)
220// ═══════════════════════════════════════════════════════════════════════════════
221//
222// Upstream (xmlmemory.c xmlMemSetup/xmlMemGet, globals.c) keeps exactly ONE
223// allocator state: the five exported DATA variables `xmlFree`, `xmlMalloc`,
224// `xmlMallocAtomic`, `xmlRealloc`, `xmlMemStrdup`. `xmlMemSetup` assigns them,
225// `xmlMemGet` reads them, and EVERY internal allocation calls through the
226// variables — so assigning `xmlMalloc = custom;` directly is equivalent to
227// `xmlMemSetup(...)`. The candidate previously kept a separate `ALLOCATOR`
228// RwLock consulted by the `*Impl` bodies, so a direct public-variable
229// assignment changed the exported symbol but not internal allocations, and
230// `xmlMemSetup` changed internal allocations but not the exported symbols:
231// two sources of truth. This was the xmlGcMemSetup-class defect R-000176
232// (11.1-Z.2). The merged model below restores the upstream single source:
233//
234// - the five exported `static mut` fn-pointer variables ARE the state;
235// - the `*Default` functions are the initial values (Rust global allocator
236// + the accounting registry) and never read the variables;
237// - the `*Impl` functions are the indirection every internal call site
238// uses: they read the current variable, so custom hooks installed via
239// `xmlMemSetup` OR direct assignment are observed everywhere.
240//
241// The write/read contract matches upstream: `xmlMemSetup`/assignment must
242// happen before concurrent use (upstream: "This has to be called before any
243// other libxml routines !"). Rust `static mut` access is `unsafe` and the
244// crate upholds the upstream single-threaded-setup ordering.
245
246/// Global allocation counters (for xmlMemUsed/xmlMemBlocks).
247///
248/// These use relaxed ordering since they are approximate debugging counters.
249/// Since 11.1-Z.3 (R-000178) they are maintained ONLY by the debug-named
250/// surface (`xmlMemMalloc`/`xmlMemFree`/`xmlMemRealloc`/`xmlMemoryStrdup` and
251/// the `*Loc` variants) — exactly like upstream's `debugMemSize`/
252/// `debugMemBlocks`, which the plain-malloc default never touches. With the
253/// default allocator installed, `xmlMemUsed()` and `xmlMemBlocks()` return 0
254/// (byte-identical with the oracle).
255static MEM_USED: AtomicUsize = AtomicUsize::new(0);
256static MEM_BLOCKS: AtomicUsize = AtomicUsize::new(0);
257
258/// Per-block metadata (upstream xmlmemory.c block table): enables
259/// xmlMemSize and the counters for the debug-named surface.
260/// The file pointer is stored as a raw address (usize) so the registry stays
261/// Send + Sync. The `file`/`line` fields mirror upstream's allocation-site
262/// record (populated by the `*Loc` variants); they are write-only until a
263/// future dump surface reads them, hence `allow(dead_code)`.
264#[derive(Clone, Copy)]
265#[allow(dead_code)]
266struct BlockMeta {
267 size: usize,
268 file: usize,
269 line: c_int,
270}
271
272static BLOCKS: once_cell::sync::Lazy<
273 parking_lot::Mutex<std::collections::HashMap<usize, BlockMeta>>,
274> = once_cell::sync::Lazy::new(|| parking_lot::Mutex::new(std::collections::HashMap::new()));
275
276/// Record a block in the registry (no-op for NULL).
277unsafe fn block_record(ptr: *mut c_void, size: usize, file: *const c_char, line: c_int) {
278 if ptr.is_null() {
279 return;
280 }
281 BLOCKS.lock().insert(
282 ptr as usize,
283 BlockMeta {
284 size,
285 file: file as usize,
286 line,
287 },
288 );
289}
290
291/// Drop a block from the registry; returns its recorded size (None if the
292/// block was unknown or NULL).
293unsafe fn block_forget(ptr: *mut c_void) -> Option<usize> {
294 if ptr.is_null() {
295 return None;
296 }
297 BLOCKS.lock().remove(&(ptr as usize)).map(|m| m.size)
298}
299
300// ═══════════════════════════════════════════════════════════════════════════════
301// Public Allocator API
302// ═══════════════════════════════════════════════════════════════════════════════
303
304/// Set custom memory allocator functions (upstream xmlmemory.h).
305///
306/// # UPSTREAM-PARITY
307///
308/// ```c
309/// int xmlMemSetup(xmlFreeFunc freeFunc,
310/// xmlMallocFunc mallocFunc,
311/// xmlReallocFunc reallocFunc,
312/// xmlStrdupFunc strdupFunc);
313/// ```
314///
315/// Returns -1 if any function is NULL (upstream xmlmemory.c: "Returns 0 on
316/// success"); otherwise assigns the five exported allocator variables
317/// (xmlFree, xmlMalloc, xmlMallocAtomic = mallocFunc, xmlRealloc,
318/// xmlMemStrdup) and returns 0. The exported variables are the single source
319/// of truth: every internal allocation reads them through the `*Impl`
320/// indirection, so this call — or a direct `xmlMalloc = custom` assignment —
321/// re-routes all allocations immediately (R-000176 fix).
322///
323/// # SAFETY
324///
325/// - All function pointers must be valid (non-null) and thread-safe
326/// - The functions must follow the C malloc/realloc/free/strdup contract
327/// - Once set, the functions remain in effect until the next `xmlMemSetup` call
328/// - The caller is responsible for ensuring the functions remain valid for the
329/// entire time they are installed
330/// - Must not race with concurrent allocation (upstream ordering contract:
331/// "This has to be called before any other libxml routines !")
332#[no_mangle]
333pub unsafe extern "C" fn xmlMemSetup(
334 freeFunc: Option<xmlFreeFunc>,
335 mallocFunc: Option<xmlMallocFunc>,
336 reallocFunc: Option<xmlReallocFunc>,
337 strdupFunc: Option<xmlStrdupFunc>,
338) -> c_int {
339 if freeFunc.is_none() || mallocFunc.is_none() || reallocFunc.is_none() || strdupFunc.is_none() {
340 return -1;
341 }
342 // SAFETY: callers must uphold the upstream single-threaded-setup
343 // ordering; each value is a non-NULL C function pointer (checked above).
344 unsafe {
345 xmlFree = freeFunc.unwrap();
346 xmlMalloc = mallocFunc.unwrap();
347 xmlMallocAtomic = mallocFunc.unwrap();
348 xmlRealloc = reallocFunc.unwrap();
349 xmlMemStrdup = strdupFunc.unwrap();
350 }
351 0
352}
353
354/// Get the current memory allocator functions (upstream xmlmemory.h).
355///
356/// # UPSTREAM-PARITY
357///
358/// ```c
359/// int xmlMemGet(xmlFreeFunc *freeFunc,
360/// xmlMallocFunc *mallocFunc,
361/// xmlReallocFunc *reallocFunc,
362/// xmlStrdupFunc *strdupFunc);
363/// ```
364///
365/// Writes the current exported allocator variables through NULL-tolerant
366/// output pointers (upstream xmlmemory.c: NULL outputs are skipped) and
367/// returns 0.
368///
369/// # SAFETY
370///
371/// - All non-NULL output pointers must be valid and writable
372#[no_mangle]
373pub unsafe extern "C" fn xmlMemGet(
374 freeFunc: *mut Option<xmlFreeFunc>,
375 mallocFunc: *mut Option<xmlMallocFunc>,
376 reallocFunc: *mut Option<xmlReallocFunc>,
377 strdupFunc: *mut Option<xmlStrdupFunc>,
378) -> c_int {
379 // SAFETY: callers must pass NULL or valid writable pointers; reads of
380 // the exported variables are safe under the upstream setup ordering.
381 unsafe {
382 if !freeFunc.is_null() {
383 ptr::write(freeFunc, Some(xmlFree));
384 }
385 if !mallocFunc.is_null() {
386 ptr::write(mallocFunc, Some(xmlMalloc));
387 }
388 if !reallocFunc.is_null() {
389 ptr::write(reallocFunc, Some(xmlRealloc));
390 }
391 if !strdupFunc.is_null() {
392 ptr::write(strdupFunc, Some(xmlMemStrdup));
393 }
394 }
395 0
396}
397
398/// Set GC-aware memory allocator functions (upstream xmlmemory.h).
399///
400/// # UPSTREAM-PARITY
401///
402/// ```c
403/// int xmlGcMemSetup(xmlFreeFunc freeFunc,
404/// xmlMallocFunc mallocFunc,
405/// xmlMallocFunc mallocAtomicFunc,
406/// xmlReallocFunc reallocFunc,
407/// xmlStrdupFunc strdupFunc);
408/// ```
409///
410/// Same contract as `xmlMemSetup` with a dedicated `mallocAtomicFunc` for
411/// atomic allocations (upstream xmlmemory.c: `xmlMallocAtomic =
412/// mallocAtomicFunc`). Returns -1 if any function is NULL, else 0.
413///
414/// # SAFETY
415///
416/// - All function pointers must be valid (non-null) and thread-safe
417/// - The functions must follow the C malloc/realloc/free/strdup contract
418/// - Must not race with concurrent allocation (upstream ordering contract)
419/// - The caller is responsible for ensuring the functions remain valid for
420/// the entire time they are installed
421#[no_mangle]
422pub unsafe extern "C" fn xmlGcMemSetup(
423 freeFunc: Option<xmlFreeFunc>,
424 mallocFunc: Option<xmlMallocFunc>,
425 mallocAtomicFunc: Option<xmlMallocFunc>,
426 reallocFunc: Option<xmlReallocFunc>,
427 strdupFunc: Option<xmlStrdupFunc>,
428) -> c_int {
429 if freeFunc.is_none()
430 || mallocFunc.is_none()
431 || mallocAtomicFunc.is_none()
432 || reallocFunc.is_none()
433 || strdupFunc.is_none()
434 {
435 return -1;
436 }
437 // SAFETY: callers must uphold the upstream single-threaded-setup
438 // ordering; each value is a non-NULL C function pointer (checked above).
439 unsafe {
440 xmlFree = freeFunc.unwrap();
441 xmlMalloc = mallocFunc.unwrap();
442 xmlMallocAtomic = mallocAtomicFunc.unwrap();
443 xmlRealloc = reallocFunc.unwrap();
444 xmlMemStrdup = strdupFunc.unwrap();
445 }
446 0
447}
448
449/// Get GC-aware memory allocator functions (upstream xmlmemory.h).
450///
451/// # UPSTREAM-PARITY
452///
453/// ```c
454/// int xmlGcMemGet(xmlFreeFunc *freeFunc,
455/// xmlMallocFunc *mallocFunc,
456/// xmlMallocFunc *mallocAtomicFunc,
457/// xmlReallocFunc *reallocFunc,
458/// xmlStrdupFunc *strdupFunc);
459/// ```
460///
461/// Same contract as `xmlMemGet` with the `mallocAtomicFunc` output.
462/// Writes through NULL-tolerant output pointers and returns 0.
463///
464/// # SAFETY
465///
466/// - All non-NULL output pointers must be valid and writable
467#[no_mangle]
468pub unsafe extern "C" fn xmlGcMemGet(
469 freeFunc: *mut Option<xmlFreeFunc>,
470 mallocFunc: *mut Option<xmlMallocFunc>,
471 mallocAtomicFunc: *mut Option<xmlMallocFunc>,
472 reallocFunc: *mut Option<xmlReallocFunc>,
473 strdupFunc: *mut Option<xmlStrdupFunc>,
474) -> c_int {
475 // SAFETY: callers must pass NULL or valid writable pointers.
476 unsafe {
477 if !freeFunc.is_null() {
478 ptr::write(freeFunc, Some(xmlFree));
479 }
480 if !mallocFunc.is_null() {
481 ptr::write(mallocFunc, Some(xmlMalloc));
482 }
483 if !mallocAtomicFunc.is_null() {
484 ptr::write(mallocAtomicFunc, Some(xmlMallocAtomic));
485 }
486 if !reallocFunc.is_null() {
487 ptr::write(reallocFunc, Some(xmlRealloc));
488 }
489 if !strdupFunc.is_null() {
490 ptr::write(strdupFunc, Some(xmlMemStrdup));
491 }
492 }
493 0
494}
495
496// ═══════════════════════════════════════════════════════════════════════════════
497// Allocation Functions
498// ═══════════════════════════════════════════════════════════════════════════════
499
500/// Allocate memory through the exported `xmlMalloc` variable.
501///
502/// # UPSTREAM-PARITY
503///
504/// ```c
505/// void *xmlMalloc(size_t size);
506/// ```
507///
508/// Every internal allocation site calls this indirection, which reads the
509/// current exported `xmlMalloc` variable — exactly how upstream internal
510/// code calls `xmlMalloc(...)` (the variable, globals.c). With no override
511/// the variable holds `xmlMallocDefault`; after `xmlMemSetup` or a direct
512/// `xmlMalloc = custom` assignment it holds the custom hook, so all internal
513/// allocations observe the override (R-000176 fix, single source of truth).
514///
515/// # SAFETY
516///
517/// - The returned pointer must be freed with `xmlFree`
518/// - `size` may be 0 (returns a valid non-NULL pointer or NULL)
519pub unsafe extern "C" fn xmlMallocImpl(size: usize) -> *mut c_void {
520 // SAFETY: reading the exported static mut is safe under the upstream
521 // setup-ordering contract; the stored value is a valid C fn pointer. The
522 // slot is the process-visible (core DSO's) one when it resolves, so
523 // whole-archive facades allocate through the same hooks as the core
524 // (R-000177 allocator coherence).
525 let hook = allocator_slot(foreign_malloc_slot(), unsafe { xmlMalloc });
526 unsafe { hook(size) }
527}
528
529/// Default `xmlMalloc` body: plain `libc::malloc` (upstream default `malloc`).
530///
531/// This is the initial value of the exported `xmlMalloc` variable and never
532/// reads the variable (no recursion). Since 11.1-Z.3 (R-000178) it performs
533/// NO accounting: upstream's counters are maintained only by the debug
534/// allocator, so with the default installed `xmlMemUsed`/`xmlMemBlocks` are 0
535/// and `xmlMemSize` is 0 — byte-identical with the oracle.
536///
537/// # Safety
538///
539/// - `size` is a byte count passed straight to `libc::malloc`; 0 is handled
540/// by the platform contract (glibc returns a unique non-NULL pointer).
541/// - The returned pointer (NULL on failure) is a libc-owned allocation that
542/// must be freed exactly once with the matching free path and never
543/// dereferenced after freeing.
544/// - This wrapper never reads the exported allocator variables, so it cannot
545/// recurse and is safe to install as a hook.
546unsafe extern "C" fn xmlMallocDefault(size: usize) -> *mut c_void {
547 unsafe { default_malloc(size) }
548}
549
550/// Allocate through the exported `xmlMallocAtomic` variable.
551///
552/// # UPSTREAM-PARITY
553///
554/// ```c
555/// void *xmlMallocAtomic(size_t size);
556/// ```
557///
558/// Identical to `xmlMalloc` but hints to the GC that the memory does not
559/// contain pointers. In modern libxml2 this is equivalent to `xmlMalloc`;
560/// `xmlMemSetup` aliases it to the same hook (upstream xmlmemory.c).
561///
562/// # SAFETY
563///
564/// - The returned pointer must be freed with `xmlFree`
565/// - `size` may be 0 (returns a valid non-NULL pointer or NULL)
566pub unsafe extern "C" fn xmlMallocAtomicImpl(size: usize) -> *mut c_void {
567 // SAFETY: see xmlMallocImpl.
568 let hook = allocator_slot(foreign_malloc_atomic_slot(), unsafe { xmlMallocAtomic });
569 unsafe { hook(size) }
570}
571
572/// Default `xmlMallocAtomic` body (initial exported-variable value).
573///
574/// Atomic allocations share the malloc accounting body; `xmlGcMemSetup`
575/// installs a dedicated atomic hook via the variable (upstream xmlmemory.c).
576unsafe extern "C" fn xmlMallocAtomicDefault(size: usize) -> *mut c_void {
577 unsafe { xmlMallocDefault(size) }
578}
579
580/// Reallocate through the exported `xmlRealloc` variable.
581///
582/// # UPSTREAM-PARITY
583///
584/// ```c
585/// void *xmlRealloc(void *ptr, size_t size);
586/// ```
587///
588/// Changes the size of the memory block pointed to by `ptr`.
589/// If `ptr` is NULL, behaves like `xmlMalloc`.
590/// If `size` is 0, may return NULL (like C realloc).
591///
592/// # SAFETY
593///
594/// - `ptr` must be a valid pointer from `xmlMalloc`, `xmlMallocAtomic`, or `xmlRealloc`,
595/// or NULL
596/// - The returned pointer must be freed with `xmlFree`
597pub unsafe extern "C" fn xmlReallocImpl(ptr: *mut c_void, size: usize) -> *mut c_void {
598 // SAFETY: see xmlMallocImpl.
599 let hook = allocator_slot(foreign_realloc_slot(), unsafe { xmlRealloc });
600 unsafe { hook(ptr, size) }
601}
602
603/// Default `xmlRealloc` body: plain `libc::realloc` (upstream default `realloc`).
604///
605/// No accounting (R-000178) — see `xmlMallocDefault`. C semantics: `realloc(NULL,
606/// n)` allocates, `realloc(p, 0)` follows the platform contract (glibc frees and
607/// returns NULL), failure leaves the old block intact.
608unsafe extern "C" fn xmlReallocDefault(ptr: *mut c_void, size: usize) -> *mut c_void {
609 unsafe { default_realloc(ptr, size) }
610}
611
612/// Free through the exported `xmlFree` variable.
613///
614/// # UPSTREAM-PARITY
615///
616/// ```c
617/// void xmlFree(void *ptr);
618/// ```
619///
620/// Frees memory previously allocated with `xmlMalloc`, `xmlMallocAtomic`,
621/// or `xmlRealloc`. If `ptr` is NULL, no operation is performed.
622///
623/// # SAFETY
624///
625/// - `ptr` must be a valid pointer from `xmlMalloc`/`xmlMallocAtomic`/`xmlRealloc`,
626/// or NULL
627/// - After this call, `ptr` must not be dereferenced
628pub unsafe extern "C" fn xmlFreeImpl(ptr: *mut c_void) {
629 // SAFETY: see xmlMallocImpl.
630 let hook = allocator_slot(foreign_free_slot(), unsafe { xmlFree });
631 unsafe { hook(ptr) }
632}
633
634/// Default `xmlFree` body: plain `libc::free` (upstream default `free`).
635///
636/// No accounting (R-000178) — see `xmlMallocDefault`.
637unsafe extern "C" fn xmlFreeDefault(ptr: *mut c_void) {
638 unsafe { default_free(ptr) };
639}
640
641/// Duplicate a C string through the exported `xmlMemStrdup` variable.
642///
643/// # UPSTREAM-PARITY
644///
645/// ```c
646/// void *xmlMemStrdup(const char *str);
647/// ```
648///
649/// Returns a pointer to the newly allocated copy, or NULL on failure.
650///
651/// # SAFETY
652///
653/// - `str` must be a valid null-terminated C string or NULL
654/// - The returned pointer must be freed with `xmlFree`
655pub unsafe extern "C" fn xmlMemStrdupImpl(str: *const c_char) -> *mut c_void {
656 // SAFETY: see xmlMallocImpl.
657 let hook = allocator_slot(foreign_mem_strdup_slot(), unsafe { xmlMemStrdup });
658 unsafe { hook(str) }
659}
660
661/// Default `xmlMemStrdup` body: plain libc strdup (upstream default
662/// `xmlPosixStrdup`). No accounting (R-000178) — see `xmlMallocDefault`.
663unsafe extern "C" fn xmlMemStrdupDefault(str: *const c_char) -> *mut c_void {
664 unsafe { default_strdup(str) }
665}
666
667// ═══════════════════════════════════════════════════════════════════════════════
668// R-000177 allocator-slot bridge
669// ═══════════════════════════════════════════════════════════════════════════════
670// The allocator hooks (xmlMemSetup / direct xmlMalloc= assignments) bind to
671// the CORE DSO's exported slots; the whole-archive facades' private copies
672// of those statics are never written, so every *Impl indirection reads the
673// process-visible (core DSO's) slot via its __xml* accessor and falls back
674// to the local exported variable when the accessor is not exported
675// (single-DSO links resolve their own export — identical storage).
676
677#[cfg(target_os = "linux")]
678macro_rules! alloc_slot_reader {
679 ($reader:ident, $cname:expr, $t:ty) => {
680 fn $reader() -> Option<*mut $t> {
681 use std::sync::OnceLock;
682 type Acc = unsafe extern "C" fn() -> *mut $t;
683 static ACC: OnceLock<Option<Acc>> = OnceLock::new();
684 let acc = *ACC.get_or_init(|| {
685 // SAFETY: dlsym(RTLD_DEFAULT) returns the exported accessor
686 // address or NULL; the transmute (pointer-sized) is sound.
687 unsafe {
688 let sym = libc::dlsym(libc::RTLD_DEFAULT, ($cname).as_ptr());
689 if sym.is_null() {
690 None
691 } else {
692 Some(std::mem::transmute::<*mut c_void, Acc>(sym))
693 }
694 }
695 });
696 acc.map(|a| unsafe { a() })
697 }
698 };
699}
700
701#[cfg(not(target_os = "linux"))]
702macro_rules! alloc_slot_reader {
703 ($reader:ident, $cname:expr, $t:ty) => {
704 fn $reader() -> Option<*mut $t> {
705 None
706 }
707 };
708}
709
710alloc_slot_reader!(foreign_malloc_slot, c"__xmlMalloc", xmlMallocFunc);
711alloc_slot_reader!(
712 foreign_malloc_atomic_slot,
713 c"__xmlMallocAtomic",
714 xmlMallocFunc
715);
716alloc_slot_reader!(foreign_realloc_slot, c"__xmlRealloc", xmlReallocFunc);
717alloc_slot_reader!(foreign_free_slot, c"__xmlFree", xmlFreeFunc);
718alloc_slot_reader!(foreign_mem_strdup_slot, c"__xmlMemStrdup", xmlStrdupFunc);
719
720/// Resolve the active allocator hook: the process-visible (core) slot when
721/// its accessor resolves, else the local exported variable via `local`.
722#[inline]
723fn allocator_slot<T: Copy>(foreign: Option<*mut T>, local: T) -> T {
724 match foreign {
725 Some(slot) => {
726 // SAFETY: the accessor returned a pointer to the core DSO's
727 // exported allocator slot (valid for the process lifetime); the
728 // stored value is a valid C fn pointer under the upstream
729 // setup-ordering contract.
730 unsafe { *slot }
731 }
732 None => local,
733 }
734}
735
736/// `xmlMallocFunc xmlMalloc` — the malloc hook (default: `xmlMallocDefault`).
737#[no_mangle]
738pub static mut xmlMalloc: xmlMallocFunc = xmlMallocDefault;
739
740/// `xmlMallocFunc xmlMallocAtomic` — the atomic-malloc hook.
741#[no_mangle]
742pub static mut xmlMallocAtomic: xmlMallocFunc = xmlMallocAtomicDefault;
743
744/// `xmlReallocFunc xmlRealloc` — the realloc hook.
745#[no_mangle]
746pub static mut xmlRealloc: xmlReallocFunc = xmlReallocDefault;
747
748/// `xmlFreeFunc xmlFree` — the free hook.
749#[no_mangle]
750pub static mut xmlFree: xmlFreeFunc = xmlFreeDefault;
751
752/// `xmlStrdupFunc xmlMemStrdup` — the strdup hook.
753#[no_mangle]
754pub static mut xmlMemStrdup: xmlStrdupFunc = xmlMemStrdupDefault;
755
756// ── LIBXML_THREAD_ALLOC_ENABLED accessors (upstream globals.c) ─────────────
757//
758// Upstream source builds with --with-thread-alloc export accessor FUNCTIONS
759// `xmlMallocFunc *__xmlMalloc(void)` etc. (globals.c, gated by
760// LIBXML_THREAD_ALLOC_ENABLED); its xmlmemory.h then redefines `xmlMalloc`
761// as `(*__xmlMalloc())`, so consumers compiled against the thread-alloc
762// profile reference these accessors instead of the variables. The candidate
763// implements them per upstream semantics: each returns a pointer to the
764// allocator slot the library uses — which is exactly the corresponding
765// exported variable above (single source of truth, R-000176) — so
766// `(*__xmlMalloc())(size)` is the candidate's `xmlMalloc` variable.
767//
768// The executed distro oracle (system 2.15.3) hides these five accessors
769// (built without thread-alloc / with hidden visibility), so they are
770// CUSTODIAN_EXTENSION exports in the disposition ledger: upstream-ABI-valid,
771// required by source-built consumers (e.g. the canonical source oracle in
772// the Phase-12 DOCKER-SUBSTITUTION court, whose libxslt references
773// __xmlFree/__xmlMalloc/__xmlRealloc).
774
775/// Upstream `xmlMallocFunc *__xmlMalloc(void)`.
776///
777/// # Safety
778///
779/// - The returned pointer is the address of the exported `xmlMalloc`
780/// variable; it stays valid for the process lifetime and may be read or
781/// written through exactly like upstream's thread-local allocator slot.
782#[no_mangle]
783pub unsafe extern "C" fn __xmlMalloc() -> *mut xmlMallocFunc {
784 core::ptr::addr_of_mut!(xmlMalloc)
785}
786
787/// Upstream `xmlMallocFunc *__xmlMallocAtomic(void)`.
788///
789/// # Safety
790///
791/// - The returned pointer is the address of the exported `xmlMallocAtomic`
792/// variable; it stays valid for the process lifetime.
793#[no_mangle]
794pub unsafe extern "C" fn __xmlMallocAtomic() -> *mut xmlMallocFunc {
795 core::ptr::addr_of_mut!(xmlMallocAtomic)
796}
797
798/// Upstream `xmlReallocFunc *__xmlRealloc(void)`.
799///
800/// # Safety
801///
802/// - The returned pointer is the address of the exported `xmlRealloc`
803/// variable; it stays valid for the process lifetime.
804#[no_mangle]
805pub unsafe extern "C" fn __xmlRealloc() -> *mut xmlReallocFunc {
806 core::ptr::addr_of_mut!(xmlRealloc)
807}
808
809/// Upstream `xmlFreeFunc *__xmlFree(void)`.
810///
811/// # Safety
812///
813/// - The returned pointer is the address of the exported `xmlFree`
814/// variable; it stays valid for the process lifetime.
815#[no_mangle]
816pub unsafe extern "C" fn __xmlFree() -> *mut xmlFreeFunc {
817 core::ptr::addr_of_mut!(xmlFree)
818}
819
820/// Upstream `xmlStrdupFunc *__xmlMemStrdup(void)`.
821///
822/// # Safety
823///
824/// - The returned pointer is the address of the exported `xmlMemStrdup`
825/// variable; it stays valid for the process lifetime.
826#[no_mangle]
827pub unsafe extern "C" fn __xmlMemStrdup() -> *mut xmlStrdupFunc {
828 core::ptr::addr_of_mut!(xmlMemStrdup)
829}
830
831// ═══════════════════════════════════════════════════════════════════════════════
832// Memory Debugging / Statistics
833// ═══════════════════════════════════════════════════════════════════════════════
834
835/// Return the total amount of memory currently allocated (approximate).
836///
837/// # UPSTREAM-PARITY
838///
839/// ```c
840/// int xmlMemUsed(void);
841/// ```
842///
843/// Returns upstream's `debugMemSize` counter, which is maintained ONLY by the
844/// debug allocator (`xmlMemMalloc`/`*Loc` surface). With the default
845/// allocator installed the counter stays 0 — byte-identical with the oracle
846/// (R-000178, verified by ALLOCATOR-DEFAULT-001). Custom allocator hooks
847/// never touch it (upstream contract).
848#[no_mangle]
849pub extern "C" fn xmlMemUsed() -> c_int {
850 MEM_USED.load(Ordering::Relaxed) as c_int
851}
852
853/// Return the current number of allocated blocks (approximate).
854///
855/// # UPSTREAM-PARITY
856///
857/// ```c
858/// int xmlMemBlocks(void);
859/// ```
860///
861/// Returns upstream's `debugMemBlocks` counter (debug allocator only; 0 with
862/// the default allocator — R-000178, byte-identical with the oracle).
863#[no_mangle]
864pub extern "C" fn xmlMemBlocks() -> c_int {
865 MEM_BLOCKS.load(Ordering::Relaxed) as c_int
866}
867
868/// Display memory allocation information to a file.
869///
870/// # UPSTREAM-PARITY
871///
872/// ```c
873/// void xmlMemDisplay(FILE *fp);
874/// ```
875///
876/// No-op — upstream 2.15.0 removed this feature (`@deprecated This feature
877/// was removed.`). The pre-Z.3 candidate printed aggregate counters; that was
878/// a divergence from the executed oracle and is removed (R-000131 sealed).
879///
880/// # SAFETY
881///
882/// - `fp` must be a valid FILE* pointer or NULL (unused)
883#[no_mangle]
884pub const unsafe extern "C" fn xmlMemDisplay(_fp: *mut c_void) {}
885
886/// Show memory allocation information.
887///
888/// # UPSTREAM-PARITY
889///
890/// ```c
891/// void xmlMemShow(FILE *fp, int nr);
892/// ```
893///
894/// No-op — upstream 2.15.0 removed this feature (`@deprecated This feature
895/// was removed.`); the candidate previously dumped the registry with a
896/// non-upstream ordering, a documented divergence that is now removed
897/// (R-000131 sealed).
898///
899/// # SAFETY
900///
901/// - `fp` must be a valid FILE* pointer or NULL (unused)
902#[no_mangle]
903pub const unsafe extern "C" fn xmlMemShow(_fp: *mut c_void, _nr: c_int) {}
904
905// ═══════════════════════════════════════════════════════════════════════════════
906// Convenience Functions (used internally)
907// ═══════════════════════════════════════════════════════════════════════════════
908
909/// Allocate zero-initialized memory.
910///
911/// # UPSTREAM-PARITY
912///
913/// This is like `xmlMalloc` followed by `memset(0)`, but some allocator
914/// hooks provide it directly.
915///
916/// # SAFETY
917///
918/// Same as `xmlMalloc`. The returned memory is zero-initialized.
919#[no_mangle]
920pub unsafe extern "C" fn xmlMallocZero(size: usize) -> *mut c_void {
921 // SAFETY: Delegates to xmlMallocImpl and zeroes the memory.
922 let ptr = unsafe { xmlMallocImpl(size) };
923 if !ptr.is_null() {
924 unsafe { ptr::write_bytes(ptr, 0, size) };
925 }
926 ptr
927}
928
929/// Allocate zero-initialized memory (atomic variant).
930///
931/// # UPSTREAM-PARITY
932///
933/// Like `xmlMallocAtomic` followed by zero-initialization.
934///
935/// # SAFETY
936///
937/// The function touches crate-global state only; it is safe
938/// as long as the caller respects the library's global
939/// initialization/cleanup ordering (xmlInitParser before use,
940/// xmlCleanupParser only after all users are done).
941///
942/// Violating the global lifecycle ordering, or calling this after
943/// teardown or from a signal handler, is undefined behavior.
944#[no_mangle]
945pub unsafe extern "C" fn xmlMallocAtomicZero(size: usize) -> *mut c_void {
946 // SAFETY: Delegates to xmlMallocAtomicImpl and zeroes the memory.
947 let ptr = unsafe { xmlMallocAtomicImpl(size) };
948 if !ptr.is_null() {
949 unsafe { ptr::write_bytes(ptr, 0, size) };
950 }
951 ptr
952}
953
954/// Reallocate and zero-initialize the new portion.
955///
956/// # UPSTREAM-PARITY
957///
958/// Like `xmlRealloc`, but zeroes any newly allocated bytes.
959///
960/// # SAFETY
961///
962/// Same as `xmlRealloc`.
963#[no_mangle]
964pub unsafe extern "C" fn xmlReallocZero(
965 ptr: *mut c_void,
966 old_size: usize,
967 new_size: usize,
968) -> *mut c_void {
969 // SAFETY: Delegates to xmlRealloc and zeroes the new portion.
970 let new_ptr = unsafe { xmlReallocImpl(ptr, new_size) };
971 if !new_ptr.is_null() && new_size > old_size {
972 unsafe {
973 ptr::write_bytes(new_ptr.add(old_size), 0, new_size - old_size);
974 }
975 }
976 new_ptr
977}
978
979// ═══════════════════════════════════════════════════════════════════════════════
980// Debug Allocator (Optional)
981// ═══════════════════════════════════════════════════════════════════════════════
982
983/// Initialize the memory layer with debugging support.
984///
985/// # UPSTREAM-PARITY
986///
987/// ```c
988/// int xmlInitMemory(void);
989/// ```
990///
991/// Initializes the memory subsystem. Returns 0 on success.
992/// This is called automatically by `xmlInitParser`.
993#[no_mangle]
994pub const extern "C" fn xmlInitMemory() -> c_int {
995 0
996}
997
998/// Clean up the memory layer.
999///
1000/// # UPSTREAM-PARITY
1001///
1002/// ```c
1003/// void xmlCleanupMemory(void);
1004/// ```
1005#[no_mangle]
1006pub const extern "C" fn xmlCleanupMemory() {
1007 // Phase 1: no cleanup needed for the default allocator.
1008}
1009
1010// ═══════════════════════════════════════════════════════════════════════════════
1011// Legacy-named allocator API (upstream xmlmemory.h)
1012// ═══════════════════════════════════════════════════════════════════════════════
1013// Upstream xmlmemory.h historically exported `xmlMemMalloc`, `xmlMemFree`,
1014// `xmlMemRealloc`, `xmlMemoryStrdup`, the `*Loc` location-tracking variants,
1015// `xmlMemSize`, `xmlMemDisplayLast` and `xmlMemoryDump` alongside the modern
1016// names. Downstream code (older consumers, some language bindings) links
1017// against these names, so the candidate exports them with identical
1018// semantics. In upstream 2.15.0 these ARE the debug allocator: always
1019// libc-backed, independent of the hook variables, and tracked by
1020// `debugMemSize`/`debugMemBlocks` (with the MEMHDR tag enabling
1021// `xmlMemSize`). The candidate mirrors that exactly: the debug-named
1022// functions and the `*Loc` variants are always libc-backed + registry- and
1023// counter-tracked, do NOT route through the exported variables, and the
1024// `*Loc` location arguments are accepted and ignored — exactly like
1025// upstream's `ATTRIBUTE_UNUSED` parameters (R-000131 sealed). The candidate
1026// returns plain libc pointers (no MEMHDR prefix), so a debug-surface block
1027// can also be freed with `xmlFree` — a safe superset of the upstream
1028// contract (upstream requires `xmlMemFree` for such blocks).
1029
1030/// Debug-surface malloc: libc + counters + registry (upstream xmlMemMalloc).
1031///
1032/// # Safety
1033///
1034/// - `size` must be a valid allocation size; the underlying `default_malloc`
1035/// follows the libc contract (0 handled by the platform).
1036/// - `file` must be NULL or a valid pointer to a NUL-terminated C string that
1037/// stays valid for the duration of the call (it is stored by address only,
1038/// never dereferenced here).
1039/// - The returned pointer (NULL on failure, with counters untouched) is owned
1040/// by the caller and must be freed exactly once via `debug_free` or
1041/// `xmlMemFree`; never dereferenced after freeing.
1042unsafe fn debug_malloc(size: usize, file: *const c_char, line: c_int) -> *mut c_void {
1043 let ptr = unsafe { default_malloc(size) };
1044 if !ptr.is_null() {
1045 MEM_USED.fetch_add(size, Ordering::Relaxed);
1046 MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
1047 unsafe { block_record(ptr, size, file, line) };
1048 }
1049 ptr
1050}
1051
1052/// Debug-surface realloc: libc + counters + registry (upstream xmlMemRealloc).
1053///
1054/// # Safety
1055///
1056/// - `ptr` must be NULL or a valid pointer previously returned by the debug
1057/// surface (`debug_malloc`/`debug_realloc`) or by a matching libc
1058/// allocation, and not yet freed; NULL behaves like malloc.
1059/// - On failure the old block is left intact and stays recorded; on success
1060/// the old pointer is invalidated and the returned pointer must be freed
1061/// exactly once via `debug_free`/`xmlMemFree`.
1062/// - `file` must be NULL or a valid NUL-terminated C string valid for the
1063/// duration of the call (stored by address only).
1064unsafe fn debug_realloc(
1065 ptr: *mut c_void,
1066 size: usize,
1067 file: *const c_char,
1068 line: c_int,
1069) -> *mut c_void {
1070 let new_ptr = unsafe { default_realloc(ptr, size) };
1071 if !new_ptr.is_null() {
1072 let old_size = unsafe { block_forget(ptr) };
1073 if let Some(old) = old_size {
1074 MEM_USED.fetch_add(size.saturating_sub(old), Ordering::Relaxed);
1075 } else if ptr.is_null() {
1076 MEM_USED.fetch_add(size, Ordering::Relaxed);
1077 MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
1078 }
1079 unsafe { block_record(new_ptr, size, file, line) };
1080 }
1081 new_ptr
1082}
1083
1084/// Debug-surface strdup: libc + counters + registry (upstream xmlMemoryStrdup).
1085///
1086/// # Safety
1087///
1088/// - `str` must be NULL or a valid pointer to a NUL-terminated C string
1089/// readable through its full length (including the terminator) for the
1090/// duration of the call; NULL yields NULL.
1091/// - `file` must be NULL or a valid NUL-terminated C string valid for the
1092/// duration of the call (stored by address only).
1093/// - The returned pointer (NULL on failure) must be freed exactly once via
1094/// `debug_free`/`xmlMemFree`; never dereferenced after freeing.
1095unsafe fn debug_strdup(str: *const c_char, file: *const c_char, line: c_int) -> *mut c_void {
1096 if str.is_null() {
1097 return ptr::null_mut();
1098 }
1099 let ptr = unsafe { default_strdup(str) };
1100 if !ptr.is_null() {
1101 let len = unsafe { libc::strlen(str) } + 1;
1102 MEM_USED.fetch_add(len, Ordering::Relaxed);
1103 MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
1104 unsafe { block_record(ptr, len, file, line) };
1105 }
1106 ptr
1107}
1108
1109/// Debug-surface free: registry/counter removal + libc free (upstream xmlMemFree).
1110/// A foreign pointer (not in the registry) is freed without touching the
1111/// counters — a safe divergence from upstream's tag-error print (which would
1112/// pollute stderr).
1113///
1114/// # Safety
1115///
1116/// - `ptr` must be NULL (a no-op) or a valid pointer previously returned by
1117/// the debug surface (`debug_malloc`/`debug_realloc`/`debug_strdup`) or by
1118/// a matching libc allocation; it must not be freed twice and must not be
1119/// dereferenced after this call.
1120/// - The registry and counters are only touched when the pointer was recorded;
1121/// a foreign pointer is still freed via libc.
1122unsafe fn debug_free(ptr: *mut c_void) {
1123 if ptr.is_null() {
1124 return;
1125 }
1126 let old_size = unsafe { block_forget(ptr) };
1127 unsafe { default_free(ptr) };
1128 if let Some(old) = old_size {
1129 MEM_USED.fetch_sub(old, Ordering::Relaxed);
1130 MEM_BLOCKS.fetch_sub(1, Ordering::Relaxed);
1131 }
1132}
1133
1134/// Allocate memory through the debug allocator (upstream xmlmemory.h).
1135///
1136/// ```c
1137/// void *xmlMemMalloc(size_t size);
1138/// ```
1139///
1140/// Always libc-backed and tracked (upstream debug allocator contract): the
1141/// block is recorded so `xmlMemSize`/`xmlMemUsed`/`xmlMemBlocks` observe it.
1142///
1143/// # SAFETY
1144///
1145/// - The returned pointer must be freed with `xmlMemFree` (or `xmlFree` —
1146/// plain libc pointer, candidate superset)
1147#[no_mangle]
1148pub unsafe extern "C" fn xmlMemMalloc(size: usize) -> *mut c_void {
1149 unsafe { debug_malloc(size, ptr::null(), 0) }
1150}
1151
1152/// Free memory through the debug allocator (upstream xmlmemory.h).
1153///
1154/// ```c
1155/// void xmlMemFree(void *ptr);
1156/// ```
1157///
1158/// Un-records the block and frees it. NULL is a no-op.
1159///
1160/// # SAFETY
1161///
1162/// - `ptr` must be valid pointers (or NULL
1163/// where the upstream C contract allows), obtained from the
1164/// matching constructor/owner and not yet freed; the callee may
1165/// take or keep ownership exactly as the C API specifies.
1166#[no_mangle]
1167pub unsafe extern "C" fn xmlMemFree(ptr: *mut c_void) {
1168 unsafe { debug_free(ptr) };
1169}
1170
1171/// Reallocate memory through the debug allocator (upstream xmlmemory.h).
1172///
1173/// ```c
1174/// void *xmlMemRealloc(void *ptr, size_t size);
1175/// ```
1176///
1177/// C realloc semantics + registry maintenance (upstream xmlMemRealloc:
1178/// NULL ptr allocates, failure leaves the old block recorded and intact).
1179///
1180/// # SAFETY
1181///
1182/// - `ptr` must be valid pointers (or NULL
1183/// where the upstream C contract allows), obtained from the
1184/// matching constructor/owner and not yet freed; the callee may
1185/// take or keep ownership exactly as the C API specifies.
1186#[no_mangle]
1187pub unsafe extern "C" fn xmlMemRealloc(ptr: *mut c_void, size: usize) -> *mut c_void {
1188 unsafe { debug_realloc(ptr, size, ptr::null(), 0) }
1189}
1190
1191/// Duplicate a string through the debug allocator (upstream xmlmemory.h).
1192///
1193/// ```c
1194/// void *xmlMemoryStrdup(const char *str);
1195/// ```
1196///
1197/// NULL returns NULL (upstream xmlPosixStrdup/xmlCharStrdup contract).
1198///
1199/// # SAFETY
1200///
1201/// - `str` must point to a valid NUL-terminated string, or NULL
1202/// - The returned pointer must be freed with `xmlMemFree` (or `xmlFree`)
1203#[no_mangle]
1204pub unsafe extern "C" fn xmlMemoryStrdup(str: *const c_char) -> *mut c_void {
1205 unsafe { debug_strdup(str, ptr::null(), 0) }
1206}
1207
1208/// Allocate memory, recording the allocation site (upstream xmlmemory.h).
1209///
1210/// ```c
1211/// void *xmlMallocLoc(size_t size, const char *file, int line);
1212/// ```
1213///
1214/// Debug-allocator contract (upstream xmlMallocLoc -> xmlMemMalloc): always
1215/// libc-backed + tracked, independent of the hook variables. Upstream 2.15.0
1216/// ignores the file/line arguments (`ATTRIBUTE_UNUSED`); the candidate
1217/// records them in the registry so `xmlMemSize` works (R-000131 sealed).
1218///
1219/// # SAFETY
1220///
1221///
1222/// - `file` must point to valid NUL-terminated
1223/// strings (or NULL where the C contract allows) for the lifetime
1224/// of the call.
1225///
1226/// The caller must not race this call with concurrent mutation of the
1227/// same objects from other threads (per-object state is not internally
1228/// synchronized). Violating any of the above is undefined behavior.
1229///
1230/// Exercised by the C-API differential courts
1231/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1232/// courts; those pass byte-for-byte against the upstream oracle.
1233#[no_mangle]
1234pub unsafe extern "C" fn xmlMallocLoc(
1235 size: usize,
1236 file: *const c_char,
1237 line: c_int,
1238) -> *mut c_void {
1239 unsafe { debug_malloc(size, file, line) }
1240}
1241
1242/// Allocate memory, recording the allocation site (upstream xmlmemory.h).
1243///
1244/// ```c
1245/// void *xmlMallocAtomicLoc(size_t size, const char *file, int line);
1246/// ```
1247///
1248/// Upstream xmlMallocAtomicLoc -> xmlMemMalloc: a plain (non-zeroed) tracked
1249/// allocation. The pre-Z.3 candidate zeroed it via `xmlMallocZero` — a real
1250/// divergence, fixed in 11.1-Z.3 (R-000178 narrative).
1251///
1252/// # SAFETY
1253///
1254///
1255/// - `file` must point to valid NUL-terminated
1256/// strings (or NULL where the C contract allows) for the lifetime
1257/// of the call.
1258///
1259/// The caller must not race this call with concurrent mutation of the
1260/// same objects from other threads (per-object state is not internally
1261/// synchronized). Violating any of the above is undefined behavior.
1262///
1263/// Exercised by the C-API differential courts
1264/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1265/// courts; those pass byte-for-byte against the upstream oracle.
1266#[no_mangle]
1267pub unsafe extern "C" fn xmlMallocAtomicLoc(
1268 size: usize,
1269 file: *const c_char,
1270 line: c_int,
1271) -> *mut c_void {
1272 unsafe { debug_malloc(size, file, line) }
1273}
1274
1275/// Reallocate memory, recording the allocation site (upstream xmlmemory.h).
1276///
1277/// ```c
1278/// void *xmlReallocLoc(void *ptr, size_t size, const char *file, int line);
1279/// ```
1280///
1281/// Debug-allocator contract (upstream xmlReallocLoc -> xmlMemRealloc): the
1282/// old record is superseded on success, kept on failure.
1283///
1284/// # SAFETY
1285///
1286/// - `ptr` must be valid pointers (or NULL
1287/// where the upstream C contract allows), obtained from the
1288/// matching constructor/owner and not yet freed; the callee may
1289/// take or keep ownership exactly as the C API specifies.
1290///
1291/// - `file` must point to valid NUL-terminated
1292/// strings (or NULL where the C contract allows) for the lifetime
1293/// of the call.
1294///
1295/// The caller must not race this call with concurrent mutation of the
1296/// same objects from other threads (per-object state is not internally
1297/// synchronized). Violating any of the above is undefined behavior.
1298///
1299/// Exercised by the C-API differential courts
1300/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1301/// courts; those pass byte-for-byte against the upstream oracle.
1302#[no_mangle]
1303pub unsafe extern "C" fn xmlReallocLoc(
1304 ptr: *mut c_void,
1305 size: usize,
1306 file: *const c_char,
1307 line: c_int,
1308) -> *mut c_void {
1309 unsafe { debug_realloc(ptr, size, file, line) }
1310}
1311
1312/// Duplicate a string, recording the allocation site (upstream xmlmemory.h).
1313///
1314/// ```c
1315/// void *xmlMemStrdupLoc(const char *str, const char *file, int line);
1316/// ```
1317///
1318/// Debug-allocator contract (upstream xmlMemStrdupLoc -> xmlMemoryStrdup).
1319///
1320/// # SAFETY
1321///
1322///
1323/// - `str`, `file` must point to valid NUL-terminated
1324/// strings (or NULL where the C contract allows) for the lifetime
1325/// of the call.
1326///
1327/// The caller must not race this call with concurrent mutation of the
1328/// same objects from other threads (per-object state is not internally
1329/// synchronized). Violating any of the above is undefined behavior.
1330///
1331/// Exercised by the C-API differential courts
1332/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1333/// courts; those pass byte-for-byte against the upstream oracle.
1334#[no_mangle]
1335pub unsafe extern "C" fn xmlMemStrdupLoc(
1336 str: *const c_char,
1337 file: *const c_char,
1338 line: c_int,
1339) -> *mut c_void {
1340 unsafe { debug_strdup(str, file, line) }
1341}
1342
1343/// Return the size of an allocated block (upstream xmlmemory.h).
1344///
1345/// ```c
1346/// size_t xmlMemSize(void *ptr);
1347/// ```
1348///
1349/// Returns the recorded size for debug-surface blocks (`xmlMemMalloc`/`*Loc`
1350/// surface) and 0 for everything else — matching upstream's MEMHDR tag
1351/// lookup, which misses on plain-malloc blocks (default allocator) and on
1352/// foreign pointers (R-000178; byte-identical with the oracle).
1353///
1354/// # SAFETY
1355///
1356/// - `ptr` must be valid pointers (or NULL
1357/// where the upstream C contract allows), obtained from the
1358/// matching constructor/owner and not yet freed; the callee may
1359/// take or keep ownership exactly as the C API specifies.
1360#[no_mangle]
1361pub unsafe extern "C" fn xmlMemSize(ptr: *mut c_void) -> usize {
1362 if ptr.is_null() {
1363 return 0;
1364 }
1365 BLOCKS
1366 .lock()
1367 .get(&(ptr as usize))
1368 .map(|m| m.size)
1369 .unwrap_or(0)
1370}
1371
1372/// Display a limited amount of memory debug information (upstream xmlmemory.h).
1373///
1374/// ```c
1375/// void xmlMemDisplayLast(FILE *fp, long nbBytes);
1376/// ```
1377///
1378/// No-op — upstream 2.15.0 removed this feature (`@deprecated This feature
1379/// was removed.`); the pre-Z.3 candidate dumped the registry with a
1380/// non-upstream format, a divergence now removed (R-000131 sealed).
1381///
1382/// # SAFETY
1383///
1384/// - `fp` must be a valid FILE* pointer or NULL (unused)
1385#[no_mangle]
1386pub const unsafe extern "C" fn xmlMemDisplayLast(_fp: *mut c_void, _nb_bytes: c_long) {}
1387
1388/// Dump memory allocation statistics (upstream xmlmemory.h).
1389///
1390/// ```c
1391/// void xmlMemoryDump(void);
1392/// ```
1393///
1394/// No-op — upstream 2.15.0 removed this feature (`@deprecated This feature
1395/// was removed.`).
1396///
1397/// # SAFETY
1398///
1399/// The function touches crate-global state only; it is safe
1400/// as long as the caller respects the library's global
1401/// initialization/cleanup ordering (xmlInitParser before use,
1402/// xmlCleanupParser only after all users are done).
1403#[no_mangle]
1404pub const unsafe extern "C" fn xmlMemoryDump() {
1405 // Upstream: empty body (feature removed in 2.15.0).
1406}
1407
1408// ═══════════════════════════════════════════════════════════════════════════════
1409// Tests
1410// ═══════════════════════════════════════════════════════════════════════════════
1411
1412#[cfg(test)]
1413mod tests {
1414 use super::*;
1415 use core::ptr;
1416
1417 /// Test basic allocation and deallocation.
1418 ///
1419 /// # Safety
1420 ///
1421 /// - `ptr` is the non-NULL result of `xmlMalloc(100)` (asserted): it is a
1422 /// valid heap allocation owned by the caller, freed exactly once by
1423 /// `xmlFree`, and not used afterwards.
1424 #[test]
1425 fn test_malloc_free() {
1426 unsafe {
1427 let ptr = xmlMalloc(100);
1428 assert!(!ptr.is_null(), "xmlMalloc(100) returned NULL");
1429 xmlFree(ptr);
1430 }
1431 }
1432
1433 /// Test that xmlMalloc(0) returns a valid pointer.
1434 ///
1435 /// # Safety
1436 ///
1437 /// - `xmlMalloc(0)` may return NULL or a unique non-NULL pointer per the
1438 /// platform contract; a non-NULL result is a valid allocation freed
1439 /// exactly once by `xmlFree` and not used afterwards.
1440 #[test]
1441 fn test_malloc_zero() {
1442 unsafe {
1443 let ptr = xmlMalloc(0);
1444 // malloc(0) may return NULL or a valid pointer.
1445 // If non-NULL, it must be freeable.
1446 if !ptr.is_null() {
1447 xmlFree(ptr);
1448 }
1449 }
1450 }
1451
1452 /// Test xmlFree(NULL) is a no-op.
1453 ///
1454 /// # Safety
1455 ///
1456 /// - Passing NULL to `xmlFree` is accepted and performs no operation; no
1457 /// pointer is dereferenced.
1458 #[test]
1459 fn test_free_null() {
1460 unsafe {
1461 xmlFree(ptr::null_mut());
1462 // Should not crash
1463 }
1464 }
1465
1466 /// Test xmlRealloc.
1467 ///
1468 /// # Safety
1469 ///
1470 /// - `ptr` must be a valid non-NULL allocation from `xmlMalloc`; on the
1471 /// successful `xmlRealloc` here the old pointer is invalidated and only
1472 /// the returned `new_ptr` (non-NULL, asserted) is freed exactly once by
1473 /// `xmlFree` and not used afterwards.
1474 #[test]
1475 fn test_realloc() {
1476 unsafe {
1477 let ptr = xmlMalloc(50);
1478 assert!(!ptr.is_null());
1479 let new_ptr = xmlRealloc(ptr, 100);
1480 assert!(!new_ptr.is_null());
1481 xmlFree(new_ptr);
1482 }
1483 }
1484
1485 /// Test xmlMemStrdup.
1486 ///
1487 /// # Safety
1488 ///
1489 /// - `s` points to a valid NUL-terminated byte string of 6 bytes (the
1490 /// literal `hello` plus terminator) that stays alive for the call.
1491 /// - `dup` is the non-NULL result of `xmlMemStrdup`, a fresh allocation of
1492 /// at least 6 bytes readable as a slice; it is freed exactly once by
1493 /// `xmlFree` and not used afterwards.
1494 #[test]
1495 fn test_mem_strdup() {
1496 unsafe {
1497 let s = b"hello\0" as *const u8 as *const c_char;
1498 let dup = xmlMemStrdup(s);
1499 assert!(!dup.is_null());
1500 // Compare the strings
1501 let orig_slice = std::slice::from_raw_parts(s as *const u8, 6);
1502 let dup_slice = std::slice::from_raw_parts(dup as *const u8, 6);
1503 assert_eq!(orig_slice, dup_slice);
1504 xmlFree(dup);
1505 }
1506 }
1507
1508 /// Test xmlMallocZero returns zeroed memory.
1509 ///
1510 /// # Safety
1511 ///
1512 /// - `ptr` is the non-NULL result of `xmlMallocZero(100)`: a valid
1513 /// allocation of at least 100 bytes, readable as a 100-byte slice
1514 /// while alive, and freed exactly once by `xmlFree` afterwards.
1515 #[test]
1516 fn test_malloc_zero_init() {
1517 unsafe {
1518 let ptr = xmlMallocZero(100) as *mut u8;
1519 assert!(!ptr.is_null());
1520 let slice = std::slice::from_raw_parts(ptr, 100);
1521 assert!(slice.iter().all(|&b| b == 0));
1522 xmlFree(ptr as *mut c_void);
1523 }
1524 }
1525
1526 /// Test custom allocator setup/get (R-000176: int returns, single source
1527 /// of truth — `xmlMemSetup` writes the exported variables and `xmlMemGet`
1528 /// reads them back).
1529 ///
1530 /// # Safety
1531 ///
1532 /// - Each output pointer passed to `xmlMemGet` is derived from a live
1533 /// stack local of matching type, so every non-NULL output points to
1534 /// valid, aligned, writable storage for the duration of the call; NULL
1535 /// outputs are tolerated and skipped.
1536 /// - The function pointers written back must be treated as valid
1537 /// C-compatible hooks (they are the currently installed allocator
1538 /// functions).
1539 #[test]
1540 fn test_mem_setup_get() {
1541 unsafe {
1542 let mut free_func: Option<xmlFreeFunc> = None;
1543 let mut malloc_func: Option<xmlMallocFunc> = None;
1544 let mut realloc_func: Option<xmlReallocFunc> = None;
1545 let mut strdup_func: Option<xmlStrdupFunc> = None;
1546
1547 let ret = xmlMemGet(
1548 &mut free_func as *mut _,
1549 &mut malloc_func as *mut _,
1550 &mut realloc_func as *mut _,
1551 &mut strdup_func as *mut _,
1552 );
1553 assert_eq!(ret, 0, "xmlMemGet must return 0");
1554 assert!(malloc_func.is_some());
1555 assert!(free_func.is_some());
1556 assert!(realloc_func.is_some());
1557 assert!(strdup_func.is_some());
1558
1559 // NULL output pointers are tolerated (upstream xmlmemory.c).
1560 let ret = xmlMemGet(
1561 ptr::null_mut(),
1562 &mut malloc_func as *mut _,
1563 ptr::null_mut(),
1564 ptr::null_mut(),
1565 );
1566 assert_eq!(ret, 0);
1567 assert!(malloc_func.is_some());
1568 }
1569 }
1570
1571 /// Test the GC allocator hooks: 5-argument Setup/Get with the dedicated
1572 /// `mallocAtomicFunc` slot (R-000176 — previously 4-arg and void).
1573 ///
1574 /// # Safety
1575 ///
1576 /// - Each output pointer passed to `xmlGcMemGet` is derived from a live
1577 /// stack local of matching type, so every non-NULL output points to
1578 /// valid, aligned, writable storage for the duration of the call; NULL
1579 /// outputs are tolerated and skipped.
1580 #[test]
1581 fn test_gc_mem_setup_get() {
1582 unsafe {
1583 let mut free_func: Option<xmlFreeFunc> = None;
1584 let mut malloc_func: Option<xmlMallocFunc> = None;
1585 let mut malloc_atomic_func: Option<xmlMallocFunc> = None;
1586 let mut realloc_func: Option<xmlReallocFunc> = None;
1587 let mut strdup_func: Option<xmlStrdupFunc> = None;
1588
1589 let ret = xmlGcMemGet(
1590 &mut free_func as *mut _,
1591 &mut malloc_func as *mut _,
1592 &mut malloc_atomic_func as *mut _,
1593 &mut realloc_func as *mut _,
1594 &mut strdup_func as *mut _,
1595 );
1596 assert_eq!(ret, 0, "xmlGcMemGet must return 0");
1597 assert!(free_func.is_some());
1598 assert!(malloc_func.is_some());
1599 assert!(malloc_atomic_func.is_some());
1600 assert!(realloc_func.is_some());
1601 assert!(strdup_func.is_some());
1602 }
1603 }
1604
1605 /// Test `xmlMemSetup` NULL validation returns -1 (upstream xmlmemory.c)
1606 /// and that a NULL `mallocAtomicFunc` makes `xmlGcMemSetup` fail.
1607 ///
1608 /// # Safety
1609 ///
1610 /// - `default_malloc` cast to `xmlMallocFunc` is a valid C-compatible
1611 /// malloc-shaped function pointer; it is only passed as an argument here
1612 /// and never called by this test.
1613 /// - The NULL hooks are rejected with -1 before any write, so no
1614 /// allocator state is modified by this test.
1615 #[test]
1616 fn test_mem_setup_null_rejected() {
1617 unsafe {
1618 let ret = xmlMemSetup(None, Some(default_malloc as xmlMallocFunc), None, None);
1619 assert_eq!(ret, -1, "NULL hook must be rejected with -1");
1620 let ret = xmlGcMemSetup(
1621 None,
1622 Some(default_malloc as xmlMallocFunc),
1623 Some(default_malloc as xmlMallocFunc),
1624 None,
1625 None,
1626 );
1627 assert_eq!(ret, -1);
1628 }
1629 }
1630
1631 /// Test the single-source-of-truth model (R-000176): a direct assignment
1632 /// to the exported `xmlMalloc` variable is observed by `xmlMemGet` AND by
1633 /// actual internal allocations through `xmlMallocImpl`.
1634 ///
1635 /// # Safety
1636 ///
1637 /// - The exported `static mut` allocator variables are read and written
1638 /// directly, which is only valid under the upstream single-threaded
1639 /// setup ordering: no other thread may allocate or read the variables
1640 /// concurrently with these assignments.
1641 /// - The values installed (`xmlMallocDefault`/`xmlFreeDefault`) are valid
1642 /// malloc/free-shaped functions; `p` from `xmlMallocImpl` is a valid
1643 /// allocation freed exactly once by `xmlFreeImpl`; the prior hook values
1644 /// are restored before the block ends.
1645 #[test]
1646 fn test_direct_assignment_coherence() {
1647 unsafe {
1648 let saved = xmlMalloc;
1649 let saved_free = xmlFree;
1650 // Install a counting hook via direct variable assignment.
1651 xmlMalloc = xmlMallocDefault;
1652 xmlFree = xmlFreeDefault;
1653
1654 // xmlMemGet reads the variable.
1655 let mut malloc_func: Option<xmlMallocFunc> = None;
1656 let ret = xmlMemGet(
1657 ptr::null_mut(),
1658 &mut malloc_func as *mut _,
1659 ptr::null_mut(),
1660 ptr::null_mut(),
1661 );
1662 assert_eq!(ret, 0);
1663 assert!(
1664 core::ptr::fn_addr_eq(malloc_func.unwrap(), xmlMallocDefault as xmlMallocFunc,),
1665 "xmlMemGet must return the directly-assigned variable"
1666 );
1667
1668 // Internal allocation routes through the variable.
1669 let p = xmlMallocImpl(64);
1670 assert!(!p.is_null());
1671 xmlFreeImpl(p);
1672
1673 // Restore the defaults so other tests are unaffected.
1674 xmlMalloc = saved;
1675 xmlFree = saved_free;
1676 }
1677 }
1678
1679 /// Test xmlMemUsed and xmlMemBlocks return 0 with the default allocator
1680 /// and the debug surface is tracked (R-000178: byte-identical with the
1681 /// oracle — default malloc untracked, xmlMemMalloc/*Loc tracked).
1682 ///
1683 /// # Safety
1684 ///
1685 /// - `ptr` from `xmlMalloc` is a valid libc allocation freed exactly once
1686 /// by `xmlFree`; `xmlMemSize` reads the registry by address only and is
1687 /// safe on freed default-allocator pointers (returns 0).
1688 /// - `dptr` from `xmlMallocLoc` and `rptr` from `xmlReallocLoc` are
1689 /// debug-surface allocations: on success the old pointer is invalidated
1690 /// and only `rptr` is freed, exactly once, by `xmlMemFree`.
1691 #[test]
1692 fn test_mem_stats() {
1693 unsafe {
1694 // Default allocator: plain libc, NOT tracked (oracle contract).
1695 let ptr = xmlMalloc(100);
1696 assert!(!ptr.is_null());
1697 assert_eq!(xmlMemSize(ptr), 0, "default-allocator blocks are untracked");
1698 xmlFree(ptr);
1699 assert_eq!(xmlMemSize(ptr), 0);
1700
1701 // Debug surface: tracked, xmlMemSize returns the recorded size.
1702 let dptr = xmlMallocLoc(100, c"mem.c".as_ptr(), 42);
1703 assert!(!dptr.is_null());
1704 assert_eq!(xmlMemSize(dptr), 100, "debug-surface blocks are tracked");
1705 let rptr = xmlReallocLoc(dptr, 200, c"mem.c".as_ptr(), 43);
1706 assert!(!rptr.is_null());
1707 // The realloc record supersedes the old one. glibc may grow the
1708 // block in place (rptr == dptr; the block at that address is then
1709 // recorded at 200) or MOVE it (the old address is released and
1710 // xmlMemSize(old) == 0 while rptr is the live 200-byte block). Both
1711 // are valid libc behaviors — assert per case so the test is
1712 // environment-independent (CI glibc moves; some hosts grow in
1713 // place).
1714 assert_eq!(xmlMemSize(rptr), 200);
1715 if rptr == dptr {
1716 assert_eq!(xmlMemSize(dptr), 200);
1717 } else {
1718 assert_eq!(
1719 xmlMemSize(dptr),
1720 0,
1721 "a moved realloc releases the old address"
1722 );
1723 }
1724 xmlMemFree(rptr);
1725 assert_eq!(xmlMemSize(rptr), 0);
1726
1727 // xmlMemSize(NULL) is 0.
1728 assert_eq!(xmlMemSize(ptr::null_mut()), 0);
1729 }
1730 }
1731
1732 /// Test the default allocator follows the C/libc contract exactly
1733 /// (R-000178): malloc(0) non-NULL, realloc(p,0) NULL after freeing,
1734 /// realloc(NULL,n) == malloc, realloc failure leaves the old block,
1735 /// malloc(SIZE_MAX) NULL, strdup(NULL) NULL.
1736 ///
1737 /// # Safety
1738 ///
1739 /// - Each non-NULL allocation is freed exactly once by `xmlFree`, and
1740 /// pointers are not used after freeing; in particular `q` is freed by
1741 /// the `realloc(p, 0)` call that returns NULL (glibc contract), so it
1742 /// must not be used afterwards, and `r` is freed explicitly after the
1743 /// failed huge realloc leaves it intact.
1744 /// - NULL arguments to `xmlRealloc`, `xmlMemStrdup` and `xmlFree` are
1745 /// accepted per the C contract.
1746 #[test]
1747 fn test_default_libc_semantics() {
1748 unsafe {
1749 // malloc(0): glibc returns a unique non-NULL pointer.
1750 let p0 = xmlMalloc(0);
1751 assert!(!p0.is_null(), "malloc(0) must be non-NULL on glibc");
1752 xmlFree(p0);
1753
1754 // realloc(NULL, n) allocates.
1755 let p = xmlRealloc(ptr::null_mut(), 16);
1756 assert!(!p.is_null());
1757 // grow: content preserved.
1758 let q = xmlRealloc(p, 256);
1759 assert!(!q.is_null());
1760 // realloc(p, 0): glibc frees and returns NULL.
1761 let z = xmlRealloc(q, 0);
1762 assert!(z.is_null(), "realloc(p, 0) returns NULL on glibc");
1763
1764 // realloc failure: old block intact.
1765 let r = xmlMalloc(8);
1766 assert!(!r.is_null());
1767 let huge = xmlRealloc(r, usize::MAX);
1768 assert!(huge.is_null(), "realloc to SIZE_MAX must fail");
1769 xmlFree(r);
1770
1771 // malloc failure.
1772 let m = xmlMalloc(usize::MAX);
1773 assert!(m.is_null(), "malloc(SIZE_MAX) must fail");
1774
1775 // strdup(NULL) returns NULL (upstream xmlPosixStrdup).
1776 let d = xmlMemStrdup(ptr::null());
1777 assert!(d.is_null());
1778
1779 // free(NULL) no-op.
1780 xmlFree(ptr::null_mut());
1781 }
1782 }
1783
1784 /// Test the debug-named surface is independent of the hook variables and
1785 /// always libc-backed + tracked (upstream debug-allocator contract).
1786 ///
1787 /// # Safety
1788 ///
1789 /// - `p`, `q` and `s` are non-NULL debug-surface allocations; after the
1790 /// successful `xmlMemRealloc` only `q` remains valid, and `q` and `s`
1791 /// are each freed exactly once by `xmlMemFree` and not used afterwards.
1792 /// - `xmlMemFree(NULL)` is a no-op; `xmlMemSize` reads the registry by
1793 /// address only.
1794 #[test]
1795 fn test_debug_surface_tracked() {
1796 unsafe {
1797 let p = xmlMemMalloc(64);
1798 assert!(!p.is_null());
1799 assert_eq!(xmlMemSize(p), 64);
1800 let q = xmlMemRealloc(p, 128);
1801 assert!(!q.is_null());
1802 assert_eq!(xmlMemSize(q), 128);
1803 let s = xmlMemoryStrdup(c"dbg".as_ptr());
1804 assert!(!s.is_null());
1805 assert_eq!(xmlMemSize(s), 4);
1806 xmlMemFree(q);
1807 xmlMemFree(s);
1808 // xmlMemFree(NULL) no-op.
1809 xmlMemFree(ptr::null_mut());
1810 }
1811 }
1812}