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}