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}