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
29use core::cell::RefCell;
30use core::ffi::c_void;
31use core::ptr;
32use core::sync::atomic::{AtomicBool, AtomicI32, AtomicPtr, Ordering};
33use std::os::raw::c_int;
34
35use crate::abi::allocator;
36use crate::abi::callbacks::{xmlGenericErrorFunc, xmlStructuredErrorFunc};
37use crate::abi::structs::_xmlError;
38use crate::abi::types::xmlErrorLevel::XML_ERR_NONE;
39use crate::abi::types::*;
40use crate::abi::versioning;
41
42// ═══════════════════════════════════════════════════════════════════════════════
43// Initialization Reference Counting
44// ═══════════════════════════════════════════════════════════════════════════════
45
46/// Reference count for xmlInitParser / xmlCleanupParser.
47static INIT_REF_COUNT: AtomicI32 = AtomicI32::new(0);
48
49/// Whether threading has been initialized.
50static THREADS_INITIALIZED: AtomicBool = AtomicBool::new(false);
51
52// ═══════════════════════════════════════════════════════════════════════════════
53// Parser Defaults
54// ═══════════════════════════════════════════════════════════════════════════════
55//
56// These are the global parser default variables exposed by libxml2's ABI.
57// Applications can read and write them directly to change default behavior.
58//
59// Upstream declarations (from parser.h / parserInternals.h):
60//
61// ```c
62// extern int xmlDoValidityCheckingDefaultValue;
63// extern int xmlDoWarningsDefaultValue;
64// extern int xmlIndentTreeOutput;
65// extern int xmlKeepBlanksDefaultValue;
66// extern int xmlLoadExtDtdDefaultValue;
67// extern int xmlPedanticParserDefaultValue;
68// ═══════════════════════════════════════════════════════════════════════════════
69// Parser Defaults
70// ═══════════════════════════════════════════════════════════════════════════════
71//
72// The defaults live in the EXPORTED C globals (src/abi/data_globals.rs) so
73// that downstream C code reading/writing them directly (the upstream
74// contract) observes and controls the same state the parser uses. The
75// accessors below are the safe-Rust view of those statics.
76
77// ═══════════════════════════════════════════════════════════════════════════════
78// Error Callback Globals
79// ═══════════════════════════════════════════════════════════════════════════════
80// Also exported as C globals (xmlGenericError/xmlGenericErrorContext,
81// xmlStructuredError/xmlStructuredErrorContext).
82
83// ═══════════════════════════════════════════════════════════════════════════════
84// Catalog Defaults
85// ═══════════════════════════════════════════════════════════════════════════════
86
87/// Catalog default allow value.
88/// 0 = strict, 1 = allow, 2 = allow document, -1 = none.
89/// Internal only (upstream keeps catalog state inside xmlCatalogSetDefaults;
90/// there is no public C global for it).
91static CATALOG_DEFAULTS: AtomicI32 = AtomicI32::new(0);
92
93// ═══════════════════════════════════════════════════════════════════════════════
94// Thread-Local Error State
95// ═══════════════════════════════════════════════════════════════════════════════
96
97thread_local! {
98    /// Last error for this thread.
99    static LAST_ERROR: RefCell<Option<_xmlError>> = const { RefCell::new(None) };
100}
101
102// ═══════════════════════════════════════════════════════════════════════════════
103// Public Accessors — Parser Defaults
104// ═══════════════════════════════════════════════════════════════════════════════
105
106/// Get the default validity checking value.
107pub fn get_validity_checking_default() -> c_int {
108    // SAFETY: reading an exported int global; matches upstream semantics.
109    unsafe { crate::abi::data_globals::xmlDoValidityCheckingDefaultValue }
110}
111
112/// Set the default validity checking value.
113pub fn set_validity_checking_default(val: c_int) {
114    // SAFETY: writing an exported int global; matches upstream semantics.
115    unsafe {
116        crate::abi::data_globals::xmlDoValidityCheckingDefaultValue = val;
117    }
118}
119
120/// Get the default warnings value.
121pub fn get_do_warnings_default() -> c_int {
122    // SAFETY: reading an exported int global (upstream xmlGetWarningsDefaultValue).
123    unsafe { crate::abi::data_globals::xmlGetWarningsDefaultValue }
124}
125
126/// Set the default warnings value.
127pub fn set_do_warnings_default(val: c_int) {
128    // SAFETY: writing an exported int global.
129    unsafe {
130        crate::abi::data_globals::xmlGetWarningsDefaultValue = val;
131    }
132}
133
134/// Get the indent tree output default.
135pub fn get_indent_tree_output() -> c_int {
136    // SAFETY: reading an exported int global (upstream xmlIndentTreeOutput).
137    unsafe { crate::abi::data_globals::xmlIndentTreeOutput }
138}
139
140/// Set the indent tree output default.
141pub fn set_indent_tree_output(val: c_int) {
142    // SAFETY: writing an exported int global.
143    unsafe {
144        crate::abi::data_globals::xmlIndentTreeOutput = val;
145    }
146}
147
148/// Get the keep blanks default value.
149pub fn get_keep_blanks_default() -> c_int {
150    // SAFETY: reading an exported int global (upstream xmlKeepBlanksDefaultValue).
151    unsafe { crate::abi::data_globals::xmlKeepBlanksDefaultValue }
152}
153
154/// Set the keep blanks default value.
155pub fn set_keep_blanks_default(val: c_int) {
156    // SAFETY: writing an exported int global.
157    unsafe {
158        crate::abi::data_globals::xmlKeepBlanksDefaultValue = val;
159    }
160}
161
162/// Get the load external DTD default value.
163pub fn get_load_ext_dtd_default() -> c_int {
164    // SAFETY: reading an exported int global (upstream xmlLoadExtDtdDefaultValue).
165    unsafe { crate::abi::data_globals::xmlLoadExtDtdDefaultValue }
166}
167
168/// Set the load external DTD default value.
169pub fn set_load_ext_dtd_default(val: c_int) {
170    // SAFETY: writing an exported int global.
171    unsafe {
172        crate::abi::data_globals::xmlLoadExtDtdDefaultValue = val;
173    }
174}
175
176/// Get the pedantic parser default.
177pub fn get_pedantic_parser_default() -> c_int {
178    // SAFETY: reading an exported int global (upstream xmlPedanticParserDefaultValue).
179    unsafe { crate::abi::data_globals::xmlPedanticParserDefaultValue }
180}
181
182/// Set the pedantic parser default.
183pub fn set_pedantic_parser_default(val: c_int) {
184    // SAFETY: writing an exported int global.
185    unsafe {
186        crate::abi::data_globals::xmlPedanticParserDefaultValue = val;
187    }
188}
189
190/// Get the substitute entities default.
191pub fn get_substitute_entities_default() -> c_int {
192    // SAFETY: reading an exported int global (upstream xmlSubstituteEntitiesDefaultValue).
193    unsafe { crate::abi::data_globals::xmlSubstituteEntitiesDefaultValue }
194}
195
196/// Set the substitute entities default.
197pub fn set_substitute_entities_default(val: c_int) {
198    // SAFETY: writing an exported int global.
199    unsafe {
200        crate::abi::data_globals::xmlSubstituteEntitiesDefaultValue = val;
201    }
202}
203
204/// Get the save no empty tags default.
205pub fn get_save_no_empty_tags() -> c_int {
206    // SAFETY: reading an exported int global (upstream xmlSaveNoEmptyTags).
207    unsafe { crate::abi::data_globals::xmlSaveNoEmptyTags }
208}
209
210/// Set the save no empty tags default.
211pub fn set_save_no_empty_tags(val: c_int) {
212    // SAFETY: writing an exported int global.
213    unsafe {
214        crate::abi::data_globals::xmlSaveNoEmptyTags = val;
215    }
216}
217
218/// Get the get warnings default.
219pub fn get_get_warnings_default() -> c_int {
220    // SAFETY: reading an exported int global (upstream xmlGetWarningsDefaultValue).
221    unsafe { crate::abi::data_globals::xmlGetWarningsDefaultValue }
222}
223
224/// Set the get warnings default.
225pub fn set_get_warnings_default(val: c_int) {
226    // SAFETY: writing an exported int global.
227    unsafe {
228        crate::abi::data_globals::xmlGetWarningsDefaultValue = val;
229    }
230}
231
232// ═══════════════════════════════════════════════════════════════════════════════
233// Public Accessors — Error Callbacks
234// ═══════════════════════════════════════════════════════════════════════════════
235
236/// Set the generic error handler.
237///
238/// # SAFETY
239///
240/// - `handler` must be a valid function pointer or NULL (to reset to default).
241/// - If non-NULL, the handler may be called at any time with `ctx`.
242pub unsafe fn set_generic_error_func(ctx: *mut c_void, handler: Option<xmlGenericErrorFunc>) {
243    // SAFETY: writing the exported C globals xmlGenericErrorContext /
244    // xmlGenericError; matches upstream xmlSetGenericErrorFunc (NULL resets
245    // to the built-in default stderr printer, error.c).
246    unsafe {
247        crate::abi::data_globals::xmlGenericErrorContext = ctx;
248        crate::abi::data_globals::xmlGenericError = match handler {
249            Some(h) => Some(h),
250            None => crate::abi::data_globals::default_generic_error_func(),
251        };
252    }
253}
254
255/// Get the generic error handler context.
256pub fn get_generic_error_ctx() -> *mut c_void {
257    // SAFETY: reading the exported C global xmlGenericErrorContext.
258    unsafe { crate::abi::data_globals::xmlGenericErrorContext }
259}
260
261/// Get the generic error handler function pointer.
262pub fn get_generic_error_func() -> Option<xmlGenericErrorFunc> {
263    // SAFETY: reading the exported C global xmlGenericError.
264    unsafe { crate::abi::data_globals::xmlGenericError }
265}
266
267/// Set the structured error handler.
268///
269/// # SAFETY
270///
271/// - `handler` must be a valid function pointer or NULL.
272pub unsafe fn set_structured_error_func(ctx: *mut c_void, handler: Option<xmlStructuredErrorFunc>) {
273    // SAFETY: writing the exported C globals xmlStructuredErrorContext /
274    // xmlStructuredError; matches upstream xmlSetStructuredErrorFunc.
275    unsafe {
276        crate::abi::data_globals::xmlStructuredErrorContext = ctx;
277        crate::abi::data_globals::xmlStructuredError = handler;
278    }
279}
280
281/// Get the structured error handler context.
282pub fn get_structured_error_ctx() -> *mut c_void {
283    // SAFETY: reading the exported C global xmlStructuredErrorContext.
284    unsafe { crate::abi::data_globals::xmlStructuredErrorContext }
285}
286
287/// Get the structured error handler function pointer.
288pub fn get_structured_error_func() -> Option<xmlStructuredErrorFunc> {
289    // SAFETY: reading the exported C global xmlStructuredError.
290    unsafe { crate::abi::data_globals::xmlStructuredError }
291}
292
293// ═══════════════════════════════════════════════════════════════════════════════
294// Public Accessors — Thread-Local Error State
295// ═══════════════════════════════════════════════════════════════════════════════
296
297/// Get the last error for this thread.
298///
299/// Returns a mutable pointer to the last error, or NULL if no error occurred.
300/// The returned pointer is valid until the next libxml2 call in this thread.
301pub fn get_last_error() -> *mut _xmlError {
302    LAST_ERROR.with(|last| {
303        let mut last = last.borrow_mut();
304        last.as_mut()
305            .map_or(ptr::null_mut(), |e| e as *mut _xmlError)
306    })
307}
308
309/// Get a reference to the last error (for structured error callback).
310pub fn with_last_error<F, R>(f: F) -> R
311where
312    F: FnOnce(Option<&_xmlError>) -> R,
313{
314    LAST_ERROR.with(|last| f(last.borrow().as_ref()))
315}
316
317/// Set the last error for this thread.
318pub fn set_last_error(err: _xmlError) {
319    // UPSTREAM-PARITY: mirror the error into the exported C global
320    // `xmlLastError` (data-ABI, residual R-000135). Upstream keeps a single
321    // global; the candidate keeps a thread-local truth and a deep-copied
322    // mirror so C consumers see the most recent error with upstream
323    // lifetime semantics (mirror strings are owned by the mirror and freed
324    // on reset, matching xmlResetError).
325    //
326    // SAFETY: sync_xml_last_error only reads `err` and writes the global
327    // mirror with freshly owned copies; the thread-local slot takes
328    // ownership of `err` itself.
329    unsafe { crate::abi::data_globals::sync_xml_last_error(&err) };
330    LAST_ERROR.with(|last| {
331        let mut last = last.borrow_mut();
332        // Free the previous slot's owned strings (upstream xmlResetError).
333        if let Some(prev) = last.as_ref() {
334            free_error_strings(prev);
335        }
336        *last = Some(err);
337    });
338}
339
340/// Free the owned string fields of a stored error (upstream xmlResetError:
341/// message/file/str1/str2/str3 are xmlMalloc'd copies).
342fn free_error_strings(err: &_xmlError) {
343    use crate::abi::allocator::xmlFreeImpl;
344    unsafe {
345        if !err.message.is_null() {
346            xmlFreeImpl(err.message as *mut core::ffi::c_void);
347        }
348        if !err.file.is_null() {
349            xmlFreeImpl(err.file as *mut core::ffi::c_void);
350        }
351        if !err.str1.is_null() {
352            xmlFreeImpl(err.str1 as *mut core::ffi::c_void);
353        }
354        if !err.str2.is_null() {
355            xmlFreeImpl(err.str2 as *mut core::ffi::c_void);
356        }
357        if !err.str3.is_null() {
358            xmlFreeImpl(err.str3 as *mut core::ffi::c_void);
359        }
360    }
361}
362
363/// Reset the last error for this thread.
364pub fn reset_last_error() {
365    LAST_ERROR.with(|last| {
366        let mut last = last.borrow_mut();
367        if let Some(prev) = last.as_ref() {
368            free_error_strings(prev);
369        }
370        *last = None;
371    });
372    // SAFETY: frees the mirror's owned strings and zeroes the global.
373    unsafe { crate::abi::data_globals::reset_xml_last_error() };
374}
375
376// ═══════════════════════════════════════════════════════════════════════════════
377// Public Accessors — Catalog Defaults
378// ═══════════════════════════════════════════════════════════════════════════════
379
380/// Get the catalog default allow value.
381pub fn get_catalog_defaults() -> c_int {
382    CATALOG_DEFAULTS.load(Ordering::Relaxed)
383}
384
385/// Set the catalog default allow value.
386pub fn set_catalog_defaults(val: c_int) {
387    CATALOG_DEFAULTS.store(val, Ordering::Relaxed);
388}
389
390// ═══════════════════════════════════════════════════════════════════════════════
391// Initialization / Cleanup
392// ═══════════════════════════════════════════════════════════════════════════════
393
394/// Initialize the parser library.
395///
396/// Must be called before any other libxml2 functions.
397/// Safe to call multiple times (reference-counted in modern libxml2).
398///
399/// # UPSTREAM-PARITY
400///
401/// In modern libxml2 (2.12+), `xmlInitParser` is reference-counted.
402/// The first call initializes all subsystems; subsequent calls
403/// increment a counter. `xmlCleanupParser` decrements the counter
404/// and only performs cleanup when it reaches zero.
405///
406/// # SAFETY
407///
408/// Not fully thread-safe during the first call; callers should
409/// call `xmlInitParser` before creating threads.
410pub unsafe fn init_parser() {
411    let prev = INIT_REF_COUNT.fetch_add(1, Ordering::AcqRel);
412    if prev == 0 {
413        // First initialization — initialize all subsystems.
414        // 1. Initialize memory subsystem.
415        allocator::xmlInitMemory();
416
417        // 2. Mark the library as initialized.
418        versioning::set_initialized();
419
420        // 3. Initialize encoding handlers.
421        crate::xml::encoding::init_encodings();
422
423        // 4. Initialize thread support.
424        init_threads();
425    }
426}
427
428/// Clean up the parser library.
429///
430/// Should be called when the library is no longer needed.
431/// Only performs actual cleanup when the reference count reaches zero.
432///
433/// # SAFETY
434///
435/// Must not be called while other libxml2 functions are executing
436/// in any thread.
437pub unsafe fn cleanup_parser() {
438    let prev = INIT_REF_COUNT.fetch_sub(1, Ordering::AcqRel);
439    if prev <= 1 {
440        // Last cleanup — clean up all subsystems.
441        // 1. Clean up catalog.
442        crate::xml::catalog::cleanup();
443
444        // 2. Clean up encoding handlers.
445        crate::xml::encoding::cleanup_encodings();
446
447        // 3. Clean up memory.
448        allocator::xmlCleanupMemory();
449
450        // 3. Reset initialization state.
451        // Note: we do NOT reset the initialized flag in case
452        // some code checks it after cleanup. This matches
453        // upstream behavior where xmlCleanupParser is best-effort.
454    }
455}
456
457/// Initialize threading support.
458///
459/// # UPSTREAM-PARITY
460///
461/// In modern libxml2, threading is initialized automatically
462/// by `xmlInitParser`. This function exists for backward
463/// compatibility.
464///
465/// Returns 0 on success.
466pub fn init_threads() -> c_int {
467    if !THREADS_INITIALIZED.swap(true, Ordering::Release) {
468        // First initialization — no-op in Rust since we use
469        // standard thread-safe primitives.
470        // In libxml2 this would set up pthread mutexes.
471    }
472    0
473}
474
475/// Clean up threading support.
476pub fn cleanup_threads() {
477    THREADS_INITIALIZED.store(false, Ordering::Release);
478}
479
480/// Check whether threads have been initialized.
481pub fn threads_initialized() -> bool {
482    THREADS_INITIALIZED.load(Ordering::Acquire)
483}
484
485/// Get the current initialization reference count.
486pub fn init_ref_count() -> c_int {
487    INIT_REF_COUNT.load(Ordering::Relaxed)
488}
489
490// ═══════════════════════════════════════════════════════════════════════════════
491// Tests
492// ═══════════════════════════════════════════════════════════════════════════════
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497
498    #[test]
499    fn test_parser_defaults_initial_values() {
500        assert_eq!(get_validity_checking_default(), 0);
501        assert_eq!(get_do_warnings_default(), 1);
502        // UPSTREAM-PARITY (globals.c 2.15): xmlIndentTreeOutputThrDef = 1.
503        assert_eq!(get_indent_tree_output(), 1);
504        assert_eq!(get_keep_blanks_default(), 1);
505        assert_eq!(get_load_ext_dtd_default(), 0);
506        assert_eq!(get_pedantic_parser_default(), 0);
507        assert_eq!(get_substitute_entities_default(), 0);
508        assert_eq!(get_save_no_empty_tags(), 0);
509        assert_eq!(get_get_warnings_default(), 1);
510    }
511
512    #[test]
513    fn test_parser_defaults_set_and_get() {
514        set_validity_checking_default(1);
515        assert_eq!(get_validity_checking_default(), 1);
516        set_validity_checking_default(0);
517        assert_eq!(get_validity_checking_default(), 0);
518
519        set_keep_blanks_default(0);
520        assert_eq!(get_keep_blanks_default(), 0);
521        set_keep_blanks_default(1);
522        assert_eq!(get_keep_blanks_default(), 1);
523
524        set_substitute_entities_default(1);
525        assert_eq!(get_substitute_entities_default(), 1);
526        set_substitute_entities_default(0);
527        assert_eq!(get_substitute_entities_default(), 0);
528    }
529
530    #[test]
531    fn test_init_cleanup_ref_count() {
532        // Reset for test
533        unsafe {
534            init_parser();
535            assert_eq!(init_ref_count(), 1);
536
537            init_parser();
538            assert_eq!(init_ref_count(), 2);
539
540            cleanup_parser();
541            assert!(init_ref_count() == 1 || init_ref_count() == 0);
542
543            // Final cleanup
544            cleanup_parser();
545        }
546    }
547
548    #[test]
549    fn test_error_callbacks_default_handlers() {
550        // UPSTREAM-PARITY (error.c): xmlGenericError defaults to the built-in
551        // stderr printer (never NULL); xmlStructuredError defaults to NULL.
552        #[cfg(target_arch = "x86_64")]
553        assert!(get_generic_error_func().is_some());
554        assert!(get_structured_error_func().is_none());
555    }
556
557    #[test]
558    fn test_error_callbacks_set_and_get() {
559        unsafe {
560            unsafe extern "C" fn dummy_handler(_ctx: *mut c_void, _msg: *const core::ffi::c_char) {}
561            let dummy_func: xmlGenericErrorFunc = dummy_handler;
562            let dummy_ctx: *mut c_void = &mut 0 as *mut i32 as *mut c_void;
563
564            set_generic_error_func(dummy_ctx, Some(dummy_func));
565            assert!(get_generic_error_func().is_some());
566            assert_eq!(get_generic_error_ctx(), dummy_ctx);
567
568            // UPSTREAM-PARITY (xmlSetGenericErrorFunc): NULL resets to the
569            // built-in default printer, it does not unset the handler.
570            set_generic_error_func(ptr::null_mut(), None);
571            #[cfg(target_arch = "x86_64")]
572            assert!(get_generic_error_func().is_some());
573            assert_eq!(get_generic_error_ctx(), ptr::null_mut());
574        }
575    }
576
577    #[test]
578    fn test_last_error_thread_local() {
579        assert!(get_last_error().is_null());
580
581        let err = _xmlError {
582            domain: XML_FROM_PARSER,
583            code: XML_ERR_OK as c_int,
584            message: ptr::null_mut(),
585            level: XML_ERR_NONE as c_int,
586            file: ptr::null_mut(),
587            line: 0,
588            str1: ptr::null_mut(),
589            str2: ptr::null_mut(),
590            str3: ptr::null_mut(),
591            int1: 0,
592            int2: 0,
593            ctxt: ptr::null_mut(),
594            node: ptr::null_mut(),
595        };
596        set_last_error(err);
597        assert!(!get_last_error().is_null());
598        unsafe {
599            assert_eq!((*get_last_error()).domain, XML_FROM_PARSER);
600        }
601
602        reset_last_error();
603        assert!(get_last_error().is_null());
604    }
605
606    #[test]
607    fn test_catalog_defaults() {
608        // Save original value (may have been set by init_parser in other tests)
609        let orig = get_catalog_defaults();
610        set_catalog_defaults(1);
611        assert_eq!(get_catalog_defaults(), 1);
612        set_catalog_defaults(0);
613        assert_eq!(get_catalog_defaults(), 0);
614        // Restore
615        set_catalog_defaults(orig);
616    }
617}