Skip to main content

libxml_rs/xml/errors/
mod.rs

1//! Error subsystem (§21, §85 Phase 1).
2//!
3//! Implements the libxml2 error reporting infrastructure:
4//!
5//! - `xmlError` struct management
6//! - Error domain/code registry
7//! - Structured error callbacks (thread-local storage)
8//! - Generic error callbacks (thread-local storage)
9//! - Last-error tracking (thread-local `xmlGetLastError`, `xmlResetLastError`, `xmlCopyError`)
10//! - Error message formatting
11//! - `xmlRaiseError()` — the central error reporting function
12//!
13//! # UPSTREAM-PARITY
14//!
15//! libxml2 has a two-tier error system:
16//!
17//! 1. **Structured errors** — `xmlStructuredErrorFunc` receives an `xmlErrorPtr`
18//!    with all structured fields (domain, code, level, line, etc.)
19//!
20//! 2. **Generic errors** — `xmlGenericErrorFunc` receives a formatted string
21//!    (printf-style). This is the older system, still widely used.
22//!
23//! Both systems coexist. When both handlers are set, both are called.
24//! The last error is stored thread-locally for retrieval via `xmlGetLastError`.
25//!
26//! # Phase 1 status
27//!
28//! Complete — all error functions are implemented.
29//! Variadic message formatting will be enhanced in Phase 2+.
30//!
31//! # Upstream contract
32//!
33//! Mirrors upstream error.c (SRC-LIBXML2-2.15.0-ERROR-C, oracle tree
34//! `oracle/historical/src/libxml2-2.15.0/error.c`): xmlRaiseError /
35//! xmlVRaiseError routing, xmlFormatError, xmlGenericErrorDefaultFunc,
36//! xmlGetLastError / xmlCtxtGetLastError / xmlResetLastError, xmlCopyError
37//! and the legacy xmlParserError / xmlParserWarning SAX handlers.
38//!
39//! # Conceptual behavior
40//!
41//! Two-tier error system: structured errors (xmlStructuredErrorFunc with the
42//! full xmlError) and generic errors (printf-style fragments). The 11.1-M/K
43//! rework routes each raise through ONE channel: a structured handler, else
44//! the custom SAX channel (channel(data, msg) once), else the legacy default
45//! streaming the xmlFormatError fragments (file/line, domain, level, message,
46//! source window, caret) as 6 variadic calls (R-000161).
47//!
48//! # Ownership & safety invariants
49//!
50//! Ownership: the thread-local last error owns its file/str1-3 copies;
51//! raise_error_streamed copies transient CStrings (R-000163: dangling
52//! pointers were the bug). The exported xmlLastError mirror is synced under
53//! lock (R-000170). SAFETY: the variadic xmlGenericErrorDefaultFunc and
54//! xmlParserError legacy handlers are x86_64 SysV inline-asm va_list shims —
55//! stable Rust cannot define variadic extern fns.
56//!
57//! # Historical quirks & epochs
58//!
59//! E-005: fatal parser errors are reported twice from 2.13.0 (attr-markup-
60//! entity); the xmlFormatError fragment stream and the 80-column source
61//! window cap are 2.15 semantics (xmlParserInputGetWindow, caret clamp).
62//! R-000163 pinned all XML_ERR_* 64-96 constants to upstream numbering
63//! (SPACE_REQUIRED 65, NAME_REQUIRED 68, GT_REQUIRED 73, TAG_NAME_MISMATCH
64//! 76, TAG_NOT_FINISHED 77 — not the synthetic renumbering).
65//!
66//! # Deliberate oddities
67//!
68//! Deliberate oddities: warnings only bump nbWarnings while other levels
69//! update errNo / nbErrors / wellFormed per xmlCtxtVErr; the source window
70//! replicates the 80-char cap, continuation-byte skip, UTF-8 forward scan and
71//! the 2.15 caret clamp (col >= n maps to size-1).
72//!
73//! # Proving courts
74//!
75//! ERROR and CALLBACK court families; ERROR-001 (48 deterministic malformed
76//! inputs x 4 passes, byte-identical), GLOBALS-THREADING, DATA-GLOBALS-001,
77//! and `cargo test --lib` (ASan-clean).
78//!
79//! # Tempting simplifications that would break parity
80//!
81//! The tempting simplification is one generic call plus a direct stderr write
82//! — a counting handler would observe 1 call instead of the 6 xmlFormatError
83//! fragments the oracle emits (R-000161). Do not route errors to stderr when a
84//! handler is installed. Never renumber the XML_ERR_* constants (R-000163).
85
86use core::ffi::c_void;
87use core::ptr;
88use std::os::raw::{c_char, c_int, c_uint};
89
90use crate::abi::callbacks::{errorSAXFunc, xmlGenericErrorFunc, xmlStructuredErrorFunc};
91use crate::abi::structs::_xmlError;
92use crate::abi::types::xmlErrorLevel::*;
93use crate::abi::types::*;
94use crate::xml::globals;
95
96// ═══════════════════════════════════════════════════════════════════════════════
97// Error Management Functions
98// ═══════════════════════════════════════════════════════════════════════════════
99
100/// Set the generic error handler.
101///
102/// # UPSTREAM-PARITY
103///
104/// ```c
105/// void xmlSetGenericErrorFunc(void *ctx, xmlGenericErrorFunc handler);
106/// ```
107///
108/// # SAFETY
109///
110/// - `handler` must be a valid function pointer or NULL (to reset to default).
111/// - If non-NULL, the handler may be called at any time with `ctx`.
112pub unsafe fn set_generic_error_func(ctx: *mut c_void, handler: Option<xmlGenericErrorFunc>) {
113    // SAFETY: Delegates to globals with same safety contract.
114    unsafe { globals::set_generic_error_func(ctx, handler) };
115}
116
117/// Set the structured error handler.
118///
119/// # UPSTREAM-PARITY
120///
121/// ```c
122/// void xmlSetStructuredErrorFunc(void *ctx, xmlStructuredErrorFunc handler);
123/// ```
124///
125/// # SAFETY
126///
127/// - `handler` must be a valid function pointer or NULL.
128pub unsafe fn set_structured_error_func(ctx: *mut c_void, handler: Option<xmlStructuredErrorFunc>) {
129    // SAFETY: Delegates to globals with same safety contract.
130    unsafe { globals::set_structured_error_func(ctx, handler) };
131}
132
133/// Get the last error for the current thread.
134///
135/// # UPSTREAM-PARITY
136///
137/// ```c
138/// xmlErrorPtr xmlGetLastError(void);
139/// ```
140///
141/// Returns a pointer to the last error, or NULL if no error occurred.
142/// The returned pointer is valid until the next libxml2 call in this thread.
143pub fn get_last_error() -> *mut _xmlError {
144    globals::get_last_error()
145}
146
147/// Copy an error from one location to another.
148///
149/// # UPSTREAM-PARITY
150///
151/// ```c
152/// xmlErrorPtr xmlCopyError(xmlErrorPtr from, xmlErrorPtr to);
153/// ```
154///
155/// Copies `from` into `to`. Returns 0 on success, -1 on error.
156///
157/// # SAFETY
158///
159/// - `from` and `to` must be valid pointers to `_xmlError` structs, or NULL.
160pub const unsafe fn copy_error(from: *const _xmlError, to: *mut _xmlError) -> c_int {
161    if from.is_null() || to.is_null() {
162        return -1;
163    }
164    // SAFETY: Caller guarantees both pointers are valid.
165    unsafe {
166        ptr::copy_nonoverlapping(from, to, 1);
167    }
168    0
169}
170
171/// Reset an error structure to its default state.
172///
173/// # UPSTREAM-PARITY
174///
175/// ```c
176/// void xmlResetError(xmlErrorPtr err);
177/// ```
178///
179/// # SAFETY
180///
181/// - `err` must be a valid pointer to `_xmlError`, or NULL.
182pub const unsafe fn reset_error(err: *mut _xmlError) {
183    if err.is_null() {
184        return;
185    }
186    // SAFETY: Caller guarantees pointer is valid.
187    unsafe {
188        ptr::write(
189            err,
190            _xmlError {
191                domain: XML_FROM_NONE,
192                code: XML_ERR_OK as c_int,
193                message: ptr::null_mut(),
194                level: XML_ERR_NONE as c_int,
195                file: ptr::null_mut(),
196                line: 0,
197                str1: ptr::null_mut(),
198                str2: ptr::null_mut(),
199                str3: ptr::null_mut(),
200                int1: 0,
201                int2: 0,
202                ctxt: ptr::null_mut(),
203                node: ptr::null_mut(),
204            },
205        );
206    }
207}
208
209/// Reset the last error for the current thread.
210///
211/// # UPSTREAM-PARITY
212///
213/// ```c
214/// void xmlResetLastError(void);
215/// ```
216pub fn reset_last_error() {
217    globals::reset_last_error();
218}
219
220/// Format an error message.
221///
222/// This function creates a formatted error message from the component parts.
223/// In Phase 1, this is a basic implementation. In Phase 2+, variadic
224/// printf-style formatting will be added.
225///
226/// Returns a C string pointer (allocated with xmlMalloc) that the caller
227/// must free with xmlFreeImpl, or NULL on allocation failure.
228///
229/// # UPSTREAM-PARITY
230///
231/// Upstream libxml2 uses `vsnprintf` internally for message formatting.
232/// We use a simple formatting approach that produces compatible output
233/// for the common error patterns.
234///
235/// # Safety
236///
237/// - `msg` must be NULL or a valid NUL-terminated C string; `str1`, `str2`
238///   and `str3` must be NULL or valid NUL-terminated C strings; the
239///   returned buffer is allocator-owned and must be freed with
240///   `xmlFreeImpl`, or is NULL on allocation failure.
241pub fn format_error_message(
242    _domain: c_int,
243    _code: c_int,
244    msg: *const c_char,
245    str1: *const c_char,
246    str2: *const c_char,
247    str3: *const c_char,
248) -> *mut c_char {
249    // Phase 1: basic message construction.
250    // If a direct message string is provided, use it.
251    if !msg.is_null() {
252        // SAFETY: Caller guarantees msg is a valid C string.
253        let msg_str = unsafe { crate::abi::allocator::xmlMemStrdupImpl(msg) };
254        return msg_str as *mut c_char;
255    }
256
257    // Build a message from the component strings.
258    // This matches upstream behavior where domain/code are combined
259    // with str1/str2/str3 into a diagnostic message.
260    let mut buf: [u8; 1024] = [0; 1024];
261    let mut pos = 0;
262
263    // Write domain prefix
264    let domain_str = match _domain {
265        XML_FROM_PARSER => "parser",
266        XML_FROM_TREE => "tree",
267        XML_FROM_NAMESPACE => "namespace",
268        XML_FROM_DTD => "dtd",
269        XML_FROM_HTML => "html",
270        XML_FROM_MEMORY => "memory",
271        XML_FROM_OUTPUT => "output",
272        XML_FROM_IO => "io",
273        XML_FROM_XPATH => "xpath",
274        XML_FROM_XPOINTER => "xpointer",
275        XML_FROM_XINCLUDE => "xinclude",
276        XML_FROM_CATALOG => "catalog",
277        XML_FROM_C14N => "c14n",
278        XML_FROM_XSLT => "xslt",
279        XML_FROM_VALID => "valid",
280        XML_FROM_CHECK => "check",
281        XML_FROM_WRITER => "writer",
282        XML_FROM_MODULE => "module",
283        XML_FROM_I18N => "i18n",
284        XML_FROM_SCHEMATRONV => "schematron",
285        XML_FROM_BUFFER => "buffer",
286        XML_FROM_URI => "uri",
287        XML_FROM_NONE => "",
288        XML_FROM_FTP => "ftp",
289        XML_FROM_HTTP => "http",
290        XML_FROM_REGEXP => "regexp",
291        XML_FROM_DATATYPE => "datatype",
292        XML_FROM_SCHEMASP => "schema parser",
293        XML_FROM_SCHEMASV => "schema validator",
294        XML_FROM_RELAXNGP => "relaxng parser",
295        XML_FROM_RELAXNGV => "relaxng validator",
296        _ => "unknown",
297    };
298
299    if !domain_str.is_empty() {
300        let bytes = domain_str.as_bytes();
301        let len = bytes.len().min(buf.len().saturating_sub(pos + 2));
302        buf[pos..pos + len].copy_from_slice(&bytes[..len]);
303        pos += len;
304        buf[pos] = b' ';
305        pos += 1;
306    }
307
308    // Append str1 if present
309    if !str1.is_null() {
310        // SAFETY: Caller guarantees str1 is a valid C string.
311        let s = unsafe { crate::abi::versioning::c_str_to_bytes(str1).unwrap_or_default() };
312        if pos + s.len() + 3 <= buf.len() {
313            buf[pos] = b'\'';
314            pos += 1;
315            buf[pos..pos + s.len()].copy_from_slice(s);
316            pos += s.len();
317            buf[pos] = b'\'';
318            pos += 1;
319            buf[pos] = b' ';
320            pos += 1;
321        }
322    }
323
324    // Append str2 if present
325    if !str2.is_null() {
326        let s = unsafe { crate::abi::versioning::c_str_to_bytes(str2).unwrap_or_default() };
327        if pos + s.len() + 3 <= buf.len() {
328            buf[pos] = b'\'';
329            pos += 1;
330            buf[pos..pos + s.len()].copy_from_slice(s);
331            pos += s.len();
332            buf[pos] = b'\'';
333            pos += 1;
334            buf[pos] = b' ';
335            pos += 1;
336        }
337    }
338
339    // Append str3 if present
340    if !str3.is_null() {
341        let s = unsafe { crate::abi::versioning::c_str_to_bytes(str3).unwrap_or_default() };
342        if pos + s.len() + 3 <= buf.len() {
343            buf[pos] = b'\'';
344            pos += 1;
345            buf[pos..pos + s.len()].copy_from_slice(s);
346            pos += s.len();
347            buf[pos] = b'\'';
348            pos += 1;
349            buf[pos] = b' ';
350            pos += 1;
351        }
352    }
353
354    // Null-terminate
355    if pos < buf.len() {
356        buf[pos] = 0;
357    } else {
358        buf[buf.len() - 1] = 0;
359    }
360
361    // Allocate and return
362    let result = unsafe { crate::abi::allocator::xmlMallocImpl(pos + 1) };
363    if result.is_null() {
364        return ptr::null_mut();
365    }
366    unsafe {
367        ptr::copy_nonoverlapping(buf.as_ptr(), result as *mut u8, pos + 1);
368    }
369    result as *mut c_char
370}
371
372/// Raise an error — the central error reporting function.
373///
374/// This is called internally when an error occurs. It:
375/// 1. Updates the thread-local last error
376/// 2. Invokes the structured error handler if one is set
377/// 3. Invokes the generic error handler if one is set (for warnings/errors)
378///
379/// # UPSTREAM-PARITY
380///
381/// ```c
382/// void xmlRaiseError(xmlErrorPtr ctxt,
383///                    xmlErrorPtr ctxt2,
384///                    xmlErrorPtr ctxt3,
385///                    xmlErrorPtr ctxt4,
386///                    xmlErrorPtr ctxt5,
387///                    int domain,
388///                    int code,
389///                    xmlErrorLevel level,
390///                    const char *file,
391///                    int line,
392///                    const char *str1,
393///                    const char *str2,
394///                    const char *str3,
395///                    int int1,
396///                    int int2,
397///                    const char *msg,
398///                    ...);
399/// ```
400///
401/// # SAFETY
402///
403/// - `ctxt` may be NULL (context of the error).
404/// - `domain`, `code`, `level`: valid error codes.
405/// - `msg` must be a valid C string or NULL.
406/// - `file` must be a valid C string or NULL.
407/// - `str1`, `str2`, `str3`: error-related strings (may be NULL).
408#[allow(clippy::too_many_arguments)]
409pub unsafe fn raise_error(
410    ctxt: *mut c_void,
411    _ctxt2: *mut c_void,
412    _ctxt3: *mut c_void,
413    _ctxt4: *mut c_void,
414    _ctxt5: *mut c_void,
415    domain: c_int,
416    code: c_int,
417    level: c_int,
418    file: *const c_char,
419    line: c_int,
420    str1: *const c_char,
421    str2: *const c_char,
422    str3: *const c_char,
423    int1: c_int,
424    _int2: c_int,
425    msg: *const c_char,
426) {
427    // Format the error message
428    let formatted_msg = format_error_message(domain, code, msg, str1, str2, str3);
429
430    // UPSTREAM-PARITY (xmlVSetError): string fields are owned copies so the
431    // stored error survives transient callers.
432    let file_copy = if file.is_null() {
433        ptr::null_mut()
434    } else {
435        crate::abi::allocator::xmlMemStrdupImpl(file) as *mut c_char
436    };
437    let str1_copy = if str1.is_null() {
438        ptr::null_mut()
439    } else {
440        crate::abi::allocator::xmlMemStrdupImpl(str1) as *mut c_char
441    };
442    let str2_copy = if str2.is_null() {
443        ptr::null_mut()
444    } else {
445        crate::abi::allocator::xmlMemStrdupImpl(str2) as *mut c_char
446    };
447    let str3_copy = if str3.is_null() {
448        ptr::null_mut()
449    } else {
450        crate::abi::allocator::xmlMemStrdupImpl(str3) as *mut c_char
451    };
452
453    // Store the last error
454    let err = _xmlError {
455        domain,
456        code,
457        message: formatted_msg,
458        level,
459        file: file_copy,
460        line,
461        str1: str1_copy,
462        str2: str2_copy,
463        str3: str3_copy,
464        int1,
465        int2: 0,
466        ctxt,
467        node: ptr::null_mut(),
468    };
469
470    globals::set_last_error(err);
471
472    // UPSTREAM-PARITY (xmlCtxtVErr): the structured handler wins; the
473    // generic channel is only used when no structured handler is set. The
474    // (handler, ctx) pairs are read atomically (11.1-X), then invoked
475    // outside the lock so a handler that re-enters the library cannot
476    // deadlock.
477    let structured = globals::with_structured_error(|h, c| (h, c));
478    if let Some(handler) = structured.0 {
479        let err_ref = globals::get_last_error();
480        if !err_ref.is_null() {
481            handler(structured.1, err_ref as *const _xmlError);
482        }
483    } else if level != 0 {
484        let generic = globals::with_generic_error(|h, c| (h, c));
485        if let Some(handler) = generic.0 {
486            let ctx = generic.1;
487            if !formatted_msg.is_null() {
488                handler(ctx, formatted_msg as *const core::ffi::c_char);
489            } else if !msg.is_null() {
490                handler(ctx, msg);
491            }
492        }
493    }
494
495    // Free the formatted message if it was allocated
496    // Note: We keep it as the last error's message, so we don't free it here.
497    // The next call to raise_error or reset_error will free the old message.
498    // Actually, in Phase 1, we don't free because the message is the last error's.
499    // A more complete implementation would free the old message when setting a new one.
500}
501
502/// Emit a legacy-format message through the generic error channel.
503///
504/// Upstream's `xmlGenericErrorDefaultFunc` writes the message to stderr;
505/// when a custom generic handler is installed it receives the message. The
506/// `level` prefix matches upstream `xmlVFormatLegacyError` (error.c 2.15).
507unsafe fn emit_legacy_message(level: &str, msg: *const c_char) {
508    if msg.is_null() {
509        return;
510    }
511    let len = libc::strlen(msg) as usize;
512    let text = core::slice::from_raw_parts(msg as *const u8, len);
513    let mut full = Vec::with_capacity(level.len() + 2 + len);
514    full.extend_from_slice(level.as_bytes());
515    full.push(b':');
516    full.push(b' ');
517    full.extend_from_slice(text);
518    if let Some(handler) = globals::get_generic_error_func() {
519        let ctx = globals::get_generic_error_ctx();
520        let mut cmsg = full.clone();
521        cmsg.push(0);
522        handler(ctx, cmsg.as_ptr() as *const c_char);
523    } else {
524        // Upstream default (xmlGenericErrorDefaultFunc): stderr.
525        let _ = libc::write(2, full.as_ptr() as *const libc::c_void, full.len());
526    }
527}
528
529/// System V AMD64 `__va_list_tag` (24 bytes) — same layout as the writer's
530/// shims and `data_globals.rs`.
531#[cfg(target_arch = "x86_64")]
532#[repr(C)]
533#[derive(Clone, Copy, Debug)]
534pub struct VaListTag {
535    gp_offset: c_uint,
536    fp_offset: c_uint,
537    overflow_arg_area: *mut c_void,
538    reg_save_area: *mut c_void,
539}
540
541#[cfg(target_arch = "x86_64")]
542unsafe extern "C" {
543    /// Format `msg` with the caller's va_list into `s` (libc `vsnprintf`).
544    ///
545    /// # Safety
546    ///
547    /// - `s` must point to a writable buffer of at least `n` bytes;
548    ///   `format` must be a valid NUL-terminated printf format string;
549    ///   `ap` must be a valid va_list matching the format's specifiers.
550    fn vsnprintf(s: *mut c_char, n: usize, format: *const c_char, ap: *mut VaListTag) -> c_int;
551}
552
553/// Format `msg` with the caller's varargs and emit `level + ": "` + the
554/// formatted text through the generic channel (upstream error.c
555/// `xmlVFormatLegacyError`: `xmlGenericError(ctx, "%s: ", level)` then the
556/// `xmlStrVASPrintf`-formatted message).
557///
558/// # Safety
559///
560/// - `msg` must be NULL or a valid NUL-terminated printf format string;
561///   `ap` must be a valid va_list matching the format's specifiers; the
562///   format is consumed exactly once.
563#[cfg(target_arch = "x86_64")]
564unsafe fn emit_legacy_message_v(level: &str, msg: *const c_char, ap: *mut VaListTag) -> c_int {
565    if msg.is_null() {
566        return 0;
567    }
568    let mut buf = [0 as c_char; 4096];
569    let n = unsafe { vsnprintf(buf.as_mut_ptr(), buf.len(), msg, ap) };
570    let n = n.clamp(0, buf.len() as c_int - 1) as usize;
571    buf[n] = 0;
572    unsafe { emit_legacy_message(level, buf.as_ptr()) };
573    0
574}
575
576/// x86_64 SysV variadic shim: captures the register save area exactly like
577/// `va_start` (2 fixed args → `gp_offset` 16), builds the va_list at
578/// rsp+176, passes it as the 3rd argument (rdx) to `receiver`, then restores
579/// the stack and returns. Same technique as `xmlStrPrintf`
580/// (exports_string.rs) and `xsltTransformError` (exports_xslt_util.rs).
581///
582/// Layout: reg_save_area = rsp+0 (6 GP + 8 SSE slots, 176 bytes); the
583/// va_list struct lives at rsp+176; overflow varargs are above the return
584/// address; a 240-byte frame keeps the `call` 16-aligned and the overflow
585/// area at rsp+256 (= entry_rsp + 8); the alignment push is popped before
586/// `ret`.
587///
588/// # Safety
589///
590/// - `receiver` must be a valid x86_64 SysV function pointer that accepts
591///   `(ctx, msg, ap)` and consumes the va_list built in the fixed stack
592///   slots; the frame layout described above must match the ABI.
593#[cfg(target_arch = "x86_64")]
594unsafe fn legacy_shim(
595    receiver: unsafe extern "C" fn(*mut c_void, *const c_char, *mut VaListTag) -> c_int,
596) -> c_int {
597    unsafe {
598        core::arch::asm!(
599            "sub rsp, 240",
600            "mov [rsp+0], rdi",
601            "mov [rsp+8], rsi",
602            "mov [rsp+16], rdx",
603            "mov [rsp+24], rcx",
604            "mov [rsp+32], r8",
605            "mov [rsp+40], r9",
606            "movaps [rsp+48], xmm0",
607            "movaps [rsp+64], xmm1",
608            "movaps [rsp+80], xmm2",
609            "movaps [rsp+96], xmm3",
610            "movaps [rsp+112], xmm4",
611            "movaps [rsp+128], xmm5",
612            "movaps [rsp+144], xmm6",
613            "movaps [rsp+160], xmm7",
614            "mov dword ptr [rsp+176], 16",
615            "mov dword ptr [rsp+180], 48",
616            "lea rax, [rsp+256]",
617            "mov [rsp+184], rax",
618            "lea rax, [rsp]",
619            "mov [rsp+192], rax",
620            "lea rdx, [rsp+176]",
621            "call {receiver}",
622            "add rsp, 240",
623            "add rsp, 8",
624            "ret",
625            receiver = in(reg) receiver as usize,
626            options(noreturn),
627        );
628    }
629}
630
631// ═══════════════════════════════════════════════════════════════════════════════
632// Generic-channel fragment streaming (upstream error.c `xmlFormatError`)
633// ═══════════════════════════════════════════════════════════════════════════════
634//
635// Upstream streams each error through the generic channel as a sequence of
636// variadic calls (e.g. `channel(data, "%s:%d: ", file, line)` followed by the
637// domain, level, message and source-context fragments). Custom handlers and the
638// built-in default (an x86_64 SysV va_list shim, see data_globals.rs) both
639// observe the same per-fragment calls. Stable Rust cannot express a variadic
640// call, so each fragment goes through a tiny x86_64 trampoline that places the
641// fixed arguments in the ABI registers and does an indirect call.
642
643/// `channel(data, fmt)` — no variadic arguments.
644#[cfg(target_arch = "x86_64")]
645#[inline]
646unsafe fn ch_call0(handler: xmlGenericErrorFunc, data: *mut c_void, fmt: *const c_char) {
647    // SAFETY: `handler` is a C-compatible generic error callback; per the
648    // SysV ABI the callee sees (data, fmt) with no additional registers
649    // consumed (rdx/rcx zeroed so a va_list-reading callee finds nothing).
650    // The compiler guarantees 16-byte stack alignment at the asm block, so
651    // the `call` is correctly aligned.
652    unsafe {
653        core::arch::asm!(
654            "xor edx, edx",
655            "xor ecx, ecx",
656            "call {h}",
657            h = in(reg) handler as usize,
658            in("rdi") data,
659            in("rsi") fmt,
660            out("rdx") _, out("rcx") _,
661            lateout("rax") _, lateout("r8") _, lateout("r9") _, lateout("r10") _, lateout("r11") _,
662        );
663    }
664}
665
666/// `channel(data, fmt, a1)` — one pointer-sized variadic argument.
667#[cfg(target_arch = "x86_64")]
668#[inline]
669unsafe fn ch_call1(handler: xmlGenericErrorFunc, data: *mut c_void, fmt: *const c_char, a1: usize) {
670    // SAFETY: as ch_call0; `a1` lands in the va_list slot after the two
671    // fixed args (rdx).
672    unsafe {
673        core::arch::asm!(
674            "xor ecx, ecx",
675            "call {h}",
676            h = in(reg) handler as usize,
677            in("rdi") data,
678            in("rsi") fmt,
679            in("rdx") a1,
680            out("rcx") _,
681            lateout("rax") _, lateout("r8") _, lateout("r9") _, lateout("r10") _, lateout("r11") _,
682        );
683    }
684}
685
686/// `channel(data, fmt, a1, a2)` — two pointer-sized variadic arguments.
687#[cfg(target_arch = "x86_64")]
688#[inline]
689unsafe fn ch_call2(
690    handler: xmlGenericErrorFunc,
691    data: *mut c_void,
692    fmt: *const c_char,
693    a1: usize,
694    a2: usize,
695) {
696    // SAFETY: as ch_call0; a1/a2 land in the va_list slots (rdx, rcx).
697    unsafe {
698        core::arch::asm!(
699            "call {h}",
700            h = in(reg) handler as usize,
701            in("rdi") data,
702            in("rsi") fmt,
703            in("rdx") a1,
704            in("rcx") a2,
705            lateout("rax") _, lateout("r8") _, lateout("r9") _, lateout("r10") _, lateout("r11") _,
706        );
707    }
708}
709
710/// Emit one raise through the generic channel with upstream's
711/// `xmlFormatError` fragment sequence (error.c 2.15): file/line prefix,
712/// domain, level, message, then the source window and caret line.
713///
714/// `file`/`line` come from the raising site's input; `source_window` is the
715/// current input line text plus the 0-based caret column (upstream
716/// `xmlParserInputGetWindow`).
717///
718/// # SAFETY
719///
720/// - `file` and `message` must be valid C strings or NULL.
721/// - `source_window` bytes must be valid for the duration of the call.
722#[allow(clippy::too_many_arguments)]
723#[cfg(target_arch = "x86_64")]
724unsafe fn format_error_streamed(
725    domain: c_int,
726    code: c_int,
727    level: c_int,
728    message: *const c_char,
729    file: *const c_char,
730    line: c_int,
731    source_window: Option<(&[u8], usize)>,
732    enc_bytes: Option<[u8; 4]>,
733    tail: Option<(c_int, Option<(&[u8], usize)>)>,
734) {
735    // SAFETY: reads the exported C globals (upstream reads the same).
736    let Some(handler) = globals::get_generic_error_func() else {
737        return;
738    };
739    let data = globals::get_generic_error_ctx();
740
741    // 1. File/line prefix (xmlFormatError).
742    if !file.is_null() {
743        ch_call2(
744            handler,
745            data,
746            c"%s:%d: ".as_ptr() as *const c_char,
747            file as usize,
748            line as usize,
749        );
750    } else if line != 0
751        && (domain == XML_FROM_PARSER
752            || domain == XML_FROM_SCHEMASV
753            || domain == XML_FROM_SCHEMASP
754            || domain == XML_FROM_DTD
755            || domain == XML_FROM_RELAXNGP
756            || domain == XML_FROM_RELAXNGV)
757    {
758        ch_call1(
759            handler,
760            data,
761            c"Entity: line %d: ".as_ptr() as *const c_char,
762            line as usize,
763        );
764    }
765
766    // 2. Domain fragment (xmlFormatError switch).
767    let dom: &[u8] = match domain {
768        XML_FROM_PARSER => b"parser \0",
769        XML_FROM_NAMESPACE => b"namespace \0",
770        XML_FROM_DTD | XML_FROM_VALID => b"validity \0",
771        XML_FROM_HTML => b"HTML parser \0",
772        XML_FROM_MEMORY => b"memory \0",
773        XML_FROM_OUTPUT => b"output \0",
774        XML_FROM_IO => b"I/O \0",
775        XML_FROM_XINCLUDE => b"XInclude \0",
776        XML_FROM_XPATH => b"XPath \0",
777        XML_FROM_XPOINTER => b"parser \0",
778        XML_FROM_REGEXP => b"regexp \0",
779        XML_FROM_MODULE => b"module \0",
780        XML_FROM_SCHEMASV => b"Schemas validity \0",
781        XML_FROM_SCHEMASP => b"Schemas parser \0",
782        XML_FROM_RELAXNGP => b"Relax-NG parser \0",
783        XML_FROM_RELAXNGV => b"Relax-NG validity \0",
784        XML_FROM_CATALOG => b"Catalog \0",
785        XML_FROM_C14N => b"C14N \0",
786        XML_FROM_XSLT => b"XSLT \0",
787        XML_FROM_I18N => b"encoding \0",
788        XML_FROM_SCHEMATRONV => b"schematron \0",
789        XML_FROM_BUFFER => b"internal buffer \0",
790        XML_FROM_URI => b"URI \0",
791        _ => b"\0",
792    };
793    if !dom.is_empty() && dom[0] != 0 {
794        ch_call0(handler, data, dom.as_ptr() as *const c_char);
795    }
796
797    // 3. Level fragment (xmlFormatError switch).
798    let lvl: &[u8] = if level == XML_ERR_NONE as c_int {
799        b": \0"
800    } else if level == XML_ERR_WARNING as c_int {
801        b"warning : \0"
802    } else if level == XML_ERR_ERROR as c_int || level == XML_ERR_FATAL as c_int {
803        b"error : \0"
804    } else {
805        b"\0"
806    };
807    if !lvl.is_empty() && lvl[0] != 0 {
808        ch_call0(handler, data, lvl.as_ptr() as *const c_char);
809    }
810
811    // 4. Message fragment.
812    if !message.is_null() {
813        let msg = message as *const u8;
814        let mut len = 0usize;
815        while unsafe { *msg.add(len) } != 0 {
816            len += 1;
817        }
818        let ends_nl = len > 0 && unsafe { *msg.add(len - 1) } == b'\n';
819        let fmt: &[u8] = if ends_nl { b"%s\0" } else { b"%s\n\0" };
820        ch_call1(handler, data, fmt.as_ptr() as *const c_char, msg as usize);
821    }
822
823    // 4b. Invalid-encoding byte dump (upstream xmlFormatError: the first 4
824    // bytes at the error position, only for XML_ERR_INVALID_ENCODING).
825    if code == XML_ERR_INVALID_ENCODING {
826        if let Some(bytes) = enc_bytes {
827            ch_call0(handler, data, c"Bytes:".as_ptr() as *const c_char);
828            for b in bytes {
829                // " 0x%02X"
830                let hex = format!(" 0x{:02X}\0", b);
831                ch_call0(handler, data, hex.as_ptr() as *const c_char);
832            }
833            ch_call0(handler, data, c"\n".as_ptr() as *const c_char);
834        }
835    }
836
837    // 5. Source window + caret (xmlParserPrintFileContextInternal).
838    if let Some((window, caret)) = source_window {
839        let mut win = window.to_vec();
840        win.push(0);
841        ch_call1(
842            handler,
843            data,
844            c"%s\n".as_ptr() as *const c_char,
845            win.as_ptr() as usize,
846        );
847        let mut caret_line = Vec::with_capacity(caret + 2);
848        for &b in window.iter().take(caret) {
849            caret_line.push(if b == b'\t' { b'\t' } else { b' ' });
850        }
851        caret_line.push(b'^');
852        caret_line.push(0);
853        ch_call1(
854            handler,
855            data,
856            c"%s\n".as_ptr() as *const c_char,
857            caret_line.as_ptr() as usize,
858        );
859    }
860
861    // 5b. "cur input" tail (error.c xmlFormatError): after the parent
862    // window, upstream prints the current (entity) input's info + window —
863    // `Entity: line %d: \n` for a nameless nested input, then its context
864    // and caret (HOSTILE-FAILURE F2 entity loops).
865    if let Some((tline, twindow)) = tail {
866        if tline != 0
867            && (domain == XML_FROM_PARSER
868                || domain == XML_FROM_SCHEMASV
869                || domain == XML_FROM_SCHEMASP
870                || domain == XML_FROM_DTD
871                || domain == XML_FROM_RELAXNGP
872                || domain == XML_FROM_RELAXNGV)
873        {
874            ch_call1(
875                handler,
876                data,
877                c"Entity: line %d: \n".as_ptr() as *const c_char,
878                tline as usize,
879            );
880        }
881        if let Some((window, caret)) = twindow {
882            let mut win = window.to_vec();
883            win.push(0);
884            ch_call1(
885                handler,
886                data,
887                c"%s\n".as_ptr() as *const c_char,
888                win.as_ptr() as usize,
889            );
890            let mut caret_line = Vec::with_capacity(caret + 2);
891            for &b in window.iter().take(caret) {
892                caret_line.push(if b == b'\t' { b'\t' } else { b' ' });
893            }
894            caret_line.push(b'^');
895            caret_line.push(0);
896            ch_call1(
897                handler,
898                data,
899                c"%s\n".as_ptr() as *const c_char,
900                caret_line.as_ptr() as usize,
901            );
902        }
903    }
904}
905
906/// How a raise delivers to the generic side of the error system (upstream
907/// `xmlVRaiseError` channel selection, error.c 2.15).
908#[derive(Clone, Copy, Debug)]
909pub enum GenericDelivery {
910    /// Custom SAX channel: single call `channel(ctx, msg)`.
911    Custom(xmlGenericErrorFunc, *mut c_void),
912    /// Legacy/default channel: stream the `xmlFormatError` fragments through
913    /// the global generic handler.
914    Stream,
915    /// No channel (SAX slot NULL): no generic delivery.
916    None,
917}
918
919/// Select the generic delivery for a parser error from the context's SAX
920/// `error` slot (upstream `xmlCtxtVErr`: `channel = ctxt->sax->error`). Used
921/// by the parser layer and by the SAX-layer depth error (HOSTILE-FAILURE F1).
922///
923/// # Safety
924///
925/// - `ctxt` must be a valid `_xmlParserCtxt` with a valid `sax` pointer.
926pub unsafe fn parser_delivery(ctxt: *mut crate::abi::structs::_xmlParserCtxt) -> GenericDelivery {
927    unsafe {
928        let sax = &*((*ctxt).sax);
929        match sax.error {
930            None => GenericDelivery::None,
931            Some(cb) if is_legacy_error_handler(cb) => GenericDelivery::Stream,
932            Some(cb) => GenericDelivery::Custom(cb, (*ctxt).userData),
933        }
934    }
935}
936
937/// Whether a SAX `error` slot holds the candidate's legacy default handler
938/// (the SAX1 shim or the default SAX2 handler) — those route through the
939/// streamed `xmlFormatError` fragments like upstream's `xmlParserError`.
940fn is_legacy_error_handler(cb: errorSAXFunc) -> bool {
941    let ptr = cb as usize;
942    ptr == XML_PARSER_ERROR_SAX1 as errorSAXFunc as usize
943        || ptr == crate::xml::sax::default::default_sax_handler::error as errorSAXFunc as usize
944}
945
946/// Raise an error with upstream's full routing (error.c 2.15
947/// `xmlVRaiseError`): update the last error, then deliver to the structured
948/// handler **or** the selected generic channel — never both.
949///
950/// `file`/`line`/`source_window` feed the generic fragment stream (the
951/// structured handler receives the complete `xmlError` instead). `col` is
952/// the 1-based byte column (upstream `input->col` → `err->int2`); `str1`..
953/// `str3`/`int1` are the upstream extra fields; `enc_bytes` feeds the
954/// `XML_ERR_INVALID_ENCODING` "Bytes:" fragment.
955///
956/// # UPSTREAM-PARITY (ownership)
957///
958/// Like upstream `xmlVSetError`, every string field of the stored error is
959/// owned (`xmlStrdup`): `file`/`str1`/`str2`/`str3` are heap copies, so the
960/// caller may pass transient C strings.
961///
962/// # SAFETY
963///
964/// - `ctxt` may be NULL.
965/// - `msg`, `file`, `str1`, `str2`, `str3` must be valid C strings or NULL.
966/// - `source_window` bytes must be valid for the duration of the call.
967#[allow(clippy::too_many_arguments)]
968pub unsafe fn raise_error_streamed(
969    ctxt: *mut c_void,
970    domain: c_int,
971    code: c_int,
972    level: c_int,
973    file: *const c_char,
974    line: c_int,
975    col: c_int,
976    str1: *const c_char,
977    str2: *const c_char,
978    str3: *const c_char,
979    int1: c_int,
980    msg: *const c_char,
981    source_window: Option<(&[u8], usize)>,
982    enc_bytes: Option<[u8; 4]>,
983    delivery: GenericDelivery,
984    tail: Option<(c_int, Option<(&[u8], usize)>)>,
985) {
986    // The streamed generic-error channel below uses an x86_64 SysV va_list
987    // trampoline (ch_call0/1/2 — register-based). Other ABIs (i686 cdecl,
988    // ARM/aarch64 AAPCS, ...) fall back to the plain raise path; full
989    // streamed-fragment parity there is an unexecuted platform obligation
990    // (atlas/PLATFORM_SURFACE_ATLAS.md, OBLIG-WORDSIZE-32 / compiler-ABI).
991    #[cfg(not(target_arch = "x86_64"))]
992    {
993        raise_error(
994            ctxt,
995            ptr::null_mut(),
996            ptr::null_mut(),
997            ptr::null_mut(),
998            ptr::null_mut(),
999            domain,
1000            code,
1001            level,
1002            file,
1003            line,
1004            str1,
1005            str2,
1006            str3,
1007            int1,
1008            col,
1009            msg,
1010        );
1011        return;
1012    }
1013
1014    #[cfg(target_arch = "x86_64")]
1015    {
1016        // UPSTREAM-PARITY (xmlVRaiseError): warnings are suppressed when the
1017        // TLS global xmlGetWarningsDefaultValue is zero.
1018        if level == xmlErrorLevel::XML_ERR_WARNING as c_int
1019            && crate::xml::globals::get_get_warnings_default() == 0
1020        {
1021            return;
1022        }
1023
1024        raise_error_streamed_x86_64(
1025            ctxt,
1026            domain,
1027            code,
1028            level,
1029            file,
1030            line,
1031            col,
1032            str1,
1033            str2,
1034            str3,
1035            int1,
1036            msg,
1037            source_window,
1038            enc_bytes,
1039            delivery,
1040            tail,
1041        );
1042    }
1043}
1044
1045/// x86-64 streamed raise (SysV va_list channel). See `raise_error_streamed`.
1046///
1047/// # Safety
1048///
1049/// - `ctxt` may be NULL; `msg`, `file`, `str1`, `str2`, `str3` must be
1050///   valid NUL-terminated C strings or NULL; `source_window` bytes must be
1051///   valid for the duration of the call; every string field is duplicated
1052///   before being stored in the thread-local last error.
1053#[allow(clippy::too_many_arguments)]
1054#[cfg(target_arch = "x86_64")]
1055unsafe fn raise_error_streamed_x86_64(
1056    ctxt: *mut c_void,
1057    domain: c_int,
1058    code: c_int,
1059    level: c_int,
1060    file: *const c_char,
1061    line: c_int,
1062    col: c_int,
1063    str1: *const c_char,
1064    str2: *const c_char,
1065    str3: *const c_char,
1066    int1: c_int,
1067    msg: *const c_char,
1068    source_window: Option<(&[u8], usize)>,
1069    enc_bytes: Option<[u8; 4]>,
1070    delivery: GenericDelivery,
1071    tail: Option<(c_int, Option<(&[u8], usize)>)>,
1072) {
1073    // UPSTREAM-PARITY (xmlVRaiseError): warnings are suppressed when the
1074    // TLS global xmlGetWarningsDefaultValue is zero.
1075    if level == xmlErrorLevel::XML_ERR_WARNING as c_int
1076        && crate::xml::globals::get_get_warnings_default() == 0
1077    {
1078        return;
1079    }
1080
1081    // Format the error message (same as raise_error).
1082    let formatted_msg = format_error_message(domain, code, msg, str1, str2, str3);
1083    let file_copy = if file.is_null() {
1084        ptr::null_mut()
1085    } else {
1086        crate::abi::allocator::xmlMemStrdupImpl(file) as *mut c_char
1087    };
1088    let str1_copy = if str1.is_null() {
1089        ptr::null_mut()
1090    } else {
1091        crate::abi::allocator::xmlMemStrdupImpl(str1) as *mut c_char
1092    };
1093    let str2_copy = if str2.is_null() {
1094        ptr::null_mut()
1095    } else {
1096        crate::abi::allocator::xmlMemStrdupImpl(str2) as *mut c_char
1097    };
1098    let str3_copy = if str3.is_null() {
1099        ptr::null_mut()
1100    } else {
1101        crate::abi::allocator::xmlMemStrdupImpl(str3) as *mut c_char
1102    };
1103    let err = _xmlError {
1104        domain,
1105        code,
1106        message: formatted_msg,
1107        level,
1108        file: file_copy,
1109        line,
1110        str1: str1_copy,
1111        str2: str2_copy,
1112        str3: str3_copy,
1113        int1,
1114        int2: col,
1115        ctxt,
1116        node: ptr::null_mut(),
1117    };
1118
1119    globals::set_last_error(err);
1120
1121    // Structured handler wins (upstream `else if` chain); the (handler, ctx)
1122    // pair is read atomically and invoked outside the lock (11.1-X).
1123    let structured = globals::with_structured_error(|h, c| (h, c));
1124    if let Some(handler) = structured.0 {
1125        let err_ref = globals::get_last_error();
1126        if !err_ref.is_null() {
1127            handler(structured.1, err_ref as *const _xmlError);
1128        }
1129        return;
1130    }
1131
1132    match delivery {
1133        GenericDelivery::Custom(channel, ctx) => {
1134            if !msg.is_null() {
1135                // SAFETY: the caller provided a valid C callback.
1136                unsafe { channel(ctx, msg) };
1137            }
1138        }
1139        GenericDelivery::Stream => {
1140            if globals::get_generic_error_func().is_some() {
1141                unsafe {
1142                    format_error_streamed(
1143                        domain,
1144                        code,
1145                        level,
1146                        formatted_msg,
1147                        file,
1148                        line,
1149                        source_window,
1150                        enc_bytes,
1151                        tail,
1152                    )
1153                };
1154            }
1155        }
1156        GenericDelivery::None => {}
1157    }
1158}
1159
1160/// Default SAX v1 error handler — upstream `void xmlParserError(void *ctx,
1161/// const char *msg, ...)`. Variadic x86_64 SysV shim (11.1-Z.2, R-000176:
1162/// the previous fixed-arity body silently dropped the varargs; upstream
1163/// error.c formats them via `xmlVFormatLegacyError`).
1164///
1165/// 2 fixed args (ctx=rdi, msg=rsi) → `gp_offset` 16; the va_list pointer is
1166/// passed as the 3rd arg (rdx) to the `xmlParserErrorV` receiver.
1167///
1168/// # SAFETY
1169///
1170/// - `ctx` may be NULL (unused by the candidate's legacy path).
1171/// - `msg` must be a valid NUL-terminated printf format string.
1172#[cfg(target_arch = "x86_64")]
1173#[no_mangle]
1174pub unsafe extern "C" fn xmlParserError() -> c_int {
1175    unsafe { legacy_shim(xmlParserErrorV) }
1176}
1177
1178/// Variadic receiver for the `xmlParserError` shim: formats `msg` with the
1179/// caller's varargs and emits `"error: "` + formatted text through the
1180/// generic channel (upstream error.c `xmlVFormatLegacyError`).
1181///
1182/// # Safety
1183///
1184/// - `msg` must be a valid NUL-terminated printf format string, `ap` a
1185///   valid va_list matching the format, `ctx` may be NULL.
1186#[no_mangle]
1187pub unsafe extern "C" fn xmlParserErrorV(
1188    ctx: *mut c_void,
1189    msg: *const c_char,
1190    ap: *mut VaListTag,
1191) -> c_int {
1192    let _ = ctx;
1193    unsafe { emit_legacy_message_v("error", msg, ap) }
1194}
1195
1196/// Default SAX v1 warning handler — upstream `void xmlParserWarning(void
1197/// *ctx, const char *msg, ...)`. Variadic shim as `xmlParserError`.
1198///
1199/// # SAFETY
1200///
1201/// - `ctx` may be NULL (unused by the candidate's legacy path).
1202/// - `msg` must be a valid NUL-terminated printf format string.
1203#[cfg(target_arch = "x86_64")]
1204#[no_mangle]
1205pub unsafe extern "C" fn xmlParserWarning() -> c_int {
1206    unsafe { legacy_shim(xmlParserWarningV) }
1207}
1208
1209/// Variadic receiver for the `xmlParserWarning` shim.
1210///
1211/// # Safety
1212///
1213/// - `msg` must be a valid NUL-terminated printf format string, `ap` a
1214///   valid va_list matching the format, `ctx` may be NULL.
1215#[no_mangle]
1216pub unsafe extern "C" fn xmlParserWarningV(
1217    ctx: *mut c_void,
1218    msg: *const c_char,
1219    ap: *mut VaListTag,
1220) -> c_int {
1221    let _ = ctx;
1222    unsafe { emit_legacy_message_v("warning", msg, ap) }
1223}
1224
1225/// Default validity error handler — upstream `void
1226/// xmlParserValidityError(void *ctx, const char *msg, ...)`. Variadic shim
1227/// as `xmlParserError`.
1228///
1229/// # SAFETY
1230///
1231/// - `ctx` may be NULL (unused by the candidate's legacy path).
1232/// - `msg` must be a valid NUL-terminated printf format string.
1233#[cfg(target_arch = "x86_64")]
1234#[no_mangle]
1235pub unsafe extern "C" fn xmlParserValidityError() -> c_int {
1236    unsafe { legacy_shim(xmlParserValidityErrorV) }
1237}
1238
1239/// Variadic receiver for the `xmlParserValidityError` shim.
1240///
1241/// # Safety
1242///
1243/// - `msg` must be a valid NUL-terminated printf format string, `ap` a
1244///   valid va_list matching the format, `ctx` may be NULL.
1245#[no_mangle]
1246pub unsafe extern "C" fn xmlParserValidityErrorV(
1247    ctx: *mut c_void,
1248    msg: *const c_char,
1249    ap: *mut VaListTag,
1250) -> c_int {
1251    let _ = ctx;
1252    unsafe { emit_legacy_message_v("validity error", msg, ap) }
1253}
1254
1255/// Default validity warning handler — upstream `void
1256/// xmlParserValidityWarning(void *ctx, const char *msg, ...)`. Variadic
1257/// shim as `xmlParserError`.
1258///
1259/// # SAFETY
1260///
1261/// - `ctx` may be NULL (unused by the candidate's legacy path).
1262/// - `msg` must be a valid NUL-terminated printf format string.
1263#[cfg(target_arch = "x86_64")]
1264#[no_mangle]
1265pub unsafe extern "C" fn xmlParserValidityWarning() -> c_int {
1266    unsafe { legacy_shim(xmlParserValidityWarningV) }
1267}
1268
1269/// Variadic receiver for the `xmlParserValidityWarning` shim.
1270///
1271/// # Safety
1272///
1273/// - `msg` must be a valid NUL-terminated printf format string, `ap` a
1274///   valid va_list matching the format, `ctx` may be NULL.
1275#[no_mangle]
1276pub unsafe extern "C" fn xmlParserValidityWarningV(
1277    ctx: *mut c_void,
1278    msg: *const c_char,
1279    ap: *mut VaListTag,
1280) -> c_int {
1281    let _ = ctx;
1282    unsafe { emit_legacy_message_v("validity warning", msg, ap) }
1283}
1284
1285/// The variadic `xmlParserError` shim, transmuted to the fixed-arity SAX v1
1286/// callback type. The shim's declared arity is a Rust-side fiction (stable
1287/// Rust cannot express `c_variadic`); the ABI is a plain code pointer and
1288/// the C variadic call contract is preserved (11.1-Z.2, R-000176).
1289pub const XML_PARSER_ERROR_SAX1: errorSAXFunc = unsafe {
1290    // SAFETY: shim and SAX callback are both plain code pointers (same ABI);
1291    // the declared arity difference is the documented shim fiction.
1292    core::mem::transmute::<unsafe extern "C" fn() -> c_int, errorSAXFunc>(xmlParserError)
1293};
1294
1295/// The variadic `xmlParserWarning` shim as the SAX v1 callback type.
1296pub const XML_PARSER_WARNING_SAX1: errorSAXFunc = unsafe {
1297    // SAFETY: see XML_PARSER_ERROR_SAX1.
1298    core::mem::transmute::<unsafe extern "C" fn() -> c_int, errorSAXFunc>(xmlParserWarning)
1299};
1300
1301/// The variadic `xmlParserValidityError` shim as the validation callback
1302/// type (`xmlValidityErrorFunc`-compatible fixed-arity pointer).
1303pub const XML_PARSER_VALIDITY_ERROR_SAX1: unsafe extern "C" fn(*mut c_void, *const c_char) =
1304    // SAFETY: see XML_PARSER_ERROR_SAX1.
1305    unsafe {
1306        core::mem::transmute::<
1307            unsafe extern "C" fn() -> c_int,
1308            unsafe extern "C" fn(*mut c_void, *const c_char),
1309        >(xmlParserValidityError)
1310    };
1311
1312/// The variadic `xmlParserValidityWarning` shim as the validation callback
1313/// type.
1314pub const XML_PARSER_VALIDITY_WARNING_SAX1: unsafe extern "C" fn(*mut c_void, *const c_char) =
1315    // SAFETY: see XML_PARSER_ERROR_SAX1.
1316    unsafe {
1317        core::mem::transmute::<
1318            unsafe extern "C" fn() -> c_int,
1319            unsafe extern "C" fn(*mut c_void, *const c_char),
1320        >(xmlParserValidityWarning)
1321    };
1322
1323// ═══════════════════════════════════════════════════════════════════════════════
1324// Tests
1325// ═══════════════════════════════════════════════════════════════════════════════
1326
1327#[cfg(test)]
1328mod tests {
1329    use super::*;
1330    use crate::abi::allocator;
1331    use core::ffi::c_void;
1332
1333    /// Reset an error struct and verify the defaults are applied.
1334    ///
1335    /// # Safety
1336    ///
1337    /// - `err` is a stack `_xmlError` whose fields are all NULL/zero; it
1338    ///   is valid for `reset_error` to write and for the subsequent reads.
1339    #[test]
1340    fn test_error_default_reset() {
1341        unsafe {
1342            let mut err = _xmlError {
1343                domain: XML_FROM_PARSER,
1344                code: XML_ERR_NO_MEMORY,
1345                message: ptr::null_mut(),
1346                level: XML_ERR_ERROR as c_int,
1347                file: ptr::null_mut(),
1348                line: 42,
1349                str1: ptr::null_mut(),
1350                str2: ptr::null_mut(),
1351                str3: ptr::null_mut(),
1352                int1: 0,
1353                int2: 0,
1354                ctxt: ptr::null_mut(),
1355                node: ptr::null_mut(),
1356            };
1357
1358            reset_error(&mut err);
1359            assert_eq!(err.domain, XML_FROM_NONE);
1360            assert_eq!(err.code, XML_ERR_OK as c_int);
1361            assert_eq!(err.level, XML_ERR_NONE as c_int);
1362            assert_eq!(err.line, 0);
1363        }
1364    }
1365
1366    /// Copy one error struct into another and verify the fields.
1367    ///
1368    /// # Safety
1369    ///
1370    /// - `from` and `to` are stack `_xmlError` structs valid for the
1371    ///   `copy_error` copy and the subsequent field reads.
1372    #[test]
1373    fn test_copy_error() {
1374        unsafe {
1375            let from = _xmlError {
1376                domain: XML_FROM_PARSER,
1377                code: XML_ERR_NO_MEMORY,
1378                message: ptr::null_mut(),
1379                level: XML_ERR_FATAL as c_int,
1380                file: ptr::null_mut(),
1381                line: 100,
1382                str1: ptr::null_mut(),
1383                str2: ptr::null_mut(),
1384                str3: ptr::null_mut(),
1385                int1: 1,
1386                int2: 2,
1387                ctxt: ptr::null_mut(),
1388                node: ptr::null_mut(),
1389            };
1390            let mut to = _xmlError {
1391                domain: XML_FROM_NONE,
1392                code: XML_ERR_OK as c_int,
1393                message: ptr::null_mut(),
1394                level: XML_ERR_NONE as c_int,
1395                file: ptr::null_mut(),
1396                line: 0,
1397                str1: ptr::null_mut(),
1398                str2: ptr::null_mut(),
1399                str3: ptr::null_mut(),
1400                int1: 0,
1401                int2: 0,
1402                ctxt: ptr::null_mut(),
1403                node: ptr::null_mut(),
1404            };
1405
1406            let result = copy_error(&from, &mut to);
1407            assert_eq!(result, 0);
1408            assert_eq!(to.domain, XML_FROM_PARSER);
1409            assert_eq!(to.code, XML_ERR_NO_MEMORY);
1410            assert_eq!(to.level, XML_ERR_FATAL as c_int);
1411            assert_eq!(to.line, 100);
1412            assert_eq!(to.int1, 1);
1413            assert_eq!(to.int2, 2);
1414        }
1415    }
1416
1417    /// Raise an error and verify it is stored as the last error.
1418    ///
1419    /// # Safety
1420    ///
1421    /// - `file`/`str1` are static NUL-terminated strings valid for the
1422    ///   raise; `raise_error` duplicates them, so the test's later reads of
1423    ///   `last` only touch the thread-local copy; `reset_last_error`
1424    ///   releases the owned strings exactly once.
1425    #[test]
1426    fn test_raise_and_get_last_error() {
1427        unsafe {
1428            reset_last_error();
1429            assert!(get_last_error().is_null());
1430
1431            let file = b"test.xml\0" as *const u8 as *const c_char;
1432            let str1 = b"element\0" as *const u8 as *const c_char;
1433
1434            raise_error(
1435                ptr::null_mut(),
1436                ptr::null_mut(),
1437                ptr::null_mut(),
1438                ptr::null_mut(),
1439                ptr::null_mut(),
1440                XML_FROM_PARSER,
1441                XML_ERR_TAG_NAME_MISMATCH,
1442                XML_ERR_ERROR as c_int,
1443                file,
1444                10,
1445                str1,
1446                ptr::null(),
1447                ptr::null(),
1448                0,
1449                0,
1450                ptr::null(),
1451            );
1452
1453            let last = get_last_error();
1454            assert!(!last.is_null());
1455            assert_eq!((*last).domain, XML_FROM_PARSER);
1456            assert_eq!((*last).code, XML_ERR_TAG_NAME_MISMATCH);
1457            assert_eq!((*last).level, XML_ERR_ERROR as c_int);
1458            assert_eq!((*last).line, 10);
1459
1460            // Check file was stored
1461            let last_file = (*last).file;
1462            assert!(!last_file.is_null());
1463
1464            reset_last_error();
1465            assert!(get_last_error().is_null());
1466        }
1467    }
1468
1469    #[test]
1470    fn test_structured_error_callback() {
1471        // Serialized against the handler-slot tests in xml::globals (11.1-X):
1472        // the structured handler slot is shared global state.
1473        let _guard = crate::xml::globals::ERROR_HANDLER_TEST_LOCK.lock();
1474        unsafe {
1475            reset_last_error();
1476
1477            // Set up a structured error handler that captures the error
1478            let mut captured_domain: c_int = 0;
1479            let captured_ptr = &mut captured_domain as *mut c_int as *mut c_void;
1480
1481            // SAFETY: The callback writes to captured_ptr which lives on the stack
1482            // for the duration of this test.
1483            extern "C" fn test_handler(ctx: *mut c_void, _err: *const _xmlError) {
1484                // SAFETY: ctx is valid for the test duration.
1485                unsafe {
1486                    let captured = &mut *(ctx as *mut c_int);
1487                    *captured = 42;
1488                }
1489            }
1490
1491            set_structured_error_func(captured_ptr, Some(test_handler as xmlStructuredErrorFunc));
1492
1493            raise_error(
1494                ptr::null_mut(),
1495                ptr::null_mut(),
1496                ptr::null_mut(),
1497                ptr::null_mut(),
1498                ptr::null_mut(),
1499                XML_FROM_PARSER,
1500                XML_ERR_OK as c_int,
1501                XML_ERR_WARNING as c_int,
1502                ptr::null(),
1503                0,
1504                ptr::null(),
1505                ptr::null(),
1506                ptr::null(),
1507                0,
1508                0,
1509                ptr::null(),
1510            );
1511
1512            assert_eq!(captured_domain, 42);
1513
1514            // Reset
1515            set_structured_error_func(ptr::null_mut(), None);
1516            reset_last_error();
1517        }
1518    }
1519
1520    /// Format messages with a direct `msg` and with domain plus `str1`.
1521    ///
1522    /// # Safety
1523    ///
1524    /// - `msg`/`str1` are static NUL-terminated strings valid for the
1525    ///   calls; each returned buffer is allocator-owned and freed with
1526    ///   `xmlFreeImpl` exactly once before the test ends.
1527    #[test]
1528    fn test_format_error_message() {
1529        unsafe {
1530            // Test with direct message
1531            let msg = b"test error\0" as *const u8 as *const c_char;
1532            let formatted = format_error_message(
1533                XML_FROM_NONE,
1534                XML_ERR_OK as c_int,
1535                msg,
1536                ptr::null(),
1537                ptr::null(),
1538                ptr::null(),
1539            );
1540            assert!(!formatted.is_null());
1541            let formatted_str = std::ffi::CStr::from_ptr(formatted);
1542            assert_eq!(formatted_str.to_bytes(), b"test error");
1543
1544            // Free the allocated message
1545            allocator::xmlFreeImpl(formatted as *mut c_void);
1546
1547            // Test with domain and str1
1548            let str1 = b"foo\0" as *const u8 as *const c_char;
1549            let formatted2 = format_error_message(
1550                XML_FROM_PARSER,
1551                XML_ERR_OK as c_int,
1552                ptr::null(),
1553                str1,
1554                ptr::null(),
1555                ptr::null(),
1556            );
1557            assert!(!formatted2.is_null());
1558            allocator::xmlFreeImpl(formatted2 as *mut c_void);
1559        }
1560    }
1561}