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}