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
39use once_cell::sync::Lazy;
40use parking_lot::Mutex;
41use std::collections::HashMap;
42use std::ffi::c_void;
43use std::os::raw::c_int;
44
45use crate::abi::types::xmlChar;
46use crate::xml::xpath::context::XPathContext;
47use crate::xml::xpath::types::XPathValue;
48
49pub mod common;
50pub mod dates;
51pub mod dynamic;
52pub mod functions;
53pub mod math;
54pub mod saxon;
55pub mod sets;
56pub mod strings;
57
58/// EXSLT namespace URIs.
59pub const EXSLT_NS_COMMON: &str = "http://exslt.org/common";
60/// Math module namespace (`math:max`, `math:min`, `math:sin`, ...).
61pub const EXSLT_NS_MATH: &str = "http://exslt.org/math";
62/// Sets module namespace (`set:difference`, `set:distinct`, ...).
63pub const EXSLT_NS_SETS: &str = "http://exslt.org/sets";
64/// Strings module namespace (`str:concat`, `str:padding`, ...).
65pub const EXSLT_NS_STRINGS: &str = "http://exslt.org/strings";
66/// Dynamic module namespace (`dyn:element`, `dyn:evaluate`, ...).
67pub const EXSLT_NS_DYNAMIC: &str = "http://exslt.org/dynamic";
68/// Functions module namespace (`func:function`, `func:result`, ...).
69pub const EXSLT_NS_FUNCTIONS: &str = "http://exslt.org/functions";
70/// Dates-and-times module namespace (`date:date`, `date:format-date`, ...).
71pub const EXSLT_NS_DATES: &str = "http://exslt.org/dates-and-times";
72
73/// The XPath function signature used throughout the EXSLT modules.
74pub type ExsltFunction = fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String>;
75
76/// A capture-capable EXSLT function (used by `func:function` bodies).
77///
78/// Stored as a leaked `&'static` reference so entries are `Copy` and the
79/// registry can hand out clones. The process-wide registry lives for the
80/// lifetime of the program, so leaking is intentional and bounded.
81pub type ExsltClosure =
82    &'static (dyn Fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String> + Send + Sync);
83
84/// Process-wide EXSLT function registry, keyed by full QName
85/// (e.g. `"math:max"`). Populated by `exsltRegisterAll()`.
86static REGISTRY: Lazy<Mutex<HashMap<String, ExsltClosure>>> =
87    Lazy::new(|| Mutex::new(HashMap::new()));
88
89/// Register a single EXSLT function under its full QName.
90pub fn register<F>(name: &str, f: F)
91where
92    F: Fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String> + Send + Sync + 'static,
93{
94    // SAFETY: the boxed closure is leaked; it lives for the process lifetime
95    // (the registry is never cleared) so the resulting 'static reference is
96    // sound.
97    let leaked: &'static (dyn Fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String>
98                  + Send
99                  + Sync) = Box::leak(Box::new(f));
100    REGISTRY.lock().insert(name.to_string(), leaked);
101}
102
103/// Look up a registered EXSLT function by full QName.
104pub fn lookup(name: &str) -> Option<ExsltClosure> {
105    REGISTRY.lock().get(name).copied()
106}
107
108/// Iterate over all registered EXSLT functions.
109pub fn iter_functions() -> Vec<(String, ExsltClosure)> {
110    REGISTRY
111        .lock()
112        .iter()
113        .map(|(k, v)| (k.clone(), *v))
114        .collect()
115}
116
117/// Whether any EXSLT functions have been registered.
118pub fn is_registered() -> bool {
119    !REGISTRY.lock().is_empty()
120}
121
122/// Register every EXSLT module (mirrors upstream `exsltRegisterAll`).
123pub fn register_all() {
124    common::register_all();
125    math::register_all();
126    sets::register_all();
127    strings::register_all();
128    dynamic::register_all();
129    dates::register_all();
130    functions::register_all();
131    saxon::register_all();
132}
133
134/// The C ABI entry point: register all EXSLT modules.
135///
136/// # UPSTREAM-PARITY
137///
138/// ```c
139/// void exsltRegisterAll(void);
140/// ```
141///
142/// Oracle behavior: registers every EXSLT function so it becomes available
143/// to subsequently created transform contexts. Calling it twice is a no-op
144/// (re-registration overwrites identical entries).
145#[no_mangle]
146pub extern "C" fn exsltRegisterAll() {
147    register_all();
148}
149
150// ═══════════════════════════════════════════════════════════════════════════════
151// Per-module registration entry points (11.1-X R-000165 closure)
152// ═══════════════════════════════════════════════════════════════════════════════
153//
154// Upstream libexslt exposes one register function per module (exslt.c). The
155// candidate's registry is keyed by "prefix:name"; each module's register_all
156// populates it. saxon/crypto have no candidate module — their register
157// functions are exported no-ops (upstream contracts: void / int 0).
158
159/// `exsltCommonRegister` — register the EXSLT common module.
160#[no_mangle]
161pub extern "C" fn exsltCommonRegister() {
162    common::register_all();
163}
164
165/// `exsltMathRegister` — register the EXSLT math module.
166#[no_mangle]
167pub extern "C" fn exsltMathRegister() {
168    math::register_all();
169}
170
171/// `exsltSetsRegister` — register the EXSLT sets module.
172#[no_mangle]
173pub extern "C" fn exsltSetsRegister() {
174    sets::register_all();
175}
176
177/// `exsltFuncRegister` — register the EXSLT functions module.
178#[no_mangle]
179pub extern "C" fn exsltFuncRegister() {
180    functions::register_all();
181}
182
183/// `exsltStrRegister` — register the EXSLT strings module.
184#[no_mangle]
185pub extern "C" fn exsltStrRegister() {
186    strings::register_all();
187}
188
189/// `exsltDateRegister` — register the EXSLT dates module.
190#[no_mangle]
191pub extern "C" fn exsltDateRegister() {
192    dates::register_all();
193}
194
195/// `exsltSaxonRegister` — register the EXSLT Saxon extensions
196/// (upstream exslt.c calls this from `exsltRegisterAll`).
197#[no_mangle]
198pub extern "C" fn exsltSaxonRegister() {
199    saxon::register_all();
200}
201
202/// `exsltDynRegister` — register the EXSLT dynamic module.
203#[no_mangle]
204pub extern "C" fn exsltDynRegister() {
205    dynamic::register_all();
206}
207
208/// `exsltCryptoRegister` — register the EXSLT crypto module. The candidate
209/// has no crypto module; upstream returns void, so this is a no-op.
210#[no_mangle]
211pub const extern "C" fn exsltCryptoRegister() {}
212
213/// `exsltDateXpathCtxtRegister(ctxt, prefix)` — register the dates module
214/// on a specific XPath context (upstream date.c). The candidate's registry
215/// is global; registration is performed for all contexts.
216#[no_mangle]
217pub extern "C" fn exsltDateXpathCtxtRegister(_ctxt: *mut c_void, _prefix: *const xmlChar) -> c_int {
218    dates::register_all();
219    0
220}
221
222/// `exsltMathXpathCtxtRegister(ctxt, prefix)` — math module (see above).
223#[no_mangle]
224pub extern "C" fn exsltMathXpathCtxtRegister(_ctxt: *mut c_void, _prefix: *const xmlChar) -> c_int {
225    math::register_all();
226    0
227}
228
229/// `exsltSetsXpathCtxtRegister(ctxt, prefix)` — sets module (see above).
230#[no_mangle]
231pub extern "C" fn exsltSetsXpathCtxtRegister(_ctxt: *mut c_void, _prefix: *const xmlChar) -> c_int {
232    sets::register_all();
233    0
234}
235
236/// `exsltStrXpathCtxtRegister(ctxt, prefix)` — strings module (see above).
237#[no_mangle]
238pub extern "C" fn exsltStrXpathCtxtRegister(_ctxt: *mut c_void, _prefix: *const xmlChar) -> c_int {
239    strings::register_all();
240    0
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_register_and_lookup() {
249        fn my_func(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
250            Ok(XPathValue::String("hello".to_string()))
251        }
252        register("test:myfunc", my_func);
253        let f = lookup("test:myfunc");
254        assert!(f.is_some());
255        let (names, _) = iter_functions()
256            .into_iter()
257            .find(|(n, _)| n == "test:myfunc")
258            .unwrap();
259        assert_eq!(names, "test:myfunc");
260    }
261
262    #[test]
263    fn test_lookup_missing() {
264        assert!(lookup("nonexistent:fn").is_none());
265    }
266
267    #[test]
268    fn test_register_all_populates() {
269        // Register everything; all module functions must be present.
270        register_all();
271        for name in [
272            "exsl:node-set",
273            "exsl:object-type",
274            "math:max",
275            "math:sin",
276            "math:constant",
277            "set:difference",
278            "set:distinct",
279            "str:tokenize",
280            "str:padding",
281            "dyn:evaluate",
282            "date:date",
283            "date:date-time",
284        ] {
285            assert!(lookup(name).is_some(), "missing EXSLT function {}", name);
286        }
287    }
288}