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}