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<crate::abi::exports_xslt_compile::xsltTransformFunction>,
170) -> c_int {
171    if ctxt.is_null() || name.is_null() || NS_uri.is_null() {
172        return -1;
173    }
174    let entry = libc::calloc(1, core::mem::size_of::<_xsltExtElement>()) as *mut _xsltExtElement;
175    if entry.is_null() {
176        return -1;
177    }
178    (*entry).name = dup_str(name);
179    (*entry).ns = dup_str(NS_uri);
180    if (*entry).name.is_null() || (*entry).ns.is_null() {
181        if !(*entry).name.is_null() {
182            libc::free((*entry).name as *mut libc::c_void);
183        }
184        if !(*entry).ns.is_null() {
185            libc::free((*entry).ns as *mut libc::c_void);
186        }
187        libc::free(entry as *mut libc::c_void);
188        return -1;
189    }
190    (*entry).func = f.map(|fp| fp as *mut c_void).unwrap_or(ptr::null_mut());
191    (*entry).next = (*ctxt).extElements as *mut _xsltExtElement;
192    (*ctxt).extElements = entry as *mut c_void;
193    0
194}
195
196/// Look up a registered extension function.
197///
198/// # SAFETY
199///
200/// - `ctxt` must be a valid `_xsltTransformContext`.
201/// - `name` and `ns` must be valid NUL-terminated strings.
202pub unsafe fn xsltFindExtFunction(
203    ctxt: *mut _xsltTransformContext,
204    name: *const xmlChar,
205    ns: *const xmlChar,
206) -> *mut c_void {
207    if ctxt.is_null() || name.is_null() || ns.is_null() {
208        return ptr::null_mut();
209    }
210    let mut cur = (*ctxt).extFunctions as *mut _xsltExtFunction;
211    while !cur.is_null() {
212        if !(*cur).name.is_null()
213            && !(*cur).ns.is_null()
214            && libc::strcmp(
215                (*cur).name as *const libc::c_char,
216                name as *const libc::c_char,
217            ) == 0
218            && libc::strcmp((*cur).ns as *const libc::c_char, ns as *const libc::c_char) == 0
219        {
220            return (*cur).func;
221        }
222        cur = (*cur).next;
223    }
224    ptr::null_mut()
225}
226
227/// Look up a registered extension element.
228///
229/// # SAFETY
230///
231/// - `ctxt` must be a valid `_xsltTransformContext`.
232/// - `name` and `ns` must be valid NUL-terminated strings.
233pub unsafe fn xsltFindExtElement(
234    ctxt: *mut _xsltTransformContext,
235    name: *const xmlChar,
236    ns: *const xmlChar,
237) -> *mut c_void {
238    if ctxt.is_null() || name.is_null() || ns.is_null() {
239        return ptr::null_mut();
240    }
241    let mut cur = (*ctxt).extElements as *mut _xsltExtElement;
242    while !cur.is_null() {
243        if !(*cur).name.is_null()
244            && !(*cur).ns.is_null()
245            && libc::strcmp(
246                (*cur).name as *const libc::c_char,
247                name as *const libc::c_char,
248            ) == 0
249            && libc::strcmp((*cur).ns as *const libc::c_char, ns as *const libc::c_char) == 0
250        {
251            return (*cur).func;
252        }
253        cur = (*cur).next;
254    }
255    ptr::null_mut()
256}
257
258/// Free all extension registrations in a transform context.
259///
260/// # SAFETY
261///
262/// - `ctxt` must be a valid `_xsltTransformContext`.
263pub unsafe fn xsltFreeExts(ctxt: *mut _xsltTransformContext) {
264    if ctxt.is_null() {
265        return;
266    }
267    // Free extension functions.
268    let mut cur = (*ctxt).extFunctions as *mut _xsltExtFunction;
269    (*ctxt).extFunctions = ptr::null_mut();
270    while !cur.is_null() {
271        let next = (*cur).next;
272        if !(*cur).name.is_null() {
273            libc::free((*cur).name as *mut libc::c_void);
274        }
275        if !(*cur).ns.is_null() {
276            libc::free((*cur).ns as *mut libc::c_void);
277        }
278        libc::free(cur as *mut libc::c_void);
279        cur = next;
280    }
281    // Free extension elements.
282    let mut cur = (*ctxt).extElements as *mut _xsltExtElement;
283    (*ctxt).extElements = ptr::null_mut();
284    while !cur.is_null() {
285        let next = (*cur).next;
286        if !(*cur).name.is_null() {
287            libc::free((*cur).name as *mut libc::c_void);
288        }
289        if !(*cur).ns.is_null() {
290            libc::free((*cur).ns as *mut libc::c_void);
291        }
292        libc::free(cur as *mut libc::c_void);
293        cur = next;
294    }
295}
296
297/// Duplicate a NUL-terminated string.
298unsafe fn dup_str(s: *const xmlChar) -> *mut xmlChar {
299    let len = libc::strlen(s as *const libc::c_char);
300    let copy = libc::malloc(len + 1) as *mut xmlChar;
301    if copy.is_null() {
302        return ptr::null_mut();
303    }
304    libc::memcpy(copy as *mut libc::c_void, s as *const libc::c_void, len);
305    *copy.add(len) = 0;
306    copy
307}
308
309use std::ffi::c_void;
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use core::ptr;
315
316    /// Allocate a zero-initialized `_xsltTransformContext`.
317    ///
318    /// # Safety
319    ///
320    /// - `libc::calloc` returns a zeroed block of the struct size or NULL;
321    ///   the caller must check for NULL before dereferencing and must
322    ///   release the block with `libc::free` when done.
323    fn make_ctxt() -> *mut _xsltTransformContext {
324        unsafe {
325            libc::calloc(1, core::mem::size_of::<_xsltTransformContext>())
326                as *mut _xsltTransformContext
327        }
328    }
329
330    /// Register an extension function and find it back by name/URI.
331    ///
332    /// # Safety
333    ///
334    /// - `ctxt` is a live zeroed `_xsltTransformContext` from `make_ctxt`
335    ///   and is released with `libc::free` after `xsltFreeExts` has run.
336    /// - `dummy` is a valid extern "C" function pointer registered and
337    ///   compared by address; the `c"..."` string literals are valid
338    ///   NUL-terminated `xmlChar` buffers passed to the register/find
339    ///   APIs, which heap-copy the names they retain.
340    #[test]
341    fn test_register_and_find_function() {
342        unsafe {
343            let ctxt = make_ctxt();
344            extern "C" fn dummy(_ctx: *mut c_void, _n: c_int) {}
345            assert_eq!(
346                xsltRegisterExtFunction(
347                    ctxt,
348                    c"myfunc".as_ptr() as *const xmlChar,
349                    c"http://example.com/ext".as_ptr() as *const xmlChar,
350                    Some(dummy),
351                ),
352                0
353            );
354            let found = xsltFindExtFunction(
355                ctxt,
356                c"myfunc".as_ptr() as *const xmlChar,
357                c"http://example.com/ext".as_ptr() as *const xmlChar,
358            );
359            assert_eq!(found, dummy as *mut c_void);
360            let not_found = xsltFindExtFunction(
361                ctxt,
362                c"other".as_ptr() as *const xmlChar,
363                c"http://example.com/ext".as_ptr() as *const xmlChar,
364            );
365            assert!(not_found.is_null());
366            xsltFreeExts(ctxt);
367            libc::free(ctxt as *mut libc::c_void);
368        }
369    }
370
371    /// NULL contexts, names, URIs, and callbacks are rejected with `-1`.
372    ///
373    /// # Safety
374    ///
375    /// - `xsltRegisterExtFunction`/`xsltRegisterExtElement` return `-1` on
376    ///   NULL arguments before dereferencing them, so passing NULL
377    ///   pointers reads no memory.
378    #[test]
379    fn test_register_null() {
380        unsafe {
381            assert_eq!(
382                xsltRegisterExtFunction(ptr::null_mut(), ptr::null(), ptr::null(), None),
383                -1
384            );
385            assert_eq!(
386                xsltRegisterExtElement(ptr::null_mut(), ptr::null(), ptr::null(), None),
387                -1
388            );
389        }
390    }
391}