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 thread support.
375 init_threads();
376 }
377}
378
379/// Clean up the parser library.
380///
381/// Should be called when the library is no longer needed.
382/// Only performs actual cleanup when the reference count reaches zero.
383///
384/// # SAFETY
385///
386/// Must not be called while other libxml2 functions are executing
387/// in any thread.
388pub unsafe fn cleanup_parser() {
389 let prev = INIT_REF_COUNT.fetch_sub(1, Ordering::AcqRel);
390 if prev <= 1 {
391 // Last cleanup — clean up all subsystems.
392 // 1. Clean up catalog.
393 // (delegated to xmlCatalogCleanup when implemented)
394
395 // 2. Clean up memory.
396 allocator::xmlCleanupMemory();
397
398 // 3. Reset initialization state.
399 // Note: we do NOT reset the initialized flag in case
400 // some code checks it after cleanup. This matches
401 // upstream behavior where xmlCleanupParser is best-effort.
402 }
403}
404
405/// Initialize threading support.
406///
407/// # UPSTREAM-PARITY
408///
409/// In modern libxml2, threading is initialized automatically
410/// by `xmlInitParser`. This function exists for backward
411/// compatibility.
412///
413/// Returns 0 on success.
414pub fn init_threads() -> c_int {
415 if !THREADS_INITIALIZED.swap(true, Ordering::Release) {
416 // First initialization — no-op in Rust since we use
417 // standard thread-safe primitives.
418 // In libxml2 this would set up pthread mutexes.
419 }
420 0
421}
422
423/// Clean up threading support.
424pub fn cleanup_threads() {
425 THREADS_INITIALIZED.store(false, Ordering::Release);
426}
427
428/// Check whether threads have been initialized.
429pub fn threads_initialized() -> bool {
430 THREADS_INITIALIZED.load(Ordering::Acquire)
431}
432
433/// Get the current initialization reference count.
434pub fn init_ref_count() -> c_int {
435 INIT_REF_COUNT.load(Ordering::Relaxed)
436}
437
438// ═══════════════════════════════════════════════════════════════════════════════
439// Tests
440// ═══════════════════════════════════════════════════════════════════════════════
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn test_parser_defaults_initial_values() {
448 assert_eq!(get_validity_checking_default(), 0);
449 assert_eq!(get_do_warnings_default(), 1);
450 assert_eq!(get_indent_tree_output(), 0);
451 assert_eq!(get_keep_blanks_default(), 1);
452 assert_eq!(get_load_ext_dtd_default(), 0);
453 assert_eq!(get_pedantic_parser_default(), 0);
454 assert_eq!(get_substitute_entities_default(), 0);
455 assert_eq!(get_save_no_empty_tags(), 0);
456 assert_eq!(get_get_warnings_default(), 1);
457 }
458
459 #[test]
460 fn test_parser_defaults_set_and_get() {
461 set_validity_checking_default(1);
462 assert_eq!(get_validity_checking_default(), 1);
463 set_validity_checking_default(0);
464 assert_eq!(get_validity_checking_default(), 0);
465
466 set_keep_blanks_default(0);
467 assert_eq!(get_keep_blanks_default(), 0);
468 set_keep_blanks_default(1);
469 assert_eq!(get_keep_blanks_default(), 1);
470
471 set_substitute_entities_default(1);
472 assert_eq!(get_substitute_entities_default(), 1);
473 set_substitute_entities_default(0);
474 assert_eq!(get_substitute_entities_default(), 0);
475 }
476
477 #[test]
478 fn test_init_cleanup_ref_count() {
479 // Reset for test
480 unsafe {
481 init_parser();
482 assert_eq!(init_ref_count(), 1);
483
484 init_parser();
485 assert_eq!(init_ref_count(), 2);
486
487 cleanup_parser();
488 assert!(init_ref_count() == 1 || init_ref_count() == 0);
489
490 // Final cleanup
491 cleanup_parser();
492 }
493 }
494
495 #[test]
496 fn test_error_callbacks_default_null() {
497 assert!(get_generic_error_func().is_none());
498 assert!(get_structured_error_func().is_none());
499 }
500
501 #[test]
502 fn test_error_callbacks_set_and_get() {
503 unsafe {
504 unsafe extern "C" fn dummy_handler(_ctx: *mut c_void, _msg: *const core::ffi::c_char) {}
505 let dummy_func: xmlGenericErrorFunc = dummy_handler;
506 let dummy_ctx: *mut c_void = &mut 0 as *mut i32 as *mut c_void;
507
508 set_generic_error_func(dummy_ctx, Some(dummy_func));
509 assert!(get_generic_error_func().is_some());
510 assert_eq!(get_generic_error_ctx(), dummy_ctx);
511
512 set_generic_error_func(ptr::null_mut(), None);
513 assert!(get_generic_error_func().is_none());
514 }
515 }
516
517 #[test]
518 fn test_last_error_thread_local() {
519 assert!(get_last_error().is_null());
520
521 let err = _xmlError {
522 domain: XML_FROM_PARSER,
523 code: XML_ERR_OK as c_int,
524 message: ptr::null_mut(),
525 level: XML_ERR_NONE as c_int,
526 file: ptr::null_mut(),
527 line: 0,
528 str1: ptr::null_mut(),
529 str2: ptr::null_mut(),
530 str3: ptr::null_mut(),
531 int1: 0,
532 int2: 0,
533 ctxt: ptr::null_mut(),
534 node: ptr::null_mut(),
535 };
536 set_last_error(err);
537 assert!(!get_last_error().is_null());
538 unsafe {
539 assert_eq!((*get_last_error()).domain, XML_FROM_PARSER);
540 }
541
542 reset_last_error();
543 assert!(get_last_error().is_null());
544 }
545
546 #[test]
547 fn test_catalog_defaults() {
548 assert_eq!(get_catalog_defaults(), 0);
549 set_catalog_defaults(1);
550 assert_eq!(get_catalog_defaults(), 1);
551 set_catalog_defaults(0);
552 assert_eq!(get_catalog_defaults(), 0);
553 }
554}