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::fmt::Write;
33use core::ptr;
34use std::os::raw::{c_char, c_int};
35
36use crate::abi::callbacks::{xmlGenericErrorFunc, xmlStructuredErrorFunc};
37use crate::abi::structs::_xmlError;
38use crate::abi::types::xmlErrorLevel::*;
39use crate::abi::types::*;
40use crate::xml::globals;
41
42// ═══════════════════════════════════════════════════════════════════════════════
43// Error Management Functions
44// ═══════════════════════════════════════════════════════════════════════════════
45
46/// Set the generic error handler.
47///
48/// # UPSTREAM-PARITY
49///
50/// ```c
51/// void xmlSetGenericErrorFunc(void *ctx, xmlGenericErrorFunc handler);
52/// ```
53///
54/// # SAFETY
55///
56/// - `handler` must be a valid function pointer or NULL (to reset to default).
57/// - If non-NULL, the handler may be called at any time with `ctx`.
58pub unsafe fn set_generic_error_func(ctx: *mut c_void, handler: Option<xmlGenericErrorFunc>) {
59    // SAFETY: Delegates to globals with same safety contract.
60    unsafe { globals::set_generic_error_func(ctx, handler) };
61}
62
63/// Set the structured error handler.
64///
65/// # UPSTREAM-PARITY
66///
67/// ```c
68/// void xmlSetStructuredErrorFunc(void *ctx, xmlStructuredErrorFunc handler);
69/// ```
70///
71/// # SAFETY
72///
73/// - `handler` must be a valid function pointer or NULL.
74pub unsafe fn set_structured_error_func(ctx: *mut c_void, handler: Option<xmlStructuredErrorFunc>) {
75    // SAFETY: Delegates to globals with same safety contract.
76    unsafe { globals::set_structured_error_func(ctx, handler) };
77}
78
79/// Get the last error for the current thread.
80///
81/// # UPSTREAM-PARITY
82///
83/// ```c
84/// xmlErrorPtr xmlGetLastError(void);
85/// ```
86///
87/// Returns a pointer to the last error, or NULL if no error occurred.
88/// The returned pointer is valid until the next libxml2 call in this thread.
89pub fn get_last_error() -> *mut _xmlError {
90    globals::get_last_error()
91}
92
93/// Copy an error from one location to another.
94///
95/// # UPSTREAM-PARITY
96///
97/// ```c
98/// xmlErrorPtr xmlCopyError(xmlErrorPtr from, xmlErrorPtr to);
99/// ```
100///
101/// Copies `from` into `to`. Returns 0 on success, -1 on error.
102///
103/// # SAFETY
104///
105/// - `from` and `to` must be valid pointers to `_xmlError` structs, or NULL.
106pub unsafe fn copy_error(from: *const _xmlError, to: *mut _xmlError) -> c_int {
107    if from.is_null() || to.is_null() {
108        return -1;
109    }
110    // SAFETY: Caller guarantees both pointers are valid.
111    unsafe {
112        ptr::copy_nonoverlapping(from, to, 1);
113    }
114    0
115}
116
117/// Reset an error structure to its default state.
118///
119/// # UPSTREAM-PARITY
120///
121/// ```c
122/// void xmlResetError(xmlErrorPtr err);
123/// ```
124///
125/// # SAFETY
126///
127/// - `err` must be a valid pointer to `_xmlError`, or NULL.
128pub unsafe fn reset_error(err: *mut _xmlError) {
129    if err.is_null() {
130        return;
131    }
132    // SAFETY: Caller guarantees pointer is valid.
133    unsafe {
134        ptr::write(
135            err,
136            _xmlError {
137                domain: XML_FROM_NONE,
138                code: XML_ERR_OK as c_int,
139                message: ptr::null_mut(),
140                level: XML_ERR_NONE as c_int,
141                file: ptr::null_mut(),
142                line: 0,
143                str1: ptr::null_mut(),
144                str2: ptr::null_mut(),
145                str3: ptr::null_mut(),
146                int1: 0,
147                int2: 0,
148                ctxt: ptr::null_mut(),
149                node: ptr::null_mut(),
150            },
151        );
152    }
153}
154
155/// Reset the last error for the current thread.
156///
157/// # UPSTREAM-PARITY
158///
159/// ```c
160/// void xmlResetLastError(void);
161/// ```
162pub fn reset_last_error() {
163    globals::reset_last_error();
164}
165
166/// Format an error message.
167///
168/// This function creates a formatted error message from the component parts.
169/// In Phase 1, this is a basic implementation. In Phase 2+, variadic
170/// printf-style formatting will be added.
171///
172/// Returns a C string pointer (allocated with xmlMalloc) that the caller
173/// must free with xmlFreeImpl, or NULL on allocation failure.
174///
175/// # UPSTREAM-PARITY
176///
177/// Upstream libxml2 uses `vsnprintf` internally for message formatting.
178/// We use a simple formatting approach that produces compatible output
179/// for the common error patterns.
180pub fn format_error_message(
181    _domain: c_int,
182    _code: c_int,
183    msg: *const c_char,
184    str1: *const c_char,
185    str2: *const c_char,
186    str3: *const c_char,
187) -> *mut c_char {
188    // Phase 1: basic message construction.
189    // If a direct message string is provided, use it.
190    if !msg.is_null() {
191        // SAFETY: Caller guarantees msg is a valid C string.
192        let msg_str = unsafe { crate::abi::allocator::xmlMemStrdupImpl(msg) };
193        return msg_str as *mut c_char;
194    }
195
196    // Build a message from the component strings.
197    // This matches upstream behavior where domain/code are combined
198    // with str1/str2/str3 into a diagnostic message.
199    let mut buf: [u8; 1024] = [0; 1024];
200    let mut pos = 0;
201
202    // Write domain prefix
203    let domain_str = match _domain {
204        XML_FROM_PARSER => "parser",
205        XML_FROM_TREE => "tree",
206        XML_FROM_NAMESPACE => "namespace",
207        XML_FROM_DTD => "dtd",
208        XML_FROM_HTML => "html",
209        XML_FROM_MEMORY => "memory",
210        XML_FROM_OUTPUT => "output",
211        XML_FROM_IO => "io",
212        XML_FROM_XPATH => "xpath",
213        XML_FROM_XPOINTER => "xpointer",
214        XML_FROM_XINCLUDE => "xinclude",
215        XML_FROM_CATALOG => "catalog",
216        XML_FROM_C14N => "c14n",
217        XML_FROM_XSLT => "xslt",
218        XML_FROM_VALID => "valid",
219        XML_FROM_CHECK => "check",
220        XML_FROM_WRITER => "writer",
221        XML_FROM_MODULE => "module",
222        XML_FROM_I18N => "i18n",
223        XML_FROM_SCHEMATRONV => "schematron",
224        XML_FROM_BUFFER => "buffer",
225        XML_FROM_URI => "uri",
226        XML_FROM_NONE => "",
227        XML_FROM_FTP => "ftp",
228        XML_FROM_HTTP => "http",
229        XML_FROM_REGEXP => "regexp",
230        XML_FROM_DATATYPE => "datatype",
231        XML_FROM_SCHEMASP => "schema parser",
232        XML_FROM_SCHEMASV => "schema validator",
233        XML_FROM_RELAXNGP => "relaxng parser",
234        XML_FROM_RELAXNGV => "relaxng validator",
235        _ => "unknown",
236    };
237
238    if !domain_str.is_empty() {
239        let bytes = domain_str.as_bytes();
240        let len = bytes.len().min(buf.len().saturating_sub(pos + 2));
241        buf[pos..pos + len].copy_from_slice(&bytes[..len]);
242        pos += len;
243        buf[pos] = b' ';
244        pos += 1;
245    }
246
247    // Append str1 if present
248    if !str1.is_null() {
249        // SAFETY: Caller guarantees str1 is a valid C string.
250        let s = unsafe { crate::abi::versioning::c_str_to_bytes(str1).unwrap_or_default() };
251        if pos + s.len() + 3 <= buf.len() {
252            buf[pos] = b'\'';
253            pos += 1;
254            buf[pos..pos + s.len()].copy_from_slice(s);
255            pos += s.len();
256            buf[pos] = b'\'';
257            pos += 1;
258            buf[pos] = b' ';
259            pos += 1;
260        }
261    }
262
263    // Append str2 if present
264    if !str2.is_null() {
265        let s = unsafe { crate::abi::versioning::c_str_to_bytes(str2).unwrap_or_default() };
266        if pos + s.len() + 3 <= buf.len() {
267            buf[pos] = b'\'';
268            pos += 1;
269            buf[pos..pos + s.len()].copy_from_slice(s);
270            pos += s.len();
271            buf[pos] = b'\'';
272            pos += 1;
273            buf[pos] = b' ';
274            pos += 1;
275        }
276    }
277
278    // Append str3 if present
279    if !str3.is_null() {
280        let s = unsafe { crate::abi::versioning::c_str_to_bytes(str3).unwrap_or_default() };
281        if pos + s.len() + 3 <= buf.len() {
282            buf[pos] = b'\'';
283            pos += 1;
284            buf[pos..pos + s.len()].copy_from_slice(s);
285            pos += s.len();
286            buf[pos] = b'\'';
287            pos += 1;
288            buf[pos] = b' ';
289            pos += 1;
290        }
291    }
292
293    // Null-terminate
294    if pos < buf.len() {
295        buf[pos] = 0;
296    } else {
297        buf[buf.len() - 1] = 0;
298    }
299
300    // Allocate and return
301    let result = unsafe { crate::abi::allocator::xmlMallocImpl(pos + 1) };
302    if result.is_null() {
303        return ptr::null_mut();
304    }
305    unsafe {
306        ptr::copy_nonoverlapping(buf.as_ptr(), result as *mut u8, pos + 1);
307    }
308    result as *mut c_char
309}
310
311/// Raise an error — the central error reporting function.
312///
313/// This is called internally when an error occurs. It:
314/// 1. Updates the thread-local last error
315/// 2. Invokes the structured error handler if one is set
316/// 3. Invokes the generic error handler if one is set (for warnings/errors)
317///
318/// # UPSTREAM-PARITY
319///
320/// ```c
321/// void xmlRaiseError(xmlErrorPtr ctxt,
322///                    xmlErrorPtr ctxt2,
323///                    xmlErrorPtr ctxt3,
324///                    xmlErrorPtr ctxt4,
325///                    xmlErrorPtr ctxt5,
326///                    int domain,
327///                    int code,
328///                    xmlErrorLevel level,
329///                    const char *file,
330///                    int line,
331///                    const char *str1,
332///                    const char *str2,
333///                    const char *str3,
334///                    int int1,
335///                    int int2,
336///                    const char *msg,
337///                    ...);
338/// ```
339///
340/// # SAFETY
341///
342/// - `ctxt` may be NULL (context of the error).
343/// - `domain`, `code`, `level`: valid error codes.
344/// - `msg` must be a valid C string or NULL.
345/// - `file` must be a valid C string or NULL.
346/// - `str1`, `str2`, `str3`: error-related strings (may be NULL).
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#[cfg(target_arch = "x86_64")]
559unsafe fn format_error_streamed(
560    domain: c_int,
561    code: c_int,
562    level: c_int,
563    message: *const c_char,
564    file: *const c_char,
565    line: c_int,
566    source_window: Option<(&[u8], usize)>,
567    enc_bytes: Option<[u8; 4]>,
568) {
569    // SAFETY: reads the exported C globals (upstream reads the same).
570    let Some(handler) = globals::get_generic_error_func() else {
571        return;
572    };
573    let data = globals::get_generic_error_ctx();
574
575    // 1. File/line prefix (xmlFormatError).
576    if !file.is_null() {
577        ch_call2(
578            handler,
579            data,
580            b"%s:%d: \0".as_ptr() as *const c_char,
581            file as usize,
582            line as usize,
583        );
584    } else if line != 0
585        && (domain == XML_FROM_PARSER
586            || domain == XML_FROM_SCHEMASV
587            || domain == XML_FROM_SCHEMASP
588            || domain == XML_FROM_DTD
589            || domain == XML_FROM_RELAXNGP
590            || domain == XML_FROM_RELAXNGV)
591    {
592        ch_call1(
593            handler,
594            data,
595            b"Entity: line %d: \0".as_ptr() as *const c_char,
596            line as usize,
597        );
598    }
599
600    // 2. Domain fragment (xmlFormatError switch).
601    let dom: &[u8] = match domain {
602        XML_FROM_PARSER => b"parser \0",
603        XML_FROM_NAMESPACE => b"namespace \0",
604        XML_FROM_DTD | XML_FROM_VALID => b"validity \0",
605        XML_FROM_HTML => b"HTML parser \0",
606        XML_FROM_MEMORY => b"memory \0",
607        XML_FROM_OUTPUT => b"output \0",
608        XML_FROM_IO => b"I/O \0",
609        XML_FROM_XINCLUDE => b"XInclude \0",
610        XML_FROM_XPATH => b"XPath \0",
611        XML_FROM_XPOINTER => b"parser \0",
612        XML_FROM_REGEXP => b"regexp \0",
613        XML_FROM_MODULE => b"module \0",
614        XML_FROM_SCHEMASV => b"Schemas validity \0",
615        XML_FROM_SCHEMASP => b"Schemas parser \0",
616        XML_FROM_RELAXNGP => b"Relax-NG parser \0",
617        XML_FROM_RELAXNGV => b"Relax-NG validity \0",
618        XML_FROM_CATALOG => b"Catalog \0",
619        XML_FROM_C14N => b"C14N \0",
620        XML_FROM_XSLT => b"XSLT \0",
621        XML_FROM_I18N => b"encoding \0",
622        XML_FROM_SCHEMATRONV => b"schematron \0",
623        XML_FROM_BUFFER => b"internal buffer \0",
624        XML_FROM_URI => b"URI \0",
625        _ => b"\0",
626    };
627    if !dom.is_empty() && dom[0] != 0 {
628        ch_call0(handler, data, dom.as_ptr() as *const c_char);
629    }
630
631    // 3. Level fragment (xmlFormatError switch).
632    let lvl: &[u8] = if level == XML_ERR_NONE as c_int {
633        b": \0"
634    } else if level == XML_ERR_WARNING as c_int {
635        b"warning : \0"
636    } else if level == XML_ERR_ERROR as c_int || level == XML_ERR_FATAL as c_int {
637        b"error : \0"
638    } else {
639        b"\0"
640    };
641    if !lvl.is_empty() && lvl[0] != 0 {
642        ch_call0(handler, data, lvl.as_ptr() as *const c_char);
643    }
644
645    // 4. Message fragment.
646    if !message.is_null() {
647        let msg = message as *const u8;
648        let mut len = 0usize;
649        while unsafe { *msg.add(len) } != 0 {
650            len += 1;
651        }
652        let ends_nl = len > 0 && unsafe { *msg.add(len - 1) } == b'\n';
653        let fmt: &[u8] = if ends_nl { b"%s\0" } else { b"%s\n\0" };
654        ch_call1(handler, data, fmt.as_ptr() as *const c_char, msg as usize);
655    }
656
657    // 4b. Invalid-encoding byte dump (upstream xmlFormatError: the first 4
658    // bytes at the error position, only for XML_ERR_INVALID_ENCODING).
659    if code == XML_ERR_INVALID_ENCODING {
660        if let Some(bytes) = enc_bytes {
661            ch_call0(handler, data, b"Bytes:\0".as_ptr() as *const c_char);
662            for b in bytes {
663                // " 0x%02X"
664                let hex = format!(" 0x{:02X}\0", b);
665                ch_call0(handler, data, hex.as_ptr() as *const c_char);
666            }
667            ch_call0(handler, data, b"\n\0".as_ptr() as *const c_char);
668        }
669    }
670
671    // 5. Source window + caret (xmlParserPrintFileContextInternal).
672    if let Some((window, caret)) = source_window {
673        let mut win = window.to_vec();
674        win.push(0);
675        ch_call1(
676            handler,
677            data,
678            b"%s\n\0".as_ptr() as *const c_char,
679            win.as_ptr() as usize,
680        );
681        let mut caret_line = Vec::with_capacity(caret + 2);
682        for &b in window.iter().take(caret) {
683            caret_line.push(if b == b'\t' { b'\t' } else { b' ' });
684        }
685        caret_line.push(b'^');
686        caret_line.push(0);
687        ch_call1(
688            handler,
689            data,
690            b"%s\n\0".as_ptr() as *const c_char,
691            caret_line.as_ptr() as usize,
692        );
693    }
694}
695
696/// How a raise delivers to the generic side of the error system (upstream
697/// `xmlVRaiseError` channel selection, error.c 2.15).
698#[derive(Clone, Copy)]
699pub enum GenericDelivery {
700    /// Custom SAX channel: single call `channel(ctx, msg)`.
701    Custom(xmlGenericErrorFunc, *mut c_void),
702    /// Legacy/default channel: stream the `xmlFormatError` fragments through
703    /// the global generic handler.
704    Stream,
705    /// No channel (SAX slot NULL): no generic delivery.
706    None,
707}
708
709/// Raise an error with upstream's full routing (error.c 2.15
710/// `xmlVRaiseError`): update the last error, then deliver to the structured
711/// handler **or** the selected generic channel — never both.
712///
713/// `file`/`line`/`source_window` feed the generic fragment stream (the
714/// structured handler receives the complete `xmlError` instead). `col` is
715/// the 1-based byte column (upstream `input->col` → `err->int2`); `str1`..
716/// `str3`/`int1` are the upstream extra fields; `enc_bytes` feeds the
717/// `XML_ERR_INVALID_ENCODING` "Bytes:" fragment.
718///
719/// # UPSTREAM-PARITY (ownership)
720///
721/// Like upstream `xmlVSetError`, every string field of the stored error is
722/// owned (`xmlStrdup`): `file`/`str1`/`str2`/`str3` are heap copies, so the
723/// caller may pass transient C strings.
724///
725/// # SAFETY
726///
727/// - `ctxt` may be NULL.
728/// - `msg`, `file`, `str1`, `str2`, `str3` must be valid C strings or NULL.
729/// - `source_window` bytes must be valid for the duration of the call.
730pub unsafe fn raise_error_streamed(
731    ctxt: *mut c_void,
732    domain: c_int,
733    code: c_int,
734    level: c_int,
735    file: *const c_char,
736    line: c_int,
737    col: c_int,
738    str1: *const c_char,
739    str2: *const c_char,
740    str3: *const c_char,
741    int1: c_int,
742    msg: *const c_char,
743    source_window: Option<(&[u8], usize)>,
744    enc_bytes: Option<[u8; 4]>,
745    delivery: GenericDelivery,
746) {
747    // The streamed generic-error channel below uses an x86_64 SysV va_list
748    // trampoline (ch_call0/1/2 — register-based). Other ABIs (i686 cdecl,
749    // ARM/aarch64 AAPCS, ...) fall back to the plain raise path; full
750    // streamed-fragment parity there is an unexecuted platform obligation
751    // (atlas/PLATFORM_SURFACE_ATLAS.md, OBLIG-WORDSIZE-32 / compiler-ABI).
752    #[cfg(not(target_arch = "x86_64"))]
753    {
754        raise_error(
755            ctxt,
756            ptr::null_mut(),
757            ptr::null_mut(),
758            ptr::null_mut(),
759            ptr::null_mut(),
760            domain,
761            code,
762            level,
763            file,
764            line,
765            str1,
766            str2,
767            str3,
768            int1,
769            col,
770            msg,
771        );
772        return;
773    }
774
775    #[cfg(target_arch = "x86_64")]
776    {
777        // UPSTREAM-PARITY (xmlVRaiseError): warnings are suppressed when the
778        // global xmlGetWarningsDefaultValue is zero.
779        if level == xmlErrorLevel::XML_ERR_WARNING as c_int
780            && unsafe { crate::abi::data_globals::xmlGetWarningsDefaultValue } == 0
781        {
782            return;
783        }
784
785        raise_error_streamed_x86_64(
786            ctxt,
787            domain,
788            code,
789            level,
790            file,
791            line,
792            col,
793            str1,
794            str2,
795            str3,
796            int1,
797            msg,
798            source_window,
799            enc_bytes,
800            delivery,
801        );
802    }
803}
804
805/// x86-64 streamed raise (SysV va_list channel). See `raise_error_streamed`.
806#[cfg(target_arch = "x86_64")]
807unsafe fn raise_error_streamed_x86_64(
808    ctxt: *mut c_void,
809    domain: c_int,
810    code: c_int,
811    level: c_int,
812    file: *const c_char,
813    line: c_int,
814    col: c_int,
815    str1: *const c_char,
816    str2: *const c_char,
817    str3: *const c_char,
818    int1: c_int,
819    msg: *const c_char,
820    source_window: Option<(&[u8], usize)>,
821    enc_bytes: Option<[u8; 4]>,
822    delivery: GenericDelivery,
823) {
824    // UPSTREAM-PARITY (xmlVRaiseError): warnings are suppressed when the
825    // global xmlGetWarningsDefaultValue is zero.
826    if level == xmlErrorLevel::XML_ERR_WARNING as c_int
827        && unsafe { crate::abi::data_globals::xmlGetWarningsDefaultValue } == 0
828    {
829        return;
830    }
831
832    // Format the error message (same as raise_error).
833    let formatted_msg = format_error_message(domain, code, msg, str1, str2, str3);
834    let file_copy = if file.is_null() {
835        ptr::null_mut()
836    } else {
837        crate::abi::allocator::xmlMemStrdupImpl(file) as *mut c_char
838    };
839    let str1_copy = if str1.is_null() {
840        ptr::null_mut()
841    } else {
842        crate::abi::allocator::xmlMemStrdupImpl(str1) as *mut c_char
843    };
844    let str2_copy = if str2.is_null() {
845        ptr::null_mut()
846    } else {
847        crate::abi::allocator::xmlMemStrdupImpl(str2) as *mut c_char
848    };
849    let str3_copy = if str3.is_null() {
850        ptr::null_mut()
851    } else {
852        crate::abi::allocator::xmlMemStrdupImpl(str3) as *mut c_char
853    };
854    let err = _xmlError {
855        domain,
856        code,
857        message: formatted_msg,
858        level,
859        file: file_copy,
860        line,
861        str1: str1_copy,
862        str2: str2_copy,
863        str3: str3_copy,
864        int1,
865        int2: col,
866        ctxt,
867        node: ptr::null_mut(),
868    };
869
870    globals::set_last_error(err);
871
872    // Structured handler wins (upstream `else if` chain); the (handler, ctx)
873    // pair is read atomically and invoked outside the lock (11.1-X).
874    let structured = globals::with_structured_error(|h, c| (h, c));
875    if let Some(handler) = structured.0 {
876        let err_ref = globals::get_last_error();
877        if !err_ref.is_null() {
878            handler(structured.1, err_ref as *const _xmlError);
879        }
880        return;
881    }
882
883    match delivery {
884        GenericDelivery::Custom(channel, ctx) => {
885            if !msg.is_null() {
886                // SAFETY: the caller provided a valid C callback.
887                unsafe { channel(ctx, msg) };
888            }
889        }
890        GenericDelivery::Stream => {
891            if globals::get_generic_error_func().is_some() {
892                unsafe {
893                    format_error_streamed(
894                        domain,
895                        code,
896                        level,
897                        formatted_msg,
898                        file,
899                        line,
900                        source_window,
901                        enc_bytes,
902                    )
903                };
904            }
905        }
906        GenericDelivery::None => {}
907    }
908}
909
910/// Default SAX v1 error handler — `void xmlParserError(void *ctx, const char *msg, ...)`.
911///
912/// # SAFETY
913///
914/// - `ctx` may be NULL (unused by the candidate's legacy path).
915/// - `msg` must be a valid NUL-terminated C string or NULL.
916#[no_mangle]
917pub unsafe extern "C" fn xmlParserError(ctx: *mut c_void, msg: *const c_char) {
918    let _ = ctx;
919    unsafe { emit_legacy_message("error", msg) };
920}
921
922/// Default SAX v1 warning handler — `void xmlParserWarning(void *ctx, const char *msg, ...)`.
923#[no_mangle]
924pub unsafe extern "C" fn xmlParserWarning(ctx: *mut c_void, msg: *const c_char) {
925    let _ = ctx;
926    unsafe { emit_legacy_message("warning", msg) };
927}
928
929/// Default validity error handler — `void xmlParserValidityError(void *ctx, const char *msg, ...)`.
930#[no_mangle]
931pub unsafe extern "C" fn xmlParserValidityError(ctx: *mut c_void, msg: *const c_char) {
932    let _ = ctx;
933    unsafe { emit_legacy_message("validity error", msg) };
934}
935
936/// Default validity warning handler — `void xmlParserValidityWarning(void *ctx, const char *msg, ...)`.
937#[no_mangle]
938pub unsafe extern "C" fn xmlParserValidityWarning(ctx: *mut c_void, msg: *const c_char) {
939    let _ = ctx;
940    unsafe { emit_legacy_message("validity warning", msg) };
941}
942
943// ═══════════════════════════════════════════════════════════════════════════════
944// Tests
945// ═══════════════════════════════════════════════════════════════════════════════
946
947#[cfg(test)]
948mod tests {
949    use super::*;
950    use crate::abi::allocator;
951    use core::ffi::c_void;
952
953    #[test]
954    fn test_error_default_reset() {
955        unsafe {
956            let mut err = _xmlError {
957                domain: XML_FROM_PARSER,
958                code: XML_ERR_NO_MEMORY,
959                message: ptr::null_mut(),
960                level: XML_ERR_ERROR as c_int,
961                file: ptr::null_mut(),
962                line: 42,
963                str1: ptr::null_mut(),
964                str2: ptr::null_mut(),
965                str3: ptr::null_mut(),
966                int1: 0,
967                int2: 0,
968                ctxt: ptr::null_mut(),
969                node: ptr::null_mut(),
970            };
971
972            reset_error(&mut err);
973            assert_eq!(err.domain, XML_FROM_NONE);
974            assert_eq!(err.code, XML_ERR_OK as c_int);
975            assert_eq!(err.level, XML_ERR_NONE as c_int);
976            assert_eq!(err.line, 0);
977        }
978    }
979
980    #[test]
981    fn test_copy_error() {
982        unsafe {
983            let from = _xmlError {
984                domain: XML_FROM_PARSER,
985                code: XML_ERR_NO_MEMORY,
986                message: ptr::null_mut(),
987                level: XML_ERR_FATAL as c_int,
988                file: ptr::null_mut(),
989                line: 100,
990                str1: ptr::null_mut(),
991                str2: ptr::null_mut(),
992                str3: ptr::null_mut(),
993                int1: 1,
994                int2: 2,
995                ctxt: ptr::null_mut(),
996                node: ptr::null_mut(),
997            };
998            let mut to = _xmlError {
999                domain: XML_FROM_NONE,
1000                code: XML_ERR_OK as c_int,
1001                message: ptr::null_mut(),
1002                level: XML_ERR_NONE as c_int,
1003                file: ptr::null_mut(),
1004                line: 0,
1005                str1: ptr::null_mut(),
1006                str2: ptr::null_mut(),
1007                str3: ptr::null_mut(),
1008                int1: 0,
1009                int2: 0,
1010                ctxt: ptr::null_mut(),
1011                node: ptr::null_mut(),
1012            };
1013
1014            let result = copy_error(&from, &mut to);
1015            assert_eq!(result, 0);
1016            assert_eq!(to.domain, XML_FROM_PARSER);
1017            assert_eq!(to.code, XML_ERR_NO_MEMORY);
1018            assert_eq!(to.level, XML_ERR_FATAL as c_int);
1019            assert_eq!(to.line, 100);
1020            assert_eq!(to.int1, 1);
1021            assert_eq!(to.int2, 2);
1022        }
1023    }
1024
1025    #[test]
1026    fn test_raise_and_get_last_error() {
1027        unsafe {
1028            reset_last_error();
1029            assert!(get_last_error().is_null());
1030
1031            let file = b"test.xml\0" as *const u8 as *const c_char;
1032            let str1 = b"element\0" as *const u8 as *const c_char;
1033
1034            raise_error(
1035                ptr::null_mut(),
1036                ptr::null_mut(),
1037                ptr::null_mut(),
1038                ptr::null_mut(),
1039                ptr::null_mut(),
1040                XML_FROM_PARSER,
1041                XML_ERR_TAG_NAME_MISMATCH,
1042                XML_ERR_ERROR as c_int,
1043                file,
1044                10,
1045                str1,
1046                ptr::null(),
1047                ptr::null(),
1048                0,
1049                0,
1050                ptr::null(),
1051            );
1052
1053            let last = get_last_error();
1054            assert!(!last.is_null());
1055            assert_eq!((*last).domain, XML_FROM_PARSER);
1056            assert_eq!((*last).code, XML_ERR_TAG_NAME_MISMATCH);
1057            assert_eq!((*last).level, XML_ERR_ERROR as c_int);
1058            assert_eq!((*last).line, 10);
1059
1060            // Check file was stored
1061            let last_file = (*last).file;
1062            assert!(!last_file.is_null());
1063
1064            reset_last_error();
1065            assert!(get_last_error().is_null());
1066        }
1067    }
1068
1069    #[test]
1070    fn test_structured_error_callback() {
1071        // Serialized against the handler-slot tests in xml::globals (11.1-X):
1072        // the structured handler slot is shared global state.
1073        let _guard = crate::xml::globals::ERROR_HANDLER_TEST_LOCK.lock();
1074        unsafe {
1075            reset_last_error();
1076
1077            // Set up a structured error handler that captures the error
1078            let mut captured_domain: c_int = 0;
1079            let captured_ptr = &mut captured_domain as *mut c_int as *mut c_void;
1080
1081            // SAFETY: The callback writes to captured_ptr which lives on the stack
1082            // for the duration of this test.
1083            extern "C" fn test_handler(ctx: *mut c_void, _err: *const _xmlError) {
1084                // SAFETY: ctx is valid for the test duration.
1085                unsafe {
1086                    let captured = &mut *(ctx as *mut c_int);
1087                    *captured = 42;
1088                }
1089            }
1090
1091            set_structured_error_func(captured_ptr, Some(test_handler as xmlStructuredErrorFunc));
1092
1093            raise_error(
1094                ptr::null_mut(),
1095                ptr::null_mut(),
1096                ptr::null_mut(),
1097                ptr::null_mut(),
1098                ptr::null_mut(),
1099                XML_FROM_PARSER,
1100                XML_ERR_OK as c_int,
1101                XML_ERR_WARNING as c_int,
1102                ptr::null(),
1103                0,
1104                ptr::null(),
1105                ptr::null(),
1106                ptr::null(),
1107                0,
1108                0,
1109                ptr::null(),
1110            );
1111
1112            assert_eq!(captured_domain, 42);
1113
1114            // Reset
1115            set_structured_error_func(ptr::null_mut(), None);
1116            reset_last_error();
1117        }
1118    }
1119
1120    #[test]
1121    fn test_format_error_message() {
1122        unsafe {
1123            // Test with direct message
1124            let msg = b"test error\0" as *const u8 as *const c_char;
1125            let formatted = format_error_message(
1126                XML_FROM_NONE,
1127                XML_ERR_OK as c_int,
1128                msg,
1129                ptr::null(),
1130                ptr::null(),
1131                ptr::null(),
1132            );
1133            assert!(!formatted.is_null());
1134            let formatted_str = std::ffi::CStr::from_ptr(formatted);
1135            assert_eq!(formatted_str.to_bytes(), b"test error");
1136
1137            // Free the allocated message
1138            allocator::xmlFreeImpl(formatted as *mut c_void);
1139
1140            // Test with domain and str1
1141            let str1 = b"foo\0" as *const u8 as *const c_char;
1142            let formatted2 = format_error_message(
1143                XML_FROM_PARSER,
1144                XML_ERR_OK as c_int,
1145                ptr::null(),
1146                str1,
1147                ptr::null(),
1148                ptr::null(),
1149            );
1150            assert!(!formatted2.is_null());
1151            allocator::xmlFreeImpl(formatted2 as *mut c_void);
1152        }
1153    }
1154}