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    // UPSTREAM-PARITY: the candidate's RVT/cached-doc list head lives in the
58    // `cache` slot (xsltTransformCachePtr upstream; unused by the candidate
59    // for that purpose — documented divergence).
60    (*entry).next = (*ctxt).cache as *mut _xsltDocCacheEntry;
61    (*ctxt).cache = entry as *mut c_void;
62}
63
64/// Default XSLT loader function (file loading).
65///
66/// # SAFETY
67///
68/// - `ctxt` may be NULL.
69/// - `URI` must be a valid NUL-terminated string.
70pub unsafe extern "C" fn xsltDefaultLoader(
71    _ctxt: *mut c_void,
72    _style: *const c_char,
73    URI: *const c_char,
74    _ns: *const c_char,
75    _secondary: c_int,
76) -> *mut _xmlParserInput {
77    if URI.is_null() {
78        return ptr::null_mut();
79    }
80    // Phase 8: load the URI via the XML parser I/O layer.
81    ptr::null_mut()
82}
83
84/// Global loader function (set via xsltSetLoaderFunc).
85static mut XSLT_LOADER: Option<
86    unsafe extern "C" fn(
87        *mut c_void,
88        *const c_char,
89        *const c_char,
90        *const c_char,
91        c_int,
92    ) -> *mut _xmlParserInput,
93> = None;
94
95/// Set the global XSLT loader function.
96///
97/// # SAFETY
98///
99/// - `loader` must be a valid function pointer or NULL.
100#[no_mangle]
101pub unsafe extern "C" fn xsltSetLoaderFunc(
102    loader: Option<
103        unsafe extern "C" fn(
104            *mut c_void,
105            *const c_char,
106            *const c_char,
107            *const c_char,
108            c_int,
109        ) -> *mut _xmlParserInput,
110    >,
111) {
112    XSLT_LOADER = loader;
113}
114
115/// Get the current global loader function.
116pub fn xsltGetLoaderFunc() -> Option<
117    unsafe extern "C" fn(
118        *mut c_void,
119        *const c_char,
120        *const c_char,
121        *const c_char,
122        c_int,
123    ) -> *mut _xmlParserInput,
124> {
125    // SAFETY: only mutated by xsltSetLoaderFunc; safe to read here.
126    unsafe { XSLT_LOADER }
127}
128
129/// Load a document by URI, using the cache.
130///
131/// Returns the loaded document (owned by the cache) or NULL on error.
132///
133/// # SAFETY
134///
135/// - `ctxt` must be a valid `_xsltTransformContext`.
136/// - `URI` must be a valid NUL-terminated string.
137pub unsafe fn xsltLoadDocument(
138    ctxt: *mut _xsltTransformContext,
139    URI: *const xmlChar,
140) -> *mut _xmlDoc {
141    if ctxt.is_null() || URI.is_null() {
142        return ptr::null_mut();
143    }
144    // Check the cache first.
145    if let Some(cached) = cache_lookup(ctxt, URI) {
146        return cached;
147    }
148    // Resolve the URI against the source document's base.
149    let base = if !(*ctxt).document.is_null()
150        && !(*(*ctxt).document).doc.is_null()
151        && !(*(*(*ctxt).document).doc).URL.is_null()
152    {
153        let len = libc::strlen((*(*(*ctxt).document).doc).URL as *const libc::c_char) as usize;
154        Some(core::slice::from_raw_parts(
155            (*(*(*ctxt).document).doc).URL,
156            len,
157        ))
158    } else {
159        None
160    };
161    let uri_bytes =
162        core::slice::from_raw_parts(URI, libc::strlen(URI as *const libc::c_char) as usize);
163    let resolved: Option<Vec<u8>> = match base {
164        Some(b) => crate::xml::uri::resolve_uri(b, uri_bytes),
165        None => Some(uri_bytes.to_vec()),
166    };
167    let resolved = match resolved {
168        Some(r) if !r.is_empty() => r,
169        _ => return ptr::null_mut(),
170    };
171    // Load via the configured loader or the default file loader.
172    let mut cstr = resolved.clone();
173    cstr.push(0);
174    let doc = load_via_loader(ctxt, cstr.as_ptr() as *mut xmlChar);
175    if !doc.is_null() {
176        cache_store(ctxt, URI, doc);
177    }
178    doc
179}
180
181/// Load a document via the configured loader.
182///
183/// Uses the global loader function if set; otherwise parses the URI as a
184/// local file. Returns a parsed document or NULL on failure.
185///
186/// # SAFETY
187///
188/// - All pointers must be valid.
189unsafe fn load_via_loader(ctxt: *mut _xsltTransformContext, uri: *mut xmlChar) -> *mut _xmlDoc {
190    // If a custom loader is configured, invoke it.
191    let loader = xsltGetLoaderFunc();
192    if let Some(loader_fn) = loader {
193        let input = loader_fn(
194            ctxt as *mut c_void,
195            ptr::null(), // style
196            uri as *const c_char,
197            ptr::null(), // ns
198            0,           // secondary
199        );
200        if !input.is_null() {
201            // The loader returned a parser input; we currently cannot feed
202            // it into a document without a parser context, so free it.
203            crate::xml::parser::helpers::free_parser_input(input);
204        }
205    }
206    // Default: parse the URI as a file path.
207    crate::abi::exports_xml2::xmlReadFile(uri as *const c_char, ptr::null(), 0)
208}
209
210/// Look up a document in the context's cache.
211///
212/// # SAFETY
213///
214/// - All pointers must be valid.
215unsafe fn cache_lookup(
216    ctxt: *mut _xsltTransformContext,
217    uri: *const xmlChar,
218) -> Option<*mut _xmlDoc> {
219    let mut cur = (*ctxt).cache as *mut _xsltDocCacheEntry;
220    while !cur.is_null() {
221        if !(*cur).uri.is_null()
222            && libc::strcmp(
223                (*cur).uri as *const libc::c_char,
224                uri as *const libc::c_char,
225            ) == 0
226        {
227            return Some((*cur).doc);
228        }
229        cur = (*cur).next;
230    }
231    None
232}
233
234/// Store a document in the context's cache.
235///
236/// # SAFETY
237///
238/// - All pointers must be valid.
239unsafe fn cache_store(ctxt: *mut _xsltTransformContext, uri: *const xmlChar, doc: *mut _xmlDoc) {
240    let entry =
241        libc::calloc(1, core::mem::size_of::<_xsltDocCacheEntry>()) as *mut _xsltDocCacheEntry;
242    if entry.is_null() {
243        return;
244    }
245    let len = libc::strlen(uri as *const libc::c_char);
246    let copy = libc::malloc(len + 1) as *mut xmlChar;
247    if copy.is_null() {
248        libc::free(entry as *mut libc::c_void);
249        return;
250    }
251    libc::memcpy(copy as *mut libc::c_void, uri as *const libc::c_void, len);
252    *copy.add(len) = 0;
253    (*entry).uri = copy;
254    (*entry).doc = doc;
255    (*entry).next = (*ctxt).cache as *mut _xsltDocCacheEntry;
256    (*ctxt).cache = entry as *mut c_void;
257}
258
259/// Free the document cache of a transform context.
260///
261/// # SAFETY
262///
263/// - `ctxt` must be a valid `_xsltTransformContext`.
264pub unsafe fn xsltFreeDocCache(ctxt: *mut _xsltTransformContext) {
265    if ctxt.is_null() {
266        return;
267    }
268    let mut cur = (*ctxt).cache as *mut _xsltDocCacheEntry;
269    (*ctxt).cache = ptr::null_mut();
270    while !cur.is_null() {
271        let next = (*cur).next;
272        if !(*cur).uri.is_null() {
273            libc::free((*cur).uri as *mut libc::c_void);
274        }
275        if !(*cur).doc.is_null() {
276            crate::xml::tree::free_doc((*cur).doc);
277        }
278        libc::free(cur as *mut libc::c_void);
279        cur = next;
280    }
281}
282
283use std::ffi::c_void;
284use std::os::raw::c_char;
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use core::ptr;
290
291    #[test]
292    fn test_null_args() {
293        unsafe {
294            assert!(xsltLoadDocument(ptr::null_mut(), ptr::null()).is_null());
295            xsltFreeDocCache(ptr::null_mut());
296        }
297    }
298}