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// R-000177 cross-DSO cell readers (three-DSO facade boundary)
203// ═══════════════════════════════════════════════════════════════════════════════
204// The whole-archive libxslt/libexslt facades carry private copies of the
205// crate — and of the per-DSO thread_local! cells — so a consumer that
206// installs hooks/changes a parser default through the CORE DSO's exported
207// setter/accessor (which is where the symbol binds; the facades hide the
208// xml* surface) writes the CORE's per-thread cell, invisible to the facade's
209// private reads. Upstream ships ONE core DSO, so every read observes the
210// registration. These readers resolve the process-visible exported __xml*
211// VALUE accessor (the core DSO's) via the dynamic symbol scope and return a
212// pointer to the CURRENT thread's cell in that DSO; the getters below prefer
213// it and fall back to the local cell only when the symbol is not exported
214// (single-DSO links resolve their own export — the very same cell — so the
215// per-thread semantics and every single-DSO court are unchanged).
216
217#[cfg(target_os = "linux")]
218macro_rules! foreign_cell {
219    ($name:ident, $cname:expr, $t:ty) => {
220        fn $name() -> Option<*mut $t> {
221            use std::sync::OnceLock;
222            type Acc = unsafe extern "C" fn() -> *mut $t;
223            static ACC: OnceLock<Option<Acc>> = OnceLock::new();
224            let acc = *ACC.get_or_init(|| {
225                // SAFETY: dlsym(RTLD_DEFAULT) returns the exported accessor
226                // address or NULL; every __xml* accessor shares the
227                // `() -> *mut T` shape (pointer-sized), so the transmute is
228                // sound. The resolved symbol address is process-stable.
229                unsafe {
230                    let sym = libc::dlsym(libc::RTLD_DEFAULT, ($cname).as_ptr());
231                    if sym.is_null() {
232                        None
233                    } else {
234                        Some(std::mem::transmute::<*mut c_void, Acc>(sym))
235                    }
236                }
237            });
238            acc.map(|a| unsafe { a() })
239        }
240    };
241}
242
243#[cfg(not(target_os = "linux"))]
244macro_rules! foreign_cell {
245    ($name:ident, $cname:expr, $t:ty) => {
246        fn $name() -> Option<*mut $t> {
247            None
248        }
249    };
250}
251
252foreign_cell!(
253    foreign_validity,
254    c"__xmlDoValidityCheckingDefaultValue",
255    c_int
256);
257foreign_cell!(foreign_warnings, c"__xmlGetWarningsDefaultValue", c_int);
258foreign_cell!(foreign_indent_tree, c"__xmlIndentTreeOutput", c_int);
259foreign_cell!(foreign_keep_blanks, c"__xmlKeepBlanksDefaultValue", c_int);
260foreign_cell!(foreign_load_ext_dtd, c"__xmlLoadExtDtdDefaultValue", c_int);
261foreign_cell!(foreign_pedantic, c"__xmlPedanticParserDefaultValue", c_int);
262foreign_cell!(
263    foreign_substitute_entities,
264    c"__xmlSubstituteEntitiesDefaultValue",
265    c_int
266);
267foreign_cell!(foreign_save_no_empty, c"__xmlSaveNoEmptyTags", c_int);
268foreign_cell!(foreign_line_numbers, c"__xmlLineNumbersDefaultValue", c_int);
269foreign_cell!(
270    foreign_tree_indent_string,
271    c"__xmlTreeIndentString",
272    *const xmlChar
273);
274foreign_cell!(
275    foreign_register_node,
276    c"__xmlRegisterNodeDefaultValue",
277    Option<unsafe extern "C" fn(*mut _xmlNode)>
278);
279foreign_cell!(
280    foreign_deregister_node,
281    c"__xmlDeregisterNodeDefaultValue",
282    Option<unsafe extern "C" fn(*mut _xmlNode)>
283);
284foreign_cell!(
285    foreign_generic_error,
286    c"__xmlGenericError",
287    Option<xmlGenericErrorFunc>
288);
289foreign_cell!(
290    foreign_generic_error_ctx,
291    c"__xmlGenericErrorContext",
292    *mut c_void
293);
294foreign_cell!(
295    foreign_structured_error,
296    c"__xmlStructuredError",
297    Option<xmlStructuredErrorFunc>
298);
299foreign_cell!(
300    foreign_structured_error_ctx,
301    c"__xmlStructuredErrorContext",
302    *mut c_void
303);
304
305/// Read a cell preferring the process-visible (core DSO) value when its
306/// accessor resolves, else the local cell.
307#[inline]
308fn read_cross<T: Copy>(foreign: Option<*mut T>, local: T) -> T {
309    match foreign {
310        Some(cell) => {
311            // SAFETY: the accessor returned a pointer to the current
312            // thread's valid, initialized cell in the exporting DSO
313            // (upstream's deprecated accessor contract); one dereference.
314            unsafe { *cell }
315        }
316        None => local,
317    }
318}
319
320// ═══════════════════════════════════════════════════════════════════════════════
321// Public Accessors — Parser Defaults
322// ═══════════════════════════════════════════════════════════════════════════════
323
324/// Get the default validity checking value (thread-local, upstream 2.15).
325pub fn get_validity_checking_default() -> c_int {
326    read_cross(foreign_validity(), tls_get(&tls::DO_VALIDITY))
327}
328
329/// Set the default validity checking value (thread-local, upstream 2.15).
330pub fn set_validity_checking_default(val: c_int) {
331    tls_set(&tls::DO_VALIDITY, val);
332}
333
334/// Get the default warnings value (thread-local, upstream 2.15).
335pub fn get_do_warnings_default() -> c_int {
336    read_cross(foreign_warnings(), tls_get(&tls::GET_WARNINGS))
337}
338
339/// Set the default warnings value (thread-local, upstream 2.15).
340pub fn set_do_warnings_default(val: c_int) {
341    tls_set(&tls::GET_WARNINGS, val);
342}
343
344/// Get the indent tree output default (thread-local, upstream 2.15).
345pub fn get_indent_tree_output() -> c_int {
346    read_cross(foreign_indent_tree(), tls_get(&tls::INDENT_TREE_OUTPUT))
347}
348
349/// Set the indent tree output default (thread-local, upstream 2.15).
350pub fn set_indent_tree_output(val: c_int) {
351    tls_set(&tls::INDENT_TREE_OUTPUT, val);
352}
353
354/// Get the keep blanks default value (thread-local, upstream 2.15).
355pub fn get_keep_blanks_default() -> c_int {
356    read_cross(foreign_keep_blanks(), tls_get(&tls::KEEP_BLANKS))
357}
358
359/// Set the keep blanks default value (thread-local, upstream 2.15).
360pub fn set_keep_blanks_default(val: c_int) {
361    tls_set(&tls::KEEP_BLANKS, val);
362}
363
364/// Get the load external DTD default value (thread-local, upstream 2.15).
365pub fn get_load_ext_dtd_default() -> c_int {
366    read_cross(foreign_load_ext_dtd(), tls_get(&tls::LOAD_EXT_DTD))
367}
368
369/// Set the load external DTD default value (thread-local, upstream 2.15).
370pub fn set_load_ext_dtd_default(val: c_int) {
371    tls_set(&tls::LOAD_EXT_DTD, val);
372}
373
374/// Get the pedantic parser default (thread-local, upstream 2.15).
375pub fn get_pedantic_parser_default() -> c_int {
376    read_cross(foreign_pedantic(), tls_get(&tls::PEDANTIC))
377}
378
379/// Set the pedantic parser default (thread-local, upstream 2.15).
380pub fn set_pedantic_parser_default(val: c_int) {
381    tls_set(&tls::PEDANTIC, val);
382}
383
384/// Get the substitute entities default (thread-local, upstream 2.15).
385pub fn get_substitute_entities_default() -> c_int {
386    read_cross(
387        foreign_substitute_entities(),
388        tls_get(&tls::SUBSTITUTE_ENTITIES),
389    )
390}
391
392/// Set the substitute entities default (thread-local, upstream 2.15).
393pub fn set_substitute_entities_default(val: c_int) {
394    tls_set(&tls::SUBSTITUTE_ENTITIES, val);
395}
396
397/// Get the save no empty tags default (thread-local, upstream 2.15).
398pub fn get_save_no_empty_tags() -> c_int {
399    read_cross(foreign_save_no_empty(), tls_get(&tls::SAVE_NO_EMPTY_TAGS))
400}
401
402/// Set the save no empty tags default (thread-local, upstream 2.15).
403pub fn set_save_no_empty_tags(val: c_int) {
404    tls_set(&tls::SAVE_NO_EMPTY_TAGS, val);
405}
406
407/// Get the get warnings default (thread-local, upstream 2.15).
408pub fn get_get_warnings_default() -> c_int {
409    read_cross(foreign_warnings(), tls_get(&tls::GET_WARNINGS))
410}
411
412/// Set the get warnings default (thread-local, upstream 2.15).
413pub fn set_get_warnings_default(val: c_int) {
414    tls_set(&tls::GET_WARNINGS, val);
415}
416
417/// Get `xmlLineNumbersDefaultValue` (thread-local, upstream 2.15).
418pub fn get_line_numbers_default() -> c_int {
419    read_cross(foreign_line_numbers(), tls_get(&tls::LINE_NUMBERS))
420}
421
422/// Set `xmlLineNumbersDefaultValue` (thread-local, upstream 2.15).
423pub fn set_line_numbers_default(val: c_int) {
424    tls_set(&tls::LINE_NUMBERS, val);
425}
426
427/// Get `xmlTreeIndentString` (thread-local, upstream 2.15).
428pub fn get_tree_indent_string() -> *const xmlChar {
429    read_cross(
430        foreign_tree_indent_string(),
431        tls_get(&tls::TREE_INDENT_STRING),
432    )
433}
434
435/// Set `xmlTreeIndentString` (thread-local, upstream 2.15).
436pub fn set_tree_indent_string(val: *const xmlChar) {
437    tls_set(&tls::TREE_INDENT_STRING, val);
438}
439
440/// Get `xmlRegisterNodeDefaultValue` (thread-local, upstream 2.15).
441pub fn get_register_node_default() -> Option<unsafe extern "C" fn(*mut _xmlNode)> {
442    read_cross(foreign_register_node(), tls_get(&tls::REGISTER_NODE))
443}
444
445/// Set `xmlRegisterNodeDefaultValue` (thread-local, upstream 2.15).
446pub fn set_register_node_default(val: Option<unsafe extern "C" fn(*mut _xmlNode)>) {
447    tls_set(&tls::REGISTER_NODE, val);
448}
449
450/// Get `xmlDeregisterNodeDefaultValue` (thread-local, upstream 2.15).
451pub fn get_deregister_node_default() -> Option<unsafe extern "C" fn(*mut _xmlNode)> {
452    read_cross(foreign_deregister_node(), tls_get(&tls::DEREGISTER_NODE))
453}
454
455/// Set `xmlDeregisterNodeDefaultValue` (thread-local, upstream 2.15).
456pub fn set_deregister_node_default(val: Option<unsafe extern "C" fn(*mut _xmlNode)>) {
457    tls_set(&tls::DEREGISTER_NODE, val);
458}
459
460/// Get `xmlParserInputBufferCreateFilenameValue` (thread-local, upstream
461/// 2.15).
462pub fn get_parser_input_buffer_create_filename_value(
463) -> Option<unsafe extern "C" fn(*const c_char, c_int) -> *mut _xmlParserInputBuffer> {
464    tls_get(&tls::PARSER_INPUT_CREATE_FILENAME)
465}
466
467/// Set `xmlParserInputBufferCreateFilenameValue` (thread-local, upstream
468/// 2.15).
469pub fn set_parser_input_buffer_create_filename_value(
470    val: Option<unsafe extern "C" fn(*const c_char, c_int) -> *mut _xmlParserInputBuffer>,
471) {
472    tls_set(&tls::PARSER_INPUT_CREATE_FILENAME, val);
473}
474
475/// Get `xmlOutputBufferCreateFilenameValue` (thread-local, upstream 2.15).
476pub fn get_output_buffer_create_filename_value() -> Option<
477    unsafe extern "C" fn(
478        *const c_char,
479        crate::abi::structs::xmlCharEncodingHandlerPtr,
480        c_int,
481    ) -> *mut _xmlOutputBuffer,
482> {
483    tls_get(&tls::OUTPUT_CREATE_FILENAME)
484}
485
486/// Set `xmlOutputBufferCreateFilenameValue` (thread-local, upstream 2.15).
487pub fn set_output_buffer_create_filename_value(
488    val: Option<
489        unsafe extern "C" fn(
490            *const c_char,
491            crate::abi::structs::xmlCharEncodingHandlerPtr,
492            c_int,
493        ) -> *mut _xmlOutputBuffer,
494    >,
495) {
496    tls_set(&tls::OUTPUT_CREATE_FILENAME, val);
497}
498
499// ═══════════════════════════════════════════════════════════════════════════════
500// R-000177 cross-DSO loader-slot bridge (Phase 14.26, ZTS php gate)
501// ═══════════════════════════════════════════════════════════════════════════════
502
503/// The `xmlParserInputBufferCreateFilenameFunc` loader signature.
504pub(crate) type ParserInputCreateFilenameFunc =
505    unsafe extern "C" fn(*const c_char, c_int) -> *mut _xmlParserInputBuffer;
506
507/// The `xmlOutputBufferCreateFilenameFunc` loader signature.
508pub(crate) type OutputCreateFilenameFunc = unsafe extern "C" fn(
509    *const c_char,
510    crate::abi::structs::xmlCharEncodingHandlerPtr,
511    c_int,
512) -> *mut _xmlOutputBuffer;
513
514/// Upstream `__xmlParserInputBufferCreateFilenameValue` accessor signature:
515/// returns a pointer to the CURRENT thread's value cell in the exporting DSO.
516pub(crate) type ParserInputCreateFilenameValueAccessor =
517    unsafe extern "C" fn() -> *mut Option<ParserInputCreateFilenameFunc>;
518
519/// Upstream `__xmlOutputBufferCreateFilenameValue` accessor signature.
520pub(crate) type OutputCreateFilenameValueAccessor =
521    unsafe extern "C" fn() -> *mut Option<OutputCreateFilenameFunc>;
522
523/// Resolve the process-visible `__xmlParserInputBufferCreateFilenameValue`
524/// accessor (the CORE DSO's export; the whole-archive facades hide it) via
525/// the dynamic symbol scope. The accessor is cached once per process — the
526/// symbol address is stable for the process lifetime.
527#[cfg(target_os = "linux")]
528fn foreign_parser_input_create_filename_value_accessor(
529) -> Option<ParserInputCreateFilenameValueAccessor> {
530    use std::sync::OnceLock;
531    static ACCESSOR: OnceLock<Option<ParserInputCreateFilenameValueAccessor>> = OnceLock::new();
532    *ACCESSOR.get_or_init(|| {
533        // SAFETY: dlsym(RTLD_DEFAULT) returns the address of the exported
534        // accessor or NULL; a non-NULL symbol is transmuted to the matching
535        // fn-pointer type (both pointer-sized).
536        unsafe {
537            let sym = libc::dlsym(
538                libc::RTLD_DEFAULT,
539                c"__xmlParserInputBufferCreateFilenameValue".as_ptr(),
540            );
541            if sym.is_null() {
542                None
543            } else {
544                Some(std::mem::transmute::<
545                    *mut c_void,
546                    ParserInputCreateFilenameValueAccessor,
547                >(sym))
548            }
549        }
550    })
551}
552
553#[cfg(not(target_os = "linux"))]
554fn foreign_parser_input_create_filename_value_accessor(
555) -> Option<ParserInputCreateFilenameValueAccessor> {
556    None
557}
558
559/// Read the current thread's `xmlParserInputBufferCreateFilenameValue`
560/// ACROSS the three-DSO facade boundary (R-000177 partition bridge).
561///
562/// The whole-archive facades (`libxslt.so.1`/`libexslt.so.0`) carry private
563/// copies of the crate — and of the per-DSO `thread_local!` cells — so a
564/// create-filename loader that a consumer (php ext/libxml MINIT) installs
565/// through the CORE DSO's exported `xmlParserInputBufferCreateFilenameDefault`
566/// lives in the core's per-thread cell and is invisible to the facade's
567/// private copy. Upstream ships ONE core DSO (libxslt NEEDs libxml2), so
568/// every internal open observes the registration; this helper restores that
569/// property for the input-loader slot by consulting the process-visible
570/// exported value accessor only when the LOCAL cell is empty.
571///
572/// Per-thread semantics are preserved in every link shape: in a single-DSO
573/// link the accessor aliases the very same cell (dlsym finds this DSO's own
574/// export), so the HOSTILE-THREADS dimension-6 invariant (a handler
575/// installed on one thread is not observable from another) is unchanged —
576/// the foreign read is always same-thread, cross-DSO.
577pub(crate) fn get_parser_input_buffer_create_filename_value_cross_dso(
578) -> Option<ParserInputCreateFilenameFunc> {
579    let local = tls_get(&tls::PARSER_INPUT_CREATE_FILENAME);
580    if local.is_some() {
581        return local;
582    }
583    match foreign_parser_input_create_filename_value_accessor() {
584        Some(accessor) => {
585            // SAFETY: the accessor returns a pointer to the current thread's
586            // valid, initialized TLS cell in the exporting DSO (upstream's
587            // deprecated accessor contract); the read is one dereference.
588            unsafe { *accessor() }
589        }
590        None => None,
591    }
592}
593
594/// Output-side twin of
595/// [`get_parser_input_buffer_create_filename_value_cross_dso`]: php MINIT
596/// also registers `php_libxml_output_buffer_create_filename` through the
597/// core's `xmlOutputBufferCreateFilenameDefault` (same R-000177 partition;
598/// needed by facade-driven writer/save paths under ZTS).
599pub(crate) fn get_output_buffer_create_filename_value_cross_dso() -> Option<OutputCreateFilenameFunc>
600{
601    let local = tls_get(&tls::OUTPUT_CREATE_FILENAME);
602    if local.is_some() {
603        return local;
604    }
605    match foreign_output_create_filename_value_accessor() {
606        Some(accessor) => {
607            // SAFETY: as the input-side twin — one dereference of the
608            // current thread's cell in the exporting DSO.
609            unsafe { *accessor() }
610        }
611        None => None,
612    }
613}
614
615#[cfg(target_os = "linux")]
616fn foreign_output_create_filename_value_accessor() -> Option<OutputCreateFilenameValueAccessor> {
617    use std::sync::OnceLock;
618    static ACCESSOR: OnceLock<Option<OutputCreateFilenameValueAccessor>> = OnceLock::new();
619    *ACCESSOR.get_or_init(|| {
620        // SAFETY: dlsym(RTLD_DEFAULT) returns the exported accessor address
621        // or NULL; a non-NULL symbol is transmuted to the fn-pointer type.
622        unsafe {
623            let sym = libc::dlsym(
624                libc::RTLD_DEFAULT,
625                c"__xmlOutputBufferCreateFilenameValue".as_ptr(),
626            );
627            if sym.is_null() {
628                None
629            } else {
630                Some(std::mem::transmute::<
631                    *mut c_void,
632                    OutputCreateFilenameValueAccessor,
633                >(sym))
634            }
635        }
636    })
637}
638
639#[cfg(not(target_os = "linux"))]
640fn foreign_output_create_filename_value_accessor() -> Option<OutputCreateFilenameValueAccessor> {
641    None
642}
643
644// ═══════════════════════════════════════════════════════════════════════════════
645// Public Accessors — Error Callbacks
646// ═══════════════════════════════════════════════════════════════════════════════
647
648/// Set the generic error handler.
649///
650/// # SAFETY
651///
652/// - `handler` must be a valid function pointer or NULL (to reset to default).
653/// - If non-NULL, the handler may be called at any time with `ctx`.
654pub unsafe fn set_generic_error_func(ctx: *mut c_void, handler: Option<xmlGenericErrorFunc>) {
655    // SAFETY: writing the per-thread error-handler slots
656    // xmlGenericErrorContext / xmlGenericError (TLS, upstream 2.15
657    // xmlSetGenericErrorFunc: NULL resets to the built-in default stderr
658    // printer, error.c). The (ctx, func) pair is written atomically under
659    // ERROR_HANDLER_LOCK (same-thread serialization, R-000171).
660    let resolved = match handler {
661        Some(h) => Some(h),
662        None => crate::abi::data_globals::default_generic_error_func(),
663    };
664    let _guard = ERROR_HANDLER_LOCK.lock();
665    tls_set(&tls::GENERIC_ERROR_CTX, ctx);
666    tls_set(&tls::GENERIC_ERROR, resolved);
667}
668
669/// Set the generic error handler context (thread-local; used by the default
670/// stderr printer to lazily default the context per thread, upstream
671/// error.c).
672pub fn set_generic_error_ctx(ctx: *mut c_void) {
673    let _guard = ERROR_HANDLER_LOCK.lock();
674    tls_set(&tls::GENERIC_ERROR_CTX, ctx);
675}
676
677/// Get the generic error handler function pointer (thread-local).
678pub fn get_generic_error_func() -> Option<xmlGenericErrorFunc> {
679    let _guard = ERROR_HANDLER_LOCK.lock();
680    read_cross(foreign_generic_error(), tls_get(&tls::GENERIC_ERROR))
681}
682
683/// Get the generic error handler context (thread-local).
684pub fn get_generic_error_ctx() -> *mut c_void {
685    let _guard = ERROR_HANDLER_LOCK.lock();
686    read_cross(
687        foreign_generic_error_ctx(),
688        tls_get(&tls::GENERIC_ERROR_CTX),
689    )
690}
691
692/// Read the generic error (func, ctx) pair atomically.
693///
694/// The closure runs after the lock is released, so a handler installed
695/// by the callback cannot deadlock.
696pub fn with_generic_error<R>(f: impl FnOnce(Option<xmlGenericErrorFunc>, *mut c_void) -> R) -> R {
697    let (h, c) = {
698        let _guard = ERROR_HANDLER_LOCK.lock();
699        (
700            read_cross(foreign_generic_error(), tls_get(&tls::GENERIC_ERROR)),
701            read_cross(
702                foreign_generic_error_ctx(),
703                tls_get(&tls::GENERIC_ERROR_CTX),
704            ),
705        )
706    };
707    f(h, c)
708}
709
710/// Set the structured error handler (thread-local, upstream 2.15).
711///
712/// # SAFETY
713///
714/// - `handler` must be a valid function pointer or NULL.
715pub unsafe fn set_structured_error_func(ctx: *mut c_void, handler: Option<xmlStructuredErrorFunc>) {
716    // SAFETY: writing the per-thread error-handler slots
717    // xmlStructuredErrorContext / xmlStructuredError (TLS, upstream 2.15
718    // xmlSetStructuredErrorFunc). The (ctx, func) pair is written
719    // atomically under ERROR_HANDLER_LOCK (same-thread serialization,
720    // R-000171).
721    let _guard = ERROR_HANDLER_LOCK.lock();
722    tls_set(&tls::STRUCTURED_ERROR_CTX, ctx);
723    tls_set(&tls::STRUCTURED_ERROR, handler);
724}
725
726/// Get the structured error handler function pointer (thread-local).
727pub fn get_structured_error_func() -> Option<xmlStructuredErrorFunc> {
728    let _guard = ERROR_HANDLER_LOCK.lock();
729    read_cross(foreign_structured_error(), tls_get(&tls::STRUCTURED_ERROR))
730}
731
732/// Get the structured error handler context (thread-local).
733pub fn get_structured_error_ctx() -> *mut c_void {
734    let _guard = ERROR_HANDLER_LOCK.lock();
735    read_cross(
736        foreign_structured_error_ctx(),
737        tls_get(&tls::STRUCTURED_ERROR_CTX),
738    )
739}
740
741/// Read the structured error (func, ctx) pair atomically.
742///
743/// The closure runs after the lock is released, so a handler installed by
744/// the callback (or an error raised from inside the handler) cannot
745/// deadlock on ERROR_HANDLER_LOCK.
746///
747/// # Safety
748///
749/// - The two reads of the per-thread `xmlStructuredError`/
750///   `xmlStructuredErrorContext` TLS slots are performed under
751///   `ERROR_HANDLER_LOCK`, matching the write side (xmlSetStructuredError),
752///   so the pair is observed atomically; the values are only borrowed for the
753///   duration of the closure and never dereferenced here.
754pub fn with_structured_error<R>(
755    f: impl FnOnce(Option<xmlStructuredErrorFunc>, *mut c_void) -> R,
756) -> R {
757    let (h, c) = {
758        let _guard = ERROR_HANDLER_LOCK.lock();
759        (
760            read_cross(foreign_structured_error(), tls_get(&tls::STRUCTURED_ERROR)),
761            read_cross(
762                foreign_structured_error_ctx(),
763                tls_get(&tls::STRUCTURED_ERROR_CTX),
764            ),
765        )
766    };
767    f(h, c)
768}
769
770// ═══════════════════════════════════════════════════════════════════════════════
771// `__xmlXxx()` pointer accessors (upstream threads.c / globals.c)
772// ═══════════════════════════════════════════════════════════════════════════════
773// The deprecated thread-local API exports one `__xmlXxx(void)` accessor per
774// TLS-era global; each returns a pointer to the CURRENT thread's slot so
775// callers can read/write it directly (upstream `#define xmlXxx
776// (*__xmlXxx())`). The candidate's exports (`crate::abi::data_globals`)
777// delegate here; the returned pointers alias the `tls` cells and stay valid
778// for the lifetime of the thread.
779
780pub(crate) fn do_validity_ptr() -> *mut c_int {
781    tls_ptr(&tls::DO_VALIDITY)
782}
783pub(crate) fn get_warnings_ptr() -> *mut c_int {
784    tls_ptr(&tls::GET_WARNINGS)
785}
786pub(crate) fn load_ext_dtd_ptr() -> *mut c_int {
787    tls_ptr(&tls::LOAD_EXT_DTD)
788}
789pub(crate) fn pedantic_ptr() -> *mut c_int {
790    tls_ptr(&tls::PEDANTIC)
791}
792pub(crate) fn line_numbers_ptr() -> *mut c_int {
793    tls_ptr(&tls::LINE_NUMBERS)
794}
795pub(crate) fn keep_blanks_ptr() -> *mut c_int {
796    tls_ptr(&tls::KEEP_BLANKS)
797}
798pub(crate) fn substitute_entities_ptr() -> *mut c_int {
799    tls_ptr(&tls::SUBSTITUTE_ENTITIES)
800}
801pub(crate) fn indent_tree_output_ptr() -> *mut c_int {
802    tls_ptr(&tls::INDENT_TREE_OUTPUT)
803}
804pub(crate) fn tree_indent_string_ptr() -> *mut *const xmlChar {
805    tls_ptr(&tls::TREE_INDENT_STRING)
806}
807pub(crate) fn save_no_empty_tags_ptr() -> *mut c_int {
808    tls_ptr(&tls::SAVE_NO_EMPTY_TAGS)
809}
810pub(crate) fn register_node_ptr() -> *mut Option<unsafe extern "C" fn(*mut _xmlNode)> {
811    tls_ptr(&tls::REGISTER_NODE)
812}
813pub(crate) fn deregister_node_ptr() -> *mut Option<unsafe extern "C" fn(*mut _xmlNode)> {
814    tls_ptr(&tls::DEREGISTER_NODE)
815}
816pub(crate) fn parser_input_create_filename_ptr(
817) -> *mut Option<unsafe extern "C" fn(*const c_char, c_int) -> *mut _xmlParserInputBuffer> {
818    tls_ptr(&tls::PARSER_INPUT_CREATE_FILENAME)
819}
820pub(crate) fn output_create_filename_ptr() -> *mut Option<
821    unsafe extern "C" fn(
822        *const c_char,
823        crate::abi::structs::xmlCharEncodingHandlerPtr,
824        c_int,
825    ) -> *mut _xmlOutputBuffer,
826> {
827    tls_ptr(&tls::OUTPUT_CREATE_FILENAME)
828}
829pub(crate) fn generic_error_ptr() -> *mut Option<xmlGenericErrorFunc> {
830    tls_ptr(&tls::GENERIC_ERROR)
831}
832pub(crate) fn generic_error_ctx_ptr() -> *mut *mut c_void {
833    tls_ptr(&tls::GENERIC_ERROR_CTX)
834}
835pub(crate) fn structured_error_ptr() -> *mut Option<xmlStructuredErrorFunc> {
836    tls_ptr(&tls::STRUCTURED_ERROR)
837}
838pub(crate) fn structured_error_ctx_ptr() -> *mut *mut c_void {
839    tls_ptr(&tls::STRUCTURED_ERROR_CTX)
840}
841
842// ═══════════════════════════════════════════════════════════════════════════════
843// Public Accessors — Thread-Local Error State
844// ═══════════════════════════════════════════════════════════════════════════════
845
846/// Get the last error for this thread.
847///
848/// Returns a mutable pointer to the last error, or NULL if no error occurred.
849/// The returned pointer is valid until the next libxml2 call in this thread.
850pub fn get_last_error() -> *mut _xmlError {
851    LAST_ERROR.with(|last| {
852        let mut last = last.borrow_mut();
853        last.as_mut()
854            .map_or(ptr::null_mut(), |e| e as *mut _xmlError)
855    })
856}
857
858/// Get a reference to the last error (for structured error callback).
859pub fn with_last_error<F, R>(f: F) -> R
860where
861    F: FnOnce(Option<&_xmlError>) -> R,
862{
863    LAST_ERROR.with(|last| f(last.borrow().as_ref()))
864}
865
866/// Set the last error for this thread.
867pub fn set_last_error(err: _xmlError) {
868    // UPSTREAM-PARITY: mirror the error into the exported C global
869    // `xmlLastError` (data-ABI, residual R-000135). Upstream keeps a single
870    // global; the candidate keeps a thread-local truth and a deep-copied
871    // mirror so C consumers see the most recent error with upstream
872    // lifetime semantics (mirror strings are owned by the mirror and freed
873    // on reset, matching xmlResetError).
874    //
875    // SAFETY: sync_xml_last_error only reads `err` and writes the global
876    // mirror with freshly owned copies; the thread-local slot takes
877    // ownership of `err` itself.
878    unsafe { crate::abi::data_globals::sync_xml_last_error(&err) };
879    LAST_ERROR.with(|last| {
880        let mut last = last.borrow_mut();
881        // Free the previous slot's owned strings (upstream xmlResetError).
882        if let Some(prev) = last.as_ref() {
883            free_error_strings(prev);
884        }
885        *last = Some(err);
886    });
887}
888
889/// Free the owned string fields of a stored error (upstream xmlResetError:
890/// message/file/str1/str2/str3 are xmlMalloc'd copies).
891///
892/// # Safety
893///
894/// - `err` must point to a valid `_xmlError` whose `message`, `file`,
895///   `str1`, `str2` and `str3` fields are NULL or pointers allocated with
896///   `xmlFreeImpl`'s allocator; each non-NULL field is freed exactly once
897///   and must not be freed or used again afterwards.
898pub(crate) fn free_error_strings(err: &_xmlError) {
899    use crate::abi::allocator::xmlFreeImpl;
900    unsafe {
901        if !err.message.is_null() {
902            xmlFreeImpl(err.message as *mut core::ffi::c_void);
903        }
904        if !err.file.is_null() {
905            xmlFreeImpl(err.file as *mut core::ffi::c_void);
906        }
907        if !err.str1.is_null() {
908            xmlFreeImpl(err.str1 as *mut core::ffi::c_void);
909        }
910        if !err.str2.is_null() {
911            xmlFreeImpl(err.str2 as *mut core::ffi::c_void);
912        }
913        if !err.str3.is_null() {
914            xmlFreeImpl(err.str3 as *mut core::ffi::c_void);
915        }
916    }
917}
918
919/// Reset the last error for this thread.
920pub fn reset_last_error() {
921    LAST_ERROR.with(|last| {
922        let mut last = last.borrow_mut();
923        if let Some(prev) = last.as_ref() {
924            free_error_strings(prev);
925        }
926        *last = None;
927    });
928    // SAFETY: frees the mirror's owned strings and zeroes the global.
929    unsafe { crate::abi::data_globals::reset_xml_last_error() };
930}
931
932// ═══════════════════════════════════════════════════════════════════════════════
933// Public Accessors — Catalog Defaults
934// ═══════════════════════════════════════════════════════════════════════════════
935
936/// Get the catalog default allow value.
937pub fn get_catalog_defaults() -> c_int {
938    CATALOG_DEFAULTS.load(Ordering::Relaxed)
939}
940
941/// Set the catalog default allow value.
942pub fn set_catalog_defaults(val: c_int) {
943    CATALOG_DEFAULTS.store(val, Ordering::Relaxed);
944}
945
946// ═══════════════════════════════════════════════════════════════════════════════
947// Initialization / Cleanup
948// ═══════════════════════════════════════════════════════════════════════════════
949
950/// Initialize the parser library.
951///
952/// Must be called before any other libxml2 functions.
953/// Safe to call multiple times (reference-counted in modern libxml2).
954///
955/// # UPSTREAM-PARITY
956///
957/// In modern libxml2 (2.12+), `xmlInitParser` is reference-counted.
958/// The first call initializes all subsystems; subsequent calls
959/// increment a counter. `xmlCleanupParser` decrements the counter
960/// and only performs cleanup when it reaches zero.
961///
962/// # SAFETY
963///
964/// Not fully thread-safe during the first call; callers should
965/// call `xmlInitParser` before creating threads.
966pub unsafe fn init_parser() {
967    let prev = INIT_REF_COUNT.fetch_add(1, Ordering::AcqRel);
968    if prev == 0 {
969        // First initialization — initialize all subsystems.
970        // 1. Initialize memory subsystem.
971        allocator::xmlInitMemory();
972
973        // 2. Mark the library as initialized.
974        versioning::set_initialized();
975
976        // 3. Initialize encoding handlers.
977        crate::xml::encoding::init_encodings();
978
979        // 4. Initialize thread support.
980        init_threads();
981    }
982}
983
984/// Clean up the parser library.
985///
986/// Should be called when the library is no longer needed.
987/// Only performs actual cleanup when the reference count reaches zero.
988///
989/// # SAFETY
990///
991/// Must not be called while other libxml2 functions are executing
992/// in any thread.
993pub unsafe fn cleanup_parser() {
994    let prev = INIT_REF_COUNT.fetch_sub(1, Ordering::AcqRel);
995    if prev <= 1 {
996        // Last cleanup — clean up all subsystems.
997        // 1. Clean up catalog.
998        crate::xml::catalog::cleanup();
999
1000        // 2. Clean up encoding handlers.
1001        crate::xml::encoding::cleanup_encodings();
1002
1003        // 3. Clean up memory.
1004        allocator::xmlCleanupMemory();
1005
1006        // 3. Reset initialization state.
1007        // Note: we do NOT reset the initialized flag in case
1008        // some code checks it after cleanup. This matches
1009        // upstream behavior where xmlCleanupParser is best-effort.
1010    }
1011}
1012
1013/// Initialize threading support.
1014///
1015/// # UPSTREAM-PARITY
1016///
1017/// In modern libxml2, threading is initialized automatically
1018/// by `xmlInitParser`. This function exists for backward
1019/// compatibility.
1020///
1021/// Returns 0 on success.
1022pub fn init_threads() -> c_int {
1023    if !THREADS_INITIALIZED.swap(true, Ordering::Release) {
1024        // First initialization — no-op in Rust since we use
1025        // standard thread-safe primitives.
1026        // In libxml2 this would set up pthread mutexes.
1027    }
1028    0
1029}
1030
1031/// Clean up threading support.
1032pub fn cleanup_threads() {
1033    THREADS_INITIALIZED.store(false, Ordering::Release);
1034}
1035
1036/// Check whether threads have been initialized.
1037pub fn threads_initialized() -> bool {
1038    THREADS_INITIALIZED.load(Ordering::Acquire)
1039}
1040
1041/// Get the current initialization reference count.
1042pub fn init_ref_count() -> c_int {
1043    INIT_REF_COUNT.load(Ordering::Relaxed)
1044}
1045
1046// ═══════════════════════════════════════════════════════════════════════════════
1047// Tests
1048// ═══════════════════════════════════════════════════════════════════════════════
1049
1050#[cfg(test)]
1051mod tests {
1052    use super::*;
1053    use crate::abi::types::xmlErrorLevel::XML_ERR_NONE;
1054    use crate::abi::types::*;
1055
1056    #[test]
1057    fn test_parser_defaults_initial_values() {
1058        assert_eq!(get_validity_checking_default(), 0);
1059        assert_eq!(get_do_warnings_default(), 1);
1060        // UPSTREAM-PARITY (globals.c 2.15): xmlIndentTreeOutputThrDef = 1.
1061        assert_eq!(get_indent_tree_output(), 1);
1062        assert_eq!(get_keep_blanks_default(), 1);
1063        assert_eq!(get_load_ext_dtd_default(), 0);
1064        assert_eq!(get_pedantic_parser_default(), 0);
1065        assert_eq!(get_substitute_entities_default(), 0);
1066        assert_eq!(get_save_no_empty_tags(), 0);
1067        assert_eq!(get_get_warnings_default(), 1);
1068    }
1069
1070    #[test]
1071    fn test_parser_defaults_set_and_get() {
1072        set_validity_checking_default(1);
1073        assert_eq!(get_validity_checking_default(), 1);
1074        set_validity_checking_default(0);
1075        assert_eq!(get_validity_checking_default(), 0);
1076
1077        set_keep_blanks_default(0);
1078        assert_eq!(get_keep_blanks_default(), 0);
1079        set_keep_blanks_default(1);
1080        assert_eq!(get_keep_blanks_default(), 1);
1081
1082        set_substitute_entities_default(1);
1083        assert_eq!(get_substitute_entities_default(), 1);
1084        set_substitute_entities_default(0);
1085        assert_eq!(get_substitute_entities_default(), 0);
1086    }
1087
1088    /// Increment and decrement the init reference count.
1089    ///
1090    /// # Safety
1091    ///
1092    /// - `init_parser`/`cleanup_parser` mutate the global reference count
1093    ///   and may run subsystem init/cleanup; the test balances the two
1094    ///   calls so the library is left in a clean state, and the reference
1095    ///   count is only read between calls.
1096    #[test]
1097    fn test_init_cleanup_ref_count() {
1098        // Reset for test
1099        unsafe {
1100            init_parser();
1101            assert_eq!(init_ref_count(), 1);
1102
1103            init_parser();
1104            assert_eq!(init_ref_count(), 2);
1105
1106            cleanup_parser();
1107            assert!(init_ref_count() == 1 || init_ref_count() == 0);
1108
1109            // Final cleanup
1110            cleanup_parser();
1111        }
1112    }
1113
1114    #[test]
1115    fn test_error_callbacks_default_handlers() {
1116        // UPSTREAM-PARITY (error.c): xmlGenericError defaults to the built-in
1117        // stderr printer (never NULL); xmlStructuredError defaults to NULL.
1118        // Serialized against the other handler-mutating tests (11.1-X): the
1119        // slots are shared global state.
1120        let _guard = ERROR_HANDLER_TEST_LOCK.lock();
1121        #[cfg(target_arch = "x86_64")]
1122        assert!(get_generic_error_func().is_some());
1123        assert!(get_structured_error_func().is_none());
1124    }
1125
1126    /// Install and reset the generic error handler slot pair.
1127    ///
1128    /// # Safety
1129    ///
1130    /// - `dummy_handler` is a valid no-op callback pointer; `dummy_ctx`
1131    ///   points to a stack `i32` alive for the test; `set_generic_error_func`
1132    ///   writes the exported slot pair under the handler lock, and NULL
1133    ///   resets to the built-in default printer.
1134    #[test]
1135    fn test_error_callbacks_set_and_get() {
1136        let _guard = ERROR_HANDLER_TEST_LOCK.lock();
1137        unsafe {
1138            /// A no-op generic error callback used as a handler pointer.
1139            ///
1140            /// # Safety
1141            ///
1142            /// - The function is never invoked by this test; when installed as a
1143            ///   handler it must be a valid function pointer, and the parameters
1144            ///   would need to be valid C pointers if it were called.
1145            unsafe extern "C" fn dummy_handler(_ctx: *mut c_void, _msg: *const core::ffi::c_char) {}
1146            let dummy_func: xmlGenericErrorFunc = dummy_handler;
1147            let dummy_ctx: *mut c_void = &mut 0 as *mut i32 as *mut c_void;
1148
1149            set_generic_error_func(dummy_ctx, Some(dummy_func));
1150            assert!(get_generic_error_func().is_some());
1151            assert_eq!(get_generic_error_ctx(), dummy_ctx);
1152
1153            // UPSTREAM-PARITY (xmlSetGenericErrorFunc): NULL resets to the
1154            // built-in default printer, it does not unset the handler.
1155            set_generic_error_func(ptr::null_mut(), None);
1156            #[cfg(target_arch = "x86_64")]
1157            assert!(get_generic_error_func().is_some());
1158            assert_eq!(get_generic_error_ctx(), ptr::null_mut());
1159        }
1160    }
1161
1162    /// Store and reset a thread-local last error.
1163    ///
1164    /// # Safety
1165    ///
1166    /// - `err` has all NULL string fields, so `set_last_error` stores it
1167    ///   without owning heap strings and `reset_last_error` frees nothing;
1168    ///   `get_last_error` returns a pointer valid while the thread-local
1169    ///   slot is not mutated.
1170    #[test]
1171    fn test_last_error_thread_local() {
1172        assert!(get_last_error().is_null());
1173
1174        let err = _xmlError {
1175            domain: XML_FROM_PARSER,
1176            code: XML_ERR_OK as c_int,
1177            message: ptr::null_mut(),
1178            level: XML_ERR_NONE as c_int,
1179            file: ptr::null_mut(),
1180            line: 0,
1181            str1: ptr::null_mut(),
1182            str2: ptr::null_mut(),
1183            str3: ptr::null_mut(),
1184            int1: 0,
1185            int2: 0,
1186            ctxt: ptr::null_mut(),
1187            node: ptr::null_mut(),
1188        };
1189        set_last_error(err);
1190        assert!(!get_last_error().is_null());
1191        unsafe {
1192            assert_eq!((*get_last_error()).domain, XML_FROM_PARSER);
1193        }
1194
1195        reset_last_error();
1196        assert!(get_last_error().is_null());
1197    }
1198
1199    #[test]
1200    fn test_catalog_defaults() {
1201        // Save original value (may have been set by init_parser in other tests)
1202        let orig = get_catalog_defaults();
1203        set_catalog_defaults(1);
1204        assert_eq!(get_catalog_defaults(), 1);
1205        set_catalog_defaults(0);
1206        assert_eq!(get_catalog_defaults(), 0);
1207        // Restore
1208        set_catalog_defaults(orig);
1209    }
1210}