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}