Skip to main content

libxml_rs/exslt/
mod.rs

1//! EXSLT implementation — native Rust (§35).
2//!
3//! EXSLT is a community-driven set of extensions to XSLT 1.0. libxslt ships
4//! implementations of the following modules:
5//!
6//! - `exsl:` — Common (exsl:node-set, exsl:object-type, exsl:document)
7//! - `math:` — Math (math:max, math:min, math:sin, math:cos, ...)
8//! - `set:` — Sets (set:difference, set:distinct, set:intersection, ...)
9//! - `str:` — Strings (str:concat, str:padding, str:split, ...)
10//! - `dyn:` — Dynamic (dyn:element, dyn:attribute, dyn:evaluate, ...)
11//! - `func:` — Functions (func:function, func:result, func:script)
12//! - `date:` — Dates and Times (date:date, date:format-date, ...)
13//!
14//! # Registration model
15//!
16//! Upstream libxslt requires an explicit `exsltRegisterAll()` call (usually
17//! from the host application; `xsltproc` calls it at startup) before EXSLT
18//! functions are available. This module mirrors that model: a process-wide
19//! registry of EXSLT functions keyed by their full QName (e.g. `"math:max"`).
20//! `exsltRegisterAll()` populates the registry; each new transform context
21//! copies the registered functions into its XPath context (§31 integration).
22//!
23//! # EXSLT namespaces
24//!
25//! | Prefix | URI |
26//! |--------|-----|
27//! | exsl | `http://exslt.org/common` |
28//! | math | `http://exslt.org/math` |
29//! | set | `http://exslt.org/sets` |
30//! | str | `http://exslt.org/strings` |
31//! | dyn | `http://exslt.org/dynamic` |
32//! | func | `http://exslt.org/functions` |
33//! | date | `http://exslt.org/dates-and-times` |
34//!
35//! # Phase 9 status
36//!
37//! Complete: all seven modules implemented and registered.
38//!
39//! # Upstream contract
40//!
41//! Parity target: upstream libexslt (the EXSLT support library shipped
42//! inside the libxslt distribution) 1.1.45, with per-module sources
43//! `SRC-LIBXSLT-1.1.42-libexslt/<module>.c` (exslt.c, common.c, date.c,
44//! dynamic.c, functions.c, math.c, saxon.c, sets.c, strings.c) under
45//! `oracle/historical/src/libxslt-1.1.42/libexslt/`. EXSLT 1.0 semantics per
46//! the EXSLT-COMMON/MATH/SETS/STRINGS/DYNAMIC/DATES standards registry
47//! (atlas/SOURCES.md). The C ABI surface is the per-module registration
48//! functions (`exsltCommonRegister`, `exsltMathRegister`, ...), `exsltRegisterAll`,
49//! and the version data symbols — closed in R-000165/R-000167 and verified
50//! by DSO-LOADER.
51//!
52//! # Conceptual behavior
53//!
54//! Upstream requires an explicit `exsltRegisterAll()` call (usually from
55//! the host application; `xsltproc` calls it at startup) before EXSLT
56//! functions are available. The candidate mirrors that model with a
57//! process-wide registry keyed by full QName; each new transform context
58//! copies the registered functions into its XPath context (§31
59//! integration).
60//!
61//! # Ownership & safety invariants
62//!
63//! The registry owns its entries. Registered closures are `Box::leak`-ed
64//! (see the `register` function below): they live for the process lifetime
65//! because the registry is never cleared — a deliberate, bounded leak. The
66//! leaked references are `Send + Sync`; lookups clone the reference out of
67//! the mutex-guarded map, so no entry is ever invalidated while in use.
68//!
69//! # Historical quirks & epochs
70//!
71//! EXSLT is part of the libxslt 1.1 series (since 1.1.0, 2004-12-15;
72//! atlas/HISTORY.md section 2). E-008 (atlas/SEMANTIC_EPOCHS.md) shows the
73//! XSLT/xsltproc output epoch frozen since 2009, which includes the EXSLT
74//! function results exercised by the xsltproc corpus. R-000112 fixed the
75//! dates module no-argument defaults; R-000165 added the missing per-module
76//! registration exports. R-000168 (platform surface) remains OPEN and
77//! touches this module and src/exslt/dates.
78//!
79//! # Deliberate oddities
80//!
81//! - `exsltCryptoRegister` is a deliberate no-op export: the candidate has
82//!   no crypto module and upstream returns void.
83//! - The `*XpathCtxtRegister` variants register globally (the registry is
84//!   process-wide) and return 0, an intentional divergence from upstream
85//!   per-context registration.
86//! - saxon has no candidate module of its own beyond the registry entries;
87//!   `exsltSaxonRegister` still exists (upstream exslt.c calls it from
88//!   `exsltRegisterAll`).
89//!
90//! # Proving courts
91//!
92//! EXSLT, CLI-XSLTPROC (exslt-using corpus stylesheets), DSO-LOADER
93//! (per-module registration exports, R-000165), and the in-crate `cargo
94//! test` suites (e.g. test_register_all_populates).
95//!
96//! # Tempting simplifications that would break parity
97//!
98//! - Making the registry per-transform instead of process-wide would break
99//!   the upstream registration model (`exsltRegisterAll` before
100//!   `xsltApplyStylesheet`) and the ABI registration functions.
101//! - Replacing the leak with scoped lifetimes would either require
102//!   invalidating live lookups or copying every function per call; the
103//!   leak is the parity-preserving choice.
104//! - Dropping the marker registrations (`func:function` et al.) would
105//!   break `function-available` and `element-available` reporting.
106
107use once_cell::sync::Lazy;
108use parking_lot::Mutex;
109use std::collections::HashMap;
110use std::ffi::c_void;
111use std::os::raw::c_int;
112
113use crate::abi::types::xmlChar;
114use crate::xml::xpath::context::XPathContext;
115use crate::xml::xpath::types::XPathValue;
116
117pub mod common;
118pub mod dates;
119pub mod dynamic;
120pub mod functions;
121pub mod math;
122pub mod saxon;
123pub mod sets;
124pub mod strings;
125
126/// EXSLT namespace URIs.
127pub const EXSLT_NS_COMMON: &str = "http://exslt.org/common";
128/// Math module namespace (`math:max`, `math:min`, `math:sin`, ...).
129pub const EXSLT_NS_MATH: &str = "http://exslt.org/math";
130/// Sets module namespace (`set:difference`, `set:distinct`, ...).
131pub const EXSLT_NS_SETS: &str = "http://exslt.org/sets";
132/// Strings module namespace (`str:concat`, `str:padding`, ...).
133pub const EXSLT_NS_STRINGS: &str = "http://exslt.org/strings";
134/// Dynamic module namespace (`dyn:element`, `dyn:evaluate`, ...).
135pub const EXSLT_NS_DYNAMIC: &str = "http://exslt.org/dynamic";
136/// Functions module namespace (`func:function`, `func:result`, ...).
137pub const EXSLT_NS_FUNCTIONS: &str = "http://exslt.org/functions";
138/// Dates-and-times module namespace (`date:date`, `date:format-date`, ...).
139pub const EXSLT_NS_DATES: &str = "http://exslt.org/dates-and-times";
140
141/// The XPath function signature used throughout the EXSLT modules.
142pub type ExsltFunction = fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String>;
143
144/// A capture-capable EXSLT function (used by `func:function` bodies).
145///
146/// Stored as a leaked `&'static` reference so entries are `Copy` and the
147/// registry can hand out clones. The process-wide registry lives for the
148/// lifetime of the program, so leaking is intentional and bounded.
149pub type ExsltClosure =
150    &'static (dyn Fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String> + Send + Sync);
151
152/// Process-wide EXSLT function registry, keyed by full QName
153/// (e.g. `"math:max"`). Populated by `exsltRegisterAll()`.
154static REGISTRY: Lazy<Mutex<HashMap<String, ExsltClosure>>> =
155    Lazy::new(|| Mutex::new(HashMap::new()));
156
157/// Register a single EXSLT function under its full QName.
158pub fn register<F>(name: &str, f: F)
159where
160    F: Fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String> + Send + Sync + 'static,
161{
162    // SAFETY: the boxed closure is leaked; it lives for the process lifetime
163    // (the registry is never cleared) so the resulting 'static reference is
164    // sound.
165    let leaked: &'static (dyn Fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String>
166                  + Send
167                  + Sync) = Box::leak(Box::new(f));
168    REGISTRY.lock().insert(name.to_string(), leaked);
169}
170
171/// Look up a registered EXSLT function by full QName.
172pub fn lookup(name: &str) -> Option<ExsltClosure> {
173    REGISTRY.lock().get(name).copied()
174}
175
176/// Iterate over all registered EXSLT functions.
177pub fn iter_functions() -> Vec<(String, ExsltClosure)> {
178    REGISTRY
179        .lock()
180        .iter()
181        .map(|(k, v)| (k.clone(), *v))
182        .collect()
183}
184
185/// Whether any EXSLT functions have been registered.
186pub fn is_registered() -> bool {
187    !REGISTRY.lock().is_empty()
188}
189
190/// Register every EXSLT module (mirrors upstream `exsltRegisterAll`).
191pub fn register_all() {
192    common::register_all();
193    math::register_all();
194    sets::register_all();
195    strings::register_all();
196    dynamic::register_all();
197    dates::register_all();
198    functions::register_all();
199    saxon::register_all();
200}
201
202/// Cross-DSO EXSLT registration gateway (R-000177).
203///
204/// `exsltRegisterAll` runs in the `libexslt.so.0` facade, but the transform
205/// (and the EXSLT registry it copies into each XPath context at
206/// `xsltApplyStylesheet`) lives in `libxslt.so.1` — the whole-archive
207/// facades partition statics, so a registration performed inside libexslt is
208/// invisible to libxslt's transform. Upstream solves this by having libexslt
209/// register INTO libxslt's extension-module registry (`xsltRegisterExtModule`,
210/// extensions.c, exported by libxslt); the candidate mirrors that dependency
211/// direction with this exported libxslt entry point, which performs the
212/// registration inside the transform layer. lxml calls `exsltRegisterAll()`
213/// (xslt.pxi) after loading all three candidate DSOs.
214#[no_mangle]
215pub extern "C" fn xsltRegisterAllExslt() {
216    register_all();
217}
218
219/// The C ABI entry point: register all EXSLT modules.
220///
221/// # UPSTREAM-PARITY
222///
223/// ```c
224/// void exsltRegisterAll(void);
225/// ```
226///
227/// Oracle behavior: registers every EXSLT function so it becomes available
228/// to subsequently created transform contexts. Calling it twice is a no-op
229/// (re-registration overwrites identical entries).
230///
231/// The registration is performed inside the transform layer
232/// (`xsltRegisterAllExslt`, exported by `libxslt.so.1`): the whole-archive
233/// facades partition statics, so a plain internal call would bind to the
234/// libexslt facade's own private registry copy which the libxslt transform
235/// never reads (R-000177). The gateway is located with `dlsym` on the
236/// already-loaded libxslt (RTLD_NOLOAD — never loads anything) and invoked;
237/// in a single-binary (staticlib) build no `libxslt.so.1` is loaded and the
238/// local `register_all()` is used instead. Mirrors upstream's dependency
239/// direction: libexslt registers INTO libxslt's extension-module registry
240/// (`xsltRegisterExtModule`, extensions.c).
241#[no_mangle]
242pub extern "C" fn exsltRegisterAll() {
243    // SAFETY: the gateway is a plain extern "C" fn with no arguments; the
244    // symbol is our own exported entry point.
245    unsafe {
246        let handle = libc::dlopen(
247            c"libxslt.so.1".as_ptr(),
248            libc::RTLD_NOLOAD | libc::RTLD_LAZY,
249        );
250        if !handle.is_null() {
251            let sym = libc::dlsym(handle, c"xsltRegisterAllExslt".as_ptr());
252            if !sym.is_null() {
253                let gateway: extern "C" fn() = core::mem::transmute(sym);
254                gateway();
255                return;
256            }
257        }
258    }
259    register_all();
260}
261
262// ═══════════════════════════════════════════════════════════════════════════════
263// Per-module registration entry points (11.1-X R-000165 closure)
264// ═══════════════════════════════════════════════════════════════════════════════
265//
266// Upstream libexslt exposes one register function per module (exslt.c). The
267// candidate's registry is keyed by "prefix:name"; each module's register_all
268// populates it. saxon/crypto have no candidate module — their register
269// functions are exported no-ops (upstream contracts: void / int 0).
270
271/// `exsltCommonRegister` — register the EXSLT common module.
272#[no_mangle]
273pub extern "C" fn exsltCommonRegister() {
274    common::register_all();
275}
276
277/// `exsltMathRegister` — register the EXSLT math module.
278#[no_mangle]
279pub extern "C" fn exsltMathRegister() {
280    math::register_all();
281}
282
283/// `exsltSetsRegister` — register the EXSLT sets module.
284#[no_mangle]
285pub extern "C" fn exsltSetsRegister() {
286    sets::register_all();
287}
288
289/// `exsltFuncRegister` — register the EXSLT functions module.
290#[no_mangle]
291pub extern "C" fn exsltFuncRegister() {
292    functions::register_all();
293}
294
295/// `exsltStrRegister` — register the EXSLT strings module.
296#[no_mangle]
297pub extern "C" fn exsltStrRegister() {
298    strings::register_all();
299}
300
301/// `exsltDateRegister` — register the EXSLT dates module.
302#[no_mangle]
303pub extern "C" fn exsltDateRegister() {
304    dates::register_all();
305}
306
307/// `exsltSaxonRegister` — register the EXSLT Saxon extensions
308/// (upstream exslt.c calls this from `exsltRegisterAll`).
309#[no_mangle]
310pub extern "C" fn exsltSaxonRegister() {
311    saxon::register_all();
312}
313
314/// `exsltDynRegister` — register the EXSLT dynamic module.
315#[no_mangle]
316pub extern "C" fn exsltDynRegister() {
317    dynamic::register_all();
318}
319
320/// `exsltCryptoRegister` — register the EXSLT crypto module. The candidate
321/// has no crypto module; upstream returns void, so this is a no-op.
322#[no_mangle]
323pub const extern "C" fn exsltCryptoRegister() {}
324
325/// `exsltDateXpathCtxtRegister(ctxt, prefix)` — register the dates module
326/// on a specific XPath context (upstream date.c). The candidate's registry
327/// is global; registration is performed for all contexts.
328#[no_mangle]
329pub extern "C" fn exsltDateXpathCtxtRegister(_ctxt: *mut c_void, _prefix: *const xmlChar) -> c_int {
330    dates::register_all();
331    0
332}
333
334/// `exsltMathXpathCtxtRegister(ctxt, prefix)` — math module (see above).
335#[no_mangle]
336pub extern "C" fn exsltMathXpathCtxtRegister(_ctxt: *mut c_void, _prefix: *const xmlChar) -> c_int {
337    math::register_all();
338    0
339}
340
341/// `exsltSetsXpathCtxtRegister(ctxt, prefix)` — sets module (see above).
342#[no_mangle]
343pub extern "C" fn exsltSetsXpathCtxtRegister(_ctxt: *mut c_void, _prefix: *const xmlChar) -> c_int {
344    sets::register_all();
345    0
346}
347
348/// `exsltStrXpathCtxtRegister(ctxt, prefix)` — strings module (see above).
349#[no_mangle]
350pub extern "C" fn exsltStrXpathCtxtRegister(_ctxt: *mut c_void, _prefix: *const xmlChar) -> c_int {
351    strings::register_all();
352    0
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn test_register_and_lookup() {
361        fn my_func(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
362            Ok(XPathValue::String("hello".to_string()))
363        }
364        register("test:myfunc", my_func);
365        let f = lookup("test:myfunc");
366        assert!(f.is_some());
367        let (names, _) = iter_functions()
368            .into_iter()
369            .find(|(n, _)| n == "test:myfunc")
370            .unwrap();
371        assert_eq!(names, "test:myfunc");
372    }
373
374    #[test]
375    fn test_lookup_missing() {
376        assert!(lookup("nonexistent:fn").is_none());
377    }
378
379    #[test]
380    fn test_register_all_populates() {
381        // Register everything; all module functions must be present.
382        register_all();
383        for name in [
384            "exsl:node-set",
385            "exsl:object-type",
386            "math:max",
387            "math:sin",
388            "math:constant",
389            "set:difference",
390            "set:distinct",
391            "str:tokenize",
392            "str:padding",
393            "dyn:evaluate",
394            "date:date",
395            "date:date-time",
396        ] {
397            assert!(lookup(name).is_some(), "missing EXSLT function {}", name);
398        }
399    }
400}