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::structs::*;
82use std::os::raw::c_int;
83use std::ptr;
84
85// ── Error domains ─────────────────────────────────────────────────────────
86//
87// These constants identify the category of an XSLT error.
88// Source: xslt.h / xsltInternals.h (libxslt 1.1.45).
89
90/// No error.
91pub const XSLT_ERR_NONE: c_int = 0;
92
93/// Unknown error.
94pub const XSLT_ERR_UNKNOWN: c_int = 1;
95
96/// Missing required namespace.
97pub const XSLT_ERR_MISSING_NAMESPACE: c_int = 2;
98
99/// Invalid namespace.
100pub const XSLT_ERR_INVALID_NAMESPACE: c_int = 3;
101
102/// Missing required attribute.
103pub const XSLT_ERR_MISSING_ATTRIBUTE: c_int = 4;
104
105/// Invalid attribute value.
106pub const XSLT_ERR_INVALID_ATTRIBUTE: c_int = 5;
107
108/// Missing required element.
109pub const XSLT_ERR_MISSING_ELEMENT: c_int = 6;
110
111/// Invalid element.
112pub const XSLT_ERR_INVALID_ELEMENT: c_int = 7;
113
114/// Missing match attribute.
115pub const XSLT_ERR_MISSING_MATCH: c_int = 8;
116
117/// Missing name attribute.
118pub const XSLT_ERR_MISSING_NAME: c_int = 9;
119
120/// Missing select attribute.
121pub const XSLT_ERR_MISSING_SELECT: c_int = 10;
122
123/// Missing test attribute.
124pub const XSLT_ERR_MISSING_TEST: c_int = 11;
125
126/// Missing use attribute.
127pub const XSLT_ERR_MISSING_USE: c_int = 12;
128
129/// Invalid match pattern.
130pub const XSLT_ERR_INVALID_MATCH: c_int = 13;
131
132/// Invalid select expression.
133pub const XSLT_ERR_INVALID_SELECT: c_int = 14;
134
135/// Invalid test expression.
136pub const XSLT_ERR_INVALID_TEST: c_int = 15;
137
138/// Invalid use expression.
139pub const XSLT_ERR_INVALID_USE: c_int = 16;
140
141/// Missing namespace.
142pub const XSLT_ERR_MISSING_NS: c_int = 17;
143
144/// Cyclic reference detected.
145pub const XSLT_ERR_CYCLIC_REFERENCE: c_int = 18;
146
147/// Recursion limit exceeded.
148pub const XSLT_ERR_RECURSION: c_int = 19;
149
150/// Internal XSLT error.
151pub const XSLT_ERR_INTERNAL: c_int = 20;
152
153// ── Error levels ──────────────────────────────────────────────────────────
154//
155// These constants indicate the severity of an XSLT error.
156// Source: xslt.h (libxslt 1.1.45).
157
158/// No error level (unset).
159pub const XSLT_ERR_LEVEL_NONE: c_int = 0;
160
161/// Warning — non-fatal issue.
162pub const XSLT_ERR_LEVEL_WARNING: c_int = 1;
163
164/// Error — processing may continue but results may be incomplete.
165pub const XSLT_ERR_LEVEL_ERROR: c_int = 2;
166
167/// Fatal error — processing cannot continue.
168pub const XSLT_ERR_LEVEL_FATAL: c_int = 3;
169
170// ── Error handler types ───────────────────────────────────────────────────
171
172/// Global XSLT error handler function type.
173///
174/// Matches the upstream `xsltTransformErrorFunc` typedef:
175/// ```c
176/// typedef void (*xsltTransformErrorFunc)(void *ctxt, void *ctx,
177/// xsltStylesheetPtr style,
178/// const xmlChar *msg, ...);
179/// ```
180pub type xsltTransformErrorFunc = Option<
181 unsafe extern "C" fn(
182 *mut std::ffi::c_void,
183 *mut std::ffi::c_void,
184 *mut _xsltStylesheet,
185 *const crate::abi::types::xmlChar,
186 ...
187 ),
188>;
189
190// ── Public API ────────────────────────────────────────────────────────────
191
192/// The last XSLT error message (thread-local).
193use std::sync::Mutex;
194
195static LAST_XSLT_ERROR: Mutex<Option<Vec<u8>>> = Mutex::new(None);
196
197/// Global debug handler: the exported `xsltGenericDebug` data global in
198/// `crate::abi::data_globals` (upstream `xsltGenericDebug`, a function
199/// pointer variable defaulting to `xsltGenericDebugDefaultFunc` — R-000174).
200///
201/// Set the generic debug handler (upstream `xsltSetGenericDebugFunc`).
202///
203/// # UPSTREAM-PARITY
204///
205/// ```c
206/// void xsltSetGenericDebugFunc(void *ctx, xmlGenericErrorFunc handler);
207/// ```
208///
209/// Upstream (xsltutils.c:650): `xsltGenericDebugContext = ctx;` and — only
210/// when `handler != NULL` — `xsltGenericDebug = handler;`. With a NULL
211/// context the default handler suppresses output; a NULL handler leaves the
212/// current handler installed.
213///
214/// # SAFETY
215///
216/// - `ctx` must be valid pointers (or NULL
217/// where the upstream C contract allows), obtained from the
218/// matching constructor/owner and not yet freed; the callee may
219/// take or keep ownership exactly as the C API specifies.
220///
221/// - `handler` must be a valid callback (or None);
222/// the callback is invoked with the documented context pointer and
223/// must itself uphold the same pointer invariants.
224///
225/// The caller must not race this call with concurrent mutation of the
226/// same objects from other threads (per-object state is not internally
227/// synchronized). Violating any of the above is undefined behavior.
228///
229/// Exercised by the C-API differential courts
230/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
231/// courts; those pass byte-for-byte against the upstream oracle.
232#[no_mangle]
233pub unsafe extern "C" fn xsltSetGenericDebugFunc(
234 ctx: *mut std::ffi::c_void,
235 handler: Option<unsafe extern "C" fn(*mut std::ffi::c_void, *const std::os::raw::c_char)>,
236) {
237 unsafe {
238 crate::abi::data_globals::xsltGenericDebugContext = ctx;
239 if handler.is_some() {
240 crate::abi::data_globals::xsltGenericDebug = handler;
241 }
242 }
243}
244
245/// Set the transform error handler for a context.
246///
247/// Registers a per-context error handler that will be called for every
248/// error reported during the transformation. Pass `None` to restore the
249/// default handler.
250///
251/// # Parameters
252///
253/// * `ctxt` — The transform context, or `std::ptr::null_mut()` for the
254/// global handler.
255/// * `ctx` — Opaque user-data pointer passed to the handler.
256/// * `handler` — The error handler function, or `None` to reset.
257pub fn xsltSetTransformErrorFunc(
258 ctxt: *mut _xsltTransformContext,
259 ctx: *mut std::ffi::c_void,
260 handler: Option<unsafe extern "C" fn(*mut std::ffi::c_void, *const std::os::raw::c_char)>,
261) {
262 if ctxt.is_null() {
263 return;
264 }
265 // SAFETY: ctxt must be a valid _xsltTransformContext.
266 unsafe {
267 (*ctxt).error = handler;
268 (*ctxt).errctx = ctx;
269 }
270}
271
272/// Report an XSLT error.
273///
274/// Faithful port of upstream xsltutils.c `xsltTransformError`: the
275/// transform context is moved to the error state, the error context line
276/// is printed (upstream `xsltPrintErrorContext`), and the message is
277/// emitted verbatim through the registered handler or stderr. Messages
278/// carry their own trailing newline, exactly as upstream's do — no
279/// newline is added here.
280///
281/// The upstream signature is variadic (`const char *msg, ...`); the
282/// candidate's callers format the message before calling (a `%s`/`%d`
283/// placeholder is never expanded by this function).
284///
285/// # Parameters
286///
287/// * `ctxt` — The transform context (may be null).
288/// * `style` — The stylesheet (may be null).
289/// * `inst` — The instruction node that triggered the error (may be null).
290/// * `msg` — The message, NUL-terminated, typically ending in `\n`.
291pub fn xsltTransformError(
292 ctxt: *mut _xsltTransformContext,
293 style: *mut _xsltStylesheet,
294 inst: *mut _xmlNode,
295 msg: *const std::os::raw::c_char,
296) {
297 if msg.is_null() {
298 return;
299 }
300 // SAFETY: msg must be a valid NUL-terminated C string.
301 let bytes =
302 unsafe { core::slice::from_raw_parts(msg as *const u8, libc::strlen(msg) as usize) };
303 let text = String::from_utf8_lossy(bytes).into_owned();
304
305 // Record the last error (the raw message, as upstream stores the
306 // formatted message).
307 if let Ok(mut last) = LAST_XSLT_ERROR.lock() {
308 *last = Some(text.clone().into_bytes());
309 }
310
311 // UPSTREAM-PARITY (xsltutils.c xsltTransformError): an error moves the
312 // transform context out of the OK state.
313 if !ctxt.is_null() {
314 // SAFETY: ctxt must be a valid _xsltTransformContext.
315 let ctx = unsafe { &mut *ctxt };
316 if ctx.state == crate::xslt::transform::XSLT_STATE_OK {
317 ctx.state = crate::xslt::transform::XSLT_STATE_ERROR;
318 }
319 let mut node = inst;
320 if node.is_null() {
321 node = ctx.inst;
322 }
323 // Build the context line (xsltPrintErrorContext) and the full
324 // message, then emit through the handler if one is registered.
325 let context_line = print_error_context(ctxt, style, node);
326 let full = format!("{}{}", context_line, text);
327 let mut cmsg = full.into_bytes();
328 let msg_len = cmsg.len();
329 cmsg.push(0);
330 let ctx = unsafe { &*ctxt };
331 if let Some(handler) = ctx.error {
332 unsafe { handler(ctx.errctx, cmsg.as_ptr() as *const std::os::raw::c_char) };
333 return;
334 }
335 let _ = unsafe { libc::write(2, cmsg.as_ptr() as *const libc::c_void, msg_len) };
336 return;
337 }
338
339 // No transform context: compile-time errors and standalone messages.
340 // (Upstream xsltPrintErrorContext is still invoked with NULL ctxt and
341 // the given style/node.)
342 let context_line = print_error_context(ptr::null_mut(), style, inst);
343 let full = format!("{}{}", context_line, text);
344 let mut cmsg = full.into_bytes();
345 let msg_len = cmsg.len();
346 cmsg.push(0);
347 let _ = unsafe { libc::write(2, cmsg.as_ptr() as *const libc::c_void, msg_len) };
348 let _ = style;
349}
350
351/// Build the error context line printed before an XSLT error message
352/// (upstream xsltutils.c `xsltPrintErrorContext`). The line is one of:
353///
354/// ```text
355/// error\n
356/// error: file F\n
357/// error: file F line N\n
358/// error: file F element E\n
359/// error: file F line N element E\n
360/// error: element E\n
361/// compilation error ... / runtime error ...
362/// ```
363fn print_error_context(
364 ctxt: *mut _xsltTransformContext,
365 style: *mut _xsltStylesheet,
366 node: *mut _xmlNode,
367) -> String {
368 let mut line = 0i64;
369 let mut file: *const std::os::raw::c_char = ptr::null();
370 let mut name: *const std::os::raw::c_char = ptr::null();
371
372 if !node.is_null() {
373 // SAFETY: node must be valid.
374 let node_ref = unsafe { &*node };
375 if node_ref.type_ == crate::abi::types::xmlElementType::XML_DOCUMENT_NODE as c_int
376 || node_ref.type_ == crate::abi::types::xmlElementType::XML_HTML_DOCUMENT_NODE as c_int
377 {
378 let doc = node as *mut crate::abi::structs::_xmlDoc;
379 // SAFETY: doc->URL is a valid NUL-terminated string or NULL.
380 file = unsafe { (*doc).URL } as *const std::os::raw::c_char;
381 } else {
382 line = crate::abi::exports_xml2::xmlGetLineNo(node) as i64;
383 // SAFETY: node->doc must be valid while the node is alive.
384 let doc = { node_ref.doc };
385 if !doc.is_null() {
386 file = unsafe { (*doc).URL } as *const std::os::raw::c_char;
387 }
388 name = node_ref.name as *const std::os::raw::c_char;
389 }
390 }
391
392 let errtype = if !ctxt.is_null() {
393 "runtime error"
394 } else if !style.is_null() {
395 "compilation error"
396 } else {
397 "error"
398 };
399
400 let s = |p: *const std::os::raw::c_char| -> String {
401 if p.is_null() {
402 String::new()
403 } else {
404 unsafe { std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned() }
405 }
406 };
407 let file_s = s(file);
408 let name_s = s(name);
409 let has_file = !file.is_null();
410 let has_name = !name.is_null();
411
412 if has_file && line != 0 && has_name {
413 format!(
414 "{}: file {} line {} element {}\n",
415 errtype, file_s, line, name_s
416 )
417 } else if has_file && has_name {
418 format!("{}: file {} element {}\n", errtype, file_s, name_s)
419 } else if has_file && line != 0 {
420 format!("{}: file {} line {}\n", errtype, file_s, line)
421 } else if has_file {
422 format!("{}: file {}\n", errtype, file_s)
423 } else if has_name {
424 format!("{}: element {}\n", errtype, name_s)
425 } else {
426 format!("{}\n", errtype)
427 }
428}
429
430/// Get the last XSLT error message as a NUL-terminated heap string.
431///
432/// Returns a pointer to the last error message, or `std::ptr::null_mut()`
433/// if no error has occurred. The caller frees with `libc::free`.
434pub fn xsltGetLastError() -> *mut std::ffi::c_void {
435 let guard = match LAST_XSLT_ERROR.lock() {
436 Ok(g) => g,
437 Err(_) => return std::ptr::null_mut(),
438 };
439 match guard.as_ref() {
440 Some(bytes) => {
441 let len = bytes.len();
442 // SAFETY: malloc returns writable memory or NULL.
443 let p = unsafe { libc::malloc(len + 1) } as *mut u8;
444 if p.is_null() {
445 return std::ptr::null_mut();
446 }
447 unsafe {
448 core::ptr::copy_nonoverlapping(bytes.as_ptr(), p, len);
449 *p.add(len) = 0;
450 }
451 p as *mut std::ffi::c_void
452 }
453 None => std::ptr::null_mut(),
454 }
455}