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}