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/// The C ABI entry point: register all EXSLT modules.
203///
204/// # UPSTREAM-PARITY
205///
206/// ```c
207/// void exsltRegisterAll(void);
208/// ```
209///
210/// Oracle behavior: registers every EXSLT function so it becomes available
211/// to subsequently created transform contexts. Calling it twice is a no-op
212/// (re-registration overwrites identical entries).
213#[no_mangle]
214pub extern "C" fn exsltRegisterAll() {
215    register_all();
216}
217
218// ═══════════════════════════════════════════════════════════════════════════════
219// Per-module registration entry points (11.1-X R-000165 closure)
220// ═══════════════════════════════════════════════════════════════════════════════
221//
222// Upstream libexslt exposes one register function per module (exslt.c). The
223// candidate's registry is keyed by "prefix:name"; each module's register_all
224// populates it. saxon/crypto have no candidate module — their register
225// functions are exported no-ops (upstream contracts: void / int 0).
226
227/// `exsltCommonRegister` — register the EXSLT common module.
228#[no_mangle]
229pub extern "C" fn exsltCommonRegister() {
230    common::register_all();
231}
232
233/// `exsltMathRegister` — register the EXSLT math module.
234#[no_mangle]
235pub extern "C" fn exsltMathRegister() {
236    math::register_all();
237}
238
239/// `exsltSetsRegister` — register the EXSLT sets module.
240#[no_mangle]
241pub extern "C" fn exsltSetsRegister() {
242    sets::register_all();
243}
244
245/// `exsltFuncRegister` — register the EXSLT functions module.
246#[no_mangle]
247pub extern "C" fn exsltFuncRegister() {
248    functions::register_all();
249}
250
251/// `exsltStrRegister` — register the EXSLT strings module.
252#[no_mangle]
253pub extern "C" fn exsltStrRegister() {
254    strings::register_all();
255}
256
257/// `exsltDateRegister` — register the EXSLT dates module.
258#[no_mangle]
259pub extern "C" fn exsltDateRegister() {
260    dates::register_all();
261}
262
263/// `exsltSaxonRegister` — register the EXSLT Saxon extensions
264/// (upstream exslt.c calls this from `exsltRegisterAll`).
265#[no_mangle]
266pub extern "C" fn exsltSaxonRegister() {
267    saxon::register_all();
268}
269
270/// `exsltDynRegister` — register the EXSLT dynamic module.
271#[no_mangle]
272pub extern "C" fn exsltDynRegister() {
273    dynamic::register_all();
274}
275
276/// `exsltCryptoRegister` — register the EXSLT crypto module. The candidate
277/// has no crypto module; upstream returns void, so this is a no-op.
278#[no_mangle]
279pub const extern "C" fn exsltCryptoRegister() {}
280
281/// `exsltDateXpathCtxtRegister(ctxt, prefix)` — register the dates module
282/// on a specific XPath context (upstream date.c). The candidate's registry
283/// is global; registration is performed for all contexts.
284#[no_mangle]
285pub extern "C" fn exsltDateXpathCtxtRegister(_ctxt: *mut c_void, _prefix: *const xmlChar) -> c_int {
286    dates::register_all();
287    0
288}
289
290/// `exsltMathXpathCtxtRegister(ctxt, prefix)` — math module (see above).
291#[no_mangle]
292pub extern "C" fn exsltMathXpathCtxtRegister(_ctxt: *mut c_void, _prefix: *const xmlChar) -> c_int {
293    math::register_all();
294    0
295}
296
297/// `exsltSetsXpathCtxtRegister(ctxt, prefix)` — sets module (see above).
298#[no_mangle]
299pub extern "C" fn exsltSetsXpathCtxtRegister(_ctxt: *mut c_void, _prefix: *const xmlChar) -> c_int {
300    sets::register_all();
301    0
302}
303
304/// `exsltStrXpathCtxtRegister(ctxt, prefix)` — strings module (see above).
305#[no_mangle]
306pub extern "C" fn exsltStrXpathCtxtRegister(_ctxt: *mut c_void, _prefix: *const xmlChar) -> c_int {
307    strings::register_all();
308    0
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn test_register_and_lookup() {
317        fn my_func(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
318            Ok(XPathValue::String("hello".to_string()))
319        }
320        register("test:myfunc", my_func);
321        let f = lookup("test:myfunc");
322        assert!(f.is_some());
323        let (names, _) = iter_functions()
324            .into_iter()
325            .find(|(n, _)| n == "test:myfunc")
326            .unwrap();
327        assert_eq!(names, "test:myfunc");
328    }
329
330    #[test]
331    fn test_lookup_missing() {
332        assert!(lookup("nonexistent:fn").is_none());
333    }
334
335    #[test]
336    fn test_register_all_populates() {
337        // Register everything; all module functions must be present.
338        register_all();
339        for name in [
340            "exsl:node-set",
341            "exsl:object-type",
342            "math:max",
343            "math:sin",
344            "math:constant",
345            "set:difference",
346            "set:distinct",
347            "str:tokenize",
348            "str:padding",
349            "dyn:evaluate",
350            "date:date",
351            "date:date-time",
352        ] {
353            assert!(lookup(name).is_some(), "missing EXSLT function {}", name);
354        }
355    }
356}