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// extern int xmlSubstituteEntitiesDefaultValue;
69// extern int xmlSaveNoEmptyTags;
70// extern int xmlGetWarningsDefaultValue;
71// ```
72
73/// Default: perform validity checking.
74/// Controlled by --valid / --novalid flags in xmllint.
75static VALIDITY_CHECKING_DEFAULT: AtomicI32 = AtomicI32::new(0);
76
77/// Default: emit warnings.
78static DO_WARNINGS_DEFAULT: AtomicI32 = AtomicI32::new(1);
79
80/// Default: indent tree output.
81static INDENT_TREE_OUTPUT: AtomicI32 = AtomicI32::new(0);
82
83/// Default: keep blank nodes.
84static KEEP_BLANKS_DEFAULT: AtomicI32 = AtomicI32::new(1);
85
86/// Default: load external DTD subsets.
87/// Bitmask of XML_DETECT_IDS, XML_COMPLETE_ATTRS, XML_SKIP_IDS.
88static LOAD_EXT_DTD_DEFAULT: AtomicI32 = AtomicI32::new(0);
89
90/// Default: pedantic parser mode.
91static PEDANTIC_PARSER_DEFAULT: AtomicI32 = AtomicI32::new(0);
92
93/// Default: substitute entities.
94static SUBSTITUTE_ENTITIES_DEFAULT: AtomicI32 = AtomicI32::new(0);
95
96/// Default: save empty tags.
97static SAVE_NO_EMPTY_TAGS: AtomicI32 = AtomicI32::new(0);
98
99/// Default: get warnings.
100static GET_WARNINGS_DEFAULT: AtomicI32 = AtomicI32::new(1);
101
102// ═══════════════════════════════════════════════════════════════════════════════
103// Error Callback Globals
104// ═══════════════════════════════════════════════════════════════════════════════
105
106/// Generic error handler context pointer.
107static GENERIC_ERROR_CTX: AtomicPtr<c_void> = AtomicPtr::new(ptr::null_mut());
108
109/// Generic error handler function pointer.
110static GENERIC_ERROR_FUNC: AtomicPtr<c_void> = AtomicPtr::new(ptr::null_mut());
111
112/// Structured error handler context pointer.
113static STRUCTURED_ERROR_CTX: AtomicPtr<c_void> = AtomicPtr::new(ptr::null_mut());
114
115/// Structured error handler function pointer.
116static STRUCTURED_ERROR_FUNC: AtomicPtr<c_void> = AtomicPtr::new(ptr::null_mut());
117
118// ═══════════════════════════════════════════════════════════════════════════════
119// Catalog Defaults
120// ═══════════════════════════════════════════════════════════════════════════════
121
122/// Catalog default allow value.
123/// 0 = strict, 1 = allow, 2 = allow document, -1 = none.
124static CATALOG_DEFAULTS: AtomicI32 = AtomicI32::new(0);
125
126// ═══════════════════════════════════════════════════════════════════════════════
127// Thread-Local Error State
128// ═══════════════════════════════════════════════════════════════════════════════
129
130thread_local! {
131    /// Last error for this thread.
132    static LAST_ERROR: RefCell<Option<_xmlError>> = const { RefCell::new(None) };
133}
134
135// ═══════════════════════════════════════════════════════════════════════════════
136// Public Accessors — Parser Defaults
137// ═══════════════════════════════════════════════════════════════════════════════
138
139/// Get the default validity checking value.
140pub fn get_validity_checking_default() -> c_int {
141    VALIDITY_CHECKING_DEFAULT.load(Ordering::Relaxed)
142}
143
144/// Set the default validity checking value.
145pub fn set_validity_checking_default(val: c_int) {
146    VALIDITY_CHECKING_DEFAULT.store(val, Ordering::Relaxed);
147}
148
149/// Get the default warnings value.
150pub fn get_do_warnings_default() -> c_int {
151    DO_WARNINGS_DEFAULT.load(Ordering::Relaxed)
152}
153
154/// Set the default warnings value.
155pub fn set_do_warnings_default(val: c_int) {
156    DO_WARNINGS_DEFAULT.store(val, Ordering::Relaxed);
157}
158
159/// Get the indent tree output default.
160pub fn get_indent_tree_output() -> c_int {
161    INDENT_TREE_OUTPUT.load(Ordering::Relaxed)
162}
163
164/// Set the indent tree output default.
165pub fn set_indent_tree_output(val: c_int) {
166    INDENT_TREE_OUTPUT.store(val, Ordering::Relaxed);
167}
168
169/// Get the keep blanks default value.
170pub fn get_keep_blanks_default() -> c_int {
171    KEEP_BLANKS_DEFAULT.load(Ordering::Relaxed)
172}
173
174/// Set the keep blanks default value.
175pub fn set_keep_blanks_default(val: c_int) {
176    KEEP_BLANKS_DEFAULT.store(val, Ordering::Relaxed);
177}
178
179/// Get the load external DTD default value.
180pub fn get_load_ext_dtd_default() -> c_int {
181    LOAD_EXT_DTD_DEFAULT.load(Ordering::Relaxed)
182}
183
184/// Set the load external DTD default value.
185pub fn set_load_ext_dtd_default(val: c_int) {
186    LOAD_EXT_DTD_DEFAULT.store(val, Ordering::Relaxed);
187}
188
189/// Get the pedantic parser default.
190pub fn get_pedantic_parser_default() -> c_int {
191    PEDANTIC_PARSER_DEFAULT.load(Ordering::Relaxed)
192}
193
194/// Set the pedantic parser default.
195pub fn set_pedantic_parser_default(val: c_int) {
196    PEDANTIC_PARSER_DEFAULT.store(val, Ordering::Relaxed);
197}
198
199/// Get the substitute entities default.
200pub fn get_substitute_entities_default() -> c_int {
201    SUBSTITUTE_ENTITIES_DEFAULT.load(Ordering::Relaxed)
202}
203
204/// Set the substitute entities default.
205pub fn set_substitute_entities_default(val: c_int) {
206    SUBSTITUTE_ENTITIES_DEFAULT.store(val, Ordering::Relaxed);
207}
208
209/// Get the save no empty tags default.
210pub fn get_save_no_empty_tags() -> c_int {
211    SAVE_NO_EMPTY_TAGS.load(Ordering::Relaxed)
212}
213
214/// Set the save no empty tags default.
215pub fn set_save_no_empty_tags(val: c_int) {
216    SAVE_NO_EMPTY_TAGS.store(val, Ordering::Relaxed);
217}
218
219/// Get the get warnings default.
220pub fn get_get_warnings_default() -> c_int {
221    GET_WARNINGS_DEFAULT.load(Ordering::Relaxed)
222}
223
224/// Set the get warnings default.
225pub fn set_get_warnings_default(val: c_int) {
226    GET_WARNINGS_DEFAULT.store(val, Ordering::Relaxed);
227}
228
229// ═══════════════════════════════════════════════════════════════════════════════
230// Public Accessors — Error Callbacks
231// ═══════════════════════════════════════════════════════════════════════════════
232
233/// Set the generic error handler.
234///
235/// # SAFETY
236///
237/// - `handler` must be a valid function pointer or NULL (to reset to default).
238/// - If non-NULL, the handler may be called at any time with `ctx`.
239pub unsafe fn set_generic_error_func(ctx: *mut c_void, handler: Option<xmlGenericErrorFunc>) {
240    GENERIC_ERROR_CTX.store(ctx, Ordering::Release);
241    GENERIC_ERROR_FUNC.store(
242        handler.map_or(ptr::null_mut(), |f| f as *mut c_void),
243        Ordering::Release,
244    );
245}
246
247/// Get the generic error handler context.
248pub fn get_generic_error_ctx() -> *mut c_void {
249    GENERIC_ERROR_CTX.load(Ordering::Acquire)
250}
251
252/// Get the generic error handler function pointer.
253pub fn get_generic_error_func() -> Option<xmlGenericErrorFunc> {
254    let ptr = GENERIC_ERROR_FUNC.load(Ordering::Acquire);
255    if ptr.is_null() {
256        None
257    } else {
258        // SAFETY: The pointer was set by set_generic_error_func and must be valid.
259        Some(unsafe { core::mem::transmute::<*mut c_void, xmlGenericErrorFunc>(ptr) })
260    }
261}
262
263/// Set the structured error handler.
264///
265/// # SAFETY
266///
267/// - `handler` must be a valid function pointer or NULL.
268pub unsafe fn set_structured_error_func(ctx: *mut c_void, handler: Option<xmlStructuredErrorFunc>) {
269    STRUCTURED_ERROR_CTX.store(ctx, Ordering::Release);
270    STRUCTURED_ERROR_FUNC.store(
271        handler.map_or(ptr::null_mut(), |f| f as *mut c_void),
272        Ordering::Release,
273    );
274}
275
276/// Get the structured error handler context.
277pub fn get_structured_error_ctx() -> *mut c_void {
278    STRUCTURED_ERROR_CTX.load(Ordering::Acquire)
279}
280
281/// Get the structured error handler function pointer.
282pub fn get_structured_error_func() -> Option<xmlStructuredErrorFunc> {
283    let ptr = STRUCTURED_ERROR_FUNC.load(Ordering::Acquire);
284    if ptr.is_null() {
285        None
286    } else {
287        // SAFETY: The pointer was set by set_structured_error_func and must be valid.
288        Some(unsafe { core::mem::transmute::<*mut c_void, xmlStructuredErrorFunc>(ptr) })
289    }
290}
291
292// ═══════════════════════════════════════════════════════════════════════════════
293// Public Accessors — Thread-Local Error State
294// ═══════════════════════════════════════════════════════════════════════════════
295
296/// Get the last error for this thread.
297///
298/// Returns a mutable pointer to the last error, or NULL if no error occurred.
299/// The returned pointer is valid until the next libxml2 call in this thread.
300pub fn get_last_error() -> *mut _xmlError {
301    LAST_ERROR.with(|last| {
302        let mut last = last.borrow_mut();
303        last.as_mut()
304            .map_or(ptr::null_mut(), |e| e as *mut _xmlError)
305    })
306}
307
308/// Get a reference to the last error (for structured error callback).
309pub fn with_last_error<F, R>(f: F) -> R
310where
311    F: FnOnce(Option<&_xmlError>) -> R,
312{
313    LAST_ERROR.with(|last| f(last.borrow().as_ref()))
314}
315
316/// Set the last error for this thread.
317pub fn set_last_error(err: _xmlError) {
318    LAST_ERROR.with(|last| {
319        *last.borrow_mut() = Some(err);
320    });
321}
322
323/// Reset the last error for this thread.
324pub fn reset_last_error() {
325    LAST_ERROR.with(|last| {
326        *last.borrow_mut() = None;
327    });
328}
329
330// ═══════════════════════════════════════════════════════════════════════════════
331// Public Accessors — Catalog Defaults
332// ═══════════════════════════════════════════════════════════════════════════════
333
334/// Get the catalog default allow value.
335pub fn get_catalog_defaults() -> c_int {
336    CATALOG_DEFAULTS.load(Ordering::Relaxed)
337}
338
339/// Set the catalog default allow value.
340pub fn set_catalog_defaults(val: c_int) {
341    CATALOG_DEFAULTS.store(val, Ordering::Relaxed);
342}
343
344// ═══════════════════════════════════════════════════════════════════════════════
345// Initialization / Cleanup
346// ═══════════════════════════════════════════════════════════════════════════════
347
348/// Initialize the parser library.
349///
350/// Must be called before any other libxml2 functions.
351/// Safe to call multiple times (reference-counted in modern libxml2).
352///
353/// # UPSTREAM-PARITY
354///
355/// In modern libxml2 (2.12+), `xmlInitParser` is reference-counted.
356/// The first call initializes all subsystems; subsequent calls
357/// increment a counter. `xmlCleanupParser` decrements the counter
358/// and only performs cleanup when it reaches zero.
359///
360/// # SAFETY
361///
362/// Not fully thread-safe during the first call; callers should
363/// call `xmlInitParser` before creating threads.
364pub unsafe fn init_parser() {
365    let prev = INIT_REF_COUNT.fetch_add(1, Ordering::AcqRel);
366    if prev == 0 {
367        // First initialization — initialize all subsystems.
368        // 1. Initialize memory subsystem.
369        allocator::xmlInitMemory();
370
371        // 2. Mark the library as initialized.
372        versioning::set_initialized();
373
374        // 3. Initialize encoding handlers.
375        crate::xml::encoding::init_encodings();
376
377        // 4. Initialize thread support.
378        init_threads();
379    }
380}
381
382/// Clean up the parser library.
383///
384/// Should be called when the library is no longer needed.
385/// Only performs actual cleanup when the reference count reaches zero.
386///
387/// # SAFETY
388///
389/// Must not be called while other libxml2 functions are executing
390/// in any thread.
391pub unsafe fn cleanup_parser() {
392    let prev = INIT_REF_COUNT.fetch_sub(1, Ordering::AcqRel);
393    if prev <= 1 {
394        // Last cleanup — clean up all subsystems.
395        // 1. Clean up catalog.
396        crate::xml::catalog::cleanup();
397
398        // 2. Clean up encoding handlers.
399        crate::xml::encoding::cleanup_encodings();
400
401        // 3. Clean up memory.
402        allocator::xmlCleanupMemory();
403
404        // 3. Reset initialization state.
405        // Note: we do NOT reset the initialized flag in case
406        // some code checks it after cleanup. This matches
407        // upstream behavior where xmlCleanupParser is best-effort.
408    }
409}
410
411/// Initialize threading support.
412///
413/// # UPSTREAM-PARITY
414///
415/// In modern libxml2, threading is initialized automatically
416/// by `xmlInitParser`. This function exists for backward
417/// compatibility.
418///
419/// Returns 0 on success.
420pub fn init_threads() -> c_int {
421    if !THREADS_INITIALIZED.swap(true, Ordering::Release) {
422        // First initialization — no-op in Rust since we use
423        // standard thread-safe primitives.
424        // In libxml2 this would set up pthread mutexes.
425    }
426    0
427}
428
429/// Clean up threading support.
430pub fn cleanup_threads() {
431    THREADS_INITIALIZED.store(false, Ordering::Release);
432}
433
434/// Check whether threads have been initialized.
435pub fn threads_initialized() -> bool {
436    THREADS_INITIALIZED.load(Ordering::Acquire)
437}
438
439/// Get the current initialization reference count.
440pub fn init_ref_count() -> c_int {
441    INIT_REF_COUNT.load(Ordering::Relaxed)
442}
443
444// ═══════════════════════════════════════════════════════════════════════════════
445// Tests
446// ═══════════════════════════════════════════════════════════════════════════════
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    #[test]
453    fn test_parser_defaults_initial_values() {
454        assert_eq!(get_validity_checking_default(), 0);
455        assert_eq!(get_do_warnings_default(), 1);
456        assert_eq!(get_indent_tree_output(), 0);
457        assert_eq!(get_keep_blanks_default(), 1);
458        assert_eq!(get_load_ext_dtd_default(), 0);
459        assert_eq!(get_pedantic_parser_default(), 0);
460        assert_eq!(get_substitute_entities_default(), 0);
461        assert_eq!(get_save_no_empty_tags(), 0);
462        assert_eq!(get_get_warnings_default(), 1);
463    }
464
465    #[test]
466    fn test_parser_defaults_set_and_get() {
467        set_validity_checking_default(1);
468        assert_eq!(get_validity_checking_default(), 1);
469        set_validity_checking_default(0);
470        assert_eq!(get_validity_checking_default(), 0);
471
472        set_keep_blanks_default(0);
473        assert_eq!(get_keep_blanks_default(), 0);
474        set_keep_blanks_default(1);
475        assert_eq!(get_keep_blanks_default(), 1);
476
477        set_substitute_entities_default(1);
478        assert_eq!(get_substitute_entities_default(), 1);
479        set_substitute_entities_default(0);
480        assert_eq!(get_substitute_entities_default(), 0);
481    }
482
483    #[test]
484    fn test_init_cleanup_ref_count() {
485        // Reset for test
486        unsafe {
487            init_parser();
488            assert_eq!(init_ref_count(), 1);
489
490            init_parser();
491            assert_eq!(init_ref_count(), 2);
492
493            cleanup_parser();
494            assert!(init_ref_count() == 1 || init_ref_count() == 0);
495
496            // Final cleanup
497            cleanup_parser();
498        }
499    }
500
501    #[test]
502    fn test_error_callbacks_default_null() {
503        assert!(get_generic_error_func().is_none());
504        assert!(get_structured_error_func().is_none());
505    }
506
507    #[test]
508    fn test_error_callbacks_set_and_get() {
509        unsafe {
510            unsafe extern "C" fn dummy_handler(_ctx: *mut c_void, _msg: *const core::ffi::c_char) {}
511            let dummy_func: xmlGenericErrorFunc = dummy_handler;
512            let dummy_ctx: *mut c_void = &mut 0 as *mut i32 as *mut c_void;
513
514            set_generic_error_func(dummy_ctx, Some(dummy_func));
515            assert!(get_generic_error_func().is_some());
516            assert_eq!(get_generic_error_ctx(), dummy_ctx);
517
518            set_generic_error_func(ptr::null_mut(), None);
519            assert!(get_generic_error_func().is_none());
520        }
521    }
522
523    #[test]
524    fn test_last_error_thread_local() {
525        assert!(get_last_error().is_null());
526
527        let err = _xmlError {
528            domain: XML_FROM_PARSER,
529            code: XML_ERR_OK as c_int,
530            message: ptr::null_mut(),
531            level: XML_ERR_NONE as c_int,
532            file: ptr::null_mut(),
533            line: 0,
534            str1: ptr::null_mut(),
535            str2: ptr::null_mut(),
536            str3: ptr::null_mut(),
537            int1: 0,
538            int2: 0,
539            ctxt: ptr::null_mut(),
540            node: ptr::null_mut(),
541        };
542        set_last_error(err);
543        assert!(!get_last_error().is_null());
544        unsafe {
545            assert_eq!((*get_last_error()).domain, XML_FROM_PARSER);
546        }
547
548        reset_last_error();
549        assert!(get_last_error().is_null());
550    }
551
552    #[test]
553    fn test_catalog_defaults() {
554        // Save original value (may have been set by init_parser in other tests)
555        let orig = get_catalog_defaults();
556        set_catalog_defaults(1);
557        assert_eq!(get_catalog_defaults(), 1);
558        set_catalog_defaults(0);
559        assert_eq!(get_catalog_defaults(), 0);
560        // Restore
561        set_catalog_defaults(orig);
562    }
563}