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// R-000177 cross-DSO loader-slot bridge (Phase 14.26, ZTS php gate)
376// ═══════════════════════════════════════════════════════════════════════════════
377
378/// The `xmlParserInputBufferCreateFilenameFunc` loader signature.
379pub(crate) type ParserInputCreateFilenameFunc =
380    unsafe extern "C" fn(*const c_char, c_int) -> *mut _xmlParserInputBuffer;
381
382/// The `xmlOutputBufferCreateFilenameFunc` loader signature.
383pub(crate) type OutputCreateFilenameFunc = unsafe extern "C" fn(
384    *const c_char,
385    crate::abi::structs::xmlCharEncodingHandlerPtr,
386    c_int,
387) -> *mut _xmlOutputBuffer;
388
389/// Upstream `__xmlParserInputBufferCreateFilenameValue` accessor signature:
390/// returns a pointer to the CURRENT thread's value cell in the exporting DSO.
391pub(crate) type ParserInputCreateFilenameValueAccessor =
392    unsafe extern "C" fn() -> *mut Option<ParserInputCreateFilenameFunc>;
393
394/// Upstream `__xmlOutputBufferCreateFilenameValue` accessor signature.
395pub(crate) type OutputCreateFilenameValueAccessor =
396    unsafe extern "C" fn() -> *mut Option<OutputCreateFilenameFunc>;
397
398/// Resolve the process-visible `__xmlParserInputBufferCreateFilenameValue`
399/// accessor (the CORE DSO's export; the whole-archive facades hide it) via
400/// the dynamic symbol scope. The accessor is cached once per process — the
401/// symbol address is stable for the process lifetime.
402#[cfg(target_os = "linux")]
403fn foreign_parser_input_create_filename_value_accessor(
404) -> Option<ParserInputCreateFilenameValueAccessor> {
405    use std::sync::OnceLock;
406    static ACCESSOR: OnceLock<Option<ParserInputCreateFilenameValueAccessor>> = OnceLock::new();
407    *ACCESSOR.get_or_init(|| {
408        // SAFETY: dlsym(RTLD_DEFAULT) returns the address of the exported
409        // accessor or NULL; a non-NULL symbol is transmuted to the matching
410        // fn-pointer type (both pointer-sized).
411        unsafe {
412            let sym = libc::dlsym(
413                libc::RTLD_DEFAULT,
414                c"__xmlParserInputBufferCreateFilenameValue".as_ptr(),
415            );
416            if sym.is_null() {
417                None
418            } else {
419                Some(std::mem::transmute::<
420                    *mut c_void,
421                    ParserInputCreateFilenameValueAccessor,
422                >(sym))
423            }
424        }
425    })
426}
427
428#[cfg(not(target_os = "linux"))]
429fn foreign_parser_input_create_filename_value_accessor(
430) -> Option<ParserInputCreateFilenameValueAccessor> {
431    None
432}
433
434/// Read the current thread's `xmlParserInputBufferCreateFilenameValue`
435/// ACROSS the three-DSO facade boundary (R-000177 partition bridge).
436///
437/// The whole-archive facades (`libxslt.so.1`/`libexslt.so.0`) carry private
438/// copies of the crate — and of the per-DSO `thread_local!` cells — so a
439/// create-filename loader that a consumer (php ext/libxml MINIT) installs
440/// through the CORE DSO's exported `xmlParserInputBufferCreateFilenameDefault`
441/// lives in the core's per-thread cell and is invisible to the facade's
442/// private copy. Upstream ships ONE core DSO (libxslt NEEDs libxml2), so
443/// every internal open observes the registration; this helper restores that
444/// property for the input-loader slot by consulting the process-visible
445/// exported value accessor only when the LOCAL cell is empty.
446///
447/// Per-thread semantics are preserved in every link shape: in a single-DSO
448/// link the accessor aliases the very same cell (dlsym finds this DSO's own
449/// export), so the HOSTILE-THREADS dimension-6 invariant (a handler
450/// installed on one thread is not observable from another) is unchanged —
451/// the foreign read is always same-thread, cross-DSO.
452pub(crate) fn get_parser_input_buffer_create_filename_value_cross_dso(
453) -> Option<ParserInputCreateFilenameFunc> {
454    let local = tls_get(&tls::PARSER_INPUT_CREATE_FILENAME);
455    if local.is_some() {
456        return local;
457    }
458    match foreign_parser_input_create_filename_value_accessor() {
459        Some(accessor) => {
460            // SAFETY: the accessor returns a pointer to the current thread's
461            // valid, initialized TLS cell in the exporting DSO (upstream's
462            // deprecated accessor contract); the read is one dereference.
463            unsafe { *accessor() }
464        }
465        None => None,
466    }
467}
468
469/// Output-side twin of
470/// [`get_parser_input_buffer_create_filename_value_cross_dso`]: php MINIT
471/// also registers `php_libxml_output_buffer_create_filename` through the
472/// core's `xmlOutputBufferCreateFilenameDefault` (same R-000177 partition;
473/// needed by facade-driven writer/save paths under ZTS).
474pub(crate) fn get_output_buffer_create_filename_value_cross_dso() -> Option<OutputCreateFilenameFunc>
475{
476    let local = tls_get(&tls::OUTPUT_CREATE_FILENAME);
477    if local.is_some() {
478        return local;
479    }
480    match foreign_output_create_filename_value_accessor() {
481        Some(accessor) => {
482            // SAFETY: as the input-side twin — one dereference of the
483            // current thread's cell in the exporting DSO.
484            unsafe { *accessor() }
485        }
486        None => None,
487    }
488}
489
490#[cfg(target_os = "linux")]
491fn foreign_output_create_filename_value_accessor() -> Option<OutputCreateFilenameValueAccessor> {
492    use std::sync::OnceLock;
493    static ACCESSOR: OnceLock<Option<OutputCreateFilenameValueAccessor>> = OnceLock::new();
494    *ACCESSOR.get_or_init(|| {
495        // SAFETY: dlsym(RTLD_DEFAULT) returns the exported accessor address
496        // or NULL; a non-NULL symbol is transmuted to the fn-pointer type.
497        unsafe {
498            let sym = libc::dlsym(
499                libc::RTLD_DEFAULT,
500                c"__xmlOutputBufferCreateFilenameValue".as_ptr(),
501            );
502            if sym.is_null() {
503                None
504            } else {
505                Some(std::mem::transmute::<
506                    *mut c_void,
507                    OutputCreateFilenameValueAccessor,
508                >(sym))
509            }
510        }
511    })
512}
513
514#[cfg(not(target_os = "linux"))]
515fn foreign_output_create_filename_value_accessor() -> Option<OutputCreateFilenameValueAccessor> {
516    None
517}
518
519// ═══════════════════════════════════════════════════════════════════════════════
520// Public Accessors — Error Callbacks
521// ═══════════════════════════════════════════════════════════════════════════════
522
523/// Set the generic error handler.
524///
525/// # SAFETY
526///
527/// - `handler` must be a valid function pointer or NULL (to reset to default).
528/// - If non-NULL, the handler may be called at any time with `ctx`.
529pub unsafe fn set_generic_error_func(ctx: *mut c_void, handler: Option<xmlGenericErrorFunc>) {
530    // SAFETY: writing the per-thread error-handler slots
531    // xmlGenericErrorContext / xmlGenericError (TLS, upstream 2.15
532    // xmlSetGenericErrorFunc: NULL resets to the built-in default stderr
533    // printer, error.c). The (ctx, func) pair is written atomically under
534    // ERROR_HANDLER_LOCK (same-thread serialization, R-000171).
535    let resolved = match handler {
536        Some(h) => Some(h),
537        None => crate::abi::data_globals::default_generic_error_func(),
538    };
539    let _guard = ERROR_HANDLER_LOCK.lock();
540    tls_set(&tls::GENERIC_ERROR_CTX, ctx);
541    tls_set(&tls::GENERIC_ERROR, resolved);
542}
543
544/// Set the generic error handler context (thread-local; used by the default
545/// stderr printer to lazily default the context per thread, upstream
546/// error.c).
547pub fn set_generic_error_ctx(ctx: *mut c_void) {
548    let _guard = ERROR_HANDLER_LOCK.lock();
549    tls_set(&tls::GENERIC_ERROR_CTX, ctx);
550}
551
552/// Get the generic error handler context (thread-local).
553pub fn get_generic_error_ctx() -> *mut c_void {
554    let _guard = ERROR_HANDLER_LOCK.lock();
555    tls_get(&tls::GENERIC_ERROR_CTX)
556}
557
558/// Get the generic error handler function pointer (thread-local).
559pub fn get_generic_error_func() -> Option<xmlGenericErrorFunc> {
560    let _guard = ERROR_HANDLER_LOCK.lock();
561    tls_get(&tls::GENERIC_ERROR)
562}
563
564/// Read the generic error (func, ctx) pair atomically.
565///
566/// The closure runs after the lock is released, so a handler installed
567/// by the callback cannot deadlock.
568pub fn with_generic_error<R>(f: impl FnOnce(Option<xmlGenericErrorFunc>, *mut c_void) -> R) -> R {
569    let (h, c) = {
570        let _guard = ERROR_HANDLER_LOCK.lock();
571        (
572            tls_get(&tls::GENERIC_ERROR),
573            tls_get(&tls::GENERIC_ERROR_CTX),
574        )
575    };
576    f(h, c)
577}
578
579/// Set the structured error handler (thread-local, upstream 2.15).
580///
581/// # SAFETY
582///
583/// - `handler` must be a valid function pointer or NULL.
584pub unsafe fn set_structured_error_func(ctx: *mut c_void, handler: Option<xmlStructuredErrorFunc>) {
585    // SAFETY: writing the per-thread error-handler slots
586    // xmlStructuredErrorContext / xmlStructuredError (TLS, upstream 2.15
587    // xmlSetStructuredErrorFunc). The (ctx, func) pair is written
588    // atomically under ERROR_HANDLER_LOCK (same-thread serialization,
589    // R-000171).
590    let _guard = ERROR_HANDLER_LOCK.lock();
591    tls_set(&tls::STRUCTURED_ERROR_CTX, ctx);
592    tls_set(&tls::STRUCTURED_ERROR, handler);
593}
594
595/// Get the structured error handler context (thread-local).
596pub fn get_structured_error_ctx() -> *mut c_void {
597    let _guard = ERROR_HANDLER_LOCK.lock();
598    tls_get(&tls::STRUCTURED_ERROR_CTX)
599}
600
601/// Get the structured error handler function pointer (thread-local).
602pub fn get_structured_error_func() -> Option<xmlStructuredErrorFunc> {
603    let _guard = ERROR_HANDLER_LOCK.lock();
604    tls_get(&tls::STRUCTURED_ERROR)
605}
606
607/// Read the structured error (func, ctx) pair atomically.
608///
609/// The closure runs after the lock is released, so a handler installed by
610/// the callback (or an error raised from inside the handler) cannot
611/// deadlock on ERROR_HANDLER_LOCK.
612///
613/// # Safety
614///
615/// - The two reads of the per-thread `xmlStructuredError`/
616///   `xmlStructuredErrorContext` TLS slots are performed under
617///   `ERROR_HANDLER_LOCK`, matching the write side (xmlSetStructuredError),
618///   so the pair is observed atomically; the values are only borrowed for the
619///   duration of the closure and never dereferenced here.
620pub fn with_structured_error<R>(
621    f: impl FnOnce(Option<xmlStructuredErrorFunc>, *mut c_void) -> R,
622) -> R {
623    let (h, c) = {
624        let _guard = ERROR_HANDLER_LOCK.lock();
625        (
626            tls_get(&tls::STRUCTURED_ERROR),
627            tls_get(&tls::STRUCTURED_ERROR_CTX),
628        )
629    };
630    f(h, c)
631}
632
633// ═══════════════════════════════════════════════════════════════════════════════
634// `__xmlXxx()` pointer accessors (upstream threads.c / globals.c)
635// ═══════════════════════════════════════════════════════════════════════════════
636// The deprecated thread-local API exports one `__xmlXxx(void)` accessor per
637// TLS-era global; each returns a pointer to the CURRENT thread's slot so
638// callers can read/write it directly (upstream `#define xmlXxx
639// (*__xmlXxx())`). The candidate's exports (`crate::abi::data_globals`)
640// delegate here; the returned pointers alias the `tls` cells and stay valid
641// for the lifetime of the thread.
642
643pub(crate) fn do_validity_ptr() -> *mut c_int {
644    tls_ptr(&tls::DO_VALIDITY)
645}
646pub(crate) fn get_warnings_ptr() -> *mut c_int {
647    tls_ptr(&tls::GET_WARNINGS)
648}
649pub(crate) fn load_ext_dtd_ptr() -> *mut c_int {
650    tls_ptr(&tls::LOAD_EXT_DTD)
651}
652pub(crate) fn pedantic_ptr() -> *mut c_int {
653    tls_ptr(&tls::PEDANTIC)
654}
655pub(crate) fn line_numbers_ptr() -> *mut c_int {
656    tls_ptr(&tls::LINE_NUMBERS)
657}
658pub(crate) fn keep_blanks_ptr() -> *mut c_int {
659    tls_ptr(&tls::KEEP_BLANKS)
660}
661pub(crate) fn substitute_entities_ptr() -> *mut c_int {
662    tls_ptr(&tls::SUBSTITUTE_ENTITIES)
663}
664pub(crate) fn indent_tree_output_ptr() -> *mut c_int {
665    tls_ptr(&tls::INDENT_TREE_OUTPUT)
666}
667pub(crate) fn tree_indent_string_ptr() -> *mut *const xmlChar {
668    tls_ptr(&tls::TREE_INDENT_STRING)
669}
670pub(crate) fn save_no_empty_tags_ptr() -> *mut c_int {
671    tls_ptr(&tls::SAVE_NO_EMPTY_TAGS)
672}
673pub(crate) fn register_node_ptr() -> *mut Option<unsafe extern "C" fn(*mut _xmlNode)> {
674    tls_ptr(&tls::REGISTER_NODE)
675}
676pub(crate) fn deregister_node_ptr() -> *mut Option<unsafe extern "C" fn(*mut _xmlNode)> {
677    tls_ptr(&tls::DEREGISTER_NODE)
678}
679pub(crate) fn parser_input_create_filename_ptr(
680) -> *mut Option<unsafe extern "C" fn(*const c_char, c_int) -> *mut _xmlParserInputBuffer> {
681    tls_ptr(&tls::PARSER_INPUT_CREATE_FILENAME)
682}
683pub(crate) fn output_create_filename_ptr() -> *mut Option<
684    unsafe extern "C" fn(
685        *const c_char,
686        crate::abi::structs::xmlCharEncodingHandlerPtr,
687        c_int,
688    ) -> *mut _xmlOutputBuffer,
689> {
690    tls_ptr(&tls::OUTPUT_CREATE_FILENAME)
691}
692pub(crate) fn generic_error_ptr() -> *mut Option<xmlGenericErrorFunc> {
693    tls_ptr(&tls::GENERIC_ERROR)
694}
695pub(crate) fn generic_error_ctx_ptr() -> *mut *mut c_void {
696    tls_ptr(&tls::GENERIC_ERROR_CTX)
697}
698pub(crate) fn structured_error_ptr() -> *mut Option<xmlStructuredErrorFunc> {
699    tls_ptr(&tls::STRUCTURED_ERROR)
700}
701pub(crate) fn structured_error_ctx_ptr() -> *mut *mut c_void {
702    tls_ptr(&tls::STRUCTURED_ERROR_CTX)
703}
704
705// ═══════════════════════════════════════════════════════════════════════════════
706// Public Accessors — Thread-Local Error State
707// ═══════════════════════════════════════════════════════════════════════════════
708
709/// Get the last error for this thread.
710///
711/// Returns a mutable pointer to the last error, or NULL if no error occurred.
712/// The returned pointer is valid until the next libxml2 call in this thread.
713pub fn get_last_error() -> *mut _xmlError {
714    LAST_ERROR.with(|last| {
715        let mut last = last.borrow_mut();
716        last.as_mut()
717            .map_or(ptr::null_mut(), |e| e as *mut _xmlError)
718    })
719}
720
721/// Get a reference to the last error (for structured error callback).
722pub fn with_last_error<F, R>(f: F) -> R
723where
724    F: FnOnce(Option<&_xmlError>) -> R,
725{
726    LAST_ERROR.with(|last| f(last.borrow().as_ref()))
727}
728
729/// Set the last error for this thread.
730pub fn set_last_error(err: _xmlError) {
731    // UPSTREAM-PARITY: mirror the error into the exported C global
732    // `xmlLastError` (data-ABI, residual R-000135). Upstream keeps a single
733    // global; the candidate keeps a thread-local truth and a deep-copied
734    // mirror so C consumers see the most recent error with upstream
735    // lifetime semantics (mirror strings are owned by the mirror and freed
736    // on reset, matching xmlResetError).
737    //
738    // SAFETY: sync_xml_last_error only reads `err` and writes the global
739    // mirror with freshly owned copies; the thread-local slot takes
740    // ownership of `err` itself.
741    unsafe { crate::abi::data_globals::sync_xml_last_error(&err) };
742    LAST_ERROR.with(|last| {
743        let mut last = last.borrow_mut();
744        // Free the previous slot's owned strings (upstream xmlResetError).
745        if let Some(prev) = last.as_ref() {
746            free_error_strings(prev);
747        }
748        *last = Some(err);
749    });
750}
751
752/// Free the owned string fields of a stored error (upstream xmlResetError:
753/// message/file/str1/str2/str3 are xmlMalloc'd copies).
754///
755/// # Safety
756///
757/// - `err` must point to a valid `_xmlError` whose `message`, `file`,
758///   `str1`, `str2` and `str3` fields are NULL or pointers allocated with
759///   `xmlFreeImpl`'s allocator; each non-NULL field is freed exactly once
760///   and must not be freed or used again afterwards.
761pub(crate) fn free_error_strings(err: &_xmlError) {
762    use crate::abi::allocator::xmlFreeImpl;
763    unsafe {
764        if !err.message.is_null() {
765            xmlFreeImpl(err.message as *mut core::ffi::c_void);
766        }
767        if !err.file.is_null() {
768            xmlFreeImpl(err.file as *mut core::ffi::c_void);
769        }
770        if !err.str1.is_null() {
771            xmlFreeImpl(err.str1 as *mut core::ffi::c_void);
772        }
773        if !err.str2.is_null() {
774            xmlFreeImpl(err.str2 as *mut core::ffi::c_void);
775        }
776        if !err.str3.is_null() {
777            xmlFreeImpl(err.str3 as *mut core::ffi::c_void);
778        }
779    }
780}
781
782/// Reset the last error for this thread.
783pub fn reset_last_error() {
784    LAST_ERROR.with(|last| {
785        let mut last = last.borrow_mut();
786        if let Some(prev) = last.as_ref() {
787            free_error_strings(prev);
788        }
789        *last = None;
790    });
791    // SAFETY: frees the mirror's owned strings and zeroes the global.
792    unsafe { crate::abi::data_globals::reset_xml_last_error() };
793}
794
795// ═══════════════════════════════════════════════════════════════════════════════
796// Public Accessors — Catalog Defaults
797// ═══════════════════════════════════════════════════════════════════════════════
798
799/// Get the catalog default allow value.
800pub fn get_catalog_defaults() -> c_int {
801    CATALOG_DEFAULTS.load(Ordering::Relaxed)
802}
803
804/// Set the catalog default allow value.
805pub fn set_catalog_defaults(val: c_int) {
806    CATALOG_DEFAULTS.store(val, Ordering::Relaxed);
807}
808
809// ═══════════════════════════════════════════════════════════════════════════════
810// Initialization / Cleanup
811// ═══════════════════════════════════════════════════════════════════════════════
812
813/// Initialize the parser library.
814///
815/// Must be called before any other libxml2 functions.
816/// Safe to call multiple times (reference-counted in modern libxml2).
817///
818/// # UPSTREAM-PARITY
819///
820/// In modern libxml2 (2.12+), `xmlInitParser` is reference-counted.
821/// The first call initializes all subsystems; subsequent calls
822/// increment a counter. `xmlCleanupParser` decrements the counter
823/// and only performs cleanup when it reaches zero.
824///
825/// # SAFETY
826///
827/// Not fully thread-safe during the first call; callers should
828/// call `xmlInitParser` before creating threads.
829pub unsafe fn init_parser() {
830    let prev = INIT_REF_COUNT.fetch_add(1, Ordering::AcqRel);
831    if prev == 0 {
832        // First initialization — initialize all subsystems.
833        // 1. Initialize memory subsystem.
834        allocator::xmlInitMemory();
835
836        // 2. Mark the library as initialized.
837        versioning::set_initialized();
838
839        // 3. Initialize encoding handlers.
840        crate::xml::encoding::init_encodings();
841
842        // 4. Initialize thread support.
843        init_threads();
844    }
845}
846
847/// Clean up the parser library.
848///
849/// Should be called when the library is no longer needed.
850/// Only performs actual cleanup when the reference count reaches zero.
851///
852/// # SAFETY
853///
854/// Must not be called while other libxml2 functions are executing
855/// in any thread.
856pub unsafe fn cleanup_parser() {
857    let prev = INIT_REF_COUNT.fetch_sub(1, Ordering::AcqRel);
858    if prev <= 1 {
859        // Last cleanup — clean up all subsystems.
860        // 1. Clean up catalog.
861        crate::xml::catalog::cleanup();
862
863        // 2. Clean up encoding handlers.
864        crate::xml::encoding::cleanup_encodings();
865
866        // 3. Clean up memory.
867        allocator::xmlCleanupMemory();
868
869        // 3. Reset initialization state.
870        // Note: we do NOT reset the initialized flag in case
871        // some code checks it after cleanup. This matches
872        // upstream behavior where xmlCleanupParser is best-effort.
873    }
874}
875
876/// Initialize threading support.
877///
878/// # UPSTREAM-PARITY
879///
880/// In modern libxml2, threading is initialized automatically
881/// by `xmlInitParser`. This function exists for backward
882/// compatibility.
883///
884/// Returns 0 on success.
885pub fn init_threads() -> c_int {
886    if !THREADS_INITIALIZED.swap(true, Ordering::Release) {
887        // First initialization — no-op in Rust since we use
888        // standard thread-safe primitives.
889        // In libxml2 this would set up pthread mutexes.
890    }
891    0
892}
893
894/// Clean up threading support.
895pub fn cleanup_threads() {
896    THREADS_INITIALIZED.store(false, Ordering::Release);
897}
898
899/// Check whether threads have been initialized.
900pub fn threads_initialized() -> bool {
901    THREADS_INITIALIZED.load(Ordering::Acquire)
902}
903
904/// Get the current initialization reference count.
905pub fn init_ref_count() -> c_int {
906    INIT_REF_COUNT.load(Ordering::Relaxed)
907}
908
909// ═══════════════════════════════════════════════════════════════════════════════
910// Tests
911// ═══════════════════════════════════════════════════════════════════════════════
912
913#[cfg(test)]
914mod tests {
915    use super::*;
916    use crate::abi::types::xmlErrorLevel::XML_ERR_NONE;
917    use crate::abi::types::*;
918
919    #[test]
920    fn test_parser_defaults_initial_values() {
921        assert_eq!(get_validity_checking_default(), 0);
922        assert_eq!(get_do_warnings_default(), 1);
923        // UPSTREAM-PARITY (globals.c 2.15): xmlIndentTreeOutputThrDef = 1.
924        assert_eq!(get_indent_tree_output(), 1);
925        assert_eq!(get_keep_blanks_default(), 1);
926        assert_eq!(get_load_ext_dtd_default(), 0);
927        assert_eq!(get_pedantic_parser_default(), 0);
928        assert_eq!(get_substitute_entities_default(), 0);
929        assert_eq!(get_save_no_empty_tags(), 0);
930        assert_eq!(get_get_warnings_default(), 1);
931    }
932
933    #[test]
934    fn test_parser_defaults_set_and_get() {
935        set_validity_checking_default(1);
936        assert_eq!(get_validity_checking_default(), 1);
937        set_validity_checking_default(0);
938        assert_eq!(get_validity_checking_default(), 0);
939
940        set_keep_blanks_default(0);
941        assert_eq!(get_keep_blanks_default(), 0);
942        set_keep_blanks_default(1);
943        assert_eq!(get_keep_blanks_default(), 1);
944
945        set_substitute_entities_default(1);
946        assert_eq!(get_substitute_entities_default(), 1);
947        set_substitute_entities_default(0);
948        assert_eq!(get_substitute_entities_default(), 0);
949    }
950
951    /// Increment and decrement the init reference count.
952    ///
953    /// # Safety
954    ///
955    /// - `init_parser`/`cleanup_parser` mutate the global reference count
956    ///   and may run subsystem init/cleanup; the test balances the two
957    ///   calls so the library is left in a clean state, and the reference
958    ///   count is only read between calls.
959    #[test]
960    fn test_init_cleanup_ref_count() {
961        // Reset for test
962        unsafe {
963            init_parser();
964            assert_eq!(init_ref_count(), 1);
965
966            init_parser();
967            assert_eq!(init_ref_count(), 2);
968
969            cleanup_parser();
970            assert!(init_ref_count() == 1 || init_ref_count() == 0);
971
972            // Final cleanup
973            cleanup_parser();
974        }
975    }
976
977    #[test]
978    fn test_error_callbacks_default_handlers() {
979        // UPSTREAM-PARITY (error.c): xmlGenericError defaults to the built-in
980        // stderr printer (never NULL); xmlStructuredError defaults to NULL.
981        // Serialized against the other handler-mutating tests (11.1-X): the
982        // slots are shared global state.
983        let _guard = ERROR_HANDLER_TEST_LOCK.lock();
984        #[cfg(target_arch = "x86_64")]
985        assert!(get_generic_error_func().is_some());
986        assert!(get_structured_error_func().is_none());
987    }
988
989    /// Install and reset the generic error handler slot pair.
990    ///
991    /// # Safety
992    ///
993    /// - `dummy_handler` is a valid no-op callback pointer; `dummy_ctx`
994    ///   points to a stack `i32` alive for the test; `set_generic_error_func`
995    ///   writes the exported slot pair under the handler lock, and NULL
996    ///   resets to the built-in default printer.
997    #[test]
998    fn test_error_callbacks_set_and_get() {
999        let _guard = ERROR_HANDLER_TEST_LOCK.lock();
1000        unsafe {
1001            /// A no-op generic error callback used as a handler pointer.
1002            ///
1003            /// # Safety
1004            ///
1005            /// - The function is never invoked by this test; when installed as a
1006            ///   handler it must be a valid function pointer, and the parameters
1007            ///   would need to be valid C pointers if it were called.
1008            unsafe extern "C" fn dummy_handler(_ctx: *mut c_void, _msg: *const core::ffi::c_char) {}
1009            let dummy_func: xmlGenericErrorFunc = dummy_handler;
1010            let dummy_ctx: *mut c_void = &mut 0 as *mut i32 as *mut c_void;
1011
1012            set_generic_error_func(dummy_ctx, Some(dummy_func));
1013            assert!(get_generic_error_func().is_some());
1014            assert_eq!(get_generic_error_ctx(), dummy_ctx);
1015
1016            // UPSTREAM-PARITY (xmlSetGenericErrorFunc): NULL resets to the
1017            // built-in default printer, it does not unset the handler.
1018            set_generic_error_func(ptr::null_mut(), None);
1019            #[cfg(target_arch = "x86_64")]
1020            assert!(get_generic_error_func().is_some());
1021            assert_eq!(get_generic_error_ctx(), ptr::null_mut());
1022        }
1023    }
1024
1025    /// Store and reset a thread-local last error.
1026    ///
1027    /// # Safety
1028    ///
1029    /// - `err` has all NULL string fields, so `set_last_error` stores it
1030    ///   without owning heap strings and `reset_last_error` frees nothing;
1031    ///   `get_last_error` returns a pointer valid while the thread-local
1032    ///   slot is not mutated.
1033    #[test]
1034    fn test_last_error_thread_local() {
1035        assert!(get_last_error().is_null());
1036
1037        let err = _xmlError {
1038            domain: XML_FROM_PARSER,
1039            code: XML_ERR_OK as c_int,
1040            message: ptr::null_mut(),
1041            level: XML_ERR_NONE as c_int,
1042            file: ptr::null_mut(),
1043            line: 0,
1044            str1: ptr::null_mut(),
1045            str2: ptr::null_mut(),
1046            str3: ptr::null_mut(),
1047            int1: 0,
1048            int2: 0,
1049            ctxt: ptr::null_mut(),
1050            node: ptr::null_mut(),
1051        };
1052        set_last_error(err);
1053        assert!(!get_last_error().is_null());
1054        unsafe {
1055            assert_eq!((*get_last_error()).domain, XML_FROM_PARSER);
1056        }
1057
1058        reset_last_error();
1059        assert!(get_last_error().is_null());
1060    }
1061
1062    #[test]
1063    fn test_catalog_defaults() {
1064        // Save original value (may have been set by init_parser in other tests)
1065        let orig = get_catalog_defaults();
1066        set_catalog_defaults(1);
1067        assert_eq!(get_catalog_defaults(), 1);
1068        set_catalog_defaults(0);
1069        assert_eq!(get_catalog_defaults(), 0);
1070        // Restore
1071        set_catalog_defaults(orig);
1072    }
1073}