Skip to main content

libxml_rs/xslt/extensions/
mod.rs

1//! XSLT extension mechanisms (§33, §35, §85 Phase 8).
2//!
3//! # Upstream contract
4//!
5//! Parity target: upstream libxslt `extensions.c` (1.1.45;
6//! `SRC-LIBXSLT-1.1.42-EXTENSIONS-C` under oracle/historical/src).
7//! Subsystem census: xslt-extension-functions, xslt-extension-elements,
8//! xslt-global-state. ABI surface: `xsltRegisterExtFunction`,
9//! `xsltRegisterExtElement`, plus the per-module extension registries
10//! (`xsltRegisterExtModule*`) exercised by XSLT-001.
11//!
12//! # Conceptual behavior
13//!
14//! Extension functions/elements are registered per transform context
15//! (upstream `extFunctions`/`extElements`), each entry carrying the
16//! namespace URI, local name, and a callback. At call time the XPath
17//! function lookup resolves `prefix:local` through the stylesheet
18//! namespace bindings and dispatches to the registered callback; extension
19//! elements are recognized during instruction execution by namespace.
20//! EXSLT rides the same mechanism (registered into every new context).
21//!
22//! # Ownership & safety invariants
23//!
24//! Entries are heap-allocated and own duplicated `name`/`ns` strings;
25//! they are freed with the context (`xsltFreeCtxtExts`, matching
26//! atlas/OWNERSHIP_ATLAS.md section 4 extension-module-data row). The
27//! callback pointer is borrowed user-code; the library never invokes
28//! unknown-arity functions with a full XPath stack unless the bridge in
29//! R-000162 is active (C XPath functions go through the parser-context
30//! bridge). `ctxt`, `name`, `NS_uri` must be valid; failure paths free
31//! partial entries exactly once.
32//!
33//! # Historical quirks & epochs
34//!
35//! Extension registration has been stable since the libxslt 1.1 series
36//! (2004+; atlas/HISTORY.md) and falls inside the E-008 frozen output
37//! epoch (2009 → 1.1.45; atlas/SEMANTIC_EPOCHS.md). R-000162 closed the
38//! C XPath-function callback bridge (`xmlXPathRegisterFunc` + the
39//! namespaced `function_lookup` fallback dispatching through
40//! `xsltFindExtFunction`); R-000165 added the per-module EXSLT
41//! registration exports; R-000140 covered the `_xslt*` ABI mirrors.
42//!
43//! # Deliberate oddities
44//!
45//! - The `f` parameter of `xsltRegisterExtFunction` is declared with an
46//!   intentionally minimal C signature (opaque `(void*, int)`) — the real
47//!   dispatch contract lives in the R-000162 bridge; the stored pointer is
48//!   opaque to this module.
49//! - Registration is a per-context linked list (upstream uses
50//!   `xmlHashTable`); the candidate linear-searches, an internal storage
51//!   divergence with identical observable semantics.
52//!
53//! # Proving courts
54//!
55//! XSLT-001 (xslt-family differential probe: `xsltRegisterExtModule*`,
56//! `xsltExtModuleFunctionLookup`), EXSLT, CLI-XSLTPROC (extension-using
57//! corpus), and the in-crate `cargo test` suites.
58//!
59//! # Tempting simplifications that would break parity
60//!
61//! - Stubbing registered functions instead of dispatching (the pre-R-000162
62//!   behavior) breaks every C extension consumer; the callback bridge is
63//!   mandatory.
64//! - Deduplicating entries by name alone would break duplicate
65//!   registration semantics (upstream last-registration-wins per context).
66//! - Freeing the callback pointer would violate the borrowed-user-data
67//!   invariant (atlas/OWNERSHIP_ATLAS.md section 6).
68//!
69//! Extensions allow stylesheets to call external functions and elements.
70//! Registered via `xsltRegisterExtFunction` (functions) and
71//! `xsltRegisterExtElement` (elements).
72//!
73//! # UPSTREAM-PARITY
74//!
75//! Upstream libxslt (extensions.c) stores extension function registrations
76//! in the transform context (`extFunctions`), each entry holding the
77//! namespace URI, name, and function pointer. Extension elements are stored
78//! similarly with their transform function.
79//!
80//! Registration is per-context; functions are looked up at call time via
81//! the context XPath function lookup mechanism.
82
83use crate::abi::structs::*;
84use crate::abi::types::*;
85use std::os::raw::c_int;
86use std::ptr;
87
88/// A registered extension function.
89#[derive(Debug)]
90#[repr(C)]
91pub struct _xsltExtFunction {
92    /// Next entry in the linked list of registered functions.
93    pub next: *mut _xsltExtFunction,
94    /// Local name of the function (e.g. `"node-set"`).
95    pub name: *mut xmlChar,
96    /// Namespace URI of the extension (e.g. `http://exslt.org/common`).
97    pub ns: *mut xmlChar,
98    /// The extension function implementation pointer.
99    pub func: *mut c_void,
100}
101
102/// A registered extension element.
103#[derive(Debug)]
104#[repr(C)]
105pub struct _xsltExtElement {
106    /// Next entry in the linked list of registered elements.
107    pub next: *mut _xsltExtElement,
108    /// Local name of the element.
109    pub name: *mut xmlChar,
110    /// Namespace URI of the extension element.
111    pub ns: *mut xmlChar,
112    /// The extension element transform function.
113    pub func: *mut c_void,
114}
115
116/// Register an XSLT extension function.
117///
118/// # SAFETY
119///
120/// - `ctxt` must be a valid `_xsltTransformContext`.
121/// - `name` and `NS_uri` must be valid NUL-terminated strings.
122/// - `f` must be a valid function pointer.
123#[no_mangle]
124pub unsafe extern "C" fn xsltRegisterExtFunction(
125    ctxt: *mut _xsltTransformContext,
126    name: *const xmlChar,
127    NS_uri: *const xmlChar,
128    f: Option<unsafe extern "C" fn(*mut c_void, c_int)>,
129) -> c_int {
130    if ctxt.is_null() || name.is_null() || NS_uri.is_null() {
131        return -1;
132    }
133    let entry = libc::calloc(1, core::mem::size_of::<_xsltExtFunction>()) as *mut _xsltExtFunction;
134    if entry.is_null() {
135        return -1;
136    }
137    (*entry).name = dup_str(name);
138    (*entry).ns = dup_str(NS_uri);
139    if (*entry).name.is_null() || (*entry).ns.is_null() {
140        if !(*entry).name.is_null() {
141            libc::free((*entry).name as *mut libc::c_void);
142        }
143        if !(*entry).ns.is_null() {
144            libc::free((*entry).ns as *mut libc::c_void);
145        }
146        libc::free(entry as *mut libc::c_void);
147        return -1;
148    }
149    (*entry).func = f.map(|fp| fp as *mut c_void).unwrap_or(ptr::null_mut());
150    // Prepend to the context's extension function list (extFunctions
151    // is a void* chain in the struct; we use a linked list here).
152    (*entry).next = (*ctxt).extFunctions as *mut _xsltExtFunction;
153    (*ctxt).extFunctions = entry as *mut c_void;
154    0
155}
156
157/// Register an XSLT extension element.
158///
159/// # SAFETY
160///
161/// - `ctxt` must be a valid `_xsltTransformContext`.
162/// - `name` and `NS_uri` must be valid NUL-terminated strings.
163/// - `f` must be a valid function pointer.
164#[no_mangle]
165pub unsafe extern "C" fn xsltRegisterExtElement(
166    ctxt: *mut _xsltTransformContext,
167    name: *const xmlChar,
168    NS_uri: *const xmlChar,
169    f: Option<
170        unsafe extern "C" fn(*mut c_void, *mut c_void, *mut _xmlNode, *mut c_void, *mut _xmlNode),
171    >,
172) -> c_int {
173    if ctxt.is_null() || name.is_null() || NS_uri.is_null() {
174        return -1;
175    }
176    let entry = libc::calloc(1, core::mem::size_of::<_xsltExtElement>()) as *mut _xsltExtElement;
177    if entry.is_null() {
178        return -1;
179    }
180    (*entry).name = dup_str(name);
181    (*entry).ns = dup_str(NS_uri);
182    if (*entry).name.is_null() || (*entry).ns.is_null() {
183        if !(*entry).name.is_null() {
184            libc::free((*entry).name as *mut libc::c_void);
185        }
186        if !(*entry).ns.is_null() {
187            libc::free((*entry).ns as *mut libc::c_void);
188        }
189        libc::free(entry as *mut libc::c_void);
190        return -1;
191    }
192    (*entry).func = f.map(|fp| fp as *mut c_void).unwrap_or(ptr::null_mut());
193    (*entry).next = (*ctxt).extElements as *mut _xsltExtElement;
194    (*ctxt).extElements = entry as *mut c_void;
195    0
196}
197
198/// Look up a registered extension function.
199///
200/// # SAFETY
201///
202/// - `ctxt` must be a valid `_xsltTransformContext`.
203/// - `name` and `ns` must be valid NUL-terminated strings.
204pub unsafe fn xsltFindExtFunction(
205    ctxt: *mut _xsltTransformContext,
206    name: *const xmlChar,
207    ns: *const xmlChar,
208) -> *mut c_void {
209    if ctxt.is_null() || name.is_null() || ns.is_null() {
210        return ptr::null_mut();
211    }
212    let mut cur = (*ctxt).extFunctions as *mut _xsltExtFunction;
213    while !cur.is_null() {
214        if !(*cur).name.is_null()
215            && !(*cur).ns.is_null()
216            && libc::strcmp(
217                (*cur).name as *const libc::c_char,
218                name as *const libc::c_char,
219            ) == 0
220            && libc::strcmp((*cur).ns as *const libc::c_char, ns as *const libc::c_char) == 0
221        {
222            return (*cur).func;
223        }
224        cur = (*cur).next;
225    }
226    ptr::null_mut()
227}
228
229/// Look up a registered extension element.
230///
231/// # SAFETY
232///
233/// - `ctxt` must be a valid `_xsltTransformContext`.
234/// - `name` and `ns` must be valid NUL-terminated strings.
235pub unsafe fn xsltFindExtElement(
236    ctxt: *mut _xsltTransformContext,
237    name: *const xmlChar,
238    ns: *const xmlChar,
239) -> *mut c_void {
240    if ctxt.is_null() || name.is_null() || ns.is_null() {
241        return ptr::null_mut();
242    }
243    let mut cur = (*ctxt).extElements as *mut _xsltExtElement;
244    while !cur.is_null() {
245        if !(*cur).name.is_null()
246            && !(*cur).ns.is_null()
247            && libc::strcmp(
248                (*cur).name as *const libc::c_char,
249                name as *const libc::c_char,
250            ) == 0
251            && libc::strcmp((*cur).ns as *const libc::c_char, ns as *const libc::c_char) == 0
252        {
253            return (*cur).func;
254        }
255        cur = (*cur).next;
256    }
257    ptr::null_mut()
258}
259
260/// Free all extension registrations in a transform context.
261///
262/// # SAFETY
263///
264/// - `ctxt` must be a valid `_xsltTransformContext`.
265pub unsafe fn xsltFreeExts(ctxt: *mut _xsltTransformContext) {
266    if ctxt.is_null() {
267        return;
268    }
269    // Free extension functions.
270    let mut cur = (*ctxt).extFunctions as *mut _xsltExtFunction;
271    (*ctxt).extFunctions = ptr::null_mut();
272    while !cur.is_null() {
273        let next = (*cur).next;
274        if !(*cur).name.is_null() {
275            libc::free((*cur).name as *mut libc::c_void);
276        }
277        if !(*cur).ns.is_null() {
278            libc::free((*cur).ns as *mut libc::c_void);
279        }
280        libc::free(cur as *mut libc::c_void);
281        cur = next;
282    }
283    // Free extension elements.
284    let mut cur = (*ctxt).extElements as *mut _xsltExtElement;
285    (*ctxt).extElements = ptr::null_mut();
286    while !cur.is_null() {
287        let next = (*cur).next;
288        if !(*cur).name.is_null() {
289            libc::free((*cur).name as *mut libc::c_void);
290        }
291        if !(*cur).ns.is_null() {
292            libc::free((*cur).ns as *mut libc::c_void);
293        }
294        libc::free(cur as *mut libc::c_void);
295        cur = next;
296    }
297}
298
299/// Duplicate a NUL-terminated string.
300unsafe fn dup_str(s: *const xmlChar) -> *mut xmlChar {
301    let len = libc::strlen(s as *const libc::c_char);
302    let copy = libc::malloc(len + 1) as *mut xmlChar;
303    if copy.is_null() {
304        return ptr::null_mut();
305    }
306    libc::memcpy(copy as *mut libc::c_void, s as *const libc::c_void, len);
307    *copy.add(len) = 0;
308    copy
309}
310
311use std::ffi::c_void;
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use core::ptr;
317
318    /// Allocate a zero-initialized `_xsltTransformContext`.
319    ///
320    /// # Safety
321    ///
322    /// - `libc::calloc` returns a zeroed block of the struct size or NULL;
323    ///   the caller must check for NULL before dereferencing and must
324    ///   release the block with `libc::free` when done.
325    fn make_ctxt() -> *mut _xsltTransformContext {
326        unsafe {
327            libc::calloc(1, core::mem::size_of::<_xsltTransformContext>())
328                as *mut _xsltTransformContext
329        }
330    }
331
332    /// Register an extension function and find it back by name/URI.
333    ///
334    /// # Safety
335    ///
336    /// - `ctxt` is a live zeroed `_xsltTransformContext` from `make_ctxt`
337    ///   and is released with `libc::free` after `xsltFreeExts` has run.
338    /// - `dummy` is a valid extern "C" function pointer registered and
339    ///   compared by address; the `c"..."` string literals are valid
340    ///   NUL-terminated `xmlChar` buffers passed to the register/find
341    ///   APIs, which heap-copy the names they retain.
342    #[test]
343    fn test_register_and_find_function() {
344        unsafe {
345            let ctxt = make_ctxt();
346            extern "C" fn dummy(_ctx: *mut c_void, _n: c_int) {}
347            assert_eq!(
348                xsltRegisterExtFunction(
349                    ctxt,
350                    c"myfunc".as_ptr() as *const xmlChar,
351                    c"http://example.com/ext".as_ptr() as *const xmlChar,
352                    Some(dummy),
353                ),
354                0
355            );
356            let found = xsltFindExtFunction(
357                ctxt,
358                c"myfunc".as_ptr() as *const xmlChar,
359                c"http://example.com/ext".as_ptr() as *const xmlChar,
360            );
361            assert_eq!(found, dummy as *mut c_void);
362            let not_found = xsltFindExtFunction(
363                ctxt,
364                c"other".as_ptr() as *const xmlChar,
365                c"http://example.com/ext".as_ptr() as *const xmlChar,
366            );
367            assert!(not_found.is_null());
368            xsltFreeExts(ctxt);
369            libc::free(ctxt as *mut libc::c_void);
370        }
371    }
372
373    /// NULL contexts, names, URIs, and callbacks are rejected with `-1`.
374    ///
375    /// # Safety
376    ///
377    /// - `xsltRegisterExtFunction`/`xsltRegisterExtElement` return `-1` on
378    ///   NULL arguments before dereferencing them, so passing NULL
379    ///   pointers reads no memory.
380    #[test]
381    fn test_register_null() {
382        unsafe {
383            assert_eq!(
384                xsltRegisterExtFunction(ptr::null_mut(), ptr::null(), ptr::null(), None),
385                -1
386            );
387            assert_eq!(
388                xsltRegisterExtElement(ptr::null_mut(), ptr::null(), ptr::null(), None),
389                -1
390            );
391        }
392    }
393}