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//!
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//! Phase 13 (HOSTILE-THREADS): the 2.15 LIBXML_THREAD_ENABLED model keeps the
39//! parser defaults, the error-handler slots and the node/IO hooks in
40//! THREAD-LOCAL storage (globals.c `xmlGetThreadLocalStorage`); the oracle
41//! DSO exports only the `__xml*` accessor functions for them. The candidate
42//! mirrors that model with `tls` cells (single source of truth per thread);
43//! the C-visible data symbols were removed for those 18 globals and the
44//! candidate headers now use the upstream macro/accessor contract.
45//! Everything else (`xmlParserVersion`, `xmlDefaultSAXHandler`, the
46//! allocator hooks, the `xmlLastError` mirror, ...) remains plain global
47//! data, matching the executed oracle.
48//!
49//! # Conceptual behavior
50//!
51//! Manages parser defaults, generic/structured error callbacks, catalog
52//! defaults, memory hooks, thread-local error state and init/cleanup
53//! reference counting. The exported data symbols are wired to the parser-
54//! default accessors so there is a single source of truth.
55//!
56//! # Ownership & safety invariants
57//!
58//! SAFETY: the TLS-era globals are per-thread cells (see `tls`); the
59//! (handler, ctx) slot pairs are serialized under ERROR_HANDLER_LOCK so
60//! readers never observe a new handler with an old context (R-000171) — the
61//! lock now serializes same-thread set/get, since each thread owns its own
62//! slots. The xmlLastError mirror is deep- copied under
63//! LAST_ERROR_MIRROR_LOCK (R-000170: concurrent sync/reset double-freed the
64//! mirror strings). Error state is thread-local.
65//!
66//! # Historical quirks & epochs
67//!
68//! Thread support predates the thread-local-globals era: globals.c threading
69//! integrated 2001-10-12/13 (commits b847864f, d0463560; LORE-0005). Modern
70//! 2.10+ initialization is lazy; deprecated init/cleanup entry points are
71//! genuine no-ops (R-000138). R-000161 fixed the exported default values
72//! (xmlLineNumbersDefaultValue 1, xmlTreeIndentString two spaces,
73//! xmlParserVersion 21503-GITv2.15.3).
74//!
75//! # Deliberate oddities
76//!
77//! Deliberate oddities: xmlGenericError and xsltGenericError default to the
78//! variadic stderr printers (asm va_list shims; R-000161) rather than NULL;
79//! the deprecated init entry points are exported no-ops reproducing upstream
80//! empty bodies (R-000138).
81//!
82//! # Proving courts
83//!
84//! ABI-DATA, ALLOCATOR, GLOBAL-STATE and THREADING court families;
85//! DATA-GLOBALS-001 differential probe (byte-identical), GLOBALS-THREADING
86//! probe, DSO-LOADER, and `cargo test --lib` (100/100 parallel runs
87//! clean after R-000170/R-000171).
88//!
89//! # Tempting simplifications that would break parity
90//!
91//! The tempting simplification is hiding the C globals behind accessors only
92//! — downstream code that reads/writes xmlDoValidityCheckingDefaultValue
93//! directly would fail to link (R-000135). Do not fix the racy C-visible
94//! symbols: the internal locks must not change the documented upstream
95//! semantics for direct consumers.
96//!
97//! # Safety
98//!
99//! - The module-level statics are synchronization primitives only
100//!   (atomics, a `parking_lot` mutex, and a `thread_local` cell); there are
101//!   no top-level `unsafe` blocks outside functions. Reads and writes of
102//!   the exported C globals in `crate::abi::data_globals` happen only
103//!   inside the accessor functions, where the `(handler, ctx)` slot pairs
104//!   are serialized under `ERROR_HANDLER_LOCK` so a reader never observes a
105//!   new handler with an old context. The thread-local `LAST_ERROR` slot is
106//!   only mutated through `set_last_error`/`reset_last_error`, which free
107//!   the previous slot's owned strings exactly once.
108use core::cell::RefCell;
109use core::ffi::c_void;
110use core::ptr;
111use core::sync::atomic::{AtomicBool, AtomicI32, Ordering};
112use std::os::raw::{c_char, c_int};
113
114use crate::abi::allocator;
115use crate::abi::callbacks::{xmlGenericErrorFunc, xmlStructuredErrorFunc};
116use crate::abi::structs::{_xmlError, _xmlNode, _xmlOutputBuffer, _xmlParserInputBuffer};
117use crate::abi::types::xmlChar;
118use crate::abi::versioning;
119
120pub mod tls;
121use tls::{tls_get, tls_ptr, tls_set};
122
123/// Serializes the exported error-handler slot pairs
124/// (`xmlGenericError`/`xmlGenericErrorContext` and
125/// `xmlStructuredError`/`xmlStructuredErrorContext`). Upstream's globals are
126/// bare racy `static mut` slots; the candidate keeps the C-visible symbols
127/// but makes internal set/get atomic as a (handler, ctx) pair so readers
128/// never observe a new handler with an old context (or vice versa). C
129/// consumers that touch the symbols directly keep upstream's documented
130/// racy semantics.
131static ERROR_HANDLER_LOCK: parking_lot::Mutex<()> = parking_lot::Mutex::new(());
132
133/// Serializes the error-handler tests that mutate the shared handler slots
134/// (11.1-X regression court wiring): `test_error_callbacks_*` (globals) and
135/// `test_structured_error_callback` (errors) must not run concurrently, or
136/// `test_error_callbacks_default_handlers` observes another test's
137/// temporarily-installed structured handler.
138#[cfg(test)]
139pub(crate) static ERROR_HANDLER_TEST_LOCK: parking_lot::Mutex<()> = parking_lot::Mutex::new(());
140
141// ═══════════════════════════════════════════════════════════════════════════════
142// Initialization Reference Counting
143// ═══════════════════════════════════════════════════════════════════════════════
144
145/// Reference count for xmlInitParser / xmlCleanupParser.
146static INIT_REF_COUNT: AtomicI32 = AtomicI32::new(0);
147
148/// Whether threading has been initialized.
149static THREADS_INITIALIZED: AtomicBool = AtomicBool::new(false);
150
151// ═══════════════════════════════════════════════════════════════════════════════
152// Parser Defaults
153// ═══════════════════════════════════════════════════════════════════════════════
154//
155// These are the global parser default variables exposed by libxml2's ABI.
156// Applications can read and write them directly to change default behavior.
157//
158// Upstream declarations (from parser.h / parserInternals.h):
159//
160// ```c
161// extern int xmlDoValidityCheckingDefaultValue;
162// extern int xmlDoWarningsDefaultValue;
163// extern int xmlIndentTreeOutput;
164// extern int xmlKeepBlanksDefaultValue;
165// extern int xmlLoadExtDtdDefaultValue;
166// extern int xmlPedanticParserDefaultValue;
167// ═══════════════════════════════════════════════════════════════════════════════
168// Parser Defaults
169// ═══════════════════════════════════════════════════════════════════════════════
170//
171// The defaults live in the EXPORTED C globals (src/abi/data_globals.rs) so
172// that downstream C code reading/writing them directly (the upstream
173// contract) observes and controls the same state the parser uses. The
174// accessors below are the safe-Rust view of those statics.
175
176// ═══════════════════════════════════════════════════════════════════════════════
177// Error Callback Globals
178// ═══════════════════════════════════════════════════════════════════════════════
179// Also exported as C globals (xmlGenericError/xmlGenericErrorContext,
180// xmlStructuredError/xmlStructuredErrorContext).
181
182// ═══════════════════════════════════════════════════════════════════════════════
183// Catalog Defaults
184// ═══════════════════════════════════════════════════════════════════════════════
185
186/// Catalog default allow value.
187/// 0 = strict, 1 = allow, 2 = allow document, -1 = none.
188/// Internal only (upstream keeps catalog state inside xmlCatalogSetDefaults;
189/// there is no public C global for it).
190static CATALOG_DEFAULTS: AtomicI32 = AtomicI32::new(0);
191
192// ═══════════════════════════════════════════════════════════════════════════════
193// Thread-Local Error State
194// ═══════════════════════════════════════════════════════════════════════════════
195
196thread_local! {
197    /// Last error for this thread.
198    static LAST_ERROR: RefCell<Option<_xmlError>> = const { RefCell::new(None) };
199}
200
201// ═══════════════════════════════════════════════════════════════════════════════
202// Public Accessors — Parser Defaults
203// ═══════════════════════════════════════════════════════════════════════════════
204
205/// Get the default validity checking value (thread-local, upstream 2.15).
206pub fn get_validity_checking_default() -> c_int {
207    tls_get(&tls::DO_VALIDITY)
208}
209
210/// Set the default validity checking value (thread-local, upstream 2.15).
211pub fn set_validity_checking_default(val: c_int) {
212    tls_set(&tls::DO_VALIDITY, val);
213}
214
215/// Get the default warnings value (thread-local, upstream 2.15).
216pub fn get_do_warnings_default() -> c_int {
217    tls_get(&tls::GET_WARNINGS)
218}
219
220/// Set the default warnings value (thread-local, upstream 2.15).
221pub fn set_do_warnings_default(val: c_int) {
222    tls_set(&tls::GET_WARNINGS, val);
223}
224
225/// Get the indent tree output default (thread-local, upstream 2.15).
226pub fn get_indent_tree_output() -> c_int {
227    tls_get(&tls::INDENT_TREE_OUTPUT)
228}
229
230/// Set the indent tree output default (thread-local, upstream 2.15).
231pub fn set_indent_tree_output(val: c_int) {
232    tls_set(&tls::INDENT_TREE_OUTPUT, val);
233}
234
235/// Get the keep blanks default value (thread-local, upstream 2.15).
236pub fn get_keep_blanks_default() -> c_int {
237    tls_get(&tls::KEEP_BLANKS)
238}
239
240/// Set the keep blanks default value (thread-local, upstream 2.15).
241pub fn set_keep_blanks_default(val: c_int) {
242    tls_set(&tls::KEEP_BLANKS, val);
243}
244
245/// Get the load external DTD default value (thread-local, upstream 2.15).
246pub fn get_load_ext_dtd_default() -> c_int {
247    tls_get(&tls::LOAD_EXT_DTD)
248}
249
250/// Set the load external DTD default value (thread-local, upstream 2.15).
251pub fn set_load_ext_dtd_default(val: c_int) {
252    tls_set(&tls::LOAD_EXT_DTD, val);
253}
254
255/// Get the pedantic parser default (thread-local, upstream 2.15).
256pub fn get_pedantic_parser_default() -> c_int {
257    tls_get(&tls::PEDANTIC)
258}
259
260/// Set the pedantic parser default (thread-local, upstream 2.15).
261pub fn set_pedantic_parser_default(val: c_int) {
262    tls_set(&tls::PEDANTIC, val);
263}
264
265/// Get the substitute entities default (thread-local, upstream 2.15).
266pub fn get_substitute_entities_default() -> c_int {
267    tls_get(&tls::SUBSTITUTE_ENTITIES)
268}
269
270/// Set the substitute entities default (thread-local, upstream 2.15).
271pub fn set_substitute_entities_default(val: c_int) {
272    tls_set(&tls::SUBSTITUTE_ENTITIES, val);
273}
274
275/// Get the save no empty tags default (thread-local, upstream 2.15).
276pub fn get_save_no_empty_tags() -> c_int {
277    tls_get(&tls::SAVE_NO_EMPTY_TAGS)
278}
279
280/// Set the save no empty tags default (thread-local, upstream 2.15).
281pub fn set_save_no_empty_tags(val: c_int) {
282    tls_set(&tls::SAVE_NO_EMPTY_TAGS, val);
283}
284
285/// Get the get warnings default (thread-local, upstream 2.15).
286pub fn get_get_warnings_default() -> c_int {
287    tls_get(&tls::GET_WARNINGS)
288}
289
290/// Set the get warnings default (thread-local, upstream 2.15).
291pub fn set_get_warnings_default(val: c_int) {
292    tls_set(&tls::GET_WARNINGS, val);
293}
294
295/// Get `xmlLineNumbersDefaultValue` (thread-local, upstream 2.15).
296pub fn get_line_numbers_default() -> c_int {
297    tls_get(&tls::LINE_NUMBERS)
298}
299
300/// Set `xmlLineNumbersDefaultValue` (thread-local, upstream 2.15).
301pub fn set_line_numbers_default(val: c_int) {
302    tls_set(&tls::LINE_NUMBERS, val);
303}
304
305/// Get `xmlTreeIndentString` (thread-local, upstream 2.15).
306pub fn get_tree_indent_string() -> *const xmlChar {
307    tls_get(&tls::TREE_INDENT_STRING)
308}
309
310/// Set `xmlTreeIndentString` (thread-local, upstream 2.15).
311pub fn set_tree_indent_string(val: *const xmlChar) {
312    tls_set(&tls::TREE_INDENT_STRING, val);
313}
314
315/// Get `xmlRegisterNodeDefaultValue` (thread-local, upstream 2.15).
316pub fn get_register_node_default() -> Option<unsafe extern "C" fn(*mut _xmlNode)> {
317    tls_get(&tls::REGISTER_NODE)
318}
319
320/// Set `xmlRegisterNodeDefaultValue` (thread-local, upstream 2.15).
321pub fn set_register_node_default(val: Option<unsafe extern "C" fn(*mut _xmlNode)>) {
322    tls_set(&tls::REGISTER_NODE, val);
323}
324
325/// Get `xmlDeregisterNodeDefaultValue` (thread-local, upstream 2.15).
326pub fn get_deregister_node_default() -> Option<unsafe extern "C" fn(*mut _xmlNode)> {
327    tls_get(&tls::DEREGISTER_NODE)
328}
329
330/// Set `xmlDeregisterNodeDefaultValue` (thread-local, upstream 2.15).
331pub fn set_deregister_node_default(val: Option<unsafe extern "C" fn(*mut _xmlNode)>) {
332    tls_set(&tls::DEREGISTER_NODE, val);
333}
334
335/// Get `xmlParserInputBufferCreateFilenameValue` (thread-local, upstream
336/// 2.15).
337pub fn get_parser_input_buffer_create_filename_value(
338) -> Option<unsafe extern "C" fn(*const c_char, c_int) -> *mut _xmlParserInputBuffer> {
339    tls_get(&tls::PARSER_INPUT_CREATE_FILENAME)
340}
341
342/// Set `xmlParserInputBufferCreateFilenameValue` (thread-local, upstream
343/// 2.15).
344pub fn set_parser_input_buffer_create_filename_value(
345    val: Option<unsafe extern "C" fn(*const c_char, c_int) -> *mut _xmlParserInputBuffer>,
346) {
347    tls_set(&tls::PARSER_INPUT_CREATE_FILENAME, val);
348}
349
350/// Get `xmlOutputBufferCreateFilenameValue` (thread-local, upstream 2.15).
351pub fn get_output_buffer_create_filename_value() -> Option<
352    unsafe extern "C" fn(
353        *const c_char,
354        crate::abi::structs::xmlCharEncodingHandlerPtr,
355        c_int,
356    ) -> *mut _xmlOutputBuffer,
357> {
358    tls_get(&tls::OUTPUT_CREATE_FILENAME)
359}
360
361/// Set `xmlOutputBufferCreateFilenameValue` (thread-local, upstream 2.15).
362pub fn set_output_buffer_create_filename_value(
363    val: Option<
364        unsafe extern "C" fn(
365            *const c_char,
366            crate::abi::structs::xmlCharEncodingHandlerPtr,
367            c_int,
368        ) -> *mut _xmlOutputBuffer,
369    >,
370) {
371    tls_set(&tls::OUTPUT_CREATE_FILENAME, val);
372}
373
374// ═══════════════════════════════════════════════════════════════════════════════
375// Public Accessors — Error Callbacks
376// ═══════════════════════════════════════════════════════════════════════════════
377
378/// Set the generic error handler.
379///
380/// # SAFETY
381///
382/// - `handler` must be a valid function pointer or NULL (to reset to default).
383/// - If non-NULL, the handler may be called at any time with `ctx`.
384pub unsafe fn set_generic_error_func(ctx: *mut c_void, handler: Option<xmlGenericErrorFunc>) {
385    // SAFETY: writing the per-thread error-handler slots
386    // xmlGenericErrorContext / xmlGenericError (TLS, upstream 2.15
387    // xmlSetGenericErrorFunc: NULL resets to the built-in default stderr
388    // printer, error.c). The (ctx, func) pair is written atomically under
389    // ERROR_HANDLER_LOCK (same-thread serialization, R-000171).
390    let resolved = match handler {
391        Some(h) => Some(h),
392        None => crate::abi::data_globals::default_generic_error_func(),
393    };
394    let _guard = ERROR_HANDLER_LOCK.lock();
395    tls_set(&tls::GENERIC_ERROR_CTX, ctx);
396    tls_set(&tls::GENERIC_ERROR, resolved);
397}
398
399/// Set the generic error handler context (thread-local; used by the default
400/// stderr printer to lazily default the context per thread, upstream
401/// error.c).
402pub fn set_generic_error_ctx(ctx: *mut c_void) {
403    let _guard = ERROR_HANDLER_LOCK.lock();
404    tls_set(&tls::GENERIC_ERROR_CTX, ctx);
405}
406
407/// Get the generic error handler context (thread-local).
408pub fn get_generic_error_ctx() -> *mut c_void {
409    let _guard = ERROR_HANDLER_LOCK.lock();
410    tls_get(&tls::GENERIC_ERROR_CTX)
411}
412
413/// Get the generic error handler function pointer (thread-local).
414pub fn get_generic_error_func() -> Option<xmlGenericErrorFunc> {
415    let _guard = ERROR_HANDLER_LOCK.lock();
416    tls_get(&tls::GENERIC_ERROR)
417}
418
419/// Read the generic error (func, ctx) pair atomically.
420///
421/// The closure runs after the lock is released, so a handler installed
422/// by the callback cannot deadlock.
423pub fn with_generic_error<R>(f: impl FnOnce(Option<xmlGenericErrorFunc>, *mut c_void) -> R) -> R {
424    let (h, c) = {
425        let _guard = ERROR_HANDLER_LOCK.lock();
426        (
427            tls_get(&tls::GENERIC_ERROR),
428            tls_get(&tls::GENERIC_ERROR_CTX),
429        )
430    };
431    f(h, c)
432}
433
434/// Set the structured error handler (thread-local, upstream 2.15).
435///
436/// # SAFETY
437///
438/// - `handler` must be a valid function pointer or NULL.
439pub unsafe fn set_structured_error_func(ctx: *mut c_void, handler: Option<xmlStructuredErrorFunc>) {
440    // SAFETY: writing the per-thread error-handler slots
441    // xmlStructuredErrorContext / xmlStructuredError (TLS, upstream 2.15
442    // xmlSetStructuredErrorFunc). The (ctx, func) pair is written
443    // atomically under ERROR_HANDLER_LOCK (same-thread serialization,
444    // R-000171).
445    let _guard = ERROR_HANDLER_LOCK.lock();
446    tls_set(&tls::STRUCTURED_ERROR_CTX, ctx);
447    tls_set(&tls::STRUCTURED_ERROR, handler);
448}
449
450/// Get the structured error handler context (thread-local).
451pub fn get_structured_error_ctx() -> *mut c_void {
452    let _guard = ERROR_HANDLER_LOCK.lock();
453    tls_get(&tls::STRUCTURED_ERROR_CTX)
454}
455
456/// Get the structured error handler function pointer (thread-local).
457pub fn get_structured_error_func() -> Option<xmlStructuredErrorFunc> {
458    let _guard = ERROR_HANDLER_LOCK.lock();
459    tls_get(&tls::STRUCTURED_ERROR)
460}
461
462/// Read the structured error (func, ctx) pair atomically.
463///
464/// The closure runs after the lock is released, so a handler installed by
465/// the callback (or an error raised from inside the handler) cannot
466/// deadlock on ERROR_HANDLER_LOCK.
467///
468/// # Safety
469///
470/// - The two reads of the per-thread `xmlStructuredError`/
471///   `xmlStructuredErrorContext` TLS slots are performed under
472///   `ERROR_HANDLER_LOCK`, matching the write side (xmlSetStructuredError),
473///   so the pair is observed atomically; the values are only borrowed for the
474///   duration of the closure and never dereferenced here.
475pub fn with_structured_error<R>(
476    f: impl FnOnce(Option<xmlStructuredErrorFunc>, *mut c_void) -> R,
477) -> R {
478    let (h, c) = {
479        let _guard = ERROR_HANDLER_LOCK.lock();
480        (
481            tls_get(&tls::STRUCTURED_ERROR),
482            tls_get(&tls::STRUCTURED_ERROR_CTX),
483        )
484    };
485    f(h, c)
486}
487
488// ═══════════════════════════════════════════════════════════════════════════════
489// `__xmlXxx()` pointer accessors (upstream threads.c / globals.c)
490// ═══════════════════════════════════════════════════════════════════════════════
491// The deprecated thread-local API exports one `__xmlXxx(void)` accessor per
492// TLS-era global; each returns a pointer to the CURRENT thread's slot so
493// callers can read/write it directly (upstream `#define xmlXxx
494// (*__xmlXxx())`). The candidate's exports (`crate::abi::data_globals`)
495// delegate here; the returned pointers alias the `tls` cells and stay valid
496// for the lifetime of the thread.
497
498pub(crate) fn do_validity_ptr() -> *mut c_int {
499    tls_ptr(&tls::DO_VALIDITY)
500}
501pub(crate) fn get_warnings_ptr() -> *mut c_int {
502    tls_ptr(&tls::GET_WARNINGS)
503}
504pub(crate) fn load_ext_dtd_ptr() -> *mut c_int {
505    tls_ptr(&tls::LOAD_EXT_DTD)
506}
507pub(crate) fn pedantic_ptr() -> *mut c_int {
508    tls_ptr(&tls::PEDANTIC)
509}
510pub(crate) fn line_numbers_ptr() -> *mut c_int {
511    tls_ptr(&tls::LINE_NUMBERS)
512}
513pub(crate) fn keep_blanks_ptr() -> *mut c_int {
514    tls_ptr(&tls::KEEP_BLANKS)
515}
516pub(crate) fn substitute_entities_ptr() -> *mut c_int {
517    tls_ptr(&tls::SUBSTITUTE_ENTITIES)
518}
519pub(crate) fn indent_tree_output_ptr() -> *mut c_int {
520    tls_ptr(&tls::INDENT_TREE_OUTPUT)
521}
522pub(crate) fn tree_indent_string_ptr() -> *mut *const xmlChar {
523    tls_ptr(&tls::TREE_INDENT_STRING)
524}
525pub(crate) fn save_no_empty_tags_ptr() -> *mut c_int {
526    tls_ptr(&tls::SAVE_NO_EMPTY_TAGS)
527}
528pub(crate) fn register_node_ptr() -> *mut Option<unsafe extern "C" fn(*mut _xmlNode)> {
529    tls_ptr(&tls::REGISTER_NODE)
530}
531pub(crate) fn deregister_node_ptr() -> *mut Option<unsafe extern "C" fn(*mut _xmlNode)> {
532    tls_ptr(&tls::DEREGISTER_NODE)
533}
534pub(crate) fn parser_input_create_filename_ptr(
535) -> *mut Option<unsafe extern "C" fn(*const c_char, c_int) -> *mut _xmlParserInputBuffer> {
536    tls_ptr(&tls::PARSER_INPUT_CREATE_FILENAME)
537}
538pub(crate) fn output_create_filename_ptr() -> *mut Option<
539    unsafe extern "C" fn(
540        *const c_char,
541        crate::abi::structs::xmlCharEncodingHandlerPtr,
542        c_int,
543    ) -> *mut _xmlOutputBuffer,
544> {
545    tls_ptr(&tls::OUTPUT_CREATE_FILENAME)
546}
547pub(crate) fn generic_error_ptr() -> *mut Option<xmlGenericErrorFunc> {
548    tls_ptr(&tls::GENERIC_ERROR)
549}
550pub(crate) fn generic_error_ctx_ptr() -> *mut *mut c_void {
551    tls_ptr(&tls::GENERIC_ERROR_CTX)
552}
553pub(crate) fn structured_error_ptr() -> *mut Option<xmlStructuredErrorFunc> {
554    tls_ptr(&tls::STRUCTURED_ERROR)
555}
556pub(crate) fn structured_error_ctx_ptr() -> *mut *mut c_void {
557    tls_ptr(&tls::STRUCTURED_ERROR_CTX)
558}
559
560// ═══════════════════════════════════════════════════════════════════════════════
561// Public Accessors — Thread-Local Error State
562// ═══════════════════════════════════════════════════════════════════════════════
563
564/// Get the last error for this thread.
565///
566/// Returns a mutable pointer to the last error, or NULL if no error occurred.
567/// The returned pointer is valid until the next libxml2 call in this thread.
568pub fn get_last_error() -> *mut _xmlError {
569    LAST_ERROR.with(|last| {
570        let mut last = last.borrow_mut();
571        last.as_mut()
572            .map_or(ptr::null_mut(), |e| e as *mut _xmlError)
573    })
574}
575
576/// Get a reference to the last error (for structured error callback).
577pub fn with_last_error<F, R>(f: F) -> R
578where
579    F: FnOnce(Option<&_xmlError>) -> R,
580{
581    LAST_ERROR.with(|last| f(last.borrow().as_ref()))
582}
583
584/// Set the last error for this thread.
585pub fn set_last_error(err: _xmlError) {
586    // UPSTREAM-PARITY: mirror the error into the exported C global
587    // `xmlLastError` (data-ABI, residual R-000135). Upstream keeps a single
588    // global; the candidate keeps a thread-local truth and a deep-copied
589    // mirror so C consumers see the most recent error with upstream
590    // lifetime semantics (mirror strings are owned by the mirror and freed
591    // on reset, matching xmlResetError).
592    //
593    // SAFETY: sync_xml_last_error only reads `err` and writes the global
594    // mirror with freshly owned copies; the thread-local slot takes
595    // ownership of `err` itself.
596    unsafe { crate::abi::data_globals::sync_xml_last_error(&err) };
597    LAST_ERROR.with(|last| {
598        let mut last = last.borrow_mut();
599        // Free the previous slot's owned strings (upstream xmlResetError).
600        if let Some(prev) = last.as_ref() {
601            free_error_strings(prev);
602        }
603        *last = Some(err);
604    });
605}
606
607/// Free the owned string fields of a stored error (upstream xmlResetError:
608/// message/file/str1/str2/str3 are xmlMalloc'd copies).
609///
610/// # Safety
611///
612/// - `err` must point to a valid `_xmlError` whose `message`, `file`,
613///   `str1`, `str2` and `str3` fields are NULL or pointers allocated with
614///   `xmlFreeImpl`'s allocator; each non-NULL field is freed exactly once
615///   and must not be freed or used again afterwards.
616pub(crate) fn free_error_strings(err: &_xmlError) {
617    use crate::abi::allocator::xmlFreeImpl;
618    unsafe {
619        if !err.message.is_null() {
620            xmlFreeImpl(err.message as *mut core::ffi::c_void);
621        }
622        if !err.file.is_null() {
623            xmlFreeImpl(err.file as *mut core::ffi::c_void);
624        }
625        if !err.str1.is_null() {
626            xmlFreeImpl(err.str1 as *mut core::ffi::c_void);
627        }
628        if !err.str2.is_null() {
629            xmlFreeImpl(err.str2 as *mut core::ffi::c_void);
630        }
631        if !err.str3.is_null() {
632            xmlFreeImpl(err.str3 as *mut core::ffi::c_void);
633        }
634    }
635}
636
637/// Reset the last error for this thread.
638pub fn reset_last_error() {
639    LAST_ERROR.with(|last| {
640        let mut last = last.borrow_mut();
641        if let Some(prev) = last.as_ref() {
642            free_error_strings(prev);
643        }
644        *last = None;
645    });
646    // SAFETY: frees the mirror's owned strings and zeroes the global.
647    unsafe { crate::abi::data_globals::reset_xml_last_error() };
648}
649
650// ═══════════════════════════════════════════════════════════════════════════════
651// Public Accessors — Catalog Defaults
652// ═══════════════════════════════════════════════════════════════════════════════
653
654/// Get the catalog default allow value.
655pub fn get_catalog_defaults() -> c_int {
656    CATALOG_DEFAULTS.load(Ordering::Relaxed)
657}
658
659/// Set the catalog default allow value.
660pub fn set_catalog_defaults(val: c_int) {
661    CATALOG_DEFAULTS.store(val, Ordering::Relaxed);
662}
663
664// ═══════════════════════════════════════════════════════════════════════════════
665// Initialization / Cleanup
666// ═══════════════════════════════════════════════════════════════════════════════
667
668/// Initialize the parser library.
669///
670/// Must be called before any other libxml2 functions.
671/// Safe to call multiple times (reference-counted in modern libxml2).
672///
673/// # UPSTREAM-PARITY
674///
675/// In modern libxml2 (2.12+), `xmlInitParser` is reference-counted.
676/// The first call initializes all subsystems; subsequent calls
677/// increment a counter. `xmlCleanupParser` decrements the counter
678/// and only performs cleanup when it reaches zero.
679///
680/// # SAFETY
681///
682/// Not fully thread-safe during the first call; callers should
683/// call `xmlInitParser` before creating threads.
684pub unsafe fn init_parser() {
685    let prev = INIT_REF_COUNT.fetch_add(1, Ordering::AcqRel);
686    if prev == 0 {
687        // First initialization — initialize all subsystems.
688        // 1. Initialize memory subsystem.
689        allocator::xmlInitMemory();
690
691        // 2. Mark the library as initialized.
692        versioning::set_initialized();
693
694        // 3. Initialize encoding handlers.
695        crate::xml::encoding::init_encodings();
696
697        // 4. Initialize thread support.
698        init_threads();
699    }
700}
701
702/// Clean up the parser library.
703///
704/// Should be called when the library is no longer needed.
705/// Only performs actual cleanup when the reference count reaches zero.
706///
707/// # SAFETY
708///
709/// Must not be called while other libxml2 functions are executing
710/// in any thread.
711pub unsafe fn cleanup_parser() {
712    let prev = INIT_REF_COUNT.fetch_sub(1, Ordering::AcqRel);
713    if prev <= 1 {
714        // Last cleanup — clean up all subsystems.
715        // 1. Clean up catalog.
716        crate::xml::catalog::cleanup();
717
718        // 2. Clean up encoding handlers.
719        crate::xml::encoding::cleanup_encodings();
720
721        // 3. Clean up memory.
722        allocator::xmlCleanupMemory();
723
724        // 3. Reset initialization state.
725        // Note: we do NOT reset the initialized flag in case
726        // some code checks it after cleanup. This matches
727        // upstream behavior where xmlCleanupParser is best-effort.
728    }
729}
730
731/// Initialize threading support.
732///
733/// # UPSTREAM-PARITY
734///
735/// In modern libxml2, threading is initialized automatically
736/// by `xmlInitParser`. This function exists for backward
737/// compatibility.
738///
739/// Returns 0 on success.
740pub fn init_threads() -> c_int {
741    if !THREADS_INITIALIZED.swap(true, Ordering::Release) {
742        // First initialization — no-op in Rust since we use
743        // standard thread-safe primitives.
744        // In libxml2 this would set up pthread mutexes.
745    }
746    0
747}
748
749/// Clean up threading support.
750pub fn cleanup_threads() {
751    THREADS_INITIALIZED.store(false, Ordering::Release);
752}
753
754/// Check whether threads have been initialized.
755pub fn threads_initialized() -> bool {
756    THREADS_INITIALIZED.load(Ordering::Acquire)
757}
758
759/// Get the current initialization reference count.
760pub fn init_ref_count() -> c_int {
761    INIT_REF_COUNT.load(Ordering::Relaxed)
762}
763
764// ═══════════════════════════════════════════════════════════════════════════════
765// Tests
766// ═══════════════════════════════════════════════════════════════════════════════
767
768#[cfg(test)]
769mod tests {
770    use super::*;
771    use crate::abi::types::xmlErrorLevel::XML_ERR_NONE;
772    use crate::abi::types::*;
773
774    #[test]
775    fn test_parser_defaults_initial_values() {
776        assert_eq!(get_validity_checking_default(), 0);
777        assert_eq!(get_do_warnings_default(), 1);
778        // UPSTREAM-PARITY (globals.c 2.15): xmlIndentTreeOutputThrDef = 1.
779        assert_eq!(get_indent_tree_output(), 1);
780        assert_eq!(get_keep_blanks_default(), 1);
781        assert_eq!(get_load_ext_dtd_default(), 0);
782        assert_eq!(get_pedantic_parser_default(), 0);
783        assert_eq!(get_substitute_entities_default(), 0);
784        assert_eq!(get_save_no_empty_tags(), 0);
785        assert_eq!(get_get_warnings_default(), 1);
786    }
787
788    #[test]
789    fn test_parser_defaults_set_and_get() {
790        set_validity_checking_default(1);
791        assert_eq!(get_validity_checking_default(), 1);
792        set_validity_checking_default(0);
793        assert_eq!(get_validity_checking_default(), 0);
794
795        set_keep_blanks_default(0);
796        assert_eq!(get_keep_blanks_default(), 0);
797        set_keep_blanks_default(1);
798        assert_eq!(get_keep_blanks_default(), 1);
799
800        set_substitute_entities_default(1);
801        assert_eq!(get_substitute_entities_default(), 1);
802        set_substitute_entities_default(0);
803        assert_eq!(get_substitute_entities_default(), 0);
804    }
805
806    /// Increment and decrement the init reference count.
807    ///
808    /// # Safety
809    ///
810    /// - `init_parser`/`cleanup_parser` mutate the global reference count
811    ///   and may run subsystem init/cleanup; the test balances the two
812    ///   calls so the library is left in a clean state, and the reference
813    ///   count is only read between calls.
814    #[test]
815    fn test_init_cleanup_ref_count() {
816        // Reset for test
817        unsafe {
818            init_parser();
819            assert_eq!(init_ref_count(), 1);
820
821            init_parser();
822            assert_eq!(init_ref_count(), 2);
823
824            cleanup_parser();
825            assert!(init_ref_count() == 1 || init_ref_count() == 0);
826
827            // Final cleanup
828            cleanup_parser();
829        }
830    }
831
832    #[test]
833    fn test_error_callbacks_default_handlers() {
834        // UPSTREAM-PARITY (error.c): xmlGenericError defaults to the built-in
835        // stderr printer (never NULL); xmlStructuredError defaults to NULL.
836        // Serialized against the other handler-mutating tests (11.1-X): the
837        // slots are shared global state.
838        let _guard = ERROR_HANDLER_TEST_LOCK.lock();
839        #[cfg(target_arch = "x86_64")]
840        assert!(get_generic_error_func().is_some());
841        assert!(get_structured_error_func().is_none());
842    }
843
844    /// Install and reset the generic error handler slot pair.
845    ///
846    /// # Safety
847    ///
848    /// - `dummy_handler` is a valid no-op callback pointer; `dummy_ctx`
849    ///   points to a stack `i32` alive for the test; `set_generic_error_func`
850    ///   writes the exported slot pair under the handler lock, and NULL
851    ///   resets to the built-in default printer.
852    #[test]
853    fn test_error_callbacks_set_and_get() {
854        let _guard = ERROR_HANDLER_TEST_LOCK.lock();
855        unsafe {
856            /// A no-op generic error callback used as a handler pointer.
857            ///
858            /// # Safety
859            ///
860            /// - The function is never invoked by this test; when installed as a
861            ///   handler it must be a valid function pointer, and the parameters
862            ///   would need to be valid C pointers if it were called.
863            unsafe extern "C" fn dummy_handler(_ctx: *mut c_void, _msg: *const core::ffi::c_char) {}
864            let dummy_func: xmlGenericErrorFunc = dummy_handler;
865            let dummy_ctx: *mut c_void = &mut 0 as *mut i32 as *mut c_void;
866
867            set_generic_error_func(dummy_ctx, Some(dummy_func));
868            assert!(get_generic_error_func().is_some());
869            assert_eq!(get_generic_error_ctx(), dummy_ctx);
870
871            // UPSTREAM-PARITY (xmlSetGenericErrorFunc): NULL resets to the
872            // built-in default printer, it does not unset the handler.
873            set_generic_error_func(ptr::null_mut(), None);
874            #[cfg(target_arch = "x86_64")]
875            assert!(get_generic_error_func().is_some());
876            assert_eq!(get_generic_error_ctx(), ptr::null_mut());
877        }
878    }
879
880    /// Store and reset a thread-local last error.
881    ///
882    /// # Safety
883    ///
884    /// - `err` has all NULL string fields, so `set_last_error` stores it
885    ///   without owning heap strings and `reset_last_error` frees nothing;
886    ///   `get_last_error` returns a pointer valid while the thread-local
887    ///   slot is not mutated.
888    #[test]
889    fn test_last_error_thread_local() {
890        assert!(get_last_error().is_null());
891
892        let err = _xmlError {
893            domain: XML_FROM_PARSER,
894            code: XML_ERR_OK as c_int,
895            message: ptr::null_mut(),
896            level: XML_ERR_NONE as c_int,
897            file: ptr::null_mut(),
898            line: 0,
899            str1: ptr::null_mut(),
900            str2: ptr::null_mut(),
901            str3: ptr::null_mut(),
902            int1: 0,
903            int2: 0,
904            ctxt: ptr::null_mut(),
905            node: ptr::null_mut(),
906        };
907        set_last_error(err);
908        assert!(!get_last_error().is_null());
909        unsafe {
910            assert_eq!((*get_last_error()).domain, XML_FROM_PARSER);
911        }
912
913        reset_last_error();
914        assert!(get_last_error().is_null());
915    }
916
917    #[test]
918    fn test_catalog_defaults() {
919        // Save original value (may have been set by init_parser in other tests)
920        let orig = get_catalog_defaults();
921        set_catalog_defaults(1);
922        assert_eq!(get_catalog_defaults(), 1);
923        set_catalog_defaults(0);
924        assert_eq!(get_catalog_defaults(), 0);
925        // Restore
926        set_catalog_defaults(orig);
927    }
928}