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