Skip to main content

libxml_rs/xslt/documents/
mod.rs

1//! XSLT document() function support (§33, §85 Phase 8).
2//!
3//! The `document()` function loads external XML documents during a
4//! transformation. Documents are cached per transform context so repeated
5//! loads of the same URI reuse the same document.
6//!
7//! # UPSTREAM-PARITY
8//!
9//! Upstream libxslt (documents.c) maintains a document cache on the
10//! transform context (`docCache` hash). `xsltLoadDocument` resolves the
11//! URI relative to the source document's base, loads it through the
12//! configured loader (default: file/network), and caches the result.
13//!
14//! The loader function can be overridden with `xsltSetLoaderFunc`.
15
16use crate::abi::structs::*;
17use crate::abi::types::*;
18use std::os::raw::c_int;
19use std::ptr;
20
21/// The XSLT document cache entry.
22#[repr(C)]
23pub struct _xsltDocCacheEntry {
24    pub next: *mut _xsltDocCacheEntry,
25    pub uri: *mut xmlChar,
26    pub doc: *mut _xmlDoc,
27}
28
29/// Register a result-tree-fragment (RVT) document in the context's document
30/// cache so it is freed exactly once at transform-context teardown
31/// (`xsltFreeDocCache` releases every cached doc).
32///
33/// The entry carries a NULL URI, so `cache_lookup` (which requires a
34/// non-NULL, matching URI) never matches it.
35///
36/// # UPSTREAM-PARITY
37///
38/// Upstream libxslt tracks RVT documents on the context (variables.c
39/// `xsltCreateRVT`) and frees them with the context; we reuse the docCache
40/// list for the same lifecycle.
41///
42/// # SAFETY
43///
44/// - `ctxt` must be a valid `_xsltTransformContext`.
45/// - Ownership of `doc` transfers to the cache (freed at teardown).
46pub(crate) unsafe fn xsltRegisterRVT(ctxt: *mut _xsltTransformContext, doc: *mut _xmlDoc) {
47    if ctxt.is_null() || doc.is_null() {
48        return;
49    }
50    let entry =
51        libc::calloc(1, core::mem::size_of::<_xsltDocCacheEntry>()) as *mut _xsltDocCacheEntry;
52    if entry.is_null() {
53        return;
54    }
55    (*entry).uri = ptr::null_mut();
56    (*entry).doc = doc;
57    (*entry).next = (*ctxt).docCache as *mut _xsltDocCacheEntry;
58    (*ctxt).docCache = entry as *mut c_void;
59}
60
61/// Default XSLT loader function (file loading).
62///
63/// # SAFETY
64///
65/// - `ctxt` may be NULL.
66/// - `URI` must be a valid NUL-terminated string.
67pub unsafe extern "C" fn xsltDefaultLoader(
68    _ctxt: *mut c_void,
69    _style: *const c_char,
70    URI: *const c_char,
71    _ns: *const c_char,
72    _secondary: c_int,
73) -> *mut _xmlParserInput {
74    if URI.is_null() {
75        return ptr::null_mut();
76    }
77    // Phase 8: load the URI via the XML parser I/O layer.
78    ptr::null_mut()
79}
80
81/// Global loader function (set via xsltSetLoaderFunc).
82static mut XSLT_LOADER: Option<
83    unsafe extern "C" fn(
84        *mut c_void,
85        *const c_char,
86        *const c_char,
87        *const c_char,
88        c_int,
89    ) -> *mut _xmlParserInput,
90> = None;
91
92/// Set the global XSLT loader function.
93///
94/// # SAFETY
95///
96/// - `loader` must be a valid function pointer or NULL.
97#[no_mangle]
98pub unsafe extern "C" fn xsltSetLoaderFunc(
99    loader: Option<
100        unsafe extern "C" fn(
101            *mut c_void,
102            *const c_char,
103            *const c_char,
104            *const c_char,
105            c_int,
106        ) -> *mut _xmlParserInput,
107    >,
108) {
109    XSLT_LOADER = loader;
110}
111
112/// Get the current global loader function.
113pub fn xsltGetLoaderFunc() -> Option<
114    unsafe extern "C" fn(
115        *mut c_void,
116        *const c_char,
117        *const c_char,
118        *const c_char,
119        c_int,
120    ) -> *mut _xmlParserInput,
121> {
122    // SAFETY: only mutated by xsltSetLoaderFunc; safe to read here.
123    unsafe { XSLT_LOADER }
124}
125
126/// Load a document by URI, using the cache.
127///
128/// Returns the loaded document (owned by the cache) or NULL on error.
129///
130/// # SAFETY
131///
132/// - `ctxt` must be a valid `_xsltTransformContext`.
133/// - `URI` must be a valid NUL-terminated string.
134pub unsafe fn xsltLoadDocument(
135    ctxt: *mut _xsltTransformContext,
136    URI: *const xmlChar,
137) -> *mut _xmlDoc {
138    if ctxt.is_null() || URI.is_null() {
139        return ptr::null_mut();
140    }
141    // Check the cache first.
142    if let Some(cached) = cache_lookup(ctxt, URI) {
143        return cached;
144    }
145    // Resolve the URI against the source document's base.
146    let base = if !(*ctxt).document.is_null() && !(*(*ctxt).document).URL.is_null() {
147        let len = libc::strlen((*(*ctxt).document).URL as *const libc::c_char) as usize;
148        Some(core::slice::from_raw_parts((*(*ctxt).document).URL, len))
149    } else {
150        None
151    };
152    let uri_bytes =
153        core::slice::from_raw_parts(URI, libc::strlen(URI as *const libc::c_char) as usize);
154    let resolved: Option<Vec<u8>> = match base {
155        Some(b) => crate::xml::uri::resolve_uri(b, uri_bytes),
156        None => Some(uri_bytes.to_vec()),
157    };
158    let resolved = match resolved {
159        Some(r) if !r.is_empty() => r,
160        _ => return ptr::null_mut(),
161    };
162    // Load via the configured loader or the default file loader.
163    let mut cstr = resolved.clone();
164    cstr.push(0);
165    let doc = load_via_loader(ctxt, cstr.as_ptr() as *mut xmlChar);
166    if !doc.is_null() {
167        cache_store(ctxt, URI, doc);
168    }
169    doc
170}
171
172/// Load a document via the configured loader.
173///
174/// Uses the global loader function if set; otherwise parses the URI as a
175/// local file. Returns a parsed document or NULL on failure.
176///
177/// # SAFETY
178///
179/// - All pointers must be valid.
180unsafe fn load_via_loader(ctxt: *mut _xsltTransformContext, uri: *mut xmlChar) -> *mut _xmlDoc {
181    // If a custom loader is configured, invoke it.
182    let loader = xsltGetLoaderFunc();
183    if let Some(loader_fn) = loader {
184        let input = loader_fn(
185            ctxt as *mut c_void,
186            ptr::null(), // style
187            uri as *const c_char,
188            ptr::null(), // ns
189            0,           // secondary
190        );
191        if !input.is_null() {
192            // The loader returned a parser input; we currently cannot feed
193            // it into a document without a parser context, so free it.
194            crate::xml::parser::helpers::free_parser_input(input);
195        }
196    }
197    // Default: parse the URI as a file path.
198    crate::abi::exports_xml2::xmlReadFile(uri as *const c_char, ptr::null(), 0)
199}
200
201/// Look up a document in the context's cache.
202///
203/// # SAFETY
204///
205/// - All pointers must be valid.
206unsafe fn cache_lookup(
207    ctxt: *mut _xsltTransformContext,
208    uri: *const xmlChar,
209) -> Option<*mut _xmlDoc> {
210    let mut cur = (*ctxt).docCache as *mut _xsltDocCacheEntry;
211    while !cur.is_null() {
212        if !(*cur).uri.is_null()
213            && libc::strcmp(
214                (*cur).uri as *const libc::c_char,
215                uri as *const libc::c_char,
216            ) == 0
217        {
218            return Some((*cur).doc);
219        }
220        cur = (*cur).next;
221    }
222    None
223}
224
225/// Store a document in the context's cache.
226///
227/// # SAFETY
228///
229/// - All pointers must be valid.
230unsafe fn cache_store(ctxt: *mut _xsltTransformContext, uri: *const xmlChar, doc: *mut _xmlDoc) {
231    let entry =
232        libc::calloc(1, core::mem::size_of::<_xsltDocCacheEntry>()) as *mut _xsltDocCacheEntry;
233    if entry.is_null() {
234        return;
235    }
236    let len = libc::strlen(uri as *const libc::c_char);
237    let copy = libc::malloc(len + 1) as *mut xmlChar;
238    if copy.is_null() {
239        libc::free(entry as *mut libc::c_void);
240        return;
241    }
242    libc::memcpy(copy as *mut libc::c_void, uri as *const libc::c_void, len);
243    *copy.add(len) = 0;
244    (*entry).uri = copy;
245    (*entry).doc = doc;
246    (*entry).next = (*ctxt).docCache as *mut _xsltDocCacheEntry;
247    (*ctxt).docCache = entry as *mut c_void;
248}
249
250/// Free the document cache of a transform context.
251///
252/// # SAFETY
253///
254/// - `ctxt` must be a valid `_xsltTransformContext`.
255pub unsafe fn xsltFreeDocCache(ctxt: *mut _xsltTransformContext) {
256    if ctxt.is_null() {
257        return;
258    }
259    let mut cur = (*ctxt).docCache as *mut _xsltDocCacheEntry;
260    (*ctxt).docCache = ptr::null_mut();
261    while !cur.is_null() {
262        let next = (*cur).next;
263        if !(*cur).uri.is_null() {
264            libc::free((*cur).uri as *mut libc::c_void);
265        }
266        if !(*cur).doc.is_null() {
267            crate::xml::tree::free_doc((*cur).doc);
268        }
269        libc::free(cur as *mut libc::c_void);
270        cur = next;
271    }
272}
273
274use std::ffi::c_void;
275use std::os::raw::c_char;
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use core::ptr;
281
282    #[test]
283    fn test_null_args() {
284        unsafe {
285            assert!(xsltLoadDocument(ptr::null_mut(), ptr::null()).is_null());
286            xsltFreeDocCache(ptr::null_mut());
287        }
288    }
289}