libxml_rs/xml/errors/mod.rs
1//! Error subsystem (§21, §85 Phase 1).
2//!
3//! Implements the libxml2 error reporting infrastructure:
4//!
5//! - `xmlError` struct management
6//! - Error domain/code registry
7//! - Structured error callbacks (thread-local storage)
8//! - Generic error callbacks (thread-local storage)
9//! - Last-error tracking (thread-local `xmlGetLastError`, `xmlResetLastError`, `xmlCopyError`)
10//! - Error message formatting
11//! - `xmlRaiseError()` — the central error reporting function
12//!
13//! # UPSTREAM-PARITY
14//!
15//! libxml2 has a two-tier error system:
16//!
17//! 1. **Structured errors** — `xmlStructuredErrorFunc` receives an `xmlErrorPtr`
18//! with all structured fields (domain, code, level, line, etc.)
19//!
20//! 2. **Generic errors** — `xmlGenericErrorFunc` receives a formatted string
21//! (printf-style). This is the older system, still widely used.
22//!
23//! Both systems coexist. When both handlers are set, both are called.
24//! The last error is stored thread-locally for retrieval via `xmlGetLastError`.
25//!
26//! # Phase 1 status
27//!
28//! Complete — all error functions are implemented.
29//! Variadic message formatting will be enhanced in Phase 2+.
30//!
31//! # Upstream contract
32//!
33//! Mirrors upstream error.c (SRC-LIBXML2-2.15.0-ERROR-C, oracle tree
34//! `oracle/historical/src/libxml2-2.15.0/error.c`): xmlRaiseError /
35//! xmlVRaiseError routing, xmlFormatError, xmlGenericErrorDefaultFunc,
36//! xmlGetLastError / xmlCtxtGetLastError / xmlResetLastError, xmlCopyError
37//! and the legacy xmlParserError / xmlParserWarning SAX handlers.
38//!
39//! # Conceptual behavior
40//!
41//! Two-tier error system: structured errors (xmlStructuredErrorFunc with the
42//! full xmlError) and generic errors (printf-style fragments). The 11.1-M/K
43//! rework routes each raise through ONE channel: a structured handler, else
44//! the custom SAX channel (channel(data, msg) once), else the legacy default
45//! streaming the xmlFormatError fragments (file/line, domain, level, message,
46//! source window, caret) as 6 variadic calls (R-000161).
47//!
48//! # Ownership & safety invariants
49//!
50//! Ownership: the thread-local last error owns its file/str1-3 copies;
51//! raise_error_streamed copies transient CStrings (R-000163: dangling
52//! pointers were the bug). The exported xmlLastError mirror is synced under
53//! lock (R-000170). SAFETY: the variadic xmlGenericErrorDefaultFunc and
54//! xmlParserError legacy handlers are x86_64 SysV inline-asm va_list shims —
55//! stable Rust cannot define variadic extern fns.
56//!
57//! # Historical quirks & epochs
58//!
59//! E-005: fatal parser errors are reported twice from 2.13.0 (attr-markup-
60//! entity); the xmlFormatError fragment stream and the 80-column source
61//! window cap are 2.15 semantics (xmlParserInputGetWindow, caret clamp).
62//! R-000163 pinned all XML_ERR_* 64-96 constants to upstream numbering
63//! (SPACE_REQUIRED 65, NAME_REQUIRED 68, GT_REQUIRED 73, TAG_NAME_MISMATCH
64//! 76, TAG_NOT_FINISHED 77 — not the synthetic renumbering).
65//!
66//! # Deliberate oddities
67//!
68//! Deliberate oddities: warnings only bump nbWarnings while other levels
69//! update errNo / nbErrors / wellFormed per xmlCtxtVErr; the source window
70//! replicates the 80-char cap, continuation-byte skip, UTF-8 forward scan and
71//! the 2.15 caret clamp (col >= n maps to size-1).
72//!
73//! # Proving courts
74//!
75//! ERROR and CALLBACK court families; ERROR-001 (48 deterministic malformed
76//! inputs x 4 passes, byte-identical), GLOBALS-THREADING, DATA-GLOBALS-001,
77//! and `cargo test --lib` (ASan-clean).
78//!
79//! # Tempting simplifications that would break parity
80//!
81//! The tempting simplification is one generic call plus a direct stderr write
82//! — a counting handler would observe 1 call instead of the 6 xmlFormatError
83//! fragments the oracle emits (R-000161). Do not route errors to stderr when a
84//! handler is installed. Never renumber the XML_ERR_* constants (R-000163).
85
86use core::ffi::c_void;
87use core::ptr;
88use std::os::raw::{c_char, c_int, c_uint};
89
90use crate::abi::callbacks::{errorSAXFunc, xmlGenericErrorFunc, xmlStructuredErrorFunc};
91use crate::abi::structs::_xmlError;
92use crate::abi::types::xmlErrorLevel::*;
93use crate::abi::types::*;
94use crate::xml::globals;
95
96// ═══════════════════════════════════════════════════════════════════════════════
97// Error Management Functions
98// ═══════════════════════════════════════════════════════════════════════════════
99
100/// Set the generic error handler.
101///
102/// # UPSTREAM-PARITY
103///
104/// ```c
105/// void xmlSetGenericErrorFunc(void *ctx, xmlGenericErrorFunc handler);
106/// ```
107///
108/// # SAFETY
109///
110/// - `handler` must be a valid function pointer or NULL (to reset to default).
111/// - If non-NULL, the handler may be called at any time with `ctx`.
112pub unsafe fn set_generic_error_func(ctx: *mut c_void, handler: Option<xmlGenericErrorFunc>) {
113 // SAFETY: Delegates to globals with same safety contract.
114 unsafe { globals::set_generic_error_func(ctx, handler) };
115}
116
117/// Set the structured error handler.
118///
119/// # UPSTREAM-PARITY
120///
121/// ```c
122/// void xmlSetStructuredErrorFunc(void *ctx, xmlStructuredErrorFunc handler);
123/// ```
124///
125/// # SAFETY
126///
127/// - `handler` must be a valid function pointer or NULL.
128pub unsafe fn set_structured_error_func(ctx: *mut c_void, handler: Option<xmlStructuredErrorFunc>) {
129 // SAFETY: Delegates to globals with same safety contract.
130 unsafe { globals::set_structured_error_func(ctx, handler) };
131}
132
133/// Get the last error for the current thread.
134///
135/// # UPSTREAM-PARITY
136///
137/// ```c
138/// xmlErrorPtr xmlGetLastError(void);
139/// ```
140///
141/// Returns a pointer to the last error, or NULL if no error occurred.
142/// The returned pointer is valid until the next libxml2 call in this thread.
143pub fn get_last_error() -> *mut _xmlError {
144 globals::get_last_error()
145}
146
147/// Copy an error from one location to another.
148///
149/// # UPSTREAM-PARITY
150///
151/// ```c
152/// xmlErrorPtr xmlCopyError(xmlErrorPtr from, xmlErrorPtr to);
153/// ```
154///
155/// Copies `from` into `to`. Returns 0 on success, -1 on error.
156///
157/// # SAFETY
158///
159/// - `from` and `to` must be valid pointers to `_xmlError` structs, or NULL.
160pub 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/// System V AMD64 `__va_list_tag` (24 bytes) — same layout as the writer's
523/// shims and `data_globals.rs`.
524#[cfg(target_arch = "x86_64")]
525#[repr(C)]
526#[derive(Clone, Copy, Debug)]
527pub struct VaListTag {
528 gp_offset: c_uint,
529 fp_offset: c_uint,
530 overflow_arg_area: *mut c_void,
531 reg_save_area: *mut c_void,
532}
533
534#[cfg(target_arch = "x86_64")]
535unsafe extern "C" {
536 fn vsnprintf(s: *mut c_char, n: usize, format: *const c_char, ap: *mut VaListTag) -> c_int;
537}
538
539/// Format `msg` with the caller's varargs and emit `level + ": "` + the
540/// formatted text through the generic channel (upstream error.c
541/// `xmlVFormatLegacyError`: `xmlGenericError(ctx, "%s: ", level)` then the
542/// `xmlStrVASPrintf`-formatted message).
543#[cfg(target_arch = "x86_64")]
544unsafe fn emit_legacy_message_v(level: &str, msg: *const c_char, ap: *mut VaListTag) -> c_int {
545 if msg.is_null() {
546 return 0;
547 }
548 let mut buf = [0 as c_char; 4096];
549 let n = unsafe { vsnprintf(buf.as_mut_ptr(), buf.len(), msg, ap) };
550 let n = n.clamp(0, buf.len() as c_int - 1) as usize;
551 buf[n] = 0;
552 unsafe { emit_legacy_message(level, buf.as_ptr()) };
553 0
554}
555
556/// x86_64 SysV variadic shim: captures the register save area exactly like
557/// `va_start` (2 fixed args → `gp_offset` 16), builds the va_list at
558/// rsp+176, passes it as the 3rd argument (rdx) to `receiver`, then restores
559/// the stack and returns. Same technique as `xmlStrPrintf`
560/// (exports_string.rs) and `xsltTransformError` (exports_xslt_util.rs).
561///
562/// Layout: reg_save_area = rsp+0 (6 GP + 8 SSE slots, 176 bytes); the
563/// va_list struct lives at rsp+176; overflow varargs are above the return
564/// address; a 240-byte frame keeps the `call` 16-aligned and the overflow
565/// area at rsp+256 (= entry_rsp + 8); the alignment push is popped before
566/// `ret`.
567#[cfg(target_arch = "x86_64")]
568unsafe fn legacy_shim(
569 receiver: unsafe extern "C" fn(*mut c_void, *const c_char, *mut VaListTag) -> c_int,
570) -> c_int {
571 unsafe {
572 core::arch::asm!(
573 "sub rsp, 240",
574 "mov [rsp+0], rdi",
575 "mov [rsp+8], rsi",
576 "mov [rsp+16], rdx",
577 "mov [rsp+24], rcx",
578 "mov [rsp+32], r8",
579 "mov [rsp+40], r9",
580 "movaps [rsp+48], xmm0",
581 "movaps [rsp+64], xmm1",
582 "movaps [rsp+80], xmm2",
583 "movaps [rsp+96], xmm3",
584 "movaps [rsp+112], xmm4",
585 "movaps [rsp+128], xmm5",
586 "movaps [rsp+144], xmm6",
587 "movaps [rsp+160], xmm7",
588 "mov dword ptr [rsp+176], 16",
589 "mov dword ptr [rsp+180], 48",
590 "lea rax, [rsp+256]",
591 "mov [rsp+184], rax",
592 "lea rax, [rsp]",
593 "mov [rsp+192], rax",
594 "lea rdx, [rsp+176]",
595 "call {receiver}",
596 "add rsp, 240",
597 "add rsp, 8",
598 "ret",
599 receiver = in(reg) receiver as usize,
600 options(noreturn),
601 );
602 }
603}
604
605// ═══════════════════════════════════════════════════════════════════════════════
606// Generic-channel fragment streaming (upstream error.c `xmlFormatError`)
607// ═══════════════════════════════════════════════════════════════════════════════
608//
609// Upstream streams each error through the generic channel as a sequence of
610// variadic calls (e.g. `channel(data, "%s:%d: ", file, line)` followed by the
611// domain, level, message and source-context fragments). Custom handlers and the
612// built-in default (an x86_64 SysV va_list shim, see data_globals.rs) both
613// observe the same per-fragment calls. Stable Rust cannot express a variadic
614// call, so each fragment goes through a tiny x86_64 trampoline that places the
615// fixed arguments in the ABI registers and does an indirect call.
616
617/// `channel(data, fmt)` — no variadic arguments.
618#[cfg(target_arch = "x86_64")]
619#[inline]
620unsafe fn ch_call0(handler: xmlGenericErrorFunc, data: *mut c_void, fmt: *const c_char) {
621 // SAFETY: `handler` is a C-compatible generic error callback; per the
622 // SysV ABI the callee sees (data, fmt) with no additional registers
623 // consumed (rdx/rcx zeroed so a va_list-reading callee finds nothing).
624 // The compiler guarantees 16-byte stack alignment at the asm block, so
625 // the `call` is correctly aligned.
626 unsafe {
627 core::arch::asm!(
628 "xor edx, edx",
629 "xor ecx, ecx",
630 "call {h}",
631 h = in(reg) handler as usize,
632 in("rdi") data,
633 in("rsi") fmt,
634 out("rdx") _, out("rcx") _,
635 lateout("rax") _, lateout("r8") _, lateout("r9") _, lateout("r10") _, lateout("r11") _,
636 );
637 }
638}
639
640/// `channel(data, fmt, a1)` — one pointer-sized variadic argument.
641#[cfg(target_arch = "x86_64")]
642#[inline]
643unsafe fn ch_call1(handler: xmlGenericErrorFunc, data: *mut c_void, fmt: *const c_char, a1: usize) {
644 // SAFETY: as ch_call0; `a1` lands in the va_list slot after the two
645 // fixed args (rdx).
646 unsafe {
647 core::arch::asm!(
648 "xor ecx, ecx",
649 "call {h}",
650 h = in(reg) handler as usize,
651 in("rdi") data,
652 in("rsi") fmt,
653 in("rdx") a1,
654 out("rcx") _,
655 lateout("rax") _, lateout("r8") _, lateout("r9") _, lateout("r10") _, lateout("r11") _,
656 );
657 }
658}
659
660/// `channel(data, fmt, a1, a2)` — two pointer-sized variadic arguments.
661#[cfg(target_arch = "x86_64")]
662#[inline]
663unsafe fn ch_call2(
664 handler: xmlGenericErrorFunc,
665 data: *mut c_void,
666 fmt: *const c_char,
667 a1: usize,
668 a2: usize,
669) {
670 // SAFETY: as ch_call0; a1/a2 land in the va_list slots (rdx, rcx).
671 unsafe {
672 core::arch::asm!(
673 "call {h}",
674 h = in(reg) handler as usize,
675 in("rdi") data,
676 in("rsi") fmt,
677 in("rdx") a1,
678 in("rcx") a2,
679 lateout("rax") _, lateout("r8") _, lateout("r9") _, lateout("r10") _, lateout("r11") _,
680 );
681 }
682}
683
684/// Emit one raise through the generic channel with upstream's
685/// `xmlFormatError` fragment sequence (error.c 2.15): file/line prefix,
686/// domain, level, message, then the source window and caret line.
687///
688/// `file`/`line` come from the raising site's input; `source_window` is the
689/// current input line text plus the 0-based caret column (upstream
690/// `xmlParserInputGetWindow`).
691///
692/// # SAFETY
693///
694/// - `file` and `message` must be valid C strings or NULL.
695/// - `source_window` bytes must be valid for the duration of the call.
696#[allow(clippy::too_many_arguments)]
697#[cfg(target_arch = "x86_64")]
698unsafe fn format_error_streamed(
699 domain: c_int,
700 code: c_int,
701 level: c_int,
702 message: *const c_char,
703 file: *const c_char,
704 line: c_int,
705 source_window: Option<(&[u8], usize)>,
706 enc_bytes: Option<[u8; 4]>,
707) {
708 // SAFETY: reads the exported C globals (upstream reads the same).
709 let Some(handler) = globals::get_generic_error_func() else {
710 return;
711 };
712 let data = globals::get_generic_error_ctx();
713
714 // 1. File/line prefix (xmlFormatError).
715 if !file.is_null() {
716 ch_call2(
717 handler,
718 data,
719 c"%s:%d: ".as_ptr() as *const c_char,
720 file as usize,
721 line as usize,
722 );
723 } else if line != 0
724 && (domain == XML_FROM_PARSER
725 || domain == XML_FROM_SCHEMASV
726 || domain == XML_FROM_SCHEMASP
727 || domain == XML_FROM_DTD
728 || domain == XML_FROM_RELAXNGP
729 || domain == XML_FROM_RELAXNGV)
730 {
731 ch_call1(
732 handler,
733 data,
734 c"Entity: line %d: ".as_ptr() as *const c_char,
735 line as usize,
736 );
737 }
738
739 // 2. Domain fragment (xmlFormatError switch).
740 let dom: &[u8] = match domain {
741 XML_FROM_PARSER => b"parser \0",
742 XML_FROM_NAMESPACE => b"namespace \0",
743 XML_FROM_DTD | XML_FROM_VALID => b"validity \0",
744 XML_FROM_HTML => b"HTML parser \0",
745 XML_FROM_MEMORY => b"memory \0",
746 XML_FROM_OUTPUT => b"output \0",
747 XML_FROM_IO => b"I/O \0",
748 XML_FROM_XINCLUDE => b"XInclude \0",
749 XML_FROM_XPATH => b"XPath \0",
750 XML_FROM_XPOINTER => b"parser \0",
751 XML_FROM_REGEXP => b"regexp \0",
752 XML_FROM_MODULE => b"module \0",
753 XML_FROM_SCHEMASV => b"Schemas validity \0",
754 XML_FROM_SCHEMASP => b"Schemas parser \0",
755 XML_FROM_RELAXNGP => b"Relax-NG parser \0",
756 XML_FROM_RELAXNGV => b"Relax-NG validity \0",
757 XML_FROM_CATALOG => b"Catalog \0",
758 XML_FROM_C14N => b"C14N \0",
759 XML_FROM_XSLT => b"XSLT \0",
760 XML_FROM_I18N => b"encoding \0",
761 XML_FROM_SCHEMATRONV => b"schematron \0",
762 XML_FROM_BUFFER => b"internal buffer \0",
763 XML_FROM_URI => b"URI \0",
764 _ => b"\0",
765 };
766 if !dom.is_empty() && dom[0] != 0 {
767 ch_call0(handler, data, dom.as_ptr() as *const c_char);
768 }
769
770 // 3. Level fragment (xmlFormatError switch).
771 let lvl: &[u8] = if level == XML_ERR_NONE as c_int {
772 b": \0"
773 } else if level == XML_ERR_WARNING as c_int {
774 b"warning : \0"
775 } else if level == XML_ERR_ERROR as c_int || level == XML_ERR_FATAL as c_int {
776 b"error : \0"
777 } else {
778 b"\0"
779 };
780 if !lvl.is_empty() && lvl[0] != 0 {
781 ch_call0(handler, data, lvl.as_ptr() as *const c_char);
782 }
783
784 // 4. Message fragment.
785 if !message.is_null() {
786 let msg = message as *const u8;
787 let mut len = 0usize;
788 while unsafe { *msg.add(len) } != 0 {
789 len += 1;
790 }
791 let ends_nl = len > 0 && unsafe { *msg.add(len - 1) } == b'\n';
792 let fmt: &[u8] = if ends_nl { b"%s\0" } else { b"%s\n\0" };
793 ch_call1(handler, data, fmt.as_ptr() as *const c_char, msg as usize);
794 }
795
796 // 4b. Invalid-encoding byte dump (upstream xmlFormatError: the first 4
797 // bytes at the error position, only for XML_ERR_INVALID_ENCODING).
798 if code == XML_ERR_INVALID_ENCODING {
799 if let Some(bytes) = enc_bytes {
800 ch_call0(handler, data, c"Bytes:".as_ptr() as *const c_char);
801 for b in bytes {
802 // " 0x%02X"
803 let hex = format!(" 0x{:02X}\0", b);
804 ch_call0(handler, data, hex.as_ptr() as *const c_char);
805 }
806 ch_call0(handler, data, c"\n".as_ptr() as *const c_char);
807 }
808 }
809
810 // 5. Source window + caret (xmlParserPrintFileContextInternal).
811 if let Some((window, caret)) = source_window {
812 let mut win = window.to_vec();
813 win.push(0);
814 ch_call1(
815 handler,
816 data,
817 c"%s\n".as_ptr() as *const c_char,
818 win.as_ptr() as usize,
819 );
820 let mut caret_line = Vec::with_capacity(caret + 2);
821 for &b in window.iter().take(caret) {
822 caret_line.push(if b == b'\t' { b'\t' } else { b' ' });
823 }
824 caret_line.push(b'^');
825 caret_line.push(0);
826 ch_call1(
827 handler,
828 data,
829 c"%s\n".as_ptr() as *const c_char,
830 caret_line.as_ptr() as usize,
831 );
832 }
833}
834
835/// How a raise delivers to the generic side of the error system (upstream
836/// `xmlVRaiseError` channel selection, error.c 2.15).
837#[derive(Clone, Copy, Debug)]
838pub enum GenericDelivery {
839 /// Custom SAX channel: single call `channel(ctx, msg)`.
840 Custom(xmlGenericErrorFunc, *mut c_void),
841 /// Legacy/default channel: stream the `xmlFormatError` fragments through
842 /// the global generic handler.
843 Stream,
844 /// No channel (SAX slot NULL): no generic delivery.
845 None,
846}
847
848/// Raise an error with upstream's full routing (error.c 2.15
849/// `xmlVRaiseError`): update the last error, then deliver to the structured
850/// handler **or** the selected generic channel — never both.
851///
852/// `file`/`line`/`source_window` feed the generic fragment stream (the
853/// structured handler receives the complete `xmlError` instead). `col` is
854/// the 1-based byte column (upstream `input->col` → `err->int2`); `str1`..
855/// `str3`/`int1` are the upstream extra fields; `enc_bytes` feeds the
856/// `XML_ERR_INVALID_ENCODING` "Bytes:" fragment.
857///
858/// # UPSTREAM-PARITY (ownership)
859///
860/// Like upstream `xmlVSetError`, every string field of the stored error is
861/// owned (`xmlStrdup`): `file`/`str1`/`str2`/`str3` are heap copies, so the
862/// caller may pass transient C strings.
863///
864/// # SAFETY
865///
866/// - `ctxt` may be NULL.
867/// - `msg`, `file`, `str1`, `str2`, `str3` must be valid C strings or NULL.
868/// - `source_window` bytes must be valid for the duration of the call.
869#[allow(clippy::too_many_arguments)]
870pub unsafe fn raise_error_streamed(
871 ctxt: *mut c_void,
872 domain: c_int,
873 code: c_int,
874 level: c_int,
875 file: *const c_char,
876 line: c_int,
877 col: c_int,
878 str1: *const c_char,
879 str2: *const c_char,
880 str3: *const c_char,
881 int1: c_int,
882 msg: *const c_char,
883 source_window: Option<(&[u8], usize)>,
884 enc_bytes: Option<[u8; 4]>,
885 delivery: GenericDelivery,
886) {
887 // The streamed generic-error channel below uses an x86_64 SysV va_list
888 // trampoline (ch_call0/1/2 — register-based). Other ABIs (i686 cdecl,
889 // ARM/aarch64 AAPCS, ...) fall back to the plain raise path; full
890 // streamed-fragment parity there is an unexecuted platform obligation
891 // (atlas/PLATFORM_SURFACE_ATLAS.md, OBLIG-WORDSIZE-32 / compiler-ABI).
892 #[cfg(not(target_arch = "x86_64"))]
893 {
894 raise_error(
895 ctxt,
896 ptr::null_mut(),
897 ptr::null_mut(),
898 ptr::null_mut(),
899 ptr::null_mut(),
900 domain,
901 code,
902 level,
903 file,
904 line,
905 str1,
906 str2,
907 str3,
908 int1,
909 col,
910 msg,
911 );
912 return;
913 }
914
915 #[cfg(target_arch = "x86_64")]
916 {
917 // UPSTREAM-PARITY (xmlVRaiseError): warnings are suppressed when the
918 // global xmlGetWarningsDefaultValue is zero.
919 if level == xmlErrorLevel::XML_ERR_WARNING as c_int
920 && unsafe { crate::abi::data_globals::xmlGetWarningsDefaultValue } == 0
921 {
922 return;
923 }
924
925 raise_error_streamed_x86_64(
926 ctxt,
927 domain,
928 code,
929 level,
930 file,
931 line,
932 col,
933 str1,
934 str2,
935 str3,
936 int1,
937 msg,
938 source_window,
939 enc_bytes,
940 delivery,
941 );
942 }
943}
944
945/// x86-64 streamed raise (SysV va_list channel). See `raise_error_streamed`.
946#[allow(clippy::too_many_arguments)]
947#[cfg(target_arch = "x86_64")]
948unsafe fn raise_error_streamed_x86_64(
949 ctxt: *mut c_void,
950 domain: c_int,
951 code: c_int,
952 level: c_int,
953 file: *const c_char,
954 line: c_int,
955 col: c_int,
956 str1: *const c_char,
957 str2: *const c_char,
958 str3: *const c_char,
959 int1: c_int,
960 msg: *const c_char,
961 source_window: Option<(&[u8], usize)>,
962 enc_bytes: Option<[u8; 4]>,
963 delivery: GenericDelivery,
964) {
965 // UPSTREAM-PARITY (xmlVRaiseError): warnings are suppressed when the
966 // global xmlGetWarningsDefaultValue is zero.
967 if level == xmlErrorLevel::XML_ERR_WARNING as c_int
968 && unsafe { crate::abi::data_globals::xmlGetWarningsDefaultValue } == 0
969 {
970 return;
971 }
972
973 // Format the error message (same as raise_error).
974 let formatted_msg = format_error_message(domain, code, msg, str1, str2, str3);
975 let file_copy = if file.is_null() {
976 ptr::null_mut()
977 } else {
978 crate::abi::allocator::xmlMemStrdupImpl(file) as *mut c_char
979 };
980 let str1_copy = if str1.is_null() {
981 ptr::null_mut()
982 } else {
983 crate::abi::allocator::xmlMemStrdupImpl(str1) as *mut c_char
984 };
985 let str2_copy = if str2.is_null() {
986 ptr::null_mut()
987 } else {
988 crate::abi::allocator::xmlMemStrdupImpl(str2) as *mut c_char
989 };
990 let str3_copy = if str3.is_null() {
991 ptr::null_mut()
992 } else {
993 crate::abi::allocator::xmlMemStrdupImpl(str3) as *mut c_char
994 };
995 let err = _xmlError {
996 domain,
997 code,
998 message: formatted_msg,
999 level,
1000 file: file_copy,
1001 line,
1002 str1: str1_copy,
1003 str2: str2_copy,
1004 str3: str3_copy,
1005 int1,
1006 int2: col,
1007 ctxt,
1008 node: ptr::null_mut(),
1009 };
1010
1011 globals::set_last_error(err);
1012
1013 // Structured handler wins (upstream `else if` chain); the (handler, ctx)
1014 // pair is read atomically and invoked outside the lock (11.1-X).
1015 let structured = globals::with_structured_error(|h, c| (h, c));
1016 if let Some(handler) = structured.0 {
1017 let err_ref = globals::get_last_error();
1018 if !err_ref.is_null() {
1019 handler(structured.1, err_ref as *const _xmlError);
1020 }
1021 return;
1022 }
1023
1024 match delivery {
1025 GenericDelivery::Custom(channel, ctx) => {
1026 if !msg.is_null() {
1027 // SAFETY: the caller provided a valid C callback.
1028 unsafe { channel(ctx, msg) };
1029 }
1030 }
1031 GenericDelivery::Stream => {
1032 if globals::get_generic_error_func().is_some() {
1033 unsafe {
1034 format_error_streamed(
1035 domain,
1036 code,
1037 level,
1038 formatted_msg,
1039 file,
1040 line,
1041 source_window,
1042 enc_bytes,
1043 )
1044 };
1045 }
1046 }
1047 GenericDelivery::None => {}
1048 }
1049}
1050
1051/// Default SAX v1 error handler — upstream `void xmlParserError(void *ctx,
1052/// const char *msg, ...)`. Variadic x86_64 SysV shim (11.1-Z.2, R-000176:
1053/// the previous fixed-arity body silently dropped the varargs; upstream
1054/// error.c formats them via `xmlVFormatLegacyError`).
1055///
1056/// 2 fixed args (ctx=rdi, msg=rsi) → `gp_offset` 16; the va_list pointer is
1057/// passed as the 3rd arg (rdx) to the `xmlParserErrorV` receiver.
1058///
1059/// # SAFETY
1060///
1061/// - `ctx` may be NULL (unused by the candidate's legacy path).
1062/// - `msg` must be a valid NUL-terminated printf format string.
1063#[cfg(target_arch = "x86_64")]
1064#[no_mangle]
1065pub unsafe extern "C" fn xmlParserError() -> c_int {
1066 unsafe { legacy_shim(xmlParserErrorV) }
1067}
1068
1069/// Variadic receiver for the `xmlParserError` shim: formats `msg` with the
1070/// caller's varargs and emits `"error: "` + formatted text through the
1071/// generic channel (upstream error.c `xmlVFormatLegacyError`).
1072///
1073/// # Safety
1074///
1075/// - `msg` must be a valid NUL-terminated printf format string, `ap` a
1076/// valid va_list matching the format, `ctx` may be NULL.
1077#[no_mangle]
1078pub unsafe extern "C" fn xmlParserErrorV(
1079 ctx: *mut c_void,
1080 msg: *const c_char,
1081 ap: *mut VaListTag,
1082) -> c_int {
1083 let _ = ctx;
1084 unsafe { emit_legacy_message_v("error", msg, ap) }
1085}
1086
1087/// Default SAX v1 warning handler — upstream `void xmlParserWarning(void
1088/// *ctx, const char *msg, ...)`. Variadic shim as `xmlParserError`.
1089///
1090/// # SAFETY
1091///
1092/// - `ctx` may be NULL (unused by the candidate's legacy path).
1093/// - `msg` must be a valid NUL-terminated printf format string.
1094#[cfg(target_arch = "x86_64")]
1095#[no_mangle]
1096pub unsafe extern "C" fn xmlParserWarning() -> c_int {
1097 unsafe { legacy_shim(xmlParserWarningV) }
1098}
1099
1100/// Variadic receiver for the `xmlParserWarning` shim.
1101///
1102/// # Safety
1103///
1104/// - `msg` must be a valid NUL-terminated printf format string, `ap` a
1105/// valid va_list matching the format, `ctx` may be NULL.
1106#[no_mangle]
1107pub unsafe extern "C" fn xmlParserWarningV(
1108 ctx: *mut c_void,
1109 msg: *const c_char,
1110 ap: *mut VaListTag,
1111) -> c_int {
1112 let _ = ctx;
1113 unsafe { emit_legacy_message_v("warning", msg, ap) }
1114}
1115
1116/// Default validity error handler — upstream `void
1117/// xmlParserValidityError(void *ctx, const char *msg, ...)`. Variadic shim
1118/// as `xmlParserError`.
1119///
1120/// # SAFETY
1121///
1122/// - `ctx` may be NULL (unused by the candidate's legacy path).
1123/// - `msg` must be a valid NUL-terminated printf format string.
1124#[cfg(target_arch = "x86_64")]
1125#[no_mangle]
1126pub unsafe extern "C" fn xmlParserValidityError() -> c_int {
1127 unsafe { legacy_shim(xmlParserValidityErrorV) }
1128}
1129
1130/// Variadic receiver for the `xmlParserValidityError` shim.
1131///
1132/// # Safety
1133///
1134/// - `msg` must be a valid NUL-terminated printf format string, `ap` a
1135/// valid va_list matching the format, `ctx` may be NULL.
1136#[no_mangle]
1137pub unsafe extern "C" fn xmlParserValidityErrorV(
1138 ctx: *mut c_void,
1139 msg: *const c_char,
1140 ap: *mut VaListTag,
1141) -> c_int {
1142 let _ = ctx;
1143 unsafe { emit_legacy_message_v("validity error", msg, ap) }
1144}
1145
1146/// Default validity warning handler — upstream `void
1147/// xmlParserValidityWarning(void *ctx, const char *msg, ...)`. Variadic
1148/// shim as `xmlParserError`.
1149///
1150/// # SAFETY
1151///
1152/// - `ctx` may be NULL (unused by the candidate's legacy path).
1153/// - `msg` must be a valid NUL-terminated printf format string.
1154#[cfg(target_arch = "x86_64")]
1155#[no_mangle]
1156pub unsafe extern "C" fn xmlParserValidityWarning() -> c_int {
1157 unsafe { legacy_shim(xmlParserValidityWarningV) }
1158}
1159
1160/// Variadic receiver for the `xmlParserValidityWarning` shim.
1161///
1162/// # Safety
1163///
1164/// - `msg` must be a valid NUL-terminated printf format string, `ap` a
1165/// valid va_list matching the format, `ctx` may be NULL.
1166#[no_mangle]
1167pub unsafe extern "C" fn xmlParserValidityWarningV(
1168 ctx: *mut c_void,
1169 msg: *const c_char,
1170 ap: *mut VaListTag,
1171) -> c_int {
1172 let _ = ctx;
1173 unsafe { emit_legacy_message_v("validity warning", msg, ap) }
1174}
1175
1176/// The variadic `xmlParserError` shim, transmuted to the fixed-arity SAX v1
1177/// callback type. The shim's declared arity is a Rust-side fiction (stable
1178/// Rust cannot express `c_variadic`); the ABI is a plain code pointer and
1179/// the C variadic call contract is preserved (11.1-Z.2, R-000176).
1180pub const XML_PARSER_ERROR_SAX1: errorSAXFunc = unsafe {
1181 // SAFETY: shim and SAX callback are both plain code pointers (same ABI);
1182 // the declared arity difference is the documented shim fiction.
1183 core::mem::transmute::<unsafe extern "C" fn() -> c_int, errorSAXFunc>(xmlParserError)
1184};
1185
1186/// The variadic `xmlParserWarning` shim as the SAX v1 callback type.
1187pub const XML_PARSER_WARNING_SAX1: errorSAXFunc = unsafe {
1188 // SAFETY: see XML_PARSER_ERROR_SAX1.
1189 core::mem::transmute::<unsafe extern "C" fn() -> c_int, errorSAXFunc>(xmlParserWarning)
1190};
1191
1192/// The variadic `xmlParserValidityError` shim as the validation callback
1193/// type (`xmlValidityErrorFunc`-compatible fixed-arity pointer).
1194pub const XML_PARSER_VALIDITY_ERROR_SAX1: unsafe extern "C" fn(*mut c_void, *const c_char) =
1195 // SAFETY: see XML_PARSER_ERROR_SAX1.
1196 unsafe {
1197 core::mem::transmute::<
1198 unsafe extern "C" fn() -> c_int,
1199 unsafe extern "C" fn(*mut c_void, *const c_char),
1200 >(xmlParserValidityError)
1201 };
1202
1203/// The variadic `xmlParserValidityWarning` shim as the validation callback
1204/// type.
1205pub const XML_PARSER_VALIDITY_WARNING_SAX1: unsafe extern "C" fn(*mut c_void, *const c_char) =
1206 // SAFETY: see XML_PARSER_ERROR_SAX1.
1207 unsafe {
1208 core::mem::transmute::<
1209 unsafe extern "C" fn() -> c_int,
1210 unsafe extern "C" fn(*mut c_void, *const c_char),
1211 >(xmlParserValidityWarning)
1212 };
1213
1214// ═══════════════════════════════════════════════════════════════════════════════
1215// Tests
1216// ═══════════════════════════════════════════════════════════════════════════════
1217
1218#[cfg(test)]
1219mod tests {
1220 use super::*;
1221 use crate::abi::allocator;
1222 use core::ffi::c_void;
1223
1224 #[test]
1225 fn test_error_default_reset() {
1226 unsafe {
1227 let mut err = _xmlError {
1228 domain: XML_FROM_PARSER,
1229 code: XML_ERR_NO_MEMORY,
1230 message: ptr::null_mut(),
1231 level: XML_ERR_ERROR as c_int,
1232 file: ptr::null_mut(),
1233 line: 42,
1234 str1: ptr::null_mut(),
1235 str2: ptr::null_mut(),
1236 str3: ptr::null_mut(),
1237 int1: 0,
1238 int2: 0,
1239 ctxt: ptr::null_mut(),
1240 node: ptr::null_mut(),
1241 };
1242
1243 reset_error(&mut err);
1244 assert_eq!(err.domain, XML_FROM_NONE);
1245 assert_eq!(err.code, XML_ERR_OK as c_int);
1246 assert_eq!(err.level, XML_ERR_NONE as c_int);
1247 assert_eq!(err.line, 0);
1248 }
1249 }
1250
1251 #[test]
1252 fn test_copy_error() {
1253 unsafe {
1254 let from = _xmlError {
1255 domain: XML_FROM_PARSER,
1256 code: XML_ERR_NO_MEMORY,
1257 message: ptr::null_mut(),
1258 level: XML_ERR_FATAL as c_int,
1259 file: ptr::null_mut(),
1260 line: 100,
1261 str1: ptr::null_mut(),
1262 str2: ptr::null_mut(),
1263 str3: ptr::null_mut(),
1264 int1: 1,
1265 int2: 2,
1266 ctxt: ptr::null_mut(),
1267 node: ptr::null_mut(),
1268 };
1269 let mut to = _xmlError {
1270 domain: XML_FROM_NONE,
1271 code: XML_ERR_OK as c_int,
1272 message: ptr::null_mut(),
1273 level: XML_ERR_NONE as c_int,
1274 file: ptr::null_mut(),
1275 line: 0,
1276 str1: ptr::null_mut(),
1277 str2: ptr::null_mut(),
1278 str3: ptr::null_mut(),
1279 int1: 0,
1280 int2: 0,
1281 ctxt: ptr::null_mut(),
1282 node: ptr::null_mut(),
1283 };
1284
1285 let result = copy_error(&from, &mut to);
1286 assert_eq!(result, 0);
1287 assert_eq!(to.domain, XML_FROM_PARSER);
1288 assert_eq!(to.code, XML_ERR_NO_MEMORY);
1289 assert_eq!(to.level, XML_ERR_FATAL as c_int);
1290 assert_eq!(to.line, 100);
1291 assert_eq!(to.int1, 1);
1292 assert_eq!(to.int2, 2);
1293 }
1294 }
1295
1296 #[test]
1297 fn test_raise_and_get_last_error() {
1298 unsafe {
1299 reset_last_error();
1300 assert!(get_last_error().is_null());
1301
1302 let file = b"test.xml\0" as *const u8 as *const c_char;
1303 let str1 = b"element\0" as *const u8 as *const c_char;
1304
1305 raise_error(
1306 ptr::null_mut(),
1307 ptr::null_mut(),
1308 ptr::null_mut(),
1309 ptr::null_mut(),
1310 ptr::null_mut(),
1311 XML_FROM_PARSER,
1312 XML_ERR_TAG_NAME_MISMATCH,
1313 XML_ERR_ERROR as c_int,
1314 file,
1315 10,
1316 str1,
1317 ptr::null(),
1318 ptr::null(),
1319 0,
1320 0,
1321 ptr::null(),
1322 );
1323
1324 let last = get_last_error();
1325 assert!(!last.is_null());
1326 assert_eq!((*last).domain, XML_FROM_PARSER);
1327 assert_eq!((*last).code, XML_ERR_TAG_NAME_MISMATCH);
1328 assert_eq!((*last).level, XML_ERR_ERROR as c_int);
1329 assert_eq!((*last).line, 10);
1330
1331 // Check file was stored
1332 let last_file = (*last).file;
1333 assert!(!last_file.is_null());
1334
1335 reset_last_error();
1336 assert!(get_last_error().is_null());
1337 }
1338 }
1339
1340 #[test]
1341 fn test_structured_error_callback() {
1342 // Serialized against the handler-slot tests in xml::globals (11.1-X):
1343 // the structured handler slot is shared global state.
1344 let _guard = crate::xml::globals::ERROR_HANDLER_TEST_LOCK.lock();
1345 unsafe {
1346 reset_last_error();
1347
1348 // Set up a structured error handler that captures the error
1349 let mut captured_domain: c_int = 0;
1350 let captured_ptr = &mut captured_domain as *mut c_int as *mut c_void;
1351
1352 // SAFETY: The callback writes to captured_ptr which lives on the stack
1353 // for the duration of this test.
1354 extern "C" fn test_handler(ctx: *mut c_void, _err: *const _xmlError) {
1355 // SAFETY: ctx is valid for the test duration.
1356 unsafe {
1357 let captured = &mut *(ctx as *mut c_int);
1358 *captured = 42;
1359 }
1360 }
1361
1362 set_structured_error_func(captured_ptr, Some(test_handler as xmlStructuredErrorFunc));
1363
1364 raise_error(
1365 ptr::null_mut(),
1366 ptr::null_mut(),
1367 ptr::null_mut(),
1368 ptr::null_mut(),
1369 ptr::null_mut(),
1370 XML_FROM_PARSER,
1371 XML_ERR_OK as c_int,
1372 XML_ERR_WARNING as c_int,
1373 ptr::null(),
1374 0,
1375 ptr::null(),
1376 ptr::null(),
1377 ptr::null(),
1378 0,
1379 0,
1380 ptr::null(),
1381 );
1382
1383 assert_eq!(captured_domain, 42);
1384
1385 // Reset
1386 set_structured_error_func(ptr::null_mut(), None);
1387 reset_last_error();
1388 }
1389 }
1390
1391 #[test]
1392 fn test_format_error_message() {
1393 unsafe {
1394 // Test with direct message
1395 let msg = b"test error\0" as *const u8 as *const c_char;
1396 let formatted = format_error_message(
1397 XML_FROM_NONE,
1398 XML_ERR_OK as c_int,
1399 msg,
1400 ptr::null(),
1401 ptr::null(),
1402 ptr::null(),
1403 );
1404 assert!(!formatted.is_null());
1405 let formatted_str = std::ffi::CStr::from_ptr(formatted);
1406 assert_eq!(formatted_str.to_bytes(), b"test error");
1407
1408 // Free the allocated message
1409 allocator::xmlFreeImpl(formatted as *mut c_void);
1410
1411 // Test with domain and str1
1412 let str1 = b"foo\0" as *const u8 as *const c_char;
1413 let formatted2 = format_error_message(
1414 XML_FROM_PARSER,
1415 XML_ERR_OK as c_int,
1416 ptr::null(),
1417 str1,
1418 ptr::null(),
1419 ptr::null(),
1420 );
1421 assert!(!formatted2.is_null());
1422 allocator::xmlFreeImpl(formatted2 as *mut c_void);
1423 }
1424 }
1425}