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