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.
234///
235/// # Safety
236///
237/// - `msg` must be NULL or a valid NUL-terminated C string; `str1`, `str2`
238/// and `str3` must be NULL or valid NUL-terminated C strings; the
239/// returned buffer is allocator-owned and must be freed with
240/// `xmlFreeImpl`, or is NULL on allocation failure.
241pub fn format_error_message(
242 _domain: c_int,
243 _code: c_int,
244 msg: *const c_char,
245 str1: *const c_char,
246 str2: *const c_char,
247 str3: *const c_char,
248) -> *mut c_char {
249 // Phase 1: basic message construction.
250 // If a direct message string is provided, use it.
251 if !msg.is_null() {
252 // SAFETY: Caller guarantees msg is a valid C string.
253 let msg_str = unsafe { crate::abi::allocator::xmlMemStrdupImpl(msg) };
254 return msg_str as *mut c_char;
255 }
256
257 // Build a message from the component strings.
258 // This matches upstream behavior where domain/code are combined
259 // with str1/str2/str3 into a diagnostic message.
260 let mut buf: [u8; 1024] = [0; 1024];
261 let mut pos = 0;
262
263 // Write domain prefix
264 let domain_str = match _domain {
265 XML_FROM_PARSER => "parser",
266 XML_FROM_TREE => "tree",
267 XML_FROM_NAMESPACE => "namespace",
268 XML_FROM_DTD => "dtd",
269 XML_FROM_HTML => "html",
270 XML_FROM_MEMORY => "memory",
271 XML_FROM_OUTPUT => "output",
272 XML_FROM_IO => "io",
273 XML_FROM_XPATH => "xpath",
274 XML_FROM_XPOINTER => "xpointer",
275 XML_FROM_XINCLUDE => "xinclude",
276 XML_FROM_CATALOG => "catalog",
277 XML_FROM_C14N => "c14n",
278 XML_FROM_XSLT => "xslt",
279 XML_FROM_VALID => "valid",
280 XML_FROM_CHECK => "check",
281 XML_FROM_WRITER => "writer",
282 XML_FROM_MODULE => "module",
283 XML_FROM_I18N => "i18n",
284 XML_FROM_SCHEMATRONV => "schematron",
285 XML_FROM_BUFFER => "buffer",
286 XML_FROM_URI => "uri",
287 XML_FROM_NONE => "",
288 XML_FROM_FTP => "ftp",
289 XML_FROM_HTTP => "http",
290 XML_FROM_REGEXP => "regexp",
291 XML_FROM_DATATYPE => "datatype",
292 XML_FROM_SCHEMASP => "schema parser",
293 XML_FROM_SCHEMASV => "schema validator",
294 XML_FROM_RELAXNGP => "relaxng parser",
295 XML_FROM_RELAXNGV => "relaxng validator",
296 _ => "unknown",
297 };
298
299 if !domain_str.is_empty() {
300 let bytes = domain_str.as_bytes();
301 let len = bytes.len().min(buf.len().saturating_sub(pos + 2));
302 buf[pos..pos + len].copy_from_slice(&bytes[..len]);
303 pos += len;
304 buf[pos] = b' ';
305 pos += 1;
306 }
307
308 // Append str1 if present
309 if !str1.is_null() {
310 // SAFETY: Caller guarantees str1 is a valid C string.
311 let s = unsafe { crate::abi::versioning::c_str_to_bytes(str1).unwrap_or_default() };
312 if pos + s.len() + 3 <= buf.len() {
313 buf[pos] = b'\'';
314 pos += 1;
315 buf[pos..pos + s.len()].copy_from_slice(s);
316 pos += s.len();
317 buf[pos] = b'\'';
318 pos += 1;
319 buf[pos] = b' ';
320 pos += 1;
321 }
322 }
323
324 // Append str2 if present
325 if !str2.is_null() {
326 let s = unsafe { crate::abi::versioning::c_str_to_bytes(str2).unwrap_or_default() };
327 if pos + s.len() + 3 <= buf.len() {
328 buf[pos] = b'\'';
329 pos += 1;
330 buf[pos..pos + s.len()].copy_from_slice(s);
331 pos += s.len();
332 buf[pos] = b'\'';
333 pos += 1;
334 buf[pos] = b' ';
335 pos += 1;
336 }
337 }
338
339 // Append str3 if present
340 if !str3.is_null() {
341 let s = unsafe { crate::abi::versioning::c_str_to_bytes(str3).unwrap_or_default() };
342 if pos + s.len() + 3 <= buf.len() {
343 buf[pos] = b'\'';
344 pos += 1;
345 buf[pos..pos + s.len()].copy_from_slice(s);
346 pos += s.len();
347 buf[pos] = b'\'';
348 pos += 1;
349 buf[pos] = b' ';
350 pos += 1;
351 }
352 }
353
354 // Null-terminate
355 if pos < buf.len() {
356 buf[pos] = 0;
357 } else {
358 buf[buf.len() - 1] = 0;
359 }
360
361 // Allocate and return
362 let result = unsafe { crate::abi::allocator::xmlMallocImpl(pos + 1) };
363 if result.is_null() {
364 return ptr::null_mut();
365 }
366 unsafe {
367 ptr::copy_nonoverlapping(buf.as_ptr(), result as *mut u8, pos + 1);
368 }
369 result as *mut c_char
370}
371
372/// Raise an error — the central error reporting function.
373///
374/// This is called internally when an error occurs. It:
375/// 1. Updates the thread-local last error
376/// 2. Invokes the structured error handler if one is set
377/// 3. Invokes the generic error handler if one is set (for warnings/errors)
378///
379/// # UPSTREAM-PARITY
380///
381/// ```c
382/// void xmlRaiseError(xmlErrorPtr ctxt,
383/// xmlErrorPtr ctxt2,
384/// xmlErrorPtr ctxt3,
385/// xmlErrorPtr ctxt4,
386/// xmlErrorPtr ctxt5,
387/// int domain,
388/// int code,
389/// xmlErrorLevel level,
390/// const char *file,
391/// int line,
392/// const char *str1,
393/// const char *str2,
394/// const char *str3,
395/// int int1,
396/// int int2,
397/// const char *msg,
398/// ...);
399/// ```
400///
401/// # SAFETY
402///
403/// - `ctxt` may be NULL (context of the error).
404/// - `domain`, `code`, `level`: valid error codes.
405/// - `msg` must be a valid C string or NULL.
406/// - `file` must be a valid C string or NULL.
407/// - `str1`, `str2`, `str3`: error-related strings (may be NULL).
408#[allow(clippy::too_many_arguments)]
409pub unsafe fn raise_error(
410 ctxt: *mut c_void,
411 _ctxt2: *mut c_void,
412 _ctxt3: *mut c_void,
413 _ctxt4: *mut c_void,
414 _ctxt5: *mut c_void,
415 domain: c_int,
416 code: c_int,
417 level: c_int,
418 file: *const c_char,
419 line: c_int,
420 str1: *const c_char,
421 str2: *const c_char,
422 str3: *const c_char,
423 int1: c_int,
424 _int2: c_int,
425 msg: *const c_char,
426) {
427 // Format the error message
428 let formatted_msg = format_error_message(domain, code, msg, str1, str2, str3);
429
430 // UPSTREAM-PARITY (xmlVSetError): string fields are owned copies so the
431 // stored error survives transient callers.
432 let file_copy = if file.is_null() {
433 ptr::null_mut()
434 } else {
435 crate::abi::allocator::xmlMemStrdupImpl(file) as *mut c_char
436 };
437 let str1_copy = if str1.is_null() {
438 ptr::null_mut()
439 } else {
440 crate::abi::allocator::xmlMemStrdupImpl(str1) as *mut c_char
441 };
442 let str2_copy = if str2.is_null() {
443 ptr::null_mut()
444 } else {
445 crate::abi::allocator::xmlMemStrdupImpl(str2) as *mut c_char
446 };
447 let str3_copy = if str3.is_null() {
448 ptr::null_mut()
449 } else {
450 crate::abi::allocator::xmlMemStrdupImpl(str3) as *mut c_char
451 };
452
453 // Store the last error
454 let err = _xmlError {
455 domain,
456 code,
457 message: formatted_msg,
458 level,
459 file: file_copy,
460 line,
461 str1: str1_copy,
462 str2: str2_copy,
463 str3: str3_copy,
464 int1,
465 int2: 0,
466 ctxt,
467 node: ptr::null_mut(),
468 };
469
470 globals::set_last_error(err);
471
472 // UPSTREAM-PARITY (xmlCtxtVErr): the structured handler wins; the
473 // generic channel is only used when no structured handler is set. The
474 // (handler, ctx) pairs are read atomically (11.1-X), then invoked
475 // outside the lock so a handler that re-enters the library cannot
476 // deadlock.
477 let structured = globals::with_structured_error(|h, c| (h, c));
478 if let Some(handler) = structured.0 {
479 let err_ref = globals::get_last_error();
480 if !err_ref.is_null() {
481 handler(structured.1, err_ref as *const _xmlError);
482 }
483 } else if level != 0 {
484 let generic = globals::with_generic_error(|h, c| (h, c));
485 if let Some(handler) = generic.0 {
486 let ctx = generic.1;
487 if !formatted_msg.is_null() {
488 handler(ctx, formatted_msg as *const core::ffi::c_char);
489 } else if !msg.is_null() {
490 handler(ctx, msg);
491 }
492 }
493 }
494
495 // Free the formatted message if it was allocated
496 // Note: We keep it as the last error's message, so we don't free it here.
497 // The next call to raise_error or reset_error will free the old message.
498 // Actually, in Phase 1, we don't free because the message is the last error's.
499 // A more complete implementation would free the old message when setting a new one.
500}
501
502/// Emit a legacy-format message through the generic error channel.
503///
504/// Upstream's `xmlGenericErrorDefaultFunc` writes the message to stderr;
505/// when a custom generic handler is installed it receives the message. The
506/// `level` prefix matches upstream `xmlVFormatLegacyError` (error.c 2.15).
507unsafe fn emit_legacy_message(level: &str, msg: *const c_char) {
508 if msg.is_null() {
509 return;
510 }
511 let len = libc::strlen(msg) as usize;
512 let text = core::slice::from_raw_parts(msg as *const u8, len);
513 let mut full = Vec::with_capacity(level.len() + 2 + len);
514 full.extend_from_slice(level.as_bytes());
515 full.push(b':');
516 full.push(b' ');
517 full.extend_from_slice(text);
518 if let Some(handler) = globals::get_generic_error_func() {
519 let ctx = globals::get_generic_error_ctx();
520 let mut cmsg = full.clone();
521 cmsg.push(0);
522 handler(ctx, cmsg.as_ptr() as *const c_char);
523 } else {
524 // Upstream default (xmlGenericErrorDefaultFunc): stderr.
525 let _ = libc::write(2, full.as_ptr() as *const libc::c_void, full.len());
526 }
527}
528
529/// System V AMD64 `__va_list_tag` (24 bytes) — same layout as the writer's
530/// shims and `data_globals.rs`.
531#[cfg(target_arch = "x86_64")]
532#[repr(C)]
533#[derive(Clone, Copy, Debug)]
534pub struct VaListTag {
535 gp_offset: c_uint,
536 fp_offset: c_uint,
537 overflow_arg_area: *mut c_void,
538 reg_save_area: *mut c_void,
539}
540
541#[cfg(target_arch = "x86_64")]
542unsafe extern "C" {
543 /// Format `msg` with the caller's va_list into `s` (libc `vsnprintf`).
544 ///
545 /// # Safety
546 ///
547 /// - `s` must point to a writable buffer of at least `n` bytes;
548 /// `format` must be a valid NUL-terminated printf format string;
549 /// `ap` must be a valid va_list matching the format's specifiers.
550 fn vsnprintf(s: *mut c_char, n: usize, format: *const c_char, ap: *mut VaListTag) -> c_int;
551}
552
553/// Format `msg` with the caller's varargs and emit `level + ": "` + the
554/// formatted text through the generic channel (upstream error.c
555/// `xmlVFormatLegacyError`: `xmlGenericError(ctx, "%s: ", level)` then the
556/// `xmlStrVASPrintf`-formatted message).
557///
558/// # Safety
559///
560/// - `msg` must be NULL or a valid NUL-terminated printf format string;
561/// `ap` must be a valid va_list matching the format's specifiers; the
562/// format is consumed exactly once.
563#[cfg(target_arch = "x86_64")]
564unsafe fn emit_legacy_message_v(level: &str, msg: *const c_char, ap: *mut VaListTag) -> c_int {
565 if msg.is_null() {
566 return 0;
567 }
568 let mut buf = [0 as c_char; 4096];
569 let n = unsafe { vsnprintf(buf.as_mut_ptr(), buf.len(), msg, ap) };
570 let n = n.clamp(0, buf.len() as c_int - 1) as usize;
571 buf[n] = 0;
572 unsafe { emit_legacy_message(level, buf.as_ptr()) };
573 0
574}
575
576/// x86_64 SysV variadic shim: captures the register save area exactly like
577/// `va_start` (2 fixed args → `gp_offset` 16), builds the va_list at
578/// rsp+176, passes it as the 3rd argument (rdx) to `receiver`, then restores
579/// the stack and returns. Same technique as `xmlStrPrintf`
580/// (exports_string.rs) and `xsltTransformError` (exports_xslt_util.rs).
581///
582/// Layout: reg_save_area = rsp+0 (6 GP + 8 SSE slots, 176 bytes); the
583/// va_list struct lives at rsp+176; overflow varargs are above the return
584/// address; a 240-byte frame keeps the `call` 16-aligned and the overflow
585/// area at rsp+256 (= entry_rsp + 8); the alignment push is popped before
586/// `ret`.
587///
588/// # Safety
589///
590/// - `receiver` must be a valid x86_64 SysV function pointer that accepts
591/// `(ctx, msg, ap)` and consumes the va_list built in the fixed stack
592/// slots; the frame layout described above must match the ABI.
593#[cfg(target_arch = "x86_64")]
594unsafe fn legacy_shim(
595 receiver: unsafe extern "C" fn(*mut c_void, *const c_char, *mut VaListTag) -> c_int,
596) -> c_int {
597 unsafe {
598 core::arch::asm!(
599 "sub rsp, 240",
600 "mov [rsp+0], rdi",
601 "mov [rsp+8], rsi",
602 "mov [rsp+16], rdx",
603 "mov [rsp+24], rcx",
604 "mov [rsp+32], r8",
605 "mov [rsp+40], r9",
606 "movaps [rsp+48], xmm0",
607 "movaps [rsp+64], xmm1",
608 "movaps [rsp+80], xmm2",
609 "movaps [rsp+96], xmm3",
610 "movaps [rsp+112], xmm4",
611 "movaps [rsp+128], xmm5",
612 "movaps [rsp+144], xmm6",
613 "movaps [rsp+160], xmm7",
614 "mov dword ptr [rsp+176], 16",
615 "mov dword ptr [rsp+180], 48",
616 "lea rax, [rsp+256]",
617 "mov [rsp+184], rax",
618 "lea rax, [rsp]",
619 "mov [rsp+192], rax",
620 "lea rdx, [rsp+176]",
621 "call {receiver}",
622 "add rsp, 240",
623 "add rsp, 8",
624 "ret",
625 receiver = in(reg) receiver as usize,
626 options(noreturn),
627 );
628 }
629}
630
631// ═══════════════════════════════════════════════════════════════════════════════
632// Generic-channel fragment streaming (upstream error.c `xmlFormatError`)
633// ═══════════════════════════════════════════════════════════════════════════════
634//
635// Upstream streams each error through the generic channel as a sequence of
636// variadic calls (e.g. `channel(data, "%s:%d: ", file, line)` followed by the
637// domain, level, message and source-context fragments). Custom handlers and the
638// built-in default (an x86_64 SysV va_list shim, see data_globals.rs) both
639// observe the same per-fragment calls. Stable Rust cannot express a variadic
640// call, so each fragment goes through a tiny x86_64 trampoline that places the
641// fixed arguments in the ABI registers and does an indirect call.
642
643/// `channel(data, fmt)` — no variadic arguments.
644#[cfg(target_arch = "x86_64")]
645#[inline]
646unsafe fn ch_call0(handler: xmlGenericErrorFunc, data: *mut c_void, fmt: *const c_char) {
647 // SAFETY: `handler` is a C-compatible generic error callback; per the
648 // SysV ABI the callee sees (data, fmt) with no additional registers
649 // consumed (rdx/rcx zeroed so a va_list-reading callee finds nothing).
650 // The compiler guarantees 16-byte stack alignment at the asm block, so
651 // the `call` is correctly aligned.
652 unsafe {
653 core::arch::asm!(
654 "xor edx, edx",
655 "xor ecx, ecx",
656 "call {h}",
657 h = in(reg) handler as usize,
658 in("rdi") data,
659 in("rsi") fmt,
660 out("rdx") _, out("rcx") _,
661 lateout("rax") _, lateout("r8") _, lateout("r9") _, lateout("r10") _, lateout("r11") _,
662 );
663 }
664}
665
666/// `channel(data, fmt, a1)` — one pointer-sized variadic argument.
667#[cfg(target_arch = "x86_64")]
668#[inline]
669unsafe fn ch_call1(handler: xmlGenericErrorFunc, data: *mut c_void, fmt: *const c_char, a1: usize) {
670 // SAFETY: as ch_call0; `a1` lands in the va_list slot after the two
671 // fixed args (rdx).
672 unsafe {
673 core::arch::asm!(
674 "xor ecx, ecx",
675 "call {h}",
676 h = in(reg) handler as usize,
677 in("rdi") data,
678 in("rsi") fmt,
679 in("rdx") a1,
680 out("rcx") _,
681 lateout("rax") _, lateout("r8") _, lateout("r9") _, lateout("r10") _, lateout("r11") _,
682 );
683 }
684}
685
686/// `channel(data, fmt, a1, a2)` — two pointer-sized variadic arguments.
687#[cfg(target_arch = "x86_64")]
688#[inline]
689unsafe fn ch_call2(
690 handler: xmlGenericErrorFunc,
691 data: *mut c_void,
692 fmt: *const c_char,
693 a1: usize,
694 a2: usize,
695) {
696 // SAFETY: as ch_call0; a1/a2 land in the va_list slots (rdx, rcx).
697 unsafe {
698 core::arch::asm!(
699 "call {h}",
700 h = in(reg) handler as usize,
701 in("rdi") data,
702 in("rsi") fmt,
703 in("rdx") a1,
704 in("rcx") a2,
705 lateout("rax") _, lateout("r8") _, lateout("r9") _, lateout("r10") _, lateout("r11") _,
706 );
707 }
708}
709
710/// Emit one raise through the generic channel with upstream's
711/// `xmlFormatError` fragment sequence (error.c 2.15): file/line prefix,
712/// domain, level, message, then the source window and caret line.
713///
714/// `file`/`line` come from the raising site's input; `source_window` is the
715/// current input line text plus the 0-based caret column (upstream
716/// `xmlParserInputGetWindow`).
717///
718/// # SAFETY
719///
720/// - `file` and `message` must be valid C strings or NULL.
721/// - `source_window` bytes must be valid for the duration of the call.
722#[allow(clippy::too_many_arguments)]
723#[cfg(target_arch = "x86_64")]
724unsafe fn format_error_streamed(
725 domain: c_int,
726 code: c_int,
727 level: c_int,
728 message: *const c_char,
729 file: *const c_char,
730 line: c_int,
731 source_window: Option<(&[u8], usize)>,
732 enc_bytes: Option<[u8; 4]>,
733) {
734 // SAFETY: reads the exported C globals (upstream reads the same).
735 let Some(handler) = globals::get_generic_error_func() else {
736 return;
737 };
738 let data = globals::get_generic_error_ctx();
739
740 // 1. File/line prefix (xmlFormatError).
741 if !file.is_null() {
742 ch_call2(
743 handler,
744 data,
745 c"%s:%d: ".as_ptr() as *const c_char,
746 file as usize,
747 line as usize,
748 );
749 } else if line != 0
750 && (domain == XML_FROM_PARSER
751 || domain == XML_FROM_SCHEMASV
752 || domain == XML_FROM_SCHEMASP
753 || domain == XML_FROM_DTD
754 || domain == XML_FROM_RELAXNGP
755 || domain == XML_FROM_RELAXNGV)
756 {
757 ch_call1(
758 handler,
759 data,
760 c"Entity: line %d: ".as_ptr() as *const c_char,
761 line as usize,
762 );
763 }
764
765 // 2. Domain fragment (xmlFormatError switch).
766 let dom: &[u8] = match domain {
767 XML_FROM_PARSER => b"parser \0",
768 XML_FROM_NAMESPACE => b"namespace \0",
769 XML_FROM_DTD | XML_FROM_VALID => b"validity \0",
770 XML_FROM_HTML => b"HTML parser \0",
771 XML_FROM_MEMORY => b"memory \0",
772 XML_FROM_OUTPUT => b"output \0",
773 XML_FROM_IO => b"I/O \0",
774 XML_FROM_XINCLUDE => b"XInclude \0",
775 XML_FROM_XPATH => b"XPath \0",
776 XML_FROM_XPOINTER => b"parser \0",
777 XML_FROM_REGEXP => b"regexp \0",
778 XML_FROM_MODULE => b"module \0",
779 XML_FROM_SCHEMASV => b"Schemas validity \0",
780 XML_FROM_SCHEMASP => b"Schemas parser \0",
781 XML_FROM_RELAXNGP => b"Relax-NG parser \0",
782 XML_FROM_RELAXNGV => b"Relax-NG validity \0",
783 XML_FROM_CATALOG => b"Catalog \0",
784 XML_FROM_C14N => b"C14N \0",
785 XML_FROM_XSLT => b"XSLT \0",
786 XML_FROM_I18N => b"encoding \0",
787 XML_FROM_SCHEMATRONV => b"schematron \0",
788 XML_FROM_BUFFER => b"internal buffer \0",
789 XML_FROM_URI => b"URI \0",
790 _ => b"\0",
791 };
792 if !dom.is_empty() && dom[0] != 0 {
793 ch_call0(handler, data, dom.as_ptr() as *const c_char);
794 }
795
796 // 3. Level fragment (xmlFormatError switch).
797 let lvl: &[u8] = if level == XML_ERR_NONE as c_int {
798 b": \0"
799 } else if level == XML_ERR_WARNING as c_int {
800 b"warning : \0"
801 } else if level == XML_ERR_ERROR as c_int || level == XML_ERR_FATAL as c_int {
802 b"error : \0"
803 } else {
804 b"\0"
805 };
806 if !lvl.is_empty() && lvl[0] != 0 {
807 ch_call0(handler, data, lvl.as_ptr() as *const c_char);
808 }
809
810 // 4. Message fragment.
811 if !message.is_null() {
812 let msg = message as *const u8;
813 let mut len = 0usize;
814 while unsafe { *msg.add(len) } != 0 {
815 len += 1;
816 }
817 let ends_nl = len > 0 && unsafe { *msg.add(len - 1) } == b'\n';
818 let fmt: &[u8] = if ends_nl { b"%s\0" } else { b"%s\n\0" };
819 ch_call1(handler, data, fmt.as_ptr() as *const c_char, msg as usize);
820 }
821
822 // 4b. Invalid-encoding byte dump (upstream xmlFormatError: the first 4
823 // bytes at the error position, only for XML_ERR_INVALID_ENCODING).
824 if code == XML_ERR_INVALID_ENCODING {
825 if let Some(bytes) = enc_bytes {
826 ch_call0(handler, data, c"Bytes:".as_ptr() as *const c_char);
827 for b in bytes {
828 // " 0x%02X"
829 let hex = format!(" 0x{:02X}\0", b);
830 ch_call0(handler, data, hex.as_ptr() as *const c_char);
831 }
832 ch_call0(handler, data, c"\n".as_ptr() as *const c_char);
833 }
834 }
835
836 // 5. Source window + caret (xmlParserPrintFileContextInternal).
837 if let Some((window, caret)) = source_window {
838 let mut win = window.to_vec();
839 win.push(0);
840 ch_call1(
841 handler,
842 data,
843 c"%s\n".as_ptr() as *const c_char,
844 win.as_ptr() as usize,
845 );
846 let mut caret_line = Vec::with_capacity(caret + 2);
847 for &b in window.iter().take(caret) {
848 caret_line.push(if b == b'\t' { b'\t' } else { b' ' });
849 }
850 caret_line.push(b'^');
851 caret_line.push(0);
852 ch_call1(
853 handler,
854 data,
855 c"%s\n".as_ptr() as *const c_char,
856 caret_line.as_ptr() as usize,
857 );
858 }
859}
860
861/// How a raise delivers to the generic side of the error system (upstream
862/// `xmlVRaiseError` channel selection, error.c 2.15).
863#[derive(Clone, Copy, Debug)]
864pub enum GenericDelivery {
865 /// Custom SAX channel: single call `channel(ctx, msg)`.
866 Custom(xmlGenericErrorFunc, *mut c_void),
867 /// Legacy/default channel: stream the `xmlFormatError` fragments through
868 /// the global generic handler.
869 Stream,
870 /// No channel (SAX slot NULL): no generic delivery.
871 None,
872}
873
874/// Raise an error with upstream's full routing (error.c 2.15
875/// `xmlVRaiseError`): update the last error, then deliver to the structured
876/// handler **or** the selected generic channel — never both.
877///
878/// `file`/`line`/`source_window` feed the generic fragment stream (the
879/// structured handler receives the complete `xmlError` instead). `col` is
880/// the 1-based byte column (upstream `input->col` → `err->int2`); `str1`..
881/// `str3`/`int1` are the upstream extra fields; `enc_bytes` feeds the
882/// `XML_ERR_INVALID_ENCODING` "Bytes:" fragment.
883///
884/// # UPSTREAM-PARITY (ownership)
885///
886/// Like upstream `xmlVSetError`, every string field of the stored error is
887/// owned (`xmlStrdup`): `file`/`str1`/`str2`/`str3` are heap copies, so the
888/// caller may pass transient C strings.
889///
890/// # SAFETY
891///
892/// - `ctxt` may be NULL.
893/// - `msg`, `file`, `str1`, `str2`, `str3` must be valid C strings or NULL.
894/// - `source_window` bytes must be valid for the duration of the call.
895#[allow(clippy::too_many_arguments)]
896pub unsafe fn raise_error_streamed(
897 ctxt: *mut c_void,
898 domain: c_int,
899 code: c_int,
900 level: c_int,
901 file: *const c_char,
902 line: c_int,
903 col: c_int,
904 str1: *const c_char,
905 str2: *const c_char,
906 str3: *const c_char,
907 int1: c_int,
908 msg: *const c_char,
909 source_window: Option<(&[u8], usize)>,
910 enc_bytes: Option<[u8; 4]>,
911 delivery: GenericDelivery,
912) {
913 // The streamed generic-error channel below uses an x86_64 SysV va_list
914 // trampoline (ch_call0/1/2 — register-based). Other ABIs (i686 cdecl,
915 // ARM/aarch64 AAPCS, ...) fall back to the plain raise path; full
916 // streamed-fragment parity there is an unexecuted platform obligation
917 // (atlas/PLATFORM_SURFACE_ATLAS.md, OBLIG-WORDSIZE-32 / compiler-ABI).
918 #[cfg(not(target_arch = "x86_64"))]
919 {
920 raise_error(
921 ctxt,
922 ptr::null_mut(),
923 ptr::null_mut(),
924 ptr::null_mut(),
925 ptr::null_mut(),
926 domain,
927 code,
928 level,
929 file,
930 line,
931 str1,
932 str2,
933 str3,
934 int1,
935 col,
936 msg,
937 );
938 return;
939 }
940
941 #[cfg(target_arch = "x86_64")]
942 {
943 // UPSTREAM-PARITY (xmlVRaiseError): warnings are suppressed when the
944 // global xmlGetWarningsDefaultValue is zero.
945 if level == xmlErrorLevel::XML_ERR_WARNING as c_int
946 && unsafe { crate::abi::data_globals::xmlGetWarningsDefaultValue } == 0
947 {
948 return;
949 }
950
951 raise_error_streamed_x86_64(
952 ctxt,
953 domain,
954 code,
955 level,
956 file,
957 line,
958 col,
959 str1,
960 str2,
961 str3,
962 int1,
963 msg,
964 source_window,
965 enc_bytes,
966 delivery,
967 );
968 }
969}
970
971/// x86-64 streamed raise (SysV va_list channel). See `raise_error_streamed`.
972///
973/// # Safety
974///
975/// - `ctxt` may be NULL; `msg`, `file`, `str1`, `str2`, `str3` must be
976/// valid NUL-terminated C strings or NULL; `source_window` bytes must be
977/// valid for the duration of the call; every string field is duplicated
978/// before being stored in the thread-local last error.
979#[allow(clippy::too_many_arguments)]
980#[cfg(target_arch = "x86_64")]
981unsafe fn raise_error_streamed_x86_64(
982 ctxt: *mut c_void,
983 domain: c_int,
984 code: c_int,
985 level: c_int,
986 file: *const c_char,
987 line: c_int,
988 col: c_int,
989 str1: *const c_char,
990 str2: *const c_char,
991 str3: *const c_char,
992 int1: c_int,
993 msg: *const c_char,
994 source_window: Option<(&[u8], usize)>,
995 enc_bytes: Option<[u8; 4]>,
996 delivery: GenericDelivery,
997) {
998 // UPSTREAM-PARITY (xmlVRaiseError): warnings are suppressed when the
999 // global xmlGetWarningsDefaultValue is zero.
1000 if level == xmlErrorLevel::XML_ERR_WARNING as c_int
1001 && unsafe { crate::abi::data_globals::xmlGetWarningsDefaultValue } == 0
1002 {
1003 return;
1004 }
1005
1006 // Format the error message (same as raise_error).
1007 let formatted_msg = format_error_message(domain, code, msg, str1, str2, str3);
1008 let file_copy = if file.is_null() {
1009 ptr::null_mut()
1010 } else {
1011 crate::abi::allocator::xmlMemStrdupImpl(file) as *mut c_char
1012 };
1013 let str1_copy = if str1.is_null() {
1014 ptr::null_mut()
1015 } else {
1016 crate::abi::allocator::xmlMemStrdupImpl(str1) as *mut c_char
1017 };
1018 let str2_copy = if str2.is_null() {
1019 ptr::null_mut()
1020 } else {
1021 crate::abi::allocator::xmlMemStrdupImpl(str2) as *mut c_char
1022 };
1023 let str3_copy = if str3.is_null() {
1024 ptr::null_mut()
1025 } else {
1026 crate::abi::allocator::xmlMemStrdupImpl(str3) as *mut c_char
1027 };
1028 let err = _xmlError {
1029 domain,
1030 code,
1031 message: formatted_msg,
1032 level,
1033 file: file_copy,
1034 line,
1035 str1: str1_copy,
1036 str2: str2_copy,
1037 str3: str3_copy,
1038 int1,
1039 int2: col,
1040 ctxt,
1041 node: ptr::null_mut(),
1042 };
1043
1044 globals::set_last_error(err);
1045
1046 // Structured handler wins (upstream `else if` chain); the (handler, ctx)
1047 // pair is read atomically and invoked outside the lock (11.1-X).
1048 let structured = globals::with_structured_error(|h, c| (h, c));
1049 if let Some(handler) = structured.0 {
1050 let err_ref = globals::get_last_error();
1051 if !err_ref.is_null() {
1052 handler(structured.1, err_ref as *const _xmlError);
1053 }
1054 return;
1055 }
1056
1057 match delivery {
1058 GenericDelivery::Custom(channel, ctx) => {
1059 if !msg.is_null() {
1060 // SAFETY: the caller provided a valid C callback.
1061 unsafe { channel(ctx, msg) };
1062 }
1063 }
1064 GenericDelivery::Stream => {
1065 if globals::get_generic_error_func().is_some() {
1066 unsafe {
1067 format_error_streamed(
1068 domain,
1069 code,
1070 level,
1071 formatted_msg,
1072 file,
1073 line,
1074 source_window,
1075 enc_bytes,
1076 )
1077 };
1078 }
1079 }
1080 GenericDelivery::None => {}
1081 }
1082}
1083
1084/// Default SAX v1 error handler — upstream `void xmlParserError(void *ctx,
1085/// const char *msg, ...)`. Variadic x86_64 SysV shim (11.1-Z.2, R-000176:
1086/// the previous fixed-arity body silently dropped the varargs; upstream
1087/// error.c formats them via `xmlVFormatLegacyError`).
1088///
1089/// 2 fixed args (ctx=rdi, msg=rsi) → `gp_offset` 16; the va_list pointer is
1090/// passed as the 3rd arg (rdx) to the `xmlParserErrorV` receiver.
1091///
1092/// # SAFETY
1093///
1094/// - `ctx` may be NULL (unused by the candidate's legacy path).
1095/// - `msg` must be a valid NUL-terminated printf format string.
1096#[cfg(target_arch = "x86_64")]
1097#[no_mangle]
1098pub unsafe extern "C" fn xmlParserError() -> c_int {
1099 unsafe { legacy_shim(xmlParserErrorV) }
1100}
1101
1102/// Variadic receiver for the `xmlParserError` shim: formats `msg` with the
1103/// caller's varargs and emits `"error: "` + formatted text through the
1104/// generic channel (upstream error.c `xmlVFormatLegacyError`).
1105///
1106/// # Safety
1107///
1108/// - `msg` must be a valid NUL-terminated printf format string, `ap` a
1109/// valid va_list matching the format, `ctx` may be NULL.
1110#[no_mangle]
1111pub unsafe extern "C" fn xmlParserErrorV(
1112 ctx: *mut c_void,
1113 msg: *const c_char,
1114 ap: *mut VaListTag,
1115) -> c_int {
1116 let _ = ctx;
1117 unsafe { emit_legacy_message_v("error", msg, ap) }
1118}
1119
1120/// Default SAX v1 warning handler — upstream `void xmlParserWarning(void
1121/// *ctx, const char *msg, ...)`. Variadic shim as `xmlParserError`.
1122///
1123/// # SAFETY
1124///
1125/// - `ctx` may be NULL (unused by the candidate's legacy path).
1126/// - `msg` must be a valid NUL-terminated printf format string.
1127#[cfg(target_arch = "x86_64")]
1128#[no_mangle]
1129pub unsafe extern "C" fn xmlParserWarning() -> c_int {
1130 unsafe { legacy_shim(xmlParserWarningV) }
1131}
1132
1133/// Variadic receiver for the `xmlParserWarning` shim.
1134///
1135/// # Safety
1136///
1137/// - `msg` must be a valid NUL-terminated printf format string, `ap` a
1138/// valid va_list matching the format, `ctx` may be NULL.
1139#[no_mangle]
1140pub unsafe extern "C" fn xmlParserWarningV(
1141 ctx: *mut c_void,
1142 msg: *const c_char,
1143 ap: *mut VaListTag,
1144) -> c_int {
1145 let _ = ctx;
1146 unsafe { emit_legacy_message_v("warning", msg, ap) }
1147}
1148
1149/// Default validity error handler — upstream `void
1150/// xmlParserValidityError(void *ctx, const char *msg, ...)`. Variadic shim
1151/// as `xmlParserError`.
1152///
1153/// # SAFETY
1154///
1155/// - `ctx` may be NULL (unused by the candidate's legacy path).
1156/// - `msg` must be a valid NUL-terminated printf format string.
1157#[cfg(target_arch = "x86_64")]
1158#[no_mangle]
1159pub unsafe extern "C" fn xmlParserValidityError() -> c_int {
1160 unsafe { legacy_shim(xmlParserValidityErrorV) }
1161}
1162
1163/// Variadic receiver for the `xmlParserValidityError` shim.
1164///
1165/// # Safety
1166///
1167/// - `msg` must be a valid NUL-terminated printf format string, `ap` a
1168/// valid va_list matching the format, `ctx` may be NULL.
1169#[no_mangle]
1170pub unsafe extern "C" fn xmlParserValidityErrorV(
1171 ctx: *mut c_void,
1172 msg: *const c_char,
1173 ap: *mut VaListTag,
1174) -> c_int {
1175 let _ = ctx;
1176 unsafe { emit_legacy_message_v("validity error", msg, ap) }
1177}
1178
1179/// Default validity warning handler — upstream `void
1180/// xmlParserValidityWarning(void *ctx, const char *msg, ...)`. Variadic
1181/// shim as `xmlParserError`.
1182///
1183/// # SAFETY
1184///
1185/// - `ctx` may be NULL (unused by the candidate's legacy path).
1186/// - `msg` must be a valid NUL-terminated printf format string.
1187#[cfg(target_arch = "x86_64")]
1188#[no_mangle]
1189pub unsafe extern "C" fn xmlParserValidityWarning() -> c_int {
1190 unsafe { legacy_shim(xmlParserValidityWarningV) }
1191}
1192
1193/// Variadic receiver for the `xmlParserValidityWarning` shim.
1194///
1195/// # Safety
1196///
1197/// - `msg` must be a valid NUL-terminated printf format string, `ap` a
1198/// valid va_list matching the format, `ctx` may be NULL.
1199#[no_mangle]
1200pub unsafe extern "C" fn xmlParserValidityWarningV(
1201 ctx: *mut c_void,
1202 msg: *const c_char,
1203 ap: *mut VaListTag,
1204) -> c_int {
1205 let _ = ctx;
1206 unsafe { emit_legacy_message_v("validity warning", msg, ap) }
1207}
1208
1209/// The variadic `xmlParserError` shim, transmuted to the fixed-arity SAX v1
1210/// callback type. The shim's declared arity is a Rust-side fiction (stable
1211/// Rust cannot express `c_variadic`); the ABI is a plain code pointer and
1212/// the C variadic call contract is preserved (11.1-Z.2, R-000176).
1213pub const XML_PARSER_ERROR_SAX1: errorSAXFunc = unsafe {
1214 // SAFETY: shim and SAX callback are both plain code pointers (same ABI);
1215 // the declared arity difference is the documented shim fiction.
1216 core::mem::transmute::<unsafe extern "C" fn() -> c_int, errorSAXFunc>(xmlParserError)
1217};
1218
1219/// The variadic `xmlParserWarning` shim as the SAX v1 callback type.
1220pub const XML_PARSER_WARNING_SAX1: errorSAXFunc = unsafe {
1221 // SAFETY: see XML_PARSER_ERROR_SAX1.
1222 core::mem::transmute::<unsafe extern "C" fn() -> c_int, errorSAXFunc>(xmlParserWarning)
1223};
1224
1225/// The variadic `xmlParserValidityError` shim as the validation callback
1226/// type (`xmlValidityErrorFunc`-compatible fixed-arity pointer).
1227pub const XML_PARSER_VALIDITY_ERROR_SAX1: unsafe extern "C" fn(*mut c_void, *const c_char) =
1228 // SAFETY: see XML_PARSER_ERROR_SAX1.
1229 unsafe {
1230 core::mem::transmute::<
1231 unsafe extern "C" fn() -> c_int,
1232 unsafe extern "C" fn(*mut c_void, *const c_char),
1233 >(xmlParserValidityError)
1234 };
1235
1236/// The variadic `xmlParserValidityWarning` shim as the validation callback
1237/// type.
1238pub const XML_PARSER_VALIDITY_WARNING_SAX1: unsafe extern "C" fn(*mut c_void, *const c_char) =
1239 // SAFETY: see XML_PARSER_ERROR_SAX1.
1240 unsafe {
1241 core::mem::transmute::<
1242 unsafe extern "C" fn() -> c_int,
1243 unsafe extern "C" fn(*mut c_void, *const c_char),
1244 >(xmlParserValidityWarning)
1245 };
1246
1247// ═══════════════════════════════════════════════════════════════════════════════
1248// Tests
1249// ═══════════════════════════════════════════════════════════════════════════════
1250
1251#[cfg(test)]
1252mod tests {
1253 use super::*;
1254 use crate::abi::allocator;
1255 use core::ffi::c_void;
1256
1257 /// Reset an error struct and verify the defaults are applied.
1258 ///
1259 /// # Safety
1260 ///
1261 /// - `err` is a stack `_xmlError` whose fields are all NULL/zero; it
1262 /// is valid for `reset_error` to write and for the subsequent reads.
1263 #[test]
1264 fn test_error_default_reset() {
1265 unsafe {
1266 let mut err = _xmlError {
1267 domain: XML_FROM_PARSER,
1268 code: XML_ERR_NO_MEMORY,
1269 message: ptr::null_mut(),
1270 level: XML_ERR_ERROR as c_int,
1271 file: ptr::null_mut(),
1272 line: 42,
1273 str1: ptr::null_mut(),
1274 str2: ptr::null_mut(),
1275 str3: ptr::null_mut(),
1276 int1: 0,
1277 int2: 0,
1278 ctxt: ptr::null_mut(),
1279 node: ptr::null_mut(),
1280 };
1281
1282 reset_error(&mut err);
1283 assert_eq!(err.domain, XML_FROM_NONE);
1284 assert_eq!(err.code, XML_ERR_OK as c_int);
1285 assert_eq!(err.level, XML_ERR_NONE as c_int);
1286 assert_eq!(err.line, 0);
1287 }
1288 }
1289
1290 /// Copy one error struct into another and verify the fields.
1291 ///
1292 /// # Safety
1293 ///
1294 /// - `from` and `to` are stack `_xmlError` structs valid for the
1295 /// `copy_error` copy and the subsequent field reads.
1296 #[test]
1297 fn test_copy_error() {
1298 unsafe {
1299 let from = _xmlError {
1300 domain: XML_FROM_PARSER,
1301 code: XML_ERR_NO_MEMORY,
1302 message: ptr::null_mut(),
1303 level: XML_ERR_FATAL as c_int,
1304 file: ptr::null_mut(),
1305 line: 100,
1306 str1: ptr::null_mut(),
1307 str2: ptr::null_mut(),
1308 str3: ptr::null_mut(),
1309 int1: 1,
1310 int2: 2,
1311 ctxt: ptr::null_mut(),
1312 node: ptr::null_mut(),
1313 };
1314 let mut to = _xmlError {
1315 domain: XML_FROM_NONE,
1316 code: XML_ERR_OK as c_int,
1317 message: ptr::null_mut(),
1318 level: XML_ERR_NONE as c_int,
1319 file: ptr::null_mut(),
1320 line: 0,
1321 str1: ptr::null_mut(),
1322 str2: ptr::null_mut(),
1323 str3: ptr::null_mut(),
1324 int1: 0,
1325 int2: 0,
1326 ctxt: ptr::null_mut(),
1327 node: ptr::null_mut(),
1328 };
1329
1330 let result = copy_error(&from, &mut to);
1331 assert_eq!(result, 0);
1332 assert_eq!(to.domain, XML_FROM_PARSER);
1333 assert_eq!(to.code, XML_ERR_NO_MEMORY);
1334 assert_eq!(to.level, XML_ERR_FATAL as c_int);
1335 assert_eq!(to.line, 100);
1336 assert_eq!(to.int1, 1);
1337 assert_eq!(to.int2, 2);
1338 }
1339 }
1340
1341 /// Raise an error and verify it is stored as the last error.
1342 ///
1343 /// # Safety
1344 ///
1345 /// - `file`/`str1` are static NUL-terminated strings valid for the
1346 /// raise; `raise_error` duplicates them, so the test's later reads of
1347 /// `last` only touch the thread-local copy; `reset_last_error`
1348 /// releases the owned strings exactly once.
1349 #[test]
1350 fn test_raise_and_get_last_error() {
1351 unsafe {
1352 reset_last_error();
1353 assert!(get_last_error().is_null());
1354
1355 let file = b"test.xml\0" as *const u8 as *const c_char;
1356 let str1 = b"element\0" as *const u8 as *const c_char;
1357
1358 raise_error(
1359 ptr::null_mut(),
1360 ptr::null_mut(),
1361 ptr::null_mut(),
1362 ptr::null_mut(),
1363 ptr::null_mut(),
1364 XML_FROM_PARSER,
1365 XML_ERR_TAG_NAME_MISMATCH,
1366 XML_ERR_ERROR as c_int,
1367 file,
1368 10,
1369 str1,
1370 ptr::null(),
1371 ptr::null(),
1372 0,
1373 0,
1374 ptr::null(),
1375 );
1376
1377 let last = get_last_error();
1378 assert!(!last.is_null());
1379 assert_eq!((*last).domain, XML_FROM_PARSER);
1380 assert_eq!((*last).code, XML_ERR_TAG_NAME_MISMATCH);
1381 assert_eq!((*last).level, XML_ERR_ERROR as c_int);
1382 assert_eq!((*last).line, 10);
1383
1384 // Check file was stored
1385 let last_file = (*last).file;
1386 assert!(!last_file.is_null());
1387
1388 reset_last_error();
1389 assert!(get_last_error().is_null());
1390 }
1391 }
1392
1393 #[test]
1394 fn test_structured_error_callback() {
1395 // Serialized against the handler-slot tests in xml::globals (11.1-X):
1396 // the structured handler slot is shared global state.
1397 let _guard = crate::xml::globals::ERROR_HANDLER_TEST_LOCK.lock();
1398 unsafe {
1399 reset_last_error();
1400
1401 // Set up a structured error handler that captures the error
1402 let mut captured_domain: c_int = 0;
1403 let captured_ptr = &mut captured_domain as *mut c_int as *mut c_void;
1404
1405 // SAFETY: The callback writes to captured_ptr which lives on the stack
1406 // for the duration of this test.
1407 extern "C" fn test_handler(ctx: *mut c_void, _err: *const _xmlError) {
1408 // SAFETY: ctx is valid for the test duration.
1409 unsafe {
1410 let captured = &mut *(ctx as *mut c_int);
1411 *captured = 42;
1412 }
1413 }
1414
1415 set_structured_error_func(captured_ptr, Some(test_handler as xmlStructuredErrorFunc));
1416
1417 raise_error(
1418 ptr::null_mut(),
1419 ptr::null_mut(),
1420 ptr::null_mut(),
1421 ptr::null_mut(),
1422 ptr::null_mut(),
1423 XML_FROM_PARSER,
1424 XML_ERR_OK as c_int,
1425 XML_ERR_WARNING as c_int,
1426 ptr::null(),
1427 0,
1428 ptr::null(),
1429 ptr::null(),
1430 ptr::null(),
1431 0,
1432 0,
1433 ptr::null(),
1434 );
1435
1436 assert_eq!(captured_domain, 42);
1437
1438 // Reset
1439 set_structured_error_func(ptr::null_mut(), None);
1440 reset_last_error();
1441 }
1442 }
1443
1444 /// Format messages with a direct `msg` and with domain plus `str1`.
1445 ///
1446 /// # Safety
1447 ///
1448 /// - `msg`/`str1` are static NUL-terminated strings valid for the
1449 /// calls; each returned buffer is allocator-owned and freed with
1450 /// `xmlFreeImpl` exactly once before the test ends.
1451 #[test]
1452 fn test_format_error_message() {
1453 unsafe {
1454 // Test with direct message
1455 let msg = b"test error\0" as *const u8 as *const c_char;
1456 let formatted = format_error_message(
1457 XML_FROM_NONE,
1458 XML_ERR_OK as c_int,
1459 msg,
1460 ptr::null(),
1461 ptr::null(),
1462 ptr::null(),
1463 );
1464 assert!(!formatted.is_null());
1465 let formatted_str = std::ffi::CStr::from_ptr(formatted);
1466 assert_eq!(formatted_str.to_bytes(), b"test error");
1467
1468 // Free the allocated message
1469 allocator::xmlFreeImpl(formatted as *mut c_void);
1470
1471 // Test with domain and str1
1472 let str1 = b"foo\0" as *const u8 as *const c_char;
1473 let formatted2 = format_error_message(
1474 XML_FROM_PARSER,
1475 XML_ERR_OK as c_int,
1476 ptr::null(),
1477 str1,
1478 ptr::null(),
1479 ptr::null(),
1480 );
1481 assert!(!formatted2.is_null());
1482 allocator::xmlFreeImpl(formatted2 as *mut c_void);
1483 }
1484 }
1485}