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 /// §16.5.10 allocator-bridge hot path: cache the ADDRESS of the
681 /// process-visible (core DSO's) exported allocator slot — NOT the
682 /// function value it currently holds. The slot's contents are
683 /// reassigned by `xmlMemSetup` / direct `xmlMalloc = ...`
684 /// assignments and every allocation must observe the CURRENT hook,
685 /// but the slot's address is fixed for the process lifetime, so
686 /// reading through the cached address preserves mutation semantics
687 /// (a fresh value per call) while dropping the per-allocation
688 /// dlsym'd accessor invocation.
689 fn $reader() -> Option<*mut $t> {
690 use std::sync::OnceLock;
691 type Acc = unsafe extern "C" fn() -> *mut $t;
692 // usize storage keeps the static Sync/Send (raw fn pointers are
693 // not); the value is a plain pointer-sized slot address.
694 static ACC: OnceLock<Option<usize>> = OnceLock::new();
695 let slot = *ACC.get_or_init(|| {
696 // SAFETY: dlsym(RTLD_DEFAULT) returns the exported accessor
697 // address or NULL; the transmute (pointer-sized) is sound.
698 // The accessor runs exactly ONCE, lazily, to learn the slot
699 // address; the slot itself is process-lifetime storage.
700 unsafe {
701 let sym = libc::dlsym(libc::RTLD_DEFAULT, ($cname).as_ptr());
702 if sym.is_null() {
703 None
704 } else {
705 let acc = std::mem::transmute::<*mut c_void, Acc>(sym);
706 Some(acc() as usize)
707 }
708 }
709 });
710 slot.map(|a| a as *mut $t)
711 }
712 };
713}
714
715#[cfg(not(target_os = "linux"))]
716macro_rules! alloc_slot_reader {
717 ($reader:ident, $cname:expr, $t:ty) => {
718 fn $reader() -> Option<*mut $t> {
719 None
720 }
721 };
722}
723
724alloc_slot_reader!(foreign_malloc_slot, c"__xmlMalloc", xmlMallocFunc);
725alloc_slot_reader!(
726 foreign_malloc_atomic_slot,
727 c"__xmlMallocAtomic",
728 xmlMallocFunc
729);
730alloc_slot_reader!(foreign_realloc_slot, c"__xmlRealloc", xmlReallocFunc);
731alloc_slot_reader!(foreign_free_slot, c"__xmlFree", xmlFreeFunc);
732alloc_slot_reader!(foreign_mem_strdup_slot, c"__xmlMemStrdup", xmlStrdupFunc);
733
734/// Resolve the active allocator hook: the process-visible (core) slot when
735/// its accessor resolves, else the local exported variable via `local`.
736#[inline]
737fn allocator_slot<T: Copy>(foreign: Option<*mut T>, local: T) -> T {
738 match foreign {
739 Some(slot) => {
740 // SAFETY: the accessor returned a pointer to the core DSO's
741 // exported allocator slot (valid for the process lifetime); the
742 // stored value is a valid C fn pointer under the upstream
743 // setup-ordering contract.
744 unsafe { *slot }
745 }
746 None => local,
747 }
748}
749
750/// `xmlMallocFunc xmlMalloc` — the malloc hook (default: `xmlMallocDefault`).
751#[no_mangle]
752pub static mut xmlMalloc: xmlMallocFunc = xmlMallocDefault;
753
754/// `xmlMallocFunc xmlMallocAtomic` — the atomic-malloc hook.
755#[no_mangle]
756pub static mut xmlMallocAtomic: xmlMallocFunc = xmlMallocAtomicDefault;
757
758/// `xmlReallocFunc xmlRealloc` — the realloc hook.
759#[no_mangle]
760pub static mut xmlRealloc: xmlReallocFunc = xmlReallocDefault;
761
762/// `xmlFreeFunc xmlFree` — the free hook.
763#[no_mangle]
764pub static mut xmlFree: xmlFreeFunc = xmlFreeDefault;
765
766/// `xmlStrdupFunc xmlMemStrdup` — the strdup hook.
767#[no_mangle]
768pub static mut xmlMemStrdup: xmlStrdupFunc = xmlMemStrdupDefault;
769
770// ── LIBXML_THREAD_ALLOC_ENABLED accessors (upstream globals.c) ─────────────
771//
772// Upstream source builds with --with-thread-alloc export accessor FUNCTIONS
773// `xmlMallocFunc *__xmlMalloc(void)` etc. (globals.c, gated by
774// LIBXML_THREAD_ALLOC_ENABLED); its xmlmemory.h then redefines `xmlMalloc`
775// as `(*__xmlMalloc())`, so consumers compiled against the thread-alloc
776// profile reference these accessors instead of the variables. The candidate
777// implements them per upstream semantics: each returns a pointer to the
778// allocator slot the library uses — which is exactly the corresponding
779// exported variable above (single source of truth, R-000176) — so
780// `(*__xmlMalloc())(size)` is the candidate's `xmlMalloc` variable.
781//
782// The executed distro oracle (system 2.15.3) hides these five accessors
783// (built without thread-alloc / with hidden visibility), so they are
784// CUSTODIAN_EXTENSION exports in the disposition ledger: upstream-ABI-valid,
785// required by source-built consumers (e.g. the canonical source oracle in
786// the Phase-12 DOCKER-SUBSTITUTION court, whose libxslt references
787// __xmlFree/__xmlMalloc/__xmlRealloc).
788
789/// Upstream `xmlMallocFunc *__xmlMalloc(void)`.
790///
791/// # Safety
792///
793/// - The returned pointer is the address of the exported `xmlMalloc`
794/// variable; it stays valid for the process lifetime and may be read or
795/// written through exactly like upstream's thread-local allocator slot.
796#[no_mangle]
797pub unsafe extern "C" fn __xmlMalloc() -> *mut xmlMallocFunc {
798 core::ptr::addr_of_mut!(xmlMalloc)
799}
800
801/// Upstream `xmlMallocFunc *__xmlMallocAtomic(void)`.
802///
803/// # Safety
804///
805/// - The returned pointer is the address of the exported `xmlMallocAtomic`
806/// variable; it stays valid for the process lifetime.
807#[no_mangle]
808pub unsafe extern "C" fn __xmlMallocAtomic() -> *mut xmlMallocFunc {
809 core::ptr::addr_of_mut!(xmlMallocAtomic)
810}
811
812/// Upstream `xmlReallocFunc *__xmlRealloc(void)`.
813///
814/// # Safety
815///
816/// - The returned pointer is the address of the exported `xmlRealloc`
817/// variable; it stays valid for the process lifetime.
818#[no_mangle]
819pub unsafe extern "C" fn __xmlRealloc() -> *mut xmlReallocFunc {
820 core::ptr::addr_of_mut!(xmlRealloc)
821}
822
823/// Upstream `xmlFreeFunc *__xmlFree(void)`.
824///
825/// # Safety
826///
827/// - The returned pointer is the address of the exported `xmlFree`
828/// variable; it stays valid for the process lifetime.
829#[no_mangle]
830pub unsafe extern "C" fn __xmlFree() -> *mut xmlFreeFunc {
831 core::ptr::addr_of_mut!(xmlFree)
832}
833
834/// Upstream `xmlStrdupFunc *__xmlMemStrdup(void)`.
835///
836/// # Safety
837///
838/// - The returned pointer is the address of the exported `xmlMemStrdup`
839/// variable; it stays valid for the process lifetime.
840#[no_mangle]
841pub unsafe extern "C" fn __xmlMemStrdup() -> *mut xmlStrdupFunc {
842 core::ptr::addr_of_mut!(xmlMemStrdup)
843}
844
845// ═══════════════════════════════════════════════════════════════════════════════
846// Memory Debugging / Statistics
847// ═══════════════════════════════════════════════════════════════════════════════
848
849/// Return the total amount of memory currently allocated (approximate).
850///
851/// # UPSTREAM-PARITY
852///
853/// ```c
854/// int xmlMemUsed(void);
855/// ```
856///
857/// Returns upstream's `debugMemSize` counter, which is maintained ONLY by the
858/// debug allocator (`xmlMemMalloc`/`*Loc` surface). With the default
859/// allocator installed the counter stays 0 — byte-identical with the oracle
860/// (R-000178, verified by ALLOCATOR-DEFAULT-001). Custom allocator hooks
861/// never touch it (upstream contract).
862#[no_mangle]
863pub extern "C" fn xmlMemUsed() -> c_int {
864 MEM_USED.load(Ordering::Relaxed) as c_int
865}
866
867/// Return the current number of allocated blocks (approximate).
868///
869/// # UPSTREAM-PARITY
870///
871/// ```c
872/// int xmlMemBlocks(void);
873/// ```
874///
875/// Returns upstream's `debugMemBlocks` counter (debug allocator only; 0 with
876/// the default allocator — R-000178, byte-identical with the oracle).
877#[no_mangle]
878pub extern "C" fn xmlMemBlocks() -> c_int {
879 MEM_BLOCKS.load(Ordering::Relaxed) as c_int
880}
881
882/// Display memory allocation information to a file.
883///
884/// # UPSTREAM-PARITY
885///
886/// ```c
887/// void xmlMemDisplay(FILE *fp);
888/// ```
889///
890/// No-op — upstream 2.15.0 removed this feature (`@deprecated This feature
891/// was removed.`). The pre-Z.3 candidate printed aggregate counters; that was
892/// a divergence from the executed oracle and is removed (R-000131 sealed).
893///
894/// # SAFETY
895///
896/// - `fp` must be a valid FILE* pointer or NULL (unused)
897#[no_mangle]
898pub const unsafe extern "C" fn xmlMemDisplay(_fp: *mut c_void) {}
899
900/// Show memory allocation information.
901///
902/// # UPSTREAM-PARITY
903///
904/// ```c
905/// void xmlMemShow(FILE *fp, int nr);
906/// ```
907///
908/// No-op — upstream 2.15.0 removed this feature (`@deprecated This feature
909/// was removed.`); the candidate previously dumped the registry with a
910/// non-upstream ordering, a documented divergence that is now removed
911/// (R-000131 sealed).
912///
913/// # SAFETY
914///
915/// - `fp` must be a valid FILE* pointer or NULL (unused)
916#[no_mangle]
917pub const unsafe extern "C" fn xmlMemShow(_fp: *mut c_void, _nr: c_int) {}
918
919// ═══════════════════════════════════════════════════════════════════════════════
920// Convenience Functions (used internally)
921// ═══════════════════════════════════════════════════════════════════════════════
922
923/// Allocate zero-initialized memory.
924///
925/// # UPSTREAM-PARITY
926///
927/// This is like `xmlMalloc` followed by `memset(0)`, but some allocator
928/// hooks provide it directly.
929///
930/// # SAFETY
931///
932/// Same as `xmlMalloc`. The returned memory is zero-initialized.
933#[no_mangle]
934pub unsafe extern "C" fn xmlMallocZero(size: usize) -> *mut c_void {
935 // SAFETY: Delegates to xmlMallocImpl and zeroes the memory.
936 let ptr = unsafe { xmlMallocImpl(size) };
937 if !ptr.is_null() {
938 unsafe { ptr::write_bytes(ptr, 0, size) };
939 }
940 ptr
941}
942
943/// Allocate zero-initialized memory (atomic variant).
944///
945/// # UPSTREAM-PARITY
946///
947/// Like `xmlMallocAtomic` followed by zero-initialization.
948///
949/// # SAFETY
950///
951/// The function touches crate-global state only; it is safe
952/// as long as the caller respects the library's global
953/// initialization/cleanup ordering (xmlInitParser before use,
954/// xmlCleanupParser only after all users are done).
955///
956/// Violating the global lifecycle ordering, or calling this after
957/// teardown or from a signal handler, is undefined behavior.
958#[no_mangle]
959pub unsafe extern "C" fn xmlMallocAtomicZero(size: usize) -> *mut c_void {
960 // SAFETY: Delegates to xmlMallocAtomicImpl and zeroes the memory.
961 let ptr = unsafe { xmlMallocAtomicImpl(size) };
962 if !ptr.is_null() {
963 unsafe { ptr::write_bytes(ptr, 0, size) };
964 }
965 ptr
966}
967
968/// Reallocate and zero-initialize the new portion.
969///
970/// # UPSTREAM-PARITY
971///
972/// Like `xmlRealloc`, but zeroes any newly allocated bytes.
973///
974/// # SAFETY
975///
976/// Same as `xmlRealloc`.
977#[no_mangle]
978pub unsafe extern "C" fn xmlReallocZero(
979 ptr: *mut c_void,
980 old_size: usize,
981 new_size: usize,
982) -> *mut c_void {
983 // SAFETY: Delegates to xmlRealloc and zeroes the new portion.
984 let new_ptr = unsafe { xmlReallocImpl(ptr, new_size) };
985 if !new_ptr.is_null() && new_size > old_size {
986 unsafe {
987 ptr::write_bytes(new_ptr.add(old_size), 0, new_size - old_size);
988 }
989 }
990 new_ptr
991}
992
993// ═══════════════════════════════════════════════════════════════════════════════
994// Debug Allocator (Optional)
995// ═══════════════════════════════════════════════════════════════════════════════
996
997/// Initialize the memory layer with debugging support.
998///
999/// # UPSTREAM-PARITY
1000///
1001/// ```c
1002/// int xmlInitMemory(void);
1003/// ```
1004///
1005/// Initializes the memory subsystem. Returns 0 on success.
1006/// This is called automatically by `xmlInitParser`.
1007#[no_mangle]
1008pub const extern "C" fn xmlInitMemory() -> c_int {
1009 0
1010}
1011
1012/// Clean up the memory layer.
1013///
1014/// # UPSTREAM-PARITY
1015///
1016/// ```c
1017/// void xmlCleanupMemory(void);
1018/// ```
1019#[no_mangle]
1020pub const extern "C" fn xmlCleanupMemory() {
1021 // Phase 1: no cleanup needed for the default allocator.
1022}
1023
1024// ═══════════════════════════════════════════════════════════════════════════════
1025// Legacy-named allocator API (upstream xmlmemory.h)
1026// ═══════════════════════════════════════════════════════════════════════════════
1027// Upstream xmlmemory.h historically exported `xmlMemMalloc`, `xmlMemFree`,
1028// `xmlMemRealloc`, `xmlMemoryStrdup`, the `*Loc` location-tracking variants,
1029// `xmlMemSize`, `xmlMemDisplayLast` and `xmlMemoryDump` alongside the modern
1030// names. Downstream code (older consumers, some language bindings) links
1031// against these names, so the candidate exports them with identical
1032// semantics. In upstream 2.15.0 these ARE the debug allocator: always
1033// libc-backed, independent of the hook variables, and tracked by
1034// `debugMemSize`/`debugMemBlocks` (with the MEMHDR tag enabling
1035// `xmlMemSize`). The candidate mirrors that exactly: the debug-named
1036// functions and the `*Loc` variants are always libc-backed + registry- and
1037// counter-tracked, do NOT route through the exported variables, and the
1038// `*Loc` location arguments are accepted and ignored — exactly like
1039// upstream's `ATTRIBUTE_UNUSED` parameters (R-000131 sealed). The candidate
1040// returns plain libc pointers (no MEMHDR prefix), so a debug-surface block
1041// can also be freed with `xmlFree` — a safe superset of the upstream
1042// contract (upstream requires `xmlMemFree` for such blocks).
1043
1044/// Debug-surface malloc: libc + counters + registry (upstream xmlMemMalloc).
1045///
1046/// # Safety
1047///
1048/// - `size` must be a valid allocation size; the underlying `default_malloc`
1049/// follows the libc contract (0 handled by the platform).
1050/// - `file` must be NULL or a valid pointer to a NUL-terminated C string that
1051/// stays valid for the duration of the call (it is stored by address only,
1052/// never dereferenced here).
1053/// - The returned pointer (NULL on failure, with counters untouched) is owned
1054/// by the caller and must be freed exactly once via `debug_free` or
1055/// `xmlMemFree`; never dereferenced after freeing.
1056unsafe fn debug_malloc(size: usize, file: *const c_char, line: c_int) -> *mut c_void {
1057 let ptr = unsafe { default_malloc(size) };
1058 if !ptr.is_null() {
1059 MEM_USED.fetch_add(size, Ordering::Relaxed);
1060 MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
1061 unsafe { block_record(ptr, size, file, line) };
1062 }
1063 ptr
1064}
1065
1066/// Debug-surface realloc: libc + counters + registry (upstream xmlMemRealloc).
1067///
1068/// # Safety
1069///
1070/// - `ptr` must be NULL or a valid pointer previously returned by the debug
1071/// surface (`debug_malloc`/`debug_realloc`) or by a matching libc
1072/// allocation, and not yet freed; NULL behaves like malloc.
1073/// - On failure the old block is left intact and stays recorded; on success
1074/// the old pointer is invalidated and the returned pointer must be freed
1075/// exactly once via `debug_free`/`xmlMemFree`.
1076/// - `file` must be NULL or a valid NUL-terminated C string valid for the
1077/// duration of the call (stored by address only).
1078unsafe fn debug_realloc(
1079 ptr: *mut c_void,
1080 size: usize,
1081 file: *const c_char,
1082 line: c_int,
1083) -> *mut c_void {
1084 let new_ptr = unsafe { default_realloc(ptr, size) };
1085 if !new_ptr.is_null() {
1086 let old_size = unsafe { block_forget(ptr) };
1087 if let Some(old) = old_size {
1088 MEM_USED.fetch_add(size.saturating_sub(old), Ordering::Relaxed);
1089 } else if ptr.is_null() {
1090 MEM_USED.fetch_add(size, Ordering::Relaxed);
1091 MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
1092 }
1093 unsafe { block_record(new_ptr, size, file, line) };
1094 }
1095 new_ptr
1096}
1097
1098/// Debug-surface strdup: libc + counters + registry (upstream xmlMemoryStrdup).
1099///
1100/// # Safety
1101///
1102/// - `str` must be NULL or a valid pointer to a NUL-terminated C string
1103/// readable through its full length (including the terminator) for the
1104/// duration of the call; NULL yields NULL.
1105/// - `file` must be NULL or a valid NUL-terminated C string valid for the
1106/// duration of the call (stored by address only).
1107/// - The returned pointer (NULL on failure) must be freed exactly once via
1108/// `debug_free`/`xmlMemFree`; never dereferenced after freeing.
1109unsafe fn debug_strdup(str: *const c_char, file: *const c_char, line: c_int) -> *mut c_void {
1110 if str.is_null() {
1111 return ptr::null_mut();
1112 }
1113 let ptr = unsafe { default_strdup(str) };
1114 if !ptr.is_null() {
1115 let len = unsafe { libc::strlen(str) } + 1;
1116 MEM_USED.fetch_add(len, Ordering::Relaxed);
1117 MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
1118 unsafe { block_record(ptr, len, file, line) };
1119 }
1120 ptr
1121}
1122
1123/// Debug-surface free: registry/counter removal + libc free (upstream xmlMemFree).
1124/// A foreign pointer (not in the registry) is freed without touching the
1125/// counters — a safe divergence from upstream's tag-error print (which would
1126/// pollute stderr).
1127///
1128/// # Safety
1129///
1130/// - `ptr` must be NULL (a no-op) or a valid pointer previously returned by
1131/// the debug surface (`debug_malloc`/`debug_realloc`/`debug_strdup`) or by
1132/// a matching libc allocation; it must not be freed twice and must not be
1133/// dereferenced after this call.
1134/// - The registry and counters are only touched when the pointer was recorded;
1135/// a foreign pointer is still freed via libc.
1136unsafe fn debug_free(ptr: *mut c_void) {
1137 if ptr.is_null() {
1138 return;
1139 }
1140 let old_size = unsafe { block_forget(ptr) };
1141 unsafe { default_free(ptr) };
1142 if let Some(old) = old_size {
1143 MEM_USED.fetch_sub(old, Ordering::Relaxed);
1144 MEM_BLOCKS.fetch_sub(1, Ordering::Relaxed);
1145 }
1146}
1147
1148/// Allocate memory through the debug allocator (upstream xmlmemory.h).
1149///
1150/// ```c
1151/// void *xmlMemMalloc(size_t size);
1152/// ```
1153///
1154/// Always libc-backed and tracked (upstream debug allocator contract): the
1155/// block is recorded so `xmlMemSize`/`xmlMemUsed`/`xmlMemBlocks` observe it.
1156///
1157/// # SAFETY
1158///
1159/// - The returned pointer must be freed with `xmlMemFree` (or `xmlFree` —
1160/// plain libc pointer, candidate superset)
1161#[no_mangle]
1162pub unsafe extern "C" fn xmlMemMalloc(size: usize) -> *mut c_void {
1163 unsafe { debug_malloc(size, ptr::null(), 0) }
1164}
1165
1166/// Free memory through the debug allocator (upstream xmlmemory.h).
1167///
1168/// ```c
1169/// void xmlMemFree(void *ptr);
1170/// ```
1171///
1172/// Un-records the block and frees it. NULL is a no-op.
1173///
1174/// # SAFETY
1175///
1176/// - `ptr` must be valid pointers (or NULL
1177/// where the upstream C contract allows), obtained from the
1178/// matching constructor/owner and not yet freed; the callee may
1179/// take or keep ownership exactly as the C API specifies.
1180#[no_mangle]
1181pub unsafe extern "C" fn xmlMemFree(ptr: *mut c_void) {
1182 unsafe { debug_free(ptr) };
1183}
1184
1185/// Reallocate memory through the debug allocator (upstream xmlmemory.h).
1186///
1187/// ```c
1188/// void *xmlMemRealloc(void *ptr, size_t size);
1189/// ```
1190///
1191/// C realloc semantics + registry maintenance (upstream xmlMemRealloc:
1192/// NULL ptr allocates, failure leaves the old block recorded and intact).
1193///
1194/// # SAFETY
1195///
1196/// - `ptr` must be valid pointers (or NULL
1197/// where the upstream C contract allows), obtained from the
1198/// matching constructor/owner and not yet freed; the callee may
1199/// take or keep ownership exactly as the C API specifies.
1200#[no_mangle]
1201pub unsafe extern "C" fn xmlMemRealloc(ptr: *mut c_void, size: usize) -> *mut c_void {
1202 unsafe { debug_realloc(ptr, size, ptr::null(), 0) }
1203}
1204
1205/// Duplicate a string through the debug allocator (upstream xmlmemory.h).
1206///
1207/// ```c
1208/// void *xmlMemoryStrdup(const char *str);
1209/// ```
1210///
1211/// NULL returns NULL (upstream xmlPosixStrdup/xmlCharStrdup contract).
1212///
1213/// # SAFETY
1214///
1215/// - `str` must point to a valid NUL-terminated string, or NULL
1216/// - The returned pointer must be freed with `xmlMemFree` (or `xmlFree`)
1217#[no_mangle]
1218pub unsafe extern "C" fn xmlMemoryStrdup(str: *const c_char) -> *mut c_void {
1219 unsafe { debug_strdup(str, ptr::null(), 0) }
1220}
1221
1222/// Allocate memory, recording the allocation site (upstream xmlmemory.h).
1223///
1224/// ```c
1225/// void *xmlMallocLoc(size_t size, const char *file, int line);
1226/// ```
1227///
1228/// Debug-allocator contract (upstream xmlMallocLoc -> xmlMemMalloc): always
1229/// libc-backed + tracked, independent of the hook variables. Upstream 2.15.0
1230/// ignores the file/line arguments (`ATTRIBUTE_UNUSED`); the candidate
1231/// records them in the registry so `xmlMemSize` works (R-000131 sealed).
1232///
1233/// # SAFETY
1234///
1235///
1236/// - `file` must point to valid NUL-terminated
1237/// strings (or NULL where the C contract allows) for the lifetime
1238/// of the call.
1239///
1240/// The caller must not race this call with concurrent mutation of the
1241/// same objects from other threads (per-object state is not internally
1242/// synchronized). Violating any of the above is undefined behavior.
1243///
1244/// Exercised by the C-API differential courts
1245/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1246/// courts; those pass byte-for-byte against the upstream oracle.
1247#[no_mangle]
1248pub unsafe extern "C" fn xmlMallocLoc(
1249 size: usize,
1250 file: *const c_char,
1251 line: c_int,
1252) -> *mut c_void {
1253 unsafe { debug_malloc(size, file, line) }
1254}
1255
1256/// Allocate memory, recording the allocation site (upstream xmlmemory.h).
1257///
1258/// ```c
1259/// void *xmlMallocAtomicLoc(size_t size, const char *file, int line);
1260/// ```
1261///
1262/// Upstream xmlMallocAtomicLoc -> xmlMemMalloc: a plain (non-zeroed) tracked
1263/// allocation. The pre-Z.3 candidate zeroed it via `xmlMallocZero` — a real
1264/// divergence, fixed in 11.1-Z.3 (R-000178 narrative).
1265///
1266/// # SAFETY
1267///
1268///
1269/// - `file` must point to valid NUL-terminated
1270/// strings (or NULL where the C contract allows) for the lifetime
1271/// of the call.
1272///
1273/// The caller must not race this call with concurrent mutation of the
1274/// same objects from other threads (per-object state is not internally
1275/// synchronized). Violating any of the above is undefined behavior.
1276///
1277/// Exercised by the C-API differential courts
1278/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1279/// courts; those pass byte-for-byte against the upstream oracle.
1280#[no_mangle]
1281pub unsafe extern "C" fn xmlMallocAtomicLoc(
1282 size: usize,
1283 file: *const c_char,
1284 line: c_int,
1285) -> *mut c_void {
1286 unsafe { debug_malloc(size, file, line) }
1287}
1288
1289/// Reallocate memory, recording the allocation site (upstream xmlmemory.h).
1290///
1291/// ```c
1292/// void *xmlReallocLoc(void *ptr, size_t size, const char *file, int line);
1293/// ```
1294///
1295/// Debug-allocator contract (upstream xmlReallocLoc -> xmlMemRealloc): the
1296/// old record is superseded on success, kept on failure.
1297///
1298/// # SAFETY
1299///
1300/// - `ptr` must be valid pointers (or NULL
1301/// where the upstream C contract allows), obtained from the
1302/// matching constructor/owner and not yet freed; the callee may
1303/// take or keep ownership exactly as the C API specifies.
1304///
1305/// - `file` must point to valid NUL-terminated
1306/// strings (or NULL where the C contract allows) for the lifetime
1307/// of the call.
1308///
1309/// The caller must not race this call with concurrent mutation of the
1310/// same objects from other threads (per-object state is not internally
1311/// synchronized). Violating any of the above is undefined behavior.
1312///
1313/// Exercised by the C-API differential courts
1314/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1315/// courts; those pass byte-for-byte against the upstream oracle.
1316#[no_mangle]
1317pub unsafe extern "C" fn xmlReallocLoc(
1318 ptr: *mut c_void,
1319 size: usize,
1320 file: *const c_char,
1321 line: c_int,
1322) -> *mut c_void {
1323 unsafe { debug_realloc(ptr, size, file, line) }
1324}
1325
1326/// Duplicate a string, recording the allocation site (upstream xmlmemory.h).
1327///
1328/// ```c
1329/// void *xmlMemStrdupLoc(const char *str, const char *file, int line);
1330/// ```
1331///
1332/// Debug-allocator contract (upstream xmlMemStrdupLoc -> xmlMemoryStrdup).
1333///
1334/// # SAFETY
1335///
1336///
1337/// - `str`, `file` must point to valid NUL-terminated
1338/// strings (or NULL where the C contract allows) for the lifetime
1339/// of the call.
1340///
1341/// The caller must not race this call with concurrent mutation of the
1342/// same objects from other threads (per-object state is not internally
1343/// synchronized). Violating any of the above is undefined behavior.
1344///
1345/// Exercised by the C-API differential courts
1346/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1347/// courts; those pass byte-for-byte against the upstream oracle.
1348#[no_mangle]
1349pub unsafe extern "C" fn xmlMemStrdupLoc(
1350 str: *const c_char,
1351 file: *const c_char,
1352 line: c_int,
1353) -> *mut c_void {
1354 unsafe { debug_strdup(str, file, line) }
1355}
1356
1357/// Return the size of an allocated block (upstream xmlmemory.h).
1358///
1359/// ```c
1360/// size_t xmlMemSize(void *ptr);
1361/// ```
1362///
1363/// Returns the recorded size for debug-surface blocks (`xmlMemMalloc`/`*Loc`
1364/// surface) and 0 for everything else — matching upstream's MEMHDR tag
1365/// lookup, which misses on plain-malloc blocks (default allocator) and on
1366/// foreign pointers (R-000178; byte-identical with the oracle).
1367///
1368/// # SAFETY
1369///
1370/// - `ptr` must be valid pointers (or NULL
1371/// where the upstream C contract allows), obtained from the
1372/// matching constructor/owner and not yet freed; the callee may
1373/// take or keep ownership exactly as the C API specifies.
1374#[no_mangle]
1375pub unsafe extern "C" fn xmlMemSize(ptr: *mut c_void) -> usize {
1376 if ptr.is_null() {
1377 return 0;
1378 }
1379 BLOCKS
1380 .lock()
1381 .get(&(ptr as usize))
1382 .map(|m| m.size)
1383 .unwrap_or(0)
1384}
1385
1386/// Display a limited amount of memory debug information (upstream xmlmemory.h).
1387///
1388/// ```c
1389/// void xmlMemDisplayLast(FILE *fp, long nbBytes);
1390/// ```
1391///
1392/// No-op — upstream 2.15.0 removed this feature (`@deprecated This feature
1393/// was removed.`); the pre-Z.3 candidate dumped the registry with a
1394/// non-upstream format, a divergence now removed (R-000131 sealed).
1395///
1396/// # SAFETY
1397///
1398/// - `fp` must be a valid FILE* pointer or NULL (unused)
1399#[no_mangle]
1400pub const unsafe extern "C" fn xmlMemDisplayLast(_fp: *mut c_void, _nb_bytes: c_long) {}
1401
1402/// Dump memory allocation statistics (upstream xmlmemory.h).
1403///
1404/// ```c
1405/// void xmlMemoryDump(void);
1406/// ```
1407///
1408/// No-op — upstream 2.15.0 removed this feature (`@deprecated This feature
1409/// was removed.`).
1410///
1411/// # SAFETY
1412///
1413/// The function touches crate-global state only; it is safe
1414/// as long as the caller respects the library's global
1415/// initialization/cleanup ordering (xmlInitParser before use,
1416/// xmlCleanupParser only after all users are done).
1417#[no_mangle]
1418pub const unsafe extern "C" fn xmlMemoryDump() {
1419 // Upstream: empty body (feature removed in 2.15.0).
1420}
1421
1422// ═══════════════════════════════════════════════════════════════════════════════
1423// Tests
1424// ═══════════════════════════════════════════════════════════════════════════════
1425
1426#[cfg(test)]
1427mod tests {
1428 use super::*;
1429 use core::ptr;
1430
1431 /// Test basic allocation and deallocation.
1432 ///
1433 /// # Safety
1434 ///
1435 /// - `ptr` is the non-NULL result of `xmlMalloc(100)` (asserted): it is a
1436 /// valid heap allocation owned by the caller, freed exactly once by
1437 /// `xmlFree`, and not used afterwards.
1438 #[test]
1439 fn test_malloc_free() {
1440 unsafe {
1441 let ptr = xmlMalloc(100);
1442 assert!(!ptr.is_null(), "xmlMalloc(100) returned NULL");
1443 xmlFree(ptr);
1444 }
1445 }
1446
1447 /// Test that xmlMalloc(0) returns a valid pointer.
1448 ///
1449 /// # Safety
1450 ///
1451 /// - `xmlMalloc(0)` may return NULL or a unique non-NULL pointer per the
1452 /// platform contract; a non-NULL result is a valid allocation freed
1453 /// exactly once by `xmlFree` and not used afterwards.
1454 #[test]
1455 fn test_malloc_zero() {
1456 unsafe {
1457 let ptr = xmlMalloc(0);
1458 // malloc(0) may return NULL or a valid pointer.
1459 // If non-NULL, it must be freeable.
1460 if !ptr.is_null() {
1461 xmlFree(ptr);
1462 }
1463 }
1464 }
1465
1466 /// Test xmlFree(NULL) is a no-op.
1467 ///
1468 /// # Safety
1469 ///
1470 /// - Passing NULL to `xmlFree` is accepted and performs no operation; no
1471 /// pointer is dereferenced.
1472 #[test]
1473 fn test_free_null() {
1474 unsafe {
1475 xmlFree(ptr::null_mut());
1476 // Should not crash
1477 }
1478 }
1479
1480 /// Test xmlRealloc.
1481 ///
1482 /// # Safety
1483 ///
1484 /// - `ptr` must be a valid non-NULL allocation from `xmlMalloc`; on the
1485 /// successful `xmlRealloc` here the old pointer is invalidated and only
1486 /// the returned `new_ptr` (non-NULL, asserted) is freed exactly once by
1487 /// `xmlFree` and not used afterwards.
1488 #[test]
1489 fn test_realloc() {
1490 unsafe {
1491 let ptr = xmlMalloc(50);
1492 assert!(!ptr.is_null());
1493 let new_ptr = xmlRealloc(ptr, 100);
1494 assert!(!new_ptr.is_null());
1495 xmlFree(new_ptr);
1496 }
1497 }
1498
1499 /// Test xmlMemStrdup.
1500 ///
1501 /// # Safety
1502 ///
1503 /// - `s` points to a valid NUL-terminated byte string of 6 bytes (the
1504 /// literal `hello` plus terminator) that stays alive for the call.
1505 /// - `dup` is the non-NULL result of `xmlMemStrdup`, a fresh allocation of
1506 /// at least 6 bytes readable as a slice; it is freed exactly once by
1507 /// `xmlFree` and not used afterwards.
1508 #[test]
1509 fn test_mem_strdup() {
1510 unsafe {
1511 let s = b"hello\0" as *const u8 as *const c_char;
1512 let dup = xmlMemStrdup(s);
1513 assert!(!dup.is_null());
1514 // Compare the strings
1515 let orig_slice = std::slice::from_raw_parts(s as *const u8, 6);
1516 let dup_slice = std::slice::from_raw_parts(dup as *const u8, 6);
1517 assert_eq!(orig_slice, dup_slice);
1518 xmlFree(dup);
1519 }
1520 }
1521
1522 /// Test xmlMallocZero returns zeroed memory.
1523 ///
1524 /// # Safety
1525 ///
1526 /// - `ptr` is the non-NULL result of `xmlMallocZero(100)`: a valid
1527 /// allocation of at least 100 bytes, readable as a 100-byte slice
1528 /// while alive, and freed exactly once by `xmlFree` afterwards.
1529 #[test]
1530 fn test_malloc_zero_init() {
1531 unsafe {
1532 let ptr = xmlMallocZero(100) as *mut u8;
1533 assert!(!ptr.is_null());
1534 let slice = std::slice::from_raw_parts(ptr, 100);
1535 assert!(slice.iter().all(|&b| b == 0));
1536 xmlFree(ptr as *mut c_void);
1537 }
1538 }
1539
1540 /// Test custom allocator setup/get (R-000176: int returns, single source
1541 /// of truth — `xmlMemSetup` writes the exported variables and `xmlMemGet`
1542 /// reads them back).
1543 ///
1544 /// # Safety
1545 ///
1546 /// - Each output pointer passed to `xmlMemGet` is derived from a live
1547 /// stack local of matching type, so every non-NULL output points to
1548 /// valid, aligned, writable storage for the duration of the call; NULL
1549 /// outputs are tolerated and skipped.
1550 /// - The function pointers written back must be treated as valid
1551 /// C-compatible hooks (they are the currently installed allocator
1552 /// functions).
1553 #[test]
1554 fn test_mem_setup_get() {
1555 unsafe {
1556 let mut free_func: Option<xmlFreeFunc> = None;
1557 let mut malloc_func: Option<xmlMallocFunc> = None;
1558 let mut realloc_func: Option<xmlReallocFunc> = None;
1559 let mut strdup_func: Option<xmlStrdupFunc> = None;
1560
1561 let ret = xmlMemGet(
1562 &mut free_func as *mut _,
1563 &mut malloc_func as *mut _,
1564 &mut realloc_func as *mut _,
1565 &mut strdup_func as *mut _,
1566 );
1567 assert_eq!(ret, 0, "xmlMemGet must return 0");
1568 assert!(malloc_func.is_some());
1569 assert!(free_func.is_some());
1570 assert!(realloc_func.is_some());
1571 assert!(strdup_func.is_some());
1572
1573 // NULL output pointers are tolerated (upstream xmlmemory.c).
1574 let ret = xmlMemGet(
1575 ptr::null_mut(),
1576 &mut malloc_func as *mut _,
1577 ptr::null_mut(),
1578 ptr::null_mut(),
1579 );
1580 assert_eq!(ret, 0);
1581 assert!(malloc_func.is_some());
1582 }
1583 }
1584
1585 /// Test the GC allocator hooks: 5-argument Setup/Get with the dedicated
1586 /// `mallocAtomicFunc` slot (R-000176 — previously 4-arg and void).
1587 ///
1588 /// # Safety
1589 ///
1590 /// - Each output pointer passed to `xmlGcMemGet` is derived from a live
1591 /// stack local of matching type, so every non-NULL output points to
1592 /// valid, aligned, writable storage for the duration of the call; NULL
1593 /// outputs are tolerated and skipped.
1594 #[test]
1595 fn test_gc_mem_setup_get() {
1596 unsafe {
1597 let mut free_func: Option<xmlFreeFunc> = None;
1598 let mut malloc_func: Option<xmlMallocFunc> = None;
1599 let mut malloc_atomic_func: Option<xmlMallocFunc> = None;
1600 let mut realloc_func: Option<xmlReallocFunc> = None;
1601 let mut strdup_func: Option<xmlStrdupFunc> = None;
1602
1603 let ret = xmlGcMemGet(
1604 &mut free_func as *mut _,
1605 &mut malloc_func as *mut _,
1606 &mut malloc_atomic_func as *mut _,
1607 &mut realloc_func as *mut _,
1608 &mut strdup_func as *mut _,
1609 );
1610 assert_eq!(ret, 0, "xmlGcMemGet must return 0");
1611 assert!(free_func.is_some());
1612 assert!(malloc_func.is_some());
1613 assert!(malloc_atomic_func.is_some());
1614 assert!(realloc_func.is_some());
1615 assert!(strdup_func.is_some());
1616 }
1617 }
1618
1619 /// Test `xmlMemSetup` NULL validation returns -1 (upstream xmlmemory.c)
1620 /// and that a NULL `mallocAtomicFunc` makes `xmlGcMemSetup` fail.
1621 ///
1622 /// # Safety
1623 ///
1624 /// - `default_malloc` cast to `xmlMallocFunc` is a valid C-compatible
1625 /// malloc-shaped function pointer; it is only passed as an argument here
1626 /// and never called by this test.
1627 /// - The NULL hooks are rejected with -1 before any write, so no
1628 /// allocator state is modified by this test.
1629 #[test]
1630 fn test_mem_setup_null_rejected() {
1631 unsafe {
1632 let ret = xmlMemSetup(None, Some(default_malloc as xmlMallocFunc), None, None);
1633 assert_eq!(ret, -1, "NULL hook must be rejected with -1");
1634 let ret = xmlGcMemSetup(
1635 None,
1636 Some(default_malloc as xmlMallocFunc),
1637 Some(default_malloc as xmlMallocFunc),
1638 None,
1639 None,
1640 );
1641 assert_eq!(ret, -1);
1642 }
1643 }
1644
1645 /// Test the single-source-of-truth model (R-000176): a direct assignment
1646 /// to the exported `xmlMalloc` variable is observed by `xmlMemGet` AND by
1647 /// actual internal allocations through `xmlMallocImpl`.
1648 ///
1649 /// # Safety
1650 ///
1651 /// - The exported `static mut` allocator variables are read and written
1652 /// directly, which is only valid under the upstream single-threaded
1653 /// setup ordering: no other thread may allocate or read the variables
1654 /// concurrently with these assignments.
1655 /// - The values installed (`xmlMallocDefault`/`xmlFreeDefault`) are valid
1656 /// malloc/free-shaped functions; `p` from `xmlMallocImpl` is a valid
1657 /// allocation freed exactly once by `xmlFreeImpl`; the prior hook values
1658 /// are restored before the block ends.
1659 #[test]
1660 fn test_direct_assignment_coherence() {
1661 unsafe {
1662 let saved = xmlMalloc;
1663 let saved_free = xmlFree;
1664 // Install a counting hook via direct variable assignment.
1665 xmlMalloc = xmlMallocDefault;
1666 xmlFree = xmlFreeDefault;
1667
1668 // xmlMemGet reads the variable.
1669 let mut malloc_func: Option<xmlMallocFunc> = None;
1670 let ret = xmlMemGet(
1671 ptr::null_mut(),
1672 &mut malloc_func as *mut _,
1673 ptr::null_mut(),
1674 ptr::null_mut(),
1675 );
1676 assert_eq!(ret, 0);
1677 assert!(
1678 core::ptr::fn_addr_eq(malloc_func.unwrap(), xmlMallocDefault as xmlMallocFunc,),
1679 "xmlMemGet must return the directly-assigned variable"
1680 );
1681
1682 // Internal allocation routes through the variable.
1683 let p = xmlMallocImpl(64);
1684 assert!(!p.is_null());
1685 xmlFreeImpl(p);
1686
1687 // Restore the defaults so other tests are unaffected.
1688 xmlMalloc = saved;
1689 xmlFree = saved_free;
1690 }
1691 }
1692
1693 /// Test xmlMemUsed and xmlMemBlocks return 0 with the default allocator
1694 /// and the debug surface is tracked (R-000178: byte-identical with the
1695 /// oracle — default malloc untracked, xmlMemMalloc/*Loc tracked).
1696 ///
1697 /// # Safety
1698 ///
1699 /// - `ptr` from `xmlMalloc` is a valid libc allocation freed exactly once
1700 /// by `xmlFree`; `xmlMemSize` reads the registry by address only and is
1701 /// safe on freed default-allocator pointers (returns 0).
1702 /// - `dptr` from `xmlMallocLoc` and `rptr` from `xmlReallocLoc` are
1703 /// debug-surface allocations: on success the old pointer is invalidated
1704 /// and only `rptr` is freed, exactly once, by `xmlMemFree`.
1705 #[test]
1706 fn test_mem_stats() {
1707 unsafe {
1708 // Default allocator: plain libc, NOT tracked (oracle contract).
1709 let ptr = xmlMalloc(100);
1710 assert!(!ptr.is_null());
1711 assert_eq!(xmlMemSize(ptr), 0, "default-allocator blocks are untracked");
1712 xmlFree(ptr);
1713 assert_eq!(xmlMemSize(ptr), 0);
1714
1715 // Debug surface: tracked, xmlMemSize returns the recorded size.
1716 let dptr = xmlMallocLoc(100, c"mem.c".as_ptr(), 42);
1717 assert!(!dptr.is_null());
1718 assert_eq!(xmlMemSize(dptr), 100, "debug-surface blocks are tracked");
1719 let rptr = xmlReallocLoc(dptr, 200, c"mem.c".as_ptr(), 43);
1720 assert!(!rptr.is_null());
1721 // The realloc record supersedes the old one. glibc may grow the
1722 // block in place (rptr == dptr; the block at that address is then
1723 // recorded at 200) or MOVE it (the old address is released and
1724 // xmlMemSize(old) == 0 while rptr is the live 200-byte block). Both
1725 // are valid libc behaviors — assert per case so the test is
1726 // environment-independent (CI glibc moves; some hosts grow in
1727 // place).
1728 assert_eq!(xmlMemSize(rptr), 200);
1729 if rptr == dptr {
1730 assert_eq!(xmlMemSize(dptr), 200);
1731 } else {
1732 assert_eq!(
1733 xmlMemSize(dptr),
1734 0,
1735 "a moved realloc releases the old address"
1736 );
1737 }
1738 xmlMemFree(rptr);
1739 assert_eq!(xmlMemSize(rptr), 0);
1740
1741 // xmlMemSize(NULL) is 0.
1742 assert_eq!(xmlMemSize(ptr::null_mut()), 0);
1743 }
1744 }
1745
1746 /// Test the default allocator follows the C/libc contract exactly
1747 /// (R-000178): malloc(0) non-NULL, realloc(p,0) NULL after freeing,
1748 /// realloc(NULL,n) == malloc, realloc failure leaves the old block,
1749 /// malloc(SIZE_MAX) NULL, strdup(NULL) NULL.
1750 ///
1751 /// # Safety
1752 ///
1753 /// - Each non-NULL allocation is freed exactly once by `xmlFree`, and
1754 /// pointers are not used after freeing; in particular `q` is freed by
1755 /// the `realloc(p, 0)` call that returns NULL (glibc contract), so it
1756 /// must not be used afterwards, and `r` is freed explicitly after the
1757 /// failed huge realloc leaves it intact.
1758 /// - NULL arguments to `xmlRealloc`, `xmlMemStrdup` and `xmlFree` are
1759 /// accepted per the C contract.
1760 #[test]
1761 fn test_default_libc_semantics() {
1762 unsafe {
1763 // malloc(0): glibc returns a unique non-NULL pointer.
1764 let p0 = xmlMalloc(0);
1765 assert!(!p0.is_null(), "malloc(0) must be non-NULL on glibc");
1766 xmlFree(p0);
1767
1768 // realloc(NULL, n) allocates.
1769 let p = xmlRealloc(ptr::null_mut(), 16);
1770 assert!(!p.is_null());
1771 // grow: content preserved.
1772 let q = xmlRealloc(p, 256);
1773 assert!(!q.is_null());
1774 // realloc(p, 0): glibc frees and returns NULL.
1775 let z = xmlRealloc(q, 0);
1776 assert!(z.is_null(), "realloc(p, 0) returns NULL on glibc");
1777
1778 // realloc failure: old block intact.
1779 let r = xmlMalloc(8);
1780 assert!(!r.is_null());
1781 let huge = xmlRealloc(r, usize::MAX);
1782 assert!(huge.is_null(), "realloc to SIZE_MAX must fail");
1783 xmlFree(r);
1784
1785 // malloc failure.
1786 let m = xmlMalloc(usize::MAX);
1787 assert!(m.is_null(), "malloc(SIZE_MAX) must fail");
1788
1789 // strdup(NULL) returns NULL (upstream xmlPosixStrdup).
1790 let d = xmlMemStrdup(ptr::null());
1791 assert!(d.is_null());
1792
1793 // free(NULL) no-op.
1794 xmlFree(ptr::null_mut());
1795 }
1796 }
1797
1798 /// Test the debug-named surface is independent of the hook variables and
1799 /// always libc-backed + tracked (upstream debug-allocator contract).
1800 ///
1801 /// # Safety
1802 ///
1803 /// - `p`, `q` and `s` are non-NULL debug-surface allocations; after the
1804 /// successful `xmlMemRealloc` only `q` remains valid, and `q` and `s`
1805 /// are each freed exactly once by `xmlMemFree` and not used afterwards.
1806 /// - `xmlMemFree(NULL)` is a no-op; `xmlMemSize` reads the registry by
1807 /// address only.
1808 #[test]
1809 fn test_debug_surface_tracked() {
1810 unsafe {
1811 let p = xmlMemMalloc(64);
1812 assert!(!p.is_null());
1813 assert_eq!(xmlMemSize(p), 64);
1814 let q = xmlMemRealloc(p, 128);
1815 assert!(!q.is_null());
1816 assert_eq!(xmlMemSize(q), 128);
1817 let s = xmlMemoryStrdup(c"dbg".as_ptr());
1818 assert!(!s.is_null());
1819 assert_eq!(xmlMemSize(s), 4);
1820 xmlMemFree(q);
1821 xmlMemFree(s);
1822 // xmlMemFree(NULL) no-op.
1823 xmlMemFree(ptr::null_mut());
1824 }
1825 }
1826}