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 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    // UPSTREAM-PARITY (error.c xmlCopyError): every string field is
165    // deep-copied with xmlStrdup — the destination owns its strings
166    // independently of the source. PHP's libxml error list (php_list_set_
167    // error_structure -> xmlCopyError) stores these copies at raise time and
168    // reads them later; a shallow copy would leave list entries pointing at
169    // the parser context's lastError strings, which the NEXT raise frees
170    // (free_error_strings) — the recorded message then reads as garbage
171    // (php bug64230: "Internal: uY\uFFFD\uFFFDU").
172    unsafe {
173        let dup = |p: *const c_char| -> *mut c_char {
174            if p.is_null() {
175                ptr::null_mut()
176            } else {
177                crate::abi::allocator::xmlMemStrdupImpl(p) as *mut c_char
178            }
179        };
180        let src = &*from;
181        let message = if src.message.is_null() {
182            ptr::null_mut()
183        } else {
184            crate::abi::allocator::xmlMemStrdupImpl(src.message as *const c_char) as *mut c_char
185        };
186        let file = if src.file.is_null() {
187            ptr::null_mut()
188        } else {
189            crate::abi::allocator::xmlMemStrdupImpl(src.file) as *mut c_char
190        };
191        let str1 = if src.str1.is_null() {
192            ptr::null_mut()
193        } else {
194            crate::abi::allocator::xmlMemStrdupImpl(src.str1) as *mut c_char
195        };
196        let str2 = if src.str2.is_null() {
197            ptr::null_mut()
198        } else {
199            crate::abi::allocator::xmlMemStrdupImpl(src.str2) as *mut c_char
200        };
201        let str3 = if src.str3.is_null() {
202            ptr::null_mut()
203        } else {
204            crate::abi::allocator::xmlMemStrdupImpl(src.str3) as *mut c_char
205        };
206        ptr::write(
207            to,
208            _xmlError {
209                domain: src.domain,
210                code: src.code,
211                message,
212                level: src.level,
213                file,
214                line: src.line,
215                str1,
216                str2,
217                str3,
218                int1: src.int1,
219                int2: src.int2,
220                ctxt: src.ctxt,
221                node: src.node,
222            },
223        );
224    }
225    0
226}
227
228/// Reset an error structure to its default state.
229///
230/// # UPSTREAM-PARITY
231///
232/// ```c
233/// void xmlResetError(xmlErrorPtr err);
234/// ```
235///
236/// # SAFETY
237///
238/// - `err` must be a valid pointer to `_xmlError`, or NULL.
239pub const unsafe fn reset_error(err: *mut _xmlError) {
240    if err.is_null() {
241        return;
242    }
243    // SAFETY: Caller guarantees pointer is valid.
244    unsafe {
245        ptr::write(
246            err,
247            _xmlError {
248                domain: XML_FROM_NONE,
249                code: XML_ERR_OK as c_int,
250                message: ptr::null_mut(),
251                level: XML_ERR_NONE as c_int,
252                file: ptr::null_mut(),
253                line: 0,
254                str1: ptr::null_mut(),
255                str2: ptr::null_mut(),
256                str3: ptr::null_mut(),
257                int1: 0,
258                int2: 0,
259                ctxt: ptr::null_mut(),
260                node: ptr::null_mut(),
261            },
262        );
263    }
264}
265
266/// Reset the last error for the current thread.
267///
268/// # UPSTREAM-PARITY
269///
270/// ```c
271/// void xmlResetLastError(void);
272/// ```
273pub fn reset_last_error() {
274    globals::reset_last_error();
275}
276
277/// Format an error message.
278///
279/// This function creates a formatted error message from the component parts.
280/// In Phase 1, this is a basic implementation. In Phase 2+, variadic
281/// printf-style formatting will be added.
282///
283/// Returns a C string pointer (allocated with xmlMalloc) that the caller
284/// must free with xmlFreeImpl, or NULL on allocation failure.
285///
286/// # UPSTREAM-PARITY
287///
288/// Upstream libxml2 uses `vsnprintf` internally for message formatting.
289/// We use a simple formatting approach that produces compatible output
290/// for the common error patterns.
291///
292/// # Safety
293///
294/// - `msg` must be NULL or a valid NUL-terminated C string; `str1`, `str2`
295///   and `str3` must be NULL or valid NUL-terminated C strings; the
296///   returned buffer is allocator-owned and must be freed with
297///   `xmlFreeImpl`, or is NULL on allocation failure.
298pub fn format_error_message(
299    _domain: c_int,
300    _code: c_int,
301    msg: *const c_char,
302    str1: *const c_char,
303    str2: *const c_char,
304    str3: *const c_char,
305) -> *mut c_char {
306    // Phase 1: basic message construction.
307    // If a direct message string is provided, use it.
308    if !msg.is_null() {
309        // SAFETY: Caller guarantees msg is a valid C string.
310        let msg_str = unsafe { crate::abi::allocator::xmlMemStrdupImpl(msg) };
311        return msg_str as *mut c_char;
312    }
313
314    // Build a message from the component strings.
315    // This matches upstream behavior where domain/code are combined
316    // with str1/str2/str3 into a diagnostic message.
317    let mut buf: [u8; 1024] = [0; 1024];
318    let mut pos = 0;
319
320    // Write domain prefix
321    let domain_str = match _domain {
322        XML_FROM_PARSER => "parser",
323        XML_FROM_TREE => "tree",
324        XML_FROM_NAMESPACE => "namespace",
325        XML_FROM_DTD => "dtd",
326        XML_FROM_HTML => "html",
327        XML_FROM_MEMORY => "memory",
328        XML_FROM_OUTPUT => "output",
329        XML_FROM_IO => "io",
330        XML_FROM_XPATH => "xpath",
331        XML_FROM_XPOINTER => "xpointer",
332        XML_FROM_XINCLUDE => "xinclude",
333        XML_FROM_CATALOG => "catalog",
334        XML_FROM_C14N => "c14n",
335        XML_FROM_XSLT => "xslt",
336        XML_FROM_VALID => "valid",
337        XML_FROM_CHECK => "check",
338        XML_FROM_WRITER => "writer",
339        XML_FROM_MODULE => "module",
340        XML_FROM_I18N => "i18n",
341        XML_FROM_SCHEMATRONV => "schematron",
342        XML_FROM_BUFFER => "buffer",
343        XML_FROM_URI => "uri",
344        XML_FROM_NONE => "",
345        XML_FROM_FTP => "ftp",
346        XML_FROM_HTTP => "http",
347        XML_FROM_REGEXP => "regexp",
348        XML_FROM_DATATYPE => "datatype",
349        XML_FROM_SCHEMASP => "schema parser",
350        XML_FROM_SCHEMASV => "schema validator",
351        XML_FROM_RELAXNGP => "relaxng parser",
352        XML_FROM_RELAXNGV => "relaxng validator",
353        _ => "unknown",
354    };
355
356    if !domain_str.is_empty() {
357        let bytes = domain_str.as_bytes();
358        let len = bytes.len().min(buf.len().saturating_sub(pos + 2));
359        buf[pos..pos + len].copy_from_slice(&bytes[..len]);
360        pos += len;
361        buf[pos] = b' ';
362        pos += 1;
363    }
364
365    // Append str1 if present
366    if !str1.is_null() {
367        // SAFETY: Caller guarantees str1 is a valid C string.
368        let s = unsafe { crate::abi::versioning::c_str_to_bytes(str1).unwrap_or_default() };
369        if pos + s.len() + 3 <= buf.len() {
370            buf[pos] = b'\'';
371            pos += 1;
372            buf[pos..pos + s.len()].copy_from_slice(s);
373            pos += s.len();
374            buf[pos] = b'\'';
375            pos += 1;
376            buf[pos] = b' ';
377            pos += 1;
378        }
379    }
380
381    // Append str2 if present
382    if !str2.is_null() {
383        let s = unsafe { crate::abi::versioning::c_str_to_bytes(str2).unwrap_or_default() };
384        if pos + s.len() + 3 <= buf.len() {
385            buf[pos] = b'\'';
386            pos += 1;
387            buf[pos..pos + s.len()].copy_from_slice(s);
388            pos += s.len();
389            buf[pos] = b'\'';
390            pos += 1;
391            buf[pos] = b' ';
392            pos += 1;
393        }
394    }
395
396    // Append str3 if present
397    if !str3.is_null() {
398        let s = unsafe { crate::abi::versioning::c_str_to_bytes(str3).unwrap_or_default() };
399        if pos + s.len() + 3 <= buf.len() {
400            buf[pos] = b'\'';
401            pos += 1;
402            buf[pos..pos + s.len()].copy_from_slice(s);
403            pos += s.len();
404            buf[pos] = b'\'';
405            pos += 1;
406            buf[pos] = b' ';
407            pos += 1;
408        }
409    }
410
411    // Null-terminate
412    if pos < buf.len() {
413        buf[pos] = 0;
414    } else {
415        buf[buf.len() - 1] = 0;
416    }
417
418    // Allocate and return
419    let result = unsafe { crate::abi::allocator::xmlMallocImpl(pos + 1) };
420    if result.is_null() {
421        return ptr::null_mut();
422    }
423    unsafe {
424        ptr::copy_nonoverlapping(buf.as_ptr(), result as *mut u8, pos + 1);
425    }
426    result as *mut c_char
427}
428
429/// Raise an error — the central error reporting function.
430///
431/// This is called internally when an error occurs. It:
432/// 1. Updates the thread-local last error
433/// 2. Invokes the structured error handler if one is set
434/// 3. Invokes the generic error handler if one is set (for warnings/errors)
435///
436/// # UPSTREAM-PARITY
437///
438/// ```c
439/// void xmlRaiseError(xmlErrorPtr ctxt,
440///                    xmlErrorPtr ctxt2,
441///                    xmlErrorPtr ctxt3,
442///                    xmlErrorPtr ctxt4,
443///                    xmlErrorPtr ctxt5,
444///                    int domain,
445///                    int code,
446///                    xmlErrorLevel level,
447///                    const char *file,
448///                    int line,
449///                    const char *str1,
450///                    const char *str2,
451///                    const char *str3,
452///                    int int1,
453///                    int int2,
454///                    const char *msg,
455///                    ...);
456/// ```
457///
458/// # SAFETY
459///
460/// - `ctxt` may be NULL (context of the error).
461/// - `domain`, `code`, `level`: valid error codes.
462/// - `msg` must be a valid C string or NULL.
463/// - `file` must be a valid C string or NULL.
464/// - `str1`, `str2`, `str3`: error-related strings (may be NULL).
465#[allow(clippy::too_many_arguments)]
466pub unsafe fn raise_error(
467    ctxt: *mut c_void,
468    _ctxt2: *mut c_void,
469    _ctxt3: *mut c_void,
470    _ctxt4: *mut c_void,
471    _ctxt5: *mut c_void,
472    domain: c_int,
473    code: c_int,
474    level: c_int,
475    file: *const c_char,
476    line: c_int,
477    str1: *const c_char,
478    str2: *const c_char,
479    str3: *const c_char,
480    int1: c_int,
481    _int2: c_int,
482    msg: *const c_char,
483) {
484    // Format the error message
485    let formatted_msg = format_error_message(domain, code, msg, str1, str2, str3);
486
487    // UPSTREAM-PARITY (xmlVSetError): string fields are owned copies so the
488    // stored error survives transient callers.
489    let file_copy = if file.is_null() {
490        ptr::null_mut()
491    } else {
492        crate::abi::allocator::xmlMemStrdupImpl(file) as *mut c_char
493    };
494    let str1_copy = if str1.is_null() {
495        ptr::null_mut()
496    } else {
497        crate::abi::allocator::xmlMemStrdupImpl(str1) as *mut c_char
498    };
499    let str2_copy = if str2.is_null() {
500        ptr::null_mut()
501    } else {
502        crate::abi::allocator::xmlMemStrdupImpl(str2) as *mut c_char
503    };
504    let str3_copy = if str3.is_null() {
505        ptr::null_mut()
506    } else {
507        crate::abi::allocator::xmlMemStrdupImpl(str3) as *mut c_char
508    };
509
510    // Store the last error
511    let err = _xmlError {
512        domain,
513        code,
514        message: formatted_msg,
515        level,
516        file: file_copy,
517        line,
518        str1: str1_copy,
519        str2: str2_copy,
520        str3: str3_copy,
521        int1,
522        int2: 0,
523        ctxt,
524        node: ptr::null_mut(),
525    };
526
527    globals::set_last_error(err);
528
529    // UPSTREAM-PARITY (xmlCtxtVErr): the structured handler wins; the
530    // generic channel is only used when no structured handler is set. The
531    // (handler, ctx) pairs are read atomically (11.1-X), then invoked
532    // outside the lock so a handler that re-enters the library cannot
533    // deadlock.
534    let structured = globals::with_structured_error(|h, c| (h, c));
535    if let Some(handler) = structured.0 {
536        let err_ref = globals::get_last_error();
537        if !err_ref.is_null() {
538            handler(structured.1, err_ref as *const _xmlError);
539        }
540    } else if level != 0 {
541        let generic = globals::with_generic_error(|h, c| (h, c));
542        if let Some(handler) = generic.0 {
543            let ctx = generic.1;
544            if !formatted_msg.is_null() {
545                handler(ctx, formatted_msg as *const core::ffi::c_char);
546            } else if !msg.is_null() {
547                handler(ctx, msg);
548            }
549        }
550    }
551
552    // Free the formatted message if it was allocated
553    // Note: We keep it as the last error's message, so we don't free it here.
554    // The next call to raise_error or reset_error will free the old message.
555    // Actually, in Phase 1, we don't free because the message is the last error's.
556    // A more complete implementation would free the old message when setting a new one.
557}
558
559/// Emit a legacy-format message through the generic error channel.
560///
561/// Upstream's `xmlGenericErrorDefaultFunc` writes the message to stderr;
562/// when a custom generic handler is installed it receives the message. The
563/// `level` prefix matches upstream `xmlVFormatLegacyError` (error.c 2.15).
564unsafe fn emit_legacy_message(level: &str, msg: *const c_char) {
565    if msg.is_null() {
566        return;
567    }
568    let len = libc::strlen(msg) as usize;
569    let text = core::slice::from_raw_parts(msg as *const u8, len);
570    let mut full = Vec::with_capacity(level.len() + 2 + len);
571    full.extend_from_slice(level.as_bytes());
572    full.push(b':');
573    full.push(b' ');
574    full.extend_from_slice(text);
575    if let Some(handler) = globals::get_generic_error_func() {
576        let ctx = globals::get_generic_error_ctx();
577        let mut cmsg = full.clone();
578        cmsg.push(0);
579        handler(ctx, cmsg.as_ptr() as *const c_char);
580    } else {
581        // Upstream default (xmlGenericErrorDefaultFunc): stderr.
582        let _ = libc::write(2, full.as_ptr() as *const libc::c_void, full.len());
583    }
584}
585
586/// System V AMD64 `__va_list_tag` (24 bytes) — same layout as the writer's
587/// shims and `data_globals.rs`.
588#[cfg(target_arch = "x86_64")]
589#[repr(C)]
590#[derive(Clone, Copy, Debug)]
591pub struct VaListTag {
592    gp_offset: c_uint,
593    fp_offset: c_uint,
594    overflow_arg_area: *mut c_void,
595    reg_save_area: *mut c_void,
596}
597
598#[cfg(target_arch = "x86_64")]
599unsafe extern "C" {
600    /// Format `msg` with the caller's va_list into `s` (libc `vsnprintf`).
601    ///
602    /// # Safety
603    ///
604    /// - `s` must point to a writable buffer of at least `n` bytes;
605    ///   `format` must be a valid NUL-terminated printf format string;
606    ///   `ap` must be a valid va_list matching the format's specifiers.
607    fn vsnprintf(s: *mut c_char, n: usize, format: *const c_char, ap: *mut VaListTag) -> c_int;
608}
609
610/// Format `msg` with the caller's varargs and emit `level + ": "` + the
611/// formatted text through the generic channel (upstream error.c
612/// `xmlVFormatLegacyError`: `xmlGenericError(ctx, "%s: ", level)` then the
613/// `xmlStrVASPrintf`-formatted message).
614///
615/// # Safety
616///
617/// - `msg` must be NULL or a valid NUL-terminated printf format string;
618///   `ap` must be a valid va_list matching the format's specifiers; the
619///   format is consumed exactly once.
620#[cfg(target_arch = "x86_64")]
621unsafe fn emit_legacy_message_v(level: &str, msg: *const c_char, ap: *mut VaListTag) -> c_int {
622    if msg.is_null() {
623        return 0;
624    }
625    let mut buf = [0 as c_char; 4096];
626    let n = unsafe { vsnprintf(buf.as_mut_ptr(), buf.len(), msg, ap) };
627    let n = n.clamp(0, buf.len() as c_int - 1) as usize;
628    buf[n] = 0;
629    unsafe { emit_legacy_message(level, buf.as_ptr()) };
630    0
631}
632
633/// x86_64 SysV variadic shim: captures the register save area exactly like
634/// `va_start` (2 fixed args → `gp_offset` 16), builds the va_list at
635/// rsp+176, passes it as the 3rd argument (rdx) to `receiver`, then restores
636/// the stack and returns. Same technique as `xmlStrPrintf`
637/// (exports_string.rs) and `xsltTransformError` (exports_xslt_util.rs).
638///
639/// Layout: reg_save_area = rsp+0 (6 GP + 8 SSE slots, 176 bytes); the
640/// va_list struct lives at rsp+176; overflow varargs are above the return
641/// address; a 240-byte frame keeps the `call` 16-aligned and the overflow
642/// area at rsp+256 (= entry_rsp + 8); the alignment push is popped before
643/// `ret`.
644///
645/// # Safety
646///
647/// - `receiver` must be a valid x86_64 SysV function pointer that accepts
648///   `(ctx, msg, ap)` and consumes the va_list built in the fixed stack
649///   slots; the frame layout described above must match the ABI.
650#[cfg(target_arch = "x86_64")]
651unsafe fn legacy_shim(
652    receiver: unsafe extern "C" fn(*mut c_void, *const c_char, *mut VaListTag) -> c_int,
653) -> c_int {
654    unsafe {
655        core::arch::asm!(
656            "sub rsp, 240",
657            "mov [rsp+0], rdi",
658            "mov [rsp+8], rsi",
659            "mov [rsp+16], rdx",
660            "mov [rsp+24], rcx",
661            "mov [rsp+32], r8",
662            "mov [rsp+40], r9",
663            "movaps [rsp+48], xmm0",
664            "movaps [rsp+64], xmm1",
665            "movaps [rsp+80], xmm2",
666            "movaps [rsp+96], xmm3",
667            "movaps [rsp+112], xmm4",
668            "movaps [rsp+128], xmm5",
669            "movaps [rsp+144], xmm6",
670            "movaps [rsp+160], xmm7",
671            "mov dword ptr [rsp+176], 16",
672            "mov dword ptr [rsp+180], 48",
673            "lea rax, [rsp+256]",
674            "mov [rsp+184], rax",
675            "lea rax, [rsp]",
676            "mov [rsp+192], rax",
677            "lea rdx, [rsp+176]",
678            "call {receiver}",
679            "add rsp, 240",
680            "add rsp, 8",
681            "ret",
682            receiver = in(reg) receiver as usize,
683            options(noreturn),
684        );
685    }
686}
687
688// ═══════════════════════════════════════════════════════════════════════════════
689// Generic-channel fragment streaming (upstream error.c `xmlFormatError`)
690// ═══════════════════════════════════════════════════════════════════════════════
691//
692// Upstream streams each error through the generic channel as a sequence of
693// variadic calls (e.g. `channel(data, "%s:%d: ", file, line)` followed by the
694// domain, level, message and source-context fragments). Custom handlers and the
695// built-in default (an x86_64 SysV va_list shim, see data_globals.rs) both
696// observe the same per-fragment calls. Stable Rust cannot express a variadic
697// call, so each fragment goes through a tiny x86_64 trampoline that places the
698// fixed arguments in the ABI registers and does an indirect call.
699
700/// `channel(data, fmt)` — no variadic arguments.
701#[cfg(target_arch = "x86_64")]
702#[inline]
703unsafe fn ch_call0(handler: xmlGenericErrorFunc, data: *mut c_void, fmt: *const c_char) {
704    // SAFETY: `handler` is a C-compatible generic error callback; per the
705    // SysV ABI the callee sees (data, fmt) with no additional registers
706    // consumed (rdx/rcx zeroed so a va_list-reading callee finds nothing).
707    // The compiler guarantees 16-byte stack alignment at the asm block, so
708    // the `call` is correctly aligned.
709    unsafe {
710        core::arch::asm!(
711            "xor edx, edx",
712            "xor ecx, ecx",
713            "call {h}",
714            h = in(reg) handler as usize,
715            in("rdi") data,
716            in("rsi") fmt,
717            out("rdx") _, out("rcx") _,
718            lateout("rax") _, lateout("r8") _, lateout("r9") _, lateout("r10") _, lateout("r11") _,
719        );
720    }
721}
722
723/// `channel(data, fmt, a1)` — one pointer-sized variadic argument.
724#[cfg(target_arch = "x86_64")]
725#[inline]
726unsafe fn ch_call1(handler: xmlGenericErrorFunc, data: *mut c_void, fmt: *const c_char, a1: usize) {
727    // SAFETY: as ch_call0; `a1` lands in the va_list slot after the two
728    // fixed args (rdx).
729    unsafe {
730        core::arch::asm!(
731            "xor ecx, ecx",
732            "call {h}",
733            h = in(reg) handler as usize,
734            in("rdi") data,
735            in("rsi") fmt,
736            in("rdx") a1,
737            out("rcx") _,
738            lateout("rax") _, lateout("r8") _, lateout("r9") _, lateout("r10") _, lateout("r11") _,
739        );
740    }
741}
742
743/// `channel(data, fmt, a1, a2)` — two pointer-sized variadic arguments.
744#[cfg(target_arch = "x86_64")]
745#[inline]
746unsafe fn ch_call2(
747    handler: xmlGenericErrorFunc,
748    data: *mut c_void,
749    fmt: *const c_char,
750    a1: usize,
751    a2: usize,
752) {
753    // SAFETY: as ch_call0; a1/a2 land in the va_list slots (rdx, rcx).
754    unsafe {
755        core::arch::asm!(
756            "call {h}",
757            h = in(reg) handler as usize,
758            in("rdi") data,
759            in("rsi") fmt,
760            in("rdx") a1,
761            in("rcx") a2,
762            lateout("rax") _, lateout("r8") _, lateout("r9") _, lateout("r10") _, lateout("r11") _,
763        );
764    }
765}
766
767/// Emit one raise through the generic channel with upstream's
768/// `xmlFormatError` fragment sequence (error.c 2.15): file/line prefix,
769/// domain, level, message, then the source window and caret line.
770///
771/// `file`/`line` come from the raising site's input; `source_window` is the
772/// current input line text plus the 0-based caret column (upstream
773/// `xmlParserInputGetWindow`).
774///
775/// # SAFETY
776///
777/// - `file` and `message` must be valid C strings or NULL.
778/// - `source_window` bytes must be valid for the duration of the call.
779#[allow(clippy::too_many_arguments)]
780#[cfg(target_arch = "x86_64")]
781unsafe fn format_error_streamed(
782    domain: c_int,
783    code: c_int,
784    level: c_int,
785    message: *const c_char,
786    file: *const c_char,
787    line: c_int,
788    source_window: Option<(&[u8], usize)>,
789    enc_bytes: Option<[u8; 4]>,
790    tail: Option<(c_int, Option<(&[u8], usize)>)>,
791) {
792    // SAFETY: reads the exported C globals (upstream reads the same).
793    let Some(handler) = globals::get_generic_error_func() else {
794        return;
795    };
796    let data = globals::get_generic_error_ctx();
797
798    // 1. File/line prefix (xmlFormatError).
799    if !file.is_null() {
800        ch_call2(
801            handler,
802            data,
803            c"%s:%d: ".as_ptr() as *const c_char,
804            file as usize,
805            line as usize,
806        );
807    } else if line != 0
808        && (domain == XML_FROM_PARSER
809            || domain == XML_FROM_SCHEMASV
810            || domain == XML_FROM_SCHEMASP
811            || domain == XML_FROM_DTD
812            || domain == XML_FROM_RELAXNGP
813            || domain == XML_FROM_RELAXNGV)
814    {
815        ch_call1(
816            handler,
817            data,
818            c"Entity: line %d: ".as_ptr() as *const c_char,
819            line as usize,
820        );
821    }
822
823    // 2. Domain fragment (xmlFormatError switch).
824    let dom: &[u8] = match domain {
825        XML_FROM_PARSER => b"parser \0",
826        XML_FROM_NAMESPACE => b"namespace \0",
827        XML_FROM_DTD | XML_FROM_VALID => b"validity \0",
828        XML_FROM_HTML => b"HTML parser \0",
829        XML_FROM_MEMORY => b"memory \0",
830        XML_FROM_OUTPUT => b"output \0",
831        XML_FROM_IO => b"I/O \0",
832        XML_FROM_XINCLUDE => b"XInclude \0",
833        XML_FROM_XPATH => b"XPath \0",
834        XML_FROM_XPOINTER => b"parser \0",
835        XML_FROM_REGEXP => b"regexp \0",
836        XML_FROM_MODULE => b"module \0",
837        XML_FROM_SCHEMASV => b"Schemas validity \0",
838        XML_FROM_SCHEMASP => b"Schemas parser \0",
839        XML_FROM_RELAXNGP => b"Relax-NG parser \0",
840        XML_FROM_RELAXNGV => b"Relax-NG validity \0",
841        XML_FROM_CATALOG => b"Catalog \0",
842        XML_FROM_C14N => b"C14N \0",
843        XML_FROM_XSLT => b"XSLT \0",
844        XML_FROM_I18N => b"encoding \0",
845        XML_FROM_SCHEMATRONV => b"schematron \0",
846        XML_FROM_BUFFER => b"internal buffer \0",
847        XML_FROM_URI => b"URI \0",
848        _ => b"\0",
849    };
850    if !dom.is_empty() && dom[0] != 0 {
851        ch_call0(handler, data, dom.as_ptr() as *const c_char);
852    }
853
854    // 3. Level fragment (xmlFormatError switch).
855    let lvl: &[u8] = if level == XML_ERR_NONE as c_int {
856        b": \0"
857    } else if level == XML_ERR_WARNING as c_int {
858        b"warning : \0"
859    } else if level == XML_ERR_ERROR as c_int || level == XML_ERR_FATAL as c_int {
860        b"error : \0"
861    } else {
862        b"\0"
863    };
864    if !lvl.is_empty() && lvl[0] != 0 {
865        ch_call0(handler, data, lvl.as_ptr() as *const c_char);
866    }
867
868    // 4. Message fragment.
869    if !message.is_null() {
870        let msg = message as *const u8;
871        let mut len = 0usize;
872        while unsafe { *msg.add(len) } != 0 {
873            len += 1;
874        }
875        let ends_nl = len > 0 && unsafe { *msg.add(len - 1) } == b'\n';
876        let fmt: &[u8] = if ends_nl { b"%s\0" } else { b"%s\n\0" };
877        ch_call1(handler, data, fmt.as_ptr() as *const c_char, msg as usize);
878    }
879
880    // 4b. Invalid-encoding byte dump (upstream xmlFormatError: the first 4
881    // bytes at the error position, only for XML_ERR_INVALID_ENCODING).
882    if code == XML_ERR_INVALID_ENCODING {
883        if let Some(bytes) = enc_bytes {
884            ch_call0(handler, data, c"Bytes:".as_ptr() as *const c_char);
885            for b in bytes {
886                // " 0x%02X"
887                let hex = format!(" 0x{:02X}\0", b);
888                ch_call0(handler, data, hex.as_ptr() as *const c_char);
889            }
890            ch_call0(handler, data, c"\n".as_ptr() as *const c_char);
891        }
892    }
893
894    // 5. Source window + caret (xmlParserPrintFileContextInternal).
895    if let Some((window, caret)) = source_window {
896        let mut win = window.to_vec();
897        win.push(0);
898        ch_call1(
899            handler,
900            data,
901            c"%s\n".as_ptr() as *const c_char,
902            win.as_ptr() as usize,
903        );
904        let mut caret_line = Vec::with_capacity(caret + 2);
905        for &b in window.iter().take(caret) {
906            caret_line.push(if b == b'\t' { b'\t' } else { b' ' });
907        }
908        caret_line.push(b'^');
909        caret_line.push(0);
910        ch_call1(
911            handler,
912            data,
913            c"%s\n".as_ptr() as *const c_char,
914            caret_line.as_ptr() as usize,
915        );
916    }
917
918    // 5b. "cur input" tail (error.c xmlFormatError): after the parent
919    // window, upstream prints the current (entity) input's info + window —
920    // `Entity: line %d: \n` for a nameless nested input, then its context
921    // and caret (HOSTILE-FAILURE F2 entity loops).
922    if let Some((tline, twindow)) = tail {
923        if tline != 0
924            && (domain == XML_FROM_PARSER
925                || domain == XML_FROM_SCHEMASV
926                || domain == XML_FROM_SCHEMASP
927                || domain == XML_FROM_DTD
928                || domain == XML_FROM_RELAXNGP
929                || domain == XML_FROM_RELAXNGV)
930        {
931            ch_call1(
932                handler,
933                data,
934                c"Entity: line %d: \n".as_ptr() as *const c_char,
935                tline as usize,
936            );
937        }
938        if let Some((window, caret)) = twindow {
939            let mut win = window.to_vec();
940            win.push(0);
941            ch_call1(
942                handler,
943                data,
944                c"%s\n".as_ptr() as *const c_char,
945                win.as_ptr() as usize,
946            );
947            let mut caret_line = Vec::with_capacity(caret + 2);
948            for &b in window.iter().take(caret) {
949                caret_line.push(if b == b'\t' { b'\t' } else { b' ' });
950            }
951            caret_line.push(b'^');
952            caret_line.push(0);
953            ch_call1(
954                handler,
955                data,
956                c"%s\n".as_ptr() as *const c_char,
957                caret_line.as_ptr() as usize,
958            );
959        }
960    }
961}
962
963/// How a raise delivers to the generic side of the error system (upstream
964/// `xmlVRaiseError` channel selection, error.c 2.15).
965#[derive(Clone, Copy, Debug)]
966pub enum GenericDelivery {
967    /// Custom SAX channel: single call `channel(ctx, msg)`.
968    Custom(xmlGenericErrorFunc, *mut c_void),
969    /// Legacy/default channel: stream the `xmlFormatError` fragments through
970    /// the global generic handler.
971    Stream,
972    /// No channel (SAX slot NULL): no generic delivery.
973    None,
974}
975
976/// Select the generic delivery for a parser error from the context's SAX
977/// `error` slot (upstream `xmlCtxtVErr`: `channel = ctxt->sax->error`). Used
978/// by the parser layer and by the SAX-layer depth error (HOSTILE-FAILURE F1).
979///
980/// # Safety
981///
982/// - `ctxt` must be a valid `_xmlParserCtxt` with a valid `sax` pointer.
983pub unsafe fn parser_delivery(ctxt: *mut crate::abi::structs::_xmlParserCtxt) -> GenericDelivery {
984    unsafe {
985        let sax = &*((*ctxt).sax);
986        match sax.error {
987            None => GenericDelivery::None,
988            Some(cb) if is_legacy_error_handler(cb) => GenericDelivery::Stream,
989            Some(cb) => GenericDelivery::Custom(cb, (*ctxt).userData),
990        }
991    }
992}
993
994/// Whether a SAX `error` slot holds the candidate's legacy default handler
995/// (the SAX1 shim or the default SAX2 handler) — those route through the
996/// streamed `xmlFormatError` fragments like upstream's `xmlParserError`.
997fn is_legacy_error_handler(cb: errorSAXFunc) -> bool {
998    let ptr = cb as usize;
999    ptr == XML_PARSER_ERROR_SAX1 as errorSAXFunc as usize
1000        || ptr == crate::xml::sax::default::default_sax_handler::error as errorSAXFunc as usize
1001}
1002
1003/// Raise an error with upstream's full routing (error.c 2.15
1004/// `xmlVRaiseError`): update the last error, then deliver to the structured
1005/// handler **or** the selected generic channel — never both.
1006///
1007/// `file`/`line`/`source_window` feed the generic fragment stream (the
1008/// structured handler receives the complete `xmlError` instead). `col` is
1009/// the 1-based byte column (upstream `input->col` → `err->int2`); `str1`..
1010/// `str3`/`int1` are the upstream extra fields; `enc_bytes` feeds the
1011/// `XML_ERR_INVALID_ENCODING` "Bytes:" fragment.
1012///
1013/// # UPSTREAM-PARITY (ownership)
1014///
1015/// Like upstream `xmlVSetError`, every string field of the stored error is
1016/// owned (`xmlStrdup`): `file`/`str1`/`str2`/`str3` are heap copies, so the
1017/// caller may pass transient C strings.
1018///
1019/// # SAFETY
1020///
1021/// - `ctxt` may be NULL.
1022/// - `msg`, `file`, `str1`, `str2`, `str3` must be valid C strings or NULL.
1023/// - `source_window` bytes must be valid for the duration of the call.
1024#[allow(clippy::too_many_arguments)]
1025pub unsafe fn raise_error_streamed(
1026    ctxt: *mut c_void,
1027    domain: c_int,
1028    code: c_int,
1029    level: c_int,
1030    file: *const c_char,
1031    line: c_int,
1032    col: c_int,
1033    str1: *const c_char,
1034    str2: *const c_char,
1035    str3: *const c_char,
1036    int1: c_int,
1037    msg: *const c_char,
1038    source_window: Option<(&[u8], usize)>,
1039    enc_bytes: Option<[u8; 4]>,
1040    delivery: GenericDelivery,
1041    tail: Option<(c_int, Option<(&[u8], usize)>)>,
1042) {
1043    // The streamed generic-error channel below uses an x86_64 SysV va_list
1044    // trampoline (ch_call0/1/2 — register-based). Other ABIs (i686 cdecl,
1045    // ARM/aarch64 AAPCS, ...) fall back to the plain raise path; full
1046    // streamed-fragment parity there is an unexecuted platform obligation
1047    // (atlas/PLATFORM_SURFACE_ATLAS.md, OBLIG-WORDSIZE-32 / compiler-ABI).
1048    #[cfg(not(target_arch = "x86_64"))]
1049    {
1050        raise_error(
1051            ctxt,
1052            ptr::null_mut(),
1053            ptr::null_mut(),
1054            ptr::null_mut(),
1055            ptr::null_mut(),
1056            domain,
1057            code,
1058            level,
1059            file,
1060            line,
1061            str1,
1062            str2,
1063            str3,
1064            int1,
1065            col,
1066            msg,
1067        );
1068        return;
1069    }
1070
1071    #[cfg(target_arch = "x86_64")]
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        raise_error_streamed_x86_64(
1082            ctxt,
1083            domain,
1084            code,
1085            level,
1086            file,
1087            line,
1088            col,
1089            str1,
1090            str2,
1091            str3,
1092            int1,
1093            msg,
1094            source_window,
1095            enc_bytes,
1096            delivery,
1097            tail,
1098        );
1099    }
1100}
1101
1102/// x86-64 streamed raise (SysV va_list channel). See `raise_error_streamed`.
1103///
1104/// # Safety
1105///
1106/// - `ctxt` may be NULL; `msg`, `file`, `str1`, `str2`, `str3` must be
1107///   valid NUL-terminated C strings or NULL; `source_window` bytes must be
1108///   valid for the duration of the call; every string field is duplicated
1109///   before being stored in the thread-local last error.
1110#[allow(clippy::too_many_arguments)]
1111#[cfg(target_arch = "x86_64")]
1112unsafe fn raise_error_streamed_x86_64(
1113    ctxt: *mut c_void,
1114    domain: c_int,
1115    code: c_int,
1116    level: c_int,
1117    file: *const c_char,
1118    line: c_int,
1119    col: c_int,
1120    str1: *const c_char,
1121    str2: *const c_char,
1122    str3: *const c_char,
1123    int1: c_int,
1124    msg: *const c_char,
1125    source_window: Option<(&[u8], usize)>,
1126    enc_bytes: Option<[u8; 4]>,
1127    delivery: GenericDelivery,
1128    tail: Option<(c_int, Option<(&[u8], usize)>)>,
1129) {
1130    // UPSTREAM-PARITY (xmlVRaiseError): warnings are suppressed when the
1131    // TLS global xmlGetWarningsDefaultValue is zero.
1132    if level == xmlErrorLevel::XML_ERR_WARNING as c_int
1133        && crate::xml::globals::get_get_warnings_default() == 0
1134    {
1135        return;
1136    }
1137
1138    // Format the error message (same as raise_error).
1139    let formatted_msg = format_error_message(domain, code, msg, str1, str2, str3);
1140    let file_copy = if file.is_null() {
1141        ptr::null_mut()
1142    } else {
1143        crate::abi::allocator::xmlMemStrdupImpl(file) as *mut c_char
1144    };
1145    let str1_copy = if str1.is_null() {
1146        ptr::null_mut()
1147    } else {
1148        crate::abi::allocator::xmlMemStrdupImpl(str1) as *mut c_char
1149    };
1150    let str2_copy = if str2.is_null() {
1151        ptr::null_mut()
1152    } else {
1153        crate::abi::allocator::xmlMemStrdupImpl(str2) as *mut c_char
1154    };
1155    let str3_copy = if str3.is_null() {
1156        ptr::null_mut()
1157    } else {
1158        crate::abi::allocator::xmlMemStrdupImpl(str3) as *mut c_char
1159    };
1160    let err = _xmlError {
1161        domain,
1162        code,
1163        message: formatted_msg,
1164        level,
1165        file: file_copy,
1166        line,
1167        str1: str1_copy,
1168        str2: str2_copy,
1169        str3: str3_copy,
1170        int1,
1171        int2: col,
1172        ctxt,
1173        node: ptr::null_mut(),
1174    };
1175
1176    // UPSTREAM-PARITY (xpath.c xmlXPathErrFmt -> error.c xmlVRaiseError): an
1177    // XPath-domain error stores in the TLS global (upstream `to = lastError`;
1178    // xmlXPathErrFmt separately pre-fills ctxt->lastError with the
1179    // message-less domain/code/str1 fields) and dispatches to the XPath
1180    // context's `error` handler with `userData`.
1181    let is_xpath_error = !ctxt.is_null() && domain == XML_FROM_XPATH;
1182
1183    // UPSTREAM-PARITY (error.c xmlVRaiseError 2.15): parser-domain errors
1184    // with a context update the PER-CONTEXT lastError (to = &ctxt->lastError)
1185    // and then mirror into the TLS global (xmlCopyError); the callback
1186    // receives the per-context error. Non-parser domains keep the TLS global
1187    // as the single storage.
1188    let is_ctxt_error = !ctxt.is_null()
1189        && matches!(
1190            domain,
1191            XML_FROM_PARSER
1192                | XML_FROM_HTML
1193                | XML_FROM_DTD
1194                | XML_FROM_NAMESPACE
1195                | XML_FROM_IO
1196                | XML_FROM_VALID
1197        );
1198    let to: *const _xmlError = if is_ctxt_error {
1199        let c = &mut *(ctxt as *mut crate::abi::structs::_xmlParserCtxt);
1200        // Free the previous per-context strings (upstream xmlResetError in
1201        // xmlVUpdateError) before taking ownership of the new ones.
1202        globals::free_error_strings(&c.lastError);
1203        ptr::write(&mut c.lastError, err);
1204        // Mirror into the TLS global with owned copies (upstream
1205        // xmlCopyError(to, lastError)).
1206        globals::set_last_error(deep_copy_error(&c.lastError));
1207        &c.lastError
1208    } else {
1209        globals::set_last_error(err);
1210        globals::get_last_error()
1211    };
1212
1213    // UPSTREAM-PARITY (parserInternals.c xmlCtxtVErr channel selection):
1214    // ctxt->errorHandler (xmlCtxtSetErrorHandler) wins, then the SAX2
1215    // `serror` slot (lxml installs this), then the legacy SAX generic
1216    // channel. The per-context structured handler is preferred over the
1217    // TLS global structured handler (error.c xmlVRaiseError order).
1218    let mut schannel: Option<xmlStructuredErrorFunc> = None;
1219    let mut sdata: *mut c_void = ptr::null_mut();
1220    if is_xpath_error {
1221        let xc = &*(ctxt as *const crate::abi::structs::_xmlXPathContext);
1222        if let Some(handler) = xc.error {
1223            schannel = Some(handler);
1224            sdata = xc.userData;
1225        }
1226    } else if is_ctxt_error {
1227        let c = &*(ctxt as *const crate::abi::structs::_xmlParserCtxt);
1228        if let Some(handler) = c.errorHandler {
1229            schannel = Some(handler);
1230            sdata = c.errorCtxt;
1231        } else if !c.sax.is_null() {
1232            let sax = &*c.sax;
1233            if sax.initialized == XML_SAX2_MAGIC as c_uint && sax.serror.is_some() {
1234                schannel = sax.serror;
1235                sdata = c.userData;
1236            }
1237        }
1238    }
1239    if let Some(handler) = schannel {
1240        // SAFETY: the selected structured callback and its data come from
1241        // the parser context (or the TLS install), valid per the C API.
1242        handler(sdata, to);
1243        return;
1244    }
1245
1246    // Global structured handler (upstream `else if` chain); the (handler,
1247    // ctx) pair is read atomically and invoked outside the lock (11.1-X).
1248    let structured = globals::with_structured_error(|h, c| (h, c));
1249    if let Some(handler) = structured.0 {
1250        if !to.is_null() {
1251            handler(structured.1, to);
1252        }
1253        return;
1254    }
1255
1256    match delivery {
1257        GenericDelivery::Custom(channel, ctx) => {
1258            if !msg.is_null() {
1259                // SAFETY: the caller provided a valid C callback.
1260                unsafe { channel(ctx, msg) };
1261            }
1262        }
1263        GenericDelivery::Stream => {
1264            if globals::get_generic_error_func().is_some() {
1265                unsafe {
1266                    format_error_streamed(
1267                        domain,
1268                        code,
1269                        level,
1270                        formatted_msg,
1271                        file,
1272                        line,
1273                        source_window,
1274                        enc_bytes,
1275                        tail,
1276                    )
1277                };
1278            }
1279        }
1280        GenericDelivery::None => {}
1281    }
1282}
1283
1284/// Deep-copy an error's owned string fields (upstream `xmlCopyError`): the
1285/// destination takes freshly strdup'd copies so both errors own their
1286/// strings independently.
1287///
1288/// # Safety
1289///
1290/// - `err` must be a valid `_xmlError` whose string fields are NULL or
1291///   NUL-terminated.
1292unsafe fn deep_copy_error(err: &_xmlError) -> _xmlError {
1293    unsafe {
1294        let dup = |p: *mut c_char| -> *mut c_char {
1295            if p.is_null() {
1296                ptr::null_mut()
1297            } else {
1298                crate::abi::allocator::xmlMemStrdupImpl(p) as *mut c_char
1299            }
1300        };
1301        _xmlError {
1302            domain: err.domain,
1303            code: err.code,
1304            message: dup(err.message),
1305            level: err.level,
1306            file: dup(err.file),
1307            line: err.line,
1308            str1: dup(err.str1),
1309            str2: dup(err.str2),
1310            str3: dup(err.str3),
1311            int1: err.int1,
1312            int2: err.int2,
1313            ctxt: err.ctxt,
1314            node: err.node,
1315        }
1316    }
1317}
1318
1319/// Default SAX v1 error handler — upstream `void xmlParserError(void *ctx,
1320/// const char *msg, ...)`. Variadic x86_64 SysV shim (11.1-Z.2, R-000176:
1321/// the previous fixed-arity body silently dropped the varargs; upstream
1322/// error.c formats them via `xmlVFormatLegacyError`).
1323///
1324/// 2 fixed args (ctx=rdi, msg=rsi) → `gp_offset` 16; the va_list pointer is
1325/// passed as the 3rd arg (rdx) to the `xmlParserErrorV` receiver.
1326///
1327/// # SAFETY
1328///
1329/// - `ctx` may be NULL (unused by the candidate's legacy path).
1330/// - `msg` must be a valid NUL-terminated printf format string.
1331#[cfg(target_arch = "x86_64")]
1332#[no_mangle]
1333pub unsafe extern "C" fn xmlParserError() -> c_int {
1334    unsafe { legacy_shim(xmlParserErrorV) }
1335}
1336
1337/// Variadic receiver for the `xmlParserError` shim: formats `msg` with the
1338/// caller's varargs and emits `"error: "` + formatted text through the
1339/// generic channel (upstream error.c `xmlVFormatLegacyError`).
1340///
1341/// x86_64-only: the va_list layout (`VaListTag`) is the SysV AMD64
1342/// `__va_list_tag`; other ABIs carry their own varargs representation and the
1343/// register-shim entry (`xmlParserError`) is x86_64-gated with it.
1344///
1345/// # Safety
1346///
1347/// - `msg` must be a valid NUL-terminated printf format string, `ap` a
1348///   valid va_list matching the format, `ctx` may be NULL.
1349#[cfg(target_arch = "x86_64")]
1350#[no_mangle]
1351pub unsafe extern "C" fn xmlParserErrorV(
1352    ctx: *mut c_void,
1353    msg: *const c_char,
1354    ap: *mut VaListTag,
1355) -> c_int {
1356    let _ = ctx;
1357    unsafe { emit_legacy_message_v("error", msg, ap) }
1358}
1359
1360/// Default SAX v1 warning handler — upstream `void xmlParserWarning(void
1361/// *ctx, const char *msg, ...)`. Variadic shim as `xmlParserError`.
1362///
1363/// # SAFETY
1364///
1365/// - `ctx` may be NULL (unused by the candidate's legacy path).
1366/// - `msg` must be a valid NUL-terminated printf format string.
1367#[cfg(target_arch = "x86_64")]
1368#[no_mangle]
1369pub unsafe extern "C" fn xmlParserWarning() -> c_int {
1370    unsafe { legacy_shim(xmlParserWarningV) }
1371}
1372
1373/// Variadic receiver for the `xmlParserWarning` shim.
1374///
1375/// x86_64-only: see `xmlParserErrorV`.
1376///
1377/// # Safety
1378///
1379/// - `msg` must be a valid NUL-terminated printf format string, `ap` a
1380///   valid va_list matching the format, `ctx` may be NULL.
1381#[cfg(target_arch = "x86_64")]
1382#[no_mangle]
1383pub unsafe extern "C" fn xmlParserWarningV(
1384    ctx: *mut c_void,
1385    msg: *const c_char,
1386    ap: *mut VaListTag,
1387) -> c_int {
1388    let _ = ctx;
1389    unsafe { emit_legacy_message_v("warning", msg, ap) }
1390}
1391
1392/// Default validity error handler — upstream `void
1393/// xmlParserValidityError(void *ctx, const char *msg, ...)`. Variadic shim
1394/// as `xmlParserError`.
1395///
1396/// # SAFETY
1397///
1398/// - `ctx` may be NULL (unused by the candidate's legacy path).
1399/// - `msg` must be a valid NUL-terminated printf format string.
1400#[cfg(target_arch = "x86_64")]
1401#[no_mangle]
1402pub unsafe extern "C" fn xmlParserValidityError() -> c_int {
1403    unsafe { legacy_shim(xmlParserValidityErrorV) }
1404}
1405
1406/// Variadic receiver for the `xmlParserValidityError` shim.
1407///
1408/// x86_64-only: see `xmlParserErrorV`.
1409///
1410/// # Safety
1411///
1412/// - `msg` must be a valid NUL-terminated printf format string, `ap` a
1413///   valid va_list matching the format, `ctx` may be NULL.
1414#[cfg(target_arch = "x86_64")]
1415#[no_mangle]
1416pub unsafe extern "C" fn xmlParserValidityErrorV(
1417    ctx: *mut c_void,
1418    msg: *const c_char,
1419    ap: *mut VaListTag,
1420) -> c_int {
1421    let _ = ctx;
1422    unsafe { emit_legacy_message_v("validity error", msg, ap) }
1423}
1424
1425/// Default validity warning handler — upstream `void
1426/// xmlParserValidityWarning(void *ctx, const char *msg, ...)`. Variadic
1427/// shim as `xmlParserError`.
1428///
1429/// # SAFETY
1430///
1431/// - `ctx` may be NULL (unused by the candidate's legacy path).
1432/// - `msg` must be a valid NUL-terminated printf format string.
1433#[cfg(target_arch = "x86_64")]
1434#[no_mangle]
1435pub unsafe extern "C" fn xmlParserValidityWarning() -> c_int {
1436    unsafe { legacy_shim(xmlParserValidityWarningV) }
1437}
1438
1439/// Variadic receiver for the `xmlParserValidityWarning` shim.
1440///
1441/// x86_64-only: see `xmlParserErrorV`.
1442///
1443/// # Safety
1444///
1445/// - `msg` must be a valid NUL-terminated printf format string, `ap` a
1446///   valid va_list matching the format, `ctx` may be NULL.
1447#[cfg(target_arch = "x86_64")]
1448#[no_mangle]
1449pub unsafe extern "C" fn xmlParserValidityWarningV(
1450    ctx: *mut c_void,
1451    msg: *const c_char,
1452    ap: *mut VaListTag,
1453) -> c_int {
1454    let _ = ctx;
1455    unsafe { emit_legacy_message_v("validity warning", msg, ap) }
1456}
1457
1458/// The variadic `xmlParserError` shim, transmuted to the fixed-arity SAX v1
1459/// callback type. The shim's declared arity is a Rust-side fiction (stable
1460/// Rust cannot express `c_variadic`); the ABI is a plain code pointer and
1461/// the C variadic call contract is preserved (11.1-Z.2, R-000176).
1462#[cfg(target_arch = "x86_64")]
1463pub const XML_PARSER_ERROR_SAX1: errorSAXFunc = unsafe {
1464    // SAFETY: shim and SAX callback are both plain code pointers (same ABI);
1465    // the declared arity difference is the documented shim fiction.
1466    core::mem::transmute::<unsafe extern "C" fn() -> c_int, errorSAXFunc>(xmlParserError)
1467};
1468
1469/// The variadic `xmlParserWarning` shim as the SAX v1 callback type.
1470#[cfg(target_arch = "x86_64")]
1471pub const XML_PARSER_WARNING_SAX1: errorSAXFunc = unsafe {
1472    // SAFETY: see XML_PARSER_ERROR_SAX1.
1473    core::mem::transmute::<unsafe extern "C" fn() -> c_int, errorSAXFunc>(xmlParserWarning)
1474};
1475
1476/// The variadic `xmlParserValidityError` shim as the validation callback
1477/// type (`xmlValidityErrorFunc`-compatible fixed-arity pointer).
1478#[cfg(target_arch = "x86_64")]
1479pub const XML_PARSER_VALIDITY_ERROR_SAX1: unsafe extern "C" fn(*mut c_void, *const c_char) =
1480    // SAFETY: see XML_PARSER_ERROR_SAX1.
1481    unsafe {
1482        core::mem::transmute::<
1483            unsafe extern "C" fn() -> c_int,
1484            unsafe extern "C" fn(*mut c_void, *const c_char),
1485        >(xmlParserValidityError)
1486    };
1487
1488/// The variadic `xmlParserValidityWarning` shim as the validation callback
1489/// type.
1490#[cfg(target_arch = "x86_64")]
1491pub const XML_PARSER_VALIDITY_WARNING_SAX1: unsafe extern "C" fn(*mut c_void, *const c_char) =
1492    // SAFETY: see XML_PARSER_ERROR_SAX1.
1493    unsafe {
1494        core::mem::transmute::<
1495            unsafe extern "C" fn() -> c_int,
1496            unsafe extern "C" fn(*mut c_void, *const c_char),
1497        >(xmlParserValidityWarning)
1498    };
1499
1500#[cfg(not(target_arch = "x86_64"))]
1501mod legacy_plain {
1502    //! Non-x86_64 fallbacks for the default legacy error/warning handlers.
1503    //!
1504    //! The x86_64 implementations forward the CALLER's C varargs to
1505    //! `vsnprintf` through a SysV register-save-area asm shim (R-000176).
1506    //! Other ABIs carry their own va_list representation; building the
1507    //! matching shim per ABI is a documented platform obligation (R-000168,
1508    //! varargs family). On those (unexecuted) ABIs the default slots emit the
1509    //! caller's message text VERBATIM — the candidate's own error machinery
1510    //! always hands these slots pre-formatted text, so the emitted bytes are
1511    //! identical for every internal raise; only a C consumer that variadically
1512    //! calls the DEFAULT handler with format arguments would observe the
1513    //! difference (unexecuted, tracked in PLATFORM_SURFACE_ATLAS).
1514    use super::emit_legacy_message;
1515    use crate::abi::callbacks::errorSAXFunc;
1516    use core::ffi::{c_char, c_void};
1517
1518    unsafe extern "C" fn error(ctx: *mut c_void, msg: *const c_char) {
1519        let _ = ctx;
1520        unsafe { emit_legacy_message("error", msg) };
1521    }
1522    unsafe extern "C" fn warning(ctx: *mut c_void, msg: *const c_char) {
1523        let _ = ctx;
1524        unsafe { emit_legacy_message("warning", msg) };
1525    }
1526    unsafe extern "C" fn validity_error(ctx: *mut c_void, msg: *const c_char) {
1527        let _ = ctx;
1528        unsafe { emit_legacy_message("validity error", msg) };
1529    }
1530    unsafe extern "C" fn validity_warning(ctx: *mut c_void, msg: *const c_char) {
1531        let _ = ctx;
1532        unsafe { emit_legacy_message("validity warning", msg) };
1533    }
1534
1535    pub const XML_PARSER_ERROR_SAX1: errorSAXFunc = error;
1536    pub const XML_PARSER_WARNING_SAX1: errorSAXFunc = warning;
1537    pub const XML_PARSER_VALIDITY_ERROR_SAX1: unsafe extern "C" fn(*mut c_void, *const c_char) =
1538        validity_error;
1539    pub const XML_PARSER_VALIDITY_WARNING_SAX1: unsafe extern "C" fn(*mut c_void, *const c_char) =
1540        validity_warning;
1541}
1542
1543#[cfg(not(target_arch = "x86_64"))]
1544pub use legacy_plain::{
1545    XML_PARSER_ERROR_SAX1, XML_PARSER_VALIDITY_ERROR_SAX1, XML_PARSER_VALIDITY_WARNING_SAX1,
1546    XML_PARSER_WARNING_SAX1,
1547};
1548
1549// ═══════════════════════════════════════════════════════════════════════════════
1550// Tests
1551// ═══════════════════════════════════════════════════════════════════════════════
1552
1553#[cfg(test)]
1554mod tests {
1555    use super::*;
1556    use crate::abi::allocator;
1557    use core::ffi::c_void;
1558
1559    /// Reset an error struct and verify the defaults are applied.
1560    ///
1561    /// # Safety
1562    ///
1563    /// - `err` is a stack `_xmlError` whose fields are all NULL/zero; it
1564    ///   is valid for `reset_error` to write and for the subsequent reads.
1565    #[test]
1566    fn test_error_default_reset() {
1567        unsafe {
1568            let mut err = _xmlError {
1569                domain: XML_FROM_PARSER,
1570                code: XML_ERR_NO_MEMORY,
1571                message: ptr::null_mut(),
1572                level: XML_ERR_ERROR as c_int,
1573                file: ptr::null_mut(),
1574                line: 42,
1575                str1: ptr::null_mut(),
1576                str2: ptr::null_mut(),
1577                str3: ptr::null_mut(),
1578                int1: 0,
1579                int2: 0,
1580                ctxt: ptr::null_mut(),
1581                node: ptr::null_mut(),
1582            };
1583
1584            reset_error(&mut err);
1585            assert_eq!(err.domain, XML_FROM_NONE);
1586            assert_eq!(err.code, XML_ERR_OK as c_int);
1587            assert_eq!(err.level, XML_ERR_NONE as c_int);
1588            assert_eq!(err.line, 0);
1589        }
1590    }
1591
1592    /// Copy one error struct into another and verify the fields.
1593    ///
1594    /// # Safety
1595    ///
1596    /// - `from` and `to` are stack `_xmlError` structs valid for the
1597    ///   `copy_error` copy and the subsequent field reads.
1598    #[test]
1599    fn test_copy_error() {
1600        unsafe {
1601            let from = _xmlError {
1602                domain: XML_FROM_PARSER,
1603                code: XML_ERR_NO_MEMORY,
1604                message: ptr::null_mut(),
1605                level: XML_ERR_FATAL as c_int,
1606                file: ptr::null_mut(),
1607                line: 100,
1608                str1: ptr::null_mut(),
1609                str2: ptr::null_mut(),
1610                str3: ptr::null_mut(),
1611                int1: 1,
1612                int2: 2,
1613                ctxt: ptr::null_mut(),
1614                node: ptr::null_mut(),
1615            };
1616            let mut to = _xmlError {
1617                domain: XML_FROM_NONE,
1618                code: XML_ERR_OK as c_int,
1619                message: ptr::null_mut(),
1620                level: XML_ERR_NONE as c_int,
1621                file: ptr::null_mut(),
1622                line: 0,
1623                str1: ptr::null_mut(),
1624                str2: ptr::null_mut(),
1625                str3: ptr::null_mut(),
1626                int1: 0,
1627                int2: 0,
1628                ctxt: ptr::null_mut(),
1629                node: ptr::null_mut(),
1630            };
1631
1632            let result = copy_error(&from, &mut to);
1633            assert_eq!(result, 0);
1634            assert_eq!(to.domain, XML_FROM_PARSER);
1635            assert_eq!(to.code, XML_ERR_NO_MEMORY);
1636            assert_eq!(to.level, XML_ERR_FATAL as c_int);
1637            assert_eq!(to.line, 100);
1638            assert_eq!(to.int1, 1);
1639            assert_eq!(to.int2, 2);
1640        }
1641    }
1642
1643    /// Raise an error and verify it is stored as the last error.
1644    ///
1645    /// # Safety
1646    ///
1647    /// - `file`/`str1` are static NUL-terminated strings valid for the
1648    ///   raise; `raise_error` duplicates them, so the test's later reads of
1649    ///   `last` only touch the thread-local copy; `reset_last_error`
1650    ///   releases the owned strings exactly once.
1651    #[test]
1652    fn test_raise_and_get_last_error() {
1653        unsafe {
1654            reset_last_error();
1655            assert!(get_last_error().is_null());
1656
1657            let file = b"test.xml\0" as *const u8 as *const c_char;
1658            let str1 = b"element\0" as *const u8 as *const c_char;
1659
1660            raise_error(
1661                ptr::null_mut(),
1662                ptr::null_mut(),
1663                ptr::null_mut(),
1664                ptr::null_mut(),
1665                ptr::null_mut(),
1666                XML_FROM_PARSER,
1667                XML_ERR_TAG_NAME_MISMATCH,
1668                XML_ERR_ERROR as c_int,
1669                file,
1670                10,
1671                str1,
1672                ptr::null(),
1673                ptr::null(),
1674                0,
1675                0,
1676                ptr::null(),
1677            );
1678
1679            let last = get_last_error();
1680            assert!(!last.is_null());
1681            assert_eq!((*last).domain, XML_FROM_PARSER);
1682            assert_eq!((*last).code, XML_ERR_TAG_NAME_MISMATCH);
1683            assert_eq!((*last).level, XML_ERR_ERROR as c_int);
1684            assert_eq!((*last).line, 10);
1685
1686            // Check file was stored
1687            let last_file = (*last).file;
1688            assert!(!last_file.is_null());
1689
1690            reset_last_error();
1691            assert!(get_last_error().is_null());
1692        }
1693    }
1694
1695    #[test]
1696    fn test_structured_error_callback() {
1697        // Serialized against the handler-slot tests in xml::globals (11.1-X):
1698        // the structured handler slot is shared global state.
1699        let _guard = crate::xml::globals::ERROR_HANDLER_TEST_LOCK.lock();
1700        unsafe {
1701            reset_last_error();
1702
1703            // Set up a structured error handler that captures the error
1704            let mut captured_domain: c_int = 0;
1705            let captured_ptr = &mut captured_domain as *mut c_int as *mut c_void;
1706
1707            // SAFETY: The callback writes to captured_ptr which lives on the stack
1708            // for the duration of this test.
1709            extern "C" fn test_handler(ctx: *mut c_void, _err: *const _xmlError) {
1710                // SAFETY: ctx is valid for the test duration.
1711                unsafe {
1712                    let captured = &mut *(ctx as *mut c_int);
1713                    *captured = 42;
1714                }
1715            }
1716
1717            set_structured_error_func(captured_ptr, Some(test_handler as xmlStructuredErrorFunc));
1718
1719            raise_error(
1720                ptr::null_mut(),
1721                ptr::null_mut(),
1722                ptr::null_mut(),
1723                ptr::null_mut(),
1724                ptr::null_mut(),
1725                XML_FROM_PARSER,
1726                XML_ERR_OK as c_int,
1727                XML_ERR_WARNING as c_int,
1728                ptr::null(),
1729                0,
1730                ptr::null(),
1731                ptr::null(),
1732                ptr::null(),
1733                0,
1734                0,
1735                ptr::null(),
1736            );
1737
1738            assert_eq!(captured_domain, 42);
1739
1740            // Reset
1741            set_structured_error_func(ptr::null_mut(), None);
1742            reset_last_error();
1743        }
1744    }
1745
1746    /// Format messages with a direct `msg` and with domain plus `str1`.
1747    ///
1748    /// # Safety
1749    ///
1750    /// - `msg`/`str1` are static NUL-terminated strings valid for the
1751    ///   calls; each returned buffer is allocator-owned and freed with
1752    ///   `xmlFreeImpl` exactly once before the test ends.
1753    #[test]
1754    fn test_format_error_message() {
1755        unsafe {
1756            // Test with direct message
1757            let msg = b"test error\0" as *const u8 as *const c_char;
1758            let formatted = format_error_message(
1759                XML_FROM_NONE,
1760                XML_ERR_OK as c_int,
1761                msg,
1762                ptr::null(),
1763                ptr::null(),
1764                ptr::null(),
1765            );
1766            assert!(!formatted.is_null());
1767            let formatted_str = std::ffi::CStr::from_ptr(formatted);
1768            assert_eq!(formatted_str.to_bytes(), b"test error");
1769
1770            // Free the allocated message
1771            allocator::xmlFreeImpl(formatted as *mut c_void);
1772
1773            // Test with domain and str1
1774            let str1 = b"foo\0" as *const u8 as *const c_char;
1775            let formatted2 = format_error_message(
1776                XML_FROM_PARSER,
1777                XML_ERR_OK as c_int,
1778                ptr::null(),
1779                str1,
1780                ptr::null(),
1781                ptr::null(),
1782            );
1783            assert!(!formatted2.is_null());
1784            allocator::xmlFreeImpl(formatted2 as *mut c_void);
1785        }
1786    }
1787}