Skip to main content

libxml_rs/xml/errors/
mod.rs

1//! Error subsystem (§21, §85 Phase 1).
2//!
3//! Implements the libxml2 error reporting infrastructure:
4//!
5//! - `xmlError` struct management
6//! - Error domain/code registry
7//! - Structured error callbacks (thread-local storage)
8//! - Generic error callbacks (thread-local storage)
9//! - Last-error tracking (thread-local `xmlGetLastError`, `xmlResetLastError`, `xmlCopyError`)
10//! - Error message formatting
11//! - `xmlRaiseError()` — the central error reporting function
12//!
13//! # UPSTREAM-PARITY
14//!
15//! libxml2 has a two-tier error system:
16//!
17//! 1. **Structured errors** — `xmlStructuredErrorFunc` receives an `xmlErrorPtr`
18//!    with all structured fields (domain, code, level, line, etc.)
19//!
20//! 2. **Generic errors** — `xmlGenericErrorFunc` receives a formatted string
21//!    (printf-style). This is the older system, still widely used.
22//!
23//! Both systems coexist. When both handlers are set, both are called.
24//! The last error is stored thread-locally for retrieval via `xmlGetLastError`.
25//!
26//! # Phase 1 status
27//!
28//! Complete — all error functions are implemented.
29//! Variadic message formatting will be enhanced in Phase 2+.
30
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/// Emit a legacy-format message through the generic error channel.
416///
417/// Upstream's `xmlGenericErrorDefaultFunc` writes the message to stderr;
418/// when a custom generic handler is installed it receives the message. The
419/// `level` prefix matches upstream `xmlVFormatLegacyError` (error.c 2.15).
420unsafe fn emit_legacy_message(level: &str, msg: *const c_char) {
421    if msg.is_null() {
422        return;
423    }
424    let len = libc::strlen(msg) as usize;
425    let text = core::slice::from_raw_parts(msg as *const u8, len);
426    let mut full = Vec::with_capacity(level.len() + 2 + len);
427    full.extend_from_slice(level.as_bytes());
428    full.push(b':');
429    full.push(b' ');
430    full.extend_from_slice(text);
431    if let Some(handler) = globals::get_generic_error_func() {
432        let ctx = globals::get_generic_error_ctx();
433        let mut cmsg = full.clone();
434        cmsg.push(0);
435        handler(ctx, cmsg.as_ptr() as *const c_char);
436    } else {
437        // Upstream default (xmlGenericErrorDefaultFunc): stderr.
438        let _ = libc::write(2, full.as_ptr() as *const libc::c_void, full.len());
439    }
440}
441
442/// Default SAX v1 error handler — `void xmlParserError(void *ctx, const char *msg, ...)`.
443///
444/// UPSTREAM-PARITY: the candidate exposes the non-variadic ABI
445/// (`fn(ctx, msg)`); variadic formatting is not reproducible on stable Rust
446/// (documented safe divergence — callers pass pre-formatted messages).
447///
448/// # SAFETY
449///
450/// - `ctx` may be NULL (unused by the candidate's legacy path).
451/// - `msg` must be a valid NUL-terminated C string or NULL.
452#[no_mangle]
453pub unsafe extern "C" fn xmlParserError(ctx: *mut c_void, msg: *const c_char) {
454    let _ = ctx;
455    unsafe { emit_legacy_message("error", msg) };
456}
457
458/// Default SAX v1 warning handler — `void xmlParserWarning(void *ctx, const char *msg, ...)`.
459#[no_mangle]
460pub unsafe extern "C" fn xmlParserWarning(ctx: *mut c_void, msg: *const c_char) {
461    let _ = ctx;
462    unsafe { emit_legacy_message("warning", msg) };
463}
464
465/// Default validity error handler — `void xmlParserValidityError(void *ctx, const char *msg, ...)`.
466#[no_mangle]
467pub unsafe extern "C" fn xmlParserValidityError(ctx: *mut c_void, msg: *const c_char) {
468    let _ = ctx;
469    unsafe { emit_legacy_message("validity error", msg) };
470}
471
472/// Default validity warning handler — `void xmlParserValidityWarning(void *ctx, const char *msg, ...)`.
473#[no_mangle]
474pub unsafe extern "C" fn xmlParserValidityWarning(ctx: *mut c_void, msg: *const c_char) {
475    let _ = ctx;
476    unsafe { emit_legacy_message("validity warning", msg) };
477}
478
479// ═══════════════════════════════════════════════════════════════════════════════
480// Tests
481// ═══════════════════════════════════════════════════════════════════════════════
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486    use crate::abi::allocator;
487    use core::ffi::c_void;
488
489    #[test]
490    fn test_error_default_reset() {
491        unsafe {
492            let mut err = _xmlError {
493                domain: XML_FROM_PARSER,
494                code: XML_ERR_NO_MEMORY,
495                message: ptr::null_mut(),
496                level: XML_ERR_ERROR as c_int,
497                file: ptr::null_mut(),
498                line: 42,
499                str1: ptr::null_mut(),
500                str2: ptr::null_mut(),
501                str3: ptr::null_mut(),
502                int1: 0,
503                int2: 0,
504                ctxt: ptr::null_mut(),
505                node: ptr::null_mut(),
506            };
507
508            reset_error(&mut err);
509            assert_eq!(err.domain, XML_FROM_NONE);
510            assert_eq!(err.code, XML_ERR_OK as c_int);
511            assert_eq!(err.level, XML_ERR_NONE as c_int);
512            assert_eq!(err.line, 0);
513        }
514    }
515
516    #[test]
517    fn test_copy_error() {
518        unsafe {
519            let from = _xmlError {
520                domain: XML_FROM_PARSER,
521                code: XML_ERR_NO_MEMORY,
522                message: ptr::null_mut(),
523                level: XML_ERR_FATAL as c_int,
524                file: ptr::null_mut(),
525                line: 100,
526                str1: ptr::null_mut(),
527                str2: ptr::null_mut(),
528                str3: ptr::null_mut(),
529                int1: 1,
530                int2: 2,
531                ctxt: ptr::null_mut(),
532                node: ptr::null_mut(),
533            };
534            let mut to = _xmlError {
535                domain: XML_FROM_NONE,
536                code: XML_ERR_OK as c_int,
537                message: ptr::null_mut(),
538                level: XML_ERR_NONE as c_int,
539                file: ptr::null_mut(),
540                line: 0,
541                str1: ptr::null_mut(),
542                str2: ptr::null_mut(),
543                str3: ptr::null_mut(),
544                int1: 0,
545                int2: 0,
546                ctxt: ptr::null_mut(),
547                node: ptr::null_mut(),
548            };
549
550            let result = copy_error(&from, &mut to);
551            assert_eq!(result, 0);
552            assert_eq!(to.domain, XML_FROM_PARSER);
553            assert_eq!(to.code, XML_ERR_NO_MEMORY);
554            assert_eq!(to.level, XML_ERR_FATAL as c_int);
555            assert_eq!(to.line, 100);
556            assert_eq!(to.int1, 1);
557            assert_eq!(to.int2, 2);
558        }
559    }
560
561    #[test]
562    fn test_raise_and_get_last_error() {
563        unsafe {
564            reset_last_error();
565            assert!(get_last_error().is_null());
566
567            let file = b"test.xml\0" as *const u8 as *const c_char;
568            let str1 = b"element\0" as *const u8 as *const c_char;
569
570            raise_error(
571                ptr::null_mut(),
572                ptr::null_mut(),
573                ptr::null_mut(),
574                ptr::null_mut(),
575                ptr::null_mut(),
576                XML_FROM_PARSER,
577                XML_ERR_TAG_NAME_MISMATCH,
578                XML_ERR_ERROR as c_int,
579                file,
580                10,
581                str1,
582                ptr::null(),
583                ptr::null(),
584                0,
585                0,
586                ptr::null(),
587            );
588
589            let last = get_last_error();
590            assert!(!last.is_null());
591            assert_eq!((*last).domain, XML_FROM_PARSER);
592            assert_eq!((*last).code, XML_ERR_TAG_NAME_MISMATCH);
593            assert_eq!((*last).level, XML_ERR_ERROR as c_int);
594            assert_eq!((*last).line, 10);
595
596            // Check file was stored
597            let last_file = (*last).file;
598            assert!(!last_file.is_null());
599
600            reset_last_error();
601            assert!(get_last_error().is_null());
602        }
603    }
604
605    #[test]
606    fn test_structured_error_callback() {
607        unsafe {
608            reset_last_error();
609
610            // Set up a structured error handler that captures the error
611            let mut captured_domain: c_int = 0;
612            let captured_ptr = &mut captured_domain as *mut c_int as *mut c_void;
613
614            // SAFETY: The callback writes to captured_ptr which lives on the stack
615            // for the duration of this test.
616            extern "C" fn test_handler(ctx: *mut c_void, _err: *const _xmlError) {
617                // SAFETY: ctx is valid for the test duration.
618                unsafe {
619                    let captured = &mut *(ctx as *mut c_int);
620                    *captured = 42;
621                }
622            }
623
624            set_structured_error_func(captured_ptr, Some(test_handler as xmlStructuredErrorFunc));
625
626            raise_error(
627                ptr::null_mut(),
628                ptr::null_mut(),
629                ptr::null_mut(),
630                ptr::null_mut(),
631                ptr::null_mut(),
632                XML_FROM_PARSER,
633                XML_ERR_OK as c_int,
634                XML_ERR_WARNING as c_int,
635                ptr::null(),
636                0,
637                ptr::null(),
638                ptr::null(),
639                ptr::null(),
640                0,
641                0,
642                ptr::null(),
643            );
644
645            assert_eq!(captured_domain, 42);
646
647            // Reset
648            set_structured_error_func(ptr::null_mut(), None);
649            reset_last_error();
650        }
651    }
652
653    #[test]
654    fn test_format_error_message() {
655        unsafe {
656            // Test with direct message
657            let msg = b"test error\0" as *const u8 as *const c_char;
658            let formatted = format_error_message(
659                XML_FROM_NONE,
660                XML_ERR_OK as c_int,
661                msg,
662                ptr::null(),
663                ptr::null(),
664                ptr::null(),
665            );
666            assert!(!formatted.is_null());
667            let formatted_str = std::ffi::CStr::from_ptr(formatted);
668            assert_eq!(formatted_str.to_bytes(), b"test error");
669
670            // Free the allocated message
671            allocator::xmlFree(formatted as *mut c_void);
672
673            // Test with domain and str1
674            let str1 = b"foo\0" as *const u8 as *const c_char;
675            let formatted2 = format_error_message(
676                XML_FROM_PARSER,
677                XML_ERR_OK as c_int,
678                ptr::null(),
679                str1,
680                ptr::null(),
681                ptr::null(),
682            );
683            assert!(!formatted2.is_null());
684            allocator::xmlFree(formatted2 as *mut c_void);
685        }
686    }
687}