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}