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
31use core::ffi::c_void;
32use core::ptr;
33use std::os::raw::{c_char, c_int};
34
35use crate::abi::callbacks::{xmlGenericErrorFunc, xmlStructuredErrorFunc};
36use crate::abi::structs::_xmlError;
37use crate::abi::types::xmlErrorLevel::*;
38use crate::abi::types::*;
39use crate::xml::globals;
40
41// ═══════════════════════════════════════════════════════════════════════════════
42// Error Management Functions
43// ═══════════════════════════════════════════════════════════════════════════════
44
45/// Set the generic error handler.
46///
47/// # UPSTREAM-PARITY
48///
49/// ```c
50/// void xmlSetGenericErrorFunc(void *ctx, xmlGenericErrorFunc handler);
51/// ```
52///
53/// # SAFETY
54///
55/// - `handler` must be a valid function pointer or NULL (to reset to default).
56/// - If non-NULL, the handler may be called at any time with `ctx`.
57pub unsafe fn set_generic_error_func(ctx: *mut c_void, handler: Option<xmlGenericErrorFunc>) {
58    // SAFETY: Delegates to globals with same safety contract.
59    unsafe { globals::set_generic_error_func(ctx, handler) };
60}
61
62/// Set the structured error handler.
63///
64/// # UPSTREAM-PARITY
65///
66/// ```c
67/// void xmlSetStructuredErrorFunc(void *ctx, xmlStructuredErrorFunc handler);
68/// ```
69///
70/// # SAFETY
71///
72/// - `handler` must be a valid function pointer or NULL.
73pub unsafe fn set_structured_error_func(ctx: *mut c_void, handler: Option<xmlStructuredErrorFunc>) {
74    // SAFETY: Delegates to globals with same safety contract.
75    unsafe { globals::set_structured_error_func(ctx, handler) };
76}
77
78/// Get the last error for the current thread.
79///
80/// # UPSTREAM-PARITY
81///
82/// ```c
83/// xmlErrorPtr xmlGetLastError(void);
84/// ```
85///
86/// Returns a pointer to the last error, or NULL if no error occurred.
87/// The returned pointer is valid until the next libxml2 call in this thread.
88pub fn get_last_error() -> *mut _xmlError {
89    globals::get_last_error()
90}
91
92/// Copy an error from one location to another.
93///
94/// # UPSTREAM-PARITY
95///
96/// ```c
97/// xmlErrorPtr xmlCopyError(xmlErrorPtr from, xmlErrorPtr to);
98/// ```
99///
100/// Copies `from` into `to`. Returns 0 on success, -1 on error.
101///
102/// # SAFETY
103///
104/// - `from` and `to` must be valid pointers to `_xmlError` structs, or NULL.
105pub const unsafe fn copy_error(from: *const _xmlError, to: *mut _xmlError) -> c_int {
106    if from.is_null() || to.is_null() {
107        return -1;
108    }
109    // SAFETY: Caller guarantees both pointers are valid.
110    unsafe {
111        ptr::copy_nonoverlapping(from, to, 1);
112    }
113    0
114}
115
116/// Reset an error structure to its default state.
117///
118/// # UPSTREAM-PARITY
119///
120/// ```c
121/// void xmlResetError(xmlErrorPtr err);
122/// ```
123///
124/// # SAFETY
125///
126/// - `err` must be a valid pointer to `_xmlError`, or NULL.
127pub const unsafe fn reset_error(err: *mut _xmlError) {
128    if err.is_null() {
129        return;
130    }
131    // SAFETY: Caller guarantees pointer is valid.
132    unsafe {
133        ptr::write(
134            err,
135            _xmlError {
136                domain: XML_FROM_NONE,
137                code: XML_ERR_OK as c_int,
138                message: ptr::null_mut(),
139                level: XML_ERR_NONE as c_int,
140                file: ptr::null_mut(),
141                line: 0,
142                str1: ptr::null_mut(),
143                str2: ptr::null_mut(),
144                str3: ptr::null_mut(),
145                int1: 0,
146                int2: 0,
147                ctxt: ptr::null_mut(),
148                node: ptr::null_mut(),
149            },
150        );
151    }
152}
153
154/// Reset the last error for the current thread.
155///
156/// # UPSTREAM-PARITY
157///
158/// ```c
159/// void xmlResetLastError(void);
160/// ```
161pub fn reset_last_error() {
162    globals::reset_last_error();
163}
164
165/// Format an error message.
166///
167/// This function creates a formatted error message from the component parts.
168/// In Phase 1, this is a basic implementation. In Phase 2+, variadic
169/// printf-style formatting will be added.
170///
171/// Returns a C string pointer (allocated with xmlMalloc) that the caller
172/// must free with xmlFreeImpl, or NULL on allocation failure.
173///
174/// # UPSTREAM-PARITY
175///
176/// Upstream libxml2 uses `vsnprintf` internally for message formatting.
177/// We use a simple formatting approach that produces compatible output
178/// for the common error patterns.
179pub fn format_error_message(
180    _domain: c_int,
181    _code: c_int,
182    msg: *const c_char,
183    str1: *const c_char,
184    str2: *const c_char,
185    str3: *const c_char,
186) -> *mut c_char {
187    // Phase 1: basic message construction.
188    // If a direct message string is provided, use it.
189    if !msg.is_null() {
190        // SAFETY: Caller guarantees msg is a valid C string.
191        let msg_str = unsafe { crate::abi::allocator::xmlMemStrdupImpl(msg) };
192        return msg_str as *mut c_char;
193    }
194
195    // Build a message from the component strings.
196    // This matches upstream behavior where domain/code are combined
197    // with str1/str2/str3 into a diagnostic message.
198    let mut buf: [u8; 1024] = [0; 1024];
199    let mut pos = 0;
200
201    // Write domain prefix
202    let domain_str = match _domain {
203        XML_FROM_PARSER => "parser",
204        XML_FROM_TREE => "tree",
205        XML_FROM_NAMESPACE => "namespace",
206        XML_FROM_DTD => "dtd",
207        XML_FROM_HTML => "html",
208        XML_FROM_MEMORY => "memory",
209        XML_FROM_OUTPUT => "output",
210        XML_FROM_IO => "io",
211        XML_FROM_XPATH => "xpath",
212        XML_FROM_XPOINTER => "xpointer",
213        XML_FROM_XINCLUDE => "xinclude",
214        XML_FROM_CATALOG => "catalog",
215        XML_FROM_C14N => "c14n",
216        XML_FROM_XSLT => "xslt",
217        XML_FROM_VALID => "valid",
218        XML_FROM_CHECK => "check",
219        XML_FROM_WRITER => "writer",
220        XML_FROM_MODULE => "module",
221        XML_FROM_I18N => "i18n",
222        XML_FROM_SCHEMATRONV => "schematron",
223        XML_FROM_BUFFER => "buffer",
224        XML_FROM_URI => "uri",
225        XML_FROM_NONE => "",
226        XML_FROM_FTP => "ftp",
227        XML_FROM_HTTP => "http",
228        XML_FROM_REGEXP => "regexp",
229        XML_FROM_DATATYPE => "datatype",
230        XML_FROM_SCHEMASP => "schema parser",
231        XML_FROM_SCHEMASV => "schema validator",
232        XML_FROM_RELAXNGP => "relaxng parser",
233        XML_FROM_RELAXNGV => "relaxng validator",
234        _ => "unknown",
235    };
236
237    if !domain_str.is_empty() {
238        let bytes = domain_str.as_bytes();
239        let len = bytes.len().min(buf.len().saturating_sub(pos + 2));
240        buf[pos..pos + len].copy_from_slice(&bytes[..len]);
241        pos += len;
242        buf[pos] = b' ';
243        pos += 1;
244    }
245
246    // Append str1 if present
247    if !str1.is_null() {
248        // SAFETY: Caller guarantees str1 is a valid C string.
249        let s = unsafe { crate::abi::versioning::c_str_to_bytes(str1).unwrap_or_default() };
250        if pos + s.len() + 3 <= buf.len() {
251            buf[pos] = b'\'';
252            pos += 1;
253            buf[pos..pos + s.len()].copy_from_slice(s);
254            pos += s.len();
255            buf[pos] = b'\'';
256            pos += 1;
257            buf[pos] = b' ';
258            pos += 1;
259        }
260    }
261
262    // Append str2 if present
263    if !str2.is_null() {
264        let s = unsafe { crate::abi::versioning::c_str_to_bytes(str2).unwrap_or_default() };
265        if pos + s.len() + 3 <= buf.len() {
266            buf[pos] = b'\'';
267            pos += 1;
268            buf[pos..pos + s.len()].copy_from_slice(s);
269            pos += s.len();
270            buf[pos] = b'\'';
271            pos += 1;
272            buf[pos] = b' ';
273            pos += 1;
274        }
275    }
276
277    // Append str3 if present
278    if !str3.is_null() {
279        let s = unsafe { crate::abi::versioning::c_str_to_bytes(str3).unwrap_or_default() };
280        if pos + s.len() + 3 <= buf.len() {
281            buf[pos] = b'\'';
282            pos += 1;
283            buf[pos..pos + s.len()].copy_from_slice(s);
284            pos += s.len();
285            buf[pos] = b'\'';
286            pos += 1;
287            buf[pos] = b' ';
288            pos += 1;
289        }
290    }
291
292    // Null-terminate
293    if pos < buf.len() {
294        buf[pos] = 0;
295    } else {
296        buf[buf.len() - 1] = 0;
297    }
298
299    // Allocate and return
300    let result = unsafe { crate::abi::allocator::xmlMallocImpl(pos + 1) };
301    if result.is_null() {
302        return ptr::null_mut();
303    }
304    unsafe {
305        ptr::copy_nonoverlapping(buf.as_ptr(), result as *mut u8, pos + 1);
306    }
307    result as *mut c_char
308}
309
310/// Raise an error — the central error reporting function.
311///
312/// This is called internally when an error occurs. It:
313/// 1. Updates the thread-local last error
314/// 2. Invokes the structured error handler if one is set
315/// 3. Invokes the generic error handler if one is set (for warnings/errors)
316///
317/// # UPSTREAM-PARITY
318///
319/// ```c
320/// void xmlRaiseError(xmlErrorPtr ctxt,
321///                    xmlErrorPtr ctxt2,
322///                    xmlErrorPtr ctxt3,
323///                    xmlErrorPtr ctxt4,
324///                    xmlErrorPtr ctxt5,
325///                    int domain,
326///                    int code,
327///                    xmlErrorLevel level,
328///                    const char *file,
329///                    int line,
330///                    const char *str1,
331///                    const char *str2,
332///                    const char *str3,
333///                    int int1,
334///                    int int2,
335///                    const char *msg,
336///                    ...);
337/// ```
338///
339/// # SAFETY
340///
341/// - `ctxt` may be NULL (context of the error).
342/// - `domain`, `code`, `level`: valid error codes.
343/// - `msg` must be a valid C string or NULL.
344/// - `file` must be a valid C string or NULL.
345/// - `str1`, `str2`, `str3`: error-related strings (may be NULL).
346#[allow(clippy::too_many_arguments)]
347pub unsafe fn raise_error(
348    ctxt: *mut c_void,
349    _ctxt2: *mut c_void,
350    _ctxt3: *mut c_void,
351    _ctxt4: *mut c_void,
352    _ctxt5: *mut c_void,
353    domain: c_int,
354    code: c_int,
355    level: c_int,
356    file: *const c_char,
357    line: c_int,
358    str1: *const c_char,
359    str2: *const c_char,
360    str3: *const c_char,
361    int1: c_int,
362    _int2: c_int,
363    msg: *const c_char,
364) {
365    // Format the error message
366    let formatted_msg = format_error_message(domain, code, msg, str1, str2, str3);
367
368    // UPSTREAM-PARITY (xmlVSetError): string fields are owned copies so the
369    // stored error survives transient callers.
370    let file_copy = if file.is_null() {
371        ptr::null_mut()
372    } else {
373        crate::abi::allocator::xmlMemStrdupImpl(file) as *mut c_char
374    };
375    let str1_copy = if str1.is_null() {
376        ptr::null_mut()
377    } else {
378        crate::abi::allocator::xmlMemStrdupImpl(str1) as *mut c_char
379    };
380    let str2_copy = if str2.is_null() {
381        ptr::null_mut()
382    } else {
383        crate::abi::allocator::xmlMemStrdupImpl(str2) as *mut c_char
384    };
385    let str3_copy = if str3.is_null() {
386        ptr::null_mut()
387    } else {
388        crate::abi::allocator::xmlMemStrdupImpl(str3) as *mut c_char
389    };
390
391    // Store the last error
392    let err = _xmlError {
393        domain,
394        code,
395        message: formatted_msg,
396        level,
397        file: file_copy,
398        line,
399        str1: str1_copy,
400        str2: str2_copy,
401        str3: str3_copy,
402        int1,
403        int2: 0,
404        ctxt,
405        node: ptr::null_mut(),
406    };
407
408    globals::set_last_error(err);
409
410    // UPSTREAM-PARITY (xmlCtxtVErr): the structured handler wins; the
411    // generic channel is only used when no structured handler is set. The
412    // (handler, ctx) pairs are read atomically (11.1-X), then invoked
413    // outside the lock so a handler that re-enters the library cannot
414    // deadlock.
415    let structured = globals::with_structured_error(|h, c| (h, c));
416    if let Some(handler) = structured.0 {
417        let err_ref = globals::get_last_error();
418        if !err_ref.is_null() {
419            handler(structured.1, err_ref as *const _xmlError);
420        }
421    } else if level != 0 {
422        let generic = globals::with_generic_error(|h, c| (h, c));
423        if let Some(handler) = generic.0 {
424            let ctx = generic.1;
425            if !formatted_msg.is_null() {
426                handler(ctx, formatted_msg as *const core::ffi::c_char);
427            } else if !msg.is_null() {
428                handler(ctx, msg);
429            }
430        }
431    }
432
433    // Free the formatted message if it was allocated
434    // Note: We keep it as the last error's message, so we don't free it here.
435    // The next call to raise_error or reset_error will free the old message.
436    // Actually, in Phase 1, we don't free because the message is the last error's.
437    // A more complete implementation would free the old message when setting a new one.
438}
439
440/// Emit a legacy-format message through the generic error channel.
441///
442/// Upstream's `xmlGenericErrorDefaultFunc` writes the message to stderr;
443/// when a custom generic handler is installed it receives the message. The
444/// `level` prefix matches upstream `xmlVFormatLegacyError` (error.c 2.15).
445unsafe fn emit_legacy_message(level: &str, msg: *const c_char) {
446    if msg.is_null() {
447        return;
448    }
449    let len = libc::strlen(msg) as usize;
450    let text = core::slice::from_raw_parts(msg as *const u8, len);
451    let mut full = Vec::with_capacity(level.len() + 2 + len);
452    full.extend_from_slice(level.as_bytes());
453    full.push(b':');
454    full.push(b' ');
455    full.extend_from_slice(text);
456    if let Some(handler) = globals::get_generic_error_func() {
457        let ctx = globals::get_generic_error_ctx();
458        let mut cmsg = full.clone();
459        cmsg.push(0);
460        handler(ctx, cmsg.as_ptr() as *const c_char);
461    } else {
462        // Upstream default (xmlGenericErrorDefaultFunc): stderr.
463        let _ = libc::write(2, full.as_ptr() as *const libc::c_void, full.len());
464    }
465}
466
467// ═══════════════════════════════════════════════════════════════════════════════
468// Generic-channel fragment streaming (upstream error.c `xmlFormatError`)
469// ═══════════════════════════════════════════════════════════════════════════════
470//
471// Upstream streams each error through the generic channel as a sequence of
472// variadic calls (e.g. `channel(data, "%s:%d: ", file, line)` followed by the
473// domain, level, message and source-context fragments). Custom handlers and the
474// built-in default (an x86_64 SysV va_list shim, see data_globals.rs) both
475// observe the same per-fragment calls. Stable Rust cannot express a variadic
476// call, so each fragment goes through a tiny x86_64 trampoline that places the
477// fixed arguments in the ABI registers and does an indirect call.
478
479/// `channel(data, fmt)` — no variadic arguments.
480#[cfg(target_arch = "x86_64")]
481#[inline]
482unsafe fn ch_call0(handler: xmlGenericErrorFunc, data: *mut c_void, fmt: *const c_char) {
483    // SAFETY: `handler` is a C-compatible generic error callback; per the
484    // SysV ABI the callee sees (data, fmt) with no additional registers
485    // consumed (rdx/rcx zeroed so a va_list-reading callee finds nothing).
486    // The compiler guarantees 16-byte stack alignment at the asm block, so
487    // the `call` is correctly aligned.
488    unsafe {
489        core::arch::asm!(
490            "xor edx, edx",
491            "xor ecx, ecx",
492            "call {h}",
493            h = in(reg) handler as usize,
494            in("rdi") data,
495            in("rsi") fmt,
496            out("rdx") _, out("rcx") _,
497            lateout("rax") _, lateout("r8") _, lateout("r9") _, lateout("r10") _, lateout("r11") _,
498        );
499    }
500}
501
502/// `channel(data, fmt, a1)` — one pointer-sized variadic argument.
503#[cfg(target_arch = "x86_64")]
504#[inline]
505unsafe fn ch_call1(handler: xmlGenericErrorFunc, data: *mut c_void, fmt: *const c_char, a1: usize) {
506    // SAFETY: as ch_call0; `a1` lands in the va_list slot after the two
507    // fixed args (rdx).
508    unsafe {
509        core::arch::asm!(
510            "xor ecx, ecx",
511            "call {h}",
512            h = in(reg) handler as usize,
513            in("rdi") data,
514            in("rsi") fmt,
515            in("rdx") a1,
516            out("rcx") _,
517            lateout("rax") _, lateout("r8") _, lateout("r9") _, lateout("r10") _, lateout("r11") _,
518        );
519    }
520}
521
522/// `channel(data, fmt, a1, a2)` — two pointer-sized variadic arguments.
523#[cfg(target_arch = "x86_64")]
524#[inline]
525unsafe fn ch_call2(
526    handler: xmlGenericErrorFunc,
527    data: *mut c_void,
528    fmt: *const c_char,
529    a1: usize,
530    a2: usize,
531) {
532    // SAFETY: as ch_call0; a1/a2 land in the va_list slots (rdx, rcx).
533    unsafe {
534        core::arch::asm!(
535            "call {h}",
536            h = in(reg) handler as usize,
537            in("rdi") data,
538            in("rsi") fmt,
539            in("rdx") a1,
540            in("rcx") a2,
541            lateout("rax") _, lateout("r8") _, lateout("r9") _, lateout("r10") _, lateout("r11") _,
542        );
543    }
544}
545
546/// Emit one raise through the generic channel with upstream's
547/// `xmlFormatError` fragment sequence (error.c 2.15): file/line prefix,
548/// domain, level, message, then the source window and caret line.
549///
550/// `file`/`line` come from the raising site's input; `source_window` is the
551/// current input line text plus the 0-based caret column (upstream
552/// `xmlParserInputGetWindow`).
553///
554/// # SAFETY
555///
556/// - `file` and `message` must be valid C strings or NULL.
557/// - `source_window` bytes must be valid for the duration of the call.
558#[allow(clippy::too_many_arguments)]
559#[cfg(target_arch = "x86_64")]
560unsafe fn format_error_streamed(
561    domain: c_int,
562    code: c_int,
563    level: c_int,
564    message: *const c_char,
565    file: *const c_char,
566    line: c_int,
567    source_window: Option<(&[u8], usize)>,
568    enc_bytes: Option<[u8; 4]>,
569) {
570    // SAFETY: reads the exported C globals (upstream reads the same).
571    let Some(handler) = globals::get_generic_error_func() else {
572        return;
573    };
574    let data = globals::get_generic_error_ctx();
575
576    // 1. File/line prefix (xmlFormatError).
577    if !file.is_null() {
578        ch_call2(
579            handler,
580            data,
581            c"%s:%d: ".as_ptr() as *const c_char,
582            file as usize,
583            line as usize,
584        );
585    } else if line != 0
586        && (domain == XML_FROM_PARSER
587            || domain == XML_FROM_SCHEMASV
588            || domain == XML_FROM_SCHEMASP
589            || domain == XML_FROM_DTD
590            || domain == XML_FROM_RELAXNGP
591            || domain == XML_FROM_RELAXNGV)
592    {
593        ch_call1(
594            handler,
595            data,
596            c"Entity: line %d: ".as_ptr() as *const c_char,
597            line as usize,
598        );
599    }
600
601    // 2. Domain fragment (xmlFormatError switch).
602    let dom: &[u8] = match domain {
603        XML_FROM_PARSER => b"parser \0",
604        XML_FROM_NAMESPACE => b"namespace \0",
605        XML_FROM_DTD | XML_FROM_VALID => b"validity \0",
606        XML_FROM_HTML => b"HTML parser \0",
607        XML_FROM_MEMORY => b"memory \0",
608        XML_FROM_OUTPUT => b"output \0",
609        XML_FROM_IO => b"I/O \0",
610        XML_FROM_XINCLUDE => b"XInclude \0",
611        XML_FROM_XPATH => b"XPath \0",
612        XML_FROM_XPOINTER => b"parser \0",
613        XML_FROM_REGEXP => b"regexp \0",
614        XML_FROM_MODULE => b"module \0",
615        XML_FROM_SCHEMASV => b"Schemas validity \0",
616        XML_FROM_SCHEMASP => b"Schemas parser \0",
617        XML_FROM_RELAXNGP => b"Relax-NG parser \0",
618        XML_FROM_RELAXNGV => b"Relax-NG validity \0",
619        XML_FROM_CATALOG => b"Catalog \0",
620        XML_FROM_C14N => b"C14N \0",
621        XML_FROM_XSLT => b"XSLT \0",
622        XML_FROM_I18N => b"encoding \0",
623        XML_FROM_SCHEMATRONV => b"schematron \0",
624        XML_FROM_BUFFER => b"internal buffer \0",
625        XML_FROM_URI => b"URI \0",
626        _ => b"\0",
627    };
628    if !dom.is_empty() && dom[0] != 0 {
629        ch_call0(handler, data, dom.as_ptr() as *const c_char);
630    }
631
632    // 3. Level fragment (xmlFormatError switch).
633    let lvl: &[u8] = if level == XML_ERR_NONE as c_int {
634        b": \0"
635    } else if level == XML_ERR_WARNING as c_int {
636        b"warning : \0"
637    } else if level == XML_ERR_ERROR as c_int || level == XML_ERR_FATAL as c_int {
638        b"error : \0"
639    } else {
640        b"\0"
641    };
642    if !lvl.is_empty() && lvl[0] != 0 {
643        ch_call0(handler, data, lvl.as_ptr() as *const c_char);
644    }
645
646    // 4. Message fragment.
647    if !message.is_null() {
648        let msg = message as *const u8;
649        let mut len = 0usize;
650        while unsafe { *msg.add(len) } != 0 {
651            len += 1;
652        }
653        let ends_nl = len > 0 && unsafe { *msg.add(len - 1) } == b'\n';
654        let fmt: &[u8] = if ends_nl { b"%s\0" } else { b"%s\n\0" };
655        ch_call1(handler, data, fmt.as_ptr() as *const c_char, msg as usize);
656    }
657
658    // 4b. Invalid-encoding byte dump (upstream xmlFormatError: the first 4
659    // bytes at the error position, only for XML_ERR_INVALID_ENCODING).
660    if code == XML_ERR_INVALID_ENCODING {
661        if let Some(bytes) = enc_bytes {
662            ch_call0(handler, data, c"Bytes:".as_ptr() as *const c_char);
663            for b in bytes {
664                // " 0x%02X"
665                let hex = format!(" 0x{:02X}\0", b);
666                ch_call0(handler, data, hex.as_ptr() as *const c_char);
667            }
668            ch_call0(handler, data, c"\n".as_ptr() as *const c_char);
669        }
670    }
671
672    // 5. Source window + caret (xmlParserPrintFileContextInternal).
673    if let Some((window, caret)) = source_window {
674        let mut win = window.to_vec();
675        win.push(0);
676        ch_call1(
677            handler,
678            data,
679            c"%s\n".as_ptr() as *const c_char,
680            win.as_ptr() as usize,
681        );
682        let mut caret_line = Vec::with_capacity(caret + 2);
683        for &b in window.iter().take(caret) {
684            caret_line.push(if b == b'\t' { b'\t' } else { b' ' });
685        }
686        caret_line.push(b'^');
687        caret_line.push(0);
688        ch_call1(
689            handler,
690            data,
691            c"%s\n".as_ptr() as *const c_char,
692            caret_line.as_ptr() as usize,
693        );
694    }
695}
696
697/// How a raise delivers to the generic side of the error system (upstream
698/// `xmlVRaiseError` channel selection, error.c 2.15).
699#[derive(Clone, Copy, Debug)]
700pub enum GenericDelivery {
701    /// Custom SAX channel: single call `channel(ctx, msg)`.
702    Custom(xmlGenericErrorFunc, *mut c_void),
703    /// Legacy/default channel: stream the `xmlFormatError` fragments through
704    /// the global generic handler.
705    Stream,
706    /// No channel (SAX slot NULL): no generic delivery.
707    None,
708}
709
710/// Raise an error with upstream's full routing (error.c 2.15
711/// `xmlVRaiseError`): update the last error, then deliver to the structured
712/// handler **or** the selected generic channel — never both.
713///
714/// `file`/`line`/`source_window` feed the generic fragment stream (the
715/// structured handler receives the complete `xmlError` instead). `col` is
716/// the 1-based byte column (upstream `input->col` → `err->int2`); `str1`..
717/// `str3`/`int1` are the upstream extra fields; `enc_bytes` feeds the
718/// `XML_ERR_INVALID_ENCODING` "Bytes:" fragment.
719///
720/// # UPSTREAM-PARITY (ownership)
721///
722/// Like upstream `xmlVSetError`, every string field of the stored error is
723/// owned (`xmlStrdup`): `file`/`str1`/`str2`/`str3` are heap copies, so the
724/// caller may pass transient C strings.
725///
726/// # SAFETY
727///
728/// - `ctxt` may be NULL.
729/// - `msg`, `file`, `str1`, `str2`, `str3` must be valid C strings or NULL.
730/// - `source_window` bytes must be valid for the duration of the call.
731#[allow(clippy::too_many_arguments)]
732pub unsafe fn raise_error_streamed(
733    ctxt: *mut c_void,
734    domain: c_int,
735    code: c_int,
736    level: c_int,
737    file: *const c_char,
738    line: c_int,
739    col: c_int,
740    str1: *const c_char,
741    str2: *const c_char,
742    str3: *const c_char,
743    int1: c_int,
744    msg: *const c_char,
745    source_window: Option<(&[u8], usize)>,
746    enc_bytes: Option<[u8; 4]>,
747    delivery: GenericDelivery,
748) {
749    // The streamed generic-error channel below uses an x86_64 SysV va_list
750    // trampoline (ch_call0/1/2 — register-based). Other ABIs (i686 cdecl,
751    // ARM/aarch64 AAPCS, ...) fall back to the plain raise path; full
752    // streamed-fragment parity there is an unexecuted platform obligation
753    // (atlas/PLATFORM_SURFACE_ATLAS.md, OBLIG-WORDSIZE-32 / compiler-ABI).
754    #[cfg(not(target_arch = "x86_64"))]
755    {
756        raise_error(
757            ctxt,
758            ptr::null_mut(),
759            ptr::null_mut(),
760            ptr::null_mut(),
761            ptr::null_mut(),
762            domain,
763            code,
764            level,
765            file,
766            line,
767            str1,
768            str2,
769            str3,
770            int1,
771            col,
772            msg,
773        );
774        return;
775    }
776
777    #[cfg(target_arch = "x86_64")]
778    {
779        // UPSTREAM-PARITY (xmlVRaiseError): warnings are suppressed when the
780        // global xmlGetWarningsDefaultValue is zero.
781        if level == xmlErrorLevel::XML_ERR_WARNING as c_int
782            && unsafe { crate::abi::data_globals::xmlGetWarningsDefaultValue } == 0
783        {
784            return;
785        }
786
787        raise_error_streamed_x86_64(
788            ctxt,
789            domain,
790            code,
791            level,
792            file,
793            line,
794            col,
795            str1,
796            str2,
797            str3,
798            int1,
799            msg,
800            source_window,
801            enc_bytes,
802            delivery,
803        );
804    }
805}
806
807/// x86-64 streamed raise (SysV va_list channel). See `raise_error_streamed`.
808#[allow(clippy::too_many_arguments)]
809#[cfg(target_arch = "x86_64")]
810unsafe fn raise_error_streamed_x86_64(
811    ctxt: *mut c_void,
812    domain: c_int,
813    code: c_int,
814    level: c_int,
815    file: *const c_char,
816    line: c_int,
817    col: c_int,
818    str1: *const c_char,
819    str2: *const c_char,
820    str3: *const c_char,
821    int1: c_int,
822    msg: *const c_char,
823    source_window: Option<(&[u8], usize)>,
824    enc_bytes: Option<[u8; 4]>,
825    delivery: GenericDelivery,
826) {
827    // UPSTREAM-PARITY (xmlVRaiseError): warnings are suppressed when the
828    // global xmlGetWarningsDefaultValue is zero.
829    if level == xmlErrorLevel::XML_ERR_WARNING as c_int
830        && unsafe { crate::abi::data_globals::xmlGetWarningsDefaultValue } == 0
831    {
832        return;
833    }
834
835    // Format the error message (same as raise_error).
836    let formatted_msg = format_error_message(domain, code, msg, str1, str2, str3);
837    let file_copy = if file.is_null() {
838        ptr::null_mut()
839    } else {
840        crate::abi::allocator::xmlMemStrdupImpl(file) as *mut c_char
841    };
842    let str1_copy = if str1.is_null() {
843        ptr::null_mut()
844    } else {
845        crate::abi::allocator::xmlMemStrdupImpl(str1) as *mut c_char
846    };
847    let str2_copy = if str2.is_null() {
848        ptr::null_mut()
849    } else {
850        crate::abi::allocator::xmlMemStrdupImpl(str2) as *mut c_char
851    };
852    let str3_copy = if str3.is_null() {
853        ptr::null_mut()
854    } else {
855        crate::abi::allocator::xmlMemStrdupImpl(str3) as *mut c_char
856    };
857    let err = _xmlError {
858        domain,
859        code,
860        message: formatted_msg,
861        level,
862        file: file_copy,
863        line,
864        str1: str1_copy,
865        str2: str2_copy,
866        str3: str3_copy,
867        int1,
868        int2: col,
869        ctxt,
870        node: ptr::null_mut(),
871    };
872
873    globals::set_last_error(err);
874
875    // Structured handler wins (upstream `else if` chain); the (handler, ctx)
876    // pair is read atomically and invoked outside the lock (11.1-X).
877    let structured = globals::with_structured_error(|h, c| (h, c));
878    if let Some(handler) = structured.0 {
879        let err_ref = globals::get_last_error();
880        if !err_ref.is_null() {
881            handler(structured.1, err_ref as *const _xmlError);
882        }
883        return;
884    }
885
886    match delivery {
887        GenericDelivery::Custom(channel, ctx) => {
888            if !msg.is_null() {
889                // SAFETY: the caller provided a valid C callback.
890                unsafe { channel(ctx, msg) };
891            }
892        }
893        GenericDelivery::Stream => {
894            if globals::get_generic_error_func().is_some() {
895                unsafe {
896                    format_error_streamed(
897                        domain,
898                        code,
899                        level,
900                        formatted_msg,
901                        file,
902                        line,
903                        source_window,
904                        enc_bytes,
905                    )
906                };
907            }
908        }
909        GenericDelivery::None => {}
910    }
911}
912
913/// Default SAX v1 error handler — `void xmlParserError(void *ctx, const char *msg, ...)`.
914///
915/// # SAFETY
916///
917/// - `ctx` may be NULL (unused by the candidate's legacy path).
918/// - `msg` must be a valid NUL-terminated C string or NULL.
919#[no_mangle]
920pub unsafe extern "C" fn xmlParserError(ctx: *mut c_void, msg: *const c_char) {
921    let _ = ctx;
922    unsafe { emit_legacy_message("error", msg) };
923}
924
925/// Default SAX v1 warning handler — `void xmlParserWarning(void *ctx, const char *msg, ...)`.
926///
927/// # SAFETY
928///
929/// - `ctx` must be valid pointers (or NULL
930///   where the upstream C contract allows), obtained from the
931///   matching constructor/owner and not yet freed; the callee may
932///   take or keep ownership exactly as the C API specifies.
933///
934/// - `msg` must point to valid NUL-terminated
935///   strings (or NULL where the C contract allows) for the lifetime
936///   of the call.
937///
938/// The caller must not race this call with concurrent mutation of the
939/// same objects from other threads (per-object state is not internally
940/// synchronized). Violating any of the above is undefined behavior.
941///
942/// Exercised by the C-API differential courts
943/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
944/// courts; those pass byte-for-byte against the upstream oracle.
945#[no_mangle]
946pub unsafe extern "C" fn xmlParserWarning(ctx: *mut c_void, msg: *const c_char) {
947    let _ = ctx;
948    unsafe { emit_legacy_message("warning", msg) };
949}
950
951/// Default validity error handler — `void xmlParserValidityError(void *ctx, const char *msg, ...)`.
952///
953/// # SAFETY
954///
955/// - `ctx` must be valid pointers (or NULL
956///   where the upstream C contract allows), obtained from the
957///   matching constructor/owner and not yet freed; the callee may
958///   take or keep ownership exactly as the C API specifies.
959///
960/// - `msg` must point to valid NUL-terminated
961///   strings (or NULL where the C contract allows) for the lifetime
962///   of the call.
963///
964/// The caller must not race this call with concurrent mutation of the
965/// same objects from other threads (per-object state is not internally
966/// synchronized). Violating any of the above is undefined behavior.
967///
968/// Exercised by the C-API differential courts
969/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
970/// courts; those pass byte-for-byte against the upstream oracle.
971#[no_mangle]
972pub unsafe extern "C" fn xmlParserValidityError(ctx: *mut c_void, msg: *const c_char) {
973    let _ = ctx;
974    unsafe { emit_legacy_message("validity error", msg) };
975}
976
977/// Default validity warning handler — `void xmlParserValidityWarning(void *ctx, const char *msg, ...)`.
978///
979/// # SAFETY
980///
981/// - `ctx` must be valid pointers (or NULL
982///   where the upstream C contract allows), obtained from the
983///   matching constructor/owner and not yet freed; the callee may
984///   take or keep ownership exactly as the C API specifies.
985///
986/// - `msg` must point to valid NUL-terminated
987///   strings (or NULL where the C contract allows) for the lifetime
988///   of the call.
989///
990/// The caller must not race this call with concurrent mutation of the
991/// same objects from other threads (per-object state is not internally
992/// synchronized). Violating any of the above is undefined behavior.
993///
994/// Exercised by the C-API differential courts
995/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
996/// courts; those pass byte-for-byte against the upstream oracle.
997#[no_mangle]
998pub unsafe extern "C" fn xmlParserValidityWarning(ctx: *mut c_void, msg: *const c_char) {
999    let _ = ctx;
1000    unsafe { emit_legacy_message("validity warning", msg) };
1001}
1002
1003// ═══════════════════════════════════════════════════════════════════════════════
1004// Tests
1005// ═══════════════════════════════════════════════════════════════════════════════
1006
1007#[cfg(test)]
1008mod tests {
1009    use super::*;
1010    use crate::abi::allocator;
1011    use core::ffi::c_void;
1012
1013    #[test]
1014    fn test_error_default_reset() {
1015        unsafe {
1016            let mut err = _xmlError {
1017                domain: XML_FROM_PARSER,
1018                code: XML_ERR_NO_MEMORY,
1019                message: ptr::null_mut(),
1020                level: XML_ERR_ERROR as c_int,
1021                file: ptr::null_mut(),
1022                line: 42,
1023                str1: ptr::null_mut(),
1024                str2: ptr::null_mut(),
1025                str3: ptr::null_mut(),
1026                int1: 0,
1027                int2: 0,
1028                ctxt: ptr::null_mut(),
1029                node: ptr::null_mut(),
1030            };
1031
1032            reset_error(&mut err);
1033            assert_eq!(err.domain, XML_FROM_NONE);
1034            assert_eq!(err.code, XML_ERR_OK as c_int);
1035            assert_eq!(err.level, XML_ERR_NONE as c_int);
1036            assert_eq!(err.line, 0);
1037        }
1038    }
1039
1040    #[test]
1041    fn test_copy_error() {
1042        unsafe {
1043            let from = _xmlError {
1044                domain: XML_FROM_PARSER,
1045                code: XML_ERR_NO_MEMORY,
1046                message: ptr::null_mut(),
1047                level: XML_ERR_FATAL as c_int,
1048                file: ptr::null_mut(),
1049                line: 100,
1050                str1: ptr::null_mut(),
1051                str2: ptr::null_mut(),
1052                str3: ptr::null_mut(),
1053                int1: 1,
1054                int2: 2,
1055                ctxt: ptr::null_mut(),
1056                node: ptr::null_mut(),
1057            };
1058            let mut to = _xmlError {
1059                domain: XML_FROM_NONE,
1060                code: XML_ERR_OK as c_int,
1061                message: ptr::null_mut(),
1062                level: XML_ERR_NONE as c_int,
1063                file: ptr::null_mut(),
1064                line: 0,
1065                str1: ptr::null_mut(),
1066                str2: ptr::null_mut(),
1067                str3: ptr::null_mut(),
1068                int1: 0,
1069                int2: 0,
1070                ctxt: ptr::null_mut(),
1071                node: ptr::null_mut(),
1072            };
1073
1074            let result = copy_error(&from, &mut to);
1075            assert_eq!(result, 0);
1076            assert_eq!(to.domain, XML_FROM_PARSER);
1077            assert_eq!(to.code, XML_ERR_NO_MEMORY);
1078            assert_eq!(to.level, XML_ERR_FATAL as c_int);
1079            assert_eq!(to.line, 100);
1080            assert_eq!(to.int1, 1);
1081            assert_eq!(to.int2, 2);
1082        }
1083    }
1084
1085    #[test]
1086    fn test_raise_and_get_last_error() {
1087        unsafe {
1088            reset_last_error();
1089            assert!(get_last_error().is_null());
1090
1091            let file = b"test.xml\0" as *const u8 as *const c_char;
1092            let str1 = b"element\0" as *const u8 as *const c_char;
1093
1094            raise_error(
1095                ptr::null_mut(),
1096                ptr::null_mut(),
1097                ptr::null_mut(),
1098                ptr::null_mut(),
1099                ptr::null_mut(),
1100                XML_FROM_PARSER,
1101                XML_ERR_TAG_NAME_MISMATCH,
1102                XML_ERR_ERROR as c_int,
1103                file,
1104                10,
1105                str1,
1106                ptr::null(),
1107                ptr::null(),
1108                0,
1109                0,
1110                ptr::null(),
1111            );
1112
1113            let last = get_last_error();
1114            assert!(!last.is_null());
1115            assert_eq!((*last).domain, XML_FROM_PARSER);
1116            assert_eq!((*last).code, XML_ERR_TAG_NAME_MISMATCH);
1117            assert_eq!((*last).level, XML_ERR_ERROR as c_int);
1118            assert_eq!((*last).line, 10);
1119
1120            // Check file was stored
1121            let last_file = (*last).file;
1122            assert!(!last_file.is_null());
1123
1124            reset_last_error();
1125            assert!(get_last_error().is_null());
1126        }
1127    }
1128
1129    #[test]
1130    fn test_structured_error_callback() {
1131        // Serialized against the handler-slot tests in xml::globals (11.1-X):
1132        // the structured handler slot is shared global state.
1133        let _guard = crate::xml::globals::ERROR_HANDLER_TEST_LOCK.lock();
1134        unsafe {
1135            reset_last_error();
1136
1137            // Set up a structured error handler that captures the error
1138            let mut captured_domain: c_int = 0;
1139            let captured_ptr = &mut captured_domain as *mut c_int as *mut c_void;
1140
1141            // SAFETY: The callback writes to captured_ptr which lives on the stack
1142            // for the duration of this test.
1143            extern "C" fn test_handler(ctx: *mut c_void, _err: *const _xmlError) {
1144                // SAFETY: ctx is valid for the test duration.
1145                unsafe {
1146                    let captured = &mut *(ctx as *mut c_int);
1147                    *captured = 42;
1148                }
1149            }
1150
1151            set_structured_error_func(captured_ptr, Some(test_handler as xmlStructuredErrorFunc));
1152
1153            raise_error(
1154                ptr::null_mut(),
1155                ptr::null_mut(),
1156                ptr::null_mut(),
1157                ptr::null_mut(),
1158                ptr::null_mut(),
1159                XML_FROM_PARSER,
1160                XML_ERR_OK as c_int,
1161                XML_ERR_WARNING as c_int,
1162                ptr::null(),
1163                0,
1164                ptr::null(),
1165                ptr::null(),
1166                ptr::null(),
1167                0,
1168                0,
1169                ptr::null(),
1170            );
1171
1172            assert_eq!(captured_domain, 42);
1173
1174            // Reset
1175            set_structured_error_func(ptr::null_mut(), None);
1176            reset_last_error();
1177        }
1178    }
1179
1180    #[test]
1181    fn test_format_error_message() {
1182        unsafe {
1183            // Test with direct message
1184            let msg = b"test error\0" as *const u8 as *const c_char;
1185            let formatted = format_error_message(
1186                XML_FROM_NONE,
1187                XML_ERR_OK as c_int,
1188                msg,
1189                ptr::null(),
1190                ptr::null(),
1191                ptr::null(),
1192            );
1193            assert!(!formatted.is_null());
1194            let formatted_str = std::ffi::CStr::from_ptr(formatted);
1195            assert_eq!(formatted_str.to_bytes(), b"test error");
1196
1197            // Free the allocated message
1198            allocator::xmlFreeImpl(formatted as *mut c_void);
1199
1200            // Test with domain and str1
1201            let str1 = b"foo\0" as *const u8 as *const c_char;
1202            let formatted2 = format_error_message(
1203                XML_FROM_PARSER,
1204                XML_ERR_OK as c_int,
1205                ptr::null(),
1206                str1,
1207                ptr::null(),
1208                ptr::null(),
1209            );
1210            assert!(!formatted2.is_null());
1211            allocator::xmlFreeImpl(formatted2 as *mut c_void);
1212        }
1213    }
1214}