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//! # Upstream contract
7//!
8//! Parity target: upstream libxslt `xsltutils.c` + `xsltInternals.h`
9//! (1.1.45; `SRC-LIBXSLT-1.1.42-XSLTUTILS-C` under oracle/historical/src).
10//! The observable surface is `xsltTransformError` (with its
11//! `xsltPrintErrorContext` context line), `xsltSetTransformErrorFunc`,
12//! `xsltSetGenericDebugFunc`/`xsltGenericDebug`, `xsltGetLastError`, and
13//! the `XSLT_ERR_*` domain/level constants from xslt.h 1.1.45.
14//!
15//! # Conceptual behavior
16//!
17//! Error reporting follows upstream routing: `xsltTransformError` moves
18//! the transform context out of the OK state (error or stopped), prints
19//! the `xsltPrintErrorContext` context line (one of `error`,
20//! `compilation error`, `runtime error`, each optionally with file/line/
21//! element), and emits the message verbatim — messages carry their own
22//! trailing newline and the function is variadic upstream, so callers
23//! format before calling (the candidate never expands `%s`/`%d`
24//! placeholders). With a registered per-context handler the message is
25//! routed there; otherwise it goes to stderr.
26//!
27//! # Ownership & safety invariants
28//!
29//! - `LAST_XSLT_ERROR` is a mutex-guarded thread-safe copy of the last raw
30//!   message; `xsltGetLastError` returns a heap copy the caller frees with
31//!   libc::free (matching the documented `caller frees` contract for
32//!   `xsltGetLastError`).
33//! - Handler slots (`error`/`errctx`) are borrowed user-data, never
34//!   dereferenced by the library — per atlas/OWNERSHIP_ATLAS.md section 6,
35//!   the caller keeps the context alive.
36//! - `XSLT_GENERIC_DEBUG`/`XSLT_GENERIC_DEBUG_CONTEXT` are process-global
37//!   statics (upstream `xsltGenericDebug` globals); callers must not race
38//!   them (R-000171 slot-race lesson applied to the debug pair).
39//!
40//! # Historical quirks & epochs
41//!
42//! E-008 (atlas/SEMANTIC_EPOCHS.md): error output participates in the
43//! byte-identical xsltproc epoch (1.1.26, 2009, through 1.1.45), so the
44//! context-line wording is frozen. R-000161 fixed error routing parity for
45//! the generic/structured handler chain (xmlFormatError fragment
46//! streaming, 6 calls per raise) and the default variadic stderr printers
47//! `xmlGenericError`/`xsltGenericError`; the candidate `xsltGenericDebug`
48//! writes through fd 2 with the upstream NULL-context suppression.
49//! R-000140 covered the `_xslt*` ABI mirrors.
50//!
51//! # Deliberate oddities
52//!
53//! - The variadic upstream signature is reduced to a pre-formatted
54//!   message (an intentional, documented divergence — see the
55//!   `xsltTransformError` docs); the emitted bytes match the oracle.
56//! - `xsltSetGenericDebugFunc` keeps the upstream NULL-context
57//!   suppression quirk: a NULL handler with a NULL context suppresses
58//!   debug output.
59//!
60//! # Proving courts
61//!
62//! ERROR-001 (error-family differential probe; R-000161), CLI-XSLTPROC
63//! (stderr byte-compare on failing stylesheets), XSLT-001, and the
64//! in-crate `cargo test` suites.
65//!
66//! # Tempting simplifications that would break parity
67//!
68//! - Replacing the context line with a plain `error:` prefix breaks
69//!   stderr byte-parity for compilation and runtime errors (the
70//!   `xsltPrintErrorContext` forms are oracle-verified).
71//! - Buffering the last-error message with the error state would drop the
72//!   frozen `state` transition (OK → ERROR/STOPPED) that the transform
73//!   loop checks to stop execution.
74//! - Writing debug output unconditionally would break the upstream
75//!   NULL-context suppression contract exercised by the CLI corpus.
76
77use crate::abi::structs::*;
78use std::os::raw::c_int;
79use std::ptr;
80
81// ── Error domains ─────────────────────────────────────────────────────────
82//
83// These constants identify the category of an XSLT error.
84// Source: xslt.h / xsltInternals.h (libxslt 1.1.45).
85
86/// No error.
87pub const XSLT_ERR_NONE: c_int = 0;
88
89/// Unknown error.
90pub const XSLT_ERR_UNKNOWN: c_int = 1;
91
92/// Missing required namespace.
93pub const XSLT_ERR_MISSING_NAMESPACE: c_int = 2;
94
95/// Invalid namespace.
96pub const XSLT_ERR_INVALID_NAMESPACE: c_int = 3;
97
98/// Missing required attribute.
99pub const XSLT_ERR_MISSING_ATTRIBUTE: c_int = 4;
100
101/// Invalid attribute value.
102pub const XSLT_ERR_INVALID_ATTRIBUTE: c_int = 5;
103
104/// Missing required element.
105pub const XSLT_ERR_MISSING_ELEMENT: c_int = 6;
106
107/// Invalid element.
108pub const XSLT_ERR_INVALID_ELEMENT: c_int = 7;
109
110/// Missing match attribute.
111pub const XSLT_ERR_MISSING_MATCH: c_int = 8;
112
113/// Missing name attribute.
114pub const XSLT_ERR_MISSING_NAME: c_int = 9;
115
116/// Missing select attribute.
117pub const XSLT_ERR_MISSING_SELECT: c_int = 10;
118
119/// Missing test attribute.
120pub const XSLT_ERR_MISSING_TEST: c_int = 11;
121
122/// Missing use attribute.
123pub const XSLT_ERR_MISSING_USE: c_int = 12;
124
125/// Invalid match pattern.
126pub const XSLT_ERR_INVALID_MATCH: c_int = 13;
127
128/// Invalid select expression.
129pub const XSLT_ERR_INVALID_SELECT: c_int = 14;
130
131/// Invalid test expression.
132pub const XSLT_ERR_INVALID_TEST: c_int = 15;
133
134/// Invalid use expression.
135pub const XSLT_ERR_INVALID_USE: c_int = 16;
136
137/// Missing namespace.
138pub const XSLT_ERR_MISSING_NS: c_int = 17;
139
140/// Cyclic reference detected.
141pub const XSLT_ERR_CYCLIC_REFERENCE: c_int = 18;
142
143/// Recursion limit exceeded.
144pub const XSLT_ERR_RECURSION: c_int = 19;
145
146/// Internal XSLT error.
147pub const XSLT_ERR_INTERNAL: c_int = 20;
148
149// ── Error levels ──────────────────────────────────────────────────────────
150//
151// These constants indicate the severity of an XSLT error.
152// Source: xslt.h (libxslt 1.1.45).
153
154/// No error level (unset).
155pub const XSLT_ERR_LEVEL_NONE: c_int = 0;
156
157/// Warning — non-fatal issue.
158pub const XSLT_ERR_LEVEL_WARNING: c_int = 1;
159
160/// Error — processing may continue but results may be incomplete.
161pub const XSLT_ERR_LEVEL_ERROR: c_int = 2;
162
163/// Fatal error — processing cannot continue.
164pub const XSLT_ERR_LEVEL_FATAL: c_int = 3;
165
166// ── Error handler types ───────────────────────────────────────────────────
167
168/// Global XSLT error handler function type.
169///
170/// Matches the upstream `xsltTransformErrorFunc` typedef:
171/// ```c
172/// typedef void (*xsltTransformErrorFunc)(void *ctxt, void *ctx,
173///                                        xsltStylesheetPtr style,
174///                                        const xmlChar *msg, ...);
175/// ```
176pub type xsltTransformErrorFunc = Option<
177    unsafe extern "C" fn(
178        *mut std::ffi::c_void,
179        *mut std::ffi::c_void,
180        *mut _xsltStylesheet,
181        *const crate::abi::types::xmlChar,
182        ...
183    ),
184>;
185
186// ── Public API ────────────────────────────────────────────────────────────
187
188/// The last XSLT error message (thread-local).
189use std::sync::Mutex;
190
191static LAST_XSLT_ERROR: Mutex<Option<Vec<u8>>> = Mutex::new(None);
192
193/// Global debug handler (upstream xsltGenericDebug).
194static mut XSLT_GENERIC_DEBUG: Option<
195    unsafe extern "C" fn(*mut std::ffi::c_void, *const std::os::raw::c_char),
196> = None;
197
198/// Set the generic debug handler (upstream `xsltSetGenericDebugFunc`).
199///
200/// # UPSTREAM-PARITY
201///
202/// ```c
203/// void xsltSetGenericDebugFunc(void *ctx, xmlGenericErrorFunc handler);
204/// ```
205///
206/// With a NULL handler, messages go to `stderr`; with a NULL context they
207/// are suppressed (upstream's default debug handler checks the context).
208///
209/// # SAFETY
210///
211/// - `ctx` must be valid pointers (or NULL
212///   where the upstream C contract allows), obtained from the
213///   matching constructor/owner and not yet freed; the callee may
214///   take or keep ownership exactly as the C API specifies.
215///
216/// - `handler` must be a valid callback (or None);
217///   the callback is invoked with the documented context pointer and
218///   must itself uphold the same pointer invariants.
219///
220/// The caller must not race this call with concurrent mutation of the
221/// same objects from other threads (per-object state is not internally
222/// synchronized). Violating any of the above is undefined behavior.
223///
224/// Exercised by the C-API differential courts
225/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
226/// courts; those pass byte-for-byte against the upstream oracle.
227#[no_mangle]
228pub unsafe extern "C" fn xsltSetGenericDebugFunc(
229    ctx: *mut std::ffi::c_void,
230    handler: Option<unsafe extern "C" fn(*mut std::ffi::c_void, *const std::os::raw::c_char)>,
231) {
232    unsafe {
233        XSLT_GENERIC_DEBUG_CONTEXT = ctx;
234        if handler.is_some() {
235            XSLT_GENERIC_DEBUG = handler;
236        }
237    }
238}
239
240static mut XSLT_GENERIC_DEBUG_CONTEXT: *mut std::ffi::c_void = std::ptr::null_mut();
241
242/// Emit a generic debug message (upstream xsltGenericDebug).
243///
244/// # SAFETY
245///
246/// - `ctx`, `msg` must be valid pointers (or NULL
247///   where the upstream C contract allows), obtained from the
248///   matching constructor/owner and not yet freed; the callee may
249///   take or keep ownership exactly as the C API specifies.
250///
251/// The caller must not race this call with concurrent mutation of the
252/// same objects from other threads (per-object state is not internally
253/// synchronized). Violating any of the above is undefined behavior.
254///
255/// Exercised by the C-API differential courts
256/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
257/// courts; those pass byte-for-byte against the upstream oracle.
258#[no_mangle]
259pub unsafe extern "C" fn xsltGenericDebug(
260    ctx: *mut std::ffi::c_void,
261    msg: *const std::os::raw::c_char,
262) {
263    if ctx.is_null() || msg.is_null() {
264        return;
265    }
266    let len = libc::strlen(msg);
267    libc::write(2, msg as *const libc::c_void, len);
268}
269
270/// Set the transform error handler for a context.
271///
272/// Registers a per-context error handler that will be called for every
273/// error reported during the transformation. Pass `None` to restore the
274/// default handler.
275///
276/// # Parameters
277///
278/// * `ctxt`   — The transform context, or `std::ptr::null_mut()` for the
279///   global handler.
280/// * `ctx`    — Opaque user-data pointer passed to the handler.
281/// * `handler` — The error handler function, or `None` to reset.
282pub fn xsltSetTransformErrorFunc(
283    ctxt: *mut _xsltTransformContext,
284    ctx: *mut std::ffi::c_void,
285    handler: Option<unsafe extern "C" fn(*mut std::ffi::c_void, *const std::os::raw::c_char)>,
286) {
287    if ctxt.is_null() {
288        return;
289    }
290    // SAFETY: ctxt must be a valid _xsltTransformContext.
291    unsafe {
292        (*ctxt).error = handler;
293        (*ctxt).errctx = ctx;
294    }
295}
296
297/// Report an XSLT error.
298///
299/// Faithful port of upstream xsltutils.c `xsltTransformError`: the
300/// transform context is moved to the error state, the error context line
301/// is printed (upstream `xsltPrintErrorContext`), and the message is
302/// emitted verbatim through the registered handler or stderr. Messages
303/// carry their own trailing newline, exactly as upstream's do — no
304/// newline is added here.
305///
306/// The upstream signature is variadic (`const char *msg, ...`); the
307/// candidate's callers format the message before calling (a `%s`/`%d`
308/// placeholder is never expanded by this function).
309///
310/// # Parameters
311///
312/// * `ctxt`  — The transform context (may be null).
313/// * `style` — The stylesheet (may be null).
314/// * `inst`  — The instruction node that triggered the error (may be null).
315/// * `msg`   — The message, NUL-terminated, typically ending in `\n`.
316pub fn xsltTransformError(
317    ctxt: *mut _xsltTransformContext,
318    style: *mut _xsltStylesheet,
319    inst: *mut _xmlNode,
320    msg: *const std::os::raw::c_char,
321) {
322    if msg.is_null() {
323        return;
324    }
325    // SAFETY: msg must be a valid NUL-terminated C string.
326    let bytes =
327        unsafe { core::slice::from_raw_parts(msg as *const u8, libc::strlen(msg) as usize) };
328    let text = String::from_utf8_lossy(bytes).into_owned();
329
330    // Record the last error (the raw message, as upstream stores the
331    // formatted message).
332    if let Ok(mut last) = LAST_XSLT_ERROR.lock() {
333        *last = Some(text.clone().into_bytes());
334    }
335
336    // UPSTREAM-PARITY (xsltutils.c xsltTransformError): an error moves the
337    // transform context out of the OK state.
338    if !ctxt.is_null() {
339        // SAFETY: ctxt must be a valid _xsltTransformContext.
340        let ctx = unsafe { &mut *ctxt };
341        if ctx.state == crate::xslt::transform::XSLT_STATE_OK {
342            ctx.state = crate::xslt::transform::XSLT_STATE_ERROR;
343        }
344        let mut node = inst;
345        if node.is_null() {
346            node = ctx.inst;
347        }
348        // Build the context line (xsltPrintErrorContext) and the full
349        // message, then emit through the handler if one is registered.
350        let context_line = print_error_context(ctxt, style, node);
351        let full = format!("{}{}", context_line, text);
352        let mut cmsg = full.into_bytes();
353        let msg_len = cmsg.len();
354        cmsg.push(0);
355        let ctx = unsafe { &*ctxt };
356        if let Some(handler) = ctx.error {
357            unsafe { handler(ctx.errctx, cmsg.as_ptr() as *const std::os::raw::c_char) };
358            return;
359        }
360        let _ = unsafe { libc::write(2, cmsg.as_ptr() as *const libc::c_void, msg_len) };
361        return;
362    }
363
364    // No transform context: compile-time errors and standalone messages.
365    // (Upstream xsltPrintErrorContext is still invoked with NULL ctxt and
366    // the given style/node.)
367    let context_line = print_error_context(ptr::null_mut(), style, inst);
368    let full = format!("{}{}", context_line, text);
369    let mut cmsg = full.into_bytes();
370    let msg_len = cmsg.len();
371    cmsg.push(0);
372    let _ = unsafe { libc::write(2, cmsg.as_ptr() as *const libc::c_void, msg_len) };
373    let _ = style;
374}
375
376/// Build the error context line printed before an XSLT error message
377/// (upstream xsltutils.c `xsltPrintErrorContext`). The line is one of:
378///
379/// ```text
380/// error\n
381/// error: file F\n
382/// error: file F line N\n
383/// error: file F element E\n
384/// error: file F line N element E\n
385/// error: element E\n
386/// compilation error ... / runtime error ...
387/// ```
388fn print_error_context(
389    ctxt: *mut _xsltTransformContext,
390    style: *mut _xsltStylesheet,
391    node: *mut _xmlNode,
392) -> String {
393    let mut line = 0i64;
394    let mut file: *const std::os::raw::c_char = ptr::null();
395    let mut name: *const std::os::raw::c_char = ptr::null();
396
397    if !node.is_null() {
398        // SAFETY: node must be valid.
399        let node_ref = unsafe { &*node };
400        if node_ref.type_ == crate::abi::types::xmlElementType::XML_DOCUMENT_NODE as c_int
401            || node_ref.type_ == crate::abi::types::xmlElementType::XML_HTML_DOCUMENT_NODE as c_int
402        {
403            let doc = node as *mut crate::abi::structs::_xmlDoc;
404            // SAFETY: doc->URL is a valid NUL-terminated string or NULL.
405            file = unsafe { (*doc).URL } as *const std::os::raw::c_char;
406        } else {
407            line = crate::abi::exports_xml2::xmlGetLineNo(node) as i64;
408            // SAFETY: node->doc must be valid while the node is alive.
409            let doc = { node_ref.doc };
410            if !doc.is_null() {
411                file = unsafe { (*doc).URL } as *const std::os::raw::c_char;
412            }
413            name = node_ref.name as *const std::os::raw::c_char;
414        }
415    }
416
417    let errtype = if !ctxt.is_null() {
418        "runtime error"
419    } else if !style.is_null() {
420        "compilation error"
421    } else {
422        "error"
423    };
424
425    let s = |p: *const std::os::raw::c_char| -> String {
426        if p.is_null() {
427            String::new()
428        } else {
429            unsafe { std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned() }
430        }
431    };
432    let file_s = s(file);
433    let name_s = s(name);
434    let has_file = !file.is_null();
435    let has_name = !name.is_null();
436
437    if has_file && line != 0 && has_name {
438        format!(
439            "{}: file {} line {} element {}\n",
440            errtype, file_s, line, name_s
441        )
442    } else if has_file && has_name {
443        format!("{}: file {} element {}\n", errtype, file_s, name_s)
444    } else if has_file && line != 0 {
445        format!("{}: file {} line {}\n", errtype, file_s, line)
446    } else if has_file {
447        format!("{}: file {}\n", errtype, file_s)
448    } else if has_name {
449        format!("{}: element {}\n", errtype, name_s)
450    } else {
451        format!("{}\n", errtype)
452    }
453}
454
455/// Get the last XSLT error message as a NUL-terminated heap string.
456///
457/// Returns a pointer to the last error message, or `std::ptr::null_mut()`
458/// if no error has occurred. The caller frees with `libc::free`.
459pub fn xsltGetLastError() -> *mut std::ffi::c_void {
460    let guard = match LAST_XSLT_ERROR.lock() {
461        Ok(g) => g,
462        Err(_) => return std::ptr::null_mut(),
463    };
464    match guard.as_ref() {
465        Some(bytes) => {
466            let len = bytes.len();
467            // SAFETY: malloc returns writable memory or NULL.
468            let p = unsafe { libc::malloc(len + 1) } as *mut u8;
469            if p.is_null() {
470                return std::ptr::null_mut();
471            }
472            unsafe {
473                core::ptr::copy_nonoverlapping(bytes.as_ptr(), p, len);
474                *p.add(len) = 0;
475            }
476            p as *mut std::ffi::c_void
477        }
478        None => std::ptr::null_mut(),
479    }
480}