Skip to main content

libxml_rs/abi/
exports_xslt_ext.rs

1//! C ABI exports for libxslt.so.1 — the "ext" family (§16, Phase 8).
2//!
3//! The extension-module registry (`xsltRegisterExtModule*`,
4//! `xsltUnregisterExtModule*`, the `xsltExtModule*Lookup` queries) and the
5//! per-context/per-style extension data accessors (`xsltGetExtData`,
6//! `xsltStyleGetExtData`, `xsltGetExtInfo`), plus the EXSLT registration
7//! entry points (`xsltRegisterAllFunctions`, `xsltRegisterAllElement`,
8//! `xsltRegisterAllExtras`, `xsltRegisterExtras`).
9//!
10//! Semantics follow upstream libxslt 1.1.45 (`archaeology/libxslt-git/
11//! libxslt/extensions.c`, `extra.c`). The candidate keeps the module
12//! registry in process-lifetime `RwLock<HashMap>` tables (upstream uses
13//! global xmlHashTable instances) with the same observable contracts:
14//! registrations return 0 on success / -1 on failure and lookups resolve
15//! by (name, URI) case-sensitively.
16//!
17//! # Upstream contract
18//!
19//! Parity target is upstream libxslt 1.1.45 `extensions.c` and `extra.c` with
20//! the upstream headers; R-000165 (11.1-O) closed the extension registration
21//! gaps (the per-module EXSLT registration entry points).
22//!
23//! # Conceptual behavior
24//!
25//! This module implements the extension-module registry ABI: the
26//! `xsltRegisterExtModule*`/`xsltUnregisterExtModule*` lifecycle, the
27//! `xsltExtModule*Lookup` queries, per-context/per-style extension data
28//! accessors (`xsltGetExtData`, `xsltStyleGetExtData`, `xsltGetExtInfo`) and
29//! the EXSLT registration entry points. The registry lives in
30//! process-lifetime RwLock tables with the same observable contracts as
31//! upstreams global hash tables (0 success / -1 failure, case-sensitive
32//! (name, URI) lookups).
33//!
34//! # Ownership & safety invariants
35//!
36//! Extension module data is owned by the context/style that registered it and
37//! released in the documented order: `xsltShutdownCtxtExts` (callbacks)
38//! before `xsltFreeCtxtExts` (storage) per OWNERSHIP_ATLAS section 7. Module
39//! function pointers are caller-kept; the registry stores and returns them
40//! verbatim.
41//!
42//! # Historical quirks & epochs
43//!
44//! The extension mechanism matured with EXSLT in the 1.1 era (2004+, HISTORY.md
45//! 2.5) and is part of the frozen E-008 transform epoch; R-000165 added the
46//! missing per-module registration exports (exsltMathRegister et al.) so the
47//! oracle export set is complete.
48//!
49//! # Deliberate oddities
50//!
51//! The RwLock-backed registry instead of upstreams xmlHashTable is a
52//! deliberate internal substitution with identical observable behavior — the
53//! ABI never exposes the registry object itself.
54//!
55//! # Proving courts
56//!
57//! The EXSLT court family, the DSO-LOADER (25/25) and HEADER-COMPILE (595/595)
58//! courts and the callback-family probes (CALLBACK-001) cover this module;
59//! the extension unit tests run under cargo test.
60//!
61//! # Tempting simplifications that would break parity
62//!
63//! A tempting simplification is to make registrations case-insensitive for
64//! convenience — upstream resolves (name, URI) case-sensitively, so a
65//! downstream module registered under a different case would silently stop
66//! resolving. Another shortcut, freeing extension data when the module
67//! unregisters, would break the shutdown-before-free ordering the OWNERSHIP
68//! courts check.
69
70#![allow(non_snake_case)]
71#![allow(unused_variables)]
72#![allow(clippy::missing_safety_doc)]
73#![allow(clippy::not_unsafe_ptr_arg_deref)]
74
75use core::ptr;
76use std::collections::HashMap;
77use std::ffi::CStr;
78use std::os::raw::{c_char, c_int, c_void};
79
80use parking_lot::RwLock;
81
82use crate::abi::allocator::{xmlFreeImpl, xmlMallocImpl};
83use crate::abi::structs::*;
84use crate::abi::types::*;
85
86// ── Registry types (upstream _xsltExtModule, extensions.c) ────────────────
87
88/// `xsltExtInitFunction`: called when a stylesheet first uses the module.
89pub type xsltExtInitFunction =
90    unsafe extern "C" fn(ctxt: *mut _xsltTransformContext, URI: *const xmlChar) -> *mut c_void;
91/// `xsltExtShutdownFunction`: called when the context is freed.
92pub type xsltExtShutdownFunction =
93    unsafe extern "C" fn(ctxt: *mut _xsltTransformContext, URI: *const xmlChar, data: *mut c_void);
94/// `xsltStyleExtInitFunction`: called at stylesheet compile time.
95pub type xsltStyleExtInitFunction =
96    unsafe extern "C" fn(style: *mut _xsltStylesheet, URI: *const xmlChar) -> *mut c_void;
97/// `xsltStyleExtShutdownFunction`: called when the stylesheet is freed.
98pub type xsltStyleExtShutdownFunction =
99    unsafe extern "C" fn(style: *mut _xsltStylesheet, URI: *const xmlChar, data: *mut c_void);
100/// `xsltTopLevelFunction`: handles a top-level extension element.
101pub type xsltTopLevelFunction =
102    unsafe extern "C" fn(style: *mut _xsltStylesheet, node: *mut _xmlNode, data: *mut c_void);
103
104#[derive(Clone, Copy)]
105struct ExtModule {
106    init_func: Option<xsltExtInitFunction>,
107    shutdown_func: Option<xsltExtShutdownFunction>,
108    style_init_func: Option<xsltStyleExtInitFunction>,
109    #[allow(dead_code)]
110    style_shutdown_func: Option<xsltStyleExtShutdownFunction>,
111}
112
113/// `_xsltExtElement` entry: name + URI + precompute + transform handlers.
114/// `_xsltExtElement` entry: name + URI + precompute + transform handlers.
115/// The fn pointers are stored as `usize` so the registry is Send + Sync
116/// (they are cast back to raw pointers at lookup time).
117#[derive(Clone, Copy)]
118struct ExtElement {
119    precomp: usize,   // xsltPreComputeFunction
120    transform: usize, // xsltTransformFunction
121}
122
123/// Registry key: "name\0URI\0" (upstream hashes the QName via xmlDictQLookup).
124fn ext_key(name: *const xmlChar, uri: *const xmlChar) -> Option<Vec<u8>> {
125    if name.is_null() || uri.is_null() {
126        return None;
127    }
128    let n = unsafe { CStr::from_ptr(name as *const c_char).to_bytes() };
129    let u = unsafe { CStr::from_ptr(uri as *const c_char).to_bytes() };
130    let mut k = Vec::with_capacity(n.len() + 1 + u.len());
131    k.extend_from_slice(n);
132    k.push(0);
133    k.extend_from_slice(u);
134    Some(k)
135}
136
137/// Global extension-module registry (upstream `xsltExtModules` hash).
138static EXT_MODULES: once_cell::sync::Lazy<RwLock<HashMap<Vec<u8>, ExtModule>>> =
139    once_cell::sync::Lazy::new(|| RwLock::new(HashMap::new()));
140/// Global extension-element registry (upstream `xsltExtElements` hash).
141/// Fn pointers stored as `usize` (Send + Sync).
142static EXT_ELEMENTS: once_cell::sync::Lazy<RwLock<HashMap<Vec<u8>, ExtElement>>> =
143    once_cell::sync::Lazy::new(|| RwLock::new(HashMap::new()));
144/// Global extension-function registry (upstream `xsltExtFunctions` hash).
145static EXT_FUNCTIONS: once_cell::sync::Lazy<RwLock<HashMap<Vec<u8>, usize>>> =
146    once_cell::sync::Lazy::new(|| RwLock::new(HashMap::new()));
147/// Global top-level-element registry (upstream `xsltExtTopLevels` hash).
148static EXT_TOPLEVELS: once_cell::sync::Lazy<RwLock<HashMap<Vec<u8>, usize>>> =
149    once_cell::sync::Lazy::new(|| RwLock::new(HashMap::new()));
150
151/// `xsltRegisterExtModule` (extensions.c): register a module by URI.
152///
153/// # UPSTREAM-PARITY
154///
155/// ```c
156/// int xsltRegisterExtModule(const xmlChar *URI,
157///                           xsltExtInitFunction initFunc,
158///                           xsltExtShutdownFunction shutdownFunc);
159/// ```
160///
161/// Returns 0 on success, -1 on error.
162#[no_mangle]
163pub unsafe extern "C" fn xsltRegisterExtModule(
164    URI: *const xmlChar,
165    initFunc: Option<xsltExtInitFunction>,
166    shutdownFunc: Option<xsltExtShutdownFunction>,
167) -> c_int {
168    // Upstream xsltRegisterExtModuleFull: NULL URI or NULL initFunc -> -1.
169    if URI.is_null() || initFunc.is_none() {
170        return -1;
171    }
172    let key = unsafe { CStr::from_ptr(URI as *const c_char).to_bytes().to_vec() };
173    EXT_MODULES.write().insert(
174        key,
175        ExtModule {
176            init_func: initFunc,
177            shutdown_func: shutdownFunc,
178            style_init_func: None,
179            style_shutdown_func: None,
180        },
181    );
182    0
183}
184
185/// `xsltRegisterExtModuleFull` (extensions.c): register a module including
186/// the stylesheet-level init/shutdown hooks.
187///
188/// # UPSTREAM-PARITY
189///
190/// ```c
191/// int xsltRegisterExtModuleFull(const xmlChar *URI,
192///                               xsltExtInitFunction initFunc,
193///                               xsltExtShutdownFunction shutdownFunc,
194///                               xsltStyleExtInitFunction styleInitFunc,
195///                               xsltStyleExtShutdownFunction styleShutdownFunc);
196/// ```
197#[no_mangle]
198pub unsafe extern "C" fn xsltRegisterExtModuleFull(
199    URI: *const xmlChar,
200    initFunc: Option<xsltExtInitFunction>,
201    shutdownFunc: Option<xsltExtShutdownFunction>,
202    styleInitFunc: Option<xsltStyleExtInitFunction>,
203    styleShutdownFunc: Option<xsltStyleExtShutdownFunction>,
204) -> c_int {
205    if URI.is_null() || initFunc.is_none() {
206        return -1;
207    }
208    let key = unsafe { CStr::from_ptr(URI as *const c_char).to_bytes().to_vec() };
209    EXT_MODULES.write().insert(
210        key,
211        ExtModule {
212            init_func: initFunc,
213            shutdown_func: shutdownFunc,
214            style_init_func: styleInitFunc,
215            style_shutdown_func: styleShutdownFunc,
216        },
217    );
218    0
219}
220
221/// `xsltRegisterExtModuleElement` (extensions.c): register an extension
222/// element (name in a module URI) with precompute + transform handlers.
223///
224/// # UPSTREAM-PARITY
225///
226/// ```c
227/// int xsltRegisterExtModuleElement(const xmlChar *name, const xmlChar *URI,
228///                                  xsltPreComputeFunction precomp,
229///                                  xsltTransformFunction transform);
230/// ```
231#[no_mangle]
232pub unsafe extern "C" fn xsltRegisterExtModuleElement(
233    name: *const xmlChar,
234    URI: *const xmlChar,
235    precomp: *mut c_void,
236    transform: *mut c_void,
237) -> c_int {
238    let Some(key) = ext_key(name, URI) else {
239        return -1;
240    };
241    EXT_ELEMENTS.write().insert(
242        key,
243        ExtElement {
244            precomp: precomp as usize,
245            transform: transform as usize,
246        },
247    );
248    0
249}
250
251/// `xsltRegisterExtModuleFunction` (extensions.c): register an XPath
252/// extension function (name in a module URI).
253///
254/// # UPSTREAM-PARITY
255///
256/// ```c
257/// int xsltRegisterExtModuleFunction(const xmlChar *name, const xmlChar *URI,
258///                                   xmlXPathFunction function);
259/// ```
260#[no_mangle]
261pub unsafe extern "C" fn xsltRegisterExtModuleFunction(
262    name: *const xmlChar,
263    URI: *const xmlChar,
264    function: *mut c_void,
265) -> c_int {
266    let Some(key) = ext_key(name, URI) else {
267        return -1;
268    };
269    EXT_FUNCTIONS.write().insert(key, function as usize);
270    0
271}
272
273/// `xsltRegisterExtModuleTopLevel` (extensions.c): register a top-level
274/// extension element handler.
275///
276/// # UPSTREAM-PARITY
277///
278/// ```c
279/// int xsltRegisterExtModuleTopLevel(const xmlChar *name, const xmlChar *URI,
280///                                   xsltTopLevelFunction function);
281/// ```
282#[no_mangle]
283pub unsafe extern "C" fn xsltRegisterExtModuleTopLevel(
284    name: *const xmlChar,
285    URI: *const xmlChar,
286    function: *mut c_void,
287) -> c_int {
288    let Some(key) = ext_key(name, URI) else {
289        return -1;
290    };
291    EXT_TOPLEVELS.write().insert(key, function as usize);
292    0
293}
294
295/// `xsltUnregisterExtModule` (extensions.c): unregister a module and all of
296/// its elements/functions/top-levels.
297///
298/// # UPSTREAM-PARITY
299///
300/// ```c
301/// int xsltUnregisterExtModule(const xmlChar *URI);
302/// ```
303#[no_mangle]
304pub unsafe extern "C" fn xsltUnregisterExtModule(URI: *const xmlChar) -> c_int {
305    if URI.is_null() {
306        return -1;
307    }
308    let uri_bytes = unsafe { CStr::from_ptr(URI as *const c_char).to_bytes().to_vec() };
309    let mut mods = EXT_MODULES.write();
310    if mods.remove(&uri_bytes).is_none() {
311        return -1;
312    }
313    drop(mods);
314    // Remove every element/function/top-level belonging to the URI.
315    let mut elems = EXT_ELEMENTS.write();
316    let mut funcs = EXT_FUNCTIONS.write();
317    let mut tops = EXT_TOPLEVELS.write();
318    let suffix: Vec<u8> = {
319        let mut s = vec![0];
320        s.extend_from_slice(&uri_bytes);
321        s
322    };
323    elems.retain(|k, _| !k.ends_with(&suffix));
324    funcs.retain(|k, _| !k.ends_with(&suffix));
325    tops.retain(|k, _| !k.ends_with(&suffix));
326    0
327}
328
329/// `xsltUnregisterExtModuleElement` (extensions.c).
330///
331/// # UPSTREAM-PARITY
332///
333/// ```c
334/// int xsltUnregisterExtModuleElement(const xmlChar *name, const xmlChar *URI);
335/// ```
336#[no_mangle]
337pub unsafe extern "C" fn xsltUnregisterExtModuleElement(
338    name: *const xmlChar,
339    URI: *const xmlChar,
340) -> c_int {
341    let Some(key) = ext_key(name, URI) else {
342        return -1;
343    };
344    if EXT_ELEMENTS.write().remove(&key).is_some() {
345        0
346    } else {
347        -1
348    }
349}
350
351/// `xsltUnregisterExtModuleFunction` (extensions.c).
352///
353/// # UPSTREAM-PARITY
354///
355/// ```c
356/// int xsltUnregisterExtModuleFunction(const xmlChar *name, const xmlChar *URI);
357/// ```
358#[no_mangle]
359pub unsafe extern "C" fn xsltUnregisterExtModuleFunction(
360    name: *const xmlChar,
361    URI: *const xmlChar,
362) -> c_int {
363    let Some(key) = ext_key(name, URI) else {
364        return -1;
365    };
366    if EXT_FUNCTIONS.write().remove(&key).is_some() {
367        0
368    } else {
369        -1
370    }
371}
372
373/// `xsltUnregisterExtModuleTopLevel` (extensions.c).
374///
375/// # UPSTREAM-PARITY
376///
377/// ```c
378/// int xsltUnregisterExtModuleTopLevel(const xmlChar *name, const xmlChar *URI);
379/// ```
380#[no_mangle]
381pub unsafe extern "C" fn xsltUnregisterExtModuleTopLevel(
382    name: *const xmlChar,
383    URI: *const xmlChar,
384) -> c_int {
385    let Some(key) = ext_key(name, URI) else {
386        return -1;
387    };
388    if EXT_TOPLEVELS.write().remove(&key).is_some() {
389        0
390    } else {
391        -1
392    }
393}
394
395/// `xsltRegisterExtPrefix` (extensions.c): register a prefix→URI mapping on
396/// the stylesheet so `xsltCheckExtPrefix` recognises it as an extension.
397///
398/// # UPSTREAM-PARITY
399///
400/// ```c
401/// int xsltRegisterExtPrefix(xsltStylesheetPtr style,
402///                           const xmlChar *prefix, const xmlChar *URI);
403/// ```
404#[no_mangle]
405pub unsafe extern "C" fn xsltRegisterExtPrefix(
406    style: *mut _xsltStylesheet,
407    prefix: *const xmlChar,
408    URI: *const xmlChar,
409) -> c_int {
410    if style.is_null() || prefix.is_null() || URI.is_null() {
411        return -1;
412    }
413    // The candidate carries registered extension prefixes as a growable
414    // linked list in the stylesheet (upstream uses style->extInfos hash).
415    let mut cur = (*style).extInfos as *mut ExtPrefixEntry;
416    while !cur.is_null() {
417        if !(*cur).prefix.is_null()
418            && libc::strcmp((*cur).prefix as *const c_char, prefix as *const c_char) == 0
419        {
420            // Re-registration with a different URI updates the mapping.
421            let new_uri =
422                crate::abi::allocator::xmlMemStrdupImpl(URI as *const c_char) as *mut c_char;
423            if new_uri.is_null() {
424                return -1;
425            }
426            xmlFreeImpl((*cur).uri as *mut c_void);
427            (*cur).uri = new_uri;
428            return 0;
429        }
430        cur = (*cur).next;
431    }
432    let entry = xmlMallocImpl(size_of::<ExtPrefixEntry>()) as *mut ExtPrefixEntry;
433    if entry.is_null() {
434        return -1;
435    }
436    let p = crate::abi::allocator::xmlMemStrdupImpl(prefix as *const c_char) as *mut c_char;
437    let u = crate::abi::allocator::xmlMemStrdupImpl(URI as *const c_char) as *mut c_char;
438    if p.is_null() || u.is_null() {
439        if !p.is_null() {
440            xmlFreeImpl(p as *mut c_void);
441        }
442        if !u.is_null() {
443            xmlFreeImpl(u as *mut c_void);
444        }
445        xmlFreeImpl(entry as *mut c_void);
446        return -1;
447    }
448    ptr::write(
449        entry,
450        ExtPrefixEntry {
451            next: (*style).extInfos as *mut ExtPrefixEntry,
452            prefix: p,
453            uri: u,
454        },
455    );
456    (*style).extInfos = entry as *mut c_void;
457    0
458}
459
460/// `xsltCheckExtPrefix` (extensions.c): 1 if `prefix` is registered as an
461/// extension prefix on the stylesheet (or is a literal-result element
462/// prefix), 0 otherwise.
463///
464/// # UPSTREAM-PARITY
465///
466/// ```c
467/// int xsltCheckExtPrefix(xsltStylesheetPtr style, const xmlChar *prefix);
468/// ```
469#[no_mangle]
470pub unsafe extern "C" fn xsltCheckExtPrefix(
471    style: *mut _xsltStylesheet,
472    prefix: *const xmlChar,
473) -> c_int {
474    if style.is_null() || prefix.is_null() {
475        return 0;
476    }
477    let mut cur = (*style).extInfos as *mut ExtPrefixEntry;
478    while !cur.is_null() {
479        if !(*cur).prefix.is_null()
480            && libc::strcmp((*cur).prefix as *const c_char, prefix as *const c_char) == 0
481        {
482            return 1;
483        }
484        cur = (*cur).next;
485    }
486    0
487}
488
489/// `xsltCheckExtURI` (extensions.c): 1 if `URI` is registered as an
490/// extension namespace on the stylesheet, 0 otherwise.
491///
492/// # UPSTREAM-PARITY
493///
494/// ```c
495/// int xsltCheckExtURI(xsltStylesheetPtr style, const xmlChar *URI);
496/// ```
497#[no_mangle]
498pub unsafe extern "C" fn xsltCheckExtURI(
499    style: *mut _xsltStylesheet,
500    URI: *const xmlChar,
501) -> c_int {
502    if style.is_null() || URI.is_null() {
503        return 0;
504    }
505    let mut cur = (*style).extInfos as *mut ExtPrefixEntry;
506    while !cur.is_null() {
507        if !(*cur).uri.is_null()
508            && libc::strcmp((*cur).uri as *const c_char, URI as *const c_char) == 0
509        {
510            return 1;
511        }
512        cur = (*cur).next;
513    }
514    0
515}
516
517/// `xsltExtElementLookup` (extensions.c): resolve an extension element's
518/// transform function, consulting the per-context registrations first then
519/// the global module registry.
520///
521/// # UPSTREAM-PARITY
522///
523/// ```c
524/// xsltTransformFunction xsltExtElementLookup(xsltTransformContextPtr ctxt,
525///                                            const xmlChar *name,
526///                                            const xmlChar *URI);
527/// ```
528#[no_mangle]
529pub unsafe extern "C" fn xsltExtElementLookup(
530    ctxt: *mut _xsltTransformContext,
531    name: *const xmlChar,
532    URI: *const xmlChar,
533) -> *mut c_void {
534    if ctxt.is_null() || name.is_null() || URI.is_null() {
535        return ptr::null_mut();
536    }
537    // Per-context registrations (xsltRegisterExtElement).
538    let found = crate::xslt::extensions::xsltFindExtElement(ctxt, name, URI);
539    if !found.is_null() {
540        return found;
541    }
542    let Some(key) = ext_key(name, URI) else {
543        return ptr::null_mut();
544    };
545    EXT_ELEMENTS
546        .read()
547        .get(&key)
548        .map(|e| e.transform as *mut c_void)
549        .unwrap_or(ptr::null_mut())
550}
551
552/// `xsltExtModuleElementLookup` (extensions.c): global element lookup.
553///
554/// # UPSTREAM-PARITY
555///
556/// ```c
557/// xsltTransformFunction xsltExtModuleElementLookup(const xmlChar *name,
558///                                                  const xmlChar *URI);
559/// ```
560#[no_mangle]
561pub unsafe extern "C" fn xsltExtModuleElementLookup(
562    name: *const xmlChar,
563    URI: *const xmlChar,
564) -> *mut c_void {
565    let Some(key) = ext_key(name, URI) else {
566        return ptr::null_mut();
567    };
568    EXT_ELEMENTS
569        .read()
570        .get(&key)
571        .map(|e| e.transform as *mut c_void)
572        .unwrap_or(ptr::null_mut())
573}
574
575/// `xsltExtModuleFunctionLookup` (extensions.c): global function lookup.
576///
577/// # UPSTREAM-PARITY
578///
579/// ```c
580/// xmlXPathFunction xsltExtModuleFunctionLookup(const xmlChar *name,
581///                                              const xmlChar *URI);
582/// ```
583#[no_mangle]
584pub unsafe extern "C" fn xsltExtModuleFunctionLookup(
585    name: *const xmlChar,
586    URI: *const xmlChar,
587) -> *mut c_void {
588    let Some(key) = ext_key(name, URI) else {
589        return ptr::null_mut();
590    };
591    EXT_FUNCTIONS
592        .read()
593        .get(&key)
594        .copied()
595        .map(|p| p as *mut c_void)
596        .unwrap_or(ptr::null_mut())
597}
598
599/// `xsltExtModuleElementPreComputeLookup` (extensions.c).
600///
601/// # UPSTREAM-PARITY
602///
603/// ```c
604/// xsltPreComputeFunction xsltExtModuleElementPreComputeLookup(
605///     const xmlChar *name, const xmlChar *URI);
606/// ```
607#[no_mangle]
608pub unsafe extern "C" fn xsltExtModuleElementPreComputeLookup(
609    name: *const xmlChar,
610    URI: *const xmlChar,
611) -> *mut c_void {
612    let Some(key) = ext_key(name, URI) else {
613        return ptr::null_mut();
614    };
615    EXT_ELEMENTS
616        .read()
617        .get(&key)
618        .map(|e| e.precomp as *mut c_void)
619        .unwrap_or(ptr::null_mut())
620}
621
622/// `xsltExtModuleTopLevelLookup` (extensions.c).
623///
624/// # UPSTREAM-PARITY
625///
626/// ```c
627/// xsltTopLevelFunction xsltExtModuleTopLevelLookup(const xmlChar *name,
628///                                                  const xmlChar *URI);
629/// ```
630#[no_mangle]
631pub unsafe extern "C" fn xsltExtModuleTopLevelLookup(
632    name: *const xmlChar,
633    URI: *const xmlChar,
634) -> *mut c_void {
635    let Some(key) = ext_key(name, URI) else {
636        return ptr::null_mut();
637    };
638    EXT_TOPLEVELS
639        .read()
640        .get(&key)
641        .copied()
642        .map(|p| p as *mut c_void)
643        .unwrap_or(ptr::null_mut())
644}
645
646/// `xsltInitCtxtExts` (extensions.c): call the init function of every module
647/// whose URI the stylesheet uses (registered extension prefixes).
648///
649/// # UPSTREAM-PARITY
650///
651/// ```c
652/// int xsltInitCtxtExts(xsltTransformContextPtr ctxt);
653/// ```
654///
655/// Returns 0 on success, -1 on error.
656#[no_mangle]
657pub unsafe extern "C" fn xsltInitCtxtExts(ctxt: *mut _xsltTransformContext) -> c_int {
658    if ctxt.is_null() || (*ctxt).style.is_null() {
659        return 0;
660    }
661    let style = (*ctxt).style;
662    let mut cur = (*style).extInfos as *mut ExtPrefixEntry;
663    while !cur.is_null() {
664        if !(*cur).uri.is_null() {
665            let key = CStr::from_ptr((*cur).uri as *const c_char)
666                .to_bytes()
667                .to_vec();
668            if let Some(module) = EXT_MODULES.read().get(&key).copied() {
669                if let Some(init) = module.init_func {
670                    let data = init(ctxt, (*cur).uri as *const xmlChar);
671                    if data.is_null() {
672                        return -1;
673                    }
674                    // Record (URI -> data) in the context's extInfos list.
675                    let entry = xmlMallocImpl(size_of::<ExtDataEntry>()) as *mut ExtDataEntry;
676                    if entry.is_null() {
677                        return -1;
678                    }
679                    let u = crate::abi::allocator::xmlMemStrdupImpl((*cur).uri as *const c_char)
680                        as *mut c_char;
681                    if u.is_null() {
682                        xmlFreeImpl(entry as *mut c_void);
683                        return -1;
684                    }
685                    ptr::write(
686                        entry,
687                        ExtDataEntry {
688                            next: (*ctxt).extInfos as *mut ExtDataEntry,
689                            uri: u,
690                            data,
691                        },
692                    );
693                    (*ctxt).extInfos = entry as *mut c_void;
694                }
695            }
696        }
697        cur = (*cur).next;
698    }
699    0
700}
701
702/// `xsltShutdownCtxtExts` (extensions.c): call the shutdown function of
703/// every initialised module on the context.
704///
705/// # UPSTREAM-PARITY
706///
707/// ```c
708/// void xsltShutdownCtxtExts(xsltTransformContextPtr ctxt);
709/// ```
710#[no_mangle]
711pub unsafe extern "C" fn xsltShutdownCtxtExts(ctxt: *mut _xsltTransformContext) {
712    if ctxt.is_null() {
713        return;
714    }
715    let mut cur = (*ctxt).extInfos as *mut ExtDataEntry;
716    while !cur.is_null() {
717        if !(*cur).uri.is_null() {
718            let key = CStr::from_ptr((*cur).uri as *const c_char)
719                .to_bytes()
720                .to_vec();
721            if let Some(module) = EXT_MODULES.read().get(&key).copied() {
722                if let Some(shutdown) = module.shutdown_func {
723                    shutdown(ctxt, (*cur).uri as *const xmlChar, (*cur).data);
724                }
725            }
726        }
727        cur = (*cur).next;
728    }
729}
730
731/// `xsltFreeCtxtExts` (extensions.c): free the context's extension data.
732///
733/// # UPSTREAM-PARITY
734///
735/// ```c
736/// void xsltFreeCtxtExts(xsltTransformContextPtr ctxt);
737/// ```
738#[no_mangle]
739pub unsafe extern "C" fn xsltFreeCtxtExts(ctxt: *mut _xsltTransformContext) {
740    if ctxt.is_null() {
741        return;
742    }
743    let mut cur = (*ctxt).extInfos as *mut ExtDataEntry;
744    (*ctxt).extInfos = ptr::null_mut();
745    while !cur.is_null() {
746        let next = (*cur).next;
747        if !(*cur).uri.is_null() {
748            xmlFreeImpl((*cur).uri as *mut c_void);
749        }
750        xmlFreeImpl(cur as *mut c_void);
751        cur = next;
752    }
753}
754
755/// `xsltGetExtData` (extensions.c): the per-context data of a module.
756///
757/// # UPSTREAM-PARITY
758///
759/// ```c
760/// void *xsltGetExtData(xsltTransformContextPtr ctxt, const xmlChar *URI);
761/// ```
762#[no_mangle]
763pub unsafe extern "C" fn xsltGetExtData(
764    ctxt: *mut _xsltTransformContext,
765    URI: *const xmlChar,
766) -> *mut c_void {
767    if ctxt.is_null() || URI.is_null() {
768        return ptr::null_mut();
769    }
770    let mut cur = (*ctxt).extInfos as *mut ExtDataEntry;
771    while !cur.is_null() {
772        if !(*cur).uri.is_null()
773            && libc::strcmp((*cur).uri as *const c_char, URI as *const c_char) == 0
774        {
775            return (*cur).data;
776        }
777        cur = (*cur).next;
778    }
779    ptr::null_mut()
780}
781
782/// `xsltStyleGetExtData` (extensions.c): the per-stylesheet data of a
783/// module, initialising it on first use via the style init hook.
784///
785/// # UPSTREAM-PARITY
786///
787/// ```c
788/// void *xsltStyleGetExtData(xsltStylesheetPtr style, const xmlChar *URI);
789/// ```
790#[no_mangle]
791pub unsafe extern "C" fn xsltStyleGetExtData(
792    style: *mut _xsltStylesheet,
793    URI: *const xmlChar,
794) -> *mut c_void {
795    if style.is_null() || URI.is_null() {
796        return ptr::null_mut();
797    }
798    let mut cur = (*style).extInfos as *mut ExtDataEntry;
799    while !cur.is_null() {
800        if !(*cur).uri.is_null()
801            && libc::strcmp((*cur).uri as *const c_char, URI as *const c_char) == 0
802        {
803            return (*cur).data;
804        }
805        cur = (*cur).next;
806    }
807    let key = CStr::from_ptr(URI as *const c_char).to_bytes().to_vec();
808    let module = EXT_MODULES.read().get(&key).copied();
809    let data = match module {
810        Some(m) => match m.style_init_func {
811            Some(init) => init(style, URI),
812            None => ptr::null_mut(),
813        },
814        None => ptr::null_mut(),
815    };
816    let entry = xmlMallocImpl(size_of::<ExtDataEntry>()) as *mut ExtDataEntry;
817    if entry.is_null() {
818        return ptr::null_mut();
819    }
820    let u = crate::abi::allocator::xmlMemStrdupImpl(URI as *const c_char) as *mut c_char;
821    if u.is_null() {
822        xmlFreeImpl(entry as *mut c_void);
823        return ptr::null_mut();
824    }
825    ptr::write(
826        entry,
827        ExtDataEntry {
828            next: (*style).extInfos as *mut ExtDataEntry,
829            uri: u,
830            data,
831        },
832    );
833    (*style).extInfos = entry as *mut c_void;
834    data
835}
836
837/// `xsltStyleStylesheetLevelGetExtData` (extensions.c): the stylesheet-level
838/// extension data of a module — the same lookup/init-on-first-use logic as
839/// `xsltStyleGetExtData` (which upstream defines as a thin wrapper of this
840/// function).
841///
842/// # UPSTREAM-PARITY
843///
844/// ```c
845/// void *xsltStyleStylesheetLevelGetExtData(xsltStylesheetPtr style,
846///                                          const xmlChar *URI);
847/// ```
848#[no_mangle]
849pub unsafe extern "C" fn xsltStyleStylesheetLevelGetExtData(
850    style: *mut _xsltStylesheet,
851    URI: *const xmlChar,
852) -> *mut c_void {
853    unsafe { xsltStyleGetExtData(style, URI) }
854}
855
856/// `xsltGetExtInfo` (extensions.c): the stylesheet's extension-data list
857/// head (upstream returns the `style->extInfos` hash pointer).
858///
859/// # UPSTREAM-PARITY
860///
861/// ```c
862/// xmlHashTablePtr xsltGetExtInfo(xsltStylesheetPtr style, const xmlChar *URI);
863/// ```
864#[no_mangle]
865pub unsafe extern "C" fn xsltGetExtInfo(
866    style: *mut _xsltStylesheet,
867    _URI: *const xmlChar,
868) -> *mut c_void {
869    if style.is_null() {
870        return ptr::null_mut();
871    }
872    (*style).extInfos
873}
874
875/// `xsltRegisterAllExtras` (extra.c): register the EXSLT "extra" extension
876/// elements (exsl:document) into the global module registry.
877///
878/// # UPSTREAM-PARITY
879///
880/// ```c
881/// void xsltRegisterAllExtras(void);
882/// ```
883#[no_mangle]
884pub unsafe extern "C" fn xsltRegisterAllExtras() {
885    // exsl:document — handled natively by the transform engine
886    // (process_exsl_document); registering the module URI makes
887    // xsltCheckExtURI agree with upstream.
888    xsltRegisterExtModule(
889        c"http://exslt.org/common".as_ptr() as *const xmlChar,
890        None,
891        None,
892    );
893}
894
895/// `xsltRegisterExtras` (extra.c): register the EXSLT functions into the
896/// context's XPath context (upstream calls xsltRegisterAllFunctions).
897///
898/// # UPSTREAM-PARITY
899///
900/// ```c
901/// void xsltRegisterExtras(xsltTransformContextPtr ctxt);
902/// ```
903#[no_mangle]
904pub unsafe extern "C" fn xsltRegisterExtras(ctxt: *mut _xsltTransformContext) {
905    if ctxt.is_null() || (*ctxt).xpathCtxt.is_null() {
906        return;
907    }
908    crate::abi::exports_xslt_functions::xsltRegisterAllFunctions((*ctxt).xpathCtxt);
909}
910
911/// `xsltRegisterAllElement` (extra.c): register the EXSLT elements into the
912/// transform context.
913///
914/// # UPSTREAM-PARITY
915///
916/// ```c
917/// void xsltRegisterAllElement(xsltTransformContextPtr ctxt);
918/// ```
919#[no_mangle]
920pub const unsafe extern "C" fn xsltRegisterAllElement(ctxt: *mut _xsltTransformContext) {
921    if ctxt.is_null() {}
922    // The engine dispatches EXSLT elements natively (process_exsl_document
923    // and the exslt module registrations); nothing to add to the context's
924    // per-context registration lists.
925}
926
927/// `xsltRegisterTestModule` (extensions.c): register the libxslt self-test
928/// extension module (a no-op surface in the candidate).
929///
930/// # UPSTREAM-PARITY
931///
932/// ```c
933/// void xsltRegisterTestModule(void);
934/// ```
935#[no_mangle]
936pub unsafe extern "C" fn xsltRegisterTestModule() {
937    xsltRegisterExtModule(
938        c"http://xmlsoft.org/XSLT/".as_ptr() as *const xmlChar,
939        None,
940        None,
941    );
942}
943
944// ── Internal helper structures (not part of the ABI) ───────────────────────
945
946/// Stylesheet extension-prefix registration (upstream style->extInfos hash
947/// entries; the candidate uses a linked list).
948#[derive(Debug)]
949#[repr(C)]
950pub struct ExtPrefixEntry {
951    /// Next entry in the linked list.
952    pub next: *mut ExtPrefixEntry,
953    /// The extension prefix mapped to `uri`.
954    pub prefix: *mut c_char,
955    /// The namespace URI the prefix is registered for.
956    pub uri: *mut c_char,
957}
958
959/// Per-context / per-style extension data record (URI -> init data).
960#[derive(Debug)]
961#[repr(C)]
962pub struct ExtDataEntry {
963    /// Next entry in the linked list.
964    pub next: *mut ExtDataEntry,
965    /// The namespace URI of the extension module.
966    pub uri: *mut c_char,
967    /// Module-specific initialization data.
968    pub data: *mut c_void,
969}