Skip to main content

libxml_rs/xml/globals/
mod.rs

1//! Global state management (§57, §85 Phase 1).
2//!
3//! Manages all library-wide global state:
4//!
5//! - Parser defaults (validity checking, entity substitution, blanks, etc.)
6//! - Generic and structured error callbacks
7//! - Catalog defaults
8//! - Memory hooks (connects to allocator.rs)
9//! - Thread-local state for errors
10//! - Initialization/cleanup reference counting
11//!
12//! # UPSTREAM-PARITY
13//!
14//! libxml2 has many global variables that control default parser behavior.
15//! These are exposed as public ABI symbols that applications can read/write
16//! directly. We use atomic operations for thread safety while maintaining
17//! the same observable semantics.
18//!
19//! # Thread safety
20//!
21//! All global state uses atomic operations or parking_lot locks.
22//! Error state is thread-local via `thread_local!`.
23//!
24//! # Phase 1 status
25//!
26//! Complete — all global state management is implemented.
27//! Future phases may add historical version-specific behavior.
28//!
29//! # Upstream contract
30//!
31//! Mirrors upstream globals.c (SRC-LIBXML2-2.15.0-GLOBALS-C, oracle tree
32//! `oracle/historical/src/libxml2-2.15.0/globals.c`): the library-wide global
33//! variables (xmlDoValidityCheckingDefaultValue, xmlLoadExtDtdDefaultValue,
34//! xmlKeepBlanksDefaultValue, xmlGenericError, xmlStructuredError,
35//! xmlDefaultSAXHandler, xmlLastError, xmlParserVersion, ...) exposed as
36//! public ABI data symbols (R-000135).
37//!
38//! # Conceptual behavior
39//!
40//! Manages parser defaults, generic/structured error callbacks, catalog
41//! defaults, memory hooks, thread-local error state and init/cleanup
42//! reference counting. The exported data symbols are wired to the parser-
43//! default accessors so there is a single source of truth.
44//!
45//! # Ownership & safety invariants
46//!
47//! SAFETY: exported C-visible globals keep upstream documented racy semantics
48//! for direct readers, while the internal Rust accessors serialize (handler,
49//! ctx) slot pairs under ERROR_HANDLER_LOCK so readers never observe a new
50//! handler with an old context (R-000171). The xmlLastError mirror is deep-
51//! copied under LAST_ERROR_MIRROR_LOCK (R-000170: concurrent sync/reset
52//! double-freed the mirror strings). Error state is thread-local.
53//!
54//! # Historical quirks & epochs
55//!
56//! Thread support predates the thread-local-globals era: globals.c threading
57//! integrated 2001-10-12/13 (commits b847864f, d0463560; LORE-0005). Modern
58//! 2.10+ initialization is lazy; deprecated init/cleanup entry points are
59//! genuine no-ops (R-000138). R-000161 fixed the exported default values
60//! (xmlLineNumbersDefaultValue 1, xmlTreeIndentString two spaces,
61//! xmlParserVersion 21503-GITv2.15.3).
62//!
63//! # Deliberate oddities
64//!
65//! Deliberate oddities: xmlGenericError and xsltGenericError default to the
66//! variadic stderr printers (asm va_list shims; R-000161) rather than NULL;
67//! the deprecated init entry points are exported no-ops reproducing upstream
68//! empty bodies (R-000138).
69//!
70//! # Proving courts
71//!
72//! ABI-DATA, ALLOCATOR, GLOBAL-STATE and THREADING court families;
73//! DATA-GLOBALS-001 differential probe (byte-identical), GLOBALS-THREADING
74//! probe, DSO-LOADER, and `cargo test --lib` (100/100 parallel runs
75//! clean after R-000170/R-000171).
76//!
77//! # Tempting simplifications that would break parity
78//!
79//! The tempting simplification is hiding the C globals behind accessors only
80//! — downstream code that reads/writes xmlDoValidityCheckingDefaultValue
81//! directly would fail to link (R-000135). Do not fix the racy C-visible
82//! symbols: the internal locks must not change the documented upstream
83//! semantics for direct consumers.
84//!
85//! # Safety
86//!
87//! - The module-level statics are synchronization primitives only
88//!   (atomics, a `parking_lot` mutex, and a `thread_local` cell); there are
89//!   no top-level `unsafe` blocks outside functions. Reads and writes of
90//!   the exported C globals in `crate::abi::data_globals` happen only
91//!   inside the accessor functions, where the `(handler, ctx)` slot pairs
92//!   are serialized under `ERROR_HANDLER_LOCK` so a reader never observes a
93//!   new handler with an old context. The thread-local `LAST_ERROR` slot is
94//!   only mutated through `set_last_error`/`reset_last_error`, which free
95//!   the previous slot's owned strings exactly once.
96use core::cell::RefCell;
97use core::ffi::c_void;
98use core::ptr;
99use core::sync::atomic::{AtomicBool, AtomicI32, Ordering};
100use std::os::raw::c_int;
101
102use crate::abi::allocator;
103use crate::abi::callbacks::{xmlGenericErrorFunc, xmlStructuredErrorFunc};
104use crate::abi::structs::_xmlError;
105use crate::abi::versioning;
106
107/// Serializes the exported error-handler slot pairs
108/// (`xmlGenericError`/`xmlGenericErrorContext` and
109/// `xmlStructuredError`/`xmlStructuredErrorContext`). Upstream's globals are
110/// bare racy `static mut` slots; the candidate keeps the C-visible symbols
111/// but makes internal set/get atomic as a (handler, ctx) pair so readers
112/// never observe a new handler with an old context (or vice versa). C
113/// consumers that touch the symbols directly keep upstream's documented
114/// racy semantics.
115static ERROR_HANDLER_LOCK: parking_lot::Mutex<()> = parking_lot::Mutex::new(());
116
117/// Serializes the error-handler tests that mutate the shared handler slots
118/// (11.1-X regression court wiring): `test_error_callbacks_*` (globals) and
119/// `test_structured_error_callback` (errors) must not run concurrently, or
120/// `test_error_callbacks_default_handlers` observes another test's
121/// temporarily-installed structured handler.
122#[cfg(test)]
123pub(crate) static ERROR_HANDLER_TEST_LOCK: parking_lot::Mutex<()> = parking_lot::Mutex::new(());
124
125// ═══════════════════════════════════════════════════════════════════════════════
126// Initialization Reference Counting
127// ═══════════════════════════════════════════════════════════════════════════════
128
129/// Reference count for xmlInitParser / xmlCleanupParser.
130static INIT_REF_COUNT: AtomicI32 = AtomicI32::new(0);
131
132/// Whether threading has been initialized.
133static THREADS_INITIALIZED: AtomicBool = AtomicBool::new(false);
134
135// ═══════════════════════════════════════════════════════════════════════════════
136// Parser Defaults
137// ═══════════════════════════════════════════════════════════════════════════════
138//
139// These are the global parser default variables exposed by libxml2's ABI.
140// Applications can read and write them directly to change default behavior.
141//
142// Upstream declarations (from parser.h / parserInternals.h):
143//
144// ```c
145// extern int xmlDoValidityCheckingDefaultValue;
146// extern int xmlDoWarningsDefaultValue;
147// extern int xmlIndentTreeOutput;
148// extern int xmlKeepBlanksDefaultValue;
149// extern int xmlLoadExtDtdDefaultValue;
150// extern int xmlPedanticParserDefaultValue;
151// ═══════════════════════════════════════════════════════════════════════════════
152// Parser Defaults
153// ═══════════════════════════════════════════════════════════════════════════════
154//
155// The defaults live in the EXPORTED C globals (src/abi/data_globals.rs) so
156// that downstream C code reading/writing them directly (the upstream
157// contract) observes and controls the same state the parser uses. The
158// accessors below are the safe-Rust view of those statics.
159
160// ═══════════════════════════════════════════════════════════════════════════════
161// Error Callback Globals
162// ═══════════════════════════════════════════════════════════════════════════════
163// Also exported as C globals (xmlGenericError/xmlGenericErrorContext,
164// xmlStructuredError/xmlStructuredErrorContext).
165
166// ═══════════════════════════════════════════════════════════════════════════════
167// Catalog Defaults
168// ═══════════════════════════════════════════════════════════════════════════════
169
170/// Catalog default allow value.
171/// 0 = strict, 1 = allow, 2 = allow document, -1 = none.
172/// Internal only (upstream keeps catalog state inside xmlCatalogSetDefaults;
173/// there is no public C global for it).
174static CATALOG_DEFAULTS: AtomicI32 = AtomicI32::new(0);
175
176// ═══════════════════════════════════════════════════════════════════════════════
177// Thread-Local Error State
178// ═══════════════════════════════════════════════════════════════════════════════
179
180thread_local! {
181    /// Last error for this thread.
182    static LAST_ERROR: RefCell<Option<_xmlError>> = const { RefCell::new(None) };
183}
184
185// ═══════════════════════════════════════════════════════════════════════════════
186// Public Accessors — Parser Defaults
187// ═══════════════════════════════════════════════════════════════════════════════
188
189/// Get the default validity checking value.
190pub fn get_validity_checking_default() -> c_int {
191    // SAFETY: reading an exported int global; matches upstream semantics.
192    unsafe { crate::abi::data_globals::xmlDoValidityCheckingDefaultValue }
193}
194
195/// Set the default validity checking value.
196pub fn set_validity_checking_default(val: c_int) {
197    // SAFETY: writing an exported int global; matches upstream semantics.
198    unsafe {
199        crate::abi::data_globals::xmlDoValidityCheckingDefaultValue = val;
200    }
201}
202
203/// Get the default warnings value.
204pub fn get_do_warnings_default() -> c_int {
205    // SAFETY: reading an exported int global (upstream xmlGetWarningsDefaultValue).
206    unsafe { crate::abi::data_globals::xmlGetWarningsDefaultValue }
207}
208
209/// Set the default warnings value.
210pub fn set_do_warnings_default(val: c_int) {
211    // SAFETY: writing an exported int global.
212    unsafe {
213        crate::abi::data_globals::xmlGetWarningsDefaultValue = val;
214    }
215}
216
217/// Get the indent tree output default.
218pub fn get_indent_tree_output() -> c_int {
219    // SAFETY: reading an exported int global (upstream xmlIndentTreeOutput).
220    unsafe { crate::abi::data_globals::xmlIndentTreeOutput }
221}
222
223/// Set the indent tree output default.
224pub fn set_indent_tree_output(val: c_int) {
225    // SAFETY: writing an exported int global.
226    unsafe {
227        crate::abi::data_globals::xmlIndentTreeOutput = val;
228    }
229}
230
231/// Get the keep blanks default value.
232pub fn get_keep_blanks_default() -> c_int {
233    // SAFETY: reading an exported int global (upstream xmlKeepBlanksDefaultValue).
234    unsafe { crate::abi::data_globals::xmlKeepBlanksDefaultValue }
235}
236
237/// Set the keep blanks default value.
238pub fn set_keep_blanks_default(val: c_int) {
239    // SAFETY: writing an exported int global.
240    unsafe {
241        crate::abi::data_globals::xmlKeepBlanksDefaultValue = val;
242    }
243}
244
245/// Get the load external DTD default value.
246pub fn get_load_ext_dtd_default() -> c_int {
247    // SAFETY: reading an exported int global (upstream xmlLoadExtDtdDefaultValue).
248    unsafe { crate::abi::data_globals::xmlLoadExtDtdDefaultValue }
249}
250
251/// Set the load external DTD default value.
252pub fn set_load_ext_dtd_default(val: c_int) {
253    // SAFETY: writing an exported int global.
254    unsafe {
255        crate::abi::data_globals::xmlLoadExtDtdDefaultValue = val;
256    }
257}
258
259/// Get the pedantic parser default.
260pub fn get_pedantic_parser_default() -> c_int {
261    // SAFETY: reading an exported int global (upstream xmlPedanticParserDefaultValue).
262    unsafe { crate::abi::data_globals::xmlPedanticParserDefaultValue }
263}
264
265/// Set the pedantic parser default.
266pub fn set_pedantic_parser_default(val: c_int) {
267    // SAFETY: writing an exported int global.
268    unsafe {
269        crate::abi::data_globals::xmlPedanticParserDefaultValue = val;
270    }
271}
272
273/// Get the substitute entities default.
274pub fn get_substitute_entities_default() -> c_int {
275    // SAFETY: reading an exported int global (upstream xmlSubstituteEntitiesDefaultValue).
276    unsafe { crate::abi::data_globals::xmlSubstituteEntitiesDefaultValue }
277}
278
279/// Set the substitute entities default.
280pub fn set_substitute_entities_default(val: c_int) {
281    // SAFETY: writing an exported int global.
282    unsafe {
283        crate::abi::data_globals::xmlSubstituteEntitiesDefaultValue = val;
284    }
285}
286
287/// Get the save no empty tags default.
288pub fn get_save_no_empty_tags() -> c_int {
289    // SAFETY: reading an exported int global (upstream xmlSaveNoEmptyTags).
290    unsafe { crate::abi::data_globals::xmlSaveNoEmptyTags }
291}
292
293/// Set the save no empty tags default.
294pub fn set_save_no_empty_tags(val: c_int) {
295    // SAFETY: writing an exported int global.
296    unsafe {
297        crate::abi::data_globals::xmlSaveNoEmptyTags = val;
298    }
299}
300
301/// Get the get warnings default.
302pub fn get_get_warnings_default() -> c_int {
303    // SAFETY: reading an exported int global (upstream xmlGetWarningsDefaultValue).
304    unsafe { crate::abi::data_globals::xmlGetWarningsDefaultValue }
305}
306
307/// Set the get warnings default.
308pub fn set_get_warnings_default(val: c_int) {
309    // SAFETY: writing an exported int global.
310    unsafe {
311        crate::abi::data_globals::xmlGetWarningsDefaultValue = val;
312    }
313}
314
315// ═══════════════════════════════════════════════════════════════════════════════
316// Public Accessors — Error Callbacks
317// ═══════════════════════════════════════════════════════════════════════════════
318
319/// Set the generic error handler.
320///
321/// # SAFETY
322///
323/// - `handler` must be a valid function pointer or NULL (to reset to default).
324/// - If non-NULL, the handler may be called at any time with `ctx`.
325pub unsafe fn set_generic_error_func(ctx: *mut c_void, handler: Option<xmlGenericErrorFunc>) {
326    // SAFETY: writing the exported C globals xmlGenericErrorContext /
327    // xmlGenericError; matches upstream xmlSetGenericErrorFunc (NULL resets
328    // to the built-in default stderr printer, error.c). The (ctx, func)
329    // pair is written atomically under ERROR_HANDLER_LOCK.
330    let resolved = match handler {
331        Some(h) => Some(h),
332        None => crate::abi::data_globals::default_generic_error_func(),
333    };
334    let _guard = ERROR_HANDLER_LOCK.lock();
335    unsafe {
336        crate::abi::data_globals::xmlGenericErrorContext = ctx;
337        crate::abi::data_globals::xmlGenericError = resolved;
338    }
339}
340
341/// Get the generic error handler context.
342pub fn get_generic_error_ctx() -> *mut c_void {
343    let _guard = ERROR_HANDLER_LOCK.lock();
344    // SAFETY: reading the exported C global xmlGenericErrorContext.
345    unsafe { crate::abi::data_globals::xmlGenericErrorContext }
346}
347
348/// Get the generic error handler function pointer.
349pub fn get_generic_error_func() -> Option<xmlGenericErrorFunc> {
350    let _guard = ERROR_HANDLER_LOCK.lock();
351    // SAFETY: reading the exported C global xmlGenericError.
352    unsafe { crate::abi::data_globals::xmlGenericError }
353}
354
355/// Read the generic error (func, ctx) pair atomically.
356///
357/// The closure runs after the lock is released, so a handler installed
358/// by the callback cannot deadlock.
359pub fn with_generic_error<R>(f: impl FnOnce(Option<xmlGenericErrorFunc>, *mut c_void) -> R) -> R {
360    let (h, c) = {
361        let _guard = ERROR_HANDLER_LOCK.lock();
362        (
363            unsafe { crate::abi::data_globals::xmlGenericError },
364            unsafe { crate::abi::data_globals::xmlGenericErrorContext },
365        )
366    };
367    f(h, c)
368}
369
370/// Set the structured error handler.
371///
372/// # SAFETY
373///
374/// - `handler` must be a valid function pointer or NULL.
375pub unsafe fn set_structured_error_func(ctx: *mut c_void, handler: Option<xmlStructuredErrorFunc>) {
376    // SAFETY: writing the exported C globals xmlStructuredErrorContext /
377    // xmlStructuredError; matches upstream xmlSetStructuredErrorFunc. The
378    // (ctx, func) pair is written atomically under ERROR_HANDLER_LOCK.
379    let _guard = ERROR_HANDLER_LOCK.lock();
380    unsafe {
381        crate::abi::data_globals::xmlStructuredErrorContext = ctx;
382        crate::abi::data_globals::xmlStructuredError = handler;
383    }
384}
385
386/// Get the structured error handler context.
387pub fn get_structured_error_ctx() -> *mut c_void {
388    let _guard = ERROR_HANDLER_LOCK.lock();
389    // SAFETY: reading the exported C global xmlStructuredErrorContext.
390    unsafe { crate::abi::data_globals::xmlStructuredErrorContext }
391}
392
393/// Get the structured error handler function pointer.
394pub fn get_structured_error_func() -> Option<xmlStructuredErrorFunc> {
395    let _guard = ERROR_HANDLER_LOCK.lock();
396    // SAFETY: reading the exported C global xmlStructuredError.
397    unsafe { crate::abi::data_globals::xmlStructuredError }
398}
399
400/// Read the structured error (func, ctx) pair atomically.
401///
402/// The closure runs after the lock is released, so a handler installed by
403/// the callback (or an error raised from inside the handler) cannot
404/// deadlock on ERROR_HANDLER_LOCK.
405///
406/// # Safety
407///
408/// - The two `unsafe` reads of the exported `xmlStructuredError`/
409///   `xmlStructuredErrorContext` data variables are performed under
410///   `ERROR_HANDLER_LOCK`, matching the write side (xmlSetStructuredError), so
411///   the pair is observed atomically; the values are only borrowed for the
412///   duration of the closure and never dereferenced here.
413pub fn with_structured_error<R>(
414    f: impl FnOnce(Option<xmlStructuredErrorFunc>, *mut c_void) -> R,
415) -> R {
416    let (h, c) = {
417        let _guard = ERROR_HANDLER_LOCK.lock();
418        (
419            unsafe { crate::abi::data_globals::xmlStructuredError },
420            unsafe { crate::abi::data_globals::xmlStructuredErrorContext },
421        )
422    };
423    f(h, c)
424}
425
426// ═══════════════════════════════════════════════════════════════════════════════
427// Public Accessors — Thread-Local Error State
428// ═══════════════════════════════════════════════════════════════════════════════
429
430/// Get the last error for this thread.
431///
432/// Returns a mutable pointer to the last error, or NULL if no error occurred.
433/// The returned pointer is valid until the next libxml2 call in this thread.
434pub fn get_last_error() -> *mut _xmlError {
435    LAST_ERROR.with(|last| {
436        let mut last = last.borrow_mut();
437        last.as_mut()
438            .map_or(ptr::null_mut(), |e| e as *mut _xmlError)
439    })
440}
441
442/// Get a reference to the last error (for structured error callback).
443pub fn with_last_error<F, R>(f: F) -> R
444where
445    F: FnOnce(Option<&_xmlError>) -> R,
446{
447    LAST_ERROR.with(|last| f(last.borrow().as_ref()))
448}
449
450/// Set the last error for this thread.
451pub fn set_last_error(err: _xmlError) {
452    // UPSTREAM-PARITY: mirror the error into the exported C global
453    // `xmlLastError` (data-ABI, residual R-000135). Upstream keeps a single
454    // global; the candidate keeps a thread-local truth and a deep-copied
455    // mirror so C consumers see the most recent error with upstream
456    // lifetime semantics (mirror strings are owned by the mirror and freed
457    // on reset, matching xmlResetError).
458    //
459    // SAFETY: sync_xml_last_error only reads `err` and writes the global
460    // mirror with freshly owned copies; the thread-local slot takes
461    // ownership of `err` itself.
462    unsafe { crate::abi::data_globals::sync_xml_last_error(&err) };
463    LAST_ERROR.with(|last| {
464        let mut last = last.borrow_mut();
465        // Free the previous slot's owned strings (upstream xmlResetError).
466        if let Some(prev) = last.as_ref() {
467            free_error_strings(prev);
468        }
469        *last = Some(err);
470    });
471}
472
473/// Free the owned string fields of a stored error (upstream xmlResetError:
474/// message/file/str1/str2/str3 are xmlMalloc'd copies).
475///
476/// # Safety
477///
478/// - `err` must point to a valid `_xmlError` whose `message`, `file`,
479///   `str1`, `str2` and `str3` fields are NULL or pointers allocated with
480///   `xmlFreeImpl`'s allocator; each non-NULL field is freed exactly once
481///   and must not be freed or used again afterwards.
482fn free_error_strings(err: &_xmlError) {
483    use crate::abi::allocator::xmlFreeImpl;
484    unsafe {
485        if !err.message.is_null() {
486            xmlFreeImpl(err.message as *mut core::ffi::c_void);
487        }
488        if !err.file.is_null() {
489            xmlFreeImpl(err.file as *mut core::ffi::c_void);
490        }
491        if !err.str1.is_null() {
492            xmlFreeImpl(err.str1 as *mut core::ffi::c_void);
493        }
494        if !err.str2.is_null() {
495            xmlFreeImpl(err.str2 as *mut core::ffi::c_void);
496        }
497        if !err.str3.is_null() {
498            xmlFreeImpl(err.str3 as *mut core::ffi::c_void);
499        }
500    }
501}
502
503/// Reset the last error for this thread.
504pub fn reset_last_error() {
505    LAST_ERROR.with(|last| {
506        let mut last = last.borrow_mut();
507        if let Some(prev) = last.as_ref() {
508            free_error_strings(prev);
509        }
510        *last = None;
511    });
512    // SAFETY: frees the mirror's owned strings and zeroes the global.
513    unsafe { crate::abi::data_globals::reset_xml_last_error() };
514}
515
516// ═══════════════════════════════════════════════════════════════════════════════
517// Public Accessors — Catalog Defaults
518// ═══════════════════════════════════════════════════════════════════════════════
519
520/// Get the catalog default allow value.
521pub fn get_catalog_defaults() -> c_int {
522    CATALOG_DEFAULTS.load(Ordering::Relaxed)
523}
524
525/// Set the catalog default allow value.
526pub fn set_catalog_defaults(val: c_int) {
527    CATALOG_DEFAULTS.store(val, Ordering::Relaxed);
528}
529
530// ═══════════════════════════════════════════════════════════════════════════════
531// Initialization / Cleanup
532// ═══════════════════════════════════════════════════════════════════════════════
533
534/// Initialize the parser library.
535///
536/// Must be called before any other libxml2 functions.
537/// Safe to call multiple times (reference-counted in modern libxml2).
538///
539/// # UPSTREAM-PARITY
540///
541/// In modern libxml2 (2.12+), `xmlInitParser` is reference-counted.
542/// The first call initializes all subsystems; subsequent calls
543/// increment a counter. `xmlCleanupParser` decrements the counter
544/// and only performs cleanup when it reaches zero.
545///
546/// # SAFETY
547///
548/// Not fully thread-safe during the first call; callers should
549/// call `xmlInitParser` before creating threads.
550pub unsafe fn init_parser() {
551    let prev = INIT_REF_COUNT.fetch_add(1, Ordering::AcqRel);
552    if prev == 0 {
553        // First initialization — initialize all subsystems.
554        // 1. Initialize memory subsystem.
555        allocator::xmlInitMemory();
556
557        // 2. Mark the library as initialized.
558        versioning::set_initialized();
559
560        // 3. Initialize encoding handlers.
561        crate::xml::encoding::init_encodings();
562
563        // 4. Initialize thread support.
564        init_threads();
565    }
566}
567
568/// Clean up the parser library.
569///
570/// Should be called when the library is no longer needed.
571/// Only performs actual cleanup when the reference count reaches zero.
572///
573/// # SAFETY
574///
575/// Must not be called while other libxml2 functions are executing
576/// in any thread.
577pub unsafe fn cleanup_parser() {
578    let prev = INIT_REF_COUNT.fetch_sub(1, Ordering::AcqRel);
579    if prev <= 1 {
580        // Last cleanup — clean up all subsystems.
581        // 1. Clean up catalog.
582        crate::xml::catalog::cleanup();
583
584        // 2. Clean up encoding handlers.
585        crate::xml::encoding::cleanup_encodings();
586
587        // 3. Clean up memory.
588        allocator::xmlCleanupMemory();
589
590        // 3. Reset initialization state.
591        // Note: we do NOT reset the initialized flag in case
592        // some code checks it after cleanup. This matches
593        // upstream behavior where xmlCleanupParser is best-effort.
594    }
595}
596
597/// Initialize threading support.
598///
599/// # UPSTREAM-PARITY
600///
601/// In modern libxml2, threading is initialized automatically
602/// by `xmlInitParser`. This function exists for backward
603/// compatibility.
604///
605/// Returns 0 on success.
606pub fn init_threads() -> c_int {
607    if !THREADS_INITIALIZED.swap(true, Ordering::Release) {
608        // First initialization — no-op in Rust since we use
609        // standard thread-safe primitives.
610        // In libxml2 this would set up pthread mutexes.
611    }
612    0
613}
614
615/// Clean up threading support.
616pub fn cleanup_threads() {
617    THREADS_INITIALIZED.store(false, Ordering::Release);
618}
619
620/// Check whether threads have been initialized.
621pub fn threads_initialized() -> bool {
622    THREADS_INITIALIZED.load(Ordering::Acquire)
623}
624
625/// Get the current initialization reference count.
626pub fn init_ref_count() -> c_int {
627    INIT_REF_COUNT.load(Ordering::Relaxed)
628}
629
630// ═══════════════════════════════════════════════════════════════════════════════
631// Tests
632// ═══════════════════════════════════════════════════════════════════════════════
633
634#[cfg(test)]
635mod tests {
636    use super::*;
637    use crate::abi::types::xmlErrorLevel::XML_ERR_NONE;
638    use crate::abi::types::*;
639
640    #[test]
641    fn test_parser_defaults_initial_values() {
642        assert_eq!(get_validity_checking_default(), 0);
643        assert_eq!(get_do_warnings_default(), 1);
644        // UPSTREAM-PARITY (globals.c 2.15): xmlIndentTreeOutputThrDef = 1.
645        assert_eq!(get_indent_tree_output(), 1);
646        assert_eq!(get_keep_blanks_default(), 1);
647        assert_eq!(get_load_ext_dtd_default(), 0);
648        assert_eq!(get_pedantic_parser_default(), 0);
649        assert_eq!(get_substitute_entities_default(), 0);
650        assert_eq!(get_save_no_empty_tags(), 0);
651        assert_eq!(get_get_warnings_default(), 1);
652    }
653
654    #[test]
655    fn test_parser_defaults_set_and_get() {
656        set_validity_checking_default(1);
657        assert_eq!(get_validity_checking_default(), 1);
658        set_validity_checking_default(0);
659        assert_eq!(get_validity_checking_default(), 0);
660
661        set_keep_blanks_default(0);
662        assert_eq!(get_keep_blanks_default(), 0);
663        set_keep_blanks_default(1);
664        assert_eq!(get_keep_blanks_default(), 1);
665
666        set_substitute_entities_default(1);
667        assert_eq!(get_substitute_entities_default(), 1);
668        set_substitute_entities_default(0);
669        assert_eq!(get_substitute_entities_default(), 0);
670    }
671
672    /// Increment and decrement the init reference count.
673    ///
674    /// # Safety
675    ///
676    /// - `init_parser`/`cleanup_parser` mutate the global reference count
677    ///   and may run subsystem init/cleanup; the test balances the two
678    ///   calls so the library is left in a clean state, and the reference
679    ///   count is only read between calls.
680    #[test]
681    fn test_init_cleanup_ref_count() {
682        // Reset for test
683        unsafe {
684            init_parser();
685            assert_eq!(init_ref_count(), 1);
686
687            init_parser();
688            assert_eq!(init_ref_count(), 2);
689
690            cleanup_parser();
691            assert!(init_ref_count() == 1 || init_ref_count() == 0);
692
693            // Final cleanup
694            cleanup_parser();
695        }
696    }
697
698    #[test]
699    fn test_error_callbacks_default_handlers() {
700        // UPSTREAM-PARITY (error.c): xmlGenericError defaults to the built-in
701        // stderr printer (never NULL); xmlStructuredError defaults to NULL.
702        // Serialized against the other handler-mutating tests (11.1-X): the
703        // slots are shared global state.
704        let _guard = ERROR_HANDLER_TEST_LOCK.lock();
705        #[cfg(target_arch = "x86_64")]
706        assert!(get_generic_error_func().is_some());
707        assert!(get_structured_error_func().is_none());
708    }
709
710    /// Install and reset the generic error handler slot pair.
711    ///
712    /// # Safety
713    ///
714    /// - `dummy_handler` is a valid no-op callback pointer; `dummy_ctx`
715    ///   points to a stack `i32` alive for the test; `set_generic_error_func`
716    ///   writes the exported slot pair under the handler lock, and NULL
717    ///   resets to the built-in default printer.
718    #[test]
719    fn test_error_callbacks_set_and_get() {
720        let _guard = ERROR_HANDLER_TEST_LOCK.lock();
721        unsafe {
722            /// A no-op generic error callback used as a handler pointer.
723            ///
724            /// # Safety
725            ///
726            /// - The function is never invoked by this test; when installed as a
727            ///   handler it must be a valid function pointer, and the parameters
728            ///   would need to be valid C pointers if it were called.
729            unsafe extern "C" fn dummy_handler(_ctx: *mut c_void, _msg: *const core::ffi::c_char) {}
730            let dummy_func: xmlGenericErrorFunc = dummy_handler;
731            let dummy_ctx: *mut c_void = &mut 0 as *mut i32 as *mut c_void;
732
733            set_generic_error_func(dummy_ctx, Some(dummy_func));
734            assert!(get_generic_error_func().is_some());
735            assert_eq!(get_generic_error_ctx(), dummy_ctx);
736
737            // UPSTREAM-PARITY (xmlSetGenericErrorFunc): NULL resets to the
738            // built-in default printer, it does not unset the handler.
739            set_generic_error_func(ptr::null_mut(), None);
740            #[cfg(target_arch = "x86_64")]
741            assert!(get_generic_error_func().is_some());
742            assert_eq!(get_generic_error_ctx(), ptr::null_mut());
743        }
744    }
745
746    /// Store and reset a thread-local last error.
747    ///
748    /// # Safety
749    ///
750    /// - `err` has all NULL string fields, so `set_last_error` stores it
751    ///   without owning heap strings and `reset_last_error` frees nothing;
752    ///   `get_last_error` returns a pointer valid while the thread-local
753    ///   slot is not mutated.
754    #[test]
755    fn test_last_error_thread_local() {
756        assert!(get_last_error().is_null());
757
758        let err = _xmlError {
759            domain: XML_FROM_PARSER,
760            code: XML_ERR_OK as c_int,
761            message: ptr::null_mut(),
762            level: XML_ERR_NONE as c_int,
763            file: ptr::null_mut(),
764            line: 0,
765            str1: ptr::null_mut(),
766            str2: ptr::null_mut(),
767            str3: ptr::null_mut(),
768            int1: 0,
769            int2: 0,
770            ctxt: ptr::null_mut(),
771            node: ptr::null_mut(),
772        };
773        set_last_error(err);
774        assert!(!get_last_error().is_null());
775        unsafe {
776            assert_eq!((*get_last_error()).domain, XML_FROM_PARSER);
777        }
778
779        reset_last_error();
780        assert!(get_last_error().is_null());
781    }
782
783    #[test]
784    fn test_catalog_defaults() {
785        // Save original value (may have been set by init_parser in other tests)
786        let orig = get_catalog_defaults();
787        set_catalog_defaults(1);
788        assert_eq!(get_catalog_defaults(), 1);
789        set_catalog_defaults(0);
790        assert_eq!(get_catalog_defaults(), 0);
791        // Restore
792        set_catalog_defaults(orig);
793    }
794}