libxml_rs/xml/errors/mod.rs
1//! Error subsystem (§21, §85 Phase 1).
2//!
3//! Implements the libxml2 error reporting infrastructure:
4//!
5//! - `xmlError` struct management
6//! - Error domain/code registry
7//! - Structured error callbacks (thread-local storage)
8//! - Generic error callbacks (thread-local storage)
9//! - Last-error tracking (thread-local `xmlGetLastError`, `xmlResetLastError`, `xmlCopyError`)
10//! - Error message formatting
11//! - `xmlRaiseError()` — the central error reporting function
12//!
13//! # UPSTREAM-PARITY
14//!
15//! libxml2 has a two-tier error system:
16//!
17//! 1. **Structured errors** — `xmlStructuredErrorFunc` receives an `xmlErrorPtr`
18//! with all structured fields (domain, code, level, line, etc.)
19//!
20//! 2. **Generic errors** — `xmlGenericErrorFunc` receives a formatted string
21//! (printf-style). This is the older system, still widely used.
22//!
23//! Both systems coexist. When both handlers are set, both are called.
24//! The last error is stored thread-locally for retrieval via `xmlGetLastError`.
25//!
26//! # Phase 1 status
27//!
28//! Complete — all error functions are implemented.
29//! Variadic message formatting will be enhanced in Phase 2+.
30
31use core::ffi::c_void;
32use core::fmt::Write;
33use core::ptr;
34use std::os::raw::{c_char, c_int};
35
36use crate::abi::callbacks::{xmlGenericErrorFunc, xmlStructuredErrorFunc};
37use crate::abi::structs::_xmlError;
38use crate::abi::types::xmlErrorLevel::*;
39use crate::abi::types::*;
40use crate::xml::globals;
41
42// ═══════════════════════════════════════════════════════════════════════════════
43// Error Management Functions
44// ═══════════════════════════════════════════════════════════════════════════════
45
46/// Set the generic error handler.
47///
48/// # UPSTREAM-PARITY
49///
50/// ```c
51/// void xmlSetGenericErrorFunc(void *ctx, xmlGenericErrorFunc handler);
52/// ```
53///
54/// # SAFETY
55///
56/// - `handler` must be a valid function pointer or NULL (to reset to default).
57/// - If non-NULL, the handler may be called at any time with `ctx`.
58pub unsafe fn set_generic_error_func(ctx: *mut c_void, handler: Option<xmlGenericErrorFunc>) {
59 // SAFETY: Delegates to globals with same safety contract.
60 unsafe { globals::set_generic_error_func(ctx, handler) };
61}
62
63/// Set the structured error handler.
64///
65/// # UPSTREAM-PARITY
66///
67/// ```c
68/// void xmlSetStructuredErrorFunc(void *ctx, xmlStructuredErrorFunc handler);
69/// ```
70///
71/// # SAFETY
72///
73/// - `handler` must be a valid function pointer or NULL.
74pub unsafe fn set_structured_error_func(ctx: *mut c_void, handler: Option<xmlStructuredErrorFunc>) {
75 // SAFETY: Delegates to globals with same safety contract.
76 unsafe { globals::set_structured_error_func(ctx, handler) };
77}
78
79/// Get the last error for the current thread.
80///
81/// # UPSTREAM-PARITY
82///
83/// ```c
84/// xmlErrorPtr xmlGetLastError(void);
85/// ```
86///
87/// Returns a pointer to the last error, or NULL if no error occurred.
88/// The returned pointer is valid until the next libxml2 call in this thread.
89pub fn get_last_error() -> *mut _xmlError {
90 globals::get_last_error()
91}
92
93/// Copy an error from one location to another.
94///
95/// # UPSTREAM-PARITY
96///
97/// ```c
98/// xmlErrorPtr xmlCopyError(xmlErrorPtr from, xmlErrorPtr to);
99/// ```
100///
101/// Copies `from` into `to`. Returns 0 on success, -1 on error.
102///
103/// # SAFETY
104///
105/// - `from` and `to` must be valid pointers to `_xmlError` structs, or NULL.
106pub unsafe fn copy_error(from: *const _xmlError, to: *mut _xmlError) -> c_int {
107 if from.is_null() || to.is_null() {
108 return -1;
109 }
110 // SAFETY: Caller guarantees both pointers are valid.
111 unsafe {
112 ptr::copy_nonoverlapping(from, to, 1);
113 }
114 0
115}
116
117/// Reset an error structure to its default state.
118///
119/// # UPSTREAM-PARITY
120///
121/// ```c
122/// void xmlResetError(xmlErrorPtr err);
123/// ```
124///
125/// # SAFETY
126///
127/// - `err` must be a valid pointer to `_xmlError`, or NULL.
128pub unsafe fn reset_error(err: *mut _xmlError) {
129 if err.is_null() {
130 return;
131 }
132 // SAFETY: Caller guarantees pointer is valid.
133 unsafe {
134 ptr::write(
135 err,
136 _xmlError {
137 domain: XML_FROM_NONE,
138 code: XML_ERR_OK as c_int,
139 message: ptr::null_mut(),
140 level: XML_ERR_NONE as c_int,
141 file: ptr::null_mut(),
142 line: 0,
143 str1: ptr::null_mut(),
144 str2: ptr::null_mut(),
145 str3: ptr::null_mut(),
146 int1: 0,
147 int2: 0,
148 ctxt: ptr::null_mut(),
149 node: ptr::null_mut(),
150 },
151 );
152 }
153}
154
155/// Reset the last error for the current thread.
156///
157/// # UPSTREAM-PARITY
158///
159/// ```c
160/// void xmlResetLastError(void);
161/// ```
162pub fn reset_last_error() {
163 globals::reset_last_error();
164}
165
166/// Format an error message.
167///
168/// This function creates a formatted error message from the component parts.
169/// In Phase 1, this is a basic implementation. In Phase 2+, variadic
170/// printf-style formatting will be added.
171///
172/// Returns a C string pointer (allocated with xmlMalloc) that the caller
173/// must free with xmlFree, or NULL on allocation failure.
174///
175/// # UPSTREAM-PARITY
176///
177/// Upstream libxml2 uses `vsnprintf` internally for message formatting.
178/// We use a simple formatting approach that produces compatible output
179/// for the common error patterns.
180pub fn format_error_message(
181 _domain: c_int,
182 _code: c_int,
183 msg: *const c_char,
184 str1: *const c_char,
185 str2: *const c_char,
186 str3: *const c_char,
187) -> *mut c_char {
188 // Phase 1: basic message construction.
189 // If a direct message string is provided, use it.
190 if !msg.is_null() {
191 // SAFETY: Caller guarantees msg is a valid C string.
192 let msg_str = unsafe { crate::abi::allocator::xmlMemStrdup(msg) };
193 return msg_str as *mut c_char;
194 }
195
196 // Build a message from the component strings.
197 // This matches upstream behavior where domain/code are combined
198 // with str1/str2/str3 into a diagnostic message.
199 let mut buf: [u8; 1024] = [0; 1024];
200 let mut pos = 0;
201
202 // Write domain prefix
203 let domain_str = match _domain {
204 XML_FROM_PARSER => "parser",
205 XML_FROM_TREE => "tree",
206 XML_FROM_NAMESPACE => "namespace",
207 XML_FROM_DTD => "dtd",
208 XML_FROM_HTML => "html",
209 XML_FROM_MEMORY => "memory",
210 XML_FROM_OUTPUT => "output",
211 XML_FROM_IO => "io",
212 XML_FROM_XPATH => "xpath",
213 XML_FROM_XPOINTER => "xpointer",
214 XML_FROM_XINCLUDE => "xinclude",
215 XML_FROM_CATALOG => "catalog",
216 XML_FROM_C14N => "c14n",
217 XML_FROM_XSLT => "xslt",
218 XML_FROM_VALID => "valid",
219 XML_FROM_CHECK => "check",
220 XML_FROM_WRITER => "writer",
221 XML_FROM_MODULE => "module",
222 XML_FROM_I18N => "i18n",
223 XML_FROM_SCHEMATRONV => "schematron",
224 XML_FROM_BUFFER => "buffer",
225 XML_FROM_URI => "uri",
226 XML_FROM_NONE => "",
227 XML_FROM_FTP => "ftp",
228 XML_FROM_HTTP => "http",
229 XML_FROM_REGEXP => "regexp",
230 XML_FROM_DATATYPE => "datatype",
231 XML_FROM_SCHEMASP => "schema parser",
232 XML_FROM_SCHEMASV => "schema validator",
233 XML_FROM_RELAXNGP => "relaxng parser",
234 XML_FROM_RELAXNGV => "relaxng validator",
235 _ => "unknown",
236 };
237
238 if !domain_str.is_empty() {
239 let bytes = domain_str.as_bytes();
240 let len = bytes.len().min(buf.len().saturating_sub(pos + 2));
241 buf[pos..pos + len].copy_from_slice(&bytes[..len]);
242 pos += len;
243 buf[pos] = b' ';
244 pos += 1;
245 }
246
247 // Append str1 if present
248 if !str1.is_null() {
249 // SAFETY: Caller guarantees str1 is a valid C string.
250 let s = unsafe { crate::abi::versioning::c_str_to_bytes(str1).unwrap_or_default() };
251 if pos + s.len() + 3 <= buf.len() {
252 buf[pos] = b'\'';
253 pos += 1;
254 buf[pos..pos + s.len()].copy_from_slice(s);
255 pos += s.len();
256 buf[pos] = b'\'';
257 pos += 1;
258 buf[pos] = b' ';
259 pos += 1;
260 }
261 }
262
263 // Append str2 if present
264 if !str2.is_null() {
265 let s = unsafe { crate::abi::versioning::c_str_to_bytes(str2).unwrap_or_default() };
266 if pos + s.len() + 3 <= buf.len() {
267 buf[pos] = b'\'';
268 pos += 1;
269 buf[pos..pos + s.len()].copy_from_slice(s);
270 pos += s.len();
271 buf[pos] = b'\'';
272 pos += 1;
273 buf[pos] = b' ';
274 pos += 1;
275 }
276 }
277
278 // Append str3 if present
279 if !str3.is_null() {
280 let s = unsafe { crate::abi::versioning::c_str_to_bytes(str3).unwrap_or_default() };
281 if pos + s.len() + 3 <= buf.len() {
282 buf[pos] = b'\'';
283 pos += 1;
284 buf[pos..pos + s.len()].copy_from_slice(s);
285 pos += s.len();
286 buf[pos] = b'\'';
287 pos += 1;
288 buf[pos] = b' ';
289 pos += 1;
290 }
291 }
292
293 // Null-terminate
294 if pos < buf.len() {
295 buf[pos] = 0;
296 } else {
297 buf[buf.len() - 1] = 0;
298 }
299
300 // Allocate and return
301 let result = unsafe { crate::abi::allocator::xmlMalloc(pos + 1) };
302 if result.is_null() {
303 return ptr::null_mut();
304 }
305 unsafe {
306 ptr::copy_nonoverlapping(buf.as_ptr(), result as *mut u8, pos + 1);
307 }
308 result as *mut c_char
309}
310
311/// Raise an error — the central error reporting function.
312///
313/// This is called internally when an error occurs. It:
314/// 1. Updates the thread-local last error
315/// 2. Invokes the structured error handler if one is set
316/// 3. Invokes the generic error handler if one is set (for warnings/errors)
317///
318/// # UPSTREAM-PARITY
319///
320/// ```c
321/// void xmlRaiseError(xmlErrorPtr ctxt,
322/// xmlErrorPtr ctxt2,
323/// xmlErrorPtr ctxt3,
324/// xmlErrorPtr ctxt4,
325/// xmlErrorPtr ctxt5,
326/// int domain,
327/// int code,
328/// xmlErrorLevel level,
329/// const char *file,
330/// int line,
331/// const char *str1,
332/// const char *str2,
333/// const char *str3,
334/// int int1,
335/// int int2,
336/// const char *msg,
337/// ...);
338/// ```
339///
340/// # SAFETY
341///
342/// - `ctxt` may be NULL (context of the error).
343/// - `domain`, `code`, `level`: valid error codes.
344/// - `msg` must be a valid C string or NULL.
345/// - `file` must be a valid C string or NULL.
346/// - `str1`, `str2`, `str3`: error-related strings (may be NULL).
347pub unsafe fn raise_error(
348 ctxt: *mut c_void,
349 _ctxt2: *mut c_void,
350 _ctxt3: *mut c_void,
351 _ctxt4: *mut c_void,
352 _ctxt5: *mut c_void,
353 domain: c_int,
354 code: c_int,
355 level: c_int,
356 file: *const c_char,
357 line: c_int,
358 str1: *const c_char,
359 str2: *const c_char,
360 str3: *const c_char,
361 int1: c_int,
362 int2: c_int,
363 msg: *const c_char,
364) {
365 // Format the error message
366 let formatted_msg = format_error_message(domain, code, msg, str1, str2, str3);
367
368 // Store the last error
369 let err = _xmlError {
370 domain,
371 code,
372 message: formatted_msg,
373 level,
374 file: file as *mut c_char,
375 line,
376 str1: str1 as *mut c_char,
377 str2: str2 as *mut c_char,
378 str3: str3 as *mut c_char,
379 int1,
380 int2,
381 ctxt,
382 node: ptr::null_mut(),
383 };
384
385 globals::set_last_error(err);
386
387 // Call the structured error handler if set
388 if let Some(handler) = globals::get_structured_error_func() {
389 let ctx = globals::get_structured_error_ctx();
390 let err_ref = globals::get_last_error();
391 if !err_ref.is_null() {
392 handler(ctx, err_ref as *const _xmlError);
393 }
394 }
395
396 // Call the generic error handler if set (for warnings/errors)
397 if let Some(handler) = globals::get_generic_error_func() {
398 if level != 0 {
399 let ctx = globals::get_generic_error_ctx();
400 if !formatted_msg.is_null() {
401 handler(ctx, formatted_msg as *const core::ffi::c_char);
402 } else if !msg.is_null() {
403 handler(ctx, msg);
404 }
405 }
406 }
407
408 // Free the formatted message if it was allocated
409 // Note: We keep it as the last error's message, so we don't free it here.
410 // The next call to raise_error or reset_error will free the old message.
411 // Actually, in Phase 1, we don't free because the message is the last error's.
412 // A more complete implementation would free the old message when setting a new one.
413}
414
415// ═══════════════════════════════════════════════════════════════════════════════
416// Tests
417// ═══════════════════════════════════════════════════════════════════════════════
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422 use crate::abi::allocator;
423 use core::ffi::c_void;
424
425 #[test]
426 fn test_error_default_reset() {
427 unsafe {
428 let mut err = _xmlError {
429 domain: XML_FROM_PARSER,
430 code: XML_ERR_NO_MEMORY,
431 message: ptr::null_mut(),
432 level: XML_ERR_ERROR as c_int,
433 file: ptr::null_mut(),
434 line: 42,
435 str1: ptr::null_mut(),
436 str2: ptr::null_mut(),
437 str3: ptr::null_mut(),
438 int1: 0,
439 int2: 0,
440 ctxt: ptr::null_mut(),
441 node: ptr::null_mut(),
442 };
443
444 reset_error(&mut err);
445 assert_eq!(err.domain, XML_FROM_NONE);
446 assert_eq!(err.code, XML_ERR_OK as c_int);
447 assert_eq!(err.level, XML_ERR_NONE as c_int);
448 assert_eq!(err.line, 0);
449 }
450 }
451
452 #[test]
453 fn test_copy_error() {
454 unsafe {
455 let from = _xmlError {
456 domain: XML_FROM_PARSER,
457 code: XML_ERR_NO_MEMORY,
458 message: ptr::null_mut(),
459 level: XML_ERR_FATAL as c_int,
460 file: ptr::null_mut(),
461 line: 100,
462 str1: ptr::null_mut(),
463 str2: ptr::null_mut(),
464 str3: ptr::null_mut(),
465 int1: 1,
466 int2: 2,
467 ctxt: ptr::null_mut(),
468 node: ptr::null_mut(),
469 };
470 let mut to = _xmlError {
471 domain: XML_FROM_NONE,
472 code: XML_ERR_OK as c_int,
473 message: ptr::null_mut(),
474 level: XML_ERR_NONE as c_int,
475 file: ptr::null_mut(),
476 line: 0,
477 str1: ptr::null_mut(),
478 str2: ptr::null_mut(),
479 str3: ptr::null_mut(),
480 int1: 0,
481 int2: 0,
482 ctxt: ptr::null_mut(),
483 node: ptr::null_mut(),
484 };
485
486 let result = copy_error(&from, &mut to);
487 assert_eq!(result, 0);
488 assert_eq!(to.domain, XML_FROM_PARSER);
489 assert_eq!(to.code, XML_ERR_NO_MEMORY);
490 assert_eq!(to.level, XML_ERR_FATAL as c_int);
491 assert_eq!(to.line, 100);
492 assert_eq!(to.int1, 1);
493 assert_eq!(to.int2, 2);
494 }
495 }
496
497 #[test]
498 fn test_raise_and_get_last_error() {
499 unsafe {
500 reset_last_error();
501 assert!(get_last_error().is_null());
502
503 let file = b"test.xml\0" as *const u8 as *const c_char;
504 let str1 = b"element\0" as *const u8 as *const c_char;
505
506 raise_error(
507 ptr::null_mut(),
508 ptr::null_mut(),
509 ptr::null_mut(),
510 ptr::null_mut(),
511 ptr::null_mut(),
512 XML_FROM_PARSER,
513 XML_ERR_TAG_NAME_MISMATCH,
514 XML_ERR_ERROR as c_int,
515 file,
516 10,
517 str1,
518 ptr::null(),
519 ptr::null(),
520 0,
521 0,
522 ptr::null(),
523 );
524
525 let last = get_last_error();
526 assert!(!last.is_null());
527 assert_eq!((*last).domain, XML_FROM_PARSER);
528 assert_eq!((*last).code, XML_ERR_TAG_NAME_MISMATCH);
529 assert_eq!((*last).level, XML_ERR_ERROR as c_int);
530 assert_eq!((*last).line, 10);
531
532 // Check file was stored
533 let last_file = (*last).file;
534 assert!(!last_file.is_null());
535
536 reset_last_error();
537 assert!(get_last_error().is_null());
538 }
539 }
540
541 #[test]
542 fn test_structured_error_callback() {
543 unsafe {
544 reset_last_error();
545
546 // Set up a structured error handler that captures the error
547 let mut captured_domain: c_int = 0;
548 let captured_ptr = &mut captured_domain as *mut c_int as *mut c_void;
549
550 // SAFETY: The callback writes to captured_ptr which lives on the stack
551 // for the duration of this test.
552 extern "C" fn test_handler(ctx: *mut c_void, _err: *const _xmlError) {
553 // SAFETY: ctx is valid for the test duration.
554 unsafe {
555 let captured = &mut *(ctx as *mut c_int);
556 *captured = 42;
557 }
558 }
559
560 set_structured_error_func(captured_ptr, Some(test_handler as xmlStructuredErrorFunc));
561
562 raise_error(
563 ptr::null_mut(),
564 ptr::null_mut(),
565 ptr::null_mut(),
566 ptr::null_mut(),
567 ptr::null_mut(),
568 XML_FROM_PARSER,
569 XML_ERR_OK as c_int,
570 XML_ERR_WARNING as c_int,
571 ptr::null(),
572 0,
573 ptr::null(),
574 ptr::null(),
575 ptr::null(),
576 0,
577 0,
578 ptr::null(),
579 );
580
581 assert_eq!(captured_domain, 42);
582
583 // Reset
584 set_structured_error_func(ptr::null_mut(), None);
585 reset_last_error();
586 }
587 }
588
589 #[test]
590 fn test_format_error_message() {
591 unsafe {
592 // Test with direct message
593 let msg = b"test error\0" as *const u8 as *const c_char;
594 let formatted = format_error_message(
595 XML_FROM_NONE,
596 XML_ERR_OK as c_int,
597 msg,
598 ptr::null(),
599 ptr::null(),
600 ptr::null(),
601 );
602 assert!(!formatted.is_null());
603 let formatted_str = std::ffi::CStr::from_ptr(formatted);
604 assert_eq!(formatted_str.to_bytes(), b"test error");
605
606 // Free the allocated message
607 allocator::xmlFree(formatted as *mut c_void);
608
609 // Test with domain and str1
610 let str1 = b"foo\0" as *const u8 as *const c_char;
611 let formatted2 = format_error_message(
612 XML_FROM_PARSER,
613 XML_ERR_OK as c_int,
614 ptr::null(),
615 str1,
616 ptr::null(),
617 ptr::null(),
618 );
619 assert!(!formatted2.is_null());
620 allocator::xmlFree(formatted2 as *mut c_void);
621 }
622 }
623}