Skip to main content

libxml_rs/xslt/errors/
mod.rs

1//! XSLT error handling (§33, §85 Phase 8).
2//!
3//! Defines error domains, error levels, error handler types, and the
4//! public API for reporting and retrieving XSLT errors.
5//!
6//! # Phase 8 status
7//!
8//! Constants and function types are fully defined. Functions are stubbed
9//! and will be implemented as part of Phase 8.
10
11use crate::abi::structs::*;
12use std::os::raw::c_int;
13use std::ptr;
14
15// ── Error domains ─────────────────────────────────────────────────────────
16//
17// These constants identify the category of an XSLT error.
18// Source: xslt.h / xsltInternals.h (libxslt 1.1.45).
19
20/// No error.
21pub const XSLT_ERR_NONE: c_int = 0;
22
23/// Unknown error.
24pub const XSLT_ERR_UNKNOWN: c_int = 1;
25
26/// Missing required namespace.
27pub const XSLT_ERR_MISSING_NAMESPACE: c_int = 2;
28
29/// Invalid namespace.
30pub const XSLT_ERR_INVALID_NAMESPACE: c_int = 3;
31
32/// Missing required attribute.
33pub const XSLT_ERR_MISSING_ATTRIBUTE: c_int = 4;
34
35/// Invalid attribute value.
36pub const XSLT_ERR_INVALID_ATTRIBUTE: c_int = 5;
37
38/// Missing required element.
39pub const XSLT_ERR_MISSING_ELEMENT: c_int = 6;
40
41/// Invalid element.
42pub const XSLT_ERR_INVALID_ELEMENT: c_int = 7;
43
44/// Missing match attribute.
45pub const XSLT_ERR_MISSING_MATCH: c_int = 8;
46
47/// Missing name attribute.
48pub const XSLT_ERR_MISSING_NAME: c_int = 9;
49
50/// Missing select attribute.
51pub const XSLT_ERR_MISSING_SELECT: c_int = 10;
52
53/// Missing test attribute.
54pub const XSLT_ERR_MISSING_TEST: c_int = 11;
55
56/// Missing use attribute.
57pub const XSLT_ERR_MISSING_USE: c_int = 12;
58
59/// Invalid match pattern.
60pub const XSLT_ERR_INVALID_MATCH: c_int = 13;
61
62/// Invalid select expression.
63pub const XSLT_ERR_INVALID_SELECT: c_int = 14;
64
65/// Invalid test expression.
66pub const XSLT_ERR_INVALID_TEST: c_int = 15;
67
68/// Invalid use expression.
69pub const XSLT_ERR_INVALID_USE: c_int = 16;
70
71/// Missing namespace.
72pub const XSLT_ERR_MISSING_NS: c_int = 17;
73
74/// Cyclic reference detected.
75pub const XSLT_ERR_CYCLIC_REFERENCE: c_int = 18;
76
77/// Recursion limit exceeded.
78pub const XSLT_ERR_RECURSION: c_int = 19;
79
80/// Internal XSLT error.
81pub const XSLT_ERR_INTERNAL: c_int = 20;
82
83// ── Error levels ──────────────────────────────────────────────────────────
84//
85// These constants indicate the severity of an XSLT error.
86// Source: xslt.h (libxslt 1.1.45).
87
88/// No error level (unset).
89pub const XSLT_ERR_LEVEL_NONE: c_int = 0;
90
91/// Warning — non-fatal issue.
92pub const XSLT_ERR_LEVEL_WARNING: c_int = 1;
93
94/// Error — processing may continue but results may be incomplete.
95pub const XSLT_ERR_LEVEL_ERROR: c_int = 2;
96
97/// Fatal error — processing cannot continue.
98pub const XSLT_ERR_LEVEL_FATAL: c_int = 3;
99
100// ── Error handler types ───────────────────────────────────────────────────
101
102/// Global XSLT error handler function type.
103///
104/// Matches the upstream `xsltTransformErrorFunc` typedef:
105/// ```c
106/// typedef void (*xsltTransformErrorFunc)(void *ctxt, void *ctx,
107///                                        xsltStylesheetPtr style,
108///                                        const xmlChar *msg, ...);
109/// ```
110pub type xsltTransformErrorFunc = Option<
111    unsafe extern "C" fn(
112        *mut std::ffi::c_void,
113        *mut std::ffi::c_void,
114        *mut _xsltStylesheet,
115        *const crate::abi::types::xmlChar,
116        ...
117    ),
118>;
119
120// ── Public API ────────────────────────────────────────────────────────────
121
122/// The last XSLT error message (thread-local).
123use std::sync::Mutex;
124
125static LAST_XSLT_ERROR: Mutex<Option<Vec<u8>>> = Mutex::new(None);
126
127/// Global debug handler (upstream xsltGenericDebug).
128static mut XSLT_GENERIC_DEBUG: Option<
129    unsafe extern "C" fn(*mut std::ffi::c_void, *const std::os::raw::c_char),
130> = None;
131
132/// Set the generic debug handler (upstream `xsltSetGenericDebugFunc`).
133///
134/// # UPSTREAM-PARITY
135///
136/// ```c
137/// void xsltSetGenericDebugFunc(void *ctx, xmlGenericErrorFunc handler);
138/// ```
139///
140/// With a NULL handler, messages go to `stderr`; with a NULL context they
141/// are suppressed (upstream's default debug handler checks the context).
142#[no_mangle]
143pub unsafe extern "C" fn xsltSetGenericDebugFunc(
144    ctx: *mut std::ffi::c_void,
145    handler: Option<unsafe extern "C" fn(*mut std::ffi::c_void, *const std::os::raw::c_char)>,
146) {
147    unsafe {
148        XSLT_GENERIC_DEBUG_CONTEXT = ctx;
149        if handler.is_some() {
150            XSLT_GENERIC_DEBUG = handler;
151        }
152    }
153}
154
155static mut XSLT_GENERIC_DEBUG_CONTEXT: *mut std::ffi::c_void = std::ptr::null_mut();
156
157/// Emit a generic debug message (upstream xsltGenericDebug).
158#[no_mangle]
159pub unsafe extern "C" fn xsltGenericDebug(
160    ctx: *mut std::ffi::c_void,
161    msg: *const std::os::raw::c_char,
162) {
163    if ctx.is_null() || msg.is_null() {
164        return;
165    }
166    let len = libc::strlen(msg);
167    libc::write(2, msg as *const libc::c_void, len);
168}
169
170/// Set the transform error handler for a context.
171///
172/// Registers a per-context error handler that will be called for every
173/// error reported during the transformation. Pass `None` to restore the
174/// default handler.
175///
176/// # Parameters
177///
178/// * `ctxt`   — The transform context, or `std::ptr::null_mut()` for the
179///              global handler.
180/// * `ctx`    — Opaque user-data pointer passed to the handler.
181/// * `handler` — The error handler function, or `None` to reset.
182pub fn xsltSetTransformErrorFunc(
183    ctxt: *mut _xsltTransformContext,
184    ctx: *mut std::ffi::c_void,
185    handler: Option<unsafe extern "C" fn(*mut std::ffi::c_void, *const std::os::raw::c_char)>,
186) {
187    if ctxt.is_null() {
188        return;
189    }
190    // SAFETY: ctxt must be a valid _xsltTransformContext.
191    unsafe {
192        (*ctxt).error = handler;
193        (*ctxt).errctx = ctx;
194    }
195}
196
197/// Report an XSLT error.
198///
199/// Faithful port of upstream xsltutils.c `xsltTransformError`: the
200/// transform context is moved to the error state, the error context line
201/// is printed (upstream `xsltPrintErrorContext`), and the message is
202/// emitted verbatim through the registered handler or stderr. Messages
203/// carry their own trailing newline, exactly as upstream's do — no
204/// newline is added here.
205///
206/// The upstream signature is variadic (`const char *msg, ...`); the
207/// candidate's callers format the message before calling (a `%s`/`%d`
208/// placeholder is never expanded by this function).
209///
210/// # Parameters
211///
212/// * `ctxt`  — The transform context (may be null).
213/// * `style` — The stylesheet (may be null).
214/// * `inst`  — The instruction node that triggered the error (may be null).
215/// * `msg`   — The message, NUL-terminated, typically ending in `\n`.
216pub fn xsltTransformError(
217    ctxt: *mut _xsltTransformContext,
218    style: *mut _xsltStylesheet,
219    inst: *mut _xmlNode,
220    msg: *const std::os::raw::c_char,
221) {
222    if msg.is_null() {
223        return;
224    }
225    // SAFETY: msg must be a valid NUL-terminated C string.
226    let bytes =
227        unsafe { core::slice::from_raw_parts(msg as *const u8, libc::strlen(msg) as usize) };
228    let text = String::from_utf8_lossy(bytes).into_owned();
229
230    // Record the last error (the raw message, as upstream stores the
231    // formatted message).
232    if let Ok(mut last) = LAST_XSLT_ERROR.lock() {
233        *last = Some(text.clone().into_bytes());
234    }
235
236    // UPSTREAM-PARITY (xsltutils.c xsltTransformError): an error moves the
237    // transform context out of the OK state.
238    if !ctxt.is_null() {
239        // SAFETY: ctxt must be a valid _xsltTransformContext.
240        let ctx = unsafe { &mut *ctxt };
241        if ctx.state == crate::xslt::transform::XSLT_STATE_OK {
242            ctx.state = crate::xslt::transform::XSLT_STATE_ERROR;
243        }
244        let mut node = inst;
245        if node.is_null() {
246            node = ctx.inst;
247        }
248        // Build the context line (xsltPrintErrorContext) and the full
249        // message, then emit through the handler if one is registered.
250        let context_line = print_error_context(ctxt, style, node);
251        let full = format!("{}{}", context_line, text);
252        let mut cmsg = full.into_bytes();
253        let msg_len = cmsg.len();
254        cmsg.push(0);
255        let ctx = unsafe { &*ctxt };
256        if let Some(handler) = ctx.error {
257            unsafe { handler(ctx.errctx, cmsg.as_ptr() as *const std::os::raw::c_char) };
258            return;
259        }
260        let _ = unsafe { libc::write(2, cmsg.as_ptr() as *const libc::c_void, msg_len) };
261        return;
262    }
263
264    // No transform context: compile-time errors and standalone messages.
265    // (Upstream xsltPrintErrorContext is still invoked with NULL ctxt and
266    // the given style/node.)
267    let context_line = print_error_context(ptr::null_mut(), style, inst);
268    let full = format!("{}{}", context_line, text);
269    let mut cmsg = full.into_bytes();
270    let msg_len = cmsg.len();
271    cmsg.push(0);
272    let _ = unsafe { libc::write(2, cmsg.as_ptr() as *const libc::c_void, msg_len) };
273    let _ = style;
274}
275
276/// Build the error context line printed before an XSLT error message
277/// (upstream xsltutils.c `xsltPrintErrorContext`). The line is one of:
278///
279/// ```text
280/// error\n
281/// error: file F\n
282/// error: file F line N\n
283/// error: file F element E\n
284/// error: file F line N element E\n
285/// error: element E\n
286/// compilation error ... / runtime error ...
287/// ```
288fn print_error_context(
289    ctxt: *mut _xsltTransformContext,
290    style: *mut _xsltStylesheet,
291    node: *mut _xmlNode,
292) -> String {
293    let mut line = 0i64;
294    let mut file: *const std::os::raw::c_char = ptr::null();
295    let mut name: *const std::os::raw::c_char = ptr::null();
296
297    if !node.is_null() {
298        // SAFETY: node must be valid.
299        let node_ref = unsafe { &*node };
300        if node_ref.type_ == crate::abi::types::xmlElementType::XML_DOCUMENT_NODE as c_int
301            || node_ref.type_ == crate::abi::types::xmlElementType::XML_HTML_DOCUMENT_NODE as c_int
302        {
303            let doc = node as *mut crate::abi::structs::_xmlDoc;
304            // SAFETY: doc->URL is a valid NUL-terminated string or NULL.
305            file = unsafe { (*doc).URL } as *const std::os::raw::c_char;
306        } else {
307            line = unsafe { crate::abi::exports_xml2::xmlGetLineNo(node) as i64 };
308            // SAFETY: node->doc must be valid while the node is alive.
309            let doc = unsafe { (*node_ref).doc };
310            if !doc.is_null() {
311                file = unsafe { (*doc).URL } as *const std::os::raw::c_char;
312            }
313            name = node_ref.name as *const std::os::raw::c_char;
314        }
315    }
316
317    let errtype = if !ctxt.is_null() {
318        "runtime error"
319    } else if !style.is_null() {
320        "compilation error"
321    } else {
322        "error"
323    };
324
325    let s = |p: *const std::os::raw::c_char| -> String {
326        if p.is_null() {
327            String::new()
328        } else {
329            unsafe { std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned() }
330        }
331    };
332    let file_s = s(file);
333    let name_s = s(name);
334    let has_file = !file.is_null();
335    let has_name = !name.is_null();
336
337    if has_file && line != 0 && has_name {
338        format!(
339            "{}: file {} line {} element {}\n",
340            errtype, file_s, line, name_s
341        )
342    } else if has_file && has_name {
343        format!("{}: file {} element {}\n", errtype, file_s, name_s)
344    } else if has_file && line != 0 {
345        format!("{}: file {} line {}\n", errtype, file_s, line)
346    } else if has_file {
347        format!("{}: file {}\n", errtype, file_s)
348    } else if has_name {
349        format!("{}: element {}\n", errtype, name_s)
350    } else {
351        format!("{}\n", errtype)
352    }
353}
354
355/// Get the last XSLT error message as a NUL-terminated heap string.
356///
357/// Returns a pointer to the last error message, or `std::ptr::null_mut()`
358/// if no error has occurred. The caller frees with `libc::free`.
359pub fn xsltGetLastError() -> *mut std::ffi::c_void {
360    let guard = match LAST_XSLT_ERROR.lock() {
361        Ok(g) => g,
362        Err(_) => return std::ptr::null_mut(),
363    };
364    match guard.as_ref() {
365        Some(bytes) => {
366            let len = bytes.len();
367            // SAFETY: malloc returns writable memory or NULL.
368            let p = unsafe { libc::malloc(len + 1) } as *mut u8;
369            if p.is_null() {
370                return std::ptr::null_mut();
371            }
372            unsafe {
373                core::ptr::copy_nonoverlapping(bytes.as_ptr(), p, len);
374                *p.add(len) = 0;
375            }
376            p as *mut std::ffi::c_void
377        }
378        None => std::ptr::null_mut(),
379    }
380}