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//! - The exported `xsltGenericDebug` / `xsltGenericDebugContext` data globals
37//!   (crate::abi::data_globals) are process-global, exactly like the upstream
38//!   `xsltGenericDebug` globals; callers must not race them (R-000171
39//!   slot-race lesson applied to the debug pair). 11.1-Z.1: `xsltGenericDebug`
40//!   is exported as DATA (function pointer, oracle D) with the upstream
41//!   default handler, not as a function (R-000174).
42//!
43//! # Historical quirks & epochs
44//!
45//! E-008 (atlas/SEMANTIC_EPOCHS.md): error output participates in the
46//! byte-identical xsltproc epoch (1.1.26, 2009, through 1.1.45), so the
47//! context-line wording is frozen. R-000161 fixed error routing parity for
48//! the generic/structured handler chain (xmlFormatError fragment
49//! streaming, 6 calls per raise) and the default variadic stderr printers
50//! `xmlGenericError`/`xsltGenericError`; the candidate `xsltGenericDebug`
51//! default handler writes through the `xsltGenericDebugContext` FILE* with
52//! the upstream NULL-context suppression (11.1-Z.1, R-000174).
53//! R-000140 covered the `_xslt*` ABI mirrors.
54//!
55//! # Deliberate oddities
56//!
57//! - The variadic upstream signature is reduced to a pre-formatted
58//!   message (an intentional, documented divergence — see the
59//!   `xsltTransformError` docs); the emitted bytes match the oracle.
60//! - `xsltSetGenericDebugFunc` keeps the upstream NULL-context
61//!   suppression quirk: a NULL handler with a NULL context suppresses
62//!   debug output.
63//!
64//! # Proving courts
65//!
66//! ERROR-001 (error-family differential probe; R-000161), CLI-XSLTPROC
67//! (stderr byte-compare on failing stylesheets), XSLT-001, and the
68//! in-crate `cargo test` suites.
69//!
70//! # Tempting simplifications that would break parity
71//!
72//! - Replacing the context line with a plain `error:` prefix breaks
73//!   stderr byte-parity for compilation and runtime errors (the
74//!   `xsltPrintErrorContext` forms are oracle-verified).
75//! - Buffering the last-error message with the error state would drop the
76//!   frozen `state` transition (OK → ERROR/STOPPED) that the transform
77//!   loop checks to stop execution.
78//! - Writing debug output unconditionally would break the upstream
79//!   NULL-context suppression contract exercised by the CLI corpus.
80
81use crate::abi::callbacks::xmlGenericErrorFunc;
82use crate::abi::structs::*;
83use std::os::raw::c_char;
84use std::os::raw::c_int;
85use std::os::raw::c_void;
86use std::ptr;
87
88// ── Error domains ─────────────────────────────────────────────────────────
89//
90// These constants identify the category of an XSLT error.
91// Source: xslt.h / xsltInternals.h (libxslt 1.1.45).
92
93/// No error.
94pub const XSLT_ERR_NONE: c_int = 0;
95
96/// Unknown error.
97pub const XSLT_ERR_UNKNOWN: c_int = 1;
98
99/// Missing required namespace.
100pub const XSLT_ERR_MISSING_NAMESPACE: c_int = 2;
101
102/// Invalid namespace.
103pub const XSLT_ERR_INVALID_NAMESPACE: c_int = 3;
104
105/// Missing required attribute.
106pub const XSLT_ERR_MISSING_ATTRIBUTE: c_int = 4;
107
108/// Invalid attribute value.
109pub const XSLT_ERR_INVALID_ATTRIBUTE: c_int = 5;
110
111/// Missing required element.
112pub const XSLT_ERR_MISSING_ELEMENT: c_int = 6;
113
114/// Invalid element.
115pub const XSLT_ERR_INVALID_ELEMENT: c_int = 7;
116
117/// Missing match attribute.
118pub const XSLT_ERR_MISSING_MATCH: c_int = 8;
119
120/// Missing name attribute.
121pub const XSLT_ERR_MISSING_NAME: c_int = 9;
122
123/// Missing select attribute.
124pub const XSLT_ERR_MISSING_SELECT: c_int = 10;
125
126/// Missing test attribute.
127pub const XSLT_ERR_MISSING_TEST: c_int = 11;
128
129/// Missing use attribute.
130pub const XSLT_ERR_MISSING_USE: c_int = 12;
131
132/// Invalid match pattern.
133pub const XSLT_ERR_INVALID_MATCH: c_int = 13;
134
135/// Invalid select expression.
136pub const XSLT_ERR_INVALID_SELECT: c_int = 14;
137
138/// Invalid test expression.
139pub const XSLT_ERR_INVALID_TEST: c_int = 15;
140
141/// Invalid use expression.
142pub const XSLT_ERR_INVALID_USE: c_int = 16;
143
144/// Missing namespace.
145pub const XSLT_ERR_MISSING_NS: c_int = 17;
146
147/// Cyclic reference detected.
148pub const XSLT_ERR_CYCLIC_REFERENCE: c_int = 18;
149
150/// Recursion limit exceeded.
151pub const XSLT_ERR_RECURSION: c_int = 19;
152
153/// Internal XSLT error.
154pub const XSLT_ERR_INTERNAL: c_int = 20;
155
156// ── Error levels ──────────────────────────────────────────────────────────
157//
158// These constants indicate the severity of an XSLT error.
159// Source: xslt.h (libxslt 1.1.45).
160
161/// No error level (unset).
162pub const XSLT_ERR_LEVEL_NONE: c_int = 0;
163
164/// Warning — non-fatal issue.
165pub const XSLT_ERR_LEVEL_WARNING: c_int = 1;
166
167/// Error — processing may continue but results may be incomplete.
168pub const XSLT_ERR_LEVEL_ERROR: c_int = 2;
169
170/// Fatal error — processing cannot continue.
171pub const XSLT_ERR_LEVEL_FATAL: c_int = 3;
172
173// ── Error handler types ───────────────────────────────────────────────────
174
175/// Global XSLT error handler function type.
176///
177/// Matches the upstream `xsltTransformErrorFunc` typedef:
178/// ```c
179/// typedef void (*xsltTransformErrorFunc)(void *ctxt, void *ctx,
180///                                        xsltStylesheetPtr style,
181///                                        const xmlChar *msg, ...);
182/// ```
183pub type xsltTransformErrorFunc = Option<
184    unsafe extern "C" fn(
185        *mut std::ffi::c_void,
186        *mut std::ffi::c_void,
187        *mut _xsltStylesheet,
188        *const crate::abi::types::xmlChar,
189        ...
190    ),
191>;
192
193// ── Public API ────────────────────────────────────────────────────────────
194
195/// The last XSLT error message (thread-local).
196use std::sync::Mutex;
197
198static LAST_XSLT_ERROR: Mutex<Option<Vec<u8>>> = Mutex::new(None);
199
200/// Global debug handler: the exported `xsltGenericDebug` data global in
201/// `crate::abi::data_globals` (upstream `xsltGenericDebug`, a function
202/// pointer variable defaulting to `xsltGenericDebugDefaultFunc` — R-000174).
203///
204/// Set the generic debug handler (upstream `xsltSetGenericDebugFunc`).
205///
206/// # UPSTREAM-PARITY
207///
208/// ```c
209/// void xsltSetGenericDebugFunc(void *ctx, xmlGenericErrorFunc handler);
210/// ```
211///
212/// Upstream (xsltutils.c:650): `xsltGenericDebugContext = ctx;` and — only
213/// when `handler != NULL` — `xsltGenericDebug = handler;`. With a NULL
214/// context the default handler suppresses output; a NULL handler leaves the
215/// current handler installed.
216///
217/// # SAFETY
218///
219/// - `ctx` must be valid pointers (or NULL
220///   where the upstream C contract allows), obtained from the
221///   matching constructor/owner and not yet freed; the callee may
222///   take or keep ownership exactly as the C API specifies.
223///
224/// - `handler` must be a valid callback (or None);
225///   the callback is invoked with the documented context pointer and
226///   must itself uphold the same pointer invariants.
227///
228/// The caller must not race this call with concurrent mutation of the
229/// same objects from other threads (per-object state is not internally
230/// synchronized). Violating any of the above is undefined behavior.
231///
232/// Exercised by the C-API differential courts
233/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
234/// courts; those pass byte-for-byte against the upstream oracle.
235#[no_mangle]
236pub unsafe extern "C" fn xsltSetGenericDebugFunc(
237    ctx: *mut std::ffi::c_void,
238    handler: Option<unsafe extern "C" fn(*mut std::ffi::c_void, *const std::os::raw::c_char)>,
239) {
240    unsafe {
241        crate::abi::data_globals::xsltGenericDebugContext = ctx;
242        if handler.is_some() {
243            crate::abi::data_globals::xsltGenericDebug = handler;
244        }
245    }
246}
247
248/// Set the transform error handler for a context.
249///
250/// Registers a per-context error handler that will be called for every
251/// error reported during the transformation. Pass `None` to restore the
252/// default handler.
253///
254/// # Parameters
255///
256/// * `ctxt`   — The transform context, or `std::ptr::null_mut()` for the
257///   global handler.
258/// * `ctx`    — Opaque user-data pointer passed to the handler.
259/// * `handler` — The error handler function, or `None` to reset.
260pub fn xsltSetTransformErrorFunc(
261    ctxt: *mut _xsltTransformContext,
262    ctx: *mut std::ffi::c_void,
263    handler: Option<unsafe extern "C" fn(*mut std::ffi::c_void, *const std::os::raw::c_char)>,
264) {
265    if ctxt.is_null() {
266        return;
267    }
268    // SAFETY: ctxt must be a valid _xsltTransformContext.
269    unsafe {
270        (*ctxt).error = handler;
271        (*ctxt).errctx = ctx;
272    }
273}
274
275/// Report an XSLT error.
276///
277/// Faithful port of upstream xsltutils.c `xsltTransformError`: the
278/// transform context is moved to the error state, the error context line
279/// is printed (upstream `xsltPrintErrorContext`), and the message is
280/// emitted verbatim through the registered handler or stderr. Messages
281/// carry their own trailing newline, exactly as upstream's do — no
282/// newline is added here.
283///
284/// The upstream signature is variadic (`const char *msg, ...`); the
285/// candidate's callers format the message before calling (a `%s`/`%d`
286/// placeholder is never expanded by this function).
287///
288/// # Parameters
289///
290/// * `ctxt`  — The transform context (may be null).
291/// * `style` — The stylesheet (may be null).
292/// * `inst`  — The instruction node that triggered the error (may be null).
293/// * `msg`   — The message, NUL-terminated, typically ending in `\n`.
294///
295/// # Safety
296///
297/// - `msg` must be NULL or a valid NUL-terminated C string; it is read
298///   with `libc::strlen` and converted to a byte slice, so it must not
299///   be dangling.
300/// - `ctxt` must be NULL or a valid `_xsltTransformContext`; it is
301///   dereferenced to read `state`, `inst`, `error`, and `errctx`, and a
302///   registered `error` handler must be a valid extern "C" function
303///   pointer callable with `errctx` and the formatted message.
304/// - `inst` must be NULL or a valid `_xmlNode`; it is passed to
305///   `print_error_context`, which dereferences it.
306/// - `style` is only used in NULL comparisons and may be NULL.
307pub fn xsltTransformError(
308    ctxt: *mut _xsltTransformContext,
309    style: *mut _xsltStylesheet,
310    inst: *mut _xmlNode,
311    msg: *const std::os::raw::c_char,
312) {
313    if msg.is_null() {
314        return;
315    }
316    // SAFETY: msg must be a valid NUL-terminated C string.
317    let bytes =
318        unsafe { core::slice::from_raw_parts(msg as *const u8, libc::strlen(msg) as usize) };
319    let text = String::from_utf8_lossy(bytes).into_owned();
320
321    // Record the last error (the raw message, as upstream stores the
322    // formatted message).
323    if let Ok(mut last) = LAST_XSLT_ERROR.lock() {
324        *last = Some(text.clone().into_bytes());
325    }
326
327    // UPSTREAM-PARITY (xsltutils.c xsltTransformError): an error moves the
328    // transform context out of the OK state.
329    let mut node = inst;
330    let mut errtype: &'static str = "error";
331    if !ctxt.is_null() {
332        // SAFETY: ctxt must be a valid _xsltTransformContext.
333        let ctx = unsafe { &mut *ctxt };
334        if ctx.state == crate::xslt::transform::XSLT_STATE_OK {
335            ctx.state = crate::xslt::transform::XSLT_STATE_ERROR;
336        }
337        if node.is_null() {
338            node = ctx.inst;
339        }
340        errtype = "runtime error";
341    } else if !style.is_null() {
342        errtype = "compilation error";
343    }
344
345    let per_ctxt_handler = if ctxt.is_null() {
346        None
347    } else {
348        // SAFETY: ctxt must be a valid _xsltTransformContext.
349        unsafe { (*ctxt).error }
350    };
351
352    // UPSTREAM-PARITY (xsltutils.c xsltTransformError 1.1.45): with no
353    // per-context handler the message is emitted through the GLOBAL
354    // xsltGenericError channel (xsltSetGenericErrorFunc), falling back to
355    // plain stderr only when even that is unset. PHP's ext/xsl registers
356    // xsl_libxslt_error_handler there at MINIT; the three-DSO facade layout
357    // keeps this static shared with the xsl module, so the php handler
358    // receives transform errors (framed, suppressible) instead of raw
359    // stderr bytes.
360    let (eff_handler, eff_ctx) = match per_ctxt_handler {
361        Some(h) => {
362            let ctx = if ctxt.is_null() {
363                ptr::null_mut()
364            } else {
365                // SAFETY: ctxt must be a valid _xsltTransformContext.
366                unsafe { (*ctxt).errctx }
367            };
368            (Some(h), ctx)
369        }
370        None => (
371            unsafe { crate::abi::data_globals::xsltGenericError },
372            unsafe { crate::abi::data_globals::xsltGenericErrorContext },
373        ),
374    };
375    let handler = eff_handler;
376    let errctx = eff_ctx;
377
378    // UPSTREAM-PARITY (xsltutils.c xsltTransformError): the error context
379    // line (xsltPrintErrorContext) and the message are emitted as TWO
380    // separate calls with the printf FORMAT intact — consumers like lxml's
381    // _receiveXSLTError parse the format string to extract the file/line/
382    // element fields and build their log entries ("runtime error, element
383    // 'value-of'"), so a pre-formatted string would lose the fields.
384    emit_error_context_line(handler, errctx, style, node, errtype);
385
386    let mut cmsg = text.into_bytes();
387    let msg_len = cmsg.len();
388    cmsg.push(0);
389    if let Some(handler) = handler {
390        // SAFETY: handler is the caller-registered C callback (upstream
391        // xmlGenericErrorFunc is variadic; the Rust typedef is
392        // non-variadic, so the call is made through a variadic fn-pointer
393        // with the same ABI); the "%s" + NUL-terminated message matches
394        // upstream's error(errctx, "%s", str).
395        let hv: unsafe extern "C" fn(*mut c_void, *const c_char, ...) =
396            unsafe { core::mem::transmute(handler) };
397        unsafe {
398            hv(
399                errctx,
400                c"%s".as_ptr() as *const c_char,
401                cmsg.as_ptr() as *const c_char,
402            )
403        };
404    } else {
405        let _ = unsafe { libc::write(2, cmsg.as_ptr() as *const libc::c_void, msg_len) };
406    }
407}
408
409/// Emit the error-context line (upstream xsltPrintErrorContext) through the
410/// registered handler with the upstream printf format, or to stderr when no
411/// handler is installed. The five format variants mirror xsltutils.c
412/// exactly; the "runtime error"/"compilation error"/"error" type is the
413/// FIRST %s argument so format-parsing consumers attribute it correctly.
414///
415/// # SAFETY
416///
417/// - `handler` must be NULL or a valid variadic C callback callable with
418///   `errctx` and a printf format plus matching arguments.
419/// - `ctxt`, `style`, `node` follow the `xsltTransformError` contract.
420fn emit_error_context_line(
421    handler: Option<xmlGenericErrorFunc>,
422    errctx: *mut c_void,
423    style: *mut _xsltStylesheet,
424    node: *mut _xmlNode,
425    errtype: &'static str,
426) {
427    let mut line = 0i64;
428    let mut file: *const std::os::raw::c_char = ptr::null();
429    let mut name: *const std::os::raw::c_char = ptr::null();
430
431    if !node.is_null() {
432        // SAFETY: node must be valid.
433        let node_ref = unsafe { &*node };
434        if node_ref.type_ == crate::abi::types::xmlElementType::XML_DOCUMENT_NODE as c_int
435            || node_ref.type_ == crate::abi::types::xmlElementType::XML_HTML_DOCUMENT_NODE as c_int
436        {
437            let doc = node as *mut crate::abi::structs::_xmlDoc;
438            // SAFETY: doc->URL is a valid NUL-terminated string or NULL.
439            file = unsafe { (*doc).URL } as *const std::os::raw::c_char;
440        } else {
441            line = crate::abi::exports_xml2::xmlGetLineNo(node) as i64;
442            // SAFETY: node->doc must be valid while the node is alive.
443            let doc = { node_ref.doc };
444            if !doc.is_null() {
445                file = unsafe { (*doc).URL } as *const std::os::raw::c_char;
446            }
447            name = node_ref.name as *const std::os::raw::c_char;
448        }
449    }
450
451    let type_cstr = std::ffi::CString::new(errtype).unwrap_or_default();
452    let type_c = type_cstr.as_ptr() as *const std::os::raw::c_char;
453    let has_file = !file.is_null();
454    let has_name = !name.is_null();
455    if let Some(handler) = handler {
456        // SAFETY: the format/argument pairs match upstream
457        // xsltPrintErrorContext, and the C callback is variadic (upstream
458        // xmlGenericErrorFunc); the non-variadic Rust typedef is called
459        // through a variadic fn-pointer with the same ABI.
460        let hv: unsafe extern "C" fn(*mut c_void, *const c_char, ...) =
461            unsafe { core::mem::transmute(handler) };
462        unsafe {
463            if has_file && line != 0 && has_name {
464                hv(
465                    errctx,
466                    c"%s: file %s line %d element %s\n".as_ptr() as *const c_char,
467                    type_c,
468                    file,
469                    line as c_int,
470                    name,
471                );
472            } else if has_file && has_name {
473                hv(
474                    errctx,
475                    c"%s: file %s element %s\n".as_ptr() as *const c_char,
476                    type_c,
477                    file,
478                    name,
479                );
480            } else if has_file && line != 0 {
481                hv(
482                    errctx,
483                    c"%s: file %s line %d\n".as_ptr() as *const c_char,
484                    type_c,
485                    file,
486                    line as c_int,
487                );
488            } else if has_file {
489                hv(
490                    errctx,
491                    c"%s: file %s\n".as_ptr() as *const c_char,
492                    type_c,
493                    file,
494                );
495            } else if has_name {
496                hv(
497                    errctx,
498                    c"%s: element %s\n".as_ptr() as *const c_char,
499                    type_c,
500                    name,
501                );
502            } else {
503                hv(errctx, c"%s\n".as_ptr() as *const c_char, type_c);
504            }
505        }
506    } else {
507        let file_s = if file.is_null() {
508            String::new()
509        } else {
510            unsafe {
511                std::ffi::CStr::from_ptr(file)
512                    .to_string_lossy()
513                    .into_owned()
514            }
515        };
516        let name_s = if name.is_null() {
517            String::new()
518        } else {
519            unsafe {
520                std::ffi::CStr::from_ptr(name)
521                    .to_string_lossy()
522                    .into_owned()
523            }
524        };
525        let line_str = if has_file && line != 0 && has_name {
526            format!(
527                "{}: file {} line {} element {}\n",
528                errtype, file_s, line, name_s
529            )
530        } else if has_file && has_name {
531            format!("{}: file {} element {}\n", errtype, file_s, name_s)
532        } else if has_file && line != 0 {
533            format!("{}: file {} line {}\n", errtype, file_s, line)
534        } else if has_file {
535            format!("{}: file {}\n", errtype, file_s)
536        } else if has_name {
537            format!("{}: element {}\n", errtype, name_s)
538        } else {
539            format!("{}\n", errtype)
540        };
541        let _ = unsafe { libc::write(2, line_str.as_ptr() as *const libc::c_void, line_str.len()) };
542    }
543    let _ = style;
544}
545
546/// Get the last XSLT error message as a NUL-terminated heap string.
547///
548/// Returns a pointer to the last error message, or `std::ptr::null_mut()`
549/// if no error has occurred. The caller frees with `libc::free`.
550pub fn xsltGetLastError() -> *mut std::ffi::c_void {
551    let guard = match LAST_XSLT_ERROR.lock() {
552        Ok(g) => g,
553        Err(_) => return std::ptr::null_mut(),
554    };
555    match guard.as_ref() {
556        Some(bytes) => {
557            let len = bytes.len();
558            // SAFETY: malloc returns writable memory or NULL.
559            let p = unsafe { libc::malloc(len + 1) } as *mut u8;
560            if p.is_null() {
561                return std::ptr::null_mut();
562            }
563            unsafe {
564                core::ptr::copy_nonoverlapping(bytes.as_ptr(), p, len);
565                *p.add(len) = 0;
566            }
567            p as *mut std::ffi::c_void
568        }
569        None => std::ptr::null_mut(),
570    }
571}